mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(proxy): clear absent letsencrypt resolvers during migration
Set the resolver to null when saved Traefik configuration has no active letsencrypt resolver. Preserve existing settings for active or unknown configurations and log parsing failures. Add regression coverage for missing, active, unknown, and malformed configurations.
This commit is contained in:
+58
-4
@@ -2,18 +2,42 @@
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Add the Traefik certificate resolver setting. Servers whose saved Traefik configuration has no
|
||||
* letsencrypt resolver get no resolver, because Traefik ignored that label and only logged an error.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->string('traefik_certificate_resolver')->nullable()->default('letsencrypt');
|
||||
});
|
||||
if (! Schema::hasColumn('server_settings', 'traefik_certificate_resolver')) {
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->string('traefik_certificate_resolver')->nullable()->default('letsencrypt');
|
||||
});
|
||||
}
|
||||
|
||||
DB::table('servers')
|
||||
->select(['id', 'proxy'])
|
||||
->orderBy('id')
|
||||
->chunkById(100, function ($servers): void {
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
if ($this->hasNoLetsEncryptResolver($server->proxy)) {
|
||||
DB::table('server_settings')
|
||||
->where('server_id', $server->id)
|
||||
->where('traefik_certificate_resolver', 'letsencrypt')
|
||||
->update(['traefik_certificate_resolver' => null]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::warning("Could not check the Traefik certificate resolver of server {$server->id}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,4 +49,34 @@ return new class extends Migration
|
||||
$table->dropColumn('traefik_certificate_resolver');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* True only when the saved configuration is a Traefik compose file without an active
|
||||
* --certificatesresolvers.letsencrypt.* option. Unknown or missing configurations return false.
|
||||
*/
|
||||
private function hasNoLetsEncryptResolver(?string $proxy): bool
|
||||
{
|
||||
$proxy = json_decode($proxy ?? '', true);
|
||||
if (strtoupper((string) data_get($proxy, 'type')) !== 'TRAEFIK') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$configuration = data_get($proxy, 'last_saved_proxy_configuration');
|
||||
if (blank($configuration)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$traefik = collect(data_get(Yaml::parse($configuration), 'services', []))
|
||||
->first(fn ($service) => is_array($service) && str_contains((string) data_get($service, 'image'), 'traefik'));
|
||||
if (! $traefik) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$command = data_get($traefik, 'command', []);
|
||||
$options = is_array($command) ? $command : preg_split('/\s+/', (string) $command);
|
||||
|
||||
return collect($options)->doesntContain(
|
||||
fn ($option) => is_string($option) && str_starts_with(strtolower(trim($option, " \t'\"")), '--certificatesresolvers.letsencrypt.')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
$this->team = Team::factory()->create();
|
||||
});
|
||||
|
||||
function traefikResolverMigrationCompose(array $commands, string $image = 'traefik:v3.6'): string
|
||||
{
|
||||
$lines = collect($commands)->map(fn (string $command) => " - '{$command}'")->implode("\n");
|
||||
|
||||
return "name: coolify-proxy\nservices:\n traefik:\n image: '{$image}'\n command:\n{$lines}\n";
|
||||
}
|
||||
|
||||
function traefikResolverMigrationServer(Team $team, array $proxy): Server
|
||||
{
|
||||
$server = Server::factory()->create(['team_id' => $team->id, 'proxy' => $proxy]);
|
||||
expect($server->settings->fresh()->traefik_certificate_resolver)->toBe('letsencrypt');
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
function runTraefikCertificateResolverMigration(): void
|
||||
{
|
||||
(require database_path('migrations/2026_10_11_075120_add_traefik_certificate_resolver_to_server_settings_table.php'))->up();
|
||||
}
|
||||
|
||||
test('clears the resolver when the saved Traefik configuration has no letsencrypt resolver', function () {
|
||||
$compose = traefikResolverMigrationCompose(['--providers.file.directory=/traefik/dynamic/', '--entrypoints.https.address=:443'])
|
||||
."# --certificatesresolvers.letsencrypt.acme.httpchallenge=true\n";
|
||||
$server = traefikResolverMigrationServer($this->team, ['type' => 'TRAEFIK', 'last_saved_proxy_configuration' => $compose]);
|
||||
|
||||
runTraefikCertificateResolverMigration();
|
||||
|
||||
expect($server->settings->fresh()->traefik_certificate_resolver)->toBeNull();
|
||||
});
|
||||
|
||||
test('keeps letsencrypt when the resolver is active or the configuration is unknown', function (array $proxy) {
|
||||
$server = traefikResolverMigrationServer($this->team, $proxy);
|
||||
|
||||
runTraefikCertificateResolverMigration();
|
||||
|
||||
expect($server->settings->fresh()->traefik_certificate_resolver)->toBe('letsencrypt');
|
||||
})->with([
|
||||
'active resolver' => [['type' => 'TRAEFIK', 'last_saved_proxy_configuration' => traefikResolverMigrationCompose([
|
||||
'--providers.file.directory=/traefik/dynamic/',
|
||||
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true',
|
||||
])]],
|
||||
'no saved configuration' => [['type' => 'TRAEFIK']],
|
||||
'caddy proxy' => [['type' => 'CADDY', 'last_saved_proxy_configuration' => "services:\n caddy:\n image: 'lucaslorentz/caddy-docker-proxy:2.8-alpine'\n"]],
|
||||
'invalid yaml' => [['type' => 'TRAEFIK', 'last_saved_proxy_configuration' => "services: [\n"]],
|
||||
]);
|
||||
Reference in New Issue
Block a user