feat(templates): add the ability to update the volume configuration (#590)

pull/595/head
Anthony Lapenna 2017-02-13 18:16:14 +13:00 committed by GitHub
parent c5552d1b8e
commit 781dad3e17
7 changed files with 188 additions and 47 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",