fix(docker): find the docker binary regardless of the login shell PATH
A non-interactive SSH exec (and sudo's secure_path) frequently has a minimal PATH (~/usr/bin:/bin). Docker installed in /usr/local/bin (get.docker.com), /snap/bin (snap) or /usr/sbin is then unreachable, so every call died with "sh: docker: not found" and the page rendered as "Docker nicht verfügbar" even though Docker was installed and running. Prepend a comprehensive PATH to every docker invocation (available, containers, containerAction, logs, composeStacks) so the binary is located independent of the remote login shell. Reproduced the exact failure and the fix through the real FleetService base64+sh transport. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/v1-foundation
parent
7535ee2d63
commit
4ab255832d
|
|
@ -16,11 +16,20 @@ class DockerService
|
|||
{
|
||||
public const ACTIONS = ['start', 'stop', 'restart', 'pause', 'unpause'];
|
||||
|
||||
/**
|
||||
* A non-interactive SSH exec (and sudo's secure_path) often has a MINIMAL PATH (~/usr/bin:/bin),
|
||||
* so `docker` installed in /usr/local/bin (get.docker.com), /snap/bin (snap), or /usr/sbin isn't
|
||||
* found → "sh: docker: not found" even though it IS installed. Prepend the common bin dirs to
|
||||
* every docker call so the binary is located regardless of the login shell's PATH. Single-quoted
|
||||
* so `${PATH:+:$PATH}` reaches the remote shell literally (not interpolated by PHP).
|
||||
*/
|
||||
private const PATH_PREFIX = 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin${PATH:+:$PATH}; ';
|
||||
|
||||
public function __construct(private FleetService $fleet) {}
|
||||
|
||||
public function available(Server $server): bool
|
||||
{
|
||||
$res = $this->fleet->runPrivileged($server, 'command -v docker >/dev/null 2>&1 && echo yes || echo no');
|
||||
$res = $this->fleet->runPrivileged($server, self::PATH_PREFIX.'command -v docker >/dev/null 2>&1 && echo yes || echo no');
|
||||
|
||||
return trim($res['output']) === 'yes';
|
||||
}
|
||||
|
|
@ -34,7 +43,7 @@ class DockerService
|
|||
// key-casing dependency. `.State` was added late, so we derive state from `.Status` text —
|
||||
// this is the whole reason a modern-only `{{json .}}` could return nothing on an older host.
|
||||
$fmt = '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}';
|
||||
$res = $this->fleet->runPrivileged($server, "docker ps -a --format '{$fmt}'");
|
||||
$res = $this->fleet->runPrivileged($server, self::PATH_PREFIX."docker ps -a --format '{$fmt}'");
|
||||
|
||||
if (! $res['ok']) {
|
||||
// Surface the REAL reason (daemon down / permission / rootless socket) rather than a
|
||||
|
|
@ -88,7 +97,7 @@ class DockerService
|
|||
}
|
||||
$this->assertValidRef($id);
|
||||
|
||||
$res = $this->fleet->runPrivileged($server, "docker {$op} {$id}");
|
||||
$res = $this->fleet->runPrivileged($server, self::PATH_PREFIX."docker {$op} {$id}");
|
||||
|
||||
return ['ok' => $res['ok'], 'output' => trim($res['output'])];
|
||||
}
|
||||
|
|
@ -101,7 +110,7 @@ class DockerService
|
|||
// Cap by BYTES too (not just lines): one huge line or a binary-ish log could otherwise bloat
|
||||
// the response or, via invalid UTF-8, break Livewire's JSON snapshot. head -c bounds it, and
|
||||
// mb_scrub drops any invalid byte sequence before it reaches a Livewire public property.
|
||||
$res = $this->fleet->runPrivileged($server, "docker logs --tail {$lines} {$id} 2>&1 | head -c 262144");
|
||||
$res = $this->fleet->runPrivileged($server, self::PATH_PREFIX."docker logs --tail {$lines} {$id} 2>&1 | head -c 262144");
|
||||
|
||||
return mb_scrub(trim($res['output']), 'UTF-8');
|
||||
}
|
||||
|
|
@ -111,7 +120,7 @@ class DockerService
|
|||
*/
|
||||
public function composeStacks(Server $server): array
|
||||
{
|
||||
$res = $this->fleet->runPrivileged($server, 'docker compose ls --format json 2>/dev/null');
|
||||
$res = $this->fleet->runPrivileged($server, self::PATH_PREFIX.'docker compose ls --format json 2>/dev/null');
|
||||
if (! $res['ok']) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class DockerServiceTest extends TestCase
|
|||
{
|
||||
[$docker, $fleet] = $this->make();
|
||||
$fleet->shouldReceive('runPrivileged')->once()
|
||||
->with(Mockery::type(Server::class), 'docker restart web1')
|
||||
->with(Mockery::type(Server::class), Mockery::on(fn ($cmd) => str_starts_with($cmd, 'export PATH=') && str_contains($cmd, 'docker restart web1')))
|
||||
->andReturn(['ok' => true, 'output' => 'web1']);
|
||||
|
||||
$res = $docker->containerAction($this->server(), 'web1', 'restart');
|
||||
|
|
@ -106,7 +106,7 @@ class DockerServiceTest extends TestCase
|
|||
{
|
||||
[$docker, $fleet] = $this->make();
|
||||
$fleet->shouldReceive('runPrivileged')->once()
|
||||
->with(Mockery::type(Server::class), 'docker logs --tail 2000 web 2>&1 | head -c 262144')
|
||||
->with(Mockery::type(Server::class), Mockery::on(fn ($cmd) => str_starts_with($cmd, 'export PATH=') && str_contains($cmd, 'docker logs --tail 2000 web 2>&1 | head -c 262144')))
|
||||
->andReturn(['ok' => true, 'output' => 'log line']);
|
||||
|
||||
$this->assertSame('log line', $docker->logs($this->server(), 'web', 99999)); // clamped to 2000, byte-capped
|
||||
|
|
|
|||
Loading…
Reference in New Issue