fix(docker): enhance container status aggregation for multi-container applications, including exclusion handling based on docker-compose configuration
This commit is contained in:
@@ -26,6 +26,8 @@ class GetContainersStatus
|
|||||||
|
|
||||||
public $server;
|
public $server;
|
||||||
|
|
||||||
|
protected ?Collection $applicationContainerStatuses;
|
||||||
|
|
||||||
public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
|
public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
|
||||||
{
|
{
|
||||||
$this->containers = $containers;
|
$this->containers = $containers;
|
||||||
@@ -119,11 +121,16 @@ class GetContainersStatus
|
|||||||
$application = $this->applications->where('id', $applicationId)->first();
|
$application = $this->applications->where('id', $applicationId)->first();
|
||||||
if ($application) {
|
if ($application) {
|
||||||
$foundApplications[] = $application->id;
|
$foundApplications[] = $application->id;
|
||||||
$statusFromDb = $application->status;
|
// Store container status for aggregation
|
||||||
if ($statusFromDb !== $containerStatus) {
|
if (! isset($this->applicationContainerStatuses)) {
|
||||||
$application->update(['status' => $containerStatus]);
|
$this->applicationContainerStatuses = collect();
|
||||||
} else {
|
}
|
||||||
$application->update(['last_online_at' => now()]);
|
if (! $this->applicationContainerStatuses->has($applicationId)) {
|
||||||
|
$this->applicationContainerStatuses->put($applicationId, collect());
|
||||||
|
}
|
||||||
|
$containerName = data_get($labels, 'com.docker.compose.service');
|
||||||
|
if ($containerName) {
|
||||||
|
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Notify user that this container should not be there.
|
// Notify user that this container should not be there.
|
||||||
@@ -320,6 +327,83 @@ class GetContainersStatus
|
|||||||
}
|
}
|
||||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aggregate multi-container application statuses
|
||||||
|
if (isset($this->applicationContainerStatuses) && $this->applicationContainerStatuses->isNotEmpty()) {
|
||||||
|
foreach ($this->applicationContainerStatuses as $applicationId => $containerStatuses) {
|
||||||
|
$application = $this->applications->where('id', $applicationId)->first();
|
||||||
|
if (! $application) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses);
|
||||||
|
if ($aggregatedStatus) {
|
||||||
|
$statusFromDb = $application->status;
|
||||||
|
if ($statusFromDb !== $aggregatedStatus) {
|
||||||
|
$application->update(['status' => $aggregatedStatus]);
|
||||||
|
} else {
|
||||||
|
$application->update(['last_online_at' => now()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ServiceChecked::dispatch($this->server->team->id);
|
ServiceChecked::dispatch($this->server->team->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function aggregateApplicationStatus($application, Collection $containerStatuses): ?string
|
||||||
|
{
|
||||||
|
// Parse docker compose to check for excluded containers
|
||||||
|
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
|
||||||
|
$excludedContainers = collect();
|
||||||
|
|
||||||
|
if ($dockerComposeRaw) {
|
||||||
|
try {
|
||||||
|
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
|
||||||
|
$services = data_get($dockerCompose, 'services', []);
|
||||||
|
|
||||||
|
foreach ($services as $serviceName => $serviceConfig) {
|
||||||
|
// Check if container should be excluded
|
||||||
|
$excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
|
||||||
|
$restartPolicy = data_get($serviceConfig, 'restart', 'always');
|
||||||
|
|
||||||
|
if ($excludeFromHc || $restartPolicy === 'no') {
|
||||||
|
$excludedContainers->push($serviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If we can't parse, treat all containers as included
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out excluded containers
|
||||||
|
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
|
||||||
|
return ! $excludedContainers->contains($containerName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If all containers are excluded, don't update status
|
||||||
|
if ($relevantStatuses->isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate status: if any container is running, app is running
|
||||||
|
$hasRunning = false;
|
||||||
|
$hasUnhealthy = false;
|
||||||
|
|
||||||
|
foreach ($relevantStatuses as $status) {
|
||||||
|
if (str($status)->contains('running')) {
|
||||||
|
$hasRunning = true;
|
||||||
|
if (str($status)->contains('unhealthy')) {
|
||||||
|
$hasUnhealthy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasRunning) {
|
||||||
|
return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// All containers are exited
|
||||||
|
return 'exited (unhealthy)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -65,6 +65,8 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
|||||||
|
|
||||||
public Collection $foundApplicationPreviewsIds;
|
public Collection $foundApplicationPreviewsIds;
|
||||||
|
|
||||||
|
public Collection $applicationContainerStatuses;
|
||||||
|
|
||||||
public bool $foundProxy = false;
|
public bool $foundProxy = false;
|
||||||
|
|
||||||
public bool $foundLogDrainContainer = false;
|
public bool $foundLogDrainContainer = false;
|
||||||
@@ -87,6 +89,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
|||||||
$this->foundServiceApplicationIds = collect();
|
$this->foundServiceApplicationIds = collect();
|
||||||
$this->foundApplicationPreviewsIds = collect();
|
$this->foundApplicationPreviewsIds = collect();
|
||||||
$this->foundServiceDatabaseIds = collect();
|
$this->foundServiceDatabaseIds = collect();
|
||||||
|
$this->applicationContainerStatuses = collect();
|
||||||
$this->allApplicationIds = collect();
|
$this->allApplicationIds = collect();
|
||||||
$this->allDatabaseUuids = collect();
|
$this->allDatabaseUuids = collect();
|
||||||
$this->allTcpProxyUuids = collect();
|
$this->allTcpProxyUuids = collect();
|
||||||
@@ -155,7 +158,14 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
|||||||
if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
||||||
$this->foundApplicationIds->push($applicationId);
|
$this->foundApplicationIds->push($applicationId);
|
||||||
}
|
}
|
||||||
$this->updateApplicationStatus($applicationId, $containerStatus);
|
// Store container status for aggregation
|
||||||
|
if (! $this->applicationContainerStatuses->has($applicationId)) {
|
||||||
|
$this->applicationContainerStatuses->put($applicationId, collect());
|
||||||
|
}
|
||||||
|
$containerName = $labels->get('com.docker.compose.service');
|
||||||
|
if ($containerName) {
|
||||||
|
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$previewKey = $applicationId.':'.$pullRequestId;
|
$previewKey = $applicationId.':'.$pullRequestId;
|
||||||
if ($this->allApplicationPreviewsIds->contains($previewKey) && $this->isRunning($containerStatus)) {
|
if ($this->allApplicationPreviewsIds->contains($previewKey) && $this->isRunning($containerStatus)) {
|
||||||
@@ -205,9 +215,86 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
|||||||
|
|
||||||
$this->updateAdditionalServersStatus();
|
$this->updateAdditionalServersStatus();
|
||||||
|
|
||||||
|
// Aggregate multi-container application statuses
|
||||||
|
$this->aggregateMultiContainerStatuses();
|
||||||
|
|
||||||
$this->checkLogDrainContainer();
|
$this->checkLogDrainContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function aggregateMultiContainerStatuses()
|
||||||
|
{
|
||||||
|
if ($this->applicationContainerStatuses->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->applicationContainerStatuses as $applicationId => $containerStatuses) {
|
||||||
|
$application = $this->applications->where('id', $applicationId)->first();
|
||||||
|
if (! $application) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse docker compose to check for excluded containers
|
||||||
|
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
|
||||||
|
$excludedContainers = collect();
|
||||||
|
|
||||||
|
if ($dockerComposeRaw) {
|
||||||
|
try {
|
||||||
|
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
|
||||||
|
$services = data_get($dockerCompose, 'services', []);
|
||||||
|
|
||||||
|
foreach ($services as $serviceName => $serviceConfig) {
|
||||||
|
// Check if container should be excluded
|
||||||
|
$excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
|
||||||
|
$restartPolicy = data_get($serviceConfig, 'restart', 'always');
|
||||||
|
|
||||||
|
if ($excludeFromHc || $restartPolicy === 'no') {
|
||||||
|
$excludedContainers->push($serviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If we can't parse, treat all containers as included
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out excluded containers
|
||||||
|
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
|
||||||
|
return ! $excludedContainers->contains($containerName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If all containers are excluded, don't update status
|
||||||
|
if ($relevantStatuses->isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate status: if any container is running, app is running
|
||||||
|
$hasRunning = false;
|
||||||
|
$hasUnhealthy = false;
|
||||||
|
|
||||||
|
foreach ($relevantStatuses as $status) {
|
||||||
|
if (str($status)->contains('running')) {
|
||||||
|
$hasRunning = true;
|
||||||
|
if (str($status)->contains('unhealthy')) {
|
||||||
|
$hasUnhealthy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$aggregatedStatus = null;
|
||||||
|
if ($hasRunning) {
|
||||||
|
$aggregatedStatus = $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
|
||||||
|
} else {
|
||||||
|
// All containers are exited
|
||||||
|
$aggregatedStatus = 'exited (unhealthy)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update application status with aggregated result
|
||||||
|
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
|
||||||
|
$application->status = $aggregatedStatus;
|
||||||
|
$application->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function updateApplicationStatus(string $applicationId, string $containerStatus)
|
private function updateApplicationStatus(string $applicationId, string $containerStatus)
|
||||||
{
|
{
|
||||||
$application = $this->applications->where('id', $applicationId)->first();
|
$application = $this->applications->where('id', $applicationId)->first();
|
||||||
|
Reference in New Issue
Block a user