feat(v5): gate V5 to development environments only

Introduce V5Feature and config so V5 routes, jobs, commands, morph maps,
and model queries run only when enabled. Move V5 migrations to
migrations-v5 (loaded only when enabled), remove Flux from production
Docker/install paths, and add isolation tests.
This commit is contained in:
Andras Bacsai committed 2026-07-19 11:44:12 +02:00
1 parent 1cb1d028de
commit d0247d3b09
45 files changed
+273 -218

No files matched your search

+2 -2
View File
@@ -93,7 +93,7 @@ it('rewrites legacy fqcn morph rows to the v5 application alias', function () {
'updated_at' => now(),
]);
$migration = include database_path('migrations/2026_07_06_090000_v5_convert_resource_connection_morphs_to_aliases.php');
$migration = include database_path('migrations-v5/2026_07_06_090000_v5_convert_resource_connection_morphs_to_aliases.php');
$migration->up();
$connection = DB::table('v5_resource_connections')->where('uuid', 'legacy-connection')->first();
@@ -155,7 +155,7 @@ it('backfills the capability booleans from the legacy json column', function ()
['name' => 'empty', 'capabilities' => null],
]);
$migration = include database_path('migrations/2026_07_06_090100_v5_convert_server_capabilities_to_booleans.php');
$migration = include database_path('migrations-v5/2026_07_06_090100_v5_convert_server_capabilities_to_booleans.php');
$migration->up();
$serversByName = DB::table('v5_servers')->get()->keyBy('name');
+21 -21
View File
@@ -7,7 +7,7 @@ beforeEach(function () {
});
it('reuses existing projects instead of creating v5 projects', function () {
expect(file_exists(database_path('migrations/2026_06_04_050157_v5_create_projects_table.php')))->toBeFalse()
expect(file_exists(database_path('migrations-v5/2026_06_04_050157_v5_create_projects_table.php')))->toBeFalse()
->and(file_exists(app_path('Models/V5/Project.php')))->toBeFalse();
});
@@ -16,10 +16,10 @@ it('creates v5 cluster tables and lets each server belong to one cluster', funct
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$serverMigration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration->up();
expect(Schema::hasTable('v5_clusters'))->toBeTrue()
@@ -61,10 +61,10 @@ it('creates v5 server tables in the shared database', function () {
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$migration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$migration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$migration->up();
expect(Schema::hasTable('v5_servers'))->toBeTrue()
@@ -109,10 +109,10 @@ it('creates v5 server canvas columns for movable caddy ingress nodes', function
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$serverMigration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration->up();
expect(Schema::hasColumns('v5_servers', [
@@ -127,10 +127,10 @@ it('creates v5 server ingress columns', function () {
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$serverMigration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration->up();
expect(Schema::hasColumns('v5_servers', [
@@ -147,13 +147,13 @@ it('creates v5 application tables for dashboard canvas nodes', function () {
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$serverMigration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration->up();
$applicationMigration = include database_path('migrations/2026_06_19_140000_v5_create_applications_table.php');
$applicationMigration = include database_path('migrations-v5/2026_06_19_140000_v5_create_applications_table.php');
$applicationMigration->up();
expect(Schema::hasTable('v5_applications'))->toBeTrue()
@@ -188,13 +188,13 @@ it('creates v5 application domain tables for zero or more inbound routes', funct
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$serverMigration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration->up();
$applicationMigration = include database_path('migrations/2026_06_19_140000_v5_create_applications_table.php');
$applicationMigration = include database_path('migrations-v5/2026_06_19_140000_v5_create_applications_table.php');
$applicationMigration->up();
expect(Schema::hasTable('v5_application_domains'))->toBeTrue()
@@ -221,16 +221,16 @@ it('creates generic v5 resource connection tables', function () {
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$serverMigration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration->up();
$applicationMigration = include database_path('migrations/2026_06_19_140000_v5_create_applications_table.php');
$applicationMigration = include database_path('migrations-v5/2026_06_19_140000_v5_create_applications_table.php');
$applicationMigration->up();
$connectionMigration = include database_path('migrations/2026_06_19_142000_v5_create_resource_connections_table.php');
$connectionMigration = include database_path('migrations-v5/2026_06_19_142000_v5_create_resource_connections_table.php');
$connectionMigration->up();
expect(Schema::hasTable('v5_resource_connections'))->toBeTrue()
@@ -269,10 +269,10 @@ it('keeps v5 server fields in the initial migration', function () {
Schema::dropIfExists('v5_servers');
Schema::dropIfExists('v5_clusters');
$clusterMigration = include database_path('migrations/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration = include database_path('migrations-v5/2026_06_16_130649_v5_create_clusters_table.php');
$clusterMigration->up();
$serverMigration = include database_path('migrations/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration = include database_path('migrations-v5/2026_06_16_130650_v5_create_servers_table.php');
$serverMigration->up();
expect(Schema::hasColumns('v5_servers', [
@@ -0,0 +1,82 @@
<?php
use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Process\Process;
function inspectV5RegistrationForEnvironment(string $environment): array
{
$script = <<<'PHP'
$environment = $argv[1];
putenv("APP_ENV={$environment}");
$_ENV['APP_ENV'] = $environment;
$_SERVER['APP_ENV'] = $environment;
require 'vendor/autoload.php';
$app = require 'bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
echo json_encode([
'routes' => collect($app['router']->getRoutes()->getRoutes())
->map(fn ($route) => $route->uri())
->values()
->all(),
'migration_paths' => $app->make('migrator')->paths(),
'horizon_supervisors' => array_keys(config("horizon.environments.{$environment}", [])),
], JSON_THROW_ON_ERROR);
PHP;
$process = new Process([PHP_BINARY, '-r', $script, $environment], base_path());
$process->mustRun();
return json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR);
}
it('registers v5 routes, migrations, and workers in development environments', function (string $environment) {
$registration = inspectV5RegistrationForEnvironment($environment);
expect($registration['routes'])
->toContain('v5')
->toContain('api/v1/internal/flux/resource-status')
->and($registration['migration_paths'])->toContain(database_path('migrations-v5'))
->and($registration['horizon_supervisors'])->toContain('v5reconcile');
})->with(['local', 'development', 'dev', 'testing']);
it('does not register v5 routes, migrations, or workers outside development', function (string $environment) {
$registration = inspectV5RegistrationForEnvironment($environment);
expect($registration['routes'])
->not->toContain('v5')
->not->toContain('api/v1/internal/flux/resource-status')
->and($registration['migration_paths'])->not->toContain(database_path('migrations-v5'))
->and($registration['horizon_supervisors'])->not->toContain('v5reconcile');
})->with(['production', 'staging']);
it('keeps v5 schema changes out of the default migration path', function () {
$v5MigrationFiles = collect(glob(database_path('migrations/*.php')))
->filter(fn (string $path) => str_contains((string) file_get_contents($path), "'v5_"));
expect($v5MigrationFiles)->toBeEmpty();
});
it('does not ship the v5 Flux runtime in the production container', function () {
$productionCompose = file_get_contents(base_path('docker-compose.prod.yml'));
$productionDockerfile = file_get_contents(base_path('docker/production/Dockerfile'));
expect($productionCompose)
->not->toContain('COOLIFY_FLUX')
->not->toContain('/data/coolify/flux')
->and($productionDockerfile)
->not->toContain('COOLIFY_FLUX_VERSION')
->not->toContain('/usr/local/bin/flux');
});
it('blocks v5 console commands when v5 is disabled', function (string $command) {
config()->set('v5.enabled', false);
expect(Artisan::call($command))->toBe(1)
->and(Artisan::output())->toContain('V5 is only available in development environments.');
})->with([
'flux:dev',
'v5:flux-generate-keys',
'v5:sync-dev-lima-servers',
]);
@@ -0,0 +1,22 @@
<?php
use App\Models\Environment;
use App\Models\Project;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Support\V5TestSchema;
uses(RefreshDatabase::class);
it('keeps v4 project and environment checks working without the v5 schema', function () {
config()->set('v5.enabled', false);
$team = Team::factory()->create();
$project = Project::factory()->create(['team_id' => $team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
V5TestSchema::dropAllTables();
expect($project->isEmpty())->toBeTrue()
->and($environment->isEmpty())->toBeTrue();
});
+1 -1
View File
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Schema;
* build their schema by hand instead of running the full migration set.
*
* The column definitions must exactly mirror the final state produced by the
* database/migrations/*v5* files (including the 2026_07_05/2026_07_06
* database/migrations-v5 files (including the 2026_07_05/2026_07_06
* additions: status_observed_at, coold_version, has_coold/is_ingress booleans
* replacing the dropped capabilities json, and NOT NULL server uuids). Update
* this class in the same change as any v5 migration.