From b13fa4dad0c57ce0ccf1a61f43850d454b05c5cb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:48:54 +0200 Subject: [PATCH] fix(database): choose the MongoDB health check shell at run time The health check from #11730 picked mongosh or the legacy mongo shell from the image name (mongo:4*). Images without mongosh whose name does not start with mongo:4, such as mongo:3.6 or registry mirrors of mongo:4.4, ran a missing mongosh (exit 127) and stayed unhealthy. The check now uses mongosh when the container has it and falls back to the legacy mongo shell otherwise. Replace the test that only checked the source text with tests that run the generated script with fake shells. --- app/Actions/Database/StartMongodb.php | 41 +++++----- tests/Unit/StartMongodbHealthCheckTest.php | 94 +++++++++++----------- 2 files changed, 63 insertions(+), 72 deletions(-) diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index cb499bba32..ed0d0dd7a9 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -328,41 +328,36 @@ class StartMongodb return $environment_variables->all(); } + /** + * The shell is chosen when the check runs: images before MongoDB 5 (and some custom images) have only the legacy mongo shell. + */ private function generate_health_check_command(): array { - $usesLegacyShell = str($this->database->image)->startsWith('mongo:4'); - $command = [ - 'CMD', - $usesLegacyShell ? 'mongo' : 'mongosh', - '--quiet', - '--host', - $this->database->uuid, - ]; + $mongosh = $this->health_check_shell_command(legacy: false); + $mongo = $this->health_check_shell_command(legacy: true); + + return ['CMD-SHELL', "if command -v mongosh >/dev/null 2>&1; then {$mongosh}; else {$mongo}; fi"]; + } + + private function health_check_shell_command(bool $legacy): string + { + $command = [$legacy ? 'mongo' : 'mongosh', '--quiet', '--host', $this->database->uuid]; if ($this->database->enable_ssl) { - $command = [ - ...$command, - $usesLegacyShell ? '--ssl' : '--tls', - $usesLegacyShell ? '--sslCAFile' : '--tlsCAFile', - '/etc/mongo/certs/ca.pem', - ]; + $command = [...$command, $legacy ? '--ssl' : '--tls', $legacy ? '--sslCAFile' : '--tlsCAFile', '/etc/mongo/certs/ca.pem']; if ($this->database->ssl_mode === 'verify-full') { - $command = [ - ...$command, - $usesLegacyShell ? '--sslPEMKeyFile' : '--tlsCertificateKeyFile', - '/etc/mongo/certs/server.pem', - ]; + $command = [...$command, $legacy ? '--sslPEMKeyFile' : '--tlsCertificateKeyFile', '/etc/mongo/certs/server.pem']; } } - return [ + $command = [ ...$command, '--eval', - $usesLegacyShell - ? 'quit(db.isMaster().ismaster === true ? 0 : 1)' - : 'quit(db.hello().isWritablePrimary === true ? 0 : 1)', + $legacy ? 'quit(db.isMaster().ismaster === true ? 0 : 1)' : 'quit(db.hello().isWritablePrimary === true ? 0 : 1)', ]; + + return implode(' ', array_map('escapeshellarg', $command)); } private function add_custom_mongo_conf() diff --git a/tests/Unit/StartMongodbHealthCheckTest.php b/tests/Unit/StartMongodbHealthCheckTest.php index b2f687bdab..adad413e45 100644 --- a/tests/Unit/StartMongodbHealthCheckTest.php +++ b/tests/Unit/StartMongodbHealthCheckTest.php @@ -3,14 +3,6 @@ use App\Actions\Database\StartMongodb; use App\Models\StandaloneMongodb; -it('uses the generated MongoDB health check in the Compose configuration', function () { - $source = remoteOutputSource('app/Actions/Database/StartMongodb.php'); - - expect($source) - ->toContain("'healthcheck' => \$this->database->healthCheckConfiguration(\$this->generate_health_check_command())") - ->not->toContain("'healthcheck' => \$this->database->healthCheckConfiguration([\n 'CMD',\n 'echo',\n 'ok'"); -}); - function generatedMongodbHealthCheckCommand(array $attributes): array { $action = new StartMongodb; @@ -21,69 +13,73 @@ function generatedMongodbHealthCheckCommand(array $attributes): array return $method->invoke($action); } -it('generates a credential-free writable-primary MongoDB health check', function (string $image, bool $enableSsl, ?string $sslMode, array $expectedCommand) { - $command = generatedMongodbHealthCheckCommand([ +/** + * Runs the health check script with fake shells on PATH and returns the program and arguments that were called. + * + * @param list $availableShells + */ +function runMongodbHealthCheck(array $healthCheck, array $availableShells): string +{ + $bin = sys_get_temp_dir().'/mongodb-healthcheck-'.bin2hex(random_bytes(4)); + mkdir($bin); + foreach ($availableShells as $shell) { + file_put_contents("$bin/$shell", "#!/bin/sh\necho $shell; for arg in \"\$@\"; do echo \"\$arg\"; done\n"); + chmod("$bin/$shell", 0755); + } + + $output = shell_exec('PATH='.escapeshellarg($bin).' /bin/sh -c '.escapeshellarg($healthCheck[1])); + + array_map('unlink', glob("$bin/*")); + rmdir($bin); + + return trim((string) $output); +} + +it('uses mongosh when the image has it and the legacy mongo shell otherwise', function (array $availableShells, bool $enableSsl, ?string $sslMode, array $expectedCall) { + $healthCheck = generatedMongodbHealthCheckCommand([ 'uuid' => 'mongodb-test-resource', - 'image' => $image, + 'image' => 'registry.example.com/mongo:any', 'enable_ssl' => $enableSsl, 'ssl_mode' => $sslMode, 'mongo_initdb_root_username' => 'root-user', 'mongo_initdb_root_password' => 'root-password', ]); - expect($command)->toBe($expectedCommand) - ->and(implode(' ', $command)) + expect($healthCheck[0])->toBe('CMD-SHELL') + ->and($healthCheck[1]) ->not->toContain('root-user') ->not->toContain('root-password') - ->not->toContain('--tlsAllowInvalidHostnames') - ->not->toContain('--tlsAllowInvalidCertificates'); + ->not->toContain('AllowInvalid') + ->and(runMongodbHealthCheck($healthCheck, $availableShells))->toBe(implode("\n", $expectedCall)); })->with([ - 'without TLS' => ['mongo:8', false, null, [ - 'CMD', 'mongosh', '--quiet', '--host', 'mongodb-test-resource', '--eval', + 'mongosh without TLS' => [['mongosh', 'mongo'], false, null, [ + 'mongosh', '--quiet', '--host', 'mongodb-test-resource', '--eval', 'quit(db.hello().isWritablePrimary === true ? 0 : 1)', ]], - 'TLS allow mode' => ['mongo:8', true, 'allow', [ - 'CMD', 'mongosh', '--quiet', '--host', 'mongodb-test-resource', + 'mongosh with TLS require' => [['mongosh'], true, 'require', [ + 'mongosh', '--quiet', '--host', 'mongodb-test-resource', '--tls', '--tlsCAFile', '/etc/mongo/certs/ca.pem', '--eval', 'quit(db.hello().isWritablePrimary === true ? 0 : 1)', ]], - 'TLS prefer mode' => ['mongo:8', true, 'prefer', [ - 'CMD', 'mongosh', '--quiet', '--host', 'mongodb-test-resource', - '--tls', '--tlsCAFile', '/etc/mongo/certs/ca.pem', '--eval', + 'mongosh with TLS verify-full' => [['mongosh'], true, 'verify-full', [ + 'mongosh', '--quiet', '--host', 'mongodb-test-resource', + '--tls', '--tlsCAFile', '/etc/mongo/certs/ca.pem', + '--tlsCertificateKeyFile', '/etc/mongo/certs/server.pem', '--eval', 'quit(db.hello().isWritablePrimary === true ? 0 : 1)', ]], - 'TLS require mode' => ['mongo:8', true, 'require', [ - 'CMD', 'mongosh', '--quiet', '--host', 'mongodb-test-resource', - '--tls', '--tlsCAFile', '/etc/mongo/certs/ca.pem', '--eval', - 'quit(db.hello().isWritablePrimary === true ? 0 : 1)', - ]], - 'TLS verify-full mode' => ['mongo:8', true, 'verify-full', [ - 'CMD', 'mongosh', '--quiet', '--host', 'mongodb-test-resource', - '--tls', - '--tlsCAFile', - '/etc/mongo/certs/ca.pem', - '--tlsCertificateKeyFile', - '/etc/mongo/certs/server.pem', - '--eval', - 'quit(db.hello().isWritablePrimary === true ? 0 : 1)', - ]], - 'MongoDB 4 without TLS' => ['mongo:4.4', false, null, [ - 'CMD', 'mongo', '--quiet', '--host', 'mongodb-test-resource', '--eval', + 'legacy mongo without TLS' => [['mongo'], false, null, [ + 'mongo', '--quiet', '--host', 'mongodb-test-resource', '--eval', 'quit(db.isMaster().ismaster === true ? 0 : 1)', ]], - 'MongoDB 4 TLS require mode' => ['mongo:4.4', true, 'require', [ - 'CMD', 'mongo', '--quiet', '--host', 'mongodb-test-resource', + 'legacy mongo with TLS require' => [['mongo'], true, 'require', [ + 'mongo', '--quiet', '--host', 'mongodb-test-resource', '--ssl', '--sslCAFile', '/etc/mongo/certs/ca.pem', '--eval', 'quit(db.isMaster().ismaster === true ? 0 : 1)', ]], - 'MongoDB 4 TLS verify-full mode' => ['mongo:4.4', true, 'verify-full', [ - 'CMD', 'mongo', '--quiet', '--host', 'mongodb-test-resource', - '--ssl', - '--sslCAFile', - '/etc/mongo/certs/ca.pem', - '--sslPEMKeyFile', - '/etc/mongo/certs/server.pem', - '--eval', + 'legacy mongo with TLS verify-full' => [['mongo'], true, 'verify-full', [ + 'mongo', '--quiet', '--host', 'mongodb-test-resource', + '--ssl', '--sslCAFile', '/etc/mongo/certs/ca.pem', + '--sslPEMKeyFile', '/etc/mongo/certs/server.pem', '--eval', 'quit(db.isMaster().ismaster === true ? 0 : 1)', ]], ]);