diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 6c9a54f77..b1333961d 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -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)) { diff --git a/app/Actions/Shared/ComplexStatusCheck.php b/app/Actions/Shared/ComplexStatusCheck.php index 3649be986..118ae33be 100644 --- a/app/Actions/Shared/ComplexStatusCheck.php +++ b/app/Actions/Shared/ComplexStatusCheck.php @@ -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); diff --git a/app/Actions/Shared/DockerInspectCache.php b/app/Actions/Shared/DockerInspectCache.php new file mode 100644 index 000000000..0f8303e56 --- /dev/null +++ b/app/Actions/Shared/DockerInspectCache.php @@ -0,0 +1,8 @@ +allApplicationsWithAdditionalServers->each(function ($application) { - ComplexStatusCheck::run($application); + ComplexStatusCheck::run($application, $dockerInspectCache); }); } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 217c82929..d142427fa 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -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