feat(v5): add server reconciliation and canvas APIs

Split V5 dashboard behavior into domain controllers and policies,
add agent token rotation/revocation, status reconciliation jobs,
ingress firewall syncing, and canvas connection APIs.

Add migrations for V5 status tracking, server capabilities, resource
connection aliases, and revoked agent tokens.
This commit is contained in:
Andras Bacsai committed 2026-07-06 17:40:37 +02:00
1 parent df854feee8
commit 6ae45684f9
147 files changed
+22444 -10407

No files matched your search

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->index('wireguard_management_ip');
$table->index('node_address');
$table->index('host');
});
Schema::table('v5_applications', function (Blueprint $table) {
$table->index('runtime_container_id');
});
Schema::table('v5_container_statuses', function (Blueprint $table) {
$table->index('last_seen_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->dropIndex(['wireguard_management_ip']);
$table->dropIndex(['node_address']);
$table->dropIndex(['host']);
});
Schema::table('v5_applications', function (Blueprint $table) {
$table->dropIndex(['runtime_container_id']);
});
Schema::table('v5_container_statuses', function (Blueprint $table) {
$table->dropIndex(['last_seen_at']);
});
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('v5_servers')
->whereNull('uuid')
->pluck('id')
->each(function (int $id): void {
DB::table('v5_servers')
->where('id', $id)
->update(['uuid' => Str::lower(Str::random(24))]);
});
Schema::table('v5_servers', function (Blueprint $table) {
$table->string('uuid')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->string('uuid')->nullable()->change();
});
}
};
@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->timestamp('status_observed_at')->nullable()->after('status');
});
Schema::table('v5_applications', function (Blueprint $table) {
$table->timestamp('status_observed_at')->nullable()->after('status_message');
});
Schema::table('v5_container_statuses', function (Blueprint $table) {
$table->timestamp('status_observed_at')->nullable()->after('status_message');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->dropColumn('status_observed_at');
});
Schema::table('v5_applications', function (Blueprint $table) {
$table->dropColumn('status_observed_at');
});
Schema::table('v5_container_statuses', function (Blueprint $table) {
$table->dropColumn('status_observed_at');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->string('coold_version')->nullable()->after('wireguard_public_key');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->dropColumn('coold_version');
});
}
};
@@ -0,0 +1,74 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Rewrites v5 polymorphic rows that stored the Application FQCN (written
* before the 'v5.application' morph alias existed) so a future class rename
* cannot orphan them. New rows pick up the alias via getMorphClass().
*/
return new class extends Migration
{
private const FQCN = 'App\Models\V5\Application';
private const ALIAS = 'v5.application';
public function up(): void
{
$this->rewriteMorphTypes(self::FQCN, self::ALIAS);
$this->rewritePairKeys(self::FQCN, self::ALIAS);
}
public function down(): void
{
$this->rewriteMorphTypes(self::ALIAS, self::FQCN);
$this->rewritePairKeys(self::ALIAS, self::FQCN);
}
private function rewriteMorphTypes(string $from, string $to): void
{
if (Schema::hasTable('v5_resource_connections')) {
foreach (['resource_one_type', 'resource_two_type'] as $column) {
DB::table('v5_resource_connections')
->where($column, $from)
->update([$column => $to]);
}
}
if (Schema::hasTable('v5_resource_connection_rules')) {
foreach (['source_resource_type', 'target_resource_type'] as $column) {
DB::table('v5_resource_connection_rules')
->where($column, $from)
->update([$column => $to]);
}
}
}
private function rewritePairKeys(string $from, string $to): void
{
if (! Schema::hasTable('v5_resource_connections')) {
return;
}
// Backslash escaping in LIKE patterns differs between Postgres and
// SQLite, so match the FQCN in PHP instead of in SQL.
DB::table('v5_resource_connections')
->select(['id', 'resource_pair_key'])
->orderBy('id')
->chunkById(100, function ($connections) use ($from, $to): void {
foreach ($connections as $connection) {
if (! str_contains((string) $connection->resource_pair_key, $from)) {
continue;
}
DB::table('v5_resource_connections')
->where('id', $connection->id)
->update([
'resource_pair_key' => str_replace($from, $to, $connection->resource_pair_key),
]);
}
});
}
};
@@ -0,0 +1,74 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Replaces the unindexable v5_servers.capabilities JSON array of magic strings
* with indexed has_coold / is_ingress booleans. The Server model keeps
* exposing a computed `capabilities` array so serialized payloads stay
* identical.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->boolean('has_coold')->default(false)->index();
$table->boolean('is_ingress')->default(false)->index();
});
DB::table('v5_servers')
->select(['id', 'capabilities'])
->orderBy('id')
->chunkById(100, function ($servers): void {
foreach ($servers as $server) {
$capabilities = json_decode($server->capabilities ?? '[]', true);
if (! is_array($capabilities) || $capabilities === []) {
continue;
}
DB::table('v5_servers')->where('id', $server->id)->update([
'has_coold' => in_array('coold', $capabilities, true),
'is_ingress' => in_array('ingress', $capabilities, true),
]);
}
});
Schema::table('v5_servers', function (Blueprint $table) {
$table->dropColumn('capabilities');
});
}
public function down(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->json('capabilities')->nullable();
});
DB::table('v5_servers')
->select(['id', 'has_coold', 'is_ingress'])
->orderBy('id')
->chunkById(100, function ($servers): void {
foreach ($servers as $server) {
$capabilities = array_values(array_filter([
$server->has_coold ? 'coold' : null,
$server->is_ingress ? 'ingress' : null,
]));
DB::table('v5_servers')->where('id', $server->id)->update([
'capabilities' => json_encode($capabilities),
]);
}
});
Schema::table('v5_servers', function (Blueprint $table) {
$table->dropIndex(['has_coold']);
$table->dropIndex(['is_ingress']);
$table->dropColumn(['has_coold', 'is_ingress']);
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->string('agent_token_jti')->nullable()->after('coold_version');
});
}
public function down(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->dropColumn('agent_token_jti');
});
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('v5_revoked_agent_tokens', function (Blueprint $table) {
$table->id();
$table->string('jti')->unique();
$table->foreignId('server_id')->nullable();
$table->timestamp('revoked_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('v5_revoked_agent_tokens');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->timestamp('agent_token_expires_at')->nullable()->after('agent_token_jti');
});
}
public function down(): void
{
Schema::table('v5_servers', function (Blueprint $table) {
$table->dropColumn('agent_token_expires_at');
});
}
};