feat(v5): add server clusters and CLI bootstrap

Create v5 cluster/server persistence, expose them on the home page,
and add a bootstrap endpoint backed by the coolify CLI. Add dev Lima
server sync support and update the dev script firewall flow.
This commit is contained in:
Andras Bacsai
2026-06-16 15:45:16 +02:00
parent 586588196d
commit 1e038fecae
16 changed files with 1389 additions and 108 deletions
+146 -17
View File
@@ -3,8 +3,12 @@
namespace App\Http\Controllers\V5;
use App\Http\Controllers\Controller;
use App\Models\PrivateKey;
use App\Models\Team;
use App\Models\User;
use App\Models\V5\Cluster as V5Cluster;
use App\Models\V5\Server as V5Server;
use App\Services\Coold\CoolifyCliBootstrap;
use App\Services\Coold\CoolifyCliVersion;
use App\Services\Flux\FluxHealth;
use Illuminate\Http\JsonResponse;
@@ -23,7 +27,9 @@ class HomeController extends Controller
return Inertia::render('Home', [
'status' => 'v5-ready',
'flux' => $fluxHealth->check(),
'cooldHosts' => $this->cooldHosts(),
'clusters' => $this->clusters($currentTeam),
'cooldServers' => $this->cooldServers($currentTeam),
'privateKeys' => $this->privateKeys($currentTeam),
'currentTeam' => $currentTeam instanceof Team ? [
'id' => $currentTeam->id,
'name' => $currentTeam->name,
@@ -50,27 +56,150 @@ class HomeController extends Controller
return response()->json($coolifyCliVersion->check());
}
/**
* @return array<int, array{id: string, wireguardIp: string|null, capabilities: array<int, string>, builderEnabled: bool, builderCapacity: int}>
*/
private function cooldHosts(): array
public function bootstrapCoolify(Request $request, CoolifyCliBootstrap $coolifyCliBootstrap): JsonResponse
{
$baseId = (string) config('coold.dev_host_id');
$count = max(0, (int) config('coold.dev_host_count'));
$builderCapacity = (int) config('coold.dev_builder_capacity');
$builderEnabled = $builderCapacity > 0;
$currentTeam = $request->attributes->get('v5.currentTeam');
$validated = $request->validate([
'host' => ['required', 'string', 'max:255'],
'ssh_user' => ['required', 'string', 'max:64'],
'ssh_port' => ['required', 'integer', 'min:1', 'max:65535'],
'private_key_uuid' => ['required', 'string'],
'wg_listen_port' => ['nullable', 'integer', 'min:1', 'max:65535'],
'wg_endpoint' => ['nullable', 'string', 'max:255'],
'enable_builder' => ['boolean'],
'builder_capacity' => ['nullable', 'integer', 'min:0', 'max:100'],
]);
if ($count === 0 || $baseId === '') {
if (! $currentTeam instanceof Team) {
abort(403);
}
$privateKey = PrivateKey::query()
->where('team_id', $currentTeam->id)
->where('uuid', $validated['private_key_uuid'])
->first();
if (! $privateKey instanceof PrivateKey) {
return response()->json([
'successful' => false,
'label' => 'Private key unavailable',
'message' => 'The selected private key is not available for the current team.',
'output' => null,
'errorOutput' => null,
'exitCode' => null,
], 403);
}
$result = $coolifyCliBootstrap->run($validated, $privateKey);
if ($result['successful']) {
$this->recordBootstrappedServer($request->user(), $currentTeam, $privateKey, $validated);
}
return response()->json($result, $result['successful'] ? 200 : 500);
}
/**
* @param array{host: string, ssh_user: string, ssh_port: int, enable_builder?: bool, builder_capacity?: int|null} $input
*/
private function recordBootstrappedServer(User $user, Team $team, PrivateKey $privateKey, array $input): void
{
$builderEnabled = (bool) ($input['enable_builder'] ?? config('coold.dev_builder_enabled', true));
$builderCapacity = $builderEnabled ? (int) ($input['builder_capacity'] ?? config('coold.dev_builder_capacity', 2)) : 0;
$capabilities = $builderEnabled ? ['coold', 'builder'] : ['coold'];
V5Server::query()->updateOrCreate([
'team_id' => $team->id,
'host' => $input['host'],
'ssh_port' => $input['ssh_port'],
], [
'created_by_user_id' => $user->id,
'private_key_id' => $privateKey->id,
'name' => $input['host'],
'ssh_user' => $input['ssh_user'],
'status' => 'installed',
'capabilities' => $capabilities,
'builder_enabled' => $builderEnabled,
'builder_capacity' => $builderCapacity,
'last_bootstrapped_at' => now(),
]);
}
/**
* @return array<int, array{uuid: string, name: string}>
*/
private function privateKeys(mixed $currentTeam): array
{
if (! $currentTeam instanceof Team) {
return [];
}
return collect(range(1, $count))
->map(fn (int $index) => [
'id' => $index === 1 ? $baseId : (string) config("coold.dev_host_id_{$index}", "{$baseId}-{$index}"),
'wireguardIp' => (string) config("coold.dev_wireguard_ip_{$index}") ?: null,
'capabilities' => $builderEnabled ? ['coold', 'builder'] : ['coold'],
'builderEnabled' => $builderEnabled,
'builderCapacity' => $builderCapacity,
return PrivateKey::query()
->where('team_id', $currentTeam->id)
->select('uuid', 'name')
->orderBy('name')
->get()
->map(fn (PrivateKey $privateKey) => [
'uuid' => $privateKey->uuid,
'name' => $privateKey->name,
])
->all();
}
/**
* @return array<int, array{id: string, name: string, description: string|null, serversCount: int, servers: array<int, array{id: string, name: string, host: string, status: string, capabilities: array<int, string>}>}>
*/
private function clusters(mixed $currentTeam): array
{
if (! $currentTeam instanceof Team) {
return [];
}
return V5Cluster::query()
->where('team_id', $currentTeam->id)
->with(['servers' => fn ($query) => $query->orderBy('name')])
->withCount('servers')
->orderBy('name')
->get()
->map(fn (V5Cluster $cluster) => [
'id' => (string) $cluster->id,
'name' => $cluster->name,
'description' => $cluster->description,
'serversCount' => $cluster->servers_count,
'servers' => $cluster->servers->map(fn (V5Server $server) => [
'id' => (string) $server->id,
'name' => $server->name,
'host' => $server->host,
'status' => $server->status,
'capabilities' => $server->capabilities ?? [],
])->all(),
])
->all();
}
/**
* @return array<int, array{id: string, host: string, sshUser: string, sshPort: int, status: string, capabilities: array<int, string>, builderEnabled: bool, builderCapacity: int, lastBootstrappedAt: string|null}>
*/
private function cooldServers(mixed $currentTeam): array
{
if (! $currentTeam instanceof Team) {
return [];
}
return V5Server::query()
->where('team_id', $currentTeam->id)
->orderBy('name')
->get()
->map(fn (V5Server $server) => [
'id' => (string) $server->id,
'host' => $server->host,
'sshUser' => $server->ssh_user,
'sshPort' => $server->ssh_port,
'status' => $server->status,
'capabilities' => $server->capabilities ?? [],
'builderEnabled' => $server->builder_enabled,
'builderCapacity' => $server->builder_capacity,
'lastBootstrappedAt' => $server->last_bootstrapped_at?->toISOString(),
])
->all();
}