Skip to content

Commit

Permalink
initial support for Docker Healthcheck (#273)
Browse files Browse the repository at this point in the history
Before this, we didn’t have a `healthcheck` at all, but instead there
was simply a check to see if the container was running.

Checking whether the container is running has been moved to Deploy stage
and now occurs at the end of the deployment process.
And between deployment and “init” there is now a correct health check.
Applications are not required to support healthcheck at all, so it is
only checked if `['State']['Health']['Status']` is present.

Without a timeout, the timeout must be set by the application itself, as
it is usually done for Docker containers healthcheck.

During a healthcheck, an application, for example, can now install some
of its own packages or do something other with its docker container.

It should not communicate with the Nextcloud itself at this
stage(healthcheck), because application is not considered enabled.

---------

Signed-off-by: Alexander Piskun <[email protected]>
Signed-off-by: Andrey Borysenko <[email protected]>
Co-authored-by: Andrey Borysenko <[email protected]>
  • Loading branch information
bigcat88 and andrey18106 authored Apr 17, 2024
1 parent 7f0806f commit 7391eee
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 15 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [2.5.0 - 2024-04-1x]
## [2.5.0 - 2024-04-23]

### Added

- Different compute device configuration for Daemon (NVIDIA, AMD, CPU). #267
- Ability to add optional parameters when registering a daemon, for example *OVERRIDE_APP_HOST*. #269
- Correct support of the Docker `HEALTHCHECK` instruction. #273

### Fixed

Expand Down
1 change: 0 additions & 1 deletion css/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,6 @@

.apps-list.installed .actions .icon-loading-small {
display: inline-block;
top: 4px;
margin-right: 10px
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Command/ExApp/Register.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 1;
}

if (!$this->dockerActions->healthcheckContainer($this->dockerActions->buildExAppContainerName($appId), $daemonConfig)) {
if (!$this->dockerActions->healthcheckContainer($this->dockerActions->buildExAppContainerName($appId), $daemonConfig, true)) {
$this->logger->error(sprintf('ExApp %s deployment failed. Error: %s', $appId, 'Container healthcheck failed.'));
if ($outputConsole) {
$output->writeln(sprintf('ExApp %s deployment failed. Error: %s', $appId, 'Container healthcheck failed.'));
Expand Down
2 changes: 1 addition & 1 deletion lib/Command/ExApp/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ private function updateExApp(InputInterface $input, OutputInterface $output, str
return 1;
}

if (!$this->dockerActions->healthcheckContainer($this->dockerActions->buildExAppContainerName($appId), $daemonConfig)) {
if (!$this->dockerActions->healthcheckContainer($this->dockerActions->buildExAppContainerName($appId), $daemonConfig, true)) {
$this->logger->error(sprintf('ExApp %s update failed. Error: %s', $appId, 'Container healthcheck failed.'));
if ($outputConsole) {
$output->writeln(sprintf('ExApp %s update failed. Error: %s', $appId, 'Container healthcheck failed.'));
Expand Down
52 changes: 44 additions & 8 deletions lib/DeployActions/DockerActions.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public function deployExApp(ExApp $exApp, DaemonConfig $daemonConfig, array $par
if (isset($result['error'])) {
return $result['error'];
}
$this->exAppService->setAppDeployProgress($exApp, 99);
if (!$this->waitTillContainerStart($this->buildExAppContainerName($exApp->getAppid()), $daemonConfig)) {
return 'container startup failed';
}
$this->exAppService->setAppDeployProgress($exApp, 100);
return '';
}
Expand Down Expand Up @@ -285,6 +289,17 @@ public function inspectContainer(string $dockerUrl, string $containerId): array
}
}

/**
* @throws GuzzleException
*/
public function getContainerLogs(string $dockerUrl, string $containerId, string $tail = 'all'): string {
$url = $this->buildApiUrl(
$dockerUrl, sprintf('containers/%s/logs?stdout=true&stderr=true&tail=%s', $containerId, $tail)
);
$response = $this->guzzleClient->get($url);
return (string) $response->getBody();
}

public function createVolume(string $dockerUrl, string $volume): array {
$url = $this->buildApiUrl($dockerUrl, 'volumes/create');
try {
Expand Down Expand Up @@ -452,16 +467,13 @@ public function resolveExAppUrl(
return sprintf('%s://%s:%s', $protocol, $exAppHost, $port);
}

public function containerStateHealthy(array $containerInfo): bool {
return $containerInfo['State']['Status'] === 'running';
}

public function healthcheckContainer(string $containerId, DaemonConfig $daemonConfig): bool {
public function waitTillContainerStart(string $containerId, DaemonConfig $daemonConfig): bool {
$dockerUrl = $this->buildDockerUrl($daemonConfig);
$attempts = 0;
$totalAttempts = 90; // ~90 seconds for container to initialize
$totalAttempts = 90; // ~90 seconds for container to start
while ($attempts < $totalAttempts) {
$containerInfo = $this->inspectContainer($this->buildDockerUrl($daemonConfig), $containerId);
if ($this->containerStateHealthy($containerInfo)) {
$containerInfo = $this->inspectContainer($dockerUrl, $containerId);
if ($containerInfo['State']['Status'] === 'running') {
return true;
}
$attempts++;
Expand All @@ -470,6 +482,30 @@ public function healthcheckContainer(string $containerId, DaemonConfig $daemonCo
return false;
}

public function healthcheckContainer(string $containerId, DaemonConfig $daemonConfig, bool $waitForSuccess): bool {
$dockerUrl = $this->buildDockerUrl($daemonConfig);
$containerInfo = $this->inspectContainer($dockerUrl, $containerId);
if (!isset($containerInfo['State']['Health']['Status'])) {
return true; // container does not support Healthcheck
}
if (!$waitForSuccess) {
return $containerInfo['State']['Health']['Status'] === 'healthy';
}
$maxTotalAttempts = 900;
while ($maxTotalAttempts > 0) {
$containerInfo = $this->inspectContainer($dockerUrl, $containerId);
if ($containerInfo['State']['Health']['Status'] === 'healthy') {
return true;
}
if ($containerInfo['State']['Health']['Status'] === 'unhealthy') {
return false;
}
$maxTotalAttempts--;
sleep(1);
}
return false;
}

public function buildDockerUrl(DaemonConfig $daemonConfig): string {
if (file_exists($daemonConfig->getHost())) {
return 'http://localhost';
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/ExAppService.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ public function setAppDeployProgress(ExApp $exApp, int $progress, string $error
}
unset($status['active']); # TO-DO: Remove in AppAPI 2.4.0
if ($progress === 100) {
$status['action'] = '';
$status['action'] = 'healthcheck';
}
$exApp->setStatus($status);
$exApp->setLastCheckTime(time());
Expand Down
8 changes: 7 additions & 1 deletion src/mixins/AppManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default {
return this.app && this.$store.getters.loading(this.app.id)
},
isInitializing() {
return this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'init'
return this.app && Object.hasOwn(this.app?.status, 'action') && (this.app.status.action === 'init' || this.app.status.action === 'healthcheck')
},
isDeploying() {
return this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'deploy'
Expand All @@ -31,6 +31,9 @@ export default {
if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'init') {
return t('app_api', '{progress}% Initializing', { progress: this.app.status?.init })
}
if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'healthcheck') {
return t('app_api', 'Healthchecking')
}
if (this.app.needsDownload) {
return t('app_api', 'Deploy and Enable')
}
Expand All @@ -43,6 +46,9 @@ export default {
if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'init') {
return t('app_api', '{progress}% Initializing', { progress: this.app.status?.init })
}
if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'healthcheck') {
return t('app_api', 'Healthchecking')
}
return t('app_api', 'Disable')
},
forceEnableButtonText() {
Expand Down
2 changes: 1 addition & 1 deletion src/store/apps.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const getters = {
},
getInitializingOrDeployingApps(state) {
return state.apps.filter(app => Object.hasOwn(app.status, 'action')
&& (app.status.action === 'deploy' || app.status.action === 'init')
&& (app.status.action === 'deploy' || app.status.action === 'init' || app.status.action === 'healthcheck')
&& app.status.type !== '')
},
}
Expand Down

0 comments on commit 7391eee

Please sign in to comment.