-
-
Notifications
You must be signed in to change notification settings - Fork 266
Backup hosts #2125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Boy132
wants to merge
24
commits into
main
Choose a base branch
from
boy132/backup-hosts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,037
−678
Open
Backup hosts #2125
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
dd4e723
start backup hosts
Boy132 ad2333e
more work on backup hosts
Boy132 12d8b23
Merge remote-tracking branch 'origin/main' into boy132/backup-hosts
Boy132 a181978
handle old backups and cleanup
Boy132 53761f8
dont allow to delete last backup host
Boy132 efebb99
fix backup hosts for "all nodes"
Boy132 150f803
fix tests
Boy132 f1dbbbb
fix migration for mysql
Boy132 7da9d8c
rabbit fixes
Boy132 6c8c2a0
Merge branch 'main' into boy132/backup-hosts
Boy132 9252b21
Merge branch 'main' into boy132/backup-hosts
Boy132 3a9f09c
run pint
Boy132 e3893ff
Merge branch 'main' into boy132/backup-hosts
Boy132 4f2a472
Merge branch 'main' into boy132/backup-hosts
Boy132 c215e95
Merge branch 'main' into boy132/backup-hosts
Boy132 bc727b7
use TablerIcon enum and update some actions
Boy132 a949a56
add info comment
Boy132 c3b597d
fix function name
Boy132 a6ba81e
small cleanup
Boy132 a9f6bcb
add simple BackupsRelationManager
Boy132 1a9cc5f
re-implement backup transfers
Boy132 f300259
Merge remote-tracking branch 'origin/main' into boy132/backup-hosts
Boy132 5057d72
rabbit fixes
Boy132 cdd69c2
rabbit fixes 2
Boy132 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
app/Extensions/BackupAdapter/BackupAdapterSchemaInterface.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?php | ||
|
|
||
| namespace App\Extensions\BackupAdapter; | ||
|
|
||
| use App\Models\Backup; | ||
| use App\Models\User; | ||
| use Filament\Schemas\Components\Component; | ||
|
|
||
| interface BackupAdapterSchemaInterface | ||
| { | ||
| public function getId(): string; | ||
|
|
||
| public function getName(): string; | ||
|
|
||
| public function createBackup(Backup $backup): void; | ||
|
|
||
| public function deleteBackup(Backup $backup): void; | ||
|
|
||
| public function getDownloadLink(Backup $backup, User $user): string; | ||
|
|
||
| /** @return Component[] */ | ||
| public function getConfigurationForm(): array; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <?php | ||
|
|
||
| namespace App\Extensions\BackupAdapter; | ||
|
|
||
| class BackupAdapterService | ||
| { | ||
| /** @var array<string, BackupAdapterSchemaInterface> */ | ||
| private array $schemas = []; | ||
|
|
||
| /** @return BackupAdapterSchemaInterface[] */ | ||
| public function getAll(): array | ||
| { | ||
| return $this->schemas; | ||
| } | ||
|
|
||
| public function get(string $id): ?BackupAdapterSchemaInterface | ||
| { | ||
| return array_get($this->schemas, $id); | ||
| } | ||
|
|
||
| public function register(BackupAdapterSchemaInterface $schema): void | ||
| { | ||
| if (array_key_exists($schema->getId(), $this->schemas)) { | ||
| return; | ||
| } | ||
|
|
||
| $this->schemas[$schema->getId()] = $schema; | ||
| } | ||
|
|
||
| /** @return array<string, string> */ | ||
| public function getMappings(): array | ||
| { | ||
| return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all(); | ||
| } | ||
| } |
14 changes: 14 additions & 0 deletions
14
app/Extensions/BackupAdapter/Schemas/BackupAdapterSchema.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
|
|
||
| namespace App\Extensions\BackupAdapter\Schemas; | ||
|
|
||
| use App\Extensions\BackupAdapter\BackupAdapterSchemaInterface; | ||
| use Illuminate\Support\Str; | ||
|
|
||
| abstract class BackupAdapterSchema implements BackupAdapterSchemaInterface | ||
| { | ||
| public function getName(): string | ||
| { | ||
| return Str::title($this->getId()); | ||
| } | ||
| } |
207 changes: 207 additions & 0 deletions
207
app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| <?php | ||
|
|
||
| namespace App\Extensions\BackupAdapter\Schemas; | ||
|
|
||
| use App\Enums\TablerIcon; | ||
| use App\Http\Controllers\Api\Remote\Backups\BackupRemoteUploadController; | ||
| use App\Models\Backup; | ||
| use App\Models\BackupHost; | ||
| use App\Models\User; | ||
| use App\Repositories\Daemon\DaemonBackupRepository; | ||
| use Aws\S3\S3Client; | ||
| use Carbon\CarbonImmutable; | ||
| use Exception; | ||
| use Filament\Forms\Components\TextInput; | ||
| use Filament\Forms\Components\Toggle; | ||
| use Filament\Schemas\Components\Component; | ||
| use Filament\Schemas\Components\StateCasts\BooleanStateCast; | ||
| use Illuminate\Support\Arr; | ||
|
|
||
| final class S3BackupSchema extends BackupAdapterSchema | ||
| { | ||
| public function __construct(private readonly DaemonBackupRepository $repository) {} | ||
|
|
||
| private function createClient(BackupHost $backupHost): S3Client | ||
| { | ||
| $config = $backupHost->configuration; | ||
| $config['version'] = 'latest'; | ||
|
|
||
| if (!empty($config['key']) && !empty($config['secret'])) { | ||
| $config['credentials'] = Arr::only($config, ['key', 'secret', 'token']); | ||
| } | ||
|
|
||
| return new S3Client($config); | ||
| } | ||
|
|
||
| public function getId(): string | ||
| { | ||
| return 's3'; | ||
| } | ||
|
|
||
| public function createBackup(Backup $backup): void | ||
| { | ||
| $this->repository->setServer($backup->server)->create($backup); | ||
| } | ||
|
|
||
| public function deleteBackup(Backup $backup): void | ||
| { | ||
| $client = $this->createClient($backup->backupHost); | ||
|
|
||
| $client->deleteObject([ | ||
| 'Bucket' => $backup->backupHost->configuration['bucket'], | ||
| 'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz", | ||
| ]); | ||
| } | ||
|
|
||
| public function getDownloadLink(Backup $backup, User $user): string | ||
| { | ||
| $client = $this->createClient($backup->backupHost); | ||
|
|
||
| $request = $client->createPresignedRequest( | ||
| $client->getCommand('GetObject', [ | ||
| 'Bucket' => $backup->backupHost->configuration['bucket'], | ||
| 'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz", | ||
| 'ContentType' => 'application/x-gzip', | ||
| ]), | ||
| CarbonImmutable::now()->addMinutes(5) | ||
| ); | ||
|
|
||
| return $request->getUri()->__toString(); | ||
| } | ||
|
|
||
| /** @return Component[] */ | ||
| public function getConfigurationForm(): array | ||
| { | ||
| return [ | ||
| TextInput::make('configuration.region') | ||
| ->label(trans('admin/setting.backup.s3.default_region')) | ||
| ->required(), | ||
| TextInput::make('configuration.key') | ||
| ->label(trans('admin/setting.backup.s3.access_key')) | ||
| ->required(), | ||
| TextInput::make('configuration.secret') | ||
| ->label(trans('admin/setting.backup.s3.secret_key')) | ||
| ->required(), | ||
| TextInput::make('configuration.bucket') | ||
| ->label(trans('admin/setting.backup.s3.bucket')) | ||
| ->required(), | ||
| TextInput::make('configuration.endpoint') | ||
| ->label(trans('admin/setting.backup.s3.endpoint')) | ||
| ->required(), | ||
| Toggle::make('configuration.use_path_style_endpoint') | ||
| ->label(trans('admin/setting.backup.s3.use_path_style_endpoint')) | ||
| ->inline(false) | ||
| ->onIcon(TablerIcon::Check) | ||
| ->offIcon(TablerIcon::X) | ||
| ->onColor('success') | ||
| ->offColor('danger') | ||
| ->live() | ||
| ->stateCast(new BooleanStateCast(false)), | ||
| ]; | ||
| } | ||
|
|
||
| /** @return array{parts: string[], part_size: int} */ | ||
| public function getUploadParts(Backup $backup, int $size): array | ||
| { | ||
| $expires = CarbonImmutable::now()->addMinutes(config('backups.presigned_url_lifespan', 60)); | ||
|
|
||
| // Params for generating the presigned urls | ||
| $params = [ | ||
| 'Bucket' => $backup->backupHost->configuration['bucket'], | ||
| 'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz", | ||
| 'ContentType' => 'application/x-gzip', | ||
| ]; | ||
|
|
||
| $storageClass = $backup->backupHost->configuration['storage_class']; | ||
| if (!is_null($storageClass)) { | ||
| $params['StorageClass'] = $storageClass; | ||
| } | ||
|
|
||
| $client = $this->createClient($backup->backupHost); | ||
|
|
||
| // Execute the CreateMultipartUpload request | ||
| $result = $client->execute($client->getCommand('CreateMultipartUpload', $params)); | ||
|
|
||
| // Get the UploadId from the CreateMultipartUpload request, this is needed to create | ||
| // the other presigned urls. | ||
| $params['UploadId'] = $result->get('UploadId'); | ||
|
|
||
| // Retrieve configured part size | ||
| $maxPartSize = config('backups.max_part_size', BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE); | ||
| if ($maxPartSize <= 0) { | ||
| $maxPartSize = BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE; | ||
| } | ||
|
|
||
| // Create as many UploadPart presigned urls as needed | ||
| $parts = []; | ||
| for ($i = 0; $i < ($size / $maxPartSize); $i++) { | ||
| $parts[] = $client->createPresignedRequest( | ||
| $client->getCommand('UploadPart', array_merge($params, ['PartNumber' => $i + 1])), | ||
| $expires | ||
| )->getUri()->__toString(); | ||
| } | ||
|
|
||
| // Set the upload_id on the backup in the database. | ||
| $backup->update(['upload_id' => $params['UploadId']]); | ||
|
|
||
| return [ | ||
| 'parts' => $parts, | ||
| 'part_size' => $maxPartSize, | ||
| ]; | ||
| } | ||
Boy132 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Marks a multipart upload in a given S3-compatible instance as failed or successful for the given backup. | ||
| * | ||
| * @param ?array<array{int, etag: string, part_number: string}> $parts | ||
| * | ||
| * @throws Exception | ||
| */ | ||
| public function completeMultipartUpload(Backup $backup, bool $successful, ?array $parts): void | ||
| { | ||
| // This should never really happen, but if it does don't let us fall victim to Amazon's | ||
| // wildly fun error messaging. Just stop the process right here. | ||
| if (empty($backup->upload_id)) { | ||
| // A failed backup doesn't need to error here, this can happen if the backup encounters | ||
| // an error before we even start the upload. AWS gives you tooling to clear these failed | ||
| // multipart uploads as needed too. | ||
| if (!$successful) { | ||
| return; | ||
| } | ||
|
|
||
| throw new Exception('Cannot complete backup request: no upload_id present on model.'); | ||
| } | ||
|
|
||
| $params = [ | ||
| 'Bucket' => $backup->backupHost->configuration['bucket'], | ||
| 'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz", | ||
| 'UploadId' => $backup->upload_id, | ||
| ]; | ||
|
|
||
| $client = $this->createClient($backup->backupHost); | ||
|
|
||
| if (!$successful) { | ||
| $client->execute($client->getCommand('AbortMultipartUpload', $params)); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // Otherwise send a CompleteMultipartUpload request. | ||
| $params['MultipartUpload'] = [ | ||
| 'Parts' => [], | ||
| ]; | ||
|
|
||
| if (is_null($parts)) { | ||
| $params['MultipartUpload']['Parts'] = $client->execute($client->getCommand('ListParts', $params))['Parts']; | ||
| } else { | ||
| foreach ($parts as $part) { | ||
| $params['MultipartUpload']['Parts'][] = [ | ||
| 'ETag' => $part['etag'], | ||
| 'PartNumber' => $part['part_number'], | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| $client->execute($client->getCommand('CompleteMultipartUpload', $params)); | ||
| } | ||
| } | ||
64 changes: 64 additions & 0 deletions
64
app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| <?php | ||
|
|
||
| namespace App\Extensions\BackupAdapter\Schemas; | ||
|
|
||
| use App\Models\Backup; | ||
| use App\Models\User; | ||
| use App\Repositories\Daemon\DaemonBackupRepository; | ||
| use App\Services\Nodes\NodeJWTService; | ||
| use Carbon\CarbonImmutable; | ||
| use Exception; | ||
| use Filament\Infolists\Components\TextEntry; | ||
| use Filament\Schemas\Components\Component; | ||
| use Illuminate\Http\Response; | ||
|
|
||
| final class WingsBackupSchema extends BackupAdapterSchema | ||
| { | ||
| public function __construct(private readonly DaemonBackupRepository $repository, private readonly NodeJWTService $jwtService) {} | ||
|
|
||
| public function getId(): string | ||
| { | ||
| return 'wings'; | ||
| } | ||
|
|
||
| public function createBackup(Backup $backup): void | ||
| { | ||
| $this->repository->setServer($backup->server)->create($backup); | ||
| } | ||
|
|
||
| /** @throws Exception */ | ||
| public function deleteBackup(Backup $backup): void | ||
| { | ||
| try { | ||
| $this->repository->setServer($backup->server)->delete($backup); | ||
| } catch (Exception $exception) { | ||
| // Don't fail the request if the Daemon responds with a 404, just assume the backup | ||
| // doesn't actually exist and remove its reference from the Panel as well. | ||
| if ($exception->getCode() !== Response::HTTP_NOT_FOUND) { | ||
| throw $exception; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public function getDownloadLink(Backup $backup, User $user): string | ||
| { | ||
| $token = $this->jwtService | ||
| ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) | ||
| ->setUser($user) | ||
| ->setClaims([ | ||
| 'backup_uuid' => $backup->uuid, | ||
| 'server_uuid' => $backup->server->uuid, | ||
| ]) | ||
| ->handle($backup->server->node, $user->id . $backup->server->uuid); | ||
|
|
||
| return $backup->server->node->getConnectionAddress() . '/download/backup?token=' . $token->toString(); | ||
| } | ||
|
|
||
| /** @return Component[] */ | ||
| public function getConfigurationForm(): array | ||
| { | ||
| return [ | ||
| TextEntry::make(trans('admin/backuphost.no_configuration')), | ||
| ]; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.