From a8611f5d80ee591b1c42375384ee95311c18c21f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 10 Feb 2024 09:54:59 +0100 Subject: [PATCH] Support loading env vars from secret files --- CHANGELOG.md | 2 +- bin/test/run-api-tests.sh | 6 ++++-- module/Core/src/Config/EnvVars.php | 23 ++++++++++++++++++++++- module/Core/test/Config/EnvVarsTest.php | 10 ++++++++++ module/Core/test/DB_PASSWORD.env | 1 + 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 module/Core/test/DB_PASSWORD.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0b0c9f0..3093f52bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added -* *Nothing* +* [#1868](https://github.com/shlinkio/shlink/issues/1868) Add support for [docker compose secrets](https://docs.docker.com/compose/use-secrets/) to the docker image. ### Changed * [#1935](https://github.com/shlinkio/shlink/issues/1935) Replace dependency on abandoned `php-middleware/request-id` with userland simple middleware. diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index b22a974e6..6a1cfb465 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -20,6 +20,8 @@ echo 'Starting server...' [ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d [ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ -o=http.address=0.0.0.0:9999 \ + -o=http.pool.debug=false \ + -o=jobs.pool.debug=false \ -o=logs.encoding=json \ -o=logs.channels.http.encoding=json \ -o=logs.channels.server.encoding=json \ @@ -29,10 +31,10 @@ echo 'Starting server...' sleep 2 # Let's give the server a couple of seconds to start vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $* -testsExitCode=$? +TESTS_EXIT_CODE=$? [ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop [ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 # Exit this script with the same code as the tests. If tests failed, this script has to fail -exit $testsExitCode +exit $TESTS_EXIT_CODE diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index ff64838be..40f311e9b 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -4,7 +4,11 @@ namespace Shlinkio\Shlink\Core\Config; +use function file_get_contents; +use function is_file; use function Shlinkio\Shlink\Config\env; +use function Shlinkio\Shlink\Config\parseEnvVar; +use function sprintf; enum EnvVars: string { @@ -77,7 +81,24 @@ enum EnvVars: string public function loadFromEnv(mixed $default = null): mixed { - return env($this->value, $default); + return env($this->value) ?? $this->loadFromFileEnv() ?? $default; + } + + /** + * Checks if an equivalent environment variable exists with the `_FILE` suffix. If so, it loads its value as a file, + * reads it, and returns its contents. + * This is useful when loading Shlink with docker compose and using secrets. + * See https://docs.docker.com/compose/use-secrets/ + */ + private function loadFromFileEnv(): string|int|bool|null + { + $file = env(sprintf('%s_FILE', $this->value)); + if ($file === null || ! is_file($file)) { + return null; + } + + $content = file_get_contents($file); + return $content ? parseEnvVar($content) : null; } public function existsInEnv(): bool diff --git a/module/Core/test/Config/EnvVarsTest.php b/module/Core/test/Config/EnvVarsTest.php index 0b012051c..dd83393b4 100644 --- a/module/Core/test/Config/EnvVarsTest.php +++ b/module/Core/test/Config/EnvVarsTest.php @@ -17,12 +17,16 @@ protected function setUp(): void { putenv(EnvVars::BASE_PATH->value . '=the_base_path'); putenv(EnvVars::DB_NAME->value . '=shlink'); + + $envFilePath = __DIR__ . '/../DB_PASSWORD.env'; + putenv(EnvVars::DB_PASSWORD->value . '_FILE=' . $envFilePath); } protected function tearDown(): void { putenv(EnvVars::BASE_PATH->value . '='); putenv(EnvVars::DB_NAME->value . '='); + putenv(EnvVars::DB_PASSWORD->value . '_FILE='); } #[Test, DataProvider('provideExistingEnvVars')] @@ -54,4 +58,10 @@ public static function provideEnvVarsValues(): iterable yield 'DB_DRIVER without default' => [EnvVars::DB_DRIVER, null, null]; yield 'DB_DRIVER with default' => [EnvVars::DB_DRIVER, 'foobar', 'foobar']; } + + #[Test] + public function fallsBackToReadEnvVarsFromFile(): void + { + self::assertEquals('this_is_the_password', EnvVars::DB_PASSWORD->loadFromEnv()); + } } diff --git a/module/Core/test/DB_PASSWORD.env b/module/Core/test/DB_PASSWORD.env new file mode 100644 index 000000000..d5b7bed86 --- /dev/null +++ b/module/Core/test/DB_PASSWORD.env @@ -0,0 +1 @@ +this_is_the_password