mirror of https://github.com/portainer/portainer
pull/4965/head
parent
6d8f5e7479
commit
fc9511dc97
|
@ -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)
|
||||
|
|
|
@ -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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -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;
|
||||
},
|
||||
]);
|
|
@ -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,
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue