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