pull/4965/head
Dmitry Salakhov 2021-04-07 13:21:58 +12:00
parent 6d8f5e7479
commit fc9511dc97
8 changed files with 352 additions and 13 deletions

View File

@ -22,6 +22,7 @@ angular
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
.constant('API_ENDPOINT_WEBHOOKS', 'api/webhooks')
.constant('API_ENDPOINT_BACKUP', 'api/backup')
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('APPLICATION_CACHE_VALIDITY', 3600)

View File

@ -0,0 +1,27 @@
angular.module('portainer.app').factory('Backup', [
'$resource',
'API_ENDPOINT_BACKUP',
function BackupFactory($resource, API_ENDPOINT_BACKUP) {
'use strict';
return $resource(
API_ENDPOINT_BACKUP + '/:subResource/:action',
{},
{
download: {
method: 'POST',
responseType: 'blob',
ignoreLoadingBar: true,
transformResponse: (data, headersGetter) => ({
file: data,
name: headersGetter('Content-Disposition').replace('attachment; filename=', ''),
}),
},
getS3Settings: { method: 'GET', params: { subResource: 's3', action: 'settings' } },
saveS3Settings: { method: 'POST', params: { subResource: 's3', action: 'settings' } },
exportS3Backup: { method: 'POST', params: { subResource: 's3', action: 'execute' } },
restoreS3Backup: { method: 'POST', params: { subResource: 's3', action: 'restore' } },
getBackupStatus: { method: 'GET', params: { subResource: 's3', action: 'status' } },
}
);
},
]);

View File

@ -0,0 +1,90 @@
angular.module('portainer.app').factory('BackupService', [
'$q',
'$async',
'Backup',
'FileUploadService',
function BackupServiceFactory($q, $async, Backup, FileUploadService) {
'use strict';
const service = {};
service.downloadBackup = function (payload) {
return Backup.download({}, payload).$promise;
};
service.uploadBackup = function (file, password) {
return FileUploadService.uploadBackup(file, password);
};
service.getS3Settings = function () {
var deferred = $q.defer();
Backup.getS3Settings()
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve backup S3 settings', err: err });
});
return deferred.promise;
};
service.saveS3Settings = function (payload) {
var deferred = $q.defer();
Backup.saveS3Settings({}, payload)
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to save backup S3 settings', err: err });
});
return deferred.promise;
};
service.exportBackup = function (payload) {
var deferred = $q.defer();
Backup.exportS3Backup({}, payload)
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to export backup', err: err });
});
return deferred.promise;
};
service.restoreFromS3 = function (payload) {
var deferred = $q.defer();
Backup.restoreS3Backup({}, payload)
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to restore backup from S3', err: err });
});
return deferred.promise;
};
service.getBackupStatus = function () {
var deferred = $q.defer();
Backup.getBackupStatus()
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve backup status', err: err });
});
return deferred.promise;
};
return service;
},
]);

View File

@ -61,6 +61,16 @@ angular.module('portainer.app').factory('FileUploadService', [
});
};
service.uploadBackup = function (file, password) {
return Upload.upload({
url: 'api/restore',
data: {
file,
password,
},
});
};
service.createSwarmStack = function (stackName, swarmId, file, env, endpointId) {
return Upload.upload({
url: 'api/stacks?method=file&type=1&endpointId=' + endpointId,

View File

@ -11,8 +11,17 @@
<!-- init password panel -->
<div class="panel panel-default">
<div class="panel-body">
<!-- toggle -->
<div style="padding-bottom: 12px;">
<a ng-click="togglePanel()">
<i ng-class="{ true: 'glyphicon glyphicon-chevron-down', false: 'glyphicon glyphicon-chevron-right' }[state.showInitPassword]" aria-hidden="true"></i
><span style="padding-left: 10px;">New Portainer installation</span>
</a>
</div>
<!-- !toggle -->
<!-- init password form -->
<form class="simple-box-form form-horizontal">
<form class="simple-box-form form-horizontal" style="padding-left: 30px;" ng-if="state.showInitPassword">
<!-- note -->
<div class="form-group">
<div class="col-sm-12">
@ -98,6 +107,99 @@
</div>
</div>
<!-- !init password panel -->
<!-- restore backup panel -->
<div class="panel panel-default">
<div class="panel-body">
<!-- toggle -->
<div style="padding-bottom: 12px;">
<a ng-click="togglePanel()">
<i ng-class="{ true: 'glyphicon glyphicon-chevron-down', false: 'glyphicon glyphicon-chevron-right' }[state.showRestorePortainer]" aria-hidden="true"></i
><span style="padding-left: 10px;">Restore Portainer from backup</span>
</a>
</div>
<!-- !toggle -->
<!-- restore form -->
<form class="simple-box-form form-horizontal" style="padding-left: 30px;" ng-if="state.showRestorePortainer">
<!-- note -->
<div class="form-group">
<div class="col-sm-9">
<span class="small text-muted">
This will restore the Portainer metadata which contains information about the endpoints, stacks and applications, as well as the configured users.
</span>
</div>
</div>
<!-- !note -->
<!-- note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
You can upload a backup file from your computer.
</span>
</div>
</div>
<!-- !note -->
<!-- select-file-input -->
<div class="form-group">
<div class="col-sm-12">
<button class="btn btn-sm btn-primary" ngf-select accept=".tar.gz,.encrypted" ng-model="formValues.BackupFile" auto-focus>Select file</button>
<span style="margin-left: 5px;">
{{ formValues.BackupFile.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.BackupFile" aria-hidden="true"></i>
</span>
</div>
</div>
<!-- !select-file-input -->
<!-- password-input -->
<div class="form-group">
<label for="password" class="col-sm-3 control-label text-left">
Password
<portainer-tooltip
position="bottom"
message="If the backup is password protected, provide the password in order to extract the backup file, otherwise this field can be left empty."
></portainer-tooltip>
</label>
<div class="col-sm-4">
<input type="password" class="form-control" ng-model="formValues.Password" id="password" />
</div>
</div>
<!-- !password-input -->
<!-- note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
You are about to restore Portainer from this backup.
</span>
</div>
<div class="col-sm-12">
<span class="small text-muted">
After restoring has completed, please log in as a user that was known by the Portainer that was restored.
</span>
</div>
</div>
<!-- !note -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="!formValues.BackupFile || state.backupInProgress"
ng-click="uploadBackup()"
button-spinner="state.backupInProgress"
>
<span ng-hide="state.backupInProgress">Restore Portainer</span>
<span ng-show="state.backupInProgress">Restoring Portainer...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
<!-- !restore backup form -->
</div>
</div>
<!-- !restore backup panel -->
</div>
</div>
<!-- !simple box -->

View File

@ -8,7 +8,9 @@ angular.module('portainer.app').controller('InitAdminController', [
'SettingsService',
'UserService',
'EndpointService',
function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService) {
'BackupService',
'StatusService',
function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) {
$scope.logo = StateManager.getState().application.logo;
$scope.formValues = {
@ -20,6 +22,13 @@ angular.module('portainer.app').controller('InitAdminController', [
$scope.state = {
actionInProgress: false,
showInitPassword: true,
showRestorePortainer: false,
};
$scope.togglePanel = function () {
$scope.state.showInitPassword = !$scope.state.showInitPassword;
$scope.state.showRestorePortainer = !$scope.state.showRestorePortainer;
};
$scope.createAdminUser = function () {
@ -55,17 +64,35 @@ angular.module('portainer.app').controller('InitAdminController', [
});
};
function createAdministratorFlow() {
UserService.administratorExists()
.then(function success(exists) {
if (exists) {
$state.go('portainer.home');
async function waitPortainerRestart() {
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 5 * 1000));
try {
const status = await StatusService.status();
if (status && status.Version) {
return;
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to verify administrator account existence');
});
} catch (e) {}
}
throw 'Timeout to wait for Portainer restarting';
}
createAdministratorFlow();
$scope.uploadBackup = async function () {
$scope.state.backupInProgress = true;
const file = $scope.formValues.BackupFile;
const password = $scope.formValues.Password;
try {
await BackupService.uploadBackup(file, password);
await waitPortainerRestart();
Notifications.success('The backup has successfully been restored');
$state.go('portainer.auth');
} catch (err) {
Notifications.error('Failure', err, 'Unable to restore the backup');
} finally {
$scope.state.backupInProgress = false;
}
};
},
]);

View File

@ -191,3 +191,56 @@
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-download" title-text="Backup Portainer"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" ng-submit="backupPortainer()" name="backupPortainerForm">
<!-- Password protect -->
<div class="form-group">
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
<div class="col-sm-1">
<label class="switch"> <input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i> </label>
</div>
</div>
<!-- !Password protect -->
<!-- Password -->
<div class="form-group" ng-if="formValues.passwordProtect">
<label for="password" class="col-sm-1 control-label text-left">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" ng-model="formValues.password" id="password" name="password" required />
</div>
</div>
<div class="form-group col-md-12" ng-show="backupPortainerForm.password.$invalid">
<div class="small text-warning">
<div ng-messages="backupPortainerForm.password.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !Password -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="downloadBackup()"
ng-disabled="backupPortainerForm.$invalid || state.backupInProgress"
button-spinner="state.backupInProgress"
>
<span ng-hide="state.backupInProgress">Download backup</span>
<span ng-show="state.backupInProgress">Downloading backup</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -4,7 +4,10 @@ angular.module('portainer.app').controller('SettingsController', [
'Notifications',
'SettingsService',
'StateManager',
function ($scope, $state, Notifications, SettingsService, StateManager) {
'BackupService',
'FileSaver',
'Blob',
function ($scope, $state, Notifications, SettingsService, StateManager, BackupService, FileSaver) {
$scope.state = {
actionInProgress: false,
availableEdgeAgentCheckinOptions: [
@ -21,6 +24,8 @@ angular.module('portainer.app').controller('SettingsController', [
value: 30,
},
],
backupInProgress: false,
};
$scope.formValues = {
@ -29,6 +34,8 @@ angular.module('portainer.app').controller('SettingsController', [
labelValue: '',
enableEdgeComputeFeatures: false,
enableTelemetry: false,
passwordProtect: false,
password: '',
};
$scope.removeFilteredContainerLabel = function (index) {
@ -49,6 +56,28 @@ angular.module('portainer.app').controller('SettingsController', [
updateSettings(settings);
};
$scope.downloadBackup = function () {
const payload = {};
if ($scope.formValues.passwordProtect) {
payload.password = $scope.formValues.password;
}
$scope.state.backupInProgress = true;
BackupService.downloadBackup(payload)
.then(function success(data) {
const downloadData = new Blob([data.file], { type: 'application/gzip' });
FileSaver.saveAs(downloadData, data.name);
Notifications.success('Backup successfully downloaded');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to download backup');
})
.finally(function final() {
$scope.state.backupInProgress = false;
});
};
$scope.saveApplicationSettings = function () {
var settings = $scope.settings;