mirror of https://github.com/portainer/portainer
feat(templates): add the ability to update the volume configuration (#590)
parent
c5552d1b8e
commit
781dad3e17
|
@ -27,12 +27,12 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- name-and-network-inputs -->
|
<!-- name-and-network-inputs -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-5">
|
||||||
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
||||||
</div>
|
</div>
|
||||||
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
<label for="container_network" class="col-sm-1 control-label text-right">Network</label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-5">
|
||||||
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
||||||
<option disabled hidden value="">Select a network</option>
|
<option disabled hidden value="">Select a network</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -61,40 +61,125 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="state.showAdvancedOptions">
|
<!-- advanced-options -->
|
||||||
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
<div ng-if="state.showAdvancedOptions">
|
||||||
<div class="col-sm-11" style="margin-top: 5px;">
|
<!-- port-mapping -->
|
||||||
<span class="label label-default interactive" ng-click="addPortBinding()">
|
<div class="form-group" >
|
||||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
|
<div class="col-sm-12" style="margin-top: 5px;">
|
||||||
</span>
|
<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-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-default" ng-model="portBinding.protocol" uib-btn-radio="'tcp'" ng-click="volume.name = ''">TCP</label>
|
||||||
|
<label class="btn btn-default" ng-model="portBinding.protocol" uib-btn-radio="'udp'" ng-click="volume.name = ''">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>
|
</div>
|
||||||
<!-- port-mapping-input-list -->
|
<!-- !port-mapping-input-list -->
|
||||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
<!-- volume-mapping -->
|
||||||
<div ng-repeat="portBinding in state.selectedTemplate.Ports" style="margin-top: 2px;">
|
<div class="form-group" >
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
<div class="col-sm-12" style="margin-top: 5px;">
|
||||||
<span class="input-group-addon">host</span>
|
<label class="control-label text-left">Volume mapping</label>
|
||||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
|
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addVolume()">
|
||||||
</div>
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
</span>
|
||||||
<span class="input-group-addon">container</span>
|
</div>
|
||||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
<div class="col-sm-12" style="margin-top: 10px" ng-if="state.selectedTemplate.Volumes.length > 0">
|
||||||
</div>
|
<span class="small text-muted">Portainer will automatically create and map a local volume when using the <b>auto</b> option.</span>
|
||||||
<div class="input-group col-sm-1 input-group-sm">
|
</div>
|
||||||
<select class="form-control" ng-model="portBinding.protocol">
|
<div ng-repeat="volume in state.selectedTemplate.Volumes">
|
||||||
<option value="tcp">tcp</option>
|
<div class="col-sm-12" style="margin-top: 10px;">
|
||||||
<option value="udp">udp</option>
|
<!-- volume-line1 -->
|
||||||
</select>
|
<div class="col-sm-12 form-inline">
|
||||||
<span class="input-group-btn">
|
<!-- container-path -->
|
||||||
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
|
<div class="input-group input-group-sm col-sm-6">
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
<span class="input-group-addon">container</span>
|
||||||
</button>
|
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container">
|
||||||
</span>
|
</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.name = ''">Auto</label>
|
||||||
|
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||||
|
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.name = ''">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-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.name">
|
||||||
|
<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.name" 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-default" ng-model="volume.readOnly" uib-btn-radio="false">Writable</label>
|
||||||
|
<label class="btn btn-default" ng-model="volume.readOnly" uib-btn-radio="true">Read-only</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !read-only -->
|
||||||
|
</div>
|
||||||
|
<!-- !volume-line2 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !port-mapping-input-list -->
|
<!-- !volume-mapping -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !port-mapping -->
|
<!-- !advanced-options -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
<button type="button" class="btn btn-default btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('templates', [])
|
angular.module('templates', [])
|
||||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination',
|
.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination',
|
||||||
function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination) {
|
function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
showAdvancedOptions: false,
|
showAdvancedOptions: false,
|
||||||
|
@ -15,6 +15,14 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageServ
|
||||||
Pagination.setPaginationCount('templates', $scope.state.pagination_count);
|
Pagination.setPaginationCount('templates', $scope.state.pagination_count);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.addVolume = function () {
|
||||||
|
$scope.state.selectedTemplate.Volumes.push({ containerPath: '', name: '', readOnly: false, type: 'auto' });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeVolume = function(index) {
|
||||||
|
$scope.state.selectedTemplate.Volumes.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.addPortBinding = function() {
|
$scope.addPortBinding = function() {
|
||||||
$scope.state.selectedTemplate.Ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
$scope.state.selectedTemplate.Ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
||||||
};
|
};
|
||||||
|
@ -27,8 +35,9 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageServ
|
||||||
$('#createContainerSpinner').show();
|
$('#createContainerSpinner').show();
|
||||||
var template = $scope.state.selectedTemplate;
|
var template = $scope.state.selectedTemplate;
|
||||||
var templateConfiguration = createTemplateConfiguration(template);
|
var templateConfiguration = createTemplateConfiguration(template);
|
||||||
|
var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
|
||||||
|
|
||||||
VolumeService.createAutoGeneratedLocalVolumes(template.Volumes)
|
VolumeService.createXAutoGeneratedLocalVolumes(generatedVolumeCount)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data);
|
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data);
|
||||||
return ImageService.pullImage(templateConfiguration.image);
|
return ImageService.pullImage(templateConfiguration.image);
|
||||||
|
@ -108,12 +117,14 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageServ
|
||||||
$q.all({
|
$q.all({
|
||||||
templates: TemplateService.getTemplates(),
|
templates: TemplateService.getTemplates(),
|
||||||
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
||||||
networks: NetworkService.getNetworks()
|
networks: NetworkService.getNetworks(),
|
||||||
|
volumes: VolumeService.getVolumes()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.templates = data.templates;
|
$scope.templates = data.templates;
|
||||||
$scope.runningContainers = data.containers;
|
$scope.runningContainers = data.containers;
|
||||||
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
||||||
|
$scope.availableVolumes = data.volumes.Volumes;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.templates = [];
|
$scope.templates = [];
|
||||||
|
|
|
@ -66,5 +66,32 @@ angular.module('portainer.helpers')
|
||||||
return env;
|
return env;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
helper.createVolumeBindings = function(volumes, generatedVolumesPile) {
|
||||||
|
volumes.forEach(function (volume) {
|
||||||
|
if (volume.containerPath) {
|
||||||
|
var binding;
|
||||||
|
if (volume.type === 'auto') {
|
||||||
|
binding = generatedVolumesPile.pop().Name + ':' + volume.containerPath;
|
||||||
|
} else if (volume.type !== 'auto' && volume.name) {
|
||||||
|
binding = volume.name + ':' + volume.containerPath;
|
||||||
|
}
|
||||||
|
if (volume.readOnly) {
|
||||||
|
binding += ':ro';
|
||||||
|
}
|
||||||
|
volume.binding = binding;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.determineRequiredGeneratedVolumeCount = function(volumes) {
|
||||||
|
var count = 0;
|
||||||
|
volumes.forEach(function (volume) {
|
||||||
|
if (volume.type === 'auto') {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
return helper;
|
return helper;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -7,7 +7,16 @@ function TemplateViewModel(data) {
|
||||||
this.Command = data.command ? data.command : '';
|
this.Command = data.command ? data.command : '';
|
||||||
this.Network = data.network ? data.network : '';
|
this.Network = data.network ? data.network : '';
|
||||||
this.Env = data.env ? data.env : [];
|
this.Env = data.env ? data.env : [];
|
||||||
this.Volumes = data.volumes ? data.volumes : [];
|
this.Volumes = [];
|
||||||
|
if (data.volumes) {
|
||||||
|
this.Volumes = data.volumes.map(function (v) {
|
||||||
|
return {
|
||||||
|
readOnly: false,
|
||||||
|
containerPath: v,
|
||||||
|
type: 'auto'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
this.Ports = [];
|
this.Ports = [];
|
||||||
if (data.ports) {
|
if (data.ports) {
|
||||||
this.Ports = data.ports.map(function (p) {
|
this.Ports = data.ports.map(function (p) {
|
||||||
|
|
|
@ -47,10 +47,14 @@ angular.module('portainer.services')
|
||||||
return configuration;
|
return configuration;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.updateContainerConfigurationWithVolumes = function(configuration, template, createdVolumes) {
|
service.updateContainerConfigurationWithVolumes = function(configuration, template, generatedVolumesPile) {
|
||||||
createdVolumes.forEach(function (volume, idx) {
|
var volumes = template.Volumes;
|
||||||
configuration.Volumes[template.Volumes[idx]] = {};
|
TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
|
||||||
configuration.HostConfig.Binds.push(volume.Name + ':' + template.Volumes[idx]);
|
volumes.forEach(function (volume) {
|
||||||
|
if (volume.binding) {
|
||||||
|
configuration.Volumes[volume.containerPath] = {};
|
||||||
|
configuration.HostConfig.Binds.push(volume.binding);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,10 @@ angular.module('portainer.services')
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
service.getVolumes = function() {
|
||||||
|
return Volume.query({}).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
function prepareVolumeQueries(template, containerConfig) {
|
function prepareVolumeQueries(template, containerConfig) {
|
||||||
var volumeQueries = [];
|
var volumeQueries = [];
|
||||||
if (template.volumes) {
|
if (template.volumes) {
|
||||||
|
@ -48,10 +52,11 @@ angular.module('portainer.services')
|
||||||
return $q.all(createVolumeQueries);
|
return $q.all(createVolumeQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
service.createAutoGeneratedLocalVolumes = function (volumes) {
|
service.createXAutoGeneratedLocalVolumes = function (x) {
|
||||||
var createVolumeQueries = volumes.map(function(volume) {
|
var createVolumeQueries = [];
|
||||||
return service.createVolume({});
|
for (var i = 0; i < x; i++) {
|
||||||
});
|
createVolumeQueries.push(service.createVolume({}));
|
||||||
|
}
|
||||||
return $q.all(createVolumeQueries);
|
return $q.all(createVolumeQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"Chart.js": "1.0.2",
|
"Chart.js": "1.0.2",
|
||||||
"angular": "~1.5.0",
|
"angular": "~1.5.0",
|
||||||
"angular-cookies": "~1.5.0",
|
"angular-cookies": "~1.5.0",
|
||||||
"angular-bootstrap": "~1.0.3",
|
"angular-bootstrap": "~2.5.0",
|
||||||
"angular-ui-router": "^0.2.15",
|
"angular-ui-router": "^0.2.15",
|
||||||
"angular-sanitize": "~1.5.0",
|
"angular-sanitize": "~1.5.0",
|
||||||
"angular-mocks": "~1.5.0",
|
"angular-mocks": "~1.5.0",
|
||||||
|
|
Loading…
Reference in New Issue