feat(ssh): add FleetService::testConnection credential probe

Connect + trivial exec against a server's stored credential; never throws,
returns {ok,error}. Used by the create-server guard to verify the SSH login
before persisting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-14 10:24:42 +02:00
parent ed909edf3e
commit 2c25303903
2 changed files with 50 additions and 0 deletions

View File

@ -118,6 +118,29 @@ class FleetService
return array_map(fn (array $l): string => implode("\n", $l), $parts);
}
/**
* Probe the server's stored credential: connect + a trivial exec. NEVER throws
* returns the failure reason instead, for the create-server guard to surface.
* Uses a short timeout so an unreachable host fails fast.
*
* @return array{ok: bool, error: ?string}
*/
public function testConnection(Server $server): array
{
try {
$ssh = (new SshClient($this->vault, timeout: 10))->connect($server);
try {
[, $code] = $ssh->run('true');
} finally {
$ssh->disconnect();
}
return ['ok' => $code === 0, 'error' => null];
} catch (Throwable $e) {
return ['ok' => false, 'error' => $e->getMessage()];
}
}
// ── Metrics (light — used by the poller and dashboard fallback) ──────
/** @return array{cpu:int,mem:int,disk:int,load:float} */

View File

@ -0,0 +1,27 @@
<?php
namespace Tests\Feature;
use App\Models\Server;
use App\Services\FleetService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class FleetTestConnectionTest extends TestCase
{
use RefreshDatabase;
public function test_unreachable_host_returns_error_without_throwing(): void
{
$server = Server::create(['name' => 'x', 'ip' => '127.0.0.1', 'ssh_port' => 1, 'status' => 'pending']);
$server->credential()->create([
'username' => 'root', 'auth_type' => 'password', 'secret' => 'nope',
]);
$server->load('credential');
$res = app(FleetService::class)->testConnection($server);
$this->assertFalse($res['ok']);
$this->assertNotNull($res['error']);
}
}