diff --git a/.dockerignore b/.dockerignore index 3eb4f38e..f8812460 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,11 @@ /.github/ +/.idea/ /config/dirigent.yaml /config/packages/dirigent.yaml /node_modules/ +/public/build/ +/public/bundles/ /storage/ +/tests/ /var/ /vendor/ diff --git a/Dockerfile b/Dockerfile index a4786561..9d9f1273 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN composer install \ --no-scripts \ --prefer-dist -FROM node:latest AS node_build +FROM node:23 AS node_build WORKDIR /srv/app @@ -35,6 +35,8 @@ LABEL org.opencontainers.image.licenses=FSL-1.1-MIT ARG UID=1000 ARG GID=1000 +COPY docker/entrypoint.sh docker/init.sh /srv/ + RUN set -e; \ addgroup -g $GID -S dirigent; \ adduser -u $UID -S -G dirigent dirigent; \ @@ -43,6 +45,7 @@ RUN set -e; \ caddy \ curl \ git \ + openssl \ php82 \ php82-ctype \ php82-curl \ @@ -64,44 +67,45 @@ RUN set -e; \ supervisor; \ ln -s /usr/sbin/php-fpm82 /usr/sbin/php-fpm; \ mkdir -p /run/postgresql /srv/config /srv/data; \ - chown -R dirigent:dirigent /run /srv; + chown -R dirigent:dirigent /run /srv; \ + chmod +x /srv/entrypoint.sh /srv/init.sh; COPY --from=composer_build /usr/bin/composer /usr/bin/composer -COPY docker/init.sh / COPY docker/Caddyfile /etc/caddy/ COPY docker/php.ini /etc/php82/conf.d/ COPY docker/php-fpm.conf /etc/php82/ COPY docker/supervisord.conf /etc/ COPY docker/process /srv/process/ +COPY docker/scripts /srv/scripts/ USER dirigent -ENV APP_ENV="prod" -ENV DATABASE_URL="postgresql://dirigent@127.0.0.1:5432/dirigent?serverVersion=16&charset=utf8" -ENV DIRIGENT_IMAGE=1 - WORKDIR /srv/app -COPY --chown=dirigent:dirigent --from=composer_build /srv/app ./ -COPY --chown=dirigent:dirigent --from=node_build /srv/app/public/build public/build/ -COPY --chown=dirigent:dirigent readme.md license.md ./ -COPY --chown=dirigent:dirigent .env.dirigent ./ -COPY --chown=dirigent:dirigent bin bin/ -COPY --chown=dirigent:dirigent config config/ -COPY --chown=dirigent:dirigent migrations migrations/ -COPY --chown=dirigent:dirigent public public/ -COPY --chown=dirigent:dirigent src src/ -COPY --chown=dirigent:dirigent translations translations/ -COPY --chown=dirigent:dirigent templates templates/ +COPY --chown=$UID:$GID --from=composer_build /srv/app ./ +COPY --chown=$UID:$GID --from=node_build /srv/app/public/build public/build/ +COPY --chown=$UID:$GID readme.md license.md ./ +COPY --chown=$UID:$GID bin/console bin/dirigent bin/ +COPY --chown=$UID:$GID docker/config.yaml config/dirigent.yaml +COPY --chown=$UID:$GID docker/env.php ./.env.dirigent.local.php +COPY --chown=$UID:$GID config config/ +COPY --chown=$UID:$GID docs docs/ +COPY --chown=$UID:$GID migrations migrations/ +COPY --chown=$UID:$GID public public/ +COPY --chown=$UID:$GID src src/ +COPY --chown=$UID:$GID translations translations/ +COPY --chown=$UID:$GID templates templates/ RUN set -e; \ chmod +x bin/console; \ chmod +x bin/dirigent; \ - composer dump-autoload --classmap-authoritative --no-ansi --no-interaction + composer dump-autoload --classmap-authoritative --no-ansi --no-interaction; +VOLUME /srv/config VOLUME /srv/data EXPOSE 7015 -CMD ["sh", "/init.sh"] +ENTRYPOINT ["/srv/entrypoint.sh"] +CMD ["-init"] diff --git a/docker/config.yaml b/docker/config.yaml new file mode 100644 index 00000000..cc4b750c --- /dev/null +++ b/docker/config.yaml @@ -0,0 +1,2 @@ +framework: + secret: '%env(file:KERNEL_SECRET_FILE)%' diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..3c709e65 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh + +set -e + +# If the first argument is `-init`, run the application. This is +# also the default command. +if [ "$1" = "-init" ]; then + set -- /srv/init.sh +else + # If the first argument is `--`, execute the remaining arguments as a + # new command, otherwise pass the arguments to the Dirigent binary. + if [ "$1" = "--" ]; then + set -- ${@:2} + else + set -- bin/dirigent "$@" + fi +fi + +exec "$@" diff --git a/docker/env.php b/docker/env.php new file mode 100644 index 00000000..bc72ec75 --- /dev/null +++ b/docker/env.php @@ -0,0 +1,14 @@ + 'prod', + 'DATABASE_URL' => 'postgresql://dirigent@127.0.0.1:5432/dirigent?serverVersion=16&charset=utf8', + 'DIRIGENT_IMAGE' => '1', + 'GITHUB_TOKEN' => '', + 'KERNEL_SECRET_FILE' => '/srv/config/secrets/kernel_secret', + 'MAILER_DSN' => 'null://null', + 'MESSENGER_TRANSPORT_DSN' => 'doctrine://default?auto_setup=0', + 'SENTRY_DSN' => '', + 'SYMFONY_DOTENV_PATH' => './.env.dirigent', + 'TRUSTED_PROXIES' => '', +]; diff --git a/docker/init.sh b/docker/init.sh index 729cfcce..370db756 100644 --- a/docker/init.sh +++ b/docker/init.sh @@ -1,5 +1,14 @@ -#!/bin/sh +#!/usr/bin/env sh set -e +# Run init scripts +for file in $(find "/srv/scripts/init" -type f | sort -t '-' -k1,1n) +do + echo "Execute init script: $file" + + sh "$file" +done + +# Start Supervisor exec supervisord diff --git a/docker/process/caddy.sh b/docker/process/caddy.sh index 3e175c7c..f51b502f 100755 --- a/docker/process/caddy.sh +++ b/docker/process/caddy.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env sh set -e diff --git a/docker/process/consumer.sh b/docker/process/consumer.sh index 3b1927e6..139ed990 100755 --- a/docker/process/consumer.sh +++ b/docker/process/consumer.sh @@ -1,10 +1,17 @@ -#!/bin/sh +#!/usr/bin/env sh set -e -while [ -z "$(netstat -an | grep :9000)" ]; do - echo "Waiting for app"; - sleep 5; -done; +while [ ! "$(netstat -an | grep :9000)" ]; do + echo "Worker is waiting for application" -exec /srv/app/bin/console messenger:consume async scheduler_packages --sleep 10 + sleep 5 +done + +function shutdown() { + bin/console messenger:stop-workers +} + +trap shutdown HUP INT QUIT ABRT KILL ALRM TERM TSTP + +exec bin/console messenger:consume async scheduler_packages --sleep 10 diff --git a/docker/process/fpm.sh b/docker/process/fpm.sh index ef96a57a..dc3b6e46 100755 --- a/docker/process/fpm.sh +++ b/docker/process/fpm.sh @@ -1,16 +1,11 @@ -#!/bin/sh +#!/usr/bin/env sh set -e -composer run-script --no-ansi --no-interaction auto-scripts +while [ ! $(pg_isready) ]; do + echo "Application is waiting for the database" -# todo temporary timeout for database connection -while ! nc -z localhost 5432; do - echo "Waiting for database connection"; - sleep 3; -done; - -bin/console doctrine:database:create --if-not-exists --no-ansi --no-interaction -bin/console doctrine:migrations:migrate --allow-no-migration --no-ansi --no-interaction + sleep 3 +done exec php-fpm diff --git a/docker/process/postgres.sh b/docker/process/postgres.sh new file mode 100755 index 00000000..8f6e1cf6 --- /dev/null +++ b/docker/process/postgres.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +set -e + +function shutdown() { + pkill postgres +} + +trap shutdown HUP INT QUIT ABRT KILL ALRM TERM TSTP + +exec postgres -D /srv/data/postgresql diff --git a/docker/process/postgresql.sh b/docker/process/postgresql.sh deleted file mode 100755 index 0e91fa05..00000000 --- a/docker/process/postgresql.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -set -e - -function shutdown() -{ - pkill postgres -} - -if [ ! -d "/srv/data/postgresql" ]; then - mkdir -p /srv/data/postgresql - initdb /srv/data/postgresql -fi - -trap shutdown HUP INT QUIT ABRT KILL ALRM TERM TSTP - -exec postgres -D /srv/data/postgresql diff --git a/docker/scripts/init/10-kernel-secret.sh b/docker/scripts/init/10-kernel-secret.sh new file mode 100644 index 00000000..0d61f881 --- /dev/null +++ b/docker/scripts/init/10-kernel-secret.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +set -e + +if [ -f "/srv/config/secrets/kernel_secret" ]; then + echo "Kernel secret exists" +fi + +# Make sure secrets directory exists +mkdir -p /srv/config/secrets + +# Generate a kernel secret and save the value +secret=$(openssl rand -base64 12) +echo $secret > /srv/config/secrets/kernel_secret + +echo "Generated a new kernel secret" diff --git a/docker/scripts/init/10-postgres-init.sh b/docker/scripts/init/10-postgres-init.sh new file mode 100644 index 00000000..5b0aabc0 --- /dev/null +++ b/docker/scripts/init/10-postgres-init.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env sh + +set -e + +if [ -d "/srv/data/postgresql" ]; then + echo "Database directory found" + + # Start Postgres server + pg_ctl start -D /srv/data/postgresql + + exit 0 +fi + +echo "Creating PostgreSQL database..." + +# Create database directory +mkdir -p /srv/data/postgresql + +# Initialize database storage +initdb /srv/data/postgresql + +# Start Postgres server +pg_ctl start -D /srv/data/postgresql + +# Create database +createdb dirigent + +echo "Created PostgreSQL database" diff --git a/docker/scripts/init/30-composer-scripts.sh b/docker/scripts/init/30-composer-scripts.sh new file mode 100644 index 00000000..651f2a7f --- /dev/null +++ b/docker/scripts/init/30-composer-scripts.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +set -e + +composer run-script --no-ansi --no-interaction auto-scripts diff --git a/docker/scripts/init/70-database-migrations.sh b/docker/scripts/init/70-database-migrations.sh new file mode 100644 index 00000000..305a298f --- /dev/null +++ b/docker/scripts/init/70-database-migrations.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -e + +bin/console doctrine:migrations:sync-metadata-storage --no-ansi --no-interaction +bin/console doctrine:migrations:migrate --allow-no-migration --no-ansi --no-interaction diff --git a/docker/scripts/init/90-postgres-close.sh b/docker/scripts/init/90-postgres-close.sh new file mode 100644 index 00000000..0e7ae6b5 --- /dev/null +++ b/docker/scripts/init/90-postgres-close.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -e + +# Stop Postgres server +pg_ctl stop -D /srv/data/postgresql diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 9a705baf..359f8301 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -14,6 +14,7 @@ command=sh /srv/process/consumer.sh stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true +stopsignal=QUIT [program:fpm] command=sh /srv/process/fpm.sh @@ -21,8 +22,8 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true -[program:postgresql] -command=sh /srv/process/postgresql.sh +[program:postgres] +command=sh /srv/process/postgres.sh stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true diff --git a/tests/Docker/Standalone/ConsoleTest.php b/tests/Docker/Standalone/ConsoleTest.php index 46fd5f11..17b16962 100644 --- a/tests/Docker/Standalone/ConsoleTest.php +++ b/tests/Docker/Standalone/ConsoleTest.php @@ -4,14 +4,6 @@ class ConsoleTest extends DockerStandaloneTestCase { - public function testComposerPlatformRequirements(): void - { - $this->assertCommandSuccessful( - ['composer', 'check-platform-reqs', '--no-dev'], - 'Platform requirements of Composer packages must be met.', - ); - } - public function testConsole(): void { $this->assertCommandSuccessful( diff --git a/tests/Docker/Standalone/DatabaseTest.php b/tests/Docker/Standalone/DatabaseTest.php index 518cae0c..34a347fc 100644 --- a/tests/Docker/Standalone/DatabaseTest.php +++ b/tests/Docker/Standalone/DatabaseTest.php @@ -4,6 +4,19 @@ class DatabaseTest extends DockerStandaloneTestCase { + public function testSchemaValid(): void + { + $this->assertCommandSuccessful( + ['bin/console', 'doctrine:schema:validate', '--skip-mapping', '--skip-property-types', '--no-interaction'], + 'The database schema must be valid.', + ); + + $this->assertCommandSuccessful( + ['bin/console', 'doctrine:migrations:up-to-date', '--no-interaction'], + 'The database migrations must be up-to-date.', + ); + } + public function testRunSql(): void { $this->assertCommandSuccessful( diff --git a/tests/Docker/Standalone/DockerStandaloneTestCase.php b/tests/Docker/Standalone/DockerStandaloneTestCase.php index ee2ab2da..a913518b 100644 --- a/tests/Docker/Standalone/DockerStandaloneTestCase.php +++ b/tests/Docker/Standalone/DockerStandaloneTestCase.php @@ -15,7 +15,7 @@ protected function setUp(): void { $this->container = (new GenericContainer('dirigent-standalone')) ->withExposedPorts(7015) - ->withMount(__DIR__ . '/scripts', '/srv/tests') + ->withMount(__DIR__ . '/scripts', '/srv/scripts/tests') ->withWait(new WaitForLog('ready to handle connections')) ->start(); } @@ -27,7 +27,7 @@ protected function tearDown(): void protected function assertCommandSuccessful(array $command, ?string $message = null): void { - $result = $this->container->exec(['sh', '/srv/tests/command-successful.sh', ...$command]); + $result = $this->container->exec(['sh', '/srv/scripts/tests/command-successful.sh', ...$command]); if ('0' === $result) { $this->addToAssertionCount(1); } else { @@ -39,4 +39,17 @@ protected function assertCommandSuccessful(array $command, ?string $message = nu $this->fail($message); } } + + protected function assertContainerFileExists(string $path, ?string $message = null): void + { + $result = $this->container->exec(['sh', '/srv/scripts/tests/file-exists.sh', $path]); + if ('0' === $result) { + $this->addToAssertionCount(1); + } else { + $message = $message ? "$message\n" : null; + $message .= "Failed asserting file \"$path\" exists."; + + $this->fail($message); + } + } } diff --git a/tests/Docker/Standalone/EntrypointTest.php b/tests/Docker/Standalone/EntrypointTest.php new file mode 100644 index 00000000..1e00299a --- /dev/null +++ b/tests/Docker/Standalone/EntrypointTest.php @@ -0,0 +1,58 @@ +withWait(new WaitForLog('ready to handle connections')) + ->start() + ->stop(); + + $this->addToAssertionCount(1); + + // Running the container with the `-init` command must result in a running application. + (new GenericContainer('dirigent-standalone')) + ->withCommand(['-init']) + ->withWait(new WaitForLog('ready to handle connections')) + ->start() + ->stop(); + + $this->addToAssertionCount(1); + } + + public function testDirigent(): void + { + $container = (new GenericContainer('dirigent-standalone')) + ->withCommand(['list']) + ->withWait(new WaitForLog('Dirigent')) + ->start(); + + $result = $container->logs(); + + $container->stop(); + + $this->assertStringStartsWith('Dirigent', $result, 'Running the container with any other command (than `-init`) must be passed to the Dirigent binary.'); + } + + public function testPassthrough(): void + { + $container = (new GenericContainer('dirigent-standalone')) + ->withCommand(['--', 'bin/console', 'list']) + ->withWait(new WaitForLog('Symfony')) + ->start(); + + $result = $container->logs(); + + $container->stop(); + + $this->assertStringStartsWith(')Symfony', $result, 'Running the container with an `--` argument must have its remaining arguments be interpreted as a command.'); + } +} diff --git a/tests/Docker/Standalone/InitTest.php b/tests/Docker/Standalone/InitTest.php new file mode 100644 index 00000000..9aedf4ed --- /dev/null +++ b/tests/Docker/Standalone/InitTest.php @@ -0,0 +1,14 @@ +assertContainerFileExists( + '/srv/config/secrets/kernel_secret', + 'A kernel_secret file must be generated.', + ); + } +} diff --git a/tests/Docker/Standalone/PhpPlatformTest.php b/tests/Docker/Standalone/PhpPlatformTest.php new file mode 100644 index 00000000..15e62fc3 --- /dev/null +++ b/tests/Docker/Standalone/PhpPlatformTest.php @@ -0,0 +1,22 @@ +assertCommandSuccessful( + ['composer', 'check-platform-reqs', '--no-dev'], + 'Platform requirements of Composer packages must be met.', + ); + } + + public function testSymfonyCacheGenerated(): void + { + $this->assertContainerFileExists( + 'var/cache/prod/CodedMonkey_Dirigent_KernelProdContainer.php', + 'The Symfony cache must be generated during initialization.', + ); + } +} diff --git a/tests/Docker/Standalone/scripts/file-exists.sh b/tests/Docker/Standalone/scripts/file-exists.sh new file mode 100644 index 00000000..b94c0512 --- /dev/null +++ b/tests/Docker/Standalone/scripts/file-exists.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +if [ -e $1 ] +then + echo "0" +else + echo "1" +fi