diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js index c2fa5f5ab..e2a394e58 100644 --- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js +++ b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('ServicesDatatableActionsController', ['$state', 'ServiceService', 'ServiceHelper', 'Notifications', 'ModalService', -function ($state, ServiceService, ServiceHelper, Notifications, ModalService) { +.controller('ServicesDatatableActionsController', ['$state', 'ServiceService', 'ServiceHelper', 'Notifications', 'ModalService', 'ImageHelper', +function ($state, ServiceService, ServiceHelper, Notifications, ModalService, ImageHelper) { this.scaleAction = function scaleService(service) { var config = ServiceHelper.serviceToConfig(service.Model); @@ -17,16 +17,6 @@ function ($state, ServiceService, ServiceHelper, Notifications, ModalService) { }); }; - this.updateAction = 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); - } - ); - }; - this.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.', @@ -37,10 +27,28 @@ function ($state, ServiceService, ServiceHelper, Notifications, ModalService) { ); }; - function forceUpdateServices(services) { + this.updateAction = function(selectedItems) { + ModalService.confirmServiceForceUpdate( + 'Do you want to force an update of the selected service(s)? All the tasks associated to the selected service(s) will be recreated.', + function (result) { + if(!result) { return; } + var pullImage = false; + if (result[0]) { + pullImage = true; + } + forceUpdateServices(selectedItems, pullImage); + } + ); + }; + + function forceUpdateServices(services, pullImage) { var actionCount = services.length; angular.forEach(services, function (service) { var config = ServiceHelper.serviceToConfig(service.Model); + if (pullImage) { + config.TaskTemplate.ContainerSpec.Image = ImageHelper.removeDigestFromRepository(config.TaskTemplate.ContainerSpec.Image); + } + // 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++; diff --git a/app/docker/helpers/imageHelper.js b/app/docker/helpers/imageHelper.js index 0ea9292bc..01e3484ac 100644 --- a/app/docker/helpers/imageHelper.js +++ b/app/docker/helpers/imageHelper.js @@ -55,5 +55,9 @@ angular.module('portainer.docker') }; }; + helper.removeDigestFromRepository = function(repository) { + return repository.split('@sha')[0]; + }; + return helper; }]); diff --git a/app/docker/models/service.js b/app/docker/models/service.js index 116590df4..07a769156 100644 --- a/app/docker/models/service.js +++ b/app/docker/models/service.js @@ -49,7 +49,7 @@ function ServiceViewModel(data, runningTasks, allTasks, nodes) { this.LogDriverName = ''; this.LogDriverOpts = []; } - + 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 || [] : []; diff --git a/app/docker/rest/service.js b/app/docker/rest/service.js index 806586104..e87dcd927 100644 --- a/app/docker/rest/service.js +++ b/app/docker/rest/service.js @@ -10,10 +10,24 @@ function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, Htt query: { method: 'GET', isArray: true, params: {filters: '@filters'} }, create: { method: 'POST', params: {action: 'create'}, - headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }, + headers: { + 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader, + // TODO: This is a temporary work-around that allows us to leverage digest pinning on + // the Docker daemon side. It has been moved client-side since Docker API version > 1.29. + // We should introduce digest pinning in Portainer as well. + 'version': '1.29' + }, ignoreLoadingBar: true }, - update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} }, + update: { + method: 'POST', params: { id: '@id', action: 'update', version: '@version' }, + headers: { + // TODO: This is a temporary work-around that allows us to leverage digest pinning on + // the Docker daemon side. It has been moved client-side since Docker API version > 1.29. + // We should introduce digest pinning in Portainer as well. + 'version': '1.29' + } + }, remove: { method: 'DELETE', params: {id: '@id'} }, logs: { method: 'GET', params: { id: '@id', action: 'logs' }, diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js index ffce87b3b..f7dcb9c59 100644 --- a/app/docker/views/services/edit/serviceController.js +++ b/app/docker/views/services/edit/serviceController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'ContainerService', 'TaskHelper', 'Notifications', 'ModalService', 'PluginService', 'Authentication', 'SettingsService', 'VolumeService', -function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, ContainerService, TaskHelper, Notifications, ModalService, PluginService, Authentication, SettingsService, VolumeService) { +.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'ContainerService', 'TaskHelper', 'Notifications', 'ModalService', 'PluginService', 'Authentication', 'SettingsService', 'VolumeService', 'ImageHelper', +function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, ContainerService, TaskHelper, Notifications, ModalService, PluginService, Authentication, SettingsService, VolumeService, ImageHelper) { $scope.state = { updateInProgress: false, @@ -354,16 +354,24 @@ 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); + 'Do you want to force an update of the service? All the tasks associated to the service will be recreated.', + function (result) { + if(!result) { return; } + var pullImage = false; + if (result[0]) { + pullImage = true; + } + forceUpdateService(service, pullImage); } ); }; - function forceUpdateService(service) { + function forceUpdateService(service, pullImage) { var config = ServiceHelper.serviceToConfig(service.Model); + if (pullImage) { + config.TaskTemplate.ContainerSpec.Image = config.TaskTemplate.ContainerSpec.Image = ImageHelper.removeDigestFromRepository(config.TaskTemplate.ContainerSpec.Image); + } + // 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++; diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 0edd374f2..196b74640 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -157,9 +157,16 @@ angular.module('portainer.app') }; service.confirmServiceForceUpdate = function(message, callback) { - service.confirm({ + service.customPrompt({ title: 'Are you sure ?', message: message, + inputType: 'checkbox', + inputOptions: [ + { + text: 'Pull latest image version', + value: '1' + } + ], buttons: { confirm: { label: 'Update',