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>
|
||||
<!-- name-and-network-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-4">
|
||||
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-5">
|
||||
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
||||
</div>
|
||||
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
||||
<div class="col-sm-4">
|
||||
<label for="container_network" class="col-sm-1 control-label text-right">Network</label>
|
||||
<div class="col-sm-5">
|
||||
<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>
|
||||
|
@ -61,40 +61,125 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.showAdvancedOptions">
|
||||
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
||||
<div class="col-sm-11" style="margin-top: 5px;">
|
||||
<span class="label label-default interactive" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
|
||||
</span>
|
||||
<!-- advanced-options -->
|
||||
<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-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>
|
||||
<!-- port-mapping-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="portBinding in state.selectedTemplate.Ports" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 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>
|
||||
<div class="input-group col-sm-5 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>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select class="form-control" ng-model="portBinding.protocol">
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
<!-- !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.containerPath" 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.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>
|
||||
<!-- !port-mapping-input-list -->
|
||||
<!-- !volume-mapping -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- !advanced-options -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<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', [])
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination',
|
||||
function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination) {
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination',
|
||||
function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination) {
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
|
@ -15,6 +15,14 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageServ
|
|||
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.state.selectedTemplate.Ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
||||
};
|
||||
|
@ -27,8 +35,9 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageServ
|
|||
$('#createContainerSpinner').show();
|
||||
var template = $scope.state.selectedTemplate;
|
||||
var templateConfiguration = createTemplateConfiguration(template);
|
||||
var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
|
||||
|
||||
VolumeService.createAutoGeneratedLocalVolumes(template.Volumes)
|
||||
VolumeService.createXAutoGeneratedLocalVolumes(generatedVolumeCount)
|
||||
.then(function success(data) {
|
||||
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data);
|
||||
return ImageService.pullImage(templateConfiguration.image);
|
||||
|
@ -108,12 +117,14 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageServ
|
|||
$q.all({
|
||||
templates: TemplateService.getTemplates(),
|
||||
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
||||
networks: NetworkService.getNetworks()
|
||||
networks: NetworkService.getNetworks(),
|
||||
volumes: VolumeService.getVolumes()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.templates = data.templates;
|
||||
$scope.runningContainers = data.containers;
|
||||
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
||||
$scope.availableVolumes = data.volumes.Volumes;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.templates = [];
|
||||
|
|
|
@ -66,5 +66,32 @@ angular.module('portainer.helpers')
|
|||
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;
|
||||
}]);
|
||||
|
|
|
@ -7,7 +7,16 @@ function TemplateViewModel(data) {
|
|||
this.Command = data.command ? data.command : '';
|
||||
this.Network = data.network ? data.network : '';
|
||||
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 = [];
|
||||
if (data.ports) {
|
||||
this.Ports = data.ports.map(function (p) {
|
||||
|
|
|
@ -47,10 +47,14 @@ angular.module('portainer.services')
|
|||
return configuration;
|
||||
};
|
||||
|
||||
service.updateContainerConfigurationWithVolumes = function(configuration, template, createdVolumes) {
|
||||
createdVolumes.forEach(function (volume, idx) {
|
||||
configuration.Volumes[template.Volumes[idx]] = {};
|
||||
configuration.HostConfig.Binds.push(volume.Name + ':' + template.Volumes[idx]);
|
||||
service.updateContainerConfigurationWithVolumes = function(configuration, template, generatedVolumesPile) {
|
||||
var volumes = template.Volumes;
|
||||
TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
|
||||
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';
|
||||
var service = {};
|
||||
|
||||
service.getVolumes = function() {
|
||||
return Volume.query({}).$promise;
|
||||
};
|
||||
|
||||
function prepareVolumeQueries(template, containerConfig) {
|
||||
var volumeQueries = [];
|
||||
if (template.volumes) {
|
||||
|
@ -48,10 +52,11 @@ angular.module('portainer.services')
|
|||
return $q.all(createVolumeQueries);
|
||||
};
|
||||
|
||||
service.createAutoGeneratedLocalVolumes = function (volumes) {
|
||||
var createVolumeQueries = volumes.map(function(volume) {
|
||||
return service.createVolume({});
|
||||
});
|
||||
service.createXAutoGeneratedLocalVolumes = function (x) {
|
||||
var createVolumeQueries = [];
|
||||
for (var i = 0; i < x; i++) {
|
||||
createVolumeQueries.push(service.createVolume({}));
|
||||
}
|
||||
return $q.all(createVolumeQueries);
|
||||
};
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"Chart.js": "1.0.2",
|
||||
"angular": "~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-sanitize": "~1.5.0",
|
||||
"angular-mocks": "~1.5.0",
|
||||
|
|
Loading…
Reference in New Issue