mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-20 02:17:09 +00:00
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:
1 parent
1cb1d028de
commit
d0247d3b09
45 files changed
+273
-218
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
+9
-5
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\V5;
|
||||
|
||||
class V5Feature
|
||||
{
|
||||
private const DEVELOPMENT_ENVIRONMENTS = ['local', 'development', 'dev', 'testing'];
|
||||
|
||||
public static function enabled(): bool
|
||||
{
|
||||
return (bool) config('v5.enabled');
|
||||
}
|
||||
|
||||
public static function enabledForEnvironment(string $environment): bool
|
||||
{
|
||||
return in_array($environment, self::DEVELOPMENT_ENVIRONMENTS, true);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user