mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-11 03:13:17 +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);
|
||||
}
|
||||
}
|
||||
+28
-27
@@ -1,7 +1,17 @@
|
||||
<?php
|
||||
|
||||
use App\Support\V5\V5Feature;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
$v5Enabled = V5Feature::enabledForEnvironment((string) env('APP_ENV', 'production'));
|
||||
$v5ReconcileSupervisor = $v5Enabled ? [
|
||||
'v5reconcile' => [
|
||||
'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,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Support\V5\V5Feature;
|
||||
|
||||
return [
|
||||
'enabled' => V5Feature::enabledForEnvironment((string) env('APP_ENV', 'production')),
|
||||
];
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
Whitespace-only changes.
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-1
@@ -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']);
|
||||
});
|
||||
|
||||
|
||||
+1
-2
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user