mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
feat(cdn): add Cloudflare cache purge to sync:cdn
Add a --purge option that purges the synced CDN files (and the legacy /coolify-nightly/ paths for nightly) from the Cloudflare cache, and remind the operator to run it after the CDN pull request is deployed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a14ef2d4ee
commit
332a970b27
4 files changed
+134
-8
No files matched your search
@@ -45,3 +45,6 @@ VITE_PORT=5173
|
||||
DUSK_DRIVER_URL=http://selenium:4444
|
||||
|
||||
# Special Keys for Andras
|
||||
# For Cloudflare cache purging (token needs Zone > Cache Purge)
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
CLOUDFLARE_ZONE_ID=
|
||||
@@ -16,7 +16,7 @@ class SyncCdn extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'sync:cdn';
|
||||
protected $signature = 'sync:cdn {--purge : Purge the Cloudflare cache for the CDN files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -42,6 +42,49 @@ class SyncCdn extends Command
|
||||
File::deleteDirectory($temporaryDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge the given CDN URLs from the Cloudflare cache.
|
||||
*
|
||||
* @param array<int, string> $urls
|
||||
*/
|
||||
private function purgeCloudflareCache(array $urls): bool
|
||||
{
|
||||
$apiToken = config('constants.cloudflare.api_token');
|
||||
$zoneId = config('constants.cloudflare.zone_id');
|
||||
|
||||
if (blank($apiToken) || blank($zoneId)) {
|
||||
$this->error('CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID must be set to purge the Cloudflare cache.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$this->info('Purging: '.$url);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withToken($apiToken)
|
||||
->timeout(30)
|
||||
->post("https://api.cloudflare.com/client/v4/zones/{$zoneId}/purge_cache", [
|
||||
'files' => array_values($urls),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Failed to purge Cloudflare cache: '.$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $response->successful() || $response->json('success') !== true) {
|
||||
$this->error('Failed to purge Cloudflare cache: '.$response->body());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info('Cloudflare cache purged.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch GitHub releases and sync to GitHub repository
|
||||
*/
|
||||
@@ -251,6 +294,7 @@ class SyncCdn extends Command
|
||||
],
|
||||
default: 'production',
|
||||
) === 'nightly';
|
||||
$cdn = 'https://cdn.coollabs.io';
|
||||
|
||||
$parent_dir = realpath(dirname(__FILE__).'/../../..');
|
||||
|
||||
@@ -282,9 +326,6 @@ class SyncCdn extends Command
|
||||
$install_script_location = "$parent_dir/other/nightly/$install_script";
|
||||
$versions_location = "$parent_dir/other/nightly/$versions";
|
||||
}
|
||||
$envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION';
|
||||
$this->info("About to sync $envLabel releases, versions, compose, and environment files to GitHub repository.");
|
||||
|
||||
if ($nightly) {
|
||||
$files = [
|
||||
$versions_location => 'json/coolify/nightly/versions.json',
|
||||
@@ -310,6 +351,23 @@ class SyncCdn extends Command
|
||||
}
|
||||
|
||||
$releasesTarget = $nightly ? 'json/coolify/nightly/releases.json' : 'json/coolify/releases.json';
|
||||
|
||||
if ($this->option('purge')) {
|
||||
$urls = array_map(
|
||||
fn (string $targetPath) => $cdn.'/'.substr($targetPath, strlen('json/')),
|
||||
[$releasesTarget, ...array_values($files)],
|
||||
);
|
||||
if ($nightly) {
|
||||
// Nightly scripts still download from the legacy /coolify-nightly/ path, which Cloudflare caches separately.
|
||||
$urls = [...$urls, ...str_replace('/coolify/nightly/', '/coolify-nightly/', $urls)];
|
||||
}
|
||||
|
||||
return $this->purgeCloudflareCache($urls) ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
$envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION';
|
||||
$this->info("About to sync $envLabel releases, versions, compose, and environment files to GitHub repository.");
|
||||
|
||||
$options = [$releasesTarget, ...array_values($files)];
|
||||
$selectedFiles = multiselect(
|
||||
label: 'Which files would you like to sync?',
|
||||
@@ -325,10 +383,12 @@ class SyncCdn extends Command
|
||||
fn (string $targetPath) => in_array($targetPath, $selectedFiles, true),
|
||||
);
|
||||
|
||||
if ($includeReleases) {
|
||||
$this->syncReleasesToGitHubRepo($files, $nightly);
|
||||
} else {
|
||||
$this->syncFilesToGitHubRepo($files, $nightly);
|
||||
$synced = $includeReleases
|
||||
? $this->syncReleasesToGitHubRepo($files, $nightly)
|
||||
: $this->syncFilesToGitHubRepo($files, $nightly);
|
||||
|
||||
if ($synced) {
|
||||
$this->info('After the CDN pull request is merged and deployed, run "php artisan sync:cdn --purge" to purge the Cloudflare cache.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Error: '.$e->getMessage());
|
||||
|
||||
@@ -136,6 +136,11 @@ return [
|
||||
'dev_webhook' => env('SERVEO_URL'),
|
||||
],
|
||||
|
||||
'cloudflare' => [
|
||||
'api_token' => env('CLOUDFLARE_API_TOKEN'),
|
||||
'zone_id' => env('CLOUDFLARE_ZONE_ID'),
|
||||
],
|
||||
|
||||
'server_checks' => [
|
||||
// Notification delay configuration for parallel server checks
|
||||
// Used for Traefik version checks and other future server check jobs
|
||||
|
||||
@@ -150,3 +150,61 @@ SH);
|
||||
'service-templates-latest.json',
|
||||
]],
|
||||
]);
|
||||
|
||||
it('purges the CDN files from the Cloudflare cache', function (string $environment, array $cdnDirectories) {
|
||||
config()->set('constants.cloudflare.api_token', 'test-token');
|
||||
config()->set('constants.cloudflare.zone_id', 'test-zone');
|
||||
|
||||
Http::fake([
|
||||
'https://api.cloudflare.com/*' => Http::response(['success' => true], 200),
|
||||
]);
|
||||
|
||||
$this->artisan('sync:cdn --purge')
|
||||
->expectsChoice('Which environment would you like to sync?', $environment, [
|
||||
'production' => 'Production',
|
||||
'nightly' => 'Nightly',
|
||||
])
|
||||
->assertExitCode(0);
|
||||
|
||||
Http::assertSentCount(1);
|
||||
Http::assertSent(fn ($request) => $request->method() === 'POST'
|
||||
&& $request->url() === 'https://api.cloudflare.com/client/v4/zones/test-zone/purge_cache'
|
||||
&& $request->hasHeader('Authorization', 'Bearer test-token')
|
||||
&& $request['files'] === collect($cdnDirectories)->flatMap(fn (string $cdnDirectory) => array_map(fn (string $file) => "https://cdn.coollabs.io/$cdnDirectory/$file", [
|
||||
'releases.json',
|
||||
'versions.json',
|
||||
'docker-compose.yml',
|
||||
'docker-compose.prod.yml',
|
||||
'.env.production',
|
||||
'install.sh',
|
||||
'upgrade.sh',
|
||||
'upgrade-postgres.sh',
|
||||
'service-templates-latest.json',
|
||||
]))->all());
|
||||
})->with([
|
||||
'production' => ['production', ['coolify']],
|
||||
'nightly with legacy path' => ['nightly', ['coolify/nightly', 'coolify-nightly']],
|
||||
]);
|
||||
|
||||
it('fails the Cloudflare purge without credentials or on an API error', function (?string $apiToken, array $response) {
|
||||
config()->set('constants.cloudflare.api_token', $apiToken);
|
||||
config()->set('constants.cloudflare.zone_id', 'test-zone');
|
||||
|
||||
Http::fake([
|
||||
'https://api.cloudflare.com/*' => Http::response($response, 400),
|
||||
]);
|
||||
|
||||
$this->artisan('sync:cdn --purge')
|
||||
->expectsChoice('Which environment would you like to sync?', 'production', [
|
||||
'production' => 'Production',
|
||||
'nightly' => 'Nightly',
|
||||
])
|
||||
->assertExitCode(1);
|
||||
|
||||
if ($apiToken === null) {
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
})->with([
|
||||
'missing token' => [null, []],
|
||||
'api error' => ['test-token', ['success' => false, 'errors' => [['code' => 10000, 'message' => 'Authentication error']]]],
|
||||
]);
|
||||
Reference in new issue
Block a user