Skip to content
Snippets Groups Projects
Commit bbda7e54 authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms
Browse files

Wip2

parent ad6cc1ce
No related branches found
No related tags found
1 merge request!3Wip2
......@@ -2,6 +2,8 @@
"name": "tleilax/studip-dockerized",
"type": "project",
"require": {
"php": "^8",
"ext-json": "*",
"symfony/console": "^6.4",
"symfony/yaml": "^6.4",
"symfony/var-dumper": "^6.4",
......
[dn]
CN=localhost
[req]
distinguished_name = dn
[EXT]
subjectAltName=DNS:localhost
keyUsage=digitalSignature
extendedKeyUsage=serverAuth
php_admin_flag[log_errors] = on
access.log = /var/log/php-access.log
php_admin_value[error_log] = /var/log/php-error.log
php_admin_value[error_log] = /var/log/error.log
access.log = /dev/null
pm.max_requests = 500
......@@ -128,12 +128,20 @@ final class Compile extends Command
{
$creator = new DockerComposeConfiguration('studip-dockerized');
$creator->hook('services', function (array $service): array {
$service['restart'] = 'unless-stopped';
return $service;
});
// Declare volumes
$volumes = [
[realpath($cwd . '/web'), '/var/www/html/studip-dockerized-config.app', 'ro'],
[realpath($cwd . '/config.json'), '/var/www/html/studip-dockerized-config.json', 'ro'],
[realpath($cwd . '/web'), '/usr/share/nginx/html/studip-dockerized-config.app', 'ro'],
[realpath($cwd . '/logs'), '/usr/share/nginx/html/logs', 'ro'],
[realpath($cwd . '/config.json'), '/usr/share/nginx/html/studip-dockerized-config.json', 'ro'],
...array_map(
fn(string $path, array $definition) => [$definition['source'], '/var/www/html/' . $path],
fn(string $path, array $definition) => [
$definition['source'], '/usr/share/nginx/html/' . $path
],
array_keys(Config::getInstance()->get('sites') ?? []),
array_values(Config::getInstance()->get('sites') ?? [])
)
......@@ -143,23 +151,30 @@ final class Compile extends Command
$creator->addService('nginx', 'nginx:alpine', [
'networks' => ['code-network'],
'depends_on' => ['redis-server', 'memcached-server'],
'ports' => array_map(
function ($port) {
return "{$port}:{$port}";
},
'ports' => array_merge(
Config::getInstance()->get('default') ? ['80:80', '443:443'] : [],
...array_map(
fn($config) => [
"{$config[0]}:{$config[0]}",
"{$config[1]}:{$config[1]}"
],
array_values(Config::getInstance()->get('php'))
),
),
]);
$mounts = [
"{$compiledPath}/nginx.conf" => 'conf.d/nginx.conf',
"{$compiledPath}/nginx.conf" => 'conf.d/default.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');
if (!str_starts_with($target, '/')) {
$target = "/etc/nginx/{$target}";
}
$creator->addServiceVolume('nginx', $source, $target, 'ro');
}
$creator->addServiceVolume('nginx', realpath("{$cwd}/logs/nginx"), '/var/log/nginx');
......@@ -225,25 +240,53 @@ final class Compile extends Command
public function createNginxConfiguration(): string
{
$creator = new NginxConfiguration();
$creator->hook('server', function (array $config): array {
$config['config'] = [
...$config['config'],
// Add upstreams and servers
foreach (Config::getInstance()->get('php') as $version => $port) {
'server_name _',
'proxy_read_timeout' => 300,
'proxy_connect_timeout' => 300,
'proxy_send_timeout' => 300,
'access_log off',
'error_log off',
'include sites.conf',
];
return $config;
});
// Add upstreams
$servers = [];
foreach (Config::getInstance()->get('php') as $version => [$port, $sslPort]) {
$index = $this->getPHPVersionIndex($version, 'php');
$creator->addUpstream("{$index}_backend", [
"server {$index}:9000"
]);
$servers[] = [$index, $port, $sslPort];
if (Config::getInstance()->get('default') === $version) {
$servers[] = [$index, 80, 443];
}
}
// Add servers
foreach ($servers as [$index, $port, $sslPort]) {
$creator->addServer([
'listen ' . $port . ' default_server ssl',
'server_name _',
'listen ' . $port . ' default_server',
'set $fastcgi_backend ' . $index . '_backend',
]);
$creator->addServer([
'listen ' . $sslPort . ' default_server ssl',
'set $fastcgi_backend ' . $index . '_backend',
'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',
]);
}
......@@ -256,15 +299,15 @@ final class Compile extends Command
// Add default location
$creator->addLocation('/sites', [
'alias /var/www/html/studip-dockerized-config.app',
'alias /usr/share/nginx/html/studip-dockerized-config.app',
'index index.php index.html index.html',
'set $site_document_root /var/www/html/studip-dockerized-config.app',
'set $site_document_root /usr/share/nginx/html/studip-dockerized-config.app',
'include php.conf',
]);
// Add configured locations
foreach (Config::getInstance()->get('sites') ?? [] as $path => $definition) {
$p = '/var/www/html/' . $path;
$p = '/usr/share/nginx/html/' . $path;
if (isset($definition['mount'])) {
$p .= '/' . $definition['mount'];
}
......
......@@ -25,13 +25,16 @@ final class Create extends Command
$process = new Process([
'openssl', 'req',
'-batch',
'-x509',
'-out', $this->getCompiledPath('ssl.crt'),
'-keyout', $this->getCompiledPath('ssl.key'),
'-newkey', 'rsa:2048',
'-nodes',
'-sha256',
'-days', '365',
'-newkey', 'rsa:2048',
'-keyout', $this->getCompiledPath('ssl.key'),
'-out', $this->getCompiledPath('ssl.crt'),
'-subj', '/CN=localhost',
'-extensions', 'EXT',
'-config', realpath(__DIR__ . '/../../../config/openssl-cert-request'),
]);
$process->run();
......
......@@ -26,12 +26,19 @@ final class Port extends Command
InputArgument::REQUIRED,
'Port to use'
);
$this->addArgument(
'ssl-port',
InputArgument::REQUIRED,
'SSL-Port to use'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$version = $input->getArgument('version');
$port = $input->getArgument('port');
$port = (int) $input->getArgument('port');
$sslPort = (int) $input->getArgument('ssl-port');
$config = Config::getInstance();
$io = new SymfonyStyle($input, $output);
......@@ -42,11 +49,11 @@ final class Port extends Command
}
$php = $config->get('php');
$php[$version] = (int) $port;
$php[$version] = [$port, $sslPort];
$config->set('php', $php);
$config->store();
$io->success("Port of PHP version {$version} was changed to {$port}");
$io->success("Ports of PHP version {$version} were changed to {$port} / {$sslPort}");
return Command::SUCCESS;
}
......
<?php
namespace Studip\Dockerized\Commands\PHP;
use Studip\Dockerized\Config;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
final class SetDefault extends Command
{
protected function configure()
{
$this->setName('php:default');
$this->setDescription('Set PHP version as default');
$this->addArgument(
'version',
InputArgument::REQUIRED,
'PHP version(s) that should be disabled',
null,
array_keys(Config::getInstance()->get('php')) + ['none']
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$config = Config::getInstance();
$io = new SymfonyStyle($input, $output);
$version = $input->getArgument('version');
if (
$version !== 'none'
&& !array_key_exists($version, $config->get('php'))
) {
$io->error("PHP version {$version} is not enabled");
return Command::FAILURE;
}
if ($version === $config->get('default')) {
return Command::SUCCESS;
}
if ($version === 'none') {
$config->delete('default');
$config->store();
$io->success('Default PHP version has been removed');
} else {
$config->set('default', $version);
$config->store();
$io->success("PHP version {$version} was set as default");
}
return Command::SUCCESS;
}
}
......@@ -40,9 +40,11 @@ final class Config
return $this->data[$key] ?? null;
}
public function set(string $key, mixed $value): void
public function set(string $key, mixed $value): Config
{
$this->data[$key] = $value;
return $this;
}
public function has(string $key): bool
......@@ -50,9 +52,11 @@ final class Config
return array_key_exists($key, $this->data);
}
public function delete(string $key): void
public function delete(string $key): Config
{
unset($this->data[$key]);
return $this;
}
public function dump(): string
......@@ -60,7 +64,7 @@ final class Config
return json_encode($this->data, JSON_PRETTY_PRINT);
}
public function reset(): void
public function reset(): Config
{
if (!file_exists($this->filename)) {
throw new \Exception("Config file '{$this->filename}' does not exist");
......@@ -71,14 +75,18 @@ final class Config
}
$this->data = json_decode(file_get_contents($this->filename), true);
return $this;
}
public function store(): void
public function store(): Config
{
if (file_exists($this->filename) && !is_writable($this->filename)) {
throw new \Exception("Config file '{$this->filename}' is not writable");
}
file_put_contents($this->filename, $this->dump());
return $this;
}
}
<?php
namespace Studip\Dockerized;
use Closure;
use Exception;
abstract class ConfigurationCreator
{
private array $configuration = [];
private array $hooks = [];
public function hook(string $type, Closure $hook): void
{
if (!isset($this->hooks[$type])) {
$this->hooks[$type] = [];
}
$this->hooks[$type][] = $hook;
}
public function addConfiguration(string $type, mixed $data, string $key = null): void
{
if (!isset($this->configuration[$type])) {
$this->configuration[$type] = [];
}
if (isset($key)) {
$this->configuration[$type][$key] = $this->applyHooks($type, $data);
} else {
$this->configuration[$type][] = $this->applyHooks($type, $data);
}
}
protected function getConfiguration(?string $type = null, mixed $default = null): mixed
{
if (func_num_args() === 0 || $type === null) {
return $this->configuration;
}
return $this->configuration[$type] ?? $default;
}
protected function applyHooks(string $type, mixed $config): mixed
{
foreach ($this->hooks[$type] ?? [] as $hook) {
$config = $hook($config);
}
return $config;
}
}
<?php
namespace Studip\Dockerized\Creators;
use Studip\Dockerized\ConfigurationCreator;
use Symfony\Component\Yaml\Yaml;
final class DockerComposeConfiguration
final class DockerComposeConfiguration extends ConfigurationCreator
{
private array $configuration = [];
private string $cwd;
public function __construct(string $cwd, string $name = '')
......@@ -13,58 +13,45 @@ final class DockerComposeConfiguration
$this->cwd = $cwd;
if ($name) {
$this->configuration[$name] = $name;
$this->addConfiguration('name', $name);
}
}
public function addService(string $service, ?string $image, array $config = []): void
{
if (!isset($this->configuration['services'])) {
$this->configuration['services'] = [];
}
$this->configuration['services'][$service] = array_merge(
isset($image) ? compact('image') : [],
$this->addConfiguration('services', array_merge(
isset($image) ? ['image' => $image] : [],
$config
);
}
public function extendService(string $service, array $config): void
{
if (!isset($this->configuration['services'][$service])) {
throw new \Exception('Service "' . $service . '" does not exist');
}
), $service);
}
public function addServiceVolume(string $service, string $source, string $target, string $modifier = ''): void
{
if (!isset($this->configuration['services'][$service])) {
$config = $this->getConfiguration('services', []);
if (!array_key_exists($service, $config)) {
throw new \Exception('Service "' . $service . '" does not exist');
}
if (!isset($this->configuration['services'][$service]['volumes'])) {
$this->configuration['services'][$service]['volumes'] = [];
if (!isset($config[$service]['volumes'])) {
$config[$service]['volumes'] = [];
}
$this->configuration['services'][$service]['volumes'][] = implode(':', array_filter([
$config[$service]['volumes'][] = implode(':', array_filter([
$source,
$target,
$modifier,
]));
$this->addConfiguration('services', $config[$service], $service);
}
public function addNetwork(string $network, array $config): void
{
if (!isset($this->configuration['networks'])) {
$this->configuration['networks'] = [];
}
$this->configuration['networks'][$network] = $config;
$this->addConfiguration('networks', $config, $network);
}
public function dump(): string
{
return Yaml::dump($this->configuration, 128);
return Yaml::dump($this->getConfiguration(), 128);
}
}
......@@ -2,58 +2,32 @@
namespace Studip\Dockerized\Creators;
use RomanPitak\Nginx\Config\Scope;
use Studip\Dockerized\ConfigurationCreator;
final class NginxConfiguration
final class NginxConfiguration extends ConfigurationCreator
{
public const EVENT_LOCATION_ADD_BEFORE = 'location:add:before';
public const EVENT_SERVER_ADD_BEFORE = 'server:add:before';
public const EVENT_UPSTREAM_ADD_BEFORE = 'upstream:add:before';
private array $locations = [];
private array $servers = [];
private array $upstreams = [];
private array $event_handlers = [];
public function __construct()
{
}
public function on(string $event, \Closure $handler): void
{
if (!isset($this->event_handlers[$event])) {
$this->event_handlers[$event] = [];
}
$this->event_handlers[$event][] = $handler;
}
private function trigger(string $event, &...$data): void
{
if (isset($this->event_handlers[$event])) {
foreach ($this->event_handlers[$event] as $handler) {
$handler(...$data);
}
}
}
public function addLocation(string $location, array $config, string $modifier = '^~')
{
$location = '/' . ltrim($location, '/');
$this->trigger(self::EVENT_LOCATION_ADD_BEFORE, $location, $config, $modifier);
$this->locations[] = compact('location', 'modifier', 'config');
$this->addConfiguration('location', [
'location' => $location = '/' . ltrim($location, '/'),
'modifier' => $modifier,
'config' => $config,
]);
}
public function addServer(array $config)
{
$this->trigger(self::EVENT_SERVER_ADD_BEFORE, $config);
$this->servers[] = compact('config');
$this->addConfiguration('server', [
'config' => $config,
]);
}
public function addUpstream(string $upstream, array $config)
{
$this->trigger(self::EVENT_UPSTREAM_ADD_BEFORE, $upstream, $config);
$this->upstreams[] = compact('upstream', 'config');
$this->addConfiguration('upstream', [
'upstream' => $upstream,
'config' => $config,
]);
}
public function dump(): string
......@@ -65,21 +39,21 @@ final class NginxConfiguration
"upstream {$upstream['upstream']}",
$upstream['config']
),
$this->upstreams
$this->getConfiguration('upstream', [])
),
...array_map(
fn($server) => $this->write(
'server',
$server['config']
),
$this->servers
$this->getConfiguration('server', [])
),
...array_map(
fn($location) => $this->write(
"location {$location['modifier']} {$location['location']}",
$location['config']
),
$this->locations
$this->getConfiguration('location', [])
),
]));
$result = (string) Scope::fromFile($tempName);
......@@ -88,12 +62,7 @@ final class NginxConfiguration
return $result;
}
public function __toString(): string
{
return $this->dump();
}
public function write(string $index, array $content): string
private function write(string $index, array $content): string
{
return sprintf(
'%s { %s }',
......
......@@ -4,13 +4,13 @@ namespace Studip\Dockerized;
final class SupportedPHPVersions
{
private const CONFIGURATION = [
'7.2' => 8072,
'7.3' => 8073,
'7.4' => 8074,
'8.0' => 8080,
'8.1' => 8081,
'8.2' => 8082,
'8.3' => 8083,
'7.2' => [8072, 18072],
'7.3' => [8073, 18073],
'7.4' => [8074, 18074],
'8.0' => [8080, 18080],
'8.1' => [8081, 18081],
'8.2' => [8082, 18082],
'8.3' => [8083, 18083],
];
private const DEFAULT = ['7.4', '8.1', '8.3'];
......
......@@ -20,6 +20,7 @@ if (file_exists(CONFIG_FILE)) {
$application->add(new Commands\Sites\Remove());
$application->add(new Commands\Sites\Show());
$application->add(new Commands\PHP\SetDefault());
$application->add(new Commands\PHP\Disable());
$application->add(new Commands\PHP\Enable());
$application->add(new Commands\PHP\Port());
......
<?php
final class LogParser
{
private string $filename;
public function __construct(string $filename)
{
$this->filename = $filename;
}
public function hasData(): bool
{
return file_exists($this->filename)
&& filesize($this->filename) > 0;
}
public function entries(): array
{
$entries = [];
$index = -1;
$fp = fopen($this->filename, 'r');
while (!feof($fp)) {
$line = trim(fgets($fp));
if (strlen($line) === 0) {
continue;
}
if ($index === -1 || preg_match('/^\[.+]/', $line)) {
$index += 1;
}
if (!isset($entries[$index])) {
$entries[$index] = '';
}
$entries[$index] .= $entries[$index] ? "\n" . $line : $line;
}
return array_reverse($entries);
}
}
const ready = new Promise(resolve => {
if (document.readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', () => {
resolve();
});
}
});
ready.then(() => {
// const tabs = document.querySelector('.tabs');
//
// // Change default selected if hash is set
// if (window.location.hash) {
// tabs.childNodes.forEach(node => {
// node.classList.toggle('active', node.href === window.location.hash);
// });
// }
//
// const selectors = Array.from(tabs.querySelectorAll('a')).map(node => {
// const selector = new URL(node.href).hash;
//
// node.addEventListener('click', (event) => {
// tabs.querySelector('.active').classList.remove('active');
// node.parentNode.classList.toggle('active');
//
// selectors.forEach(s => {
// document.querySelector(s).toggleAttribute('hidden', s !== selector)
// });
//
// event.preventDefault();
// });
//
// if (!node.parentNode.classList.contains('active')) {
// document.querySelector(selector).setAttribute('hidden', '');
// }
//
// return selector;
// });
})
.tabs {
background-color: #fff;
border-bottom: 1px solid #ccc;
font-size: 1.5em;
line-height: 1.5em;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 1.5em;
padding: 0 1em;
}
.tabs a {
text-decoration: none;
}
.tabs a:focus {
font-weight: bold;
}
section {
padding-top: 1.5em;
}
section:not(:target) {
display: none;
}
table {
font-size: 3.5em;
width: 100%;
}
th {
text-align: left;
}
tr:hover td {
background-color: #fec;
}
<?php
require_once __DIR__ . '/LogParser.php';
$config = json_decode(file_get_contents(__DIR__ . '/../studip-dockerized-config.json'), true);
$versions = $config['php'];
ksort($versions);
$log = new LogParser(__DIR__ . '/../logs/php/error.log');
?>
<!doctype html>
<html>
<head>
<link href="assets/style.css" type="text/css" rel="stylesheet">
<script src="assets/script.js"></script>
<title>Available sites</title>
</head>
<body>
<style>
html {
font-size: 4em;
}
table {
width: 100%;
}
th {
text-align: left;
}
</style>
<nav class="tabs">
<span class="active">
<a href="#sites">Sites</a>
</span>
<span>
<a href="#logs">Logs</a>
</span>
</nav>
<section id="sites">
<?php if (empty($config['sites'])): ?>
No sites defined
<?php else: ?>
......@@ -31,12 +38,23 @@ th {
<tbody>
<?php foreach ($config['sites'] ?? [] as $path => $definition): ?>
<tr>
<td><?= htmlentities($path) ?></td>
<?php foreach ($config['php'] as $version => $port): ?>
<td>
<?php if (!empty($config['default'])): ?>
<a href="https://localhost/<?= htmlentities($path) ?>">
<?= htmlentities($path) ?>
</a>
<?php else: ?>
<?= htmlentities($path) ?>
<?php endif; ?>
</td>
<?php foreach ($versions as $version => [$port, $sslPort]): ?>
<td>
<a href="https://localhost:<?= $port ?>/<?= htmlentities($path) ?>">
<?= htmlentities($version) ?>
</a>
<a href="https://localhost:<?= $sslPort ?>/<?= htmlentities($path) ?>">
(SSL)
</a>
</td>
<?php endforeach; ?>
</tr>
......@@ -44,5 +62,17 @@ th {
</tbody>
</table>
<?php endif; ?>
</section>
<section id="logs">
<?php if ($log->hasData()): ?>
<ul>
<?php foreach ($log->entries() as $entry): ?>
<li><?= nl2br(htmlentities($entry)) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
</body>
</html>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment