Skip to content

Commit

Permalink
feat: #65 allow to backup volumes
Browse files Browse the repository at this point in the history
  • Loading branch information
bohdan-shulha committed Jul 16, 2024
1 parent 487ace4 commit 3aba5f1
Show file tree
Hide file tree
Showing 29 changed files with 594 additions and 25 deletions.
115 changes: 115 additions & 0 deletions app/Console/Commands/DispatchBackupTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace App\Console\Commands;

use App\Models\Node;
use App\Models\NodeTask;
use App\Models\NodeTaskGroupType;
use App\Models\NodeTasks\ServiceExec\ServiceExecMeta;
use App\Models\NodeTaskType;
use App\Models\Service;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class DispatchBackupTask extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:dispatch-backup-task {--service-id=} {--process=} {--volume=}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Adds a task to create Volume Backup on a Node.';

/**
* Execute the console command.
*/
public function handle(): void
{
DB::transaction(function () {
$this->dispatchBackupTask();
});
}

/**
* @throws Exception
*/
protected function dispatchBackupTask(): void
{
/* @var Service $service */
$service = Service::findOrFail($this->option('service-id'));

$deployment = $service->latestDeployment;

$process = $deployment->data->findProcess($this->option('process'));
if ($process === null) {
throw new Exception("Could not find process {$this->option('process')} in deployment {$deployment->id}.");
}

$volume = $process->findVolume($this->option('volume'));
if ($volume === null) {
throw new Exception("Could not find volume {$this->option('volume')} in process {$process->name}.");
}

$node = Node::findOrFail($deployment->data->placementNodeId);

$taskGroup = $node->taskGroups()->create([
'swarm_id' => $node->swarm_id,
'node_id' => $node->id,
'type' => NodeTaskGroupType::CreateBackup,
'invoker_id' => $deployment->latestTaskGroup->invoker_id,
'team_id' => $service->team_id,
]);

$date = now()->format('Y-m-d_His');
$backupFileName = dockerize_name("svc-{$service->id}-{$process->name}-{$volume->name}-{$date}") . '.tar.gz';
$archivePath = "{$process->backupVolume->path}/$backupFileName";
$backupCommand = "tar czfv $archivePath -C {$volume->path} .";

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

$taskGroup->tasks()->createMany([
[
'type' => NodeTaskType::ServiceExec,
'meta' => [
'serviceId' => $service->id,
'command' => $backupCommand,
],
'payload' => [
'ProcessName' => $process->dockerName,
'ExecSpec' => [
'AttachStdout' => true,
'AttachStderr' => true,
'Cmd' => ['sh', '-c', $backupCommand],
]
]
],
[
'type' => NodeTaskType::UploadS3File,
'meta' => [
'serviceId' => $service->id,
],
'payload' => [
'S3StorageConfigName' => $s3Storage->dockerName,
'VolumeSpec' => [
'Type' => 'volume',
'Source' => $process->backupVolume->dockerName,
'Target' => $process->backupVolume->path,
],
'SrcFilePath' => $archivePath,
'DestFilePath' => $s3Storage->pathPrefix . '/' . $backupFileName,
],
]
]);
}
}
9 changes: 9 additions & 0 deletions app/Events/NodeTasks/ServiceExec/ServiceExecCompleted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Events\NodeTasks\ServiceExec;

use App\Events\NodeTasks\BaseTaskEvent;

class ServiceExecCompleted extends BaseTaskEvent
{
}
9 changes: 9 additions & 0 deletions app/Events/NodeTasks/ServiceExec/ServiceExecFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Events\NodeTasks\ServiceExec;

use App\Events\NodeTasks\BaseTaskEvent;

class ServiceExecFailed extends BaseTaskEvent
{
}
9 changes: 9 additions & 0 deletions app/Events/NodeTasks/UploadS3File/UploadS3FileCompleted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Events\NodeTasks\UploadS3File;

use App\Events\NodeTasks\BaseTaskEvent;

class UploadS3FileCompleted extends BaseTaskEvent
{
}
9 changes: 9 additions & 0 deletions app/Events/NodeTasks/UploadS3File/UploadS3FileFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Events\NodeTasks\UploadS3File;

use App\Events\NodeTasks\BaseTaskEvent;

class UploadS3FileFailed extends BaseTaskEvent
{
}
10 changes: 7 additions & 3 deletions app/Http/Controllers/ServiceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function create()
$networks = count($swarms) ? $swarms[0]->networks : [];
$nodes = count($swarms) ? $swarms[0]->nodes : [];
$dockerRegistries = count($swarms) ? $swarms[0]->data->registries : [];
$s3Storages = count($swarms) ? $swarms[0]->data->s3Storages : [];

$deploymentData = DeploymentData::make([
'networkName' => count($networks) ? $networks[0]->name : null,
Expand All @@ -50,7 +51,8 @@ public function create()
'networks' => $networks,
'nodes' => $nodes,
'deploymentData' => $deploymentData,
'dockerRegistries' => $dockerRegistries
'dockerRegistries' => $dockerRegistries,
's3Storages' => $s3Storages,
]);
}

Expand Down Expand Up @@ -82,13 +84,15 @@ public function show(Service $service)

$networks = $service->swarm->networks;
$nodes = $service->swarm->nodes;
$dockerRegistries = $service->swarm->data->registries ;
$dockerRegistries = $service->swarm->data->registries;
$s3Storages = $service->swarm->data->s3Storages;

return Inertia::render('Services/Show', [
'service' => $service,
'networks' => $networks,
'nodes' => $nodes,
'dockerRegistries' => $dockerRegistries
'dockerRegistries' => $dockerRegistries,
's3Storages' => $s3Storages,
]);
}

Expand Down
4 changes: 1 addition & 3 deletions app/Http/Controllers/SwarmController.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,9 @@ public function updateS3Storages(Swarm $swarm, Request $request)
$tasks = [];

foreach ($swarmData->s3Storages as $s3Storage) {
$previous = $s3Storage->dockerName ? $swarm->data->findS3Storage($s3Storage->dockerName) : null;
$previous = $swarm->data->findS3Storage($s3Storage->id);
if ($previous) {
if ($s3Storage->sameAs($previous)) {
$s3Storage->dockerName = $previous->dockerName;

continue;
}
}
Expand Down
7 changes: 6 additions & 1 deletion app/Http/Controllers/SwarmTaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Models\Swarm;
use App\Models\SwarmData;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class SwarmTaskController extends Controller
{
Expand All @@ -31,6 +32,8 @@ public function initCluster(InitClusterFormRequest $request)
'data' => SwarmData::validateAndCreate([
'registriesRev' => 0,
'registries' => [],
's3StoragesRev' => 0,
's3Storages' => [],
]),
]);

Expand Down Expand Up @@ -121,7 +124,7 @@ public function initCluster(InitClusterFormRequest $request)
'data' => DeploymentData::validateAndCreate([
'networkName' => $network->docker_name,
'internalDomain' => 'caddy.ptah.local',
'placementNodeId' => null,
'placementNodeId' => $node->id,
'processes' => [
[
'name' => 'svc',
Expand Down Expand Up @@ -155,10 +158,12 @@ public function initCluster(InitClusterFormRequest $request)
'secretFiles' => [],
'volumes' => [
[
'id' => 'volume-' . Str::random(11),
'name' => 'data',
'path' => '/data',
],
[
'id' => 'volume-' . Str::random(11),
'name' => 'config',
'path' => '/config',
]
Expand Down
18 changes: 18 additions & 0 deletions app/Models/DeploymentData/BackupSchedule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Models\DeploymentData;

use Spatie\LaravelData\Data;

class BackupSchedule extends Data
{
public function __construct(
public CronPreset $preset,
public string $s3StorageId,
// TODO: !!! validate CRON expr
public string $expr,
)
{

}
}
18 changes: 18 additions & 0 deletions app/Models/DeploymentData/BackupVolume.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Models\DeploymentData;

use Spatie\LaravelData\Data;

class BackupVolume extends Data
{
public function __construct(
public string $id,
public string $name,
public ?string $dockerName,
public string $path,
)
{

}
}
9 changes: 9 additions & 0 deletions app/Models/DeploymentData/CronPreset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Models\DeploymentData;

enum CronPreset: string
{
case Daily = 'daily';
case Custom = 'custom';
}
57 changes: 49 additions & 8 deletions app/Models/DeploymentData/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Models\NodeTasks\UpdateService\UpdateServiceMeta;
use App\Models\NodeTaskType;
use App\Rules\RequiredIfArrayHas;
use Illuminate\Support\Str;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\Validation\Enum;
use Spatie\LaravelData\Attributes\Validation\Rule;
Expand Down Expand Up @@ -43,6 +44,7 @@ public function __construct(
#[DataCollectionOf(Volume::class)]
/* @var Volume[] */
public array $volumes,
public ?BackupVolume $backupVolume,
public int $replicas,
#[DataCollectionOf(NodePort::class)]
/* @var NodePort[] */
Expand All @@ -59,6 +61,12 @@ public function __construct(
{

}

public function findVolume(string $dockerName): ?Volume
{
return collect($this->volumes)->first(fn(Volume $volume) => $volume->dockerName === $dockerName);
}

public function findConfigFile(string $path): ?ConfigFile
{
return collect($this->configFiles)->first(fn(ConfigFile $file) => $file->path === $path);
Expand Down Expand Up @@ -205,6 +213,46 @@ public function asNodeTasks(Deployment $deployment): array
'serviceName' => $deployment->service->name,
];

$volumes = $this->volumes;

$mounts = collect($volumes)
->map(fn(Volume $volume) => [
'Type' => 'volume',
'Source' => $volume->dockerName,
'Target' => $volume->path,
'VolumeOptions' => [
'Labels' => dockerize_labels([
'id' => $volume->id,
...$labels,
]),
]
])
->toArray();

// TODO: if (has volumes with backups enabled OR has a Backup Script defined)
if (count($this->volumes)) {
if ($this->backupVolume == null) {
$this->backupVolume = BackupVolume::validateAndCreate([
'id' => 'backups-' . Str::random(11),
'name' => 'backups',
'dockerName' => $this->makeResourceName('/ptah/backups'),
'path' => '/ptah/backups',
]);
}

$mounts[] = [
'Type' => 'volume',
'Source' => $this->backupVolume->dockerName,
'Target' => $this->backupVolume->path,
'VolumeOptions' => [
'Labels' => dockerize_labels([
'id' => $this->backupVolume->id,
...$labels,
]),
]
];
}

// FIXME: this is going to work wrong if the initial deployment is pending.
// Don't allow to schedule deployments if the service has not been created yet?
// This code is duplicated in the next block
Expand All @@ -228,14 +276,7 @@ public function asNodeTasks(Deployment $deployment): array
'Args' => $args,
'Hostname' => "dpl-{$deployment->id}.{$internalDomain}",
'Env' => collect($this->envVars)->map(fn(EnvVar $var) => "{$var->name}={$var->value}")->toArray(),
'Mounts' => collect($this->volumes)->map(fn(Volume $volume) => [
'Type' => 'volume',
'Source' => $volume->dockerName,
'Target' => $volume->path,
'VolumeOptions' => [
'Labels' => $labels,
]
])->toArray(),
'Mounts' => $mounts,
'Hosts' => [
$internalDomain,
],
Expand Down
4 changes: 3 additions & 1 deletion app/Models/DeploymentData/Volume.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
class Volume extends Data
{
public function __construct(
public string $id,
public string $name,
public ?string $dockerName,
public string $path
public string $path,
public ?BackupSchedule $backupSchedule
)
{

Expand Down
Loading

0 comments on commit 3aba5f1

Please sign in to comment.