diff --git a/app/components/service/service.html b/app/components/service/service.html index 16245f8e2..ba298a3d9 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -38,6 +38,7 @@ <td>ID</td> <td> {{ service.Id }} + <button class="btn btn-xs btn-primary" ng-click="forceUpdateService(service)"><i class="fa fa-refresh space-right" aria-hidden="true" ng-disabled="isUpdating" ng-if="applicationState.endpoint.apiVersion >= 1.25"></i>Force update this service</button> <button class="btn btn-xs btn-danger" ng-click="removeService()"><i class="fa fa-trash space-right" aria-hidden="true" ng-disabled="isUpdating"></i>Delete this service</button> </td> </tr> diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 74c72a02a..2c4a5602e 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -168,7 +168,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, } }; - $scope.addLogDriverOpt = function addLogDriverOpt(service) { + $scope.addLogDriverOpt = function addLogDriverOpt(service) { service.LogDriverOpts.push({ key: '', value: '', originalValue: '' }); updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts); }; @@ -182,16 +182,16 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) { updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts); } - }; - $scope.updateLogDriverName = function updateLogDriverName(service) { - updateServiceArray(service, 'LogDriverName', service.LogDriverName); - }; + }; + $scope.updateLogDriverName = function updateLogDriverName(service) { + updateServiceArray(service, 'LogDriverName', service.LogDriverName); + }; $scope.addHostsEntry = function (service) { if (!service.Hosts) { service.Hosts = []; } - service.Hosts.push({ hostname: '', ip: '' }); + service.Hosts.push({ hostname: '', ip: '' }); }; $scope.removeHostsEntry = function(service, index) { var removedElement = service.Hosts.splice(index, 1); @@ -199,9 +199,9 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, updateServiceArray(service, 'Hosts', service.Hosts); } }; - $scope.updateHostsEntry = function(service, entry) { + $scope.updateHostsEntry = function(service, entry) { updateServiceArray(service, 'Hosts', service.Hosts); - }; + }; $scope.cancelChanges = function cancelChanges(service, keys) { if (keys) { // clean out the keys only from the list of modified keys @@ -239,7 +239,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : []; config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : []; config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : []; - + if (service.Mode === 'replicated') { config.Mode.Replicated.Replicas = service.Replicas; } @@ -279,17 +279,17 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, MaxAttempts: service.RestartMaxAttempts, Window: ServiceHelper.translateHumanDurationToNanos(service.RestartWindow) || 0 }; - + config.TaskTemplate.LogDriver = null; - if (service.LogDriverName) { + if (service.LogDriverName) { config.TaskTemplate.LogDriver = { Name: service.LogDriverName }; if (service.LogDriverName !== 'none') { var logOpts = ServiceHelper.translateKeyValueToLogDriverOpts(service.LogDriverOpts); if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { config.TaskTemplate.LogDriver.Options = logOpts; } - } - } + } + } if (service.Ports) { service.Ports.forEach(function (binding) { @@ -338,6 +338,32 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, }); } + $scope.forceUpdateService = function(service) { + ModalService.confirmServiceForceUpdate( + 'Do you want to force update this service? All the tasks associated to the selected service(s) will be recreated.', + function onConfirm(confirmed) { + if(!confirmed) { return; } + forceUpdateService(service); + } + ); + }; + + function forceUpdateService(service) { + var config = ServiceHelper.serviceToConfig(service.Model); + // As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random + // value or an increment of the counter value to force an update. + config.TaskTemplate.ForceUpdate++; + ServiceService.update(service, config) + .then(function success(data) { + Notifications.success('Service successfully updated', service.Name); + $scope.cancelChanges({}); + initView(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to force update service', service.Name); + }); + } + function translateServiceArrays(service) { service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : []; service.ServiceConfigs = service.Configs ? service.Configs.map(ConfigHelper.flattenConfig) : []; @@ -365,7 +391,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, } function initView() { - var apiVersion = $scope.applicationState.endpoint.apiVersion; + var apiVersion = $scope.applicationState.endpoint.apiVersion; ServiceService.service($transition$.params().id) .then(function success(data) { diff --git a/app/components/services/services.html b/app/components/services/services.html index 3c23ac6cd..48eebed2e 100644 --- a/app/components/services/services.html +++ b/app/components/services/services.html @@ -16,7 +16,9 @@ show-ownership-column="applicationState.application.authentication" remove-action="removeAction" scale-action="scaleAction" + force-update-action="forceUpdateAction" swarm-manager-ip="swarmManagerIP" + show-force-update-button="applicationState.endpoint.apiVersion >= 1.25" ></services-datatable> </div> </div> diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js index e2223da48..8937fa392 100644 --- a/app/components/services/servicesController.js +++ b/app/components/services/servicesController.js @@ -17,6 +17,39 @@ function ($q, $scope, $state, Service, ServiceService, ServiceHelper, Notificati }); }; + $scope.forceUpdateAction = function(selectedItems) { + ModalService.confirmServiceForceUpdate( + 'Do you want to force update of selected service(s)? All the tasks associated to the selected service(s) will be recreated.', + function onConfirm(confirmed) { + if(!confirmed) { return; } + forceUpdateServices(selectedItems); + } + ); + }; + + function forceUpdateServices(services) { + var actionCount = services.length; + angular.forEach(services, function (service) { + var config = ServiceHelper.serviceToConfig(service.Model); + // As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random + // value or an increment of the counter value to force an update. + config.TaskTemplate.ForceUpdate++; + ServiceService.update(service, config) + .then(function success(data) { + Notifications.success('Service successfully updated', service.Name); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to force update service', service.Name); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); + }); + } + $scope.removeAction = function(selectedItems) { ModalService.confirmDeletion( 'Do you want to remove the selected service(s)? All the containers associated to the selected service(s) will be removed too.', diff --git a/app/directives/ui/datatables/services-datatable/servicesDatatable.html b/app/directives/ui/datatables/services-datatable/servicesDatatable.html index 60afade12..bd458edd2 100644 --- a/app/directives/ui/datatables/services-datatable/servicesDatatable.html +++ b/app/directives/ui/datatables/services-datatable/servicesDatatable.html @@ -16,6 +16,10 @@ ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"> <i class="fa fa-trash space-right" aria-hidden="true"></i>Remove </button> + <button ng-if="$ctrl.showForceUpdateButton" type="button" class="btn btn-sm btn-primary" + ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.forceUpdateAction($ctrl.state.selectedItems)"> + <i class="fa fa-refresh space-right" aria-hidden="true"></i>Force update + </button> <button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.service"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add service </button> diff --git a/app/directives/ui/datatables/services-datatable/servicesDatatable.js b/app/directives/ui/datatables/services-datatable/servicesDatatable.js index 14ae8954c..9d33fea66 100644 --- a/app/directives/ui/datatables/services-datatable/servicesDatatable.js +++ b/app/directives/ui/datatables/services-datatable/servicesDatatable.js @@ -12,6 +12,8 @@ angular.module('ui').component('servicesDatatable', { showOwnershipColumn: '<', removeAction: '<', scaleAction: '<', - swarmManagerIp: '<' + swarmManagerIp: '<', + forceUpdateAction: '<', + showForceUpdateButton: '<' } }); diff --git a/app/services/modalService.js b/app/services/modalService.js index 81b0b84a9..4c81bdcc6 100644 --- a/app/services/modalService.js +++ b/app/services/modalService.js @@ -156,5 +156,19 @@ angular.module('portainer.services') }); }; + service.confirmServiceForceUpdate = function(message, callback) { + service.confirm({ + title: 'Are you sure ?', + message: message, + buttons: { + confirm: { + label: 'Update', + className: 'btn-primary' + } + }, + callback: callback + }); + }; + return service; }]);