Merge pull request #4842 from peaklabs-dev/docker-cleanup-executions-ui

feat: Docker cleanup execution UI and some UI improvements
This commit is contained in:
Andras Bacsai
2025-01-16 21:42:02 +01:00
committed by GitHub
30 changed files with 894 additions and 331 deletions

View File

@@ -25,17 +25,25 @@ class CleanupDocker
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
];
$serverSettings = $server->settings;
if ($serverSettings->delete_unused_volumes) {
if ($server->settings->delete_unused_volumes) {
$commands[] = 'docker volume prune -af';
}
if ($serverSettings->delete_unused_networks) {
if ($server->settings->delete_unused_networks) {
$commands[] = 'docker network prune -f';
}
$cleanupLog = [];
foreach ($commands as $command) {
instant_remote_process([$command], $server, false);
$commandOutput = instant_remote_process([$command], $server, false);
if ($commandOutput !== null) {
$cleanupLog[] = [
'command' => $command,
'output' => $commandOutput,
];
}
}
return $cleanupLog;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Events;
use App\Models\DockerCleanupExecution;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class DockerCleanupDone implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public DockerCleanupExecution $execution) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('team.'.$this->execution->server->team->id),
];
}
}

View File

@@ -18,7 +18,7 @@ use App\Models\SwarmDocker;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Traits\ExecuteRemoteCommand;
use Exception;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -317,7 +317,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
throw $e;
} finally {
$this->application_deployment_queue->update([
'finished_at' => now(),
'finished_at' => Carbon::now()->toImmutable(),
]);
if ($this->use_build_server) {
@@ -1501,7 +1501,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]
);
if ($this->saved_outputs->get('commit_message')) {
$commit_message = str($this->saved_outputs->get('commit_message'))->limit(47);
$commit_message = str($this->saved_outputs->get('commit_message'));
$this->application_deployment_queue->commit_message = $commit_message->value();
ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update(
['commit_message' => $commit_message->value()]

View File

@@ -331,6 +331,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->team) {
BackupCreated::dispatch($this->team->id);
}
if ($this->backup_log) {
$this->backup_log->update([
'finished_at' => Carbon::now()->toImmutable(),
]);
}
}
}

View File

@@ -3,9 +3,12 @@
namespace App\Jobs;
use App\Actions\Server\CleanupDocker;
use App\Events\DockerCleanupDone;
use App\Models\DockerCleanupExecution;
use App\Models\Server;
use App\Notifications\Server\DockerCleanupFailed;
use App\Notifications\Server\DockerCleanupSuccess;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -24,6 +27,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
public ?DockerCleanupExecution $execution_log = null;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
@@ -38,37 +43,89 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$this->execution_log = DockerCleanupExecution::create([
'server_id' => $this->server->id,
]);
$this->usageBefore = $this->server->getDiskUsage();
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
CleanupDocker::run(server: $this->server);
$cleanup_log = CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
$this->server->team?->notify(new DockerCleanupSuccess($this->server, ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
$message = ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
$this->execution_log->update([
'status' => 'success',
'message' => $message,
'cleanup_log' => $cleanup_log,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
return;
}
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
CleanupDocker::run(server: $this->server);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk usage could be determined.'));
$cleanup_log = CleanupDocker::run(server: $this->server);
$message = 'Docker cleanup job executed successfully, but no disk usage could be determined.';
$this->execution_log->update([
'status' => 'success',
'message' => $message,
'cleanup_log' => $cleanup_log,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
}
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
CleanupDocker::run(server: $this->server);
$cleanup_log = CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
$diskSaved = $this->usageBefore - $usageAfter;
if ($diskSaved > 0) {
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
$message = 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
} else {
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
$message = 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
}
$this->execution_log->update([
'status' => 'success',
'message' => $message,
'cleanup_log' => $cleanup_log,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
} else {
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'No cleanup needed for '.$this->server->name));
$message = 'No cleanup needed for '.$this->server->name;
$this->execution_log->update([
'status' => 'success',
'message' => $message,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
}
} catch (\Throwable $e) {
if ($this->execution_log) {
$this->execution_log->update([
'status' => 'failed',
'message' => $e->getMessage(),
]);
event(new DockerCleanupDone($this->execution_log));
}
$this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage()));
throw $e;
} finally {
if ($this->execution_log) {
$this->execution_log->update([
'finished_at' => Carbon::now()->toImmutable(),
]);
}
}
}
}

View File

@@ -11,6 +11,7 @@ use App\Models\Service;
use App\Models\Team;
use App\Notifications\ScheduledTask\TaskFailed;
use App\Notifications\ScheduledTask\TaskSuccess;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -131,6 +132,11 @@ class ScheduledTaskJob implements ShouldQueue
throw $e;
} finally {
ScheduledTaskDone::dispatch($this->team->id);
if ($this->task_log) {
$this->task_log->update([
'finished_at' => Carbon::now()->toImmutable(),
]);
}
}
}
}

View File

@@ -117,29 +117,6 @@ class BackupExecutions extends Component
return null;
}
public function getServerTimezone()
{
$server = $this->server();
if (! $server) {
return 'UTC';
}
return $server->settings->server_timezone;
}
public function formatDateInServerTimezone($date)
{
$serverTimezone = $this->getServerTimezone();
$dateObj = new \DateTime($date);
try {
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
} catch (\Exception) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
public function render()
{
return view('livewire.project.database.backup-executions', [

View File

@@ -141,17 +141,4 @@ class Executions extends Component
return $lines->count() > ($this->currentPage * $this->logsPerPage);
}
public function formatDateInServerTimezone($date)
{
$serverTimezone = $this->serverTimezone;
$dateObj = new \DateTime($date);
try {
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
} catch (\Exception) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Server;
use App\Jobs\DockerCleanupJob;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -19,21 +18,6 @@ class Advanced extends Component
#[Validate(['integer', 'min:1', 'max:99'])]
public int $serverDiskUsageNotificationThreshold = 50;
#[Validate(['string', 'required'])]
public string $dockerCleanupFrequency = '*/10 * * * *';
#[Validate(['integer', 'min:1', 'max:99'])]
public int $dockerCleanupThreshold = 10;
#[Validate('boolean')]
public bool $forceDockerCleanup = false;
#[Validate('boolean')]
public bool $deleteUnusedVolumes = false;
#[Validate('boolean')]
public bool $deleteUnusedNetworks = false;
#[Validate(['integer', 'min:1'])]
public int $concurrentBuilds = 1;
@@ -47,7 +31,7 @@ class Advanced extends Component
$this->parameters = get_route_parameters();
$this->syncData();
} catch (\Throwable) {
return redirect()->route('server.show');
return redirect()->route('server.index');
}
}
@@ -57,23 +41,13 @@ class Advanced extends Component
$this->validate();
$this->server->settings->concurrent_builds = $this->concurrentBuilds;
$this->server->settings->dynamic_timeout = $this->dynamicTimeout;
$this->server->settings->force_docker_cleanup = $this->forceDockerCleanup;
$this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency;
$this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold;
$this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold;
$this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes;
$this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks;
$this->server->settings->server_disk_usage_check_frequency = $this->serverDiskUsageCheckFrequency;
$this->server->settings->save();
} else {
$this->concurrentBuilds = $this->server->settings->concurrent_builds;
$this->dynamicTimeout = $this->server->settings->dynamic_timeout;
$this->forceDockerCleanup = $this->server->settings->force_docker_cleanup;
$this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency;
$this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold;
$this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold;
$this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes;
$this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks;
$this->serverDiskUsageCheckFrequency = $this->server->settings->server_disk_usage_check_frequency;
}
}
@@ -88,23 +62,9 @@ class Advanced extends Component
}
}
public function manualCleanup()
{
try {
DockerCleanupJob::dispatch($this->server, true);
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
if (! validate_cron_expression($this->dockerCleanupFrequency)) {
$this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency');
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.');
}
if (! validate_cron_expression($this->serverDiskUsageCheckFrequency)) {
$this->serverDiskUsageCheckFrequency = $this->server->settings->getOriginal('server_disk_usage_check_frequency');
throw new \Exception('Invalid Cron / Human expression for Disk Usage Check Frequency.');

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Livewire\Server;
use App\Jobs\DockerCleanupJob;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
class DockerCleanup extends Component
{
public Server $server;
public array $parameters = [];
#[Validate(['string', 'required'])]
public string $dockerCleanupFrequency = '*/10 * * * *';
#[Validate(['integer', 'min:1', 'max:99'])]
public int $dockerCleanupThreshold = 10;
#[Validate('boolean')]
public bool $forceDockerCleanup = false;
#[Validate('boolean')]
public bool $deleteUnusedVolumes = false;
#[Validate('boolean')]
public bool $deleteUnusedNetworks = false;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->parameters = get_route_parameters();
$this->syncData();
} catch (\Throwable) {
return redirect()->route('server.index');
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->server->settings->force_docker_cleanup = $this->forceDockerCleanup;
$this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency;
$this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold;
$this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes;
$this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks;
$this->server->settings->save();
} else {
$this->forceDockerCleanup = $this->server->settings->force_docker_cleanup;
$this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency;
$this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold;
$this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes;
$this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks;
}
}
public function instantSave()
{
try {
$this->syncData(true);
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCleanup()
{
try {
DockerCleanupJob::dispatch($this->server, true);
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
if (! validate_cron_expression($this->dockerCleanupFrequency)) {
$this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency');
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.');
}
$this->syncData(true);
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.docker-cleanup');
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Livewire\Server;
use App\Models\DockerCleanupExecution;
use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component;
class DockerCleanupExecutions extends Component
{
public Server $server;
public Collection $executions;
public ?int $selectedKey = null;
public $selectedExecution = null;
public bool $isPollingActive = false;
public $currentPage = 1;
public $logsPerPage = 100;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},DockerCleanupDone" => 'refreshExecutions',
];
}
public function mount(Server $server)
{
$this->server = $server;
$this->refreshExecutions();
}
public function refreshExecutions(): void
{
$this->executions = $this->server->dockerCleanupExecutions()
->orderBy('created_at', 'desc')
->take(20)
->get();
if ($this->selectedKey) {
$this->selectedExecution = DockerCleanupExecution::find($this->selectedKey);
if ($this->selectedExecution && $this->selectedExecution->status !== 'running') {
$this->isPollingActive = false;
}
}
}
public function selectExecution($key): void
{
if ($key == $this->selectedKey) {
$this->selectedKey = null;
$this->selectedExecution = null;
$this->currentPage = 1;
$this->isPollingActive = false;
return;
}
$this->selectedKey = $key;
$this->selectedExecution = DockerCleanupExecution::find($key);
$this->currentPage = 1;
if ($this->selectedExecution && $this->selectedExecution->status === 'running') {
$this->isPollingActive = true;
}
}
public function polling()
{
if ($this->selectedExecution && $this->isPollingActive) {
$this->selectedExecution->refresh();
if ($this->selectedExecution->status !== 'running') {
$this->isPollingActive = false;
}
}
$this->refreshExecutions();
}
public function loadMoreLogs()
{
$this->currentPage++;
}
public function getLogLinesProperty()
{
if (! $this->selectedExecution) {
return collect();
}
if (! $this->selectedExecution->message) {
return collect(['Waiting for execution output...']);
}
$lines = collect(explode("\n", $this->selectedExecution->message));
return $lines->take($this->currentPage * $this->logsPerPage);
}
public function downloadLogs(int $executionId)
{
$execution = $this->executions->firstWhere('id', $executionId);
if (! $execution) {
return;
}
return response()->streamDownload(function () use ($execution) {
echo $execution->message;
}, "docker-cleanup-{$execution->uuid}.log");
}
public function hasMoreLogs()
{
if (! $this->selectedExecution || ! $this->selectedExecution->message) {
return false;
}
$lines = collect(explode("\n", $this->selectedExecution->message));
return $lines->count() > ($this->currentPage * $this->logsPerPage);
}
public function render()
{
return view('livewire.server.docker-cleanup-executions');
}
}

View File

@@ -81,7 +81,7 @@ class ApplicationDeploymentQueue extends Model
return null;
}
return str($this->commit_message)->trim()->limit(50)->value();
return str($this->commit_message)->value();
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DockerCleanupExecution extends BaseModel
{
protected $guarded = [];
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
}

View File

@@ -199,6 +199,11 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class);
}
public function dockerCleanupExecutions()
{
return $this->hasMany(DockerCleanupExecution::class);
}
public function proxySet()
{
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;