Fix: Prevent version downgrades and centralize CDN configuration (#7383)
## Root Cause Between Nov 25-26, a CDN redirect was added without curl's `-L` flag, causing version cache corruption and automatic downgrades. ## Three Critical Bugs Fixed ### Bug #1: CheckForUpdatesJob could overwrite newer cached version - Problem: CDN serving older version would overwrite local cache - Solution: Smart version merge - keep max Coolify version, update other components - Location: app/Jobs/CheckForUpdatesJob.php:33-52 ### Bug #2: Manual updates bypassed downgrade protection - Problem: Downgrade guard only applied to auto-updates - Solution: Always block downgrades for both manual and auto-updates - Location: app/Actions/Server/UpdateCoolify.php:65-75 ### Bug #3: Updates used stale local cache - Problem: Never validated cache against CDN at update time - Solution: Fetch fresh CDN data before executing updates - Location: app/Actions/Server/UpdateCoolify.php:34-49 ## Additional Improvement: Centralized CDN Configuration Added three new config keys for easy CDN management: - `cdn_url` - Base CDN URL (default: https://cdn.coollabs.io) - `versions_url` - Full versions.json URL - `upgrade_script_url` - Full upgrade.sh URL All configurable via environment variables: ```bash CDN_URL=https://cdn.coolify.io VERSIONS_URL=https://custom-cdn.example.com/versions.json UPGRADE_SCRIPT_URL=https://custom-cdn.example.com/upgrade.sh ``` ## Files Modified - config/constants.php - CDN configuration - app/Jobs/CheckForUpdatesJob.php - Smart version merge + centralized URL - app/Actions/Server/UpdateCoolify.php - Downgrade protection + fresh fetch + centralized URLs - app/Jobs/CheckHelperImageJob.php - Centralized URL - bootstrap/helpers/shared.php - Centralized URL ## Testing - ✅ All modified files pass Pint formatting - ✅ 78 unit tests pass (2 pre-existing failures unrelated to changes) ## Impact - No breaking changes - defaults to current CDN - Easy CDN migration via environment variables - Prevents all downgrade scenarios - Maintains independent Sentinel/Helper/Traefik updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0d7f777814
commit
d9774d2968
5 changed files with 72 additions and 9 deletions
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
|
|
@ -29,7 +31,25 @@ public function handle($manual_update = false)
|
|||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server, false, false);
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
|
||||
// Fetch fresh version from CDN instead of using cache
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->timeout(10)
|
||||
->get(config('constants.coolify.versions_url'));
|
||||
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$this->latestVersion = data_get($versions, 'coolify.v4.version');
|
||||
} else {
|
||||
// Fallback to cache if CDN unavailable
|
||||
Log::warning('Failed to fetch fresh version from CDN (unsuccessful response), using cache');
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Failed to fetch fresh version from CDN, using cache', ['error' => $e->getMessage()]);
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
}
|
||||
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
if (! $manual_update) {
|
||||
if (! $settings->is_auto_update_enabled) {
|
||||
|
|
@ -42,6 +62,20 @@ public function handle($manual_update = false)
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ALWAYS check for downgrades (even for manual updates)
|
||||
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
|
||||
Log::error('Downgrade prevented', [
|
||||
'target_version' => $this->latestVersion,
|
||||
'current_version' => $this->currentVersion,
|
||||
'manual_update' => $manual_update,
|
||||
]);
|
||||
throw new \Exception(
|
||||
"Cannot downgrade from {$this->currentVersion} to {$this->latestVersion}. ".
|
||||
'If you need to downgrade, please do so manually via Docker commands.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->update();
|
||||
$settings->new_version_available = false;
|
||||
$settings->save();
|
||||
|
|
@ -56,8 +90,9 @@ private function update()
|
|||
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
||||
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
||||
|
||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||
remote_process([
|
||||
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
|
||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
|
||||
], $this->server);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
|
@ -22,20 +23,44 @@ public function handle(): void
|
|||
return;
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
$response = Http::retry(3, 1000)->get(config('constants.coolify.versions_url'));
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
|
||||
$latest_version = data_get($versions, 'coolify.v4.version');
|
||||
$current_version = config('constants.coolify.version');
|
||||
|
||||
// Read existing cached version
|
||||
$existingVersions = null;
|
||||
$existingCoolifyVersion = null;
|
||||
if (File::exists(base_path('versions.json'))) {
|
||||
$existingVersions = json_decode(File::get(base_path('versions.json')), true);
|
||||
$existingCoolifyVersion = data_get($existingVersions, 'coolify.v4.version');
|
||||
}
|
||||
|
||||
// Detect CDN serving older Coolify version
|
||||
if ($existingCoolifyVersion && version_compare($latest_version, $existingCoolifyVersion, '<')) {
|
||||
Log::warning('CDN served older Coolify version', [
|
||||
'cdn_version' => $latest_version,
|
||||
'cached_version' => $existingCoolifyVersion,
|
||||
'current_version' => $current_version,
|
||||
]);
|
||||
|
||||
// Keep the NEWER Coolify version from cache, but update other components
|
||||
$versions['coolify']['v4']['version'] = $existingCoolifyVersion;
|
||||
$latest_version = $existingCoolifyVersion;
|
||||
}
|
||||
|
||||
// ALWAYS write versions.json (for Sentinel, Helper, Traefik updates)
|
||||
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
|
||||
|
||||
// Invalidate cache to ensure fresh data is loaded
|
||||
invalidate_versions_cache();
|
||||
|
||||
// Only mark new version available if Coolify version actually increased
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
$settings->update(['new_version_available' => true]);
|
||||
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
|
||||
|
||||
// Invalidate cache to ensure fresh data is loaded
|
||||
invalidate_versions_cache();
|
||||
} else {
|
||||
$settings->update(['new_version_available' => false]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public function __construct() {}
|
|||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
$response = Http::retry(3, 1000)->get(config('constants.coolify.versions_url'));
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$settings = instanceSettings();
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ function get_route_parameters(): array
|
|||
function get_latest_sentinel_version(): string
|
||||
{
|
||||
try {
|
||||
$response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
$response = Http::get(config('constants.coolify.versions_url'));
|
||||
$versions = $response->json();
|
||||
|
||||
return data_get($versions, 'coolify.sentinel.version');
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
|
||||
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'),
|
||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||
'cdn_url' => env('CDN_URL', 'https://cdn.coollabs.io'),
|
||||
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'),
|
||||
'upgrade_script_url' => env('UPGRADE_SCRIPT_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/upgrade.sh'),
|
||||
'releases_url' => 'https://cdn.coolify.io/releases.json',
|
||||
],
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue