diff --git a/app/app.js b/app/app.js
index 16cb0bc9d..02754f6c8 100644
--- a/app/app.js
+++ b/app/app.js
@@ -65,7 +65,8 @@ angular.module('portainer', [
'users',
'userSettings',
'volume',
- 'volumes'])
+ 'volumes',
+ 'rzModule'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
'use strict';
diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js
index d7723503d..c521d0aae 100644
--- a/app/components/createService/createServiceController.js
+++ b/app/components/createService/createServiceController.js
@@ -1,8 +1,8 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference.
angular.module('createService', [])
-.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper',
-function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper) {
+.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService',
+function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService) {
$scope.formValues = {
Name: '',
@@ -28,13 +28,25 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
UpdateOrder: 'stop-first',
FailureAction: 'pause',
Secrets: [],
- AccessControlData: new AccessControlFormData()
+ AccessControlData: new AccessControlFormData(),
+ CpuLimit: 0,
+ CpuReservation: 0,
+ MemoryLimit: 0,
+ MemoryReservation: 0,
+ MemoryLimitUnit: 'MB',
+ MemoryReservationUnit: 'MB'
};
$scope.state = {
formValidationError: ''
};
+ $scope.refreshSlider = function () {
+ $timeout(function () {
+ $scope.$broadcast('rzSliderForceRender');
+ });
+ };
+
$scope.addPortBinding = function() {
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
};
@@ -224,6 +236,38 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
}
}
+ function prepareResourcesCpuConfig(config, input) {
+ // CPU Limit
+ if (input.CpuLimit > 0) {
+ config.TaskTemplate.Resources.Limits.NanoCPUs = input.CpuLimit * 1000000000;
+ }
+ // CPU Reservation
+ if (input.CpuReservation > 0) {
+ config.TaskTemplate.Resources.Reservations.NanoCPUs = input.CpuReservation * 1000000000;
+ }
+ }
+
+ function prepareResourcesMemoryConfig(config, input) {
+ // Memory Limit - Round to 0.125
+ var memoryLimit = (Math.round(input.MemoryLimit * 8) / 8).toFixed(3);
+ memoryLimit *= 1024 * 1024;
+ if (input.MemoryLimitUnit === 'GB') {
+ memoryLimit *= 1024;
+ }
+ if (memoryLimit > 0) {
+ config.TaskTemplate.Resources.Limits.MemoryBytes = memoryLimit;
+ }
+ // Memory Resevation - Round to 0.125
+ var memoryReservation = (Math.round(input.MemoryReservation * 8) / 8).toFixed(3);
+ memoryReservation *= 1024 * 1024;
+ if (input.MemoryReservationUnit === 'GB') {
+ memoryReservation *= 1024;
+ }
+ if (memoryReservation > 0) {
+ config.TaskTemplate.Resources.Reservations.MemoryBytes = memoryReservation;
+ }
+ }
+
function prepareConfiguration() {
var input = $scope.formValues;
var config = {
@@ -232,7 +276,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
ContainerSpec: {
Mounts: []
},
- Placement: {}
+ Placement: {},
+ Resources: {
+ Limits: {},
+ Reservations: {}
+ }
},
Mode: {},
EndpointSpec: {}
@@ -248,6 +296,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
prepareUpdateConfig(config, input);
prepareSecretConfig(config, input);
preparePlacementConfig(config, input);
+ prepareResourcesCpuConfig(config, input);
+ prepareResourcesMemoryConfig(config, input);
return config;
}
@@ -305,16 +355,30 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
function initView() {
$('#loadingViewSpinner').show();
var apiVersion = $scope.applicationState.endpoint.apiVersion;
+ var provider = $scope.applicationState.endpoint.mode.provider;
$q.all({
volumes: VolumeService.volumes(),
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
- networks: NetworkService.networks(true, true, false, false)
+ networks: NetworkService.networks(true, true, false, false),
+ nodes: NodeService.nodes()
})
.then(function success(data) {
$scope.availableVolumes = data.volumes;
$scope.availableNetworks = data.networks;
$scope.availableSecrets = data.secrets;
+ // Set max cpu value
+ var maxCpus = 0;
+ for (var n in data.nodes) {
+ if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
+ maxCpus = data.nodes[n].CPUs;
+ }
+ }
+ if (maxCpus > 0) {
+ $scope.state.sliderMaxCpu = maxCpus / 1000000000;
+ } else {
+ $scope.state.sliderMaxCpu = 32;
+ }
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to initialize view');
diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html
index d34ad9bd1..ad3c254ed 100644
--- a/app/components/createService/createservice.html
+++ b/app/components/createService/createservice.html
@@ -133,7 +133,7 @@
Labels
Update config
Secrets
- Placement
+ Resources & Placement
@@ -442,9 +442,9 @@
-
-
-
+
+
+
diff --git a/app/components/createService/includes/placement.html b/app/components/createService/includes/placement.html
deleted file mode 100644
index c12547fac..000000000
--- a/app/components/createService/includes/placement.html
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
diff --git a/app/components/createService/includes/resources-placement.html b/app/components/createService/includes/resources-placement.html
new file mode 100644
index 000000000..6c8b85dd6
--- /dev/null
+++ b/app/components/createService/includes/resources-placement.html
@@ -0,0 +1,136 @@
+
diff --git a/app/components/service/includes/resources.html b/app/components/service/includes/resources.html
index 12228cb2f..e24de75b5 100644
--- a/app/components/service/includes/resources.html
+++ b/app/components/service/includes/resources.html
@@ -6,31 +6,77 @@
- CPU limits |
-
- {{ service.LimitNanoCPUs / 1000000000 }}
+ |
+ Memory reservation (MB)
|
- None |
-
-
- Memory limits |
- {{service.LimitMemoryBytes|humansize}} |
- None |
-
-
- CPU reservation |
-
- {{service.ReservationNanoCPUs / 1000000000}}
+ |
+
+ |
+
+
+ Minimum memory available on a node to run a task (set to 0 for unlimited)
+
|
- None |
- Memory reservation |
- {{service.ReservationMemoryBytes|humansize}} |
- None |
+
+ Memory limit (MB)
+ |
+
+
+ |
+
+
+ Maximum memory usage per task (set to 0 for unlimited)
+
+ |
+
+
+
+
+ CPU reservation
+
+ |
+
+
+ |
+
+
+ Minimum CPU available on a node to run a task
+
+ |
+
+
+
+
+ CPU limit
+
+ |
+
+
+ |
+
+
+ Maximum CPU usage per task
+
+ |
+
+
+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js
index 3b5f8e2a1..775525aa1 100644
--- a/app/components/service/serviceController.js
+++ b/app/components/service/serviceController.js
@@ -204,14 +204,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences);
+ // Round memory values to 0.125 and convert MB to B
+ var memoryLimit = (Math.round(service.LimitMemoryBytes * 8) / 8).toFixed(3);
+ memoryLimit *= 1024 * 1024;
+ var memoryReservation = (Math.round(service.ReservationMemoryBytes * 8) / 8).toFixed(3);
+ memoryReservation *= 1024 * 1024;
config.TaskTemplate.Resources = {
Limits: {
- NanoCPUs: service.LimitNanoCPUs,
- MemoryBytes: service.LimitMemoryBytes
+ NanoCPUs: service.LimitNanoCPUs * 1000000000,
+ MemoryBytes: memoryLimit
},
Reservations: {
- NanoCPUs: service.ReservationNanoCPUs,
- MemoryBytes: service.ReservationMemoryBytes
+ NanoCPUs: service.ReservationNanoCPUs * 1000000000,
+ MemoryBytes: memoryReservation
}
};
@@ -244,7 +249,11 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadingViewSpinner').hide();
- Notifications.success('Service successfully updated', 'Service updated');
+ if (data.message && data.message.match(/^rpc error:/)) {
+ Notifications.error(data.message, 'Error');
+ } else {
+ Notifications.success('Service successfully updated', 'Service updated');
+ }
$scope.cancelChanges({});
initView();
}, function (e) {
@@ -288,6 +297,13 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
}
+ function transformResources(service) {
+ service.LimitNanoCPUs = service.LimitNanoCPUs / 1000000000 || 0;
+ service.ReservationNanoCPUs = service.ReservationNanoCPUs / 1000000000 || 0;
+ service.LimitMemoryBytes = service.LimitMemoryBytes / 1024 / 1024 || 0;
+ service.ReservationMemoryBytes = service.ReservationMemoryBytes / 1024 / 1024 || 0;
+ }
+
function initView() {
$('#loadingViewSpinner').show();
var apiVersion = $scope.applicationState.endpoint.apiVersion;
@@ -299,6 +315,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
$scope.lastVersion = service.Version;
}
+ transformResources(service);
translateServiceArrays(service);
$scope.service = service;
originalService = angular.copy(service);
@@ -314,6 +331,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
$scope.nodes = data.nodes;
$scope.secrets = data.secrets;
+ // Set max cpu value
+ var maxCpus = 0;
+ for (var n in data.nodes) {
+ if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
+ maxCpus = data.nodes[n].CPUs;
+ }
+ }
+ if (maxCpus > 0) {
+ $scope.state.sliderMaxCpu = maxCpus / 1000000000;
+ } else {
+ $scope.state.sliderMaxCpu = 32;
+ }
+
$timeout(function() {
$anchorScroll();
});
diff --git a/app/directives/slider/por-slider.js b/app/directives/slider/por-slider.js
new file mode 100644
index 000000000..a44104789
--- /dev/null
+++ b/app/directives/slider/por-slider.js
@@ -0,0 +1,12 @@
+angular.module('portainer').component('porSlider', {
+ templateUrl: 'app/directives/slider/porSlider.html',
+ controller: 'porSliderController',
+ bindings: {
+ model: '=',
+ onChange: '&',
+ floor: '<',
+ ceil: '<',
+ step: '<',
+ precision: '<'
+ }
+});
diff --git a/app/directives/slider/porSlider.html b/app/directives/slider/porSlider.html
new file mode 100644
index 000000000..94cb04a75
--- /dev/null
+++ b/app/directives/slider/porSlider.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/directives/slider/porSliderController.js b/app/directives/slider/porSliderController.js
new file mode 100644
index 000000000..4958800b2
--- /dev/null
+++ b/app/directives/slider/porSliderController.js
@@ -0,0 +1,22 @@
+angular.module('portainer')
+.controller('porSliderController', function () {
+ var ctrl = this;
+
+ ctrl.options = {
+ floor: ctrl.floor,
+ ceil: ctrl.ceil,
+ step: ctrl.step,
+ precision: ctrl.precision,
+ showSelectionBar: true,
+ translate: function(value, sliderId, label) {
+ if (label === 'floor' || value === 0) {
+ return 'unlimited';
+ }
+ return value;
+ },
+ onChange: function() {
+ ctrl.onChange();
+ }
+ };
+
+});
diff --git a/app/helpers/serviceHelper.js b/app/helpers/serviceHelper.js
index bddab33c6..ec3411421 100644
--- a/app/helpers/serviceHelper.js
+++ b/app/helpers/serviceHelper.js
@@ -119,6 +119,5 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
}
return [];
}
-
};
}]);
diff --git a/bower.json b/bower.json
index 3fb1da483..e1ea5474e 100644
--- a/bower.json
+++ b/bower.json
@@ -49,7 +49,8 @@
"angular-multi-select": "~4.0.0",
"toastr": "~2.1.3",
"xterm.js": "~2.8.1",
- "chart.js": "~2.6.0"
+ "chart.js": "~2.6.0",
+ "angularjs-slider": "^6.4.0"
},
"resolutions": {
"angular": "1.5.11"
diff --git a/vendor.yml b/vendor.yml
index f4846a4e0..2a8a5c52a 100644
--- a/vendor.yml
+++ b/vendor.yml
@@ -13,6 +13,7 @@ js:
- bower_components/toastr/toastr.js
- bower_components/xterm.js/dist/xterm.js
- bower_components/xterm.js/dist/addons/fit/fit.js
+ - bower_components/angularjs-slider/dist/rzslider.js
minified:
- bower_components/jquery/dist/jquery.min.js
- bower_components/bootstrap/dist/js/bootstrap.min.js
@@ -27,6 +28,7 @@ js:
- bower_components/toastr/toastr.min.js
- bower_components/xterm.js/dist/xterm.js
- bower_components/xterm.js/dist/addons/fit/fit.js
+ - bower_components/angularjs-slider/dist/rzslider.min.js
css:
regular:
- bower_components/bootstrap/dist/css/bootstrap.css
@@ -36,6 +38,7 @@ css:
- bower_components/font-awesome/css/font-awesome.css
- bower_components/toastr/toastr.css
- bower_components/xterm.js/dist/xterm.css
+ - bower_components/angularjs-slider/dist/rzslider.css
minified:
- bower_components/bootstrap/dist/css/bootstrap.min.css
- bower_components/rdash-ui/dist/css/rdash.min.css
@@ -44,6 +47,7 @@ css:
- bower_components/font-awesome/css/font-awesome.min.css
- bower_components/toastr/toastr.min.css
- bower_components/xterm.js/dist/xterm.css
+ - bower_components/angularjs-slider/rzslider.min.css
angular:
regular:
- bower_components/angular/angular.js