mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-29 01:54:37 +08:00
The retry labels (Traefik retry middleware, Caddy lb_try_duration) changed the labels of every application. On the first rolling update after a settings save, the old and the new container defined the same Traefik router differently, so Traefik dropped the route and returned 503 until the old container stopped. Remove the retry labels from this release. The rolling update drain wait stays.
206 lines
8.4 KiB
PHP
206 lines
8.4 KiB
PHP
<?php
|
|
|
|
use App\Enums\ProxyTypes;
|
|
use App\Jobs\ApplicationDeploymentJob;
|
|
use App\Livewire\Project\Application\Advanced;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Models\ApplicationPreview;
|
|
use App\Models\Environment;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Process;
|
|
use Illuminate\Support\Sleep;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Server::flushIdentityMap();
|
|
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
|
config(['constants.ssh.mux_enabled' => false]);
|
|
});
|
|
|
|
class TestableRollingUpdateDrainDeploymentJob extends ApplicationDeploymentJob
|
|
{
|
|
public function __construct() {}
|
|
}
|
|
|
|
function createApplicationForRollingUpdateDrainTest(string $proxyType = 'TRAEFIK', bool $exactLabels = false): Application
|
|
{
|
|
$team = Team::factory()->create();
|
|
$project = Project::factory()->create(['team_id' => $team->id]);
|
|
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
|
$server = Server::factory()->create([
|
|
'team_id' => $team->id,
|
|
'private_key_id' => PrivateKey::factory()->create(['team_id' => $team->id])->id,
|
|
'proxy' => ['type' => $proxyType],
|
|
]);
|
|
$server->settings->update(['generate_exact_labels' => $exactLabels]);
|
|
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
|
|
|
return Application::factory()->createOne([
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
'fqdn' => 'https://example.com',
|
|
'ports_exposes' => '3000',
|
|
])->fresh();
|
|
}
|
|
|
|
/**
|
|
* Runs the drain step of a rolling update against a faked server and returns the deployment log lines.
|
|
*
|
|
* @param array<string, string>|null $oldProxyLabels labels of the running old container, null when there is none
|
|
* @param array<string, string> $newProxyLabels
|
|
* @return list<string>
|
|
*/
|
|
function runRollingUpdateDrainStep(Application $application, bool $newVersionIsHealthy, ?array $oldProxyLabels = ['traefik.enable' => 'true'], array $newProxyLabels = ['traefik.enable' => 'true']): array
|
|
{
|
|
Process::fake(function ($process) use ($application, $oldProxyLabels, $newProxyLabels) {
|
|
$command = is_array(remoteProcessScript($process)) ? implode(' ', remoteProcessScript($process)) : remoteProcessScript($process);
|
|
if (str_contains($command, 'docker inspect')) {
|
|
return Process::result(output: json_encode(['coolify.name' => 'new', ...$newProxyLabels])."\n".json_encode(['coolify.name' => 'old', ...$oldProxyLabels]));
|
|
}
|
|
if (str_contains($command, 'docker ps') && str_contains($command, "coolify.applicationUuid={$application->uuid}")) {
|
|
$containers = [['ID' => 'new-id', 'Names' => 'app-new', 'State' => 'running', 'Labels' => '']];
|
|
if ($oldProxyLabels !== null) {
|
|
$containers[] = ['ID' => 'old-id', 'Names' => 'app-old', 'State' => 'running', 'Labels' => ''];
|
|
}
|
|
|
|
return Process::result(output: collect($containers)->map(fn (array $container) => json_encode($container))->implode("\n"));
|
|
}
|
|
|
|
return Process::result(output: '');
|
|
});
|
|
|
|
$logs = [];
|
|
$queue = Mockery::mock(ApplicationDeploymentQueue::class)->makePartial();
|
|
$queue->shouldReceive('addLogEntry')->andReturnUsing(function (string $message) use (&$logs) {
|
|
$logs[] = $message;
|
|
});
|
|
|
|
$job = new TestableRollingUpdateDrainDeploymentJob;
|
|
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
|
foreach ([
|
|
'application' => $application->fresh(),
|
|
'application_deployment_queue' => $queue,
|
|
'deployment_uuid' => 'rolling-update-drain',
|
|
'server' => $application->destination->server,
|
|
'container_name' => 'app-new',
|
|
'pull_request_id' => 0,
|
|
] as $property => $value) {
|
|
$reflection->getProperty($property)->setValue($job, $value);
|
|
}
|
|
|
|
Cache::forget('deployment-new-version-healthy:rolling-update-drain');
|
|
if ($newVersionIsHealthy) {
|
|
Cache::put('deployment-new-version-healthy:rolling-update-drain', true);
|
|
}
|
|
|
|
$reflection->getMethod('wait_for_proxy_to_route_new_container')->invoke($job);
|
|
|
|
return $logs;
|
|
}
|
|
|
|
it('keeps the application proxy labels of v4.5.0 without a retry, so a redeploy after an upgrade does not change them', function (string $proxyType, bool $exactLabels) {
|
|
$application = createApplicationForRollingUpdateDrainTest($proxyType, $exactLabels);
|
|
|
|
expect(implode("\n", generateLabelsApplication($application)))
|
|
->not->toContain('retry')
|
|
->not->toContain('lb_try_duration');
|
|
})->with([
|
|
'Traefik generic labels' => [ProxyTypes::TRAEFIK->value, false],
|
|
'Traefik exact labels' => [ProxyTypes::TRAEFIK->value, true],
|
|
'Caddy exact labels' => [ProxyTypes::CADDY->value, true],
|
|
]);
|
|
|
|
it('does not add retry labels to preview deployments', function () {
|
|
$application = createApplicationForRollingUpdateDrainTest();
|
|
$preview = ApplicationPreview::forceCreate([
|
|
'application_id' => $application->id,
|
|
'pull_request_id' => 7,
|
|
'pull_request_html_url' => 'https://github.com/coollabsio/coolify/pull/7',
|
|
'fqdn' => 'https://pr-7.example.com',
|
|
]);
|
|
|
|
expect(implode("\n", generateLabelsApplication($application, $preview)))
|
|
->not->toContain('retry')
|
|
->not->toContain('lb_try_duration');
|
|
});
|
|
|
|
it('waits for the proxy before old containers stop when the new container is healthy', function () {
|
|
Sleep::fake();
|
|
$application = createApplicationForRollingUpdateDrainTest();
|
|
|
|
$logs = runRollingUpdateDrainStep($application, newVersionIsHealthy: true);
|
|
|
|
Sleep::assertSequence([Sleep::for(DEFAULT_ROLLING_UPDATE_DRAIN_SECONDS)->seconds()]);
|
|
expect($logs)->toBe(['Waiting 5 seconds for the proxy to send traffic to the new container.']);
|
|
});
|
|
|
|
it('does not wait when the drain time is 0 or the new container is not healthy', function (int $drainSeconds, bool $newVersionIsHealthy) {
|
|
Sleep::fake();
|
|
$application = createApplicationForRollingUpdateDrainTest();
|
|
$application->settings->update(['rolling_update_drain_seconds' => $drainSeconds]);
|
|
|
|
$logs = runRollingUpdateDrainStep($application, $newVersionIsHealthy);
|
|
|
|
Sleep::assertNeverSlept();
|
|
expect($logs)->toBe([]);
|
|
})->with([
|
|
'drain turned off' => [0, true],
|
|
'new container not healthy' => [5, false],
|
|
]);
|
|
|
|
it('does not wait when no old container runs', function () {
|
|
Sleep::fake();
|
|
$application = createApplicationForRollingUpdateDrainTest();
|
|
|
|
$logs = runRollingUpdateDrainStep($application, newVersionIsHealthy: true, oldProxyLabels: null);
|
|
|
|
Sleep::assertNeverSlept();
|
|
expect($logs)->toBe([]);
|
|
});
|
|
|
|
it('stops the old containers without waiting when the proxy labels changed', function () {
|
|
Sleep::fake();
|
|
$application = createApplicationForRollingUpdateDrainTest();
|
|
|
|
$logs = runRollingUpdateDrainStep(
|
|
$application,
|
|
newVersionIsHealthy: true,
|
|
oldProxyLabels: ['traefik.http.routers.http-0-app.middlewares' => 'gzip-sse'],
|
|
newProxyLabels: ['traefik.http.routers.http-0-app.middlewares' => 'gzip-sse,retry-app'],
|
|
);
|
|
|
|
Sleep::assertNeverSlept();
|
|
expect($logs)->toBe(['The proxy labels changed, so the old containers stop without waiting.']);
|
|
});
|
|
|
|
it('saves the rolling update drain time on the advanced page', function () {
|
|
$user = User::factory()->create();
|
|
$application = createApplicationForRollingUpdateDrainTest();
|
|
$application->team()->members()->attach($user->id, ['role' => 'owner']);
|
|
$this->actingAs($user);
|
|
session(['currentTeam' => $application->team()]);
|
|
|
|
Livewire::test(Advanced::class, ['application' => $application])
|
|
->assertSet('rollingUpdateDrainSeconds', DEFAULT_ROLLING_UPDATE_DRAIN_SECONDS)
|
|
->set('rollingUpdateDrainSeconds', 20)
|
|
->call('saveRollingUpdateDrainSeconds')
|
|
->assertHasNoErrors()
|
|
->set('rollingUpdateDrainSeconds', 301)
|
|
->call('saveRollingUpdateDrainSeconds')
|
|
->assertHasErrors(['rollingUpdateDrainSeconds' => ['max']]);
|
|
|
|
expect($application->settings->fresh()->rolling_update_drain_seconds)->toBe(20);
|
|
});
|