This commit is contained in:
Jonathan Ho 2026-03-11 03:17:22 +08:00 committed by GitHub
commit df28b09102
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 91 additions and 27 deletions

View file

@ -5,6 +5,7 @@ namespace App\Actions\Docker;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Actions\Shared\ComplexStatusCheck;
use App\Actions\Shared\DockerInspectCache;
use App\Events\ServiceChecked;
use App\Models\ApplicationPreview;
use App\Models\Server;
@ -47,10 +48,11 @@ class GetContainersStatus
}
$this->applications = $this->server->applications();
$skip_these_applications = collect([]);
$dockerInspectCache = new DockerInspectCache();
foreach ($this->applications as $application) {
if ($application->additional_servers->count() > 0) {
$skip_these_applications->push($application);
ComplexStatusCheck::run($application);
ComplexStatusCheck::run($application, $dockerInspectCache);
$this->applications = $this->applications->filter(function ($value, $key) use ($application) {
return $value->id !== $application->id;
});
@ -60,7 +62,12 @@ class GetContainersStatus
return ! $skip_these_applications->pluck('id')->contains($value->id);
});
if ($this->containers === null) {
['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
if (isset($dockerInspectCache->data[$this->server->id]) && !$this->server->isSwarm()) {
$this->containers = collect($dockerInspectCache->data[$this->server->id]);
$this->containerReplicates = collect([]);
} else {
['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
}
}
if (is_null($this->containers)) {

View file

@ -5,6 +5,7 @@ namespace App\Actions\Shared;
use App\Models\Application;
use App\Services\ContainerStatusAggregator;
use App\Traits\CalculatesExcludedStatus;
use App\Actions\Shared\DockerInspectCache;
use Lorisleiva\Actions\Concerns\AsAction;
class ComplexStatusCheck
@ -12,10 +13,21 @@ class ComplexStatusCheck
use AsAction;
use CalculatesExcludedStatus;
public function handle(Application $application)
public function handle(Application $application, DockerInspectCache $dockerInspectCache = new DockerInspectCache())
{
$servers = $application->additional_servers;
$servers->push($application->destination->server);
$serversToInspect = $servers->filter(fn($server) => !isset($dockerInspectCache->data[$server->id]));
if ($serversToInspect->isNotEmpty()) {
$results = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $serversToInspect, false);
foreach ($results as $serverId => $result) {
$dockerInspectCache->data[$serverId] = format_docker_command_output_to_json($result);
}
}
foreach ($servers as $server) {
$is_main_server = $application->destination->server->id === $server->id;
if (! $server->isFunctional()) {
@ -29,8 +41,15 @@ class ComplexStatusCheck
continue;
}
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false);
$containers = format_docker_command_output_to_json($containers);
$allContainers = $dockerInspectCache->data[$server->id];
$containers = collect($allContainers)->filter(function ($container) use ($application) {
$labels = data_get($container, 'Config.Labels', []);
$appId = $labels['coolify.applicationId'] ?? null;
$pullRequestId = $labels['coolify.pullRequestId'] ?? null;
return $appId !== null && intval($appId) === $application->id && $pullRequestId !== null && intval($pullRequestId) === 0;
});
if ($containers->count() > 0) {
$statusToSet = $this->aggregateContainerStatuses($application, $containers);

View file

@ -0,0 +1,8 @@
<?php
namespace App\Actions\Shared;
class DockerInspectCache
{
public array $data = [];
}

View file

@ -8,6 +8,7 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Server\StartLogDrain;
use App\Actions\Shared\ComplexStatusCheck;
use App\Actions\Shared\DockerInspectCache;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
@ -602,8 +603,9 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
private function updateAdditionalServersStatus()
{
$dockerInspectCache = new DockerInspectCache();
$this->allApplicationsWithAdditionalServers->each(function ($application) {
ComplexStatusCheck::run($application);
ComplexStatusCheck::run($application, $dockerInspectCache);
});
}

View file

@ -118,36 +118,64 @@ function instant_remote_process_with_timeout(Collection|array $command, Server $
);
}
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false, ?int $timeout = null, bool $disableMultiplexing = false): ?string
{
function instant_remote_process(
Collection|array $command,
Server|Collection|array $server,
bool $throwError = true,
bool $no_sudo = false,
?int $timeout = null,
bool $disableMultiplexing = false
): string|array|null {
$command = $command instanceof Collection ? $command->toArray() : $command;
if ($server->isNonRoot() && ! $no_sudo) {
$command = parseCommandsByLineForSudo(collect($command), $server);
}
$command_string = implode("\n", $command);
$effectiveTimeout = $timeout ?? config('constants.ssh.command_timeout');
// Use the variable server instead of servers for backward compatibility
$servers = $server instanceof Collection ? $server : collect(is_array($server) ? $server : [$server]);
return \App\Helpers\SshRetryHandler::retry(
function () use ($server, $command_string, $effectiveTimeout, $disableMultiplexing) {
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string, $disableMultiplexing);
$process = Process::timeout($effectiveTimeout)->run($sshCommand);
function () use ($servers, $command, $no_sudo, $timeout, $disableMultiplexing) {
$results = Process::concurrently(function ($pool) use (
$servers,
$command,
$no_sudo,
$timeout,
$disableMultiplexing
) {
foreach ($servers as $server) {
if ($server->isNonRoot() && ! $no_sudo) {
$command = parseCommandsByLineForSudo(collect($command), $server);
}
$command_string = implode("\n", $command);
$effectiveTimeout = $timeout ?? config('constants.ssh.command_timeout');
$output = trim($process->output());
$exitCode = $process->exitCode();
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string, $disableMultiplexing);
if ($exitCode !== 0) {
excludeCertainErrors($process->errorOutput(), $exitCode);
$pool->as($server->id)->timeout($effectiveTimeout)->command($sshCommand);
}
});
$outputs = [];
foreach ($results->collect() as $serverId => $process) {
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
excludeCertainErrors($process->errorOutput(), $exitCode);
}
// Sanitize output to ensure valid UTF-8 encoding
$output = $output === 'null' ? null : sanitize_utf8_text($output);
$outputs[$serverId] = $output;
}
// Sanitize output to ensure valid UTF-8 encoding
$output = $output === 'null' ? null : sanitize_utf8_text($output);
return $output;
if ($servers->count() === 1) {
return $outputs[$servers->first()->id];
}
return $outputs;
},
[
'server' => $server->ip,
'command_preview' => substr($command_string, 0, 100),
'server' => implode(', ', $servers->pluck('ip')->toArray()),
'command_preview' => substr(implode("\n", $command), 0, 100),
'function' => 'instant_remote_process',
],
$throwError