Skip to content

Commit 4a697e4

Browse files
committed
feat: #228 allow to set team-wide backup retention setting
1 parent 7f788aa commit 4a697e4

19 files changed

+350
-11
lines changed

Diff for: _ide_helper_actions.php

+25-9
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,36 @@ class CreateService {}
6969
*/
7070
class StartDeployment {}
7171

72+
namespace App\Actions\Teams\Settings;
73+
74+
/**
75+
* @method static \Lorisleiva\Actions\Decorators\JobDecorator|\Lorisleiva\Actions\Decorators\UniqueJobDecorator makeJob(\App\Models\Team $team, int $retentionDays)
76+
* @method static \Lorisleiva\Actions\Decorators\UniqueJobDecorator makeUniqueJob(\App\Models\Team $team, int $retentionDays)
77+
* @method static \Illuminate\Foundation\Bus\PendingDispatch dispatch(\App\Models\Team $team, int $retentionDays)
78+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchIf(bool $boolean, \App\Models\Team $team, int $retentionDays)
79+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchUnless(bool $boolean, \App\Models\Team $team, int $retentionDays)
80+
* @method static dispatchSync(\App\Models\Team $team, int $retentionDays)
81+
* @method static dispatchNow(\App\Models\Team $team, int $retentionDays)
82+
* @method static dispatchAfterResponse(\App\Models\Team $team, int $retentionDays)
83+
* @method static void run(\App\Models\Team $team, int $retentionDays)
84+
*/
85+
class SaveTeamBackupSettings {}
86+
7287
namespace App\Actions\Workers;
7388

7489
/**
75-
* @method static \Lorisleiva\Actions\Decorators\JobDecorator|\Lorisleiva\Actions\Decorators\UniqueJobDecorator makeJob(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
76-
* @method static \Lorisleiva\Actions\Decorators\UniqueJobDecorator makeUniqueJob(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
77-
* @method static \Illuminate\Foundation\Bus\PendingDispatch dispatch(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
78-
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchIf(bool $boolean, \App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
79-
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchUnless(bool $boolean, \App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
80-
* @method static dispatchSync(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
81-
* @method static dispatchNow(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
82-
* @method static dispatchAfterResponse(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
83-
* @method static void run(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, \App\Models\Backup $backup = null)
90+
* @method static \Lorisleiva\Actions\Decorators\JobDecorator|\Lorisleiva\Actions\Decorators\UniqueJobDecorator makeJob(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
91+
* @method static \Lorisleiva\Actions\Decorators\UniqueJobDecorator makeUniqueJob(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
92+
* @method static \Illuminate\Foundation\Bus\PendingDispatch dispatch(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
93+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchIf(bool $boolean, \App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
94+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchUnless(bool $boolean, \App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
95+
* @method static dispatchSync(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
96+
* @method static dispatchNow(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
97+
* @method static dispatchAfterResponse(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
98+
* @method static void run(\App\Models\Service $service, \App\Models\DeploymentData\Process $process, \App\Models\DeploymentData\Worker $worker, ?\App\Models\Backup $backup = null)
8499
*/
85100
class ExecuteWorker {}
101+
class RemoveStaleBackups {}
86102

87103
namespace Lorisleiva\Actions\Concerns;
88104

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Actions\Teams\Settings;
4+
5+
use App\Models\Team;
6+
use Lorisleiva\Actions\ActionRequest;
7+
use Lorisleiva\Actions\Concerns\AsAction;
8+
9+
class SaveTeamBackupSettings
10+
{
11+
use AsAction;
12+
13+
public function handle(Team $team, int $retentionDays): void
14+
{
15+
$team->update(['backup_retention_days' => $retentionDays]);
16+
}
17+
18+
public function rules(): array
19+
{
20+
return [
21+
'retentionDays' => ['required', 'integer', 'min:1', 'max:30'],
22+
];
23+
}
24+
25+
public function asController(ActionRequest $request, Team $team): void
26+
{
27+
$this->handle($team, $request->input('retentionDays'));
28+
}
29+
}

Diff for: app/Actions/Workers/RemoveStaleBackups.php

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace App\Actions\Workers;
4+
5+
use App\Models\Backup;
6+
use App\Models\NodeTaskGroup;
7+
use App\Models\NodeTaskGroupType;
8+
use App\Models\NodeTasks\RemoveS3File\RemoveS3FileMeta;
9+
use App\Models\NodeTaskType;
10+
use App\Models\Service;
11+
use App\Models\Team;
12+
use Illuminate\Contracts\Queue\ShouldQueue;
13+
use Illuminate\Database\Eloquent\Collection;
14+
use Illuminate\Foundation\Queue\Queueable;
15+
use Illuminate\Support\Facades\Log;
16+
use Lorisleiva\Actions\Concerns\AsCommand;
17+
18+
class RemoveStaleBackups implements ShouldQueue
19+
{
20+
use AsCommand, Queueable;
21+
22+
public string $commandSignature = 'app:backups:remove-stale {--team=}';
23+
24+
public function handle(?Team $team): void
25+
{
26+
$teams = $team?->id ? collect([$team]) : Team::all();
27+
28+
$teams->each(function (Team $team) {
29+
$team->services()->each(function (Service $service) use ($team) {
30+
$service->backups()->where('created_at', '<', now()->subDays($team->backup_retention_days))->chunk(20, function (Collection $backups) use ($service) {
31+
$tasks = $backups->map(function (Backup $backup) use ($service) {
32+
$s3Storage = $service->swarm->data->findS3Storage($backup->s3_storage_id);
33+
if (! $s3Storage) {
34+
Log::warning('S3Storage not found for backup', ['backup_id' => $backup->id]);
35+
36+
return;
37+
}
38+
39+
return [
40+
'type' => NodeTaskType::RemoveS3File,
41+
'meta' => RemoveS3FileMeta::validateAndCreate([
42+
's3StorageId' => $s3Storage->id,
43+
'filePath' => $backup->dest_path,
44+
'backupId' => $backup->id,
45+
]),
46+
'payload' => [
47+
'S3StorageConfigName' => $s3Storage->dockerName,
48+
'FilePath' => $backup->dest_path,
49+
],
50+
];
51+
});
52+
53+
if (empty($tasks)) {
54+
return;
55+
}
56+
57+
$taskGroup = NodeTaskGroup::create([
58+
'type' => NodeTaskGroupType::BackupRemove,
59+
'node_id' => null,
60+
'team_id' => $service->team_id,
61+
'swarm_id' => $service->swarm_id,
62+
'invoker_id' => $service->latestDeployment->latestTaskGroup->invoker_id,
63+
]);
64+
65+
$taskGroup->tasks()->createMany($tasks);
66+
});
67+
});
68+
});
69+
}
70+
71+
public function asCommand($command)
72+
{
73+
$team = Team::findOrFail($command->option('team'));
74+
75+
$this->handle($team);
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace App\Events\NodeTasks\RemoveS3File;
4+
5+
use App\Events\NodeTasks\BaseTaskEvent;
6+
7+
class RemoveS3FileCompleted extends BaseTaskEvent {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace App\Events\NodeTasks\RemoveS3File;
4+
5+
use App\Events\NodeTasks\BaseTaskEvent;
6+
7+
class RemoveS3FileFailed extends BaseTaskEvent {}

Diff for: app/Http/Controllers/TeamSettingsController.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Models\Team;
6+
use Inertia\Inertia;
7+
8+
class TeamSettingsController extends Controller
9+
{
10+
public function index(Team $team)
11+
{
12+
return Inertia::render('Teams/Settings', [
13+
'team' => $team,
14+
]);
15+
}
16+
}

Diff for: app/Listeners/RecordBackupStatus.php

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Events\NodeTaskGroups\BackupCreate\BackupCreateCompleted;
66
use App\Events\NodeTaskGroups\BackupCreate\BackupCreateFailed;
7+
use App\Events\NodeTasks\RemoveS3File\RemoveS3FileCompleted;
78
use App\Models\Backup;
89
use App\Models\BackupStatus;
910
use Illuminate\Events\Dispatcher;
@@ -23,6 +24,7 @@ public function subscribe(Dispatcher $dispatcher): array
2324
return [
2425
BackupCreateCompleted::class => 'handleCompleted',
2526
BackupCreateFailed::class => 'handleFailed',
27+
RemoveS3FileCompleted::class => 'handleRemoveS3FileCompleted',
2628
];
2729
}
2830

@@ -38,4 +40,13 @@ public function handleFailed(BackupCreateFailed $event): void
3840
{
3941
Backup::where('task_group_id', $event->taskGroup->id)->update(['status' => BackupStatus::Failed, 'ended_at' => now()]);
4042
}
43+
44+
public function handleRemoveS3FileCompleted(RemoveS3FileCompleted $event): void
45+
{
46+
if (! $event->task->meta->backupId) {
47+
return;
48+
}
49+
50+
Backup::where('id', $event->task->meta->backupId)->delete();
51+
}
4152
}

Diff for: app/Models/NodeTaskGroupType.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ enum NodeTaskGroupType: int
1414
case SelfUpgrade = 4;
1515
case UpdateDockerRegistries = 5;
1616
case UpdateS3Storages = 6;
17-
case CreateBackup = 7;
17+
case __Unused__Safe_To_Re_Use__ = 7;
1818
case JoinSwarm = 8;
1919
case UpdateDirdConfig = 9;
2020
case LaunchService = 10;
2121
case ExecuteWorker = 11;
2222
case BackupCreate = 12;
2323
case BackupRestore = 13;
24+
case BackupRemove = 14;
2425

2526
public function completed(): ?string
2627
{

Diff for: app/Models/NodeTaskType.php

+9
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
use App\Events\NodeTasks\PullDockerImage\PullDockerImageFailed;
3737
use App\Events\NodeTasks\RebuildCaddyConfig\ApplyCaddyConfigCompleted;
3838
use App\Events\NodeTasks\RebuildCaddyConfig\ApplyCaddyConfigFailed;
39+
use App\Events\NodeTasks\RemoveS3File\RemoveS3FileCompleted;
40+
use App\Events\NodeTasks\RemoveS3File\RemoveS3FileFailed;
3941
use App\Events\NodeTasks\ServiceExec\ServiceExecCompleted;
4042
use App\Events\NodeTasks\ServiceExec\ServiceExecFailed;
4143
use App\Events\NodeTasks\UpdateAgentSymlink\UpdateAgentSymlinkCompleted;
@@ -82,6 +84,8 @@
8284
use App\Models\NodeTasks\LaunchService\LaunchServiceResult;
8385
use App\Models\NodeTasks\PullDockerImage\PullDockerImageMeta;
8486
use App\Models\NodeTasks\PullDockerImage\PullDockerImageResult;
87+
use App\Models\NodeTasks\RemoveS3File\RemoveS3FileMeta;
88+
use App\Models\NodeTasks\RemoveS3File\RemoveS3FileResult;
8589
use App\Models\NodeTasks\ServiceExec\ServiceExecMeta;
8690
use App\Models\NodeTasks\ServiceExec\ServiceExecResult;
8791
use App\Models\NodeTasks\UpdateAgentSymlink\UpdateAgentSymlinkMeta;
@@ -121,6 +125,7 @@ enum NodeTaskType: int
121125
case UpdateDirdConfig = 20;
122126
case LaunchService = 21;
123127
case DownloadS3File = 22;
128+
case RemoveS3File = 23;
124129

125130
public function meta(): string
126131
{
@@ -148,6 +153,7 @@ public function meta(): string
148153
self::UpdateDirdConfig => UpdateDirdConfigMeta::class,
149154
self::LaunchService => LaunchServiceMeta::class,
150155
self::DownloadS3File => DownloadS3FileMeta::class,
156+
self::RemoveS3File => RemoveS3FileMeta::class,
151157
};
152158
}
153159

@@ -177,6 +183,7 @@ public function result(): string
177183
self::UpdateDirdConfig => UpdateDirdConfigResult::class,
178184
self::LaunchService => LaunchServiceResult::class,
179185
self::DownloadS3File => DownloadS3FileResult::class,
186+
self::RemoveS3File => RemoveS3FileResult::class,
180187
};
181188
}
182189

@@ -206,6 +213,7 @@ public function completed(): string
206213
self::UpdateDirdConfig => UpdateDirdConfigCompleted::class,
207214
self::LaunchService => LaunchServiceCompleted::class,
208215
self::DownloadS3File => DownloadS3FileCompleted::class,
216+
self::RemoveS3File => RemoveS3FileCompleted::class,
209217
};
210218
}
211219

@@ -235,6 +243,7 @@ public function failed(): string
235243
self::UpdateDirdConfig => UpdateDirdConfigFailed::class,
236244
self::LaunchService => LaunchServiceFailed::class,
237245
self::DownloadS3File => DownloadS3FileFailed::class,
246+
self::RemoveS3File => RemoveS3FileFailed::class,
238247
};
239248
}
240249
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace App\Models\NodeTasks\RemoveS3File;
4+
5+
use App\Models\NodeTasks\AbstractTaskMeta;
6+
7+
class RemoveS3FileMeta extends AbstractTaskMeta
8+
{
9+
public function __construct(
10+
public string $s3StorageId,
11+
public string $filePath,
12+
public ?int $backupId = null,
13+
) {
14+
//
15+
}
16+
17+
public function formattedHtml(): string
18+
{
19+
return "Remove S3 File {$this->filePath}";
20+
}
21+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace App\Models\NodeTasks\RemoveS3File;
4+
5+
use App\Models\NodeTasks\AbstractTaskResult;
6+
7+
class RemoveS3FileResult extends AbstractTaskResult
8+
{
9+
public function __construct(
10+
) {
11+
//
12+
}
13+
14+
public function formattedHtml(): string
15+
{
16+
return 'RemoveS3File - Task Result';
17+
}
18+
}

Diff for: app/Models/Service.php

+5
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public function tasks(): HasMany
112112
return $this->hasMany(NodeTask::class, 'meta__service_id', 'id');
113113
}
114114

115+
public function backups(): HasMany
116+
{
117+
return $this->hasMany(Backup::class);
118+
}
119+
115120
public function makeResourceName($name): string
116121
{
117122
return dockerize_name('svc_'.$this->id.'_'.$name);

Diff for: app/Models/Team.php

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Team extends JetstreamTeam
3333
'personal_team',
3434
'billing_name',
3535
'billing_email',
36+
'backup_retention_days',
3637
];
3738

3839
protected $appends = [

Diff for: bootstrap/app.php

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use ApiNodes\Http\Middleware\AgentTokenAuth;
44
use App\Actions\Workers\ExecuteWorker;
5+
use App\Actions\Workers\RemoveStaleBackups;
56
use App\Http\Middleware\AdminAccess;
67
use App\Http\Middleware\HandleInertiaRequests;
78
use App\Jobs\CheckAgentUpdates;
@@ -66,6 +67,11 @@
6667
->onOneServer()
6768
->withoutOverlapping();
6869

70+
$schedule->job(RemoveStaleBackups::class)
71+
->hourly()
72+
->onOneServer()
73+
->withoutOverlapping();
74+
6975
Service::withoutGlobalScope(TeamScope::class)->with(['latestDeployment'])->chunk(100, function (
7076
/* @var Service[] $services */
7177
$services

Diff for: database/migrations/2024_10_10_183624_create_backups_table.php

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public function up(): void
2020
$table->string('worker');
2121
$table->string('s3_storage_id');
2222
$table->string('dest_path');
23-
$table->string('archive_format')->nullable();
2423
$table->enum('status', ['in_progress', 'succeeded', 'failed']);
2524
$table->timestamp('started_at')->nullable();
2625
$table->timestamp('ended_at')->nullable();

0 commit comments

Comments
 (0)