From ee39f4c78976e3a517089eb0b7e53b68f12fda30 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Thu, 18 Apr 2024 07:23:00 +0000 Subject: [PATCH] fixes #6, fixes #8 --- config/nginx-php.conf | 11 ++ lib/Commands/Compile.php | 139 ++++++++++++------ lib/Commands/Docker/Build.php | 32 ++++ lib/Commands/Keys/Create.php | 47 ++++++ lib/Config.php | 2 +- lib/Creators/NginxConfiguration.php | 18 +-- lib/DockerComposeCommand.php | 36 ++++- .../GetCompiledDockerComposeYMLPath.php | 17 --- lib/Traits/GetCompiledPath.php | 10 ++ studip-docker | 6 +- web/index.php | 2 +- 11 files changed, 235 insertions(+), 85 deletions(-) create mode 100644 config/nginx-php.conf create mode 100644 lib/Commands/Keys/Create.php delete mode 100644 lib/Traits/GetCompiledDockerComposeYMLPath.php create mode 100644 lib/Traits/GetCompiledPath.php diff --git a/config/nginx-php.conf b/config/nginx-php.conf new file mode 100644 index 0000000..f1329d4 --- /dev/null +++ b/config/nginx-php.conf @@ -0,0 +1,11 @@ +location ~ \.php(?:$|/) { + include fastcgi_params; + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + fastcgi_intercept_errors on; + fastcgi_pass $fastcgi_backend; + fastcgi_index index.php; + fastcgi_param SERVER_NAME $http_host; + fastcgi_param DOCUMENT_ROOT $site_document_root; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param SCRIPT_FILENAME $request_filename; +} diff --git a/lib/Commands/Compile.php b/lib/Commands/Compile.php index 3ead649..f05ad95 100644 --- a/lib/Commands/Compile.php +++ b/lib/Commands/Compile.php @@ -4,14 +4,18 @@ namespace Studip\Dockerized\Commands; use Studip\Dockerized\Config; use Studip\Dockerized\Creators\DockerComposeConfiguration; use Studip\Dockerized\Creators\NginxConfiguration; +use Studip\Dockerized\Traits\GetCompiledPath; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class Compile extends \Symfony\Component\Console\Command\Command +final class Compile extends Command { + use GetCompiledPath; + protected function configure() { $this->setName('compile'); @@ -21,10 +25,14 @@ final class Compile extends \Symfony\Component\Console\Command\Command p', InputOption::VALUE_OPTIONAL, 'Path to store the compiled files', - realpath(__DIR__ . '/../../compiled') + $this->getCompiledPath() ); $this->addOption('nginx', null, InputOption::VALUE_NEGATABLE, 'Compile nginx configuration', true); $this->addOption('docker', null, InputOption::VALUE_NEGATABLE, 'Compile docker configuration', true); + + $this->addOption('build', 'b', InputOption::VALUE_NEGATABLE, 'Whether to build the containers'); + $this->addOption('force', 'F', InputOption::VALUE_NONE, 'Force compilation'); + $this->addOption('start', 's', InputOption::VALUE_NONE, 'Start container after compilation (requires "build")'); } protected function initialize(InputInterface $input, OutputInterface $output) @@ -48,25 +56,41 @@ final class Compile extends \Symfony\Component\Console\Command\Command protected function execute(InputInterface $input, OutputInterface $output) { $compiledPath = realpath($input->getOption('path')); + $force = $input->getOption('force'); $io = new SymfonyStyle($input, $output); - $writeConfigFile = function (string $filename, string $content) use ($compiledPath, $io): void { - file_put_contents( - "{$compiledPath}/{$filename}", - $content - ); - $io->success('Config file ' . $filename . ' written'); + $writeConfigFile = function (string $filename, string $content) use ($compiledPath, $force, $io): bool { + $filepath = "{$compiledPath}/{$filename}"; + + if ( + !file_exists($filepath) + || $content !== file_get_contents($filepath) + || $force + ) { + file_put_contents($filepath, $content); + $io->success('Config file ' . $filename . ' written'); + + return true; + } + + return false; }; + $changed = 0; if ($input->getOption('nginx')) { - $writeConfigFile( + $changed += (int) $writeConfigFile( 'nginx.conf', $this->createNginxConfiguration() ); + + $changed += (int) $writeConfigFile( + 'nginx-sites.conf', + $this->createNginxSites() + ); } if ($input->getOption('docker')) { - $writeConfigFile( + $changed += (int) $writeConfigFile( 'docker-compose.yml', $this->createDockerComposerConfiguration( realpath(__DIR__ . '/../..'), @@ -75,6 +99,23 @@ final class Compile extends \Symfony\Component\Console\Command\Command ); } + if ($changed === 0) { + $io->comment('No changes'); + } + if ($input->getOption('build')) { + $this->getApplication()->doRun( + new ArrayInput(['command' => 'docker:build']), + $output + ); + + if ($input->getOption('start')) { + $this->getApplication()->doRun( + new ArrayInput(['command' => 'docker:start']), + $output + ); + } + } + return Command::SUCCESS; } @@ -89,8 +130,8 @@ final class Compile extends \Symfony\Component\Console\Command\Command // Declare volumes $volumes = [ - [realpath(__DIR__ . '/../../web'), '/var/www/html/studip-dockerized-config.app', 'ro'], - [realpath(__DIR__ . '/../../config.json'), '/var/www/html/studip-dockerized-config.json', 'ro'], + [realpath($cwd . '/web'), '/var/www/html/studip-dockerized-config.app', 'ro'], + [realpath($cwd . '/config.json'), '/var/www/html/studip-dockerized-config.json', 'ro'], ...array_map( fn(string $path, array $definition) => [$definition['source'], '/var/www/html/' . $path], array_keys(Config::getInstance()->get('sites') ?? []), @@ -110,15 +151,18 @@ final class Compile extends \Symfony\Component\Console\Command\Command ), ]); - $creator->addServiceVolume( - 'nginx', - "{$compiledPath}/nginx.conf", - '/etc/nginx/conf.d/default.conf', - 'ro' - ); + $mounts = [ + "{$compiledPath}/nginx.conf" => 'conf.d/nginx.conf', + "{$compiledPath}/nginx-sites.conf" => 'sites.conf', + "{$cwd}/config/nginx-php.conf" => 'php.conf', + "{$compiledPath}/ssl.crt" => "ssl.crt", + "{$compiledPath}/ssl.key" => "ssl.key", + ]; + foreach ($mounts as $source => $target) { + $creator->addServiceVolume('nginx', $source, "/etc/nginx/{$target}", 'ro'); + } $creator->addServiceVolume('nginx', realpath("{$cwd}/logs/nginx"), '/var/log/nginx'); - foreach ($volumes as $volume) { $creator->addServiceVolume('nginx', ...$volume); } @@ -176,26 +220,40 @@ final class Compile extends \Symfony\Component\Console\Command\Command { $creator = new NginxConfiguration(); - // Register event handler to add the nginx-php configuration to each location - $creator->on(NginxConfiguration::EVENT_LOCATION_ADD_BEFORE, function ($location, &$config) use ($creator) { - $config[] = $creator->write('location ~ \.php(?:$|/)', [ - 'include fastcgi_params', - 'fastcgi_split_path_info ^(.+?\.php)(/.*)$', - 'fastcgi_intercept_errors on', - 'fastcgi_pass $fastcgi_backend', - 'fastcgi_index index.php', - 'fastcgi_param SERVER_NAME $http_host', - 'fastcgi_param DOCUMENT_ROOT $site_document_root', - 'fastcgi_param PATH_INFO $fastcgi_path_info', - 'fastcgi_param SCRIPT_FILENAME $request_filename', + // Add upstreams and servers + foreach (Config::getInstance()->get('php') as $version => $port) { + $index = $this->getPHPVersionIndex($version, 'php'); + + $creator->addUpstream("{$index}_backend", [ + "server {$index}:9000" ]); - }); + + $creator->addServer([ + 'listen ' . $port . ' default_server ssl', + 'server_name _', + + 'ssl_certificate /etc/nginx/ssl.crt', + 'ssl_certificate_key /etc/nginx/ssl.key', + + 'root /var/www/html', + 'set $fastcgi_backend ' . $index . '_backend', + 'include sites.conf', + ]); + } + + return $creator->dump(); + } + + private function createNginxSites(): string + { + $creator = new NginxConfiguration(); // Add default location $creator->addLocation('/sites', [ 'alias /var/www/html/studip-dockerized-config.app', 'index index.php index.html index.html', 'set $site_document_root /var/www/html/studip-dockerized-config.app', + 'include php.conf', ]); // Add configured locations @@ -209,22 +267,7 @@ final class Compile extends \Symfony\Component\Console\Command\Command 'alias ' . $p, 'index index.php index.html index.html', 'set $site_document_root ' . $p, - ]); - } - - // Add upstreams and servers - foreach (Config::getInstance()->get('php') as $version => $port) { - $index = $this->getPHPVersionIndex($version, 'php'); - - $creator->addUpstream("{$index}_backend", [ - "server {$index}:9000" - ]); - - $creator->addServer([ - 'listen ' . $port . ' default_server', - 'server_name _', - 'root /var/www/html', - 'set $fastcgi_backend ' . $index . '_backend', + 'include php.conf', ]); } diff --git a/lib/Commands/Docker/Build.php b/lib/Commands/Docker/Build.php index 080e444..3ba1047 100644 --- a/lib/Commands/Docker/Build.php +++ b/lib/Commands/Docker/Build.php @@ -2,6 +2,10 @@ namespace Studip\Dockerized\Commands\Docker; use Studip\Dockerized\DockerComposeCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; final class Build extends DockerComposeCommand { @@ -12,6 +16,34 @@ final class Build extends DockerComposeCommand } protected function getDockerComposeCommand(): array { + $dockerRunning = $this->isDockerRunning(); + if ($dockerRunning) { + $this->createDockerComposeCommand(['down']); + } + return ['up', '--no-start']; } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $dockerRunning = $this->isDockerRunning(); + if ($dockerRunning) { + $this->getApplication()->doRun( + new ArrayInput(['command' => 'docker:stop']), + $output + ); + } + + $result = parent::execute($input, $output); + if ($result !== Command::SUCCESS || !$dockerRunning) { + return $result; + } + + $this->getApplication()->doRun( + new ArrayInput(['command' => 'docker:start']), + $output + ); + + return Command::SUCCESS; + } } \ No newline at end of file diff --git a/lib/Commands/Keys/Create.php b/lib/Commands/Keys/Create.php new file mode 100644 index 0000000..dcec238 --- /dev/null +++ b/lib/Commands/Keys/Create.php @@ -0,0 +1,47 @@ +<?php /** @noinspection PhpExpressionResultUnusedInspection */ + +namespace Studip\Dockerized\Commands\Keys; + +use Studip\Dockerized\Traits\GetCompiledPath; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Process\Process; + +final class Create extends Command +{ + use GetCompiledPath; + + protected function configure() + { + $this->setName('keys:create'); + $this->setDescription('Creates necessary keys for ssl'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $process = new Process([ + 'openssl', 'req', + '-batch', + '-x509', + '-nodes', + '-days', '365', + '-newkey', 'rsa:2048', + '-keyout', $this->getCompiledPath('ssl.key'), + '-out', $this->getCompiledPath('ssl.crt'), + ]); + $process->run(); + + if (!$process->isSuccessful()) { + $io->error(['Could not create keys', $process->getErrorOutput()]); + return Command::FAILURE; + } + + $io->success('Keys created successfully'); + + return Command::SUCCESS; + } +} diff --git a/lib/Config.php b/lib/Config.php index 147ed44..74cb7db 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -75,7 +75,7 @@ final class Config public function store(): void { - if (!is_writable($this->filename)) { + if (file_exists($this->filename) && !is_writable($this->filename)) { throw new \Exception("Config file '{$this->filename}' is not writable"); } diff --git a/lib/Creators/NginxConfiguration.php b/lib/Creators/NginxConfiguration.php index fba16ee..9e4c710 100644 --- a/lib/Creators/NginxConfiguration.php +++ b/lib/Creators/NginxConfiguration.php @@ -70,19 +70,17 @@ final class NginxConfiguration ...array_map( fn($server) => $this->write( 'server', - array_merge( - $server['config'], - array_map( - fn($location) => $this->write( - "location {$location['modifier']} {$location['location']}", - $location['config'] - ), - $this->locations - ) - ) + $server['config'] ), $this->servers ), + ...array_map( + fn($location) => $this->write( + "location {$location['modifier']} {$location['location']}", + $location['config'] + ), + $this->locations + ), ])); $result = (string) Scope::fromFile($tempName); unlink($tempName); diff --git a/lib/DockerComposeCommand.php b/lib/DockerComposeCommand.php index adc8636..f6cf466 100644 --- a/lib/DockerComposeCommand.php +++ b/lib/DockerComposeCommand.php @@ -1,7 +1,7 @@ <?php namespace Studip\Dockerized; -use Studip\Dockerized\Traits\GetCompiledDockerComposeYMLPath; +use Studip\Dockerized\Traits\GetCompiledPath; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -9,17 +9,29 @@ use Symfony\Component\Process\Process; abstract class DockerComposeCommand extends Command { - use GetCompiledDockerComposeYMLPath; + use GetCompiledPath; abstract protected function getDockerComposeCommand(): array; protected function execute(InputInterface $input, OutputInterface $output) { - $process = new Process([ + $process = $this->createDockerComposeCommand($this->getDockerComposeCommand()); + $this->streamProcess($process, $output); + return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE; + } + + protected function createDockerComposeCommand(array $command): Process + { + return new Process([ 'docker', 'compose', - '-f', self::GetCompiledDockerComposeYMLPath(), - ...$this->getDockerComposeCommand(), + '-f', self::GetCompiledPath() . '/docker-compose.yml', + ...$command, ]); + + } + + protected function streamProcess(Process $process, OutputInterface $output): void + { $process->setTty(true); $process->mustRun(function ($type, $buffer) use ($output) { if (Process::OUT === $type) { @@ -28,7 +40,19 @@ abstract class DockerComposeCommand extends Command $output->write('<error>' . $buffer . '</error>'); } }); + } - return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE; + protected function isDockerRunning(): bool + { + $process = $this->createDockerComposeCommand(['ps']); + $process->run(); + + if (!$process->isSuccessful()) { + return false; + } + + $output = trim($process->getOutput()); + + return count(explode("\n", $output)) > 1; } } \ No newline at end of file diff --git a/lib/Traits/GetCompiledDockerComposeYMLPath.php b/lib/Traits/GetCompiledDockerComposeYMLPath.php deleted file mode 100644 index 0c04b92..0000000 --- a/lib/Traits/GetCompiledDockerComposeYMLPath.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php -namespace Studip\Dockerized\Traits; - -trait GetCompiledDockerComposeYMLPath -{ - protected static function GetCompiledDockerComposeYMLPath(): string - { - $filename = realpath(__DIR__ . '/../../compiled/docker-compose.yml'); - if (!file_exists($filename)) { - throw new \Exception('docker-compose.yml was not yet compiled'); - } - if (!is_readable($filename)) { - throw new \Exception('docker-compose.yml is not readable'); - } - return $filename; - } -} \ No newline at end of file diff --git a/lib/Traits/GetCompiledPath.php b/lib/Traits/GetCompiledPath.php new file mode 100644 index 0000000..d0169b0 --- /dev/null +++ b/lib/Traits/GetCompiledPath.php @@ -0,0 +1,10 @@ +<?php +namespace Studip\Dockerized\Traits; + +trait GetCompiledPath +{ + protected static function GetCompiledPath(?string $filename = null): string + { + return realpath(__DIR__ . '/../../compiled') . ($filename ? '/' . $filename : ''); + } +} \ No newline at end of file diff --git a/studip-docker b/studip-docker index a1bb30e..aa12677 100755 --- a/studip-docker +++ b/studip-docker @@ -7,13 +7,13 @@ use Symfony\Component\Console\Application; const CONFIG_FILE = __DIR__ . '/config.json'; -\Studip\Dockerized\Config::load(CONFIG_FILE); - $application = new Application('Stud.IP Dockerized', '1.0'); $application->add(new Commands\Init()); if (file_exists(CONFIG_FILE)) { + \Studip\Dockerized\Config::load(CONFIG_FILE); + $application->add(new Commands\Compile()); $application->add(new Commands\Sites\Add()); @@ -27,6 +27,8 @@ if (file_exists(CONFIG_FILE)) { $application->add(new Commands\Docker\Build()); $application->add(new Commands\Docker\Start()); $application->add(new Commands\Docker\Stop()); + + $application->add(new Commands\Keys\Create()); } $application->run(); diff --git a/web/index.php b/web/index.php index 8169b24..5fdebf2 100644 --- a/web/index.php +++ b/web/index.php @@ -34,7 +34,7 @@ th { <td><?= htmlentities($path) ?></td> <?php foreach ($config['php'] as $version => $port): ?> <td> - <a href="http://localhost:<?= $port ?>/<?= htmlentities($path) ?>"> + <a href="https://localhost:<?= $port ?>/<?= htmlentities($path) ?>"> <?= htmlentities($version) ?> </a> </td> -- GitLab