mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
Status checks now queue a delayed check for each application, preview, service part, or database that goes from up to exited. After 2 minutes it sends the "Resource status changes" notification if the resource is still stopped, no deployment runs, and Coolify did not stop it. Each resource sends at most one notification in 15 minutes. Closes #9493
317 lines
9.2 KiB
PHP
317 lines
9.2 KiB
PHP
<?php
|
|
|
|
use App\Events\ServiceChecked;
|
|
use App\Jobs\NotifyResourceStoppedJob;
|
|
use App\Jobs\PushServerUpdateJob;
|
|
use App\Models\Environment;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\StandalonePostgresql;
|
|
use App\Models\Team;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
test('database last_online_at is not updated when status is unchanged', function () {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'running:healthy',
|
|
'last_online_at' => now()->subMinutes(5),
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
|
|
$data = [
|
|
'containers' => [
|
|
[
|
|
'name' => $database->uuid,
|
|
'state' => 'running',
|
|
'health_status' => 'healthy',
|
|
'labels' => [
|
|
'coolify.managed' => 'true',
|
|
'coolify.type' => 'database',
|
|
'com.docker.compose.service' => $database->uuid,
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$oldLastOnline = $database->last_online_at;
|
|
|
|
$job = new PushServerUpdateJob($server, $data);
|
|
$job->handle();
|
|
|
|
$database->refresh();
|
|
|
|
expect((string) $database->last_online_at)->toBe((string) $oldLastOnline);
|
|
expect($database->status)->toBe('running:healthy');
|
|
});
|
|
|
|
test('database status is updated when container status changes', function () {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'exited',
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
|
|
$data = [
|
|
'containers' => [
|
|
[
|
|
'name' => $database->uuid,
|
|
'state' => 'running',
|
|
'health_status' => 'healthy',
|
|
'labels' => [
|
|
'coolify.managed' => 'true',
|
|
'coolify.type' => 'database',
|
|
'com.docker.compose.service' => $database->uuid,
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$job = new PushServerUpdateJob($server, $data);
|
|
$job->handle();
|
|
|
|
$database->refresh();
|
|
|
|
expect($database->status)->toBe('running:healthy');
|
|
});
|
|
|
|
test('database is not marked exited when an incomplete containers snapshot is empty', function () {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'running:healthy',
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
|
|
// Empty containers = Sentinel might have failed, should NOT mark as exited
|
|
$data = [
|
|
'containers' => [],
|
|
'snapshot' => ['complete' => false],
|
|
];
|
|
|
|
$job = new PushServerUpdateJob($server, $data);
|
|
$job->handle();
|
|
|
|
$database->refresh();
|
|
|
|
// Status should remain running, NOT be set to exited
|
|
expect($database->status)->toBe('running:healthy');
|
|
});
|
|
|
|
test('database is marked exited when a complete containers snapshot is empty', function () {
|
|
Queue::fake([NotifyResourceStoppedJob::class]);
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'running:healthy',
|
|
]);
|
|
|
|
$data = [
|
|
'containers' => [],
|
|
'snapshot' => ['complete' => true],
|
|
];
|
|
|
|
(new PushServerUpdateJob($database->destination->server, $data))->handle();
|
|
|
|
expect($database->refresh()->status)->toBe('exited:unhealthy');
|
|
});
|
|
|
|
function createPushUpdatePostgresql(Team $team, array $attributes = []): StandalonePostgresql
|
|
{
|
|
$lastOnlineAt = $attributes['last_online_at'] ?? null;
|
|
unset($attributes['last_online_at']);
|
|
|
|
$server = Server::factory()->create(['team_id' => $team->id]);
|
|
$destination = StandaloneDocker::where('server_id', $server->id)->first()
|
|
?? StandaloneDocker::factory()->create(['server_id' => $server->id]);
|
|
$project = Project::factory()->create(['team_id' => $team->id]);
|
|
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
|
|
|
$database = StandalonePostgresql::create(array_merge([
|
|
'uuid' => (string) str()->uuid(),
|
|
'name' => 'postgres-'.str()->random(8),
|
|
'postgres_password' => 'secret',
|
|
'status' => 'exited',
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
'environment_id' => $environment->id,
|
|
], $attributes));
|
|
|
|
if ($lastOnlineAt !== null) {
|
|
$database->forceFill(['last_online_at' => $lastOnlineAt])->saveQuietly();
|
|
}
|
|
|
|
return $database;
|
|
}
|
|
|
|
test('partial sentinel snapshots do not mark missing databases as exited', function () {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'running:healthy',
|
|
'is_public' => true,
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
Queue::fake();
|
|
|
|
$data = [
|
|
'snapshot' => [
|
|
'version' => 1,
|
|
'complete' => false,
|
|
],
|
|
'containers' => [
|
|
[
|
|
'name' => 'unrelated-container',
|
|
'state' => 'running',
|
|
'health_status' => 'healthy',
|
|
'labels' => [
|
|
'coolify.managed' => 'true',
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$job = new PushServerUpdateJob($server, $data);
|
|
$job->handle();
|
|
|
|
$database->refresh();
|
|
|
|
expect($database->status)->toBe('running:healthy');
|
|
});
|
|
|
|
test('partial sentinel snapshots apply directly observed database status updates', function () {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'exited',
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
Queue::fake();
|
|
|
|
$data = [
|
|
'snapshot' => [
|
|
'version' => 1,
|
|
'complete' => false,
|
|
],
|
|
'containers' => [
|
|
[
|
|
'name' => $database->uuid,
|
|
'state' => 'running',
|
|
'health_status' => 'healthy',
|
|
'labels' => [
|
|
'coolify.managed' => 'true',
|
|
'coolify.type' => 'database',
|
|
'com.docker.compose.service' => $database->uuid,
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$job = new PushServerUpdateJob($server, $data);
|
|
$job->handle();
|
|
|
|
$database->refresh();
|
|
|
|
expect($database->status)->toBe('running:healthy');
|
|
});
|
|
|
|
test('partial sentinel snapshots do not trigger missing tcp proxy recovery', function () {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'exited',
|
|
'is_public' => true,
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
Queue::fake();
|
|
Event::fake([ServiceChecked::class]);
|
|
|
|
$data = [
|
|
'snapshot' => [
|
|
'version' => 1,
|
|
'complete' => false,
|
|
],
|
|
'containers' => [
|
|
[
|
|
'name' => $database->uuid,
|
|
'state' => 'running',
|
|
'health_status' => 'healthy',
|
|
'labels' => [
|
|
'coolify.managed' => 'true',
|
|
'coolify.type' => 'database',
|
|
'com.docker.compose.service' => $database->uuid,
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$job = new PushServerUpdateJob($server, $data);
|
|
$job->handle();
|
|
|
|
$database->refresh();
|
|
|
|
expect($database->status)->toBe('running:healthy');
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
test('sentinel push broadcasts ServiceChecked to the server team after updating statuses', function (bool $complete) {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'exited',
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
Event::fake([ServiceChecked::class]);
|
|
|
|
$data = [
|
|
'snapshot' => [
|
|
'version' => 1,
|
|
'complete' => $complete,
|
|
],
|
|
'containers' => [
|
|
[
|
|
'name' => $database->uuid,
|
|
'state' => 'running',
|
|
'health_status' => 'healthy',
|
|
'labels' => [
|
|
'coolify.managed' => 'true',
|
|
'coolify.type' => 'database',
|
|
'com.docker.compose.service' => $database->uuid,
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$job = new PushServerUpdateJob($server, $data);
|
|
$job->handle();
|
|
|
|
expect($database->refresh()->status)->toBe('running:healthy');
|
|
Event::assertDispatched(ServiceChecked::class, fn (ServiceChecked $event) => $event->teamId === $team->id);
|
|
})->with([
|
|
'complete snapshot' => true,
|
|
'partial snapshot' => false,
|
|
]);
|
|
|
|
test('sentinel push does not broadcast ServiceChecked for an empty partial snapshot', function () {
|
|
$team = Team::factory()->create();
|
|
$database = createPushUpdatePostgresql($team, [
|
|
'status' => 'running:healthy',
|
|
]);
|
|
|
|
$server = $database->destination->server;
|
|
Event::fake([ServiceChecked::class]);
|
|
|
|
$job = new PushServerUpdateJob($server, [
|
|
'snapshot' => ['version' => 1, 'complete' => false],
|
|
'containers' => [],
|
|
]);
|
|
$job->handle();
|
|
|
|
Event::assertNotDispatched(ServiceChecked::class);
|
|
});
|