value
@@ -355,7 +355,7 @@
+
+
diff --git a/app/components/service/includes/placementPreferences.html b/app/components/service/includes/placementPreferences.html
new file mode 100644
index 000000000..210556e72
--- /dev/null
+++ b/app/components/service/includes/placementPreferences.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ There are no placement preferences for this service.
+
+
+
+
+
+
+
+
+
diff --git a/app/components/service/service.html b/app/components/service/service.html
index 4d15c6045..fd88628c2 100644
--- a/app/components/service/service.html
+++ b/app/components/service/service.html
@@ -113,6 +113,7 @@
Network & published ports
Resource limits & reservations
Placement constraints
+
Placement preferences
Restart policy
Update configuration
Service labels
@@ -152,6 +153,7 @@
Service specification
+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js
index a69c2fb78..83b3d9353 100644
--- a/app/components/service/serviceController.js
+++ b/app/components/service/serviceController.js
@@ -1,6 +1,6 @@
angular.module('service', [])
-.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
-function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
+.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
+function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
@@ -124,10 +124,24 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
}
};
- $scope.updatePlacementConstraint = function updatePlacementConstraint(service, constraint) {
+ $scope.updatePlacementConstraint = function(service, constraint) {
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
};
+ $scope.addPlacementPreference = function(service) {
+ service.ServicePreferences.push({ strategy: 'spread', value: '' });
+ updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
+ };
+ $scope.removePlacementPreference = function(service, index) {
+ var removedElement = service.ServicePreferences.splice(index, 1);
+ if (removedElement !== null) {
+ updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
+ }
+ };
+ $scope.updatePlacementPreference = function(service, constraint) {
+ updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
+ };
+
$scope.addPublishedPort = function addPublishedPort(service) {
if (!service.Ports) {
service.Ports = [];
@@ -174,9 +188,9 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
$('#loadingViewSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;
- config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
- config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
- config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
+ config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
+ config.TaskTemplate.ContainerSpec.Env = ServiceHelper.translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
+ config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceContainerLabels);
config.TaskTemplate.ContainerSpec.Image = service.Image;
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
@@ -188,6 +202,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
config.TaskTemplate.Placement = {};
}
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
+ config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences);
config.TaskTemplate.Resources = {
Limits: {
@@ -263,11 +278,12 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
function translateServiceArrays(service) {
service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
- service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
- service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
- service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
+ service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env);
+ service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels);
+ service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels);
service.ServiceMounts = angular.copy(service.Mounts);
- service.ServiceConstraints = translateConstraintsToKeyValue(service.Constraints);
+ service.ServiceConstraints = ServiceHelper.translateConstraintsToKeyValue(service.Constraints);
+ service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
}
function initView() {
@@ -310,7 +326,6 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
Notifications.error('Failure', err, 'Unable to retrieve service details');
})
.finally(function final() {
-
$('#loadingViewSpinner').hide();
});
}
@@ -341,80 +356,5 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
service.hasChanges = true;
}
- function translateEnvironmentVariables(env) {
- if (env) {
- var variables = [];
- env.forEach(function(variable) {
- var idx = variable.indexOf('=');
- var keyValue = [variable.slice(0,idx), variable.slice(idx+1)];
- var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
- variables.push({ key: keyValue[0], value: originalValue, originalKey: keyValue[0], originalValue: originalValue, added: true});
- });
- return variables;
- }
- return [];
- }
- function translateEnvironmentVariablesToEnv(env) {
- if (env) {
- var variables = [];
- env.forEach(function(variable) {
- if (variable.key && variable.key !== '') {
- variables.push(variable.key + '=' + variable.value);
- }
- });
- return variables;
- }
- return [];
- }
-
- function translateLabelsToServiceLabels(Labels) {
- var labels = [];
- if (Labels) {
- Object.keys(Labels).forEach(function(key) {
- labels.push({ key: key, value: Labels[key], originalKey: key, originalValue: Labels[key], added: true});
- });
- }
- return labels;
- }
- function translateServiceLabelsToLabels(labels) {
- var Labels = {};
- if (labels) {
- labels.forEach(function(label) {
- Labels[label.key] = label.value;
- });
- }
- return Labels;
- }
-
- function translateConstraintsToKeyValue(constraints) {
- function getOperator(constraint) {
- var indexEquals = constraint.indexOf('==');
- if (indexEquals >= 0) {
- return [indexEquals, '=='];
- }
- return [constraint.indexOf('!='), '!='];
- }
- if (constraints) {
- var keyValueConstraints = [];
- constraints.forEach(function(constraint) {
- var operatorIndices = getOperator(constraint);
-
- var key = constraint.slice(0, operatorIndices[0]);
- var operator = operatorIndices[1];
- var value = constraint.slice(operatorIndices[0] + 2);
-
- keyValueConstraints.push({
- key: key,
- value: value,
- operator: operator,
- originalKey: key,
- originalValue: value
- });
- });
- return keyValueConstraints;
- }
- return [];
- }
-
initView();
}]);
diff --git a/app/helpers/serviceHelper.js b/app/helpers/serviceHelper.js
index 1b2f453ce..bddab33c6 100644
--- a/app/helpers/serviceHelper.js
+++ b/app/helpers/serviceHelper.js
@@ -1,5 +1,4 @@
-angular.module('portainer.helpers')
-.factory('ServiceHelper', [function ServiceHelperFactory() {
+angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHelperFactory() {
'use strict';
return {
serviceToConfig: function(service) {
@@ -13,17 +12,113 @@ angular.module('portainer.helpers')
EndpointSpec: service.Spec.EndpointSpec
};
},
+ translateKeyValueToPlacementPreferences: function(keyValuePreferences) {
+ if (keyValuePreferences) {
+ var preferences = [];
+ keyValuePreferences.forEach(function(preference) {
+ if (preference.strategy && preference.strategy !== '' && preference.value && preference.value !== '') {
+ switch (preference.strategy.toLowerCase()) {
+ case 'spread':
+ preferences.push({
+ 'Spread': {
+ 'SpreadDescriptor': preference.value
+ }
+ });
+ break;
+ }
+ }
+ });
+ return preferences;
+ }
+ return [];
+ },
translateKeyValueToPlacementConstraints: function(keyValueConstraints) {
if (keyValueConstraints) {
var constraints = [];
- keyValueConstraints.forEach(function(keyValueConstraint) {
- if (keyValueConstraint.key && keyValueConstraint.key !== '' && keyValueConstraint.value && keyValueConstraint.value !== '') {
- constraints.push(keyValueConstraint.key + keyValueConstraint.operator + keyValueConstraint.value);
+ keyValueConstraints.forEach(function(constraint) {
+ if (constraint.key && constraint.key !== '' && constraint.value && constraint.value !== '') {
+ constraints.push(constraint.key + constraint.operator + constraint.value);
}
});
return constraints;
}
return [];
- }
+ },
+ translateEnvironmentVariables: function(env) {
+ if (env) {
+ var variables = [];
+ env.forEach(function(variable) {
+ var idx = variable.indexOf('=');
+ var keyValue = [variable.slice(0, idx), variable.slice(idx + 1)];
+ var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
+ variables.push({
+ key: keyValue[0],
+ value: originalValue,
+ originalKey: keyValue[0],
+ originalValue: originalValue,
+ added: true
+ });
+ });
+ return variables;
+ }
+ return [];
+ },
+ translateEnvironmentVariablesToEnv: function(env) {
+ if (env) {
+ var variables = [];
+ env.forEach(function(variable) {
+ if (variable.key && variable.key !== '') {
+ variables.push(variable.key + '=' + variable.value);
+ }
+ });
+ return variables;
+ }
+ return [];
+ },
+ translatePreferencesToKeyValue: function(preferences) {
+ if (preferences) {
+ var keyValuePreferences = [];
+ preferences.forEach(function(preference) {
+ if (preference.Spread) {
+ keyValuePreferences.push({
+ strategy: 'Spread',
+ value: preference.Spread.SpreadDescriptor
+ });
+ }
+ });
+ return keyValuePreferences;
+ }
+ return [];
+ },
+ translateConstraintsToKeyValue: function(constraints) {
+ function getOperator(constraint) {
+ var indexEquals = constraint.indexOf('==');
+ if (indexEquals >= 0) {
+ return [indexEquals, '=='];
+ }
+ return [constraint.indexOf('!='), '!='];
+ }
+ if (constraints) {
+ var keyValueConstraints = [];
+ constraints.forEach(function(constraint) {
+ var operatorIndices = getOperator(constraint);
+
+ var key = constraint.slice(0, operatorIndices[0]);
+ var operator = operatorIndices[1];
+ var value = constraint.slice(operatorIndices[0] + 2);
+
+ keyValueConstraints.push({
+ key: key,
+ value: value,
+ operator: operator,
+ originalKey: key,
+ originalValue: value
+ });
+ });
+ return keyValueConstraints;
+ }
+ return [];
+ }
+
};
}]);
diff --git a/app/models/docker/service.js b/app/models/docker/service.js
index 766a4aaee..5b2a7f53e 100644
--- a/app/models/docker/service.js
+++ b/app/models/docker/service.js
@@ -41,27 +41,37 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.RestartWindow = 0;
}
this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : [];
+ this.Preferences = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Preferences || [] : [];
+ this.Platforms = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Platforms || [] : [];
this.Labels = data.Spec.Labels;
var containerSpec = data.Spec.TaskTemplate.ContainerSpec;
if (containerSpec) {
this.ContainerLabels = containerSpec.Labels;
- this.Env = containerSpec.Env;
- this.Mounts = containerSpec.Mounts || [];
- this.User = containerSpec.User;
- this.Dir = containerSpec.Dir;
this.Command = containerSpec.Command;
this.Arguments = containerSpec.Args;
+ this.Hostname = containerSpec.Hostname;
+ this.Env = containerSpec.Env;
+ this.Dir = containerSpec.Dir;
+ this.User = containerSpec.User;
+ this.Groups = containerSpec.Groups;
+ this.TTY = containerSpec.TTY;
+ this.OpenStdin = containerSpec.OpenStdin;
+ this.ReadOnly = containerSpec.ReadOnly;
+ this.Mounts = containerSpec.Mounts || [];
+ this.StopSignal = containerSpec.StopSignal;
+ this.StopGracePeriod = containerSpec.StopGracePeriod;
+ this.HealthCheck = containerSpec.HealthCheck || {};
+ this.Hosts = containerSpec.Hosts;
+ this.DNSConfig = containerSpec.DNSConfig;
this.Secrets = containerSpec.Secrets;
}
if (data.Endpoint) {
this.Ports = data.Endpoint.Ports;
}
- this.Mounts = [];
- if (data.Spec.TaskTemplate.ContainerSpec.Mounts) {
- this.Mounts = data.Spec.TaskTemplate.ContainerSpec.Mounts;
- }
+ this.LogDriver = data.Spec.TaskTemplate.LogDriver;
+ this.Runtime = data.Spec.TaskTemplate.Runtime;
this.VirtualIPs = data.Endpoint ? data.Endpoint.VirtualIPs : [];
@@ -75,6 +85,8 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.UpdateFailureAction = 'pause';
}
+ this.RollbackConfig = data.Spec.RollbackConfig;
+
this.Checked = false;
this.Scale = false;
this.EditName = false;