Files
coolify/tests/Feature/LogDrain/LogDrainConfigRefreshTest.php
T
Andras BacsaiandClaude Opus 5.5 8d07d178f0 fix(log-drain): send Coolify labels to Fluent Bit drains
Containers without the Coolify default variables sent no project,
environment, or service to the drain. The fluentd driver now also sends
the Coolify container labels; the built-in configs use them as a
fallback and add coolify.service_name.

Closes #8415

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 22:42:37 +01:00

180 lines
6.4 KiB
PHP

<?php
use App\Actions\Server\StartLogDrain;
use App\Jobs\PushServerUpdateJob;
use App\Jobs\ServerCheckJob;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Yaml\Yaml;
uses(RefreshDatabase::class);
beforeEach(function () {
Server::flushIdentityMap();
Cache::flush();
Notification::fake();
Storage::fake('ssh-keys');
Storage::fake('ssh-mux');
config(['constants.ssh.mux_enabled' => false]);
InstanceSettings::forceCreate(['id' => 0]);
$this->team = Team::factory()->create();
$this->server = Server::factory()->create([
'team_id' => $this->team->id,
'name' => 'log-drain-server',
'private_key_id' => PrivateKey::factory()->create(['team_id' => $this->team->id])->id,
]);
$this->server->settings()->update([
'is_reachable' => true,
'is_usable' => true,
'is_logdrain_axiom_enabled' => true,
'logdrain_axiom_dataset_name' => 'dataset',
'logdrain_axiom_api_key' => 'key',
]);
});
function sentinelPushWithLogDrain(Server $server, array $labels): void
{
(new PushServerUpdateJob($server->fresh(), [
'containers' => [
['name' => 'coolify-log-drain', 'state' => 'running', 'health_status' => 'unknown', 'labels' => ['coolify.managed' => 'true', ...$labels]],
],
'filesystem_usage_root' => ['used_percentage' => 10],
]))->handle();
}
function serverCheckWithLogDrain(Server $server, array $labels): void
{
$job = new ServerCheckJob($server->fresh());
$job->containers = collect([
['Name' => '/coolify-log-drain', 'State' => ['Status' => 'running'], 'Config' => ['Labels' => ['coolify.managed' => 'true', ...$labels]]],
]);
(fn () => $this->checkLogDrainContainer())->call($job);
}
/**
* @return Collection<int, string>
*/
function startLogDrainScripts(Server $server): Collection
{
$scripts = new Collection;
Process::fake(function ($process) use ($scripts) {
$scripts->push(remoteProcessScript($process));
return Process::result();
});
StartLogDrain::run($server->fresh());
return $scripts;
}
function generatedLogDrainFile(Collection $scripts, string $file): string
{
preg_match("/echo '([A-Za-z0-9+\\/=]*)' \\| base64 -d \\| tee \\S+\\/".preg_quote($file, '/').' /', $scripts->implode("\n"), $matches);
return base64_decode($matches[1]);
}
it('restarts a log drain that an older Coolify version started, once', function () {
Queue::fake();
sentinelPushWithLogDrain($this->server, []);
sentinelPushWithLogDrain($this->server, [StartLogDrain::CONFIG_VERSION_LABEL => '0']);
StartLogDrain::assertPushed(1);
});
it('keeps a log drain that has the current configuration', function () {
Queue::fake();
sentinelPushWithLogDrain($this->server, [StartLogDrain::CONFIG_VERSION_LABEL => StartLogDrain::CONFIG_VERSION]);
StartLogDrain::assertNotPushed();
});
it('restarts an outdated log drain in the SSH server check, once', function () {
Queue::fake();
serverCheckWithLogDrain($this->server, []);
serverCheckWithLogDrain($this->server, []);
StartLogDrain::assertPushed(1);
});
it('keeps a current log drain in the SSH server check', function () {
Queue::fake();
serverCheckWithLogDrain($this->server, [StartLogDrain::CONFIG_VERSION_LABEL => StartLogDrain::CONFIG_VERSION]);
StartLogDrain::assertNotPushed();
});
it('labels the log drain container with the current configuration version', function () {
$compose = Yaml::parse(generatedLogDrainFile(startLogDrainScripts($this->server), 'docker-compose.yml'));
expect($compose['services']['coolify-log-drain']['labels'])
->toContain(StartLogDrain::CONFIG_VERSION_LABEL.'='.StartLogDrain::CONFIG_VERSION);
});
it('pulls the new image before it removes the running log drain', function () {
$script = startLogDrainScripts($this->server)->implode("\n");
$pull = strpos($script, 'docker-compose.yml pull');
$remove = strpos($script, 'docker rm -f coolify-log-drain');
$start = strpos($script, 'docker-compose.yml up -d');
expect($pull)->toBeInt()
->and($remove)->toBeGreaterThan($pull)
->and($start)->toBeGreaterThan($remove);
});
it('keeps the running log drain when the custom config is not allowed', function () {
$this->server->settings->update([
'is_logdrain_axiom_enabled' => false,
'is_logdrain_custom_enabled' => true,
'logdrain_custom_config' => "[OUTPUT]\n Name exec\n",
]);
$scripts = new Collection;
Process::fake(function ($process) use ($scripts) {
$scripts->push(remoteProcessScript($process));
return Process::result();
});
expect(fn () => StartLogDrain::run($this->server->fresh()))->toThrow(Exception::class, 'exec plugin')
->and($scripts->implode("\n"))->not->toContain('docker rm -f coolify-log-drain');
});
/**
* Fails when the generated log drain files change. Bump StartLogDrain::CONFIG_VERSION, so that the
* server checks restart running log drains with the new files, then update the hash.
*/
it('changes the configuration version when the generated files change', function () {
$hashes = collect([
'newrelic' => ['is_logdrain_axiom_enabled' => false, 'is_logdrain_newrelic_enabled' => true, 'logdrain_newrelic_license_key' => 'key', 'logdrain_newrelic_base_uri' => 'https://log-api.newrelic.com/log/v1'],
'axiom' => [],
'custom' => ['is_logdrain_axiom_enabled' => false, 'is_logdrain_custom_enabled' => true, 'logdrain_custom_config' => "[OUTPUT]\n Name stdout\n", 'logdrain_custom_config_parser' => "[PARSER]\n Name p\n"],
])->map(function (array $settings) {
$this->server->settings->update($settings);
$scripts = startLogDrainScripts($this->server);
return collect(['fluent-bit.conf', 'parsers.conf', 'docker-compose.yml', 'README.md'])
->map(fn (string $file) => generatedLogDrainFile($scripts, $file))
->implode("\0");
});
expect([StartLogDrain::CONFIG_VERSION, hash('sha256', $hashes->implode("\0"))])
->toBe(['2', '4f4ceaf642f0de8009e256b54f2bbe2f842190297bbf2da92f2b31dd0a9baf3d']);
});