Feature/bouncer (#7)
* Initial work * Setup script stub * Wrap runit and call dos2unix beforehand. * Working to the point I need to make it sync in and out of s3. * Seems like we're done and its working. * Add build process. * Add build process. * Bugfixes discovered during deployment. * Copy certs into /live because certbot is a pain. * More elegant about hammering letsencrypt. * Working!
This commit is contained in:
parent
d820562de7
commit
2fd5c62074
26 changed files with 4086 additions and 2 deletions
34
.github/workflows/bouncer.yml
vendored
Normal file
34
.github/workflows/bouncer.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
name: Build Nginx + LetsEncrypt Bouncer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- bouncer
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- Build PHP Flavours
|
||||||
|
branches: [ 'master', 'feature/**' ]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bouncer-build:
|
||||||
|
name: "Bake Bouncer Container"
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: docker/setup-qemu-action@v1
|
||||||
|
- uses: docker/setup-buildx-action@v1
|
||||||
|
- uses: docker/login-action@v1
|
||||||
|
name: Login to Docker Hub
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||||
|
- uses: docker/build-push-action@v2
|
||||||
|
name: Build & Push
|
||||||
|
with:
|
||||||
|
context: bouncer
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
pull: true
|
||||||
|
push: true
|
||||||
|
tags: benzine/bouncer
|
||||||
1
bouncer/.dockerignore
Normal file
1
bouncer/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
vendor
|
||||||
3
bouncer/.gitignore
vendored
Normal file
3
bouncer/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/docker-compose.override.yml
|
||||||
|
/vendor
|
||||||
|
/.php-cs-fixer.cache
|
||||||
22
bouncer/.php-cs-fixer.php
Normal file
22
bouncer/.php-cs-fixer.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
$finder = PhpCsFixer\Finder::create();
|
||||||
|
$finder->in(__DIR__);
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config)
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setHideProgress(false)
|
||||||
|
->setRules([
|
||||||
|
'@PSR2' => true,
|
||||||
|
'strict_param' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'@PhpCsFixer' => true,
|
||||||
|
'@PHP73Migration' => true,
|
||||||
|
'no_php4_constructor' => true,
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'no_useless_else' => true,
|
||||||
|
'no_superfluous_phpdoc_tags' => true,
|
||||||
|
'void_return' => true,
|
||||||
|
'yoda_style' => false,
|
||||||
|
])
|
||||||
|
->setFinder($finder)
|
||||||
|
;
|
||||||
48
bouncer/Dockerfile
Normal file
48
bouncer/Dockerfile
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
FROM benzine/php:cli-8.0
|
||||||
|
LABEL maintainer="Matthew Baggett <matthew@baggett.me>" \
|
||||||
|
org.label-schema.vcs-url="https://github.com/benzine-framework/docker"
|
||||||
|
COPY self-signed-certificates /certs
|
||||||
|
|
||||||
|
# Install nginx, certbot
|
||||||
|
RUN apt-get -qq update && \
|
||||||
|
# Install pre-dependencies to use apt-key.
|
||||||
|
apt-get -yqq install --no-install-recommends \
|
||||||
|
lsb-core \
|
||||||
|
gnupg \
|
||||||
|
&& \
|
||||||
|
# Add nginx ppa
|
||||||
|
sh -c 'echo "deb http://ppa.launchpad.net/nginx/stable/ubuntu $(lsb_release -sc) main" \
|
||||||
|
> /etc/apt/sources.list.d/nginx-stable.list' && \
|
||||||
|
# Add nginx key
|
||||||
|
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C && \
|
||||||
|
apt-get -qq update && \
|
||||||
|
apt-get -yqq install --no-install-recommends \
|
||||||
|
nginx \
|
||||||
|
python-certbot-nginx \
|
||||||
|
&& \
|
||||||
|
apt-get remove -yqq \
|
||||||
|
lsb-core \
|
||||||
|
cups-common \
|
||||||
|
&& \
|
||||||
|
apt-get autoremove -yqq && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/lib/dpkg/status.old /var/cache/debconf/templates.dat /var/log/dpkg.log /var/log/lastlog /var/log/apt/*.log
|
||||||
|
|
||||||
|
VOLUME /etc/letsencrypt
|
||||||
|
|
||||||
|
COPY nginx.runit /etc/service/nginx/run
|
||||||
|
COPY logs.runit /etc/service/nginx-logs/run
|
||||||
|
COPY bouncer.runit /etc/service/bouncer/run
|
||||||
|
COPY logs-nginx-access.runit /etc/service/logs-nginx-access/run
|
||||||
|
COPY logs-nginx-error.runit /etc/service/logs-nginx-error/run
|
||||||
|
RUN chmod +x /etc/service/*/run
|
||||||
|
COPY NginxDefault /etc/nginx/sites-enabled/default
|
||||||
|
COPY NginxSSL /etc/nginx/sites-enabled/default-ssl
|
||||||
|
COPY NginxTemplate.twig /app/
|
||||||
|
# Disable daemonising in nginx
|
||||||
|
RUN sed -i '1s;^;daemon off\;\n;' /etc/nginx/nginx.conf
|
||||||
|
COPY bouncer /app
|
||||||
|
COPY composer.* /app/
|
||||||
|
RUN composer install && \
|
||||||
|
chmod +x /app/bouncer && \
|
||||||
|
mkdir -p /var/log/bouncer
|
||||||
25
bouncer/NginxDefault
Normal file
25
bouncer/NginxDefault
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
client_max_body_size 1024M;
|
||||||
|
|
||||||
|
root /app/public;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
index index.html index.htm;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# First attempt to serve request as file, then
|
||||||
|
# as directory, then fall back to displaying a 404.
|
||||||
|
try_files $uri $uri/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# deny access to .htaccess files, if Apache's document root
|
||||||
|
# concurs with nginx's one
|
||||||
|
#
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
bouncer/NginxSSL
Normal file
30
bouncer/NginxSSL
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
client_max_body_size 1024M;
|
||||||
|
|
||||||
|
root /app/public;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
index index.html index.htm;
|
||||||
|
|
||||||
|
ssl_certificate /certs/example.crt;
|
||||||
|
ssl_certificate_key /certs/example.key;
|
||||||
|
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||||
|
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# First attempt to serve request as file, then
|
||||||
|
# as directory, then fall back to displaying a 404.
|
||||||
|
try_files $uri $uri/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# deny access to .htaccess files, if Apache's document root
|
||||||
|
# concurs with nginx's one
|
||||||
|
#
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
bouncer/NginxTemplate.twig
Normal file
33
bouncer/NginxTemplate.twig
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
server {
|
||||||
|
{% if allowNonSSL %}
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
{% endif %}
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name {{ domains|join(' ') }};
|
||||||
|
access_log /var/log/bouncer/{{ name }}.access.log;
|
||||||
|
error_log /var/log/bouncer/{{ name }}.error.log;
|
||||||
|
|
||||||
|
{% if useTemporaryCert %}
|
||||||
|
ssl_certificate /certs/example.crt;
|
||||||
|
ssl_certificate_key /certs/example.key;
|
||||||
|
{% else %}
|
||||||
|
ssl_certificate /etc/letsencrypt/live/{{ name }}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/{{ name }}/privkey.pem;
|
||||||
|
{% endif %}
|
||||||
|
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||||
|
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass {{ targetPath }};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% if not allowNonSSL %}
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name {{ domains|join(' ') }};
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
488
bouncer/bouncer
Executable file
488
bouncer/bouncer
Executable file
|
|
@ -0,0 +1,488 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
|
use AdamBrett\ShellWrapper\Command\Builder as CommandBuilder;
|
||||||
|
use AdamBrett\ShellWrapper\Runners\Exec;
|
||||||
|
use Aws\S3\S3Client;
|
||||||
|
use Bramus\Monolog\Formatter\ColoredLineFormatter;
|
||||||
|
use GuzzleHttp\Client as Guzzle;
|
||||||
|
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
|
||||||
|
use League\Flysystem\FileAttributes;
|
||||||
|
use League\Flysystem\Filesystem;
|
||||||
|
use League\Flysystem\Local\LocalFilesystemAdapter;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Spatie\Emoji\Emoji;
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
|
class BouncerTarget
|
||||||
|
{
|
||||||
|
private string $id;
|
||||||
|
private array $domains;
|
||||||
|
private string $ip;
|
||||||
|
private int $port = 80;
|
||||||
|
private bool $letsEncrypt = false;
|
||||||
|
private string $targetPath;
|
||||||
|
private bool $allowNonSSL = false;
|
||||||
|
|
||||||
|
private bool $useTemporaryCert = true;
|
||||||
|
|
||||||
|
public function __toArray()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->getId(),
|
||||||
|
'name' => $this->getName(),
|
||||||
|
'domains' => $this->getDomains(),
|
||||||
|
'letsEncrypt' => $this->isLetsEncrypt(),
|
||||||
|
'targetPath' => $this->getTargetPath(),
|
||||||
|
'useTemporaryCert' => $this->isUseTemporaryCert(),
|
||||||
|
'allowNonSSL' => $this->isAllowNonSSL(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isUseTemporaryCert(): bool
|
||||||
|
{
|
||||||
|
return $this->useTemporaryCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUseTemporaryCert(bool $useTemporaryCert): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->useTemporaryCert = $useTemporaryCert;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setId(string $id): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDomains(): array
|
||||||
|
{
|
||||||
|
return $this->domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $domains
|
||||||
|
*/
|
||||||
|
public function setDomains(array $domains): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->domains = $domains;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLetsEncrypt(): bool
|
||||||
|
{
|
||||||
|
return $this->letsEncrypt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLetsEncrypt(bool $letsEncrypt): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->letsEncrypt = $letsEncrypt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTargetPath(): string
|
||||||
|
{
|
||||||
|
return $this->targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTargetPath(string $targetPath): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->targetPath = $targetPath;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIp(): string
|
||||||
|
{
|
||||||
|
return $this->ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIp(string $ip): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->ip = $ip;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPort(): int
|
||||||
|
{
|
||||||
|
return $this->port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPort(int $port): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->port = $port;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return reset($this->domains);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAllowNonSSL(): bool
|
||||||
|
{
|
||||||
|
return $this->allowNonSSL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAllowNonSSL(bool $allowNonSSL): BouncerTarget
|
||||||
|
{
|
||||||
|
$this->allowNonSSL = $allowNonSSL;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bouncer
|
||||||
|
{
|
||||||
|
private array $environment;
|
||||||
|
private Guzzle $client;
|
||||||
|
private FilesystemLoader $loader;
|
||||||
|
private Environment $twig;
|
||||||
|
private Filesystem $configFilesystem;
|
||||||
|
private Filesystem $certificateStoreLocal;
|
||||||
|
private ?Filesystem $certificateStoreRemote;
|
||||||
|
private Logger $logger;
|
||||||
|
private string $instanceStateHash = '';
|
||||||
|
private array $fileHashes;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->environment = array_merge($_ENV, $_SERVER);
|
||||||
|
ksort($this->environment);
|
||||||
|
|
||||||
|
$this->logger = new Monolog\Logger('bouncer');
|
||||||
|
$this->logger->pushHandler(new StreamHandler('/var/log/bouncer.log', Logger::DEBUG));
|
||||||
|
$stdout = new StreamHandler('php://stdout', Logger::DEBUG);
|
||||||
|
$stdout->setFormatter(new ColoredLineFormatter(null, "%level_name%: %message% \n"));
|
||||||
|
$this->logger->pushHandler($stdout);
|
||||||
|
|
||||||
|
$this->client = new Guzzle(
|
||||||
|
[
|
||||||
|
'base_uri' => 'http://localhost',
|
||||||
|
'curl' => [
|
||||||
|
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->loader = new FilesystemLoader([
|
||||||
|
__DIR__,
|
||||||
|
]);
|
||||||
|
$this->twig = new Environment($this->loader);
|
||||||
|
|
||||||
|
// Set up Filesystem for sites-enabled path
|
||||||
|
$this->configFilesystem = new Filesystem(new LocalFilesystemAdapter('/etc/nginx/sites-enabled'));
|
||||||
|
|
||||||
|
// Set up Local certificate store
|
||||||
|
$this->certificateStoreLocal = new Filesystem(new LocalFilesystemAdapter('/etc/letsencrypt'));
|
||||||
|
|
||||||
|
// Set up Remote certificate store, if configured
|
||||||
|
if ($this->environment['BOUNCER_S3_BUCKET']) {
|
||||||
|
$this->certificateStoreRemote = new Filesystem(
|
||||||
|
new AwsS3V3Adapter(
|
||||||
|
new S3Client([
|
||||||
|
'endpoint' => $this->environment['BOUNCER_S3_ENDPOINT'],
|
||||||
|
'use_path_style_endpoint' => isset($this->environment['BOUNCER_S3_USE_PATH_STYLE_ENDPOINT']),
|
||||||
|
'credentials' => [
|
||||||
|
'key' => $this->environment['BOUNCER_S3_KEY_ID'],
|
||||||
|
'secret' => $this->environment['BOUNCER_S3_KEY_SECRET'],
|
||||||
|
],
|
||||||
|
'region' => $this->environment['BOUNCER_S3_REGION'] ?? 'us-east',
|
||||||
|
'version' => 'latest',
|
||||||
|
]),
|
||||||
|
$this->environment['BOUNCER_S3_BUCKET'],
|
||||||
|
$this->environment['BOUNCER_S3_PREFIX'] ?? ''
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||||
|
*
|
||||||
|
* @return BouncerTarget[]
|
||||||
|
*/
|
||||||
|
public function findContainers(): array
|
||||||
|
{
|
||||||
|
$bouncerTargets = [];
|
||||||
|
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
|
||||||
|
foreach ($containers as $container) {
|
||||||
|
$envs = [];
|
||||||
|
$inspect = json_decode($this->client->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
|
||||||
|
if (isset($inspect['Config']['Env'])) {
|
||||||
|
foreach ($inspect['Config']['Env'] as $environmentItem) {
|
||||||
|
if (stripos($environmentItem, '=') !== false) {
|
||||||
|
[$envKey, $envVal] = explode('=', $environmentItem, 2);
|
||||||
|
$envs[$envKey] = $envVal;
|
||||||
|
} else {
|
||||||
|
$envs[$envKey] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($envs['BOUNCER_DOMAIN'])) {
|
||||||
|
$bouncerTarget = (new BouncerTarget())
|
||||||
|
->setId($inspect['Id'])
|
||||||
|
;
|
||||||
|
foreach ($envs as $eKey => $eVal) {
|
||||||
|
switch ($eKey) {
|
||||||
|
case 'BOUNCER_DOMAIN':
|
||||||
|
$domains = explode(',', $eVal);
|
||||||
|
array_walk($domains, function (&$domain, $key): void { $domain = trim($domain); });
|
||||||
|
$bouncerTarget->setDomains($domains);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BOUNCER_LETSENCRYPT':
|
||||||
|
$bouncerTarget->setLetsEncrypt(in_array(strtolower($eVal), ['yes', 'true'], true));
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BOUNCER_TARGET_PORT':
|
||||||
|
$bouncerTarget->setPort($eVal);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BOUNCER_ALLOW_NON_SSL':
|
||||||
|
$bouncerTarget->setAllowNonSSL(in_array(strtolower($eVal), ['yes', 'true'], true));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($inspect['NetworkSettings']['IPAddress']) && !empty($inspect['NetworkSettings']['IPAddress'])) {
|
||||||
|
// As per docker service
|
||||||
|
$bouncerTarget->setIp($inspect['NetworkSettings']['IPAddress']);
|
||||||
|
} else {
|
||||||
|
// As per docker compose
|
||||||
|
$networks = array_values($inspect['NetworkSettings']['Networks']);
|
||||||
|
$bouncerTarget->setIp($networks[0]['IPAddress']);
|
||||||
|
}
|
||||||
|
$this->logger->debug(sprintf('Decided that %s has the ip %s', $bouncerTarget->getName(), $bouncerTarget->getIp()));
|
||||||
|
|
||||||
|
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getIp(), $bouncerTarget->getPort()));
|
||||||
|
|
||||||
|
$bouncerTargets[] = $bouncerTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bouncerTargets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->logger->info(sprintf('%s Starting Bouncer...', Emoji::CHARACTER_TIMER_CLOCK));
|
||||||
|
$this->stateHasChanged();
|
||||||
|
while (true) {
|
||||||
|
$this->runLoop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when something has changed.
|
||||||
|
*
|
||||||
|
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||||
|
*/
|
||||||
|
private function stateHasChanged(): bool
|
||||||
|
{
|
||||||
|
$newInstanceStates = [];
|
||||||
|
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
|
||||||
|
foreach ($containers as $container) {
|
||||||
|
$inspect = json_decode($this->client->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
|
||||||
|
$newInstanceStates[$inspect['Id']] = implode('::', [
|
||||||
|
$inspect['Name'],
|
||||||
|
$inspect['Created'],
|
||||||
|
$inspect['Image'],
|
||||||
|
$inspect['State']['Status'],
|
||||||
|
sha1(implode('|', $inspect['Config']['Env'])),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$newStateHash = sha1(implode("\n", $newInstanceStates));
|
||||||
|
//$this->logger->debug(sprintf("Old state = %s. New State = %s.", substr($this->instanceStateHash,0,7), substr($newStateHash, 0,7)));
|
||||||
|
if ($this->instanceStateHash != $newStateHash) {
|
||||||
|
$this->instanceStateHash = $newStateHash;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runLoop(): void
|
||||||
|
{
|
||||||
|
if ($this->s3Enabled()) {
|
||||||
|
$this->getCertificatesFromS3();
|
||||||
|
}
|
||||||
|
$targets = $this->findContainers();
|
||||||
|
$this->logger->info(sprintf('%s Found %d services with BOUNCER_DOMAIN set', Emoji::CHARACTER_MAGNIFYING_GLASS_TILTED_LEFT, count($targets)));
|
||||||
|
foreach ($targets as $target) {
|
||||||
|
$this->generateNginxConfig($target);
|
||||||
|
}
|
||||||
|
$this->generateLetsEncryptCerts($targets);
|
||||||
|
if ($this->s3Enabled()) {
|
||||||
|
$this->writeCertificatesToS3();
|
||||||
|
}
|
||||||
|
$this->waitUntilContainerChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function waitUntilContainerChange(): void
|
||||||
|
{
|
||||||
|
while ($this->stateHasChanged() === false) {
|
||||||
|
sleep(5);
|
||||||
|
}
|
||||||
|
$this->logger->debug(sprintf('%s Host Container state has changed', Emoji::CHARACTER_WARNING));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function s3Enabled(): bool
|
||||||
|
{
|
||||||
|
return $this->certificateStoreRemote instanceof Filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCertificatesFromS3(): void
|
||||||
|
{
|
||||||
|
$this->logger->info(sprintf('%s Downloading Certificates from S3', Emoji::CHARACTER_DOWN_ARROW));
|
||||||
|
foreach ($this->certificateStoreRemote->listContents('/', true) as $file) {
|
||||||
|
/** @var FileAttributes $file */
|
||||||
|
if ($file->isFile()) {
|
||||||
|
$localPath = "archive/{$file->path()}";
|
||||||
|
#$this->logger->debug(sprintf(" > Downloading {$file->path()} "));
|
||||||
|
$this->certificateStoreLocal->writeStream($localPath, $this->certificateStoreRemote->readStream($file->path()));
|
||||||
|
$this->fileHashes[$localPath] = sha1($this->certificateStoreLocal->read($localPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy certs into /live because certbot is a pain.
|
||||||
|
foreach ($this->certificateStoreLocal->listContents('/archive', true) as $newLocalCert) {
|
||||||
|
/** @var FileAttributes $newLocalCert */
|
||||||
|
if ($newLocalCert->isFile() && pathinfo($newLocalCert->path(), PATHINFO_EXTENSION) == 'pem') {
|
||||||
|
$livePath = str_replace('archive/', 'live/', $newLocalCert->path());
|
||||||
|
$this->certificateStoreLocal->writeStream($livePath, $this->certificateStoreLocal->readStream($newLocalCert->path()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fileChanged(string $localPath)
|
||||||
|
{
|
||||||
|
if (!isset($this->fileHashes[$localPath])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sha1($this->certificateStoreLocal->read($localPath)) != $this->fileHashes[$localPath]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeCertificatesToS3(): void
|
||||||
|
{
|
||||||
|
$this->logger->info(sprintf('%s Uploading Certificates to S3', Emoji::CHARACTER_UP_ARROW));
|
||||||
|
foreach ($this->certificateStoreLocal->listContents('/archive', true) as $file) {
|
||||||
|
/** @var FileAttributes $file */
|
||||||
|
if ($file->isFile()) {
|
||||||
|
$remotePath = str_replace('archive/', '', $file->path());
|
||||||
|
if (!$this->certificateStoreRemote->fileExists($remotePath) || $this->fileChanged($file->path())) {
|
||||||
|
#$this->logger->debug(sprintf(" > Uploading {$file->path()} "));
|
||||||
|
$this->certificateStoreRemote->writeStream($remotePath, $this->certificateStoreLocal->readStream($file->path()));
|
||||||
|
} else {
|
||||||
|
#$this->logger->debug(sprintf(" > Skipping uploading {$file->path()}, file not changed."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateNginxConfig(BouncerTarget $target): self
|
||||||
|
{
|
||||||
|
$this->configFilesystem->write(
|
||||||
|
$target->getName(),
|
||||||
|
$this->twig->render('NginxTemplate.twig', $target->__toArray())
|
||||||
|
);
|
||||||
|
$this->logger->info(sprintf('%s Created config for %s', Emoji::CHARACTER_PENCIL, $target->getName()));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param BouncerTarget[] $targets
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
private function generateLetsEncryptCerts(array $targets): self
|
||||||
|
{
|
||||||
|
foreach ($targets as $target) {
|
||||||
|
if (!$target->isLetsEncrypt()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$testAgeFile = "/archive/{$target->getName()}/fullchain1.pem";
|
||||||
|
if ($this->certificateStoreLocal->fileExists($testAgeFile)) {
|
||||||
|
$ssl = openssl_x509_parse($this->certificateStoreLocal->read($testAgeFile));
|
||||||
|
$timeRemainingSeconds = $ssl['validTo_time_t'] - time();
|
||||||
|
if ($timeRemainingSeconds > 2592000) {
|
||||||
|
$this->logger->info(sprintf(
|
||||||
|
'%s Skipping %s, certificate is still good for %d days',
|
||||||
|
Emoji::CHARACTER_PARTYING_FACE,
|
||||||
|
$target->getName(),
|
||||||
|
round($timeRemainingSeconds / 86400)
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$shell = new Exec();
|
||||||
|
$command = new CommandBuilder('/usr/bin/certbot');
|
||||||
|
$command->addSubCommand('certonly');
|
||||||
|
$command->addArgument('nginx');
|
||||||
|
if ($this->environment['BOUNCER_LETSENCRYPT_MODE'] != 'production') {
|
||||||
|
$command->addArgument('test-cert');
|
||||||
|
}
|
||||||
|
$command->addFlag('d', implode(',', $target->getDomains()));
|
||||||
|
$command->addFlag('n');
|
||||||
|
$command->addFlag('m', $this->environment['BOUNCER_LETSENCRYPT_EMAIL']);
|
||||||
|
$command->addArgument('agree-tos');
|
||||||
|
$this->logger->info(sprintf('%s Generating letsencrypt for %s - %s', Emoji::CHARACTER_PENCIL, $target->getName(), $command->__toString()));
|
||||||
|
$shell->run($command);
|
||||||
|
|
||||||
|
if ($shell->getReturnValue() == 0) {
|
||||||
|
$this->logger->info(sprintf('%s Generating successful', Emoji::CHARACTER_PARTY_POPPER));
|
||||||
|
} else {
|
||||||
|
$this->logger->critical(sprintf('%s Generating failed!', Emoji::CHARACTER_WARNING));
|
||||||
|
}
|
||||||
|
|
||||||
|
$target->setUseTemporaryCert(false);
|
||||||
|
$this->generateNginxConfig($target);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->restartNginx();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function restartNginx(): void
|
||||||
|
{
|
||||||
|
$shell = new Exec();
|
||||||
|
$command = new CommandBuilder('/usr/sbin/nginx');
|
||||||
|
$command->addFlag('s', 'reload');
|
||||||
|
$this->logger->info(sprintf('%s Restarting nginx', Emoji::CHARACTER_TIMER_CLOCK));
|
||||||
|
$shell->run($command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(new Bouncer())->run();
|
||||||
3
bouncer/bouncer.runit
Executable file
3
bouncer/bouncer.runit
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "Starting Bouncer"
|
||||||
|
/app/bouncer
|
||||||
32
bouncer/composer.json
Normal file
32
bouncer/composer.json
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "benzine/bouncer",
|
||||||
|
"description": "Nginx Configuration Management",
|
||||||
|
"type": "project",
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.0",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"kint-php/kint": "^3.3",
|
||||||
|
"guzzlehttp/guzzle": "^7.3",
|
||||||
|
"twig/twig": "^3.0",
|
||||||
|
"league/flysystem": "^2.1",
|
||||||
|
"monolog/monolog": "^2.2",
|
||||||
|
"bramus/monolog-colored-line-formatter": "~3.0",
|
||||||
|
"adambrett/shell-wrapper": "dev-master",
|
||||||
|
"league/flysystem-aws-s3-v3": "^2.1",
|
||||||
|
"spatie/emoji": "^2.3"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matthew Baggett",
|
||||||
|
"email": "matthew@baggett.me"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3209
bouncer/composer.lock
generated
Normal file
3209
bouncer/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
37
bouncer/docker-compose.yml
Normal file
37
bouncer/docker-compose.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
version: "3.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
bouncer:
|
||||||
|
image: benzine/bouncer
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./:/app
|
||||||
|
environment:
|
||||||
|
- BOUNCER_LETSENCRYPT_MODE=staging
|
||||||
|
- BOUNCER_LETSENCRYPT_EMAIL=matthew@baggett.me
|
||||||
|
- BOUNCER_S3_ENDPOINT=http://grey.ooo:9000
|
||||||
|
- BOUNCER_S3_KEY_ID=geusebio
|
||||||
|
- BOUNCER_S3_KEY_SECRET=changeme
|
||||||
|
- BOUNCER_S3_BUCKET=bouncer-certificates
|
||||||
|
- BOUNCER_S3_USE_PATH_STYLE_ENDPOINT="yes"
|
||||||
|
ports:
|
||||||
|
- 127.0.99.100:80:80
|
||||||
|
- 127.0.99.100:443:443
|
||||||
|
|
||||||
|
web-a:
|
||||||
|
image: benzine/php:nginx
|
||||||
|
volumes:
|
||||||
|
- ./test/public-web-a:/app/public
|
||||||
|
environment:
|
||||||
|
- BOUNCER_DOMAIN=a.web.grey.ooo
|
||||||
|
- BOUNCER_LETSENCRYPT=true
|
||||||
|
|
||||||
|
# web-b:
|
||||||
|
# image: benzine/php:nginx
|
||||||
|
# volumes:
|
||||||
|
# - ./test/public-web-b:/app/public
|
||||||
|
# environment:
|
||||||
|
# - BOUNCER_DOMAIN=b.web.grey.ooo
|
||||||
|
#
|
||||||
|
#
|
||||||
43
bouncer/grey-ooo-test.yml
Normal file
43
bouncer/grey-ooo-test.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
version: "3.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
bouncer:
|
||||||
|
image: benzine/bouncer
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- BOUNCER_LETSENCRYPT_MODE=production
|
||||||
|
- BOUNCER_LETSENCRYPT_EMAIL=matthew@baggett.me
|
||||||
|
- BOUNCER_S3_ENDPOINT=http://grey.ooo:9000
|
||||||
|
- BOUNCER_S3_KEY_ID=geusebio
|
||||||
|
- BOUNCER_S3_KEY_SECRET=teblE0neTf2NQcVFaZIRkSF44RscyQ3G
|
||||||
|
- BOUNCER_S3_BUCKET=bouncer-certificates
|
||||||
|
- BOUNCER_S3_USE_PATH_STYLE_ENDPOINT="yes"
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
|
||||||
|
web-a:
|
||||||
|
image: benzine/php:nginx
|
||||||
|
volumes:
|
||||||
|
- ./test/public-web-a:/app/public
|
||||||
|
environment:
|
||||||
|
- BOUNCER_DOMAIN=a.web.grey.ooo
|
||||||
|
- BOUNCER_LETSENCRYPT=true
|
||||||
|
|
||||||
|
web-b:
|
||||||
|
image: benzine/php:nginx
|
||||||
|
volumes:
|
||||||
|
- ./test/public-web-b:/app/public
|
||||||
|
environment:
|
||||||
|
- BOUNCER_DOMAIN=b.web.grey.ooo
|
||||||
|
- BOUNCER_LETSENCRYPT=true
|
||||||
|
|
||||||
|
web-c:
|
||||||
|
image: benzine/php:nginx
|
||||||
|
volumes:
|
||||||
|
- ./test/public-web-c:/app/public
|
||||||
|
environment:
|
||||||
|
- BOUNCER_DOMAIN=c.web.grey.ooo
|
||||||
|
- BOUNCER_LETSENCRYPT=true
|
||||||
|
|
||||||
2
bouncer/logs-nginx-access.runit
Executable file
2
bouncer/logs-nginx-access.runit
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
tail -f /var/log/nginx/access.log | sed --unbuffered 's|.*\[.*\] |[NGINX] |g' | grep -v /v1/ping
|
||||||
2
bouncer/logs-nginx-error.runit
Executable file
2
bouncer/logs-nginx-error.runit
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
tail -f /var/log/nginx/error.log
|
||||||
6
bouncer/logs.runit
Normal file
6
bouncer/logs.runit
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [[ -f /var/log/bouncer/*.log ]]; then
|
||||||
|
tail -f /var/log/bouncer/*.log
|
||||||
|
else
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
3
bouncer/nginx.runit
Executable file
3
bouncer/nginx.runit
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "Starting Nginx"
|
||||||
|
/usr/sbin/nginx
|
||||||
1
bouncer/public/index.html
Normal file
1
bouncer/public/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Nothing here.
|
||||||
22
bouncer/self-signed-certificates/example.crt
Normal file
22
bouncer/self-signed-certificates/example.crt
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDnTCCAoWgAwIBAgIUJYUUmBQ2/ERZ7xinAJzVhiFWViYwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwXTELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0Zsb3JpZGExDjAMBgNVBAcMBU1p
|
||||||
|
YW1pMRYwFAYDVQQKDA1FeGFtcGxlIEdyb3VwMRQwEgYDVQQDDAtleGFtcGxlLm9y
|
||||||
|
ZzAgFw0yMTA1MzAxNzU4MzlaGA8yMTIxMDUwNjE3NTgzOVowXTELMAkGA1UEBhMC
|
||||||
|
VVMxEDAOBgNVBAgMB0Zsb3JpZGExDjAMBgNVBAcMBU1pYW1pMRYwFAYDVQQKDA1F
|
||||||
|
eGFtcGxlIEdyb3VwMRQwEgYDVQQDDAtleGFtcGxlLm9yZzCCASIwDQYJKoZIhvcN
|
||||||
|
AQEBBQADggEPADCCAQoCggEBANJa9OcoCW+mej8qDMCCTGnqMAuUqBIj1wZLgOdT
|
||||||
|
4DHriq1vKi1JLsDZkYekrCq/sfWo97kDXsdK6YN4+mua5EN4cTG3mSpal+RgLTc2
|
||||||
|
HMKHFfgzPzIN/n5AEqzdVZb5j0P3LoUNH687AlplW0BB+K64Gw//2KPx0Q8Fkhq2
|
||||||
|
I97V8SRpqds78PJHzhfuZNs/AUFpFXnYHJyO2Q63Btq2aoTMQyoLDRBBxin70II2
|
||||||
|
6Cjh3k6EhMY+HuYS1AjfI8cDQw289asJBLa6zPoD0VGaGNfCSrOzxrUqfhIoOkuY
|
||||||
|
W7rOIsK6rSSu1neSKQIiOLVjQxifxrQIIKTQhRiSplgD9LUCAwEAAaNTMFEwHQYD
|
||||||
|
VR0OBBYEFADK74w4AGeETK72k/htsnol9ye0MB8GA1UdIwQYMBaAFADK74w4AGeE
|
||||||
|
TK72k/htsnol9ye0MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
|
||||||
|
AKElv0xx95lD2leXEOfD6DKakrzuE8lONmcrkfjehTOd7jbqblnj8u1DCWytwB8P
|
||||||
|
gEr5FXve0iy7avGoNkU33MufbbQokAMoTs/IA+rwMfv0unupT1aYN8TTEXJJ100j
|
||||||
|
MXBsq/PvNkBNwkBcXjYHHsVjdM3bptbaw9A4V9opfMjQXAY5wuk3rBBm8On2rJKy
|
||||||
|
Qksh/uLoe8wbZ5dvLv9oc9sRpIilaSy8TcbrHkDIaWA5WCdVFfcayDGYdjhCYLGW
|
||||||
|
tj/48g0THvJv6JvVYwFJqTM690YUSlxaOHQE2ZneLytocVyAdEL2MMldRezvtI1z
|
||||||
|
1OXOia2G7koNYtS7cD8G1IM=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
bouncer/self-signed-certificates/example.key
Normal file
28
bouncer/self-signed-certificates/example.key
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDSWvTnKAlvpno/
|
||||||
|
KgzAgkxp6jALlKgSI9cGS4DnU+Ax64qtbyotSS7A2ZGHpKwqv7H1qPe5A17HSumD
|
||||||
|
ePprmuRDeHExt5kqWpfkYC03NhzChxX4Mz8yDf5+QBKs3VWW+Y9D9y6FDR+vOwJa
|
||||||
|
ZVtAQfiuuBsP/9ij8dEPBZIatiPe1fEkaanbO/DyR84X7mTbPwFBaRV52BycjtkO
|
||||||
|
twbatmqEzEMqCw0QQcYp+9CCNugo4d5OhITGPh7mEtQI3yPHA0MNvPWrCQS2usz6
|
||||||
|
A9FRmhjXwkqzs8a1Kn4SKDpLmFu6ziLCuq0krtZ3kikCIji1Y0MYn8a0CCCk0IUY
|
||||||
|
kqZYA/S1AgMBAAECggEARqfQjPgwuzTi6OZ55AugGQ9VVf53uagaKH4h7RGKQ5pH
|
||||||
|
OVwWgaGMN7CcpkAUqEM9RjOcCaPtKOmrp8Jx8sTTGSqScs2lf8lwLYB0j4/4dwqi
|
||||||
|
wXyNJIX4znU9EJ1Di3OFwKF9Gam/077xWmWjEeFW43DpfiVEokSuIOqRGbHGOKlt
|
||||||
|
2ygHJu+rmPapEPyYqSWQnAkYX0DW/KCAGiyIAqph/SgrCDTdsxbNOa2OwDygPC54
|
||||||
|
7xW0yCduvgFLh9bxedF8iifzRkPw710cxyqVsYwHiwugDgxL4NiK1DlWbpBimab5
|
||||||
|
ocye9+ElymMZ8DTjpA85cXny/TtoqJfqTs1YGYgrvQKBgQDwHnAcY0BjQ4o+ZneG
|
||||||
|
oqBJeQ8KCMRU4pEIa5QOOeUr46gtiPIfcFh/BJUHQ61qk7gcJj5BV2GXNS7+m+sU
|
||||||
|
RC3Usblm9twwxZn7mfoOk4z9NEfBI2MXmbB8ARjAQBCost+3KQAoSIL1AyDKiAlY
|
||||||
|
2JfMt+73+kwUsg7b9g0pYIfn/wKBgQDgRJPlSIxJs2mbjzUwVBAeslct2W0dehrh
|
||||||
|
V0sXPxEhJHWX6P343vLqRHRsKgqhbU/vy+3JrIS9ftwGKcmb+Y9EJgYrR+D3ZYzs
|
||||||
|
idSOsunSspJgbCG5mHE1VQhr8IpHeCkuSt22aFErLfsjzXWZIewK2tqZN1QUjdc5
|
||||||
|
EJHOD4UDSwKBgFYRYvgZ72NlOzFAw0kkE7YiSWy8Vbtjdr8A6JHs2KNRt9+Sfc8d
|
||||||
|
Eut8dfqjnI5eIpkccCY1rwpnCtBCjRG3moHprl4k0Co/OgGAYKxG4TuFOM8W4xb7
|
||||||
|
hNH+BqQqko4Vh7D8Zk0KKL6v/1n5RvhssoSzzVlfg1PLux3G5VLWggB7AoGAAP/N
|
||||||
|
OORN27Y07kCBGCoHuFtLECU72znEDOT6rKvXQ7KJ45diKk2z/182tZSqX3XBOWxL
|
||||||
|
Lu7Z2I5MJKri/xLplIAm3uJ/GhsVuagTjl81s36gMFXLAKyxNG+gjfqQYykh5dbn
|
||||||
|
jfyBABRAXjR4JaqFBrda6fvZIA5RuytbuvNOwGkCgYAUs82tDGLiqyMPd2jgYS3k
|
||||||
|
aL62f0TLKHjmTCmRca7IqXbqcMbAj+LgAHI2HfCfjc4KWd68ZGRLcpDlehMcis1f
|
||||||
|
PQi3HW+2b9dAZX6+HAIGiVem//ckYXgUza4MMosh0hXquGs1yJ/VNWC+HPIHrj6X
|
||||||
|
9tvvvHnGKav329q/Z/8K/A==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
1
bouncer/test/public-web-a/index.html
Normal file
1
bouncer/test/public-web-a/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>Website A</h1>
|
||||||
1
bouncer/test/public-web-b/index.html
Normal file
1
bouncer/test/public-web-b/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>Website B</h1>
|
||||||
1
bouncer/test/public-web-c/index.html
Normal file
1
bouncer/test/public-web-c/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>Website C</h1>
|
||||||
|
|
@ -11,7 +11,6 @@ ENV DEBIAN_FRONTEND="teletype" \
|
||||||
COLOUR_NONE='\e[39m' \
|
COLOUR_NONE='\e[39m' \
|
||||||
DEFAULT_TZ='Europe/London'
|
DEFAULT_TZ='Europe/London'
|
||||||
|
|
||||||
CMD ["runsvdir", "-P", "/etc/service"]
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV PATH="/app:/app/bin:/app/vendor/bin:${PATH}"
|
ENV PATH="/app:/app/bin:/app/vendor/bin:${PATH}"
|
||||||
|
|
@ -20,5 +19,8 @@ COPY installers /installers
|
||||||
COPY etc /etc
|
COPY etc /etc
|
||||||
COPY usr /usr
|
COPY usr /usr
|
||||||
|
|
||||||
|
CMD ["/usr/bin/marshall"]
|
||||||
|
|
||||||
RUN /installers/install && \
|
RUN /installers/install && \
|
||||||
rm -rf /marshall /installers
|
rm -rf /marshall /installers && \
|
||||||
|
chmod +x /usr/bin/marshall
|
||||||
|
|
|
||||||
7
marshall/usr/bin/marshall
Executable file
7
marshall/usr/bin/marshall
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Fix for windows hosts manging run files
|
||||||
|
dos2unix -q /etc/service/*/run
|
||||||
|
|
||||||
|
# Start Runit.
|
||||||
|
runsvdir -P /etc/service
|
||||||
Loading…
Reference in a new issue