Fix api-v2 CORS at the edge without changing rollout flows (#330)

Add API-only Traefik CORS middleware labels while preserving configured origins and the existing rollout/load-balancer behavior.
This commit is contained in:
Jeppe B
2026-07-29 09:01:08 +02:00
committed by GitHub
parent 710baad28e
commit 448e0e50c2
6 changed files with 228 additions and 18 deletions
@@ -5,6 +5,7 @@ app_require('classes/coolify_manager.php');
use classes\coolify_api_client;
use classes\coolify_manager;
use classes\cors_policy;
class CoolifyManagerHetznerTargetSetFake
{
@@ -418,9 +419,10 @@ it('adds explicit Coolify application route labels for gateway API domains', fun
$payload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io', 'api-app-uuid', 8080, base64_encode(implode("\n", [
'custom.keep=true',
'traefik.http.routers.https-0-api-app-uuid.entryPoints=old',
'traefik.http.routers.https-0-api-app-uuid.middlewares=legacy',
'traefik.http.routers.https-0-api-app-uuid.tls.certresolver=dns-cloudflare',
'traefik.http.services.https-0-api-app-uuid.loadbalancer.server.port=9090',
])));
])), 'api', 'https://partner.example.test/app');
$labels = explode("\n", base64_decode($payload['custom_labels'], true));
expect($payload['domains'])->toBe('https://api-v2.truckwash.io:8080')
@@ -429,6 +431,12 @@ it('adds explicit Coolify application route labels for gateway API domains', fun
->and($labels)->toContain('custom.keep=true')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.rule=Host(`api-v2.truckwash.io`) && PathPrefix(`/`)')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.entryPoints=https')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.middlewares=https-0-api-app-uuid-cors,gzip')
->and($labels)->toContain('traefik.http.middlewares.https-0-api-app-uuid-cors.headers.accesscontrolallowcredentials=true')
->and($labels)->toContain(
'traefik.http.middlewares.https-0-api-app-uuid-cors.headers.accesscontrolalloworiginlist='
. implode(',', cors_policy::allowedOrigins('https://partner.example.test/app'))
)
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.tls.certresolver=letsencrypt')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.tls.domains[0].main=api-v2.truckwash.io')
->and($labels)->toContain('traefik.http.services.https-0-api-app-uuid.loadbalancer.server.port=8080');
@@ -449,21 +457,49 @@ it('adds explicit Coolify application route labels for gateway API domains', fun
'app' => 'frontend',
]))->toBe('https://api-v2.truckwash.io/internal/frontend');
$pathPayload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io/internal/api', 'api-app-uuid', 8080, '');
$pathPayload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io/internal/api', 'api-app-uuid', 8080, '', 'api', '*');
$pathLabels = explode("\n", base64_decode($pathPayload['custom_labels'], true));
expect($pathPayload['domains'])->toBe('https://api-v2.truckwash.io:8080/internal/api')
->and($pathLabels)->toContain('traefik.http.routers.https-0-api-app-uuid.rule=Host(`api-v2.truckwash.io`) && PathPrefix(`/internal/api`)')
->and($pathLabels)->toContain('traefik.http.middlewares.https-0-api-app-uuid-stripprefix.stripprefix.prefixes=/internal/api')
->and($pathLabels)->toContain('traefik.http.routers.https-0-api-app-uuid.middlewares=https-0-api-app-uuid-stripprefix,gzip');
->and($pathLabels)->toContain('traefik.http.middlewares.https-0-api-app-uuid-cors.headers.accesscontrolalloworiginlistregex=^(https?://[^/]+|capacitor://[^/]+)$')
->and($pathLabels)->toContain('traefik.http.routers.https-0-api-app-uuid.middlewares=https-0-api-app-uuid-cors,https-0-api-app-uuid-stripprefix,gzip');
$frontendPayload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io/internal/frontend', 'frontend-app-uuid', 80, '');
$frontendPayload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io/internal/frontend', 'frontend-app-uuid', 80, '', 'frontend');
$frontendLabels = explode("\n", base64_decode($frontendPayload['custom_labels'], true));
expect($frontendPayload['domains'])->toBe('https://api-v2.truckwash.io:80/internal/frontend')
->and($frontendLabels)->toContain('traefik.http.routers.https-0-frontend-app-uuid.rule=Host(`api-v2.truckwash.io`) && PathPrefix(`/internal/frontend`)')
->and($frontendLabels)->toContain('traefik.http.middlewares.https-0-frontend-app-uuid-stripprefix.stripprefix.prefixes=/internal/frontend')
->and($frontendLabels)->toContain('traefik.http.routers.https-0-frontend-app-uuid.middlewares=https-0-frontend-app-uuid-stripprefix,gzip');
expect(implode("\n", $frontendLabels))->not->toContain('-cors');
});
it('resolves gateway CORS from deployment context with the same precedence as runtime env', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'gatewayRouteTargetCorsConfig');
$config = $method->invoke(null, [
'deploy_context_json' => json_encode([
'coolify_env' => [
'CORS' => 'https://array.example.test/app',
],
'coolify_env_file' => "CORS=https://file.example.test/path\nOTHER=value",
'env' => "CORS=*\n",
]),
]);
expect($config)->toBe('*');
$requiredConfig = $method->invoke(null, [
'deploy_context_json' => json_encode([
'runtime_env' => [
'CORS' => 'https://partner.example.test/app',
],
]),
]);
expect(explode(',', $requiredConfig))
->toContain('https://partner.example.test')
->toContain('https://truckwash.io');
});
it('isolates and restores Hetzner load balancer IP targets for gateway certificate bootstrap', function (): void {