From 4798489435274fdd921771cbe1d0959bb4f63af0 Mon Sep 17 00:00:00 2001 From: Jean-Yves <7360784+docjyJ@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:18:30 +0200 Subject: [PATCH] Improuve Container States Signed-off-by: Jean-Yves <7360784+docjyJ@users.noreply.github.com> --- php/src/Container/Container.php | 18 +-- php/src/Container/ContainerState.php | 38 +++++- php/src/Container/UpdateState.php | 12 ++ php/src/Container/VersionState.php | 8 -- php/src/Controller/DockerController.php | 9 +- php/src/Docker/DockerActionManager.php | 165 +++++++++++------------- php/templates/containers.twig | 30 ++++- 7 files changed, 152 insertions(+), 128 deletions(-) create mode 100644 php/src/Container/UpdateState.php delete mode 100644 php/src/Container/VersionState.php diff --git a/php/src/Container/Container.php b/php/src/Container/Container.php index 0f9e9de575d..07dde98c2ea 100644 --- a/php/src/Container/Container.php +++ b/php/src/Container/Container.php @@ -5,6 +5,7 @@ use AIO\Data\ConfigurationManager; use AIO\Docker\DockerActionManager; use AIO\ContainerDefinitionFetcher; +use GuzzleHttp\Exception\GuzzleException; readonly class Container { public function __construct( @@ -112,20 +113,13 @@ public function GetVolumes() : ContainerVolumes { return $this->volumes; } - public function GetRunningState() : ContainerState { - return $this->dockerActionManager->GetContainerRunningState($this); + /** @throws GuzzleException */ + public function GetContainerState() : ContainerState { + return $this->dockerActionManager->GetContainerState($this); } - public function GetRestartingState() : ContainerState { - return $this->dockerActionManager->GetContainerRestartingState($this); - } - - public function GetUpdateState() : VersionState { - return $this->dockerActionManager->GetContainerUpdateState($this); - } - - public function GetStartingState() : ContainerState { - return $this->dockerActionManager->GetContainerStartingState($this); + public function GetUpdateState() : UpdateState { + return $this->dockerActionManager->GetUpdateState($this); } /** diff --git a/php/src/Container/ContainerState.php b/php/src/Container/ContainerState.php index f6481027fdd..4bc3d7bc41c 100644 --- a/php/src/Container/ContainerState.php +++ b/php/src/Container/ContainerState.php @@ -2,11 +2,35 @@ namespace AIO\Container; -enum ContainerState: string { - case ImageDoesNotExist = 'image_does_not_exist'; - case NotRestarting = 'not_restarting'; - case Restarting = 'restarting'; - case Running = 'running'; - case Starting = 'starting'; - case Stopped = 'stopped'; +enum ContainerState { + case DoesNotExist; + case Restarting; + case Healthy; + case Starting; + case Stopped; + case Unhealthy; + + public function isStopped(): bool { + return $this == self::Stopped; + } + + public function isStarting(): bool { + return $this == self::Starting; + } + + public function isRestarting(): bool { + return $this == self::Restarting; + } + + public function isHealthy(): bool { + return $this == self::Healthy; + } + + public function isUnhealthy(): bool { + return $this == self::Unhealthy; + } + + public function isRunning(): bool { + return $this->isHealthy() || $this->isUnhealthy() || $this->isStarting() || $this->isRestarting(); + } } diff --git a/php/src/Container/UpdateState.php b/php/src/Container/UpdateState.php new file mode 100644 index 00000000000..4ec12cfc549 --- /dev/null +++ b/php/src/Container/UpdateState.php @@ -0,0 +1,12 @@ +containerDefinitionFetcher->GetContainerById($id); @@ -28,7 +30,7 @@ private function PerformRecursiveContainerStart(string $id, bool $pullImage = tr // Don't start if container is already running // This is expected to happen if a container is defined in depends_on of multiple containers - if ($container->GetRunningState() === ContainerState::Running) { + if ($container->GetContainerState()->isRunning()) { error_log('Not starting ' . $id . ' because it was already started.'); return; } @@ -240,6 +242,7 @@ public function stopTopContainer() : void { $this->PerformRecursiveContainerStop($id); } + /** @throws GuzzleException */ public function StartDomaincheckContainer() : void { # Don't start if domain is already set @@ -254,10 +257,10 @@ public function StartDomaincheckContainer() : void $domaincheckContainer = $this->containerDefinitionFetcher->GetContainerById($id); $apacheContainer = $this->containerDefinitionFetcher->GetContainerById(self::TOP_CONTAINER); // Don't start if apache is already running - if ($apacheContainer->GetRunningState() === ContainerState::Running) { + if ($apacheContainer->GetContainerState()->isRunning()) { return; // Don't start if domaincheck is already running - } elseif ($domaincheckContainer->GetRunningState() === ContainerState::Running) { + } elseif ($domaincheckContainer->GetContainerState()->isRunning()) { $domaincheckWasStarted = apcu_fetch($cacheKey); // Start domaincheck again when 10 minutes are over by not returning here if($domaincheckWasStarted !== false && is_string($domaincheckWasStarted)) { diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 12a641e0869..3ef48309209 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -3,10 +3,13 @@ namespace AIO\Docker; use AIO\Container\Container; -use AIO\Container\VersionState; +use AIO\Container\UpdateState; use AIO\Container\ContainerState; use AIO\Data\ConfigurationManager; +use AssertionError; +use Exception; use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; use AIO\ContainerDefinitionFetcher; use http\Env\Response; @@ -35,50 +38,7 @@ private function BuildImageName(Container $container) : string { return $container->GetContainerName() . ':' . $tag; } - public function GetContainerRunningState(Container $container) : ContainerState - { - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); - try { - $response = $this->guzzleClient->get($url); - } catch (RequestException $e) { - if ($e->getCode() === 404) { - return ContainerState::ImageDoesNotExist; - } - throw $e; - } - - $responseBody = json_decode((string)$response->getBody(), true); - - if ($responseBody['State']['Running'] === true) { - return ContainerState::Running; - } else { - return ContainerState::Stopped; - } - } - - public function GetContainerRestartingState(Container $container) : ContainerState - { - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); - try { - $response = $this->guzzleClient->get($url); - } catch (RequestException $e) { - if ($e->getCode() === 404) { - return ContainerState::ImageDoesNotExist; - } - throw $e; - } - - $responseBody = json_decode((string)$response->getBody(), true); - - if ($responseBody['State']['Restarting'] === true) { - return ContainerState::Restarting; - } else { - return ContainerState::NotRestarting; - } - } - - public function GetContainerUpdateState(Container $container) : VersionState - { + public function GetUpdateState(Container $container): UpdateState { $tag = $container->GetImageTag(); if ($tag === '%AIO_CHANNEL%') { $tag = $this->GetCurrentChannel(); @@ -86,47 +46,66 @@ public function GetContainerUpdateState(Container $container) : VersionState $runningDigests = $this->GetRepoDigestsOfContainer($container->GetIdentifier()); if ($runningDigests === null) { - return VersionState::Different; + return UpdateState::Outdated; } $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); if ($remoteDigest === null) { - return VersionState::Equal; + return UpdateState::Latest; } - foreach($runningDigests as $runningDigest) { - if ($runningDigest === $remoteDigest) { - return VersionState::Equal; - } - } - return VersionState::Different; + return in_array($remoteDigest, $runningDigests, true) ? UpdateState::Latest : UpdateState::Outdated; } - public function GetContainerStartingState(Container $container) : ContainerState - { - $runningState = $this->GetContainerRunningState($container); - if ($runningState === ContainerState::Stopped || $runningState === ContainerState::ImageDoesNotExist) { - return $runningState; + /** @throws GuzzleException */ + public function GetContainerState(Container $container): ContainerState { + $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); + try { + $response = $this->guzzleClient->get($url); + } catch (GuzzleException $e) { + if ($e->getCode() === 404) { + return ContainerState::DoesNotExist; + } + throw $e; } - $containerName = $container->GetIdentifier(); - $internalPort = $container->GetInternalPort(); - if($internalPort === '%APACHE_PORT%') { - $internalPort = $this->configurationManager->GetApachePort(); - } elseif($internalPort === '%TALK_PORT%') { - $internalPort = $this->configurationManager->GetTalkPort(); + $body = json_decode($response->getBody()->getContents(), true); + assert(is_array($body)); + assert(is_array($body['State'])); + + $state = match ($body['State']['Status']) { + 'running' => ContainerState::Healthy, + 'created' => ContainerState::Starting, + 'restarting' => ContainerState::Restarting, + 'paused', 'removing', 'exited', 'dead' => ContainerState::Stopped, + default => throw new AssertionError() + }; + + if ($state->isHealthy() && is_array($body['State']['Health'])) { + $state = match ($body['State']['Health']['Status']) { + 'starting' => ContainerState::Starting, + 'unhealthy' => ContainerState::Unhealthy, + default => $state, + }; } - if ($internalPort !== "" && $internalPort !== 'host') { - $connection = @fsockopen($containerName, (int)$internalPort, $errno, $errstr, 0.2); - if ($connection) { - fclose($connection); - return ContainerState::Running; - } else { - return ContainerState::Starting; + if ($state->isHealthy()) { + $containerName = $container->GetIdentifier(); + $internalPort = $container->GetInternalPort(); + if ($internalPort === '%APACHE_PORT%') { + $internalPort = $this->configurationManager->GetApachePort(); + } elseif ($internalPort === '%TALK_PORT%') { + $internalPort = $this->configurationManager->GetTalkPort(); + } + if (is_numeric($internalPort)) { + $connection = @fsockopen($containerName, intval($internalPort), $errno, $errstr, 0.2); + if ($connection) { + fclose($connection); + } else { + $state = ContainerState::Starting; + } } - } else { - return ContainerState::Running; } + return $state; } public function DeleteContainer(Container $container) : void { @@ -167,7 +146,7 @@ public function StartContainer(Container $container) : void { try { $this->guzzleClient->post($url); } catch (RequestException $e) { - throw new \Exception("Could not start container " . $container->GetIdentifier() . ": " . $e->getMessage()); + throw new Exception("Could not start container " . $container->GetIdentifier() . ": " . $e->getMessage()); } } @@ -404,7 +383,7 @@ public function CreateContainer(Container $container) : void { } else { $secret = $this->configurationManager->GetSecret($out[1]); if ($secret === "") { - throw new \Exception("The secret " . $out[1] . " is empty. Cannot substitute its value. Please check if it is defined in secrets of containers.json."); + throw new Exception("The secret " . $out[1] . " is empty. Cannot substitute its value. Please check if it is defined in secrets of containers.json."); } $replacements[1] = $secret; } @@ -573,7 +552,7 @@ public function CreateContainer(Container $container) : void { ] ); } catch (RequestException $e) { - throw new \Exception("Could not create container " . $container->GetIdentifier() . ": " . $e->getMessage()); + throw new Exception("Could not create container " . $container->GetIdentifier() . ": " . $e->getMessage()); } } @@ -609,17 +588,16 @@ public function PullImage(Container $container) : void $this->guzzleClient->post($url); } catch (RequestException $e) { if ($imageIsThere === false) { - throw new \Exception("Could not pull image " . $imageName . ". Please run 'sudo docker exec -it nextcloud-aio-mastercontainer docker pull " . $imageName . "' in order to find out why it failed."); + throw new Exception("Could not pull image " . $imageName . ". Please run 'sudo docker exec -it nextcloud-aio-mastercontainer docker pull " . $imageName . "' in order to find out why it failed."); } } } - private function isContainerUpdateAvailable(string $id) : string - { + private function isContainerUpdateAvailable(string $id): string { $container = $this->containerDefinitionFetcher->GetContainerById($id); $updateAvailable = ""; - if ($container->GetUpdateState() === VersionState::Different) { + if ($container->GetUpdateState() === UpdateState::Outdated) { $updateAvailable = '1'; } foreach ($container->GetDependsOn() as $dependency) { @@ -718,7 +696,7 @@ private function GetRepoDigestsOfContainer(string $containerName) : ?array { } return null; - } catch (\Exception $e) { + } catch (Exception $e) { return null; } } @@ -747,7 +725,7 @@ public function GetCurrentChannel() : string { $tag = 'latest'; } return $tag; - } catch (\Exception $e) { + } catch (Exception $e) { error_log('Could not get current channel ' . $e->getMessage()); } @@ -778,9 +756,9 @@ public function IsMastercontainerUpdateAvailable() : bool return true; } - public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh') : void - { - if ($this->GetContainerStartingState($container) === ContainerState::Running) { + /** @throws GuzzleException */ + public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh'): void { + if ($this->GetContainerState($container)->isHealthy()) { $containerName = $container->GetIdentifier(); @@ -867,7 +845,7 @@ private function ConnectContainerIdToNetwork(string $id, string $internalPort, s } catch (RequestException $e) { // 409 is undocumented and gets thrown if the network already exists. if ($e->getCode() !== 409) { - throw new \Exception("Could not create the nextcloud-aio network: " . $e->getMessage()); + throw new Exception("Could not create the nextcloud-aio network: " . $e->getMessage()); } } @@ -961,19 +939,24 @@ public function GetDatabasecontainerExitCode() : int } } - public function isLoginAllowed() : bool { + /** + * @throws GuzzleException + * @throws Exception + */ + public function isLoginAllowed(): bool { $id = 'nextcloud-aio-apache'; $apacheContainer = $this->containerDefinitionFetcher->GetContainerById($id); - if ($this->GetContainerStartingState($apacheContainer) === ContainerState::Running) { + if ($this->GetContainerState($apacheContainer)->isRunning()) { return false; } return true; } - public function isBackupContainerRunning() : bool { + /** @throws GuzzleException */ + public function isBackupContainerRunning(): bool { $id = 'nextcloud-aio-borgbackup'; $backupContainer = $this->containerDefinitionFetcher->GetContainerById($id); - if ($this->GetContainerRunningState($backupContainer) === ContainerState::Running) { + if ($this->GetContainerState($backupContainer)->isRunning()) { return true; } return false; @@ -991,7 +974,7 @@ private function GetCreatedTimeOfNextcloudImage() : ?string { } return str_replace('T', ' ', (string)$imageOutput['Created']); - } catch (\Exception $e) { + } catch (Exception $e) { return null; } } diff --git a/php/templates/containers.twig b/php/templates/containers.twig index d1ddb499dec..3481b5108ab 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -41,19 +41,20 @@ {% endif %} {% for container in containers %} - {% if container.GetDisplayName() != '' and container.GetRunningState().value == 'running' %} + {% set runingState = container.GetContainerState() %} + {% if container.GetDisplayName() != '' and runingState.isRunning() %} {% set isAnyRunning = true %} {% endif %} - {% if container.GetDisplayName() != '' and container.GetRestartingState().value == 'restarting' %} + {% if container.GetDisplayName() != '' and runingState.isRestarting() %} {% set isAnyRestarting = true %} {% endif %} - {% if container.GetIdentifier() == 'nextcloud-aio-watchtower' and container.GetRunningState().value == 'running' %} + {% if container.GetIdentifier() == 'nextcloud-aio-watchtower' and container.GetContainerState().isRunning() %} {% set isWatchtowerRunning = true %} {% endif %} - {% if container.GetIdentifier() == 'nextcloud-aio-domaincheck' and container.GetRunningState().value == 'running' %} + {% if container.GetIdentifier() == 'nextcloud-aio-domaincheck' and container.GetContainerState().isRunning() %} {% set isDomaincheckRunning = true %} {% endif %} - {% if container.GetIdentifier() == 'nextcloud-aio-apache' and container.GetStartingState().value == 'starting' %} + {% if container.GetIdentifier() == 'nextcloud-aio-apache' and runingState.isStarting() %} {% set isApacheStarting = true %} {% endif %} {% endfor %} @@ -262,14 +263,29 @@ {% for container in containers %} {% if container.GetDisplayName() != '' %}
  • - {% if container.GetStartingState().value == 'starting' %} + {% set runningState = container.GetContainerState() %} + {% if runningState.isStarting() %} {{ container.GetDisplayName() }} (Starting) {% if container.GetDocumentation() != '' %} (docs) {% endif %} - {% elseif container.GetRunningState().value == 'running' %} + {% elseif runningState.isUnhealthy() %} + + {{ container.GetDisplayName() }} (Unhealthy) + {% if container.GetDocumentation() != '' %} + (docs) + {% endif %} + + {% elseif runningState.isRestarting() %} + + {{ container.GetDisplayName() }} (Restarting) + {% if container.GetDocumentation() != '' %} + (docs) + {% endif %} + + {% elseif runningState.isHealthy() %} {{ container.GetDisplayName() }} (Running) {% if container.GetDocumentation() != '' %}