From 3cb96235b78012304a48adc047942c341cb7b3b7 Mon Sep 17 00:00:00 2001 From: Thomas Krzero Date: Wed, 20 Sep 2017 08:32:19 +0200 Subject: [PATCH] #516 feat(services) - add the ability to manage cpu/mem limits --- app/app.js | 3 +- .../createService/createServiceController.js | 74 +++++++++- .../createService/createservice.html | 8 +- .../createService/includes/placement.html | 57 -------- .../includes/resources-placement.html | 136 ++++++++++++++++++ .../service/includes/resources.html | 82 ++++++++--- app/components/service/serviceController.js | 40 +++++- app/directives/slider/por-slider.js | 12 ++ app/directives/slider/porSlider.html | 3 + app/directives/slider/porSliderController.js | 22 +++ app/helpers/serviceHelper.js | 1 - bower.json | 3 +- vendor.yml | 4 + 13 files changed, 353 insertions(+), 92 deletions(-) delete mode 100644 app/components/createService/includes/placement.html create mode 100644 app/components/createService/includes/resources-placement.html create mode 100644 app/directives/slider/por-slider.js create mode 100644 app/directives/slider/porSlider.html create mode 100644 app/directives/slider/porSliderController.js 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 @@ -
    -
    -
    - - - placement constraint - -
    -
    -
    -
    - name - -
    -
    - -
    -
    - value - -
    - -
    -
    -
    -
    - -
    -
    -
    - - - placement preference - -
    -
    -
    -
    - strategy - -
    -
    - value - -
    - -
    -
    -
    -
    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 @@ +
    +
    + Resources +
    + +
    + +
    + +
    +
    + +
    +
    +

    + Minimum memory available on a node to run a task +

    +
    +
    + + +
    + +
    + +
    +
    + +
    +
    +

    + Maximum memory usage per task (set to 0 for unlimited) +

    +
    +
    + + +
    + +
    + +
    +
    +

    + Minimum CPU available on a node to run a task +

    +
    +
    + + +
    + +
    + +
    +
    +

    + Maximum CPU usage per task +

    +
    +
    + +
    + Placement +
    + +
    +
    + + + placement constraint + +
    +
    +
    +
    + name + +
    +
    + +
    +
    + value + +
    + +
    +
    +
    + + +
    +
    + + + placement preference + +
    +
    +
    +
    + strategy + +
    +
    + value + +
    + +
    +
    +
    + +
    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