Skip to content

Commit

Permalink
feat: #227 add backups page
Browse files Browse the repository at this point in the history
  • Loading branch information
bohdan-shulha committed Oct 15, 2024
1 parent 1f585b7 commit f0b8aac
Show file tree
Hide file tree
Showing 25 changed files with 500 additions and 75 deletions.
13 changes: 9 additions & 4 deletions api-nodes/Http/Controllers/NextTaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ public function __invoke(Node $node)

protected function getNextTaskFromGroup(Node $node, NodeTaskGroup $taskGroup)
{
if ($taskGroup->tasks()->running()->first()) {
return new Response([
'error_message' => 'Another task should be already running.',
], 409);
$runningTask = $taskGroup->tasks()->running()->first();
if ($runningTask) {
// FIXME: log an error/warning, display a message to the user

return $runningTask;
// FIXME: consider reverting the solution to the previous version below
// return new Response([
// 'error_message' => 'Another task should be already running.',
// ], 409);
}

$task = $taskGroup->tasks()->pending()->first();
Expand Down
2 changes: 1 addition & 1 deletion app/Actions/Nodes/InitCluster.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ private function getCaddyProcessConfig(Node $node): array
'dockerName' => 'caddy',
'command' => 'sh /start.sh',
'replicas' => 1,
'launchMode' => LaunchMode::Daemon,
'launchMode' => LaunchMode::Daemon->value,
'schedule' => null,
'releaseCommand' => [
'command' => null,
Expand Down
48 changes: 36 additions & 12 deletions app/Console/Commands/ExecuteScheduledWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace App\Console\Commands;

use App\Models\Backup;
use App\Models\BackupStatus;
use App\Models\Node;
use App\Models\NodeTaskGroup;
use App\Models\NodeTaskGroupType;
use App\Models\NodeTasks\UploadS3File\UploadS3FileMeta;
use App\Models\NodeTaskType;
use App\Models\Service;
use Exception;
Expand Down Expand Up @@ -43,8 +46,12 @@ protected function executeWorker(): void

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

$taskGroupType = $worker->backupCreate
? NodeTaskGroupType::BackupCreate
: NodeTaskGroupType::ExecuteScheduledWorker;

$taskGroup = NodeTaskGroup::create([
'type' => NodeTaskGroupType::LaunchService,
'type' => $taskGroupType,
'swarm_id' => $service->swarm_id,
'node_id' => $node->id,
'invoker_id' => $deployment->latestTaskGroup->invoker_id,
Expand All @@ -58,41 +65,58 @@ protected function executeWorker(): void
...$worker->asNodeTasks($deployment, $process, desiredReplicas: 1),
];

if ($worker->backupOptions) {
$s3Storage = $node->swarm->data->findS3Storage($worker->backupOptions->s3StorageId);
if ($worker->backupCreate) {
$s3Storage = $node->swarm->data->findS3Storage($worker->backupCreate->s3StorageId);
if ($s3Storage === null) {
throw new Exception("Could not find S3 storage {$worker->backupOptions->s3StorageId} in swarm {$node->swarm_id}.");
throw new Exception("Could not find S3 storage {$worker->backupCreate->s3StorageId} in swarm {$node->swarm_id}.");
}

$archiveFormat = $worker->backupOptions->archive?->format->value;
$archiveFormat = $worker->backupCreate->archive?->format->value;

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

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

$tasks[] = [
'type' => NodeTaskType::UploadS3File,
'meta' => [
'meta' => UploadS3FileMeta::validateAndCreate([
'serviceId' => $service->id,
'destPath' => $backupFilePath,
],
]),
'payload' => [
'Archive' => [
'Enabled' => $worker->backupOptions->archive !== null,
'Enabled' => $worker->backupCreate->archive !== null,
'Format' => $archiveFormat,
],
'S3StorageConfigName' => $s3Storage->dockerName,
'VolumeSpec' => [
'Type' => 'volume',
'Source' => $worker->backupOptions->backupVolume->dockerName,
'Target' => $worker->backupOptions->backupVolume->path,
'Source' => $worker->backupCreate->backupVolume->dockerName,
'Target' => $worker->backupCreate->backupVolume->path,
],
'SrcFilePath' => $worker->backupOptions->backupVolume->path,
'SrcFilePath' => $worker->backupCreate->backupVolume->path,
'DestFilePath' => $backupFilePath,
'RemoveSrcFile' => true,
],
];

// FIXME: what to do with backups which are "in progress" now for the same worker?
$backup = new Backup;

$backup->forceFill([
'team_id' => $service->team_id,
'task_group_id' => $taskGroup->id,
'service_id' => $service->id,
'process' => $process->name,
'worker' => $worker->name,
's3_storage_id' => $s3Storage->id,
'dest_path' => $backupFilePath,
'status' => BackupStatus::InProgress,
'started_at' => now(),
]);

$backup->save();
}

$taskGroup->tasks()->createMany($tasks);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace App\Events\NodeTaskGroups\BackupCreate;

use App\Events\NodeTaskGroups\BaseTaskGroupEvent;

class BackupCreateCompleted extends BaseTaskGroupEvent {}
7 changes: 7 additions & 0 deletions app/Events/NodeTaskGroups/BackupCreate/BackupCreateFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace App\Events\NodeTaskGroups\BackupCreate;

use App\Events\NodeTaskGroups\BaseTaskGroupEvent;

class BackupCreateFailed extends BaseTaskGroupEvent {}
34 changes: 34 additions & 0 deletions app/Events/NodeTaskGroups/BaseTaskGroupEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Events\NodeTaskGroups;

use App\Models\NodeTaskGroup;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class BaseTaskGroupEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;

/**
* Create a new event instance.
*/
public function __construct(public NodeTaskGroup $taskGroup)
{
//
}

/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}
19 changes: 19 additions & 0 deletions app/Http/Controllers/ServiceBackupController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Http\Controllers;

use App\Models\Backup;
use App\Models\Service;
use Inertia\Inertia;
use Inertia\Response;

class ServiceBackupController extends Controller
{
public function index(Service $service): Response
{
$backups = Backup::where('service_id', $service->id)->latest()->paginate();
$s3Storages = $service->swarm->data->s3Storages;

return Inertia::render('Services/Backups', ['service' => $service, 'backups' => $backups, 's3Storages' => $s3Storages]);
}
}
41 changes: 41 additions & 0 deletions app/Listeners/RecordBackupStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace App\Listeners;

use App\Events\NodeTaskGroups\BackupCreate\BackupCreateCompleted;
use App\Events\NodeTaskGroups\BackupCreate\BackupCreateFailed;
use App\Models\Backup;
use App\Models\BackupStatus;
use Illuminate\Events\Dispatcher;

class RecordBackupStatus
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}

public function subscribe(Dispatcher $dispatcher): array
{
return [
BackupCreateCompleted::class => 'handleCompleted',
BackupCreateFailed::class => 'handleFailed',
];
}

/**
* Handle the event.
*/
public function handleCompleted(BackupCreateCompleted $event): void
{
Backup::where('task_group_id', $event->taskGroup->id)->update(['status' => BackupStatus::Succeeded, 'ended_at' => now()]);
}

public function handleFailed(BackupCreateFailed $event): void
{
Backup::where('task_group_id', $event->taskGroup->id)->update(['status' => BackupStatus::Failed, 'ended_at' => now()]);
}
}
13 changes: 13 additions & 0 deletions app/Models/Backup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Models;

use App\Traits\HasOwningTeam;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Backup extends Model
{
use HasFactory;
use HasOwningTeam;
}
10 changes: 10 additions & 0 deletions app/Models/BackupStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Models;

enum BackupStatus: string
{
case InProgress = 'in_progress';
case Succeeded = 'succeeded';
case Failed = 'failed';
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Spatie\LaravelData\Data;

class BackupOptions extends Data
class BackupCreateOptions extends Data
{
public function __construct(
public string $s3StorageId,
Expand Down
12 changes: 12 additions & 0 deletions app/Models/DeploymentData/BackupRestoreOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace App\Models\DeploymentData;

use Spatie\LaravelData\Data;

class BackupRestoreOptions extends Data
{
public function __construct(
public ?Volume $restoreVolume,
) {}
}
7 changes: 2 additions & 5 deletions app/Models/DeploymentData/LaunchMode.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ public function isDaemon(): bool
return $this === self::Daemon;
}

public function isBackup(): bool
{
return $this === self::BackupCreate || $this === self::BackupRestore;
}

public function maxInitialReplicas(): int
{
if ($this->isDaemon()) {
Expand All @@ -40,3 +35,5 @@ public function maxInitialReplicas(): int
return 0;
}
}

const CRONJOB_LAUNCH_MODES = [LaunchMode::Cronjob, LaunchMode::BackupCreate];
44 changes: 32 additions & 12 deletions app/Models/DeploymentData/Worker.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
use Illuminate\Support\Str;
use Spatie\LaravelData\Attributes\Validation\Enum;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\ProhibitedIf;
use Spatie\LaravelData\Attributes\Validation\RequiredUnless;
use Spatie\LaravelData\Attributes\Validation\ProhibitedUnless;
use Spatie\LaravelData\Attributes\Validation\RequiredIf;
use Spatie\LaravelData\Attributes\Validation\Rule;
use Spatie\LaravelData\Data;

Expand All @@ -28,12 +28,14 @@ public function __construct(
public int $replicas,
#[Enum(LaunchMode::class)]
public LaunchMode $launchMode,
#[RequiredUnless('launchMode', [LaunchMode::Daemon, LaunchMode::Manual]), ProhibitedIf('launchMode', [LaunchMode::Daemon, LaunchMode::Manual]), Rule(new Crontab)]
#[RequiredIf('launchMode', CRONJOB_LAUNCH_MODES), ProhibitedUnless('launchMode', CRONJOB_LAUNCH_MODES), Rule(new Crontab)]
public ?string $crontab,
public ReleaseCommand $releaseCommand,
public Healthcheck $healthcheck,
#[RequiredUnless('launchMode', [LaunchMode::Daemon, LaunchMode::Manual]), ProhibitedIf('launchMode', [LaunchMode::Daemon, LaunchMode::Manual])]
public ?BackupOptions $backupOptions,
#[RequiredIf('launchMode', LaunchMode::BackupCreate), ProhibitedUnless('launchMode', LaunchMode::BackupCreate)]
public ?BackupCreateOptions $backupCreate,
#[RequiredIf('launchMode', LaunchMode::BackupRestore), ProhibitedUnless('launchMode', LaunchMode::BackupRestore)]
public ?BackupRestoreOptions $backupRestore,
) {
$maxReplicas = $this->launchMode->maxReplicas();
if ($this->replicas > $maxReplicas) {
Expand All @@ -60,14 +62,25 @@ public function asNodeTasks(Deployment $deployment, Process $process, bool $pull
$this->dockerName = $process->makeResourceName('wkr_'.$this->name);
}

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

$this->backupOptions->backupVolume = Volume::validateAndCreate([
$this->backupCreate->backupVolume = Volume::validateAndCreate([
'id' => 'volume-'.Str::random(11),
'name' => $dockerName,
'dockerName' => $dockerName,
'path' => '/ptah/backups',
'path' => '/ptah/backup/create',
]);
}

if ($this->launchMode->value === LaunchMode::BackupRestore->value && ! $this->backupRestore->restoreVolume) {
$dockerName = dockerize_name($this->dockerName.'_vol_ptah_restore');

$this->backupRestore->restoreVolume = Volume::validateAndCreate([
'id' => 'volume-'.Str::random(11),
'name' => $dockerName,
'dockerName' => $dockerName,
'path' => '/ptah/backup/restore',
]);
}

Expand All @@ -91,6 +104,8 @@ public function asNodeTasks(Deployment $deployment, Process $process, bool $pull
'serviceId' => $deployment->service_id,
'serviceName' => $deployment->service->name,
'dockerName' => $this->dockerName,
'processName' => $process->name,
'workerName' => $this->name,
]),
'payload' => [
'ReleaseCommand' => $this->getReleaseCommandPayload($process, $labels),
Expand Down Expand Up @@ -216,8 +231,12 @@ private function getMounts(Deployment $deployment, Process $process, array $labe
{
$mounts = $process->getMounts($deployment);

if ($this->backupOptions) {
$mounts[] = $this->backupOptions->backupVolume->asMount($labels);
if ($this->backupCreate) {
$mounts[] = $this->backupCreate->backupVolume->asMount($labels);
}

if ($this->backupRestore) {
$mounts[] = $this->backupRestore->restoreVolume->asMount($labels);
}

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

if ($this->backupOptions) {
$backupVolume = $this->backupCreate?->backupVolume ?? $this->backupRestore?->restoreVolume;
if ($backupVolume) {
$envVars[] = EnvVar::validateAndCreate([
'name' => 'PTAH_BACKUP_DIR',
'value' => $this->backupOptions->backupVolume->path,
'value' => $backupVolume->path,
]);
}

Expand Down
Loading

0 comments on commit f0b8aac

Please sign in to comment.