diff --git a/app/components/createNetwork/createNetworkController.js b/app/components/createNetwork/createNetworkController.js index 10aaf9049..01b0b3c7a 100644 --- a/app/components/createNetwork/createNetworkController.js +++ b/app/components/createNetwork/createNetworkController.js @@ -1,6 +1,6 @@ angular.module('createNetwork', []) -.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', -function ($scope, $state, Notifications, Network) { +.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', 'LabelHelper', +function ($scope, $state, Notifications, Network, LabelHelper) { $scope.formValues = { DriverOptions: [], Subnet: '', @@ -30,7 +30,7 @@ function ($scope, $state, Notifications, Network) { }; $scope.addLabel = function() { - $scope.formValues.Labels.push({ name: '', value: ''}); + $scope.formValues.Labels.push({ key: '', value: ''}); }; $scope.removeLabel = function(index) { @@ -74,13 +74,7 @@ function ($scope, $state, Notifications, Network) { } function prepareLabelsConfig(config) { - var labels = {}; - $scope.formValues.Labels.forEach(function (label) { - if (label.name && label.value) { - labels[label.name] = label.value; - } - }); - config.Labels = labels; + config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); } function prepareConfiguration() { diff --git a/app/components/createNetwork/createnetwork.html b/app/components/createNetwork/createnetwork.html index fdf591b88..a9f1ef7d4 100644 --- a/app/components/createNetwork/createnetwork.html +++ b/app/components/createNetwork/createnetwork.html @@ -90,7 +90,7 @@
name - +
value diff --git a/app/components/createSecret/createSecretController.js b/app/components/createSecret/createSecretController.js index ca2fcdc79..3f2533270 100644 --- a/app/components/createSecret/createSecretController.js +++ b/app/components/createSecret/createSecretController.js @@ -1,6 +1,6 @@ angular.module('createSecret', []) -.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', -function ($scope, $state, Notifications, SecretService) { +.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', +function ($scope, $state, Notifications, SecretService, LabelHelper) { $scope.formValues = { Name: '', Data: '', @@ -9,7 +9,7 @@ function ($scope, $state, Notifications, SecretService) { }; $scope.addLabel = function() { - $scope.formValues.Labels.push({ name: '', value: ''}); + $scope.formValues.Labels.push({ key: '', value: ''}); }; $scope.removeLabel = function(index) { @@ -17,13 +17,7 @@ function ($scope, $state, Notifications, SecretService) { }; function prepareLabelsConfig(config) { - var labels = {}; - $scope.formValues.Labels.forEach(function (label) { - if (label.name && label.value) { - labels[label.name] = label.value; - } - }); - config.Labels = labels; + config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); } function prepareSecretData(config) { diff --git a/app/components/createSecret/createsecret.html b/app/components/createSecret/createsecret.html index 2a840af60..c918e8cd5 100644 --- a/app/components/createSecret/createsecret.html +++ b/app/components/createSecret/createsecret.html @@ -52,7 +52,7 @@
name - +
value diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index 9a29f315d..846e188f1 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', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator', 'RegistryService', 'HttpRequestHelper', -function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator, RegistryService, HttpRequestHelper) { +.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator', 'RegistryService', 'HttpRequestHelper', +function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator, RegistryService, HttpRequestHelper) { $scope.formValues = { Name: '', @@ -23,6 +23,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic Ports: [], Parallelism: 1, PlacementConstraints: [], + PlacementPreferences: [], UpdateDelay: 0, FailureAction: 'pause', Secrets: [] @@ -81,7 +82,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic }; $scope.addPlacementPreference = function() { - $scope.formValues.PlacementPreferences.push({ key: '', operator: '==', value: '' }); + $scope.formValues.PlacementPreferences.push({ strategy: 'spread', value: '' }); }; $scope.removePlacementPreference = function(index) { @@ -89,7 +90,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic }; $scope.addLabel = function() { - $scope.formValues.Labels.push({ name: '', value: ''}); + $scope.formValues.Labels.push({ key: '', value: ''}); }; $scope.removeLabel = function(index) { @@ -97,7 +98,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic }; $scope.addContainerLabel = function() { - $scope.formValues.ContainerLabels.push({ name: '', value: ''}); + $scope.formValues.ContainerLabels.push({ key: '', value: ''}); }; $scope.removeContainerLabel = function(index) { @@ -170,21 +171,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic } function prepareLabelsConfig(config, input) { - var labels = {}; - input.Labels.forEach(function (label) { - if (label.name && label.value) { - labels[label.name] = label.value; - } - }); - config.Labels = labels; - - var containerLabels = {}; - input.ContainerLabels.forEach(function (label) { - if (label.name && label.value) { - containerLabels[label.name] = label.value; - } - }); - config.TaskTemplate.ContainerSpec.Labels = containerLabels; + config.Labels = LabelHelper.fromKeyValueToLabelHash(input.Labels); + config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(input.ContainerLabels); } function prepareVolumes(config, input) { @@ -213,8 +201,10 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic FailureAction: input.FailureAction }; } + function preparePlacementConfig(config, input) { config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints); + config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(input.PlacementPreferences); } function prepareSecretConfig(config, input) { diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 25e58e568..f96f834ef 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -328,7 +328,7 @@
name - +
value @@ -355,7 +355,7 @@
name - +
value diff --git a/app/components/createService/includes/placement.html b/app/components/createService/includes/placement.html index f33c5ab88..c12547fac 100644 --- a/app/components/createService/includes/placement.html +++ b/app/components/createService/includes/placement.html @@ -29,3 +29,29 @@
+ +
+
+
+ + + placement preference + +
+
+
+
+ strategy + +
+
+ value + +
+ +
+
+
+
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.

+
+ + + + + + + + + + + + + + +
StrategyValue
+
+ +
+
+
+ + + + +
+
+
+ + + +
+
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;