>}> $servers * @return array{ * deployments: list, * services: list, * deployment: string, * service: string, * sources: list, * streamKey: string, * } */ public static function select(array $servers, string $deployment, string $service, bool $groupDeployments): array { $containers = self::containers($servers, $groupDeployments); if ($containers === []) { return self::emptySelection(); } $deploymentIds = $groupDeployments ? self::sortedDeploymentIds($containers) : []; $chosenDeployment = $groupDeployments ? self::pickDeployment($deploymentIds, self::normalizeDeployment($deployment)) : '0'; $visible = array_values(array_filter( $containers, fn (array $container): bool => $container['deployment'] === $chosenDeployment, )); if ($visible === []) { return self::emptySelection(); } $groups = self::groupByService($visible); $chosenKey = self::pickServiceKey($groups, $service); $sources = self::sourcesFor($groups[$chosenKey]); return [ 'deployments' => $groupDeployments ? self::deploymentOptions($deploymentIds, $containers) : [], 'services' => self::serviceOptions($groups), 'deployment' => $chosenDeployment, 'service' => count($groups) > 1 ? $chosenKey : '', 'sources' => $sources, 'streamKey' => self::streamKey($sources), ]; } /** * @return array{ * deployments: list, * services: list, * deployment: string, * service: string, * sources: list, * streamKey: string, * } */ private static function emptySelection(): array { return [ 'deployments' => [], 'services' => [], 'deployment' => '0', 'service' => '', 'sources' => [], 'streamKey' => '', ]; } /** * @param list>}> $servers * @return list */ private static function containers(array $servers, bool $groupDeployments): array { $containers = []; foreach (array_values($servers) as $index => $server) { foreach ($server['containers'] ?? [] as $container) { if (! is_array($container)) { continue; } $name = (string) data_get($container, 'Names', ''); if (! ValidationPatterns::isValidContainerName($name)) { continue; } $labels = self::labels($container); $state = strtolower(trim((string) data_get($container, 'State', 'running'))); $containers[] = [ 'name' => $name, 'key' => self::serviceKey($name, $labels), 'state' => $state !== '' ? $state : 'running', 'deployment' => self::deploymentId($labels, $groupDeployments), 'server' => (string) ($server['uuid'] ?? ''), 'serverName' => (string) ($server['name'] ?? ''), 'serverIndex' => $index, ]; } } return $containers; } /** * @param array $container */ private static function labels(array $container): Collection { $raw = $container['Labels'] ?? null; if (is_array($raw)) { return collect($raw); } if (! is_string($raw) || trim($raw) === '') { return collect(); } $parsed = format_docker_labels_to_json($raw); return $parsed instanceof Collection ? $parsed : collect(); } private static function serviceKey(string $containerName, Collection $labels): string { $compose = trim((string) $labels->get('com.docker.compose.service', '')); if ($compose !== '') { return $compose; } $name = trim((string) $labels->get('coolify.name', '')); if ($name !== '') { return $name; } $headline = str($containerName)->beforeLast('-')->headline()->toString(); return $headline !== '' ? $headline : $containerName; } private static function deploymentId(Collection $labels, bool $groupDeployments): string { if (! $groupDeployments) { return '0'; } return self::normalizeDeployment((string) $labels->get('coolify.pullRequestId', '')); } private static function normalizeDeployment(string $deployment): string { if ($deployment === '' || ! ctype_digit($deployment) || (int) $deployment === 0) { return '0'; } return (string) (int) $deployment; } /** * @param list $containers * @return list */ private static function sortedDeploymentIds(array $containers): array { $ids = array_values(array_unique(array_column($containers, 'deployment'))); usort($ids, function (string $left, string $right): int { if ($left === '0') { return -1; } if ($right === '0') { return 1; } return (int) $right <=> (int) $left; }); return $ids; } /** * @param list $deploymentIds */ private static function pickDeployment(array $deploymentIds, string $preferred): string { if (in_array($preferred, $deploymentIds, true)) { return $preferred; } if (in_array('0', $deploymentIds, true)) { return '0'; } return $deploymentIds[0] ?? '0'; } /** * Deployments show the same status dot and caption as services, for all their containers. * * @param list $ids * @param list $containers * @return list */ private static function deploymentOptions(array $ids, array $containers): array { return array_map(function (string $id) use ($containers): array { $states = array_column(array_filter( $containers, fn (array $container): bool => $container['deployment'] === $id, ), 'state'); return self::statusOption($id, $id === '0' ? 'Production' : 'PR #'.$id, $states); }, $ids); } /** * @param list $states * @return array{value: string, label: string, dot: string, state?: string} */ private static function statusOption(string $value, string $label, array $states): array { $option = ['value' => $value, 'label' => $label, 'dot' => self::serviceDot($states)]; $state = self::serviceStateLabel($states); if ($state !== null) { $option['state'] = $state; } return $option; } /** * @param list $containers * @return array> */ private static function groupByService(array $containers): array { $groups = []; foreach ($containers as $container) { $groups[$container['key']][] = $container; } uksort($groups, fn (string $left, string $right): int => strcasecmp($left, $right)); return $groups; } /** * @param array> $groups */ private static function pickServiceKey(array $groups, string $service): string { if ($service !== '' && isset($groups[$service])) { return $service; } foreach ($groups as $key => $containers) { foreach ($containers as $container) { if ($container['state'] === 'running') { return $key; } } } return (string) array_key_first($groups); } /** * @param array> $groups * @return list */ private static function serviceOptions(array $groups): array { $options = []; foreach ($groups as $key => $containers) { $option = self::statusOption($key, $key, array_column($containers, 'state')); if (count($containers) > 1) { $option['description'] = count($containers).' containers'; } $options[] = $option; } return $options; } /** * @param list $states */ private static function serviceDot(array $states): string { $allRunning = true; $anyLive = false; foreach ($states as $state) { if ($state !== 'running') { $allRunning = false; } if (in_array($state, ['running', 'starting', 'restarting', 'degraded'], true)) { $anyLive = true; } } if ($allRunning) { return 'success'; } if (! $anyLive) { return 'error'; } return 'warning'; } /** * A running service needs no caption. Anything else is named: Stopped when * nothing is running, the transitional state when that is the only problem, * and Degraded when the containers disagree. * * @param list $states */ private static function serviceStateLabel(array $states): ?string { $problems = []; $anyLive = false; foreach ($states as $state) { $state = strtolower($state); if (in_array($state, ['running', 'starting', 'restarting', 'degraded'], true)) { $anyLive = true; } if ($state !== 'running') { $problems[$state] = true; } } if ($problems === []) { return null; } $problemStates = array_keys($problems); if ($problemStates === ['starting']) { return 'Starting'; } if ($problemStates === ['restarting']) { return 'Restarting'; } if ($problemStates === ['degraded']) { return 'Degraded'; } if (! $anyLive) { return 'Stopped'; } return 'Degraded'; } /** * @param list $containers * @return list */ private static function sourcesFor(array $containers): array { usort($containers, function (array $left, array $right): int { $byServer = $left['serverIndex'] <=> $right['serverIndex']; return $byServer !== 0 ? $byServer : strcmp($left['name'], $right['name']); }); $serverCounts = []; foreach ($containers as $container) { $serverCounts[$container['server']] = ($serverCounts[$container['server']] ?? 0) + 1; } $multipleServers = count($serverCounts) > 1; $sources = []; foreach ($containers as $container) { $sources[] = [ 'server' => $container['server'], 'container' => $container['name'], 'label' => self::sourceLabel($container, $multipleServers, $serverCounts[$container['server']], count($containers)), ]; } return self::uniqueLabels($sources); } /** * @param array{name: string, server: string, serverName: string} $container */ private static function sourceLabel(array $container, bool $multipleServers, int $onServer, int $total): string { if ($total < 2) { return $container['name']; } $parts = []; if ($multipleServers) { $name = trim($container['serverName']); $parts[] = $name !== '' ? $name : $container['server']; } if ($onServer > 1 || ! $multipleServers) { $parts[] = $container['name']; } return implode(' ', $parts); } /** * @param list $sources * @return list */ private static function uniqueLabels(array $sources): array { $counts = array_count_values(array_column($sources, 'label')); foreach ($sources as $index => $source) { if ($counts[$source['label']] < 2) { continue; } if (! str_contains($source['label'], $source['container'])) { $sources[$index]['label'] = trim($source['label'].' '.$source['container']); } } $counts = array_count_values(array_column($sources, 'label')); foreach ($sources as $index => $source) { if (($counts[$source['label']] ?? 0) < 2) { continue; } $sources[$index]['label'] = $source['label'].' '.substr($source['server'], 0, 8); } return $sources; } /** * @param list $sources */ private static function streamKey(array $sources): string { return implode('|', array_map( fn (array $source): string => $source['server'].':'.$source['container'], $sources, )); } }