Skip to content

Commit a51160e

Browse files
committed
feat: #227 add backups page
1 parent dcdcbc3 commit a51160e

25 files changed

+500
-75
lines changed

Diff for: api-nodes/Http/Controllers/NextTaskController.php

+9-4
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,15 @@ public function __invoke(Node $node)
3939

4040
protected function getNextTaskFromGroup(Node $node, NodeTaskGroup $taskGroup)
4141
{
42-
if ($taskGroup->tasks()->running()->first()) {
43-
return new Response([
44-
'error_message' => 'Another task should be already running.',
45-
], 409);
42+
$runningTask = $taskGroup->tasks()->running()->first();
43+
if ($runningTask) {
44+
// FIXME: log an error/warning, display a message to the user
45+
46+
return $runningTask;
47+
// FIXME: consider reverting the solution to the previous version below
48+
// return new Response([
49+
// 'error_message' => 'Another task should be already running.',
50+
// ], 409);
4651
}
4752

4853
$task = $taskGroup->tasks()->pending()->first();

Diff for: app/Actions/Nodes/InitCluster.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private function getCaddyProcessConfig(Node $node): array
203203
'dockerName' => 'caddy',
204204
'command' => 'sh /start.sh',
205205
'replicas' => 1,
206-
'launchMode' => LaunchMode::Daemon,
206+
'launchMode' => LaunchMode::Daemon->value,
207207
'schedule' => null,
208208
'releaseCommand' => [
209209
'command' => null,

Diff for: app/Console/Commands/ExecuteScheduledWorker.php

+36-12
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace App\Console\Commands;
44

5+
use App\Models\Backup;
6+
use App\Models\BackupStatus;
57
use App\Models\Node;
68
use App\Models\NodeTaskGroup;
79
use App\Models\NodeTaskGroupType;
10+
use App\Models\NodeTasks\UploadS3File\UploadS3FileMeta;
811
use App\Models\NodeTaskType;
912
use App\Models\Service;
1013
use Exception;
@@ -43,8 +46,12 @@ protected function executeWorker(): void
4346

4447
$node = $process->placementNodeId ? Node::findOrFail($process->placementNodeId) : null;
4548

49+
$taskGroupType = $worker->backupCreate
50+
? NodeTaskGroupType::BackupCreate
51+
: NodeTaskGroupType::ExecuteScheduledWorker;
52+
4653
$taskGroup = NodeTaskGroup::create([
47-
'type' => NodeTaskGroupType::LaunchService,
54+
'type' => $taskGroupType,
4855
'swarm_id' => $service->swarm_id,
4956
'node_id' => $node->id,
5057
'invoker_id' => $deployment->latestTaskGroup->invoker_id,
@@ -58,41 +65,58 @@ protected function executeWorker(): void
5865
...$worker->asNodeTasks($deployment, $process, desiredReplicas: 1),
5966
];
6067

61-
if ($worker->backupOptions) {
62-
$s3Storage = $node->swarm->data->findS3Storage($worker->backupOptions->s3StorageId);
68+
if ($worker->backupCreate) {
69+
$s3Storage = $node->swarm->data->findS3Storage($worker->backupCreate->s3StorageId);
6370
if ($s3Storage === null) {
64-
throw new Exception("Could not find S3 storage {$worker->backupOptions->s3StorageId} in swarm {$node->swarm_id}.");
71+
throw new Exception("Could not find S3 storage {$worker->backupCreate->s3StorageId} in swarm {$node->swarm_id}.");
6572
}
6673

67-
$archiveFormat = $worker->backupOptions->archive?->format->value;
74+
$archiveFormat = $worker->backupCreate->archive?->format->value;
6875

6976
$date = now()->format('Y-m-d_His');
7077

7178
$ext = $archiveFormat ? ".$archiveFormat" : '';
72-
$backupFilePath = "/{$service->slug}/{$process->name}/{$worker->name}/{$service->slug}-{$process->name}-{$worker->name}-{$date}$ext";
79+
$backupFilePath = "/{$service->slug}/{$process->name}/{$worker->name}/{$service->slug}-{$process->name}-{$worker->name}-{$date}{$ext}";
7380

7481
$tasks[] = [
7582
'type' => NodeTaskType::UploadS3File,
76-
'meta' => [
83+
'meta' => UploadS3FileMeta::validateAndCreate([
7784
'serviceId' => $service->id,
7885
'destPath' => $backupFilePath,
79-
],
86+
]),
8087
'payload' => [
8188
'Archive' => [
82-
'Enabled' => $worker->backupOptions->archive !== null,
89+
'Enabled' => $worker->backupCreate->archive !== null,
8390
'Format' => $archiveFormat,
8491
],
8592
'S3StorageConfigName' => $s3Storage->dockerName,
8693
'VolumeSpec' => [
8794
'Type' => 'volume',
88-
'Source' => $worker->backupOptions->backupVolume->dockerName,
89-
'Target' => $worker->backupOptions->backupVolume->path,
95+
'Source' => $worker->backupCreate->backupVolume->dockerName,
96+
'Target' => $worker->backupCreate->backupVolume->path,
9097
],
91-
'SrcFilePath' => $worker->backupOptions->backupVolume->path,
98+
'SrcFilePath' => $worker->backupCreate->backupVolume->path,
9299
'DestFilePath' => $backupFilePath,
93100
'RemoveSrcFile' => true,
94101
],
95102
];
103+
104+
// FIXME: what to do with backups which are "in progress" now for the same worker?
105+
$backup = new Backup;
106+
107+
$backup->forceFill([
108+
'team_id' => $service->team_id,
109+
'task_group_id' => $taskGroup->id,
110+
'service_id' => $service->id,
111+
'process' => $process->name,
112+
'worker' => $worker->name,
113+
's3_storage_id' => $s3Storage->id,
114+
'dest_path' => $backupFilePath,
115+
'status' => BackupStatus::InProgress,
116+
'started_at' => now(),
117+
]);
118+
119+
$backup->save();
96120
}
97121

98122
$taskGroup->tasks()->createMany($tasks);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace App\Events\NodeTaskGroups\BackupCreate;
4+
5+
use App\Events\NodeTaskGroups\BaseTaskGroupEvent;
6+
7+
class BackupCreateCompleted extends BaseTaskGroupEvent {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace App\Events\NodeTaskGroups\BackupCreate;
4+
5+
use App\Events\NodeTaskGroups\BaseTaskGroupEvent;
6+
7+
class BackupCreateFailed extends BaseTaskGroupEvent {}

Diff for: app/Events/NodeTaskGroups/BaseTaskGroupEvent.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Events\NodeTaskGroups;
4+
5+
use App\Models\NodeTaskGroup;
6+
use Illuminate\Broadcasting\InteractsWithSockets;
7+
use Illuminate\Broadcasting\PrivateChannel;
8+
use Illuminate\Foundation\Events\Dispatchable;
9+
use Illuminate\Queue\SerializesModels;
10+
11+
class BaseTaskGroupEvent
12+
{
13+
use Dispatchable, InteractsWithSockets, SerializesModels;
14+
15+
/**
16+
* Create a new event instance.
17+
*/
18+
public function __construct(public NodeTaskGroup $taskGroup)
19+
{
20+
//
21+
}
22+
23+
/**
24+
* Get the channels the event should broadcast on.
25+
*
26+
* @return array<int, \Illuminate\Broadcasting\Channel>
27+
*/
28+
public function broadcastOn(): array
29+
{
30+
return [
31+
new PrivateChannel('channel-name'),
32+
];
33+
}
34+
}

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

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Models\Backup;
6+
use App\Models\Service;
7+
use Inertia\Inertia;
8+
use Inertia\Response;
9+
10+
class ServiceBackupController extends Controller
11+
{
12+
public function index(Service $service): Response
13+
{
14+
$backups = Backup::where('service_id', $service->id)->latest()->paginate();
15+
$s3Storages = $service->swarm->data->s3Storages;
16+
17+
return Inertia::render('Services/Backups', ['service' => $service, 'backups' => $backups, 's3Storages' => $s3Storages]);
18+
}
19+
}

Diff for: app/Listeners/RecordBackupStatus.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace App\Listeners;
4+
5+
use App\Events\NodeTaskGroups\BackupCreate\BackupCreateCompleted;
6+
use App\Events\NodeTaskGroups\BackupCreate\BackupCreateFailed;
7+
use App\Models\Backup;
8+
use App\Models\BackupStatus;
9+
use Illuminate\Events\Dispatcher;
10+
11+
class RecordBackupStatus
12+
{
13+
/**
14+
* Create the event listener.
15+
*/
16+
public function __construct()
17+
{
18+
//
19+
}
20+
21+
public function subscribe(Dispatcher $dispatcher): array
22+
{
23+
return [
24+
BackupCreateCompleted::class => 'handleCompleted',
25+
BackupCreateFailed::class => 'handleFailed',
26+
];
27+
}
28+
29+
/**
30+
* Handle the event.
31+
*/
32+
public function handleCompleted(BackupCreateCompleted $event): void
33+
{
34+
Backup::where('task_group_id', $event->taskGroup->id)->update(['status' => BackupStatus::Succeeded, 'ended_at' => now()]);
35+
}
36+
37+
public function handleFailed(BackupCreateFailed $event): void
38+
{
39+
Backup::where('task_group_id', $event->taskGroup->id)->update(['status' => BackupStatus::Failed, 'ended_at' => now()]);
40+
}
41+
}

Diff for: app/Models/Backup.php

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use App\Traits\HasOwningTeam;
6+
use Illuminate\Database\Eloquent\Factories\HasFactory;
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
class Backup extends Model
10+
{
11+
use HasFactory;
12+
use HasOwningTeam;
13+
}

Diff for: app/Models/BackupStatus.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
enum BackupStatus: string
6+
{
7+
case InProgress = 'in_progress';
8+
case Succeeded = 'succeeded';
9+
case Failed = 'failed';
10+
}

Diff for: app/Models/DeploymentData/BackupOptions.php renamed to app/Models/DeploymentData/BackupCreateOptions.php

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

55
use Spatie\LaravelData\Data;
66

7-
class BackupOptions extends Data
7+
class BackupCreateOptions extends Data
88
{
99
public function __construct(
1010
public string $s3StorageId,

Diff for: app/Models/DeploymentData/BackupRestoreOptions.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace App\Models\DeploymentData;
4+
5+
use Spatie\LaravelData\Data;
6+
7+
class BackupRestoreOptions extends Data
8+
{
9+
public function __construct(
10+
public ?Volume $restoreVolume,
11+
) {}
12+
}

Diff for: app/Models/DeploymentData/LaunchMode.php

+2-5
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ public function isDaemon(): bool
2626
return $this === self::Daemon;
2727
}
2828

29-
public function isBackup(): bool
30-
{
31-
return $this === self::BackupCreate || $this === self::BackupRestore;
32-
}
33-
3429
public function maxInitialReplicas(): int
3530
{
3631
if ($this->isDaemon()) {
@@ -40,3 +35,5 @@ public function maxInitialReplicas(): int
4035
return 0;
4136
}
4237
}
38+
39+
const CRONJOB_LAUNCH_MODES = [LaunchMode::Cronjob, LaunchMode::BackupCreate];

Diff for: app/Models/DeploymentData/Worker.php

+32-12
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
use Illuminate\Support\Str;
1212
use Spatie\LaravelData\Attributes\Validation\Enum;
1313
use Spatie\LaravelData\Attributes\Validation\Min;
14-
use Spatie\LaravelData\Attributes\Validation\ProhibitedIf;
15-
use Spatie\LaravelData\Attributes\Validation\RequiredUnless;
14+
use Spatie\LaravelData\Attributes\Validation\ProhibitedUnless;
15+
use Spatie\LaravelData\Attributes\Validation\RequiredIf;
1616
use Spatie\LaravelData\Attributes\Validation\Rule;
1717
use Spatie\LaravelData\Data;
1818

@@ -28,12 +28,14 @@ public function __construct(
2828
public int $replicas,
2929
#[Enum(LaunchMode::class)]
3030
public LaunchMode $launchMode,
31-
#[RequiredUnless('launchMode', [LaunchMode::Daemon, LaunchMode::Manual]), ProhibitedIf('launchMode', [LaunchMode::Daemon, LaunchMode::Manual]), Rule(new Crontab)]
31+
#[RequiredIf('launchMode', CRONJOB_LAUNCH_MODES), ProhibitedUnless('launchMode', CRONJOB_LAUNCH_MODES), Rule(new Crontab)]
3232
public ?string $crontab,
3333
public ReleaseCommand $releaseCommand,
3434
public Healthcheck $healthcheck,
35-
#[RequiredUnless('launchMode', [LaunchMode::Daemon, LaunchMode::Manual]), ProhibitedIf('launchMode', [LaunchMode::Daemon, LaunchMode::Manual])]
36-
public ?BackupOptions $backupOptions,
35+
#[RequiredIf('launchMode', LaunchMode::BackupCreate), ProhibitedUnless('launchMode', LaunchMode::BackupCreate)]
36+
public ?BackupCreateOptions $backupCreate,
37+
#[RequiredIf('launchMode', LaunchMode::BackupRestore), ProhibitedUnless('launchMode', LaunchMode::BackupRestore)]
38+
public ?BackupRestoreOptions $backupRestore,
3739
) {
3840
$maxReplicas = $this->launchMode->maxReplicas();
3941
if ($this->replicas > $maxReplicas) {
@@ -60,14 +62,25 @@ public function asNodeTasks(Deployment $deployment, Process $process, bool $pull
6062
$this->dockerName = $process->makeResourceName('wkr_'.$this->name);
6163
}
6264

63-
if ($this->launchMode->isBackup() && ! $this->backupOptions->backupVolume) {
65+
if ($this->launchMode->value === LaunchMode::BackupCreate->value && ! $this->backupCreate->backupVolume) {
6466
$dockerName = dockerize_name($this->dockerName.'_vol_ptah_backup');
6567

66-
$this->backupOptions->backupVolume = Volume::validateAndCreate([
68+
$this->backupCreate->backupVolume = Volume::validateAndCreate([
6769
'id' => 'volume-'.Str::random(11),
6870
'name' => $dockerName,
6971
'dockerName' => $dockerName,
70-
'path' => '/ptah/backups',
72+
'path' => '/ptah/backup/create',
73+
]);
74+
}
75+
76+
if ($this->launchMode->value === LaunchMode::BackupRestore->value && ! $this->backupRestore->restoreVolume) {
77+
$dockerName = dockerize_name($this->dockerName.'_vol_ptah_restore');
78+
79+
$this->backupRestore->restoreVolume = Volume::validateAndCreate([
80+
'id' => 'volume-'.Str::random(11),
81+
'name' => $dockerName,
82+
'dockerName' => $dockerName,
83+
'path' => '/ptah/backup/restore',
7184
]);
7285
}
7386

@@ -91,6 +104,8 @@ public function asNodeTasks(Deployment $deployment, Process $process, bool $pull
91104
'serviceId' => $deployment->service_id,
92105
'serviceName' => $deployment->service->name,
93106
'dockerName' => $this->dockerName,
107+
'processName' => $process->name,
108+
'workerName' => $this->name,
94109
]),
95110
'payload' => [
96111
'ReleaseCommand' => $this->getReleaseCommandPayload($process, $labels),
@@ -216,8 +231,12 @@ private function getMounts(Deployment $deployment, Process $process, array $labe
216231
{
217232
$mounts = $process->getMounts($deployment);
218233

219-
if ($this->backupOptions) {
220-
$mounts[] = $this->backupOptions->backupVolume->asMount($labels);
234+
if ($this->backupCreate) {
235+
$mounts[] = $this->backupCreate->backupVolume->asMount($labels);
236+
}
237+
238+
if ($this->backupRestore) {
239+
$mounts[] = $this->backupRestore->restoreVolume->asMount($labels);
221240
}
222241

223242
return $mounts;
@@ -269,10 +288,11 @@ private function getEnvVars(Deployment $deployment, Process $process): array
269288
'value' => $this->getHostname($deployment, $process),
270289
]);
271290

272-
if ($this->backupOptions) {
291+
$backupVolume = $this->backupCreate?->backupVolume ?? $this->backupRestore?->restoreVolume;
292+
if ($backupVolume) {
273293
$envVars[] = EnvVar::validateAndCreate([
274294
'name' => 'PTAH_BACKUP_DIR',
275-
'value' => $this->backupOptions->backupVolume->path,
295+
'value' => $backupVolume->path,
276296
]);
277297
}
278298

0 commit comments

Comments
 (0)