feat(services): rollback service capability (#3057)

* feat(services): rollback service capability

* refactor(services): notification reword

Co-Authored-By: William <william.conquest@portainer.io>

* refactor(services): remove TODO comment + add note on rollback capability

* fix(services): service update rpc error version out of sync

* feat(services): confirmation modal on rollback

* feat(services): rpc error no previous spec message
pull/3148/head
xAt0mZ 2019-09-10 00:56:57 +02:00 committed by Anthony Lapenna
parent ec19faaa24
commit 52704e681b
4 changed files with 70 additions and 12 deletions

View File

@ -14,19 +14,13 @@ function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, Htt
method: 'POST', params: {action: 'create'},
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' },
method: 'POST', params: { id: '@id', action: 'update', version: '@version', rollback: '@rollback' },
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'
}
},

View File

@ -58,8 +58,17 @@ function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, Resource
return deferred.promise;
};
service.update = function(service, config) {
return Service.update({ id: service.Id, version: service.Version }, config).$promise;
service.update = function(serv, config, rollback) {
return service.service(serv.Id).then((data) => {
const params = {
id: serv.Id,
version: data.Version
};
if (rollback) {
params.rollback = rollback
}
return Service.update(params, config).$promise;
});
};
service.logs = function(id, stdout, stderr, timestamps, since, tail) {

View File

@ -91,11 +91,18 @@
</tr>
<tr authorization="DockerServiceLogs, DockerServiceUpdate, DockerServiceDelete">
<td colspan="2">
<p class="small text-muted">
Note: you can only rollback one level of changes. Clicking the rollback button without making a new change will undo your previous rollback
<p>
<a authorization="DockerServiceLogs" ng-if="applicationState.endpoint.apiVersion >= 1.30" class="btn btn-primary btn-sm" type="button" ui-sref="docker.services.service.logs({id: service.Id})"><i class="fa fa-file-alt space-right" aria-hidden="true"></i>Service logs</a>
<button authorization="DockerServiceUpdate" type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || isUpdating" ng-click="forceUpdateService(service)" button-spinner="state.updateInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
<span ng-hide="state.updateInProgress"><i class="fa fa-sync space-right" aria-hidden="true"></i>Update the service</span>
<span ng-show="state.updateInProgress">Update in progress...</span>
</button>
<button authorization="DockerServiceUpdate" type="button" class="btn btn-primary btn-sm" ng-disabled="state.rollbackInProgress || isUpdating" ng-click="rollbackService(service)" button-spinner="state.rollbackInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
<span ng-hide="state.rollbackInProgress"><i class="fa fa-undo space-right" aria-hidden="true"></i>Rollback the service</span>
<span ng-show="state.rollbackInProgress">Rollback in progress...</span>
</button>
<button authorization="DockerServiceDelete" type="button" class="btn btn-danger btn-sm" ng-disabled="state.deletionInProgress || isUpdating" ng-click="removeService()" button-spinner="state.deletionInProgress">
<span ng-hide="state.deletionInProgress"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete the service</span>
<span ng-show="state.deletionInProgress">Deletion in progress...</span>

View File

@ -22,7 +22,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
$scope.state = {
updateInProgress: false,
deletionInProgress: false
deletionInProgress: false,
rollbackInProgress: false,
};
$scope.tasks = [];
@ -281,7 +282,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
return hasChanges;
};
$scope.updateService = function updateService(service) {
function buildChanges(service) {
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;
config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
@ -361,8 +362,55 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
Mode: (config.EndpointSpec && config.EndpointSpec.Mode) || 'vip',
Ports: service.Ports
};
return service, config;
}
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
function rollbackService(service) {
$scope.state.rollbackInProgress = true;
let config = {};
service, config = buildChanges(service);
ServiceService.update(service, config, 'previous')
.then(function (data) {
if (data.message && data.message.match(/^rpc error:/)) {
Notifications.error(data.message, 'Error');
} else {
Notifications.success('Success', 'Service successfully rolled back');
$scope.cancelChanges({});
initView();
}
}).catch(function (e) {
if (e.data.message && e.data.message.includes('does not have a previous spec')) {
Notifications.error('Failure', { message: 'No previous config to rollback to.' });
} else {
Notifications.error('Failure', e, 'Unable to rollback service');
}
}).finally(function () {
$scope.state.rollbackInProgress = false;
});
}
$scope.rollbackService = function(service) {
ModalService.confirm({
title: 'Rollback service',
message: 'Are you sure you want to rollback?',
buttons: {
confirm: {
label: 'Yes',
className: 'btn-danger'
}
},
callback: function onConfirm(confirmed) {
if(!confirmed) { return; }
rollbackService(service);
}
});
};
$scope.updateService = function updateService(service) {
let config = {};
service, config = buildChanges(service);
ServiceService.update(service, config)
.then(function (data) {
if (data.message && data.message.match(/^rpc error:/)) {
Notifications.error(data.message, 'Error');
} else {