Skip to content

Commit

Permalink
feat: #64 allow to run arbitrary backup commands for processes
Browse files Browse the repository at this point in the history
  • Loading branch information
bohdan-shulha committed Jul 16, 2024
1 parent 3aba5f1 commit 698905f
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 14 deletions.
113 changes: 113 additions & 0 deletions app/Console/Commands/DispatchProcessBackupTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

namespace App\Console\Commands;

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

class DispatchProcessBackupTask extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:backups:processes:create {--service-id=} {--process=} {--backup-cmd-id=}';

/**
* 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}.");
}

$backupCmd = $process->findProcessBackup($this->option('backup-cmd-id'));
if ($backupCmd === null) {
throw new Exception("Could not find backup command {$this->option('backup-cmd-id')} 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}_backup-{$backupCmd->name}-{$date}") . '.tar.gz';
$archivePath = "{$process->backupVolume->path}/$backupFileName";
$backupCommand = "mkdir -p /tmp/{$backupCmd->id} && cd /tmp/{$backupCmd->id} && {$backupCmd->command} && tar czfv $archivePath -C /tmp/{$backupCmd->id} . && rm -rf /tmp/{$backupCmd->id}";

$s3Storage = $node->swarm->data->findS3Storage($backupCmd->backupSchedule->s3StorageId);
if ($s3Storage === null) {
throw new Exception("Could not find S3 storage {$backupCmd->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,
],
]
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
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
class DispatchVolumeBackupTask extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:dispatch-backup-task {--service-id=} {--process=} {--volume=}';
protected $signature = 'app:backups:volumes:create {--service-id=} {--process=} {--volume-id=}';

/**
* The console command description.
Expand Down Expand Up @@ -53,7 +51,7 @@ protected function dispatchBackupTask(): void
throw new Exception("Could not find process {$this->option('process')} in deployment {$deployment->id}.");
}

$volume = $process->findVolume($this->option('volume'));
$volume = $process->findVolume($this->option('volume-id'));
if ($volume === null) {
throw new Exception("Could not find volume {$this->option('volume')} in process {$process->name}.");
}
Expand All @@ -69,10 +67,11 @@ protected function dispatchBackupTask(): void
]);

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

// TODO: get rid of copy-pasted code.
$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}.");
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/SwarmTaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public function initCluster(InitClusterFormRequest $request)
'command' => null,
],
'command' => 'sh /start.sh',
'backups' => [],
'workers' => [],
'envVars' => [
[
Expand Down
1 change: 1 addition & 0 deletions app/Models/DeploymentData.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static function make(array $attributes): static
'command' => null,
]),
'command' => '',
'backups' => [],
'workers' => [],
'launchMode' => LaunchMode::Daemon->value,
'envVars' => [],
Expand Down
20 changes: 17 additions & 3 deletions app/Models/DeploymentData/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public function __construct(
public string $dockerImage,
public ReleaseCommand $releaseCommand,
public ?string $command,
#[DataCollectionOf(ProcessBackup::class)]
/* @var ProcessBackup[] */
public array $backups,
#[DataCollectionOf(Worker::class)]
/* @var Worker[] */
public array $workers,
Expand Down Expand Up @@ -62,9 +65,14 @@ public function __construct(

}

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

public function findProcessBackup(string $id): ?ProcessBackup
{
return collect($this->backups)->first(fn(ProcessBackup $backup) => $backup->id === $id);
}

public function findConfigFile(string $path): ?ConfigFile
Expand Down Expand Up @@ -253,6 +261,12 @@ public function asNodeTasks(Deployment $deployment): array
];
}

$envVars = $this->envVars;
$envVars[] = EnvVar::validateAndCreate([
'name' => "PTAH_HOSTNAME",
'value' => $internalDomain,
]);

// 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 @@ -275,7 +289,7 @@ public function asNodeTasks(Deployment $deployment): array
'Command' => $command,
'Args' => $args,
'Hostname' => "dpl-{$deployment->id}.{$internalDomain}",
'Env' => collect($this->envVars)->map(fn(EnvVar $var) => "{$var->name}={$var->value}")->toArray(),
'Env' => collect($envVars)->map(fn(EnvVar $var) => "{$var->name}={$var->value}")->toArray(),
'Mounts' => $mounts,
'Hosts' => [
$internalDomain,
Expand Down
19 changes: 19 additions & 0 deletions app/Models/DeploymentData/ProcessBackup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Models\DeploymentData;

use Spatie\LaravelData\Data;

class ProcessBackup extends Data
{

public function __construct(
public string $id,
public string $name,
public string $command,
public BackupSchedule $backupSchedule,
)
{

}
}
26 changes: 22 additions & 4 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?php

use ApiNodes\Http\Middleware\AgentTokenAuth;
use App\Console\Commands\DispatchBackupTask;
use App\Console\Commands\DispatchProcessBackupTask;
use App\Console\Commands\DispatchVolumeBackupTask;
use App\Http\Middleware\HandleInertiaRequests;
use App\Jobs\CheckAgentUpdates;
use App\Models\DeploymentData\CronPreset;
Expand Down Expand Up @@ -52,6 +53,10 @@
) use ($schedule) {
foreach ($services as $service) {
foreach ($service->latestDeployment->data->processes as $process) {
// FIXME: this is an array when running migrations
if (is_array($process)) {
continue;
}
if ($process->replicas === 0) {
continue;
}
Expand All @@ -62,14 +67,27 @@
}

$schedule
->command(DispatchBackupTask::class, [
'serviceId' => $service->id,
'volumeId' => $volume->id,
->command(DispatchVolumeBackupTask::class, [
'--service-id' => $service->id,
'--process' => $process->dockerName,
'--volume-id' => $volume->id,
])
->cron($backupSchedule->expr)
->onOneServer()
->withoutOverlapping();
}

foreach ($process->backups as $backup) {
$schedule
->command(DispatchProcessBackupTask::class, [
'--service-id' => $service->id,
'--process' => $process->dockerName,
'--backup-cmd-id' => $backup->id,
])
->cron($backup->backupSchedule->expr)
->onOneServer()
->withoutOverlapping();
}
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::update("
UPDATE deployments
SET data = jsonb_set(
data,
'{processes}',
(
SELECT jsonb_agg(process || jsonb_build_object('backups', '[]'::jsonb))
FROM jsonb_array_elements(data->'processes') AS process
)
);
");
}

/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};
Loading

0 comments on commit 698905f

Please sign in to comment.