diff --git a/app/Console/Commands/FluxDev.php b/app/Console/Commands/FluxDev.php index 202abf91b..cf54e5bec 100644 --- a/app/Console/Commands/FluxDev.php +++ b/app/Console/Commands/FluxDev.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Services\Flux\AgentTokenIssuer; +use App\Support\V5\V5Feature; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; @@ -13,8 +14,7 @@ class FluxDev extends Command {host_id=coold-dev : Stable coold host id} {--caps= : Comma-separated host capabilities} {--ttl=3600 : Token lifetime in seconds} - {--output= : Optional path to write the token with 0600 permissions} - {--force : Allow running outside local/development environments}'; + {--output= : Optional path to write the token with 0600 permissions}'; protected $description = 'Run Flux development helpers.'; @@ -30,8 +30,8 @@ class FluxDev extends Command public function handle(AgentTokenIssuer $agentTokenIssuer): int { - if (! app()->environment(['local', 'development', 'testing']) && ! $this->option('force')) { - $this->error('This command is intended for development only. Use --force to override.'); + if (! V5Feature::enabled()) { + $this->error('V5 is only available in development environments.'); return self::FAILURE; } diff --git a/app/Console/Commands/V5FluxGenerateKeys.php b/app/Console/Commands/V5FluxGenerateKeys.php index 6f459ad59..1b66a11f7 100644 --- a/app/Console/Commands/V5FluxGenerateKeys.php +++ b/app/Console/Commands/V5FluxGenerateKeys.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Services\Flux\AgentTokenIssuer; +use App\Support\V5\V5Feature; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; @@ -25,6 +26,12 @@ class V5FluxGenerateKeys extends Command public function handle(AgentTokenIssuer $agentTokenIssuer): int { + if (! V5Feature::enabled()) { + $this->error('V5 is only available in development environments.'); + + return self::FAILURE; + } + $privateKeyPath = (string) config('flux.jwt_private_key_path'); $publicKeyPath = (string) config('flux.jwt_public_key_path'); diff --git a/app/Console/Commands/V5SyncDevLimaServers.php b/app/Console/Commands/V5SyncDevLimaServers.php index 256df7e7f..ebe9ceffc 100644 --- a/app/Console/Commands/V5SyncDevLimaServers.php +++ b/app/Console/Commands/V5SyncDevLimaServers.php @@ -6,6 +6,7 @@ use App\Actions\V5\Server\SyncDevLimaServers; use App\Models\PrivateKey; use App\Models\Team; use App\Models\User; +use App\Support\V5\V5Feature; use Illuminate\Console\Command; class V5SyncDevLimaServers extends Command @@ -15,15 +16,14 @@ class V5SyncDevLimaServers extends Command {--user-id=0 : User recorded as creator} {--private-key-id= : Optional private key used by the dev servers} {--cluster=Development-Lima : Cluster name for the dev Lima servers} - {--server=* : Server as name|host|ssh_user|ssh_port|wireguard_management_ip} - {--force : Allow running outside local/development environments}'; + {--server=* : Server as name|host|ssh_user|ssh_port|wireguard_management_ip}'; protected $description = 'Sync development Lima VMs into the v5 server/cluster tables.'; public function handle(): int { - if (! app()->environment(['local', 'development', 'testing']) && ! $this->option('force')) { - $this->error('This command is intended for development only. Use --force to override.'); + if (! V5Feature::enabled()) { + $this->error('V5 is only available in development environments.'); return self::FAILURE; } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index cb7995082..95dd654ba 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -18,6 +18,7 @@ use App\Jobs\UpdateCoolifyJob; use App\Jobs\V5ReconcileServersJob; use App\Jobs\V5RotateAgentTokensJob; use App\Models\InstanceSettings; +use App\Support\V5\V5Feature; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -51,13 +52,10 @@ class Kernel extends ConsoleKernel $this->scheduleInstance->command('sanctum:prune-expired --hours=1')->hourly()->onOneServer(); $this->scheduleInstance->job(new ApiTokenExpirationWarningJob)->hourly()->onOneServer(); - // V5 reconciliation loop: pull-based safety net for the push-only - // coold -> flux -> webhook status pipeline, plus container status pruning. - $this->scheduleInstance->job(new V5ReconcileServersJob)->everyFiveMinutes()->withoutOverlapping()->onOneServer(); - - // V5 host JWT rotation: re-mints and SSH-pushes a fresh host token - // before the on-disk token expires so coold reconnects stay authorized. - $this->scheduleInstance->job(new V5RotateAgentTokensJob)->everyFifteenMinutes()->withoutOverlapping()->onOneServer(); + if (V5Feature::enabled()) { + $this->scheduleInstance->job(new V5ReconcileServersJob)->everyFiveMinutes()->withoutOverlapping()->onOneServer(); + $this->scheduleInstance->job(new V5RotateAgentTokensJob)->everyFifteenMinutes()->withoutOverlapping()->onOneServer(); + } if (isDev()) { // Instance Jobs diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index acfa1c03f..185bc46e3 100644 --- a/app/Livewire/Project/Resource/Index.php +++ b/app/Livewire/Project/Resource/Index.php @@ -5,6 +5,7 @@ namespace App\Livewire\Project\Resource; use App\Models\Environment; use App\Models\Project; use App\Models\V5\Application as V5Application; +use App\Support\V5\V5Feature; use Illuminate\Support\Collection; use Livewire\Component; @@ -58,21 +59,26 @@ class Index extends Component // Load projects and environments for breadcrumb navigation $this->allProjects = Project::ownedByCurrentTeamCached(); + $environmentRelations = [ + 'applications:id,uuid,name,environment_id', + 'services:id,uuid,name,environment_id', + 'postgresqls:id,uuid,name,environment_id', + 'redis:id,uuid,name,environment_id', + 'mongodbs:id,uuid,name,environment_id', + 'mysqls:id,uuid,name,environment_id', + 'mariadbs:id,uuid,name,environment_id', + 'keydbs:id,uuid,name,environment_id', + 'dragonflies:id,uuid,name,environment_id', + 'clickhouses:id,uuid,name,environment_id', + ]; + + if (V5Feature::enabled()) { + $environmentRelations[] = 'v5Applications:id,uuid,name,environment_id,status'; + } + $this->allEnvironments = $project->environments() ->select('id', 'uuid', 'name', 'project_id') - ->with([ - 'applications:id,uuid,name,environment_id', - 'v5Applications:id,uuid,name,environment_id,status', - 'services:id,uuid,name,environment_id', - 'postgresqls:id,uuid,name,environment_id', - 'redis:id,uuid,name,environment_id', - 'mongodbs:id,uuid,name,environment_id', - 'mysqls:id,uuid,name,environment_id', - 'mariadbs:id,uuid,name,environment_id', - 'keydbs:id,uuid,name,environment_id', - 'dragonflies:id,uuid,name,environment_id', - 'clickhouses:id,uuid,name,environment_id', - ]) + ->with($environmentRelations) ->get(); $this->environment = $environment->loadCount([ @@ -105,8 +111,8 @@ class Index extends Component return $application; }); - $this->applications = $this->applications - ->merge(V5Application::query() + if (V5Feature::enabled()) { + $this->applications = $this->applications->merge(V5Application::query() ->where('team_id', currentTeam()->id) ->where('project_id', $this->project->id) ->where('environment_id', $this->environment->id) @@ -120,8 +126,10 @@ class Index extends Component ]); return $application; - })) - ->sortBy('name'); + })); + } + + $this->applications = $this->applications->sortBy('name'); // Load all database resources in a single query per type $databaseTypes = [ diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 0e5e3c673..e010ed45b 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Models\V5\Application as V5Application; use App\Models\V5\ResourceConnection as V5ResourceConnection; +use App\Support\V5\V5Feature; use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -56,8 +57,10 @@ class Environment extends BaseModel public function isEmpty() { - return ! V5Application::query()->where('environment_id', $this->id)->exists() && - ! V5ResourceConnection::query()->where('environment_id', $this->id)->exists() && + return (! V5Feature::enabled() || ( + ! V5Application::query()->where('environment_id', $this->id)->exists() && + ! V5ResourceConnection::query()->where('environment_id', $this->id)->exists() + )) && $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && diff --git a/app/Models/Project.php b/app/Models/Project.php index fe64e320d..ba560a840 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Models\V5\Application as V5Application; use App\Models\V5\ResourceConnection as V5ResourceConnection; +use App\Support\V5\V5Feature; use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -146,8 +147,10 @@ class Project extends BaseModel public function isEmpty() { - return ! V5Application::query()->where('project_id', $this->id)->exists() && - ! V5ResourceConnection::query()->where('project_id', $this->id)->exists() && + return (! V5Feature::enabled() || ( + ! V5Application::query()->where('project_id', $this->id)->exists() && + ! V5ResourceConnection::query()->where('project_id', $this->id)->exists() + )) && $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && diff --git a/app/Models/Team.php b/app/Models/Team.php index 665251653..914a68f0f 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -8,6 +8,7 @@ use App\Jobs\V5TeardownTeamJob; use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsPushover; +use App\Support\V5\V5Feature; use App\Notifications\Channels\SendsSlack; use App\Traits\HasNotificationSettings; use App\Traits\HasSafeStringAttribute; @@ -80,11 +81,14 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen // DB cascade removes the servers/applications/private keys. Captured // synchronously into a queued job so an unreachable host cannot block // or fail the team deletion (see V5TeardownTeamJob). Guarded so a v5 - // teardown problem never breaks v4 team deletion. - try { - V5TeardownTeamJob::dispatchForTeam($team); - } catch (\Throwable $exception) { - report($exception); + // teardown problem never breaks v4 team deletion. This is disabled + // with the rest of v5 outside development environments. + if (V5Feature::enabled()) { + try { + V5TeardownTeamJob::dispatchForTeam($team); + } catch (\Throwable $exception) { + report($exception); + } } RevokeUserTeamTokens::forTeam($team->id); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9e9bb1c6c..580aaa078 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; use App\Models\PersonalAccessToken; use App\Models\V5\Application; +use App\Support\V5\V5Feature; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\App; @@ -29,7 +30,12 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { $this->configureCommands(); - $this->configureMorphMap(); + + if (V5Feature::enabled()) { + $this->loadMigrationsFrom(database_path('migrations-v5')); + $this->configureMorphMap(); + } + $this->configureModels(); $this->configurePasswords(); $this->configureSanctumModel(); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 7c1969395..32ddc4ad5 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Support\V5\V5Feature; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Http\Request; @@ -34,10 +35,12 @@ class RouteServiceProvider extends ServiceProvider Route::prefix('webhooks') ->group(base_path('routes/webhooks.php')); - Route::middleware('v5.web') - ->prefix('v5') - ->as('v5.') - ->group(base_path('routes/v5.php')); + if (V5Feature::enabled()) { + Route::middleware('v5.web') + ->prefix('v5') + ->as('v5.') + ->group(base_path('routes/v5.php')); + } Route::middleware('web') ->group(base_path('routes/web.php')); @@ -60,13 +63,11 @@ class RouteServiceProvider extends ServiceProvider return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); }); - // v5 authenticated web endpoints run synchronous SSH/Flux work per - // request (connectivity checks, bootstrap, diagnostics). Throttle per - // user so a single member cannot pin FPM workers by hammering them, - // while leaving ample headroom for the canvas's 3s cluster polling. - RateLimiter::for('v5', function (Request $request) { - return Limit::perMinute(120)->by($request->user()?->id ?: $request->ip()); - }); + if (V5Feature::enabled()) { + RateLimiter::for('v5', function (Request $request) { + return Limit::perMinute(120)->by($request->user()?->id ?: $request->ip()); + }); + } RateLimiter::for('feedback', function (Request $request) { return Limit::perMinute(3)->by($request->user()?->id ?: $request->ip()); diff --git a/app/Support/V5/V5Feature.php b/app/Support/V5/V5Feature.php new file mode 100644 index 000000000..6ad2161f3 --- /dev/null +++ b/app/Support/V5/V5Feature.php @@ -0,0 +1,18 @@ + [ + 'autoScalingStrategy' => 'size', + 'minProcesses' => env('HORIZON_V5_RECONCILE_MIN_PROCESSES', 1), + 'maxProcesses' => env('HORIZON_V5_RECONCILE_MAX_PROCESSES', 1), + ], +] : []; + return [ /* @@ -193,23 +203,20 @@ return [ 'timeout' => env('HORIZON_TIMEOUT', 36000), ], - // Dedicated low-priority pool for the v5 reconcile + host-token rotation - // jobs (queue `v5-reconcile`, set via onQueue()). Isolated from the - // user-facing high/default deploy pool so a starved rotation cannot let - // host tokens drift to expiry, and so the 5-minute fleet fan-out never - // blocks deploys. - 'v5reconcile' => [ - 'connection' => 'redis', - 'balance' => env('HORIZON_V5_RECONCILE_BALANCE', 'false'), - 'queue' => 'v5-reconcile', - 'maxTime' => env('HORIZON_V5_RECONCILE_MAX_TIME', 0), - 'maxJobs' => 200, - 'memory' => 128, - 'tries' => 1, - 'nice' => 10, - 'sleep' => 3, - 'timeout' => env('HORIZON_V5_RECONCILE_TIMEOUT', 300), - ], + ...($v5Enabled ? [ + 'v5reconcile' => [ + 'connection' => 'redis', + 'balance' => env('HORIZON_V5_RECONCILE_BALANCE', 'false'), + 'queue' => 'v5-reconcile', + 'maxTime' => env('HORIZON_V5_RECONCILE_MAX_TIME', 0), + 'maxJobs' => 200, + 'memory' => 128, + 'tries' => 1, + 'nice' => 10, + 'sleep' => 3, + 'timeout' => env('HORIZON_V5_RECONCILE_TIMEOUT', 300), + ], + ] : []), ], 'environments' => [ @@ -221,11 +228,6 @@ return [ 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), ], - 'v5reconcile' => [ - 'autoScalingStrategy' => 'size', - 'minProcesses' => env('HORIZON_V5_RECONCILE_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_V5_RECONCILE_MAX_PROCESSES', 2), - ], ], 'local' => [ 's6' => [ @@ -235,11 +237,10 @@ return [ 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), ], - 'v5reconcile' => [ - 'autoScalingStrategy' => 'size', - 'minProcesses' => env('HORIZON_V5_RECONCILE_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_V5_RECONCILE_MAX_PROCESSES', 1), - ], + ...$v5ReconcileSupervisor, ], + 'development' => $v5ReconcileSupervisor, + 'dev' => $v5ReconcileSupervisor, + 'testing' => $v5ReconcileSupervisor, ], ]; diff --git a/config/v5.php b/config/v5.php new file mode 100644 index 000000000..7aa9cfdec --- /dev/null +++ b/config/v5.php @@ -0,0 +1,7 @@ + V5Feature::enabledForEnvironment((string) env('APP_ENV', 'production')), +]; diff --git a/database/migrations/2026_06_16_130649_v5_create_clusters_table.php b/database/migrations-v5/2026_06_16_130649_v5_create_clusters_table.php similarity index 100% rename from database/migrations/2026_06_16_130649_v5_create_clusters_table.php rename to database/migrations-v5/2026_06_16_130649_v5_create_clusters_table.php diff --git a/database/migrations/2026_06_16_130650_v5_create_servers_table.php b/database/migrations-v5/2026_06_16_130650_v5_create_servers_table.php similarity index 100% rename from database/migrations/2026_06_16_130650_v5_create_servers_table.php rename to database/migrations-v5/2026_06_16_130650_v5_create_servers_table.php diff --git a/database/migrations/2026_06_19_140000_v5_create_applications_table.php b/database/migrations-v5/2026_06_19_140000_v5_create_applications_table.php similarity index 100% rename from database/migrations/2026_06_19_140000_v5_create_applications_table.php rename to database/migrations-v5/2026_06_19_140000_v5_create_applications_table.php diff --git a/database/migrations/2026_06_19_142000_v5_create_resource_connections_table.php b/database/migrations-v5/2026_06_19_142000_v5_create_resource_connections_table.php similarity index 100% rename from database/migrations/2026_06_19_142000_v5_create_resource_connections_table.php rename to database/migrations-v5/2026_06_19_142000_v5_create_resource_connections_table.php diff --git a/database/migrations/2026_06_19_182231_create_container_statuses_table.php b/database/migrations-v5/2026_06_19_182231_create_container_statuses_table.php similarity index 100% rename from database/migrations/2026_06_19_182231_create_container_statuses_table.php rename to database/migrations-v5/2026_06_19_182231_create_container_statuses_table.php diff --git a/database/migrations/2026_07_05_215736_v5_add_status_lookup_indexes.php b/database/migrations-v5/2026_07_05_215736_v5_add_status_lookup_indexes.php similarity index 100% rename from database/migrations/2026_07_05_215736_v5_add_status_lookup_indexes.php rename to database/migrations-v5/2026_07_05_215736_v5_add_status_lookup_indexes.php diff --git a/database/migrations/2026_07_05_215736_v5_make_servers_uuid_not_null.php b/database/migrations-v5/2026_07_05_215736_v5_make_servers_uuid_not_null.php similarity index 100% rename from database/migrations/2026_07_05_215736_v5_make_servers_uuid_not_null.php rename to database/migrations-v5/2026_07_05_215736_v5_make_servers_uuid_not_null.php diff --git a/database/migrations/2026_07_05_222616_v5_add_status_observed_at_columns.php b/database/migrations-v5/2026_07_05_222616_v5_add_status_observed_at_columns.php similarity index 100% rename from database/migrations/2026_07_05_222616_v5_add_status_observed_at_columns.php rename to database/migrations-v5/2026_07_05_222616_v5_add_status_observed_at_columns.php diff --git a/database/migrations/2026_07_05_222940_v5_add_coold_version_to_servers_table.php b/database/migrations-v5/2026_07_05_222940_v5_add_coold_version_to_servers_table.php similarity index 100% rename from database/migrations/2026_07_05_222940_v5_add_coold_version_to_servers_table.php rename to database/migrations-v5/2026_07_05_222940_v5_add_coold_version_to_servers_table.php diff --git a/database/migrations/2026_07_06_090000_v5_convert_resource_connection_morphs_to_aliases.php b/database/migrations-v5/2026_07_06_090000_v5_convert_resource_connection_morphs_to_aliases.php similarity index 100% rename from database/migrations/2026_07_06_090000_v5_convert_resource_connection_morphs_to_aliases.php rename to database/migrations-v5/2026_07_06_090000_v5_convert_resource_connection_morphs_to_aliases.php diff --git a/database/migrations/2026_07_06_090100_v5_convert_server_capabilities_to_booleans.php b/database/migrations-v5/2026_07_06_090100_v5_convert_server_capabilities_to_booleans.php similarity index 100% rename from database/migrations/2026_07_06_090100_v5_convert_server_capabilities_to_booleans.php rename to database/migrations-v5/2026_07_06_090100_v5_convert_server_capabilities_to_booleans.php diff --git a/database/migrations/2026_07_06_100000_v5_add_agent_token_jti_to_servers_table.php b/database/migrations-v5/2026_07_06_100000_v5_add_agent_token_jti_to_servers_table.php similarity index 100% rename from database/migrations/2026_07_06_100000_v5_add_agent_token_jti_to_servers_table.php rename to database/migrations-v5/2026_07_06_100000_v5_add_agent_token_jti_to_servers_table.php diff --git a/database/migrations/2026_07_06_100100_v5_create_revoked_agent_tokens_table.php b/database/migrations-v5/2026_07_06_100100_v5_create_revoked_agent_tokens_table.php similarity index 100% rename from database/migrations/2026_07_06_100100_v5_create_revoked_agent_tokens_table.php rename to database/migrations-v5/2026_07_06_100100_v5_create_revoked_agent_tokens_table.php diff --git a/database/migrations/2026_07_06_110000_v5_add_agent_token_expires_at_to_servers_table.php b/database/migrations-v5/2026_07_06_110000_v5_add_agent_token_expires_at_to_servers_table.php similarity index 100% rename from database/migrations/2026_07_06_110000_v5_add_agent_token_expires_at_to_servers_table.php rename to database/migrations-v5/2026_07_06_110000_v5_add_agent_token_expires_at_to_servers_table.php diff --git a/database/migrations/2026_07_06_130000_v5_add_missing_uuid_columns.php b/database/migrations-v5/2026_07_06_130000_v5_add_missing_uuid_columns.php similarity index 100% rename from database/migrations/2026_07_06_130000_v5_add_missing_uuid_columns.php rename to database/migrations-v5/2026_07_06_130000_v5_add_missing_uuid_columns.php diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1f6b756a8..864dd475c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -11,7 +11,6 @@ services: - /data/coolify/databases:/var/www/html/storage/app/databases - /data/coolify/services:/var/www/html/storage/app/services - /data/coolify/backups:/var/www/html/storage/app/backups - - /data/coolify/flux:/var/www/html/storage/app/flux environment: - APP_ENV=${APP_ENV:-production} - PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M} @@ -24,10 +23,8 @@ services: - /data/coolify/source/.env ports: - "${APP_PORT:-8000}:8080" - - "${COOLIFY_FLUX_PORT:-6443}:6443" expose: - "${APP_PORT:-8000}" - - "${COOLIFY_FLUX_PORT:-6443}" healthcheck: test: curl --fail http://127.0.0.1:8080/api/health || exit 1 interval: 5s diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index cdea56467..34e4b6789 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -5,9 +5,6 @@ ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z # https://github.com/cloudflare/cloudflared/releases ARG CLOUDFLARED_VERSION=2025.7.0 -# https://github.com/coollabsio/coold/releases/tag/nightly -ARG COOLIFY_FLUX_VERSION=nightly -ARG COOLIFY_CLI_VERSION=nightly # https://www.postgresql.org/support/versioning/ # Note: We are using version 18 of the postgres client (while still using postgres 15 for the postgres server) as version 15 has been removed from Alpine 3.23+ https://pkgs.alpinelinux.org/packages?name=postgresql*-client&branch=v3.23&repo=&arch=x86_64&origin=&flagged=&maintainer= ARG POSTGRES_VERSION=18 @@ -76,11 +73,8 @@ FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION} ARG USER_ID ARG GROUP_ID ARG TARGETPLATFORM -ARG TARGETARCH ARG POSTGRES_VERSION ARG CLOUDFLARED_VERSION -ARG COOLIFY_FLUX_VERSION -ARG COOLIFY_CLI_VERSION ARG NGINX_VERSION ARG CI=true @@ -114,7 +108,6 @@ RUN --mount=type=cache,target=/var/cache/apk \ apk add --no-cache \ postgresql${POSTGRES_VERSION}-client \ openssh-client \ - openssl \ git \ git-lfs \ jq \ @@ -135,49 +128,6 @@ RUN mkdir -p /usr/local/bin && \ fi && \ chmod +x /usr/local/bin/cloudflared -# Install Flux from coold nightly release based on architecture -RUN set -eux; \ - mkdir -p /usr/local/bin /run/coolify /etc/coolify; \ - chown -R www-data:www-data /run/coolify /etc/coolify; \ - case "${TARGETARCH:-}" in \ - amd64|arm64) FLUX_ARCH="${TARGETARCH}" ;; \ - "") \ - case "$(uname -m)" in \ - x86_64) FLUX_ARCH="amd64" ;; \ - aarch64) FLUX_ARCH="arm64" ;; \ - *) echo "unsupported Flux arch: $(uname -m)" >&2; exit 1 ;; \ - esac ;; \ - *) echo "unsupported Flux TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ - esac; \ - curl -fsSL --retry 3 --max-time 120 \ - -o /tmp/flux.tar.gz \ - "https://github.com/coollabsio/coold/releases/download/${COOLIFY_FLUX_VERSION}/flux-linux-musl-${FLUX_ARCH}.tar.gz"; \ - tar -xzf /tmp/flux.tar.gz -C /tmp; \ - test -f /tmp/flux; \ - install -m 0755 /tmp/flux /usr/local/bin/flux; \ - rm -f /tmp/flux /tmp/flux.tar.gz - -# Install coolify from coold nightly release based on architecture -RUN set -eux; \ - mkdir -p /usr/local/bin; \ - case "${TARGETARCH:-}" in \ - amd64|arm64) COOLIFY_CLI_ARCH="${TARGETARCH}" ;; \ - "") \ - case "$(uname -m)" in \ - x86_64) COOLIFY_CLI_ARCH="amd64" ;; \ - aarch64) COOLIFY_CLI_ARCH="arm64" ;; \ - *) echo "unsupported coolify arch: $(uname -m)" >&2; exit 1 ;; \ - esac ;; \ - *) echo "unsupported coolify TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ - esac; \ - curl -fsSL --retry 3 --max-time 120 \ - -o /tmp/coolify.tar.gz \ - "https://github.com/coollabsio/coold/releases/download/${COOLIFY_CLI_VERSION}/coolify-linux-musl-${COOLIFY_CLI_ARCH}.tar.gz"; \ - tar -xzf /tmp/coolify.tar.gz -C /tmp; \ - test -f /tmp/coolify; \ - install -m 0755 /tmp/coolify /usr/local/bin/coolify; \ - rm -f /tmp/coolify /tmp/coolify.tar.gz - # Configure PHP COPY docker/production/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini ENV PHP_OPCACHE_ENABLE=1 diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-script b/docker/production/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-script deleted file mode 100644 index e69de29bb..000000000 diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/run b/docker/production/etc/s6-overlay/s6-rc.d/flux/run deleted file mode 100755 index 9d8598cf9..000000000 --- a/docker/production/etc/s6-overlay/s6-rc.d/flux/run +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh - -cd /var/www/html - -. /etc/s6-overlay/scripts/container-role -role="$(coolify_container_role_value)" - -if ! coolify_container_has_role flux; then - echo " INFO Flux is disabled for role '$role', sleeping." - exec sleep infinity -fi - -if grep -qE '^COOLIFY_FLUX_ENABLED=false' .env 2>/dev/null || [ "${COOLIFY_FLUX_ENABLED:-}" = "false" ]; then - echo " INFO Flux is disabled, sleeping." - exec sleep infinity -fi - -export COOLIFY_FLUX_GRPC_BIND="${COOLIFY_FLUX_GRPC_BIND:-0.0.0.0:6443}" -export COOLIFY_FLUX_UNIX_SOCKET_PATH="${COOLIFY_FLUX_UNIX_SOCKET_PATH:-/run/coolify/flux.sock}" -export COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH="${COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH:-/var/www/html/storage/app/flux/jwt.priv}" -export COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH="${COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH:-/var/www/html/storage/app/flux/jwt.pub}" -export COOLIFY_FLUX_ALLOW_PUBLIC_BIND="${COOLIFY_FLUX_ALLOW_PUBLIC_BIND:-1}" -export COOLIFY_FLUX_LARAVEL_API_URL="${COOLIFY_FLUX_LARAVEL_API_URL:-http://127.0.0.1:8080}" -if [ -z "${COOLIFY_FLUX_LARAVEL_API_TOKEN:-}" ] && [ -f .env ]; then - COOLIFY_FLUX_LARAVEL_API_TOKEN="$(grep -E '^COOLIFY_FLUX_LARAVEL_API_TOKEN=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | sed "s/^['\"]//; s/['\"]$//")" -fi -export COOLIFY_FLUX_LARAVEL_API_TOKEN="${COOLIFY_FLUX_LARAVEL_API_TOKEN:-}" -if [ ! -r "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH" ]; then - echo " INFO Flux JWT public key not found at $COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH, generating keypair..." - mkdir -p "$(dirname "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH")" "$(dirname "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH")" - openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp" - chmod 0600 "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp" - mv "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp" "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH" - openssl pkey -in "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH" -pubout -out "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp" - chmod 0644 "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp" - mv "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp" "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH" -fi - -if ! /usr/local/bin/flux --version >/dev/null 2>&1; then - echo " ERROR Flux binary cannot run in this container. Check that the installed coold nightly Flux artifact is compatible with Alpine." - exec sleep infinity -fi - -mkdir -p "$(dirname "$COOLIFY_FLUX_UNIX_SOCKET_PATH")" - -echo " INFO Flux is enabled for role '$role', starting..." -exec /usr/local/bin/flux diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/type b/docker/production/etc/s6-overlay/s6-rc.d/flux/type deleted file mode 100644 index 1780f9f44..000000000 --- a/docker/production/etc/s6-overlay/s6-rc.d/flux/type +++ /dev/null @@ -1 +0,0 @@ -longrun \ No newline at end of file diff --git a/docker/production/etc/s6-overlay/s6-rc.d/user/contents.d/flux b/docker/production/etc/s6-overlay/s6-rc.d/user/contents.d/flux deleted file mode 100644 index e69de29bb..000000000 diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 33437aad5..f4458d440 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -228,12 +228,11 @@ if [ "$WARNING_SPACE" = true ]; then sleep 5 fi -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel,flux} +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic chown -R 9999:root /data/coolify -chown -R 9999:root /data/coolify/flux chmod -R 700 /data/coolify INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log" diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index a15ec39a8..635ddd77c 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -171,9 +171,6 @@ else log "Network 'coolify' already exists" fi -mkdir -p /data/coolify/flux -chown -R 9999:root /data/coolify/flux - # Check if Docker config file exists DOCKER_CONFIG_MOUNT="" if [ -f /root/.docker/config.json ]; then diff --git a/resources/views/livewire/project/resource/index.blade.php b/resources/views/livewire/project/resource/index.blade.php index 4caafb287..20411a734 100644 --- a/resources/views/livewire/project/resource/index.blade.php +++ b/resources/views/livewire/project/resource/index.blade.php @@ -91,7 +91,7 @@ ->merge($env->clickhouses ?? collect()); $envResources = collect() ->merge($env->applications->map(fn($app) => ['type' => 'application', 'resource' => $app])) - ->merge($env->v5Applications->map(fn($app) => ['type' => 'v5-application', 'resource' => $app])) + ->merge(config('v5.enabled') ? $env->v5Applications->map(fn($app) => ['type' => 'v5-application', 'resource' => $app]) : collect()) ->merge($envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db])) ->merge($env->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc])) ->sortBy(fn($item) => strtolower($item['resource']->name)); @@ -141,7 +141,7 @@ ->merge($env->clickhouses ?? collect()); $envResources = collect() ->merge($env->applications->map(fn($app) => ['type' => 'application', 'resource' => $app])) - ->merge($env->v5Applications->map(fn($app) => ['type' => 'v5-application', 'resource' => $app])) + ->merge(config('v5.enabled') ? $env->v5Applications->map(fn($app) => ['type' => 'v5-application', 'resource' => $app]) : collect()) ->merge($envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db])) ->merge($env->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc])); @endphp diff --git a/routes/api.php b/routes/api.php index 3c4c1024f..dff13905f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Api\DigitalOceanController; use App\Http\Controllers\Api\GithubController; use App\Http\Controllers\Api\HetznerController; use App\Http\Controllers\Api\Internal\FluxResourceStatusController; +use App\Support\V5\V5Feature; use App\Http\Controllers\Api\OtherController; use App\Http\Controllers\Api\ProjectController; use App\Http\Controllers\Api\ResourcesController; @@ -287,7 +288,10 @@ Route::group([ Route::group([ 'prefix' => 'v1', ], function () { - Route::post('/internal/flux/resource-status', FluxResourceStatusController::class); + if (V5Feature::enabled()) { + Route::post('/internal/flux/resource-status', FluxResourceStatusController::class); + } + Route::post('/sentinel/push', [SentinelController::class, 'push']); }); diff --git a/scripts/install.sh b/scripts/install.sh index 25315c834..96449bb79 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -228,12 +228,11 @@ if [ "$WARNING_SPACE" = true ]; then sleep 5 fi -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel,flux} +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic chown -R 9999:root /data/coolify -chown -R 9999:root /data/coolify/flux chmod -R 700 /data/coolify INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 144dcb21c..206327f65 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -171,9 +171,6 @@ else log "Network 'coolify' already exists" fi -mkdir -p /data/coolify/flux -chown -R 9999:root /data/coolify/flux - # Fix SSH directory ownership if not owned by container user UID 9999 (fixes #6621) # Only changes owner — preserves existing group to respect custom setups SSH_OWNER=$(stat -c '%u' /data/coolify/ssh 2>/dev/null || echo "unknown") diff --git a/tests/Feature/V5/ModelHygieneTest.php b/tests/Feature/V5/ModelHygieneTest.php index c0c0729a9..dc9e62b4e 100644 --- a/tests/Feature/V5/ModelHygieneTest.php +++ b/tests/Feature/V5/ModelHygieneTest.php @@ -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'); diff --git a/tests/Feature/V5/V5MigrationSchemaTest.php b/tests/Feature/V5/V5MigrationSchemaTest.php index 9be2f1653..204002bba 100644 --- a/tests/Feature/V5/V5MigrationSchemaTest.php +++ b/tests/Feature/V5/V5MigrationSchemaTest.php @@ -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', [ diff --git a/tests/Feature/V5DevelopmentIsolationTest.php b/tests/Feature/V5DevelopmentIsolationTest.php new file mode 100644 index 000000000..74a1b3275 --- /dev/null +++ b/tests/Feature/V5DevelopmentIsolationTest.php @@ -0,0 +1,82 @@ +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', +]); diff --git a/tests/Feature/V5DisabledModelIsolationTest.php b/tests/Feature/V5DisabledModelIsolationTest.php new file mode 100644 index 000000000..62cdd4357 --- /dev/null +++ b/tests/Feature/V5DisabledModelIsolationTest.php @@ -0,0 +1,22 @@ +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(); +}); diff --git a/tests/Support/V5TestSchema.php b/tests/Support/V5TestSchema.php index 8ecf46f8a..fdc7e7f2b 100644 --- a/tests/Support/V5TestSchema.php +++ b/tests/Support/V5TestSchema.php @@ -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.