137 lines
4.5 KiB
PHP
137 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Docker;
|
|
|
|
use App\Exceptions\DockerNotInstalled;
|
|
use App\Models\AuditEvent;
|
|
use App\Models\HostCredential;
|
|
use App\Models\Server;
|
|
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;
|
|
|
|
/**
|
|
* Docker for the CLUSEV HOST — the machine Clusev runs on, reached via the stored HostCredential
|
|
* over the local Docker gateway. Admin-only (manage-fleet), the same bar as the host terminal: a
|
|
* `Stop` on a control-plane container takes Clusev down. Per-SERVER container management lives on
|
|
* the server-details page (the "Docker" tab / App\Livewire\Servers\ServerDocker). Loads lazily.
|
|
*/
|
|
#[Layout('layouts.app')]
|
|
class Index extends Component
|
|
{
|
|
/** @var array<int, array<string, string>> */
|
|
public array $containers = [];
|
|
|
|
public bool $connected = false;
|
|
|
|
public bool $ready = false;
|
|
|
|
/** Docker binary absent on the host — an honest state, not an error. */
|
|
public bool $notInstalled = false;
|
|
|
|
/** No host SSH login configured yet — point the operator to setup. */
|
|
public bool $hostNotConfigured = false;
|
|
|
|
/** The docker error (daemon down / permission / rootless), surfaced so "empty" isn't misleading. */
|
|
public ?string $error = null;
|
|
|
|
public function mount(): void
|
|
{
|
|
// The host is the control-plane machine — admin-only, like the host terminal.
|
|
abort_unless(Auth::user()?->can('manage-fleet'), 403);
|
|
}
|
|
|
|
public function title(): string
|
|
{
|
|
return __('docker.title');
|
|
}
|
|
|
|
/** A transient Server pointing at the Clusev host (built from the host login), or null if unset. */
|
|
private function hostServer(): ?Server
|
|
{
|
|
return HostCredential::current()?->toServer();
|
|
}
|
|
|
|
public function load(DockerService $docker): void
|
|
{
|
|
$this->containers = [];
|
|
$this->connected = false;
|
|
$this->notInstalled = false;
|
|
$this->hostNotConfigured = false;
|
|
$this->error = null;
|
|
|
|
$server = $this->hostServer();
|
|
if (! $server) {
|
|
$this->hostNotConfigured = true;
|
|
$this->ready = true;
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// ONE SSH round-trip: containers() derives the "not installed" state from the docker ps
|
|
// call itself (a missing binary throws DockerNotInstalled), so no separate available() probe.
|
|
$this->containers = $docker->containers($server);
|
|
$this->connected = true;
|
|
} catch (DockerNotInstalled) {
|
|
$this->notInstalled = 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('manage-fleet'), 403);
|
|
|
|
$server = $this->hostServer();
|
|
if (! $server) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$res = $docker->containerAction($server, $id, $op);
|
|
AuditEvent::create([
|
|
'user_id' => Auth::id(),
|
|
'server_id' => null, // the host is transient (no fleet row); the target label names it
|
|
'actor' => Auth::user()?->name ?? 'system',
|
|
'action' => 'docker.action',
|
|
'target' => "{$op} {$id} · {$server->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 host container (ref re-validated in DockerService). */
|
|
public function viewLogs(string $id, string $name = ''): void
|
|
{
|
|
abort_unless(Auth::user()?->can('manage-fleet'), 403);
|
|
|
|
$this->dispatch('openModal', component: 'modals.container-logs', arguments: [
|
|
// 0 is the sentinel the logs modal resolves to the Clusev host (no persisted id).
|
|
'serverId' => 0,
|
|
'ref' => $id,
|
|
'name' => $name,
|
|
]);
|
|
}
|
|
|
|
public function render(): View
|
|
{
|
|
return view('livewire.docker.index')->title($this->title());
|
|
}
|
|
}
|