diff --git a/config/nginx-php.conf b/config/nginx-php.conf new file mode 100644 index 0000000000000000000000000000000000000000..f1329d4fe6e60cbb0191a3ae715140ff96e37428 --- /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 3ead649497207bf892cb6d5dc47a4b48a5680ea9..f05ad95320b70a2591540628c99f4075b383a8bd 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 080e444e727ffaa7a7c9d2586374464de75f47e7..3ba1047f6728ed06eb0c7d93e49928adc5d15530 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 0000000000000000000000000000000000000000..dcec2387c57042fdcfde29048a3e89f81eec8589 --- /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 147ed44b3ae8ba6bcc0437092d8eae03b8fd61ab..74cb7db9647149585ee5bed767353d9b503f957e 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 fba16eec5b1c1c4b1c1a6916082de2f357a44d53..9e4c7103fd84901bfc392da4ce2545641252f537 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 adc8636e873d7b6e11fc98063153130a96f534b9..f6cf4669dcbfb448982cadf307916bf88a14da42 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 0c04b9229f26dbac5b2d1aeed0113feb6e7633ec..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..d0169b00de1145843337d822cd65ad18e16d6ea2 --- /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 a1bb30e561cc38b53971820cd00f978a56ab77ca..aa126775ff0714be8fb39f2fabde3f9a758cc5d2 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 8169b242c432de2e8dbe88c2163169d37c058399..5fdebf2f5bcf369aac79fd9c7046e25311ecbc94 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>