From 1b51daf9c4e6efd4a7275ed1efbcd7d33d8b97ea Mon Sep 17 00:00:00 2001 From: baron_l Date: Sun, 19 Aug 2018 08:05:16 +0200 Subject: [PATCH] fix(services): fix invalid replica count (#1990) (#2127) * fix(services): replicas numbers display is now correct with constraints and down nodes * refactor(helpers): constraint helper has less complexity * feat(services): constraints on node/engine labels are now supported * refactor(helpers): ConstraintsHelper - remove regex patterns and improve code lisibility * refactor(helpers): rework matchesConstraint() for better code lisibility and lodash find() instead for IE compatibility --- .../services-datatable/servicesDatatable.html | 2 +- app/docker/filters/filters.js | 10 +- app/docker/helpers/constraintsHelper.js | 106 ++++++++++++++++++ app/docker/models/task.js | 1 + 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 app/docker/helpers/constraintsHelper.js diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index 8ed52ff2b..ba3d870ad 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -96,7 +96,7 @@ {{ item.Image | hideshasum }} {{ item.Mode }} - {{ item.Tasks | runningtaskscount }} / {{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount) }} + {{ item.Tasks | runningtaskscount }} / {{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount:item) }} Scale diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js index a55419333..a07f1be0d 100644 --- a/app/docker/filters/filters.js +++ b/app/docker/filters/filters.js @@ -192,26 +192,26 @@ angular.module('portainer.docker') return ''; }; }) -.filter('availablenodecount', function () { +.filter('availablenodecount', ['ConstraintsHelper', function (ConstraintsHelper) { 'use strict'; - return function (nodes) { + return function (nodes, service) { var availableNodes = 0; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; - if (node.Availability === 'active' && node.Status === 'ready') { + if (node.Availability === 'active' && node.Status === 'ready' && ConstraintsHelper.matchesServiceConstraints(service, node)) { availableNodes++; } } return availableNodes; }; -}) +}]) .filter('runningtaskscount', function () { 'use strict'; return function (tasks) { var runningTasks = 0; for (var i = 0; i < tasks.length; i++) { var task = tasks[i]; - if (task.Status.State === 'running') { + if (task.Status.State === 'running' && task.DesiredState === 'running') { runningTasks++; } } diff --git a/app/docker/helpers/constraintsHelper.js b/app/docker/helpers/constraintsHelper.js new file mode 100644 index 000000000..b2700deba --- /dev/null +++ b/app/docker/helpers/constraintsHelper.js @@ -0,0 +1,106 @@ +function ConstraintModel(op, key, value) { + this.op = op; + this.value = value; + this.key = key; +} + +var patterns = { + id: { + nodeId: 'node.id', + nodeHostname: 'node.hostname', + nodeRole: 'node.role', + nodeLabels: 'node.labels.', + engineLabels: 'engine.labels.' + }, + op: { + eq: '==', + neq: '!=' + } +}; + +function matchesConstraint(value, constraint) { + if (!constraint || + (constraint.op === patterns.op.eq && value === constraint.value) || + (constraint.op === patterns.op.neq && value !== constraint.value)) { + return true; + } + return false; +} + +function matchesLabel(labels, constraint) { + if (!constraint) { + return true; + } + var found = _.find(labels, function (label) { + return label.key === constraint.key && label.value === constraint.value; + }); + return found !== undefined; +} + +function extractValue(constraint, op) { + return constraint.split(op).pop().trim(); +} + +function extractCustomLabelKey(constraint, op, baseLabelKey) { + return constraint.split(op).shift().trim().replace(baseLabelKey, ''); +} + +angular.module('portainer.docker') + .factory('ConstraintsHelper', [function ConstraintsHelperFactory() { + 'use strict'; + return { + transformConstraints: function (constraints) { + var transform = {}; + for (var i = 0; i < constraints.length; i++) { + var constraint = constraints[i]; + + var op; + if (constraint.includes(patterns.op.eq)) { + op = patterns.op.eq; + } else if (constraint.includes(patterns.op.neq)) { + op = patterns.op.neq; + } + + var value = extractValue(constraint, op); + var key = ''; + switch (true) { + case constraint.includes(patterns.id.nodeId): + transform.nodeId = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.nodeHostname): + transform.nodeHostname = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.nodeRole): + transform.nodeRole = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.nodeLabels): + key = extractCustomLabelKey(constraint, op, patterns.id.nodeLabels); + transform.nodeLabels = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.engineLabels): + key = extractCustomLabelKey(constraint, op, patterns.id.engineLabels); + transform.engineLabels = new ConstraintModel(op, key, value); + break; + default: + break; + } + } + return transform; + }, + matchesServiceConstraints: function (service, node) { + if (service.Constraints === undefined || service.Constraints.length === 0) { + return true; + } + var constraints = this.transformConstraints(angular.copy(service.Constraints)); + if (matchesConstraint(node.Id, constraints.nodeId) && + matchesConstraint(node.Hostname, constraints.nodeHostname) && + matchesConstraint(node.Role, constraints.nodeRole) && + matchesLabel(node.Labels, constraints.nodeLabels) && + matchesLabel(node.EngineLabels, constraints.engineLabels) + ) { + return true; + } + return false; + } + }; + }]); \ No newline at end of file diff --git a/app/docker/models/task.js b/app/docker/models/task.js index e28390483..5714c1509 100644 --- a/app/docker/models/task.js +++ b/app/docker/models/task.js @@ -5,6 +5,7 @@ function TaskViewModel(data) { this.Slot = data.Slot; this.Spec = data.Spec; this.Status = data.Status; + this.DesiredState = data.DesiredState; this.ServiceId = data.ServiceID; this.NodeId = data.NodeID; if (data.Status && data.Status.ContainerStatus && data.Status.ContainerStatus.ContainerID) {