clusev/app/Livewire/Docker/Index.php

122 lines
4.0 KiB
PHP

<?php
namespace App\Livewire\Docker;
use App\Livewire\Concerns\WithFleetContext;
use App\Models\AuditEvent;
use App\Services\DockerService;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Throwable;
/**
* Container management for the active fleet server (agentless over SSH). Viewing the list is open
* to any role; container actions (start/stop/restart/pause) require `operate`, matching how systemd
* service actions are gated. Loads lazily via wire:init like Files/Services.
*/
#[Layout('layouts.app')]
class Index extends Component
{
use WithFleetContext;
/** @var array<int, array<string, string>> */
public array $containers = [];
public bool $connected = false;
public bool $ready = false;
/** Docker binary absent on the host (e.g. a native mail server) — an honest state, not an error. */
public bool $notInstalled = false;
/** The docker error (daemon down / permission / rootless), surfaced so "empty" isn't misleading. */
public ?string $error = null;
public function title(): string
{
return __('docker.title');
}
public function load(DockerService $docker): void
{
$this->containers = [];
$this->connected = false;
$this->notInstalled = false;
$this->error = null;
$active = $this->activeServer();
if ($active && $active->credential_exists) {
try {
// Probe first: a host with no docker binary (native mail/web server) gets a clean
// "not installed" panel instead of the raw "sh: docker: not found" shell error.
if (! $docker->available($active)) {
$this->notInstalled = true;
} else {
$this->containers = $docker->containers($active);
$this->connected = true;
}
} catch (Throwable $e) {
$this->connected = false;
$this->error = mb_scrub(mb_strcut($e->getMessage(), 0, 300), 'UTF-8');
}
}
$this->ready = true;
}
public function action(string $id, string $op, DockerService $docker): void
{
abort_unless(Auth::user()?->can('operate'), 403);
$active = $this->activeServer();
if (! $active || ! $active->credential_exists) {
return;
}
try {
$res = $docker->containerAction($active, $id, $op);
AuditEvent::create([
'user_id' => Auth::id(),
'server_id' => $active->id,
'actor' => Auth::user()?->name ?? 'system',
'action' => 'docker.action',
'target' => "{$op} {$id} · {$active->name}",
'ip' => request()->ip(),
]);
$this->dispatch('notify', message: $res['ok']
? __('docker.action_ok', ['op' => $op, 'ref' => $id])
: __('docker.action_failed', ['error' => $res['output']]));
} catch (InvalidArgumentException) {
$this->dispatch('notify', message: __('docker.invalid_ref'));
}
$this->load($docker);
}
/** Open the read-only logs modal for a container (the ref is re-validated in DockerService). */
public function viewLogs(string $id): void
{
// Container logs can contain secrets (env dumps, tokens in traces), so reading them is an
// operate action — consistent with gating file CONTENT reads. The list itself stays open.
abort_unless(Auth::user()?->can('operate'), 403);
$active = $this->activeServer();
if (! $active) {
return;
}
$this->dispatch('openModal', component: 'modals.container-logs', arguments: [
'serverId' => $active->id,
'ref' => $id,
]);
}
public function render(): View
{
return view('livewire.docker.index')->title($this->title());
}
}