mirror of https://github.com/portainer/portainer
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 messagepull/3148/head
parent
ec19faaa24
commit
52704e681b
|
@ -14,19 +14,13 @@ function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, Htt
|
||||||
method: 'POST', params: {action: 'create'},
|
method: 'POST', params: {action: 'create'},
|
||||||
headers: {
|
headers: {
|
||||||
'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader,
|
'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'
|
'version': '1.29'
|
||||||
},
|
},
|
||||||
ignoreLoadingBar: true
|
ignoreLoadingBar: true
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
method: 'POST', params: { id: '@id', action: 'update', version: '@version' },
|
method: 'POST', params: { id: '@id', action: 'update', version: '@version', rollback: '@rollback' },
|
||||||
headers: {
|
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'
|
'version': '1.29'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,8 +58,17 @@ function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, Resource
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.update = function(service, config) {
|
service.update = function(serv, config, rollback) {
|
||||||
return Service.update({ id: service.Id, version: service.Version }, config).$promise;
|
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) {
|
service.logs = function(id, stdout, stderr, timestamps, since, tail) {
|
||||||
|
|
|
@ -91,11 +91,18 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr authorization="DockerServiceLogs, DockerServiceUpdate, DockerServiceDelete">
|
<tr authorization="DockerServiceLogs, DockerServiceUpdate, DockerServiceDelete">
|
||||||
<td colspan="2">
|
<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>
|
<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">
|
<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-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>
|
<span ng-show="state.updateInProgress">Update in progress...</span>
|
||||||
</button>
|
</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">
|
<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-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>
|
<span ng-show="state.deletionInProgress">Deletion in progress...</span>
|
||||||
|
|
|
@ -22,7 +22,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
updateInProgress: false,
|
updateInProgress: false,
|
||||||
deletionInProgress: false
|
deletionInProgress: false,
|
||||||
|
rollbackInProgress: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.tasks = [];
|
$scope.tasks = [];
|
||||||
|
@ -281,7 +282,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
||||||
return hasChanges;
|
return hasChanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateService = function updateService(service) {
|
function buildChanges(service) {
|
||||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||||
config.Name = service.Name;
|
config.Name = service.Name;
|
||||||
config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
|
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',
|
Mode: (config.EndpointSpec && config.EndpointSpec.Mode) || 'vip',
|
||||||
Ports: service.Ports
|
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:/)) {
|
if (data.message && data.message.match(/^rpc error:/)) {
|
||||||
Notifications.error(data.message, 'Error');
|
Notifications.error(data.message, 'Error');
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue