mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05: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:
@@ -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')),
|
||||
];
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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