Improve release pipeline, add phpstan, general refactoring.

This commit is contained in:
Greyscale 2024-05-18 16:15:55 +02:00
parent 40d816494b
commit 5226b33620
25 changed files with 4419 additions and 447 deletions

View file

@ -1,107 +0,0 @@
name: Build Bouncer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: "0 14 * * 2" # 2pm Patch Tuesday
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
name: Build Docker Swarm Loadbalancer
runs-on: ubuntu-latest
steps:
- name: "Setup: Checkout Source"
uses: actions/checkout@v4
- name: "Setup: Get Date"
id: date
run: |
{
echo "datetime=$(date +'%Y-%m-%d %H:%M:%S')"
echo "date=$(date +'%Y-%m-%d')"
echo "time=$(date +'%H:%M:%S')"
echo "container_build_datetime=$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ')"
} >> "$GITHUB_OUTPUT"
- name: "Setup: PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
- name: "Setup: Setup QEMU"
uses: docker/setup-qemu-action@v3
- name: "Setup: Expose GitHub Runtime"
uses: crazy-max/ghaction-github-runtime@v3
- name: "Setup: Setup Docker Buildx"
uses: docker/setup-buildx-action@v3
- name: "Setup: Login to Docker Hub"
uses: docker/login-action@v3
with:
username: matthewbaggett
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: "Setup: Login to GHCR"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: matthewbaggett
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Setup: Find Composer Cache Directory"
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: "Setup: Composer Cache"
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-bouncer-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-bouncer-composer-
- name: "Dependencies: Composer Install"
run: composer install --ignore-platform-reqs
- name: "Build: Build & Push Image"
uses: docker/build-push-action@v5
with:
context: .
target: bouncer
platforms: ${{ !env.ACT && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
pull: true
push: true
build-args: |
GIT_SHA=${{ github.sha }}
GIT_BUILD_ID=${{ github.ref_name }}
BUILD_DATE=${{ steps.date.outputs.container_build_datetime }}
GIT_COMMIT_MESSAGE=${{ github.event.head_commit.message }}
tags: |
benzine/bouncer:latest
ghcr.io/benzine-framework/bouncer:latest
cache-from: ${{ !env.ACT && 'type=gha' || '' }}
cache-to: ${{ !env.ACT && 'type=gha,mode=max' || '' }}
build-contexts: |
php:cli=docker-image://ghcr.io/benzine-framework/php:cli-8.2
- name: "Post-Build: Validate build"
shell: bash
run: |
docker \
run \
--rm \
ghcr.io/benzine-framework/bouncer:latest \
/usr/bin/install-report

36
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Build Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: "0 14 * * 2" # 2pm Patch Tuesday
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build-container:
uses: ./.github/workflows/docker.build.yml
secrets: inherit
check-php:
uses: ./.github/workflows/php.check.yml
secrets: inherit
check-trunk:
uses: ./.github/workflows/trunk.check.yml
secrets: inherit
release-container:
needs:
- build-container
- check-php
- check-trunk
uses: ./.github/workflows/docker.release.yml
secrets: inherit

72
.github/workflows/docker.build.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: Build Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
PLATFORMS: linux/amd64,linux/arm64
CANDIDATE_IMAGE: ghcr.io/benzine-framework/bouncer:build-${{ github.sha }}
jobs:
build:
name: Build Swarm Loadbalancer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: date
run: |
{
echo "datetime=$(date +'%Y-%m-%d %H:%M:%S')"
echo "date=$(date +'%Y-%m-%d')"
echo "time=$(date +'%H:%M:%S')"
echo "container_build_datetime=$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ')"
} >> "$GITHUB_OUTPUT"
- id: read-php-version
run: echo "php_version=$(jq -r '.require["php"]' composer.json | sed -E 's/[^0-9.]//g')" >> $GITHUB_OUTPUT
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.read-php-version.outputs.php_version }}
- uses: docker/setup-qemu-action@v3
- uses: crazy-max/ghaction-github-runtime@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: matthewbaggett
password: ${{ secrets.GITHUB_TOKEN }}
- id: composer-cache-find
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- id: composer-cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "${{ runner.os }}-bouncer-composer-${{ hashFiles('**/composer.lock') }}"
restore-keys: ${{ runner.os }}-bouncer-composer-
- run: composer install --ignore-platform-reqs
- name: "Build & Push Candidate Image as ${{ env.CANDIDATE_IMAGE }}"
uses: docker/build-push-action@v5
with:
context: .
target: bouncer
build-contexts: |
php:cli=docker-image://ghcr.io/benzine-framework/php:cli-${{ steps.read-php-version.outputs.php_version }}
build-args: |
GIT_SHA=${{ github.sha }}
GIT_BUILD_ID=${{ github.ref_name }}
BUILD_DATE=${{ steps.date.outputs.container_build_datetime }}
GIT_COMMIT_MESSAGE=${{ github.event.head_commit.message }}
platforms: ${{ !env.ACT && env.PLATFORMS || 'linux/amd64' }}
pull: true
push: true
tags: ${{ env.CANDIDATE_IMAGE }}
cache-from: ${{ !env.ACT && 'type=gha' || '' }}
cache-to: ${{ !env.ACT && 'type=gha,mode=max' || '' }}

42
.github/workflows/docker.release.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Release Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CANDIDATE_IMAGE: ghcr.io/benzine-framework/bouncer:build-${{ github.sha }}
RELEASE_IMAGE_GHCR: ghcr.io/benzine-framework/bouncer:latest
RELEASE_IMAGE_DOCKER: benzine/bouncer:latest
jobs:
release:
name: Release Swarm Loadbalancer
runs-on: ubuntu-latest
strategy:
matrix:
registry:
- ghcr
- docker
fail-fast: false
steps:
- name: "Pull Candidate Image"
run: docker pull ${{ env.CANDIDATE_IMAGE }}
- name: "Login to Docker Hub"
if: matrix.registry == 'docker'
run: docker login -u matthewbaggett -p ${{ secrets.DOCKER_HUB_TOKEN }}
- name: "Login to GHCR"
if: matrix.registry == 'ghcr'
run: docker login ghcr.io -u matthewbaggett -p ${{ secrets.GITHUB_TOKEN }}
- name: "Tag Candidate Image"
run: docker tag ${{ env.CANDIDATE_IMAGE }} ${{ matrix.registry == 'ghcr' && env.RELEASE_IMAGE_GHCR || env.RELEASE_IMAGE_DOCKER }}
- name: "Push Release Image"
run: docker push ${{ matrix.registry == 'ghcr' && env.RELEASE_IMAGE_GHCR || env.RELEASE_IMAGE_DOCKER }}

54
.github/workflows/docker.validate.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Validate Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
workflow_run:
workflows: ["Build Swarm Loadbalancer"]
types:
- completed
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CANDIDATE_IMAGE: ghcr.io/benzine-framework/bouncer:build-${{ github.sha }}
jobs:
validate-install-report:
name: Run Install Report
runs-on: ubuntu-latest
steps:
- name: "Post-Build: Validate build"
shell: bash
run: |
docker \
run \
--rm \
${{ env.CANDIDATE_IMAGE }} \
/usr/bin/install-report
validate-dive-report:
name: Run Dive
runs-on: ubuntu-latest
steps:
# Use Dive to inspect the image for junk
- name: "Post-Build: Dive"
uses: wagoodman/dive@v0.10.0
with:
args: ${{ env.CANDIDATE_IMAGE }}
validate-vulnerability-report:
name: Run Trivy
runs-on: ubuntu-latest
steps:
# Inspect the container for security vulnerabilities
- name: "Post-Build: Trivy"
uses: aquasecurity/trivy-action@v0.3.0
with:
image-ref: ${{ env.CANDIDATE_IMAGE }}
format: table
exit-code: 1

69
.github/workflows/php.check.yml vendored Normal file
View file

@ -0,0 +1,69 @@
name: "QC: PHP"
permissions: read-all
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: "0 11 * * 2" # 11am Patch Tuesday
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
php-stan:
name: PHPStan
runs-on: ubuntu-latest
permissions:
checks: write # For trunk to post annotations
contents: read # For repo checkout
steps:
- name: "Checkout"
uses: actions/checkout@v4
- name: "Read PHP version from composer.json"
id: read-php-version
run: echo "php_version=$(jq -r '.require["php"]' composer.json | sed -E 's/[^0-9.]//g')" >> $GITHUB_OUTPUT
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.read-php-version.outputs.php_version }}
tools: phpstan
- name: Run PHPStan
run: phpstan analyse src
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
permissions:
checks: write # For trunk to post annotations
contents: read # For repo checkout
steps:
- name: "Checkout"
uses: actions/checkout@v4
- name: "Read PHP version from composer.json"
id: read-php-version
run: echo "php_version=$(jq -r '.require["php"]' composer.json | sed -E 's/[^0-9.]//g')" >> $GITHUB_OUTPUT
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.read-php-version.outputs.php_version }}
tools: php-cs-fixer
- name: "Run PHP CS Fixer"
run: php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose
# If there are changed files, create a PR, assign it to whom created the push and fail the build
- name: "Create PR"
uses: peter-evans/create-pull-request@v3
with:
title: "Apply php-cs-fixer changes"
commit-message: "Apply php-cs-fixer changes"
branch: "php-cs-fixer-${{ github.sha }}"
token: ${{ secrets.GITHUB_TOKEN }}
assignees: ${{ github.actor }}
labels: "auto-apply"
body: |
This PR was automatically created to apply php-cs-fixer changes.
Please review the changes and merge if they are correct.

View file

@ -0,0 +1,8 @@
parameters:
level: 5
paths:
- src
- bin
- tests
scanFiles:
- bin/bouncer

View file

@ -1,4 +1,4 @@
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage. # checkov:skip=CKV_DOCKER_3 I don't have time for rootless
FROM php:cli as bouncer FROM php:cli as bouncer
LABEL maintainer="Matthew Baggett <matthew@baggett.me>" \ LABEL maintainer="Matthew Baggett <matthew@baggett.me>" \
@ -96,21 +96,8 @@ HEALTHCHECK --start-period=30s \
RUN ls -lah /app /app/bin RUN ls -lah /app /app/bin
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage. # checkov:skip=CKV_DOCKER_3 This is a test container.
FROM php:nginx as test-app-a FROM php:nginx as test-app
COPY test/public-web-a /app/public COPY tests/testsites /app/public
HEALTHCHECK --start-period=30s \ HEALTHCHECK --start-period=30s \
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1 CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
FROM php:nginx as test-app-b
COPY test/public-web-b /app/public
HEALTHCHECK --start-period=30s \
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
FROM php:nginx as test-app-c
COPY test/public-web-c /app/public
HEALTHCHECK --start-period=30s \
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1

View file

@ -1,5 +1,16 @@
# Automatic Swarm Nginx Load Balancer # Automatic Swarm Nginx Load Balancer
This is a non-production-ready automatic loadbalancer that works by sniffing the docker socket for changes to the running systems in a docker swarm.
It probably works in k8s too, but I don't use k8s personally.
![GitHub Action: Build Status](https://img.shields.io/github/actions/workflow/status/benzine-framework/docker-swarm-loadbalancer/docker.build.yml?logo=github&label=Build)
![GitHub Action: QC Status](https://img.shields.io/github/actions/workflow/status/benzine-framework/docker-swarm-loadbalancer/trunk.check.yml?logo=github&label=QC)
![Docker Pulls](https://img.shields.io/docker/pulls/benzine/bouncer?logo=docker&label=Docker%20Hub%20Pulls)
![Docker Image Size](https://img.shields.io/docker/image-size/benzine/bouncer?logo=docker&label=Container%20Size)
_\* this doesn't include the number of times it's been pulled from the github container registry, and theres no nice way to pull that from a REST API so there isn't a pretty badge for that._
## Environment variables ## Environment variables
This container has its own environment variables, AS WELL AS scanning for some environment variables associated with your services. This container has its own environment variables, AS WELL AS scanning for some environment variables associated with your services.

View file

@ -1,18 +1,21 @@
{ {
"name": "benzine/bouncer", "name": "benzine/bouncer",
"description": "Automated Docker-swarm aware Nginx configuration management", "description": "Automated Docker-swarm aware Nginx configuration management",
"type": "project",
"config": {
"sort-packages": true
},
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"type": "project",
"authors": [
{
"name": "Matthew Baggett",
"email": "matthew@baggett.me"
}
],
"require": { "require": {
"php": "^8.1", "php": "^8.2",
"ext-curl": "*", "ext-curl": "*",
"ext-json": "*", "ext-json": "*",
"ext-openssl": "*", "ext-openssl": "*",
"adambrett/shell-wrapper": "~1.0", "adambrett/shell-wrapper": "^1.0",
"bramus/monolog-colored-line-formatter": "~3.1", "bramus/monolog-colored-line-formatter": "^3.1",
"guzzlehttp/guzzle": "^7.8", "guzzlehttp/guzzle": "^7.8",
"kint-php/kint": "^3.3", "kint-php/kint": "^3.3",
"league/flysystem": "^2.5", "league/flysystem": "^2.5",
@ -21,27 +24,40 @@
"nesbot/carbon": "^2.72", "nesbot/carbon": "^2.72",
"phpspec/php-diff": "^1.1", "phpspec/php-diff": "^1.1",
"spatie/emoji": "^2.3", "spatie/emoji": "^2.3",
"symfony/polyfill-php83": "^1.28",
"symfony/yaml": "^6.4", "symfony/yaml": "^6.4",
"twig/twig": "^3.8" "twig/twig": "^3.8"
}, },
"authors": [
{
"name": "Matthew Baggett",
"email": "matthew@baggett.me"
}
],
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^3.46" "ergebnis/composer-normalize": "^2.42",
"friendsofphp/php-cs-fixer": "^3.46",
"php-static-analysis/rector-rule": "^0.2.2",
"phpstan/extension-installer": "^1.2.0",
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^11",
"rawr/phpunit-data-provider": "^3.3",
"rector/rector": "^1.0",
"rregeer/phpunit-coverage-check": "^0.3.1",
"squizlabs/php_codesniffer": "^3.7"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Bouncer\\": "src/" "Bouncer\\": "src/"
} }
}, },
"scripts": {
"fix": "php-cs-fixer fix"
},
"bin": [ "bin": [
"bin/bouncer" "bin/bouncer"
] ],
"config": {
"allow-plugins": {
"ergebnis/composer-normalize": true,
"phpstan/extension-installer": true
},
"sort-packages": true
},
"scripts": {
"fix": "php-cs-fixer fix",
"stan": "phpstan analyse",
"test": "phpunit"
}
} }

3852
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -27,12 +27,11 @@ services:
additional_contexts: additional_contexts:
- php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2 - php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2
volumes: volumes:
- ./test/public-web-a:/app/public - ./tests/testsites:/app/public
environment: environment:
- BOUNCER_DOMAIN=a.web.grey.ooo - BOUNCER_DOMAIN=a.web.grey.ooo
- BOUNCER_TARGET_PORT=80 - BOUNCER_TARGET_PORT=80
# - BOUNCER_LETSENCRYPT=true - SITE_NAME=A
web-b: web-b:
image: test-app-b image: test-app-b
build: build:
@ -40,8 +39,8 @@ services:
additional_contexts: additional_contexts:
- php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2 - php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2
volumes: volumes:
- ./test/public-web-b:/app/public - ./tests/testsites:/app/public
environment: environment:
- BOUNCER_DOMAIN=b.web.grey.ooo - BOUNCER_DOMAIN=b.web.grey.ooo
- BOUNCER_TARGET_PORT=80 - BOUNCER_TARGET_PORT=80
# - BOUNCER_LETSENCRYPT=true - SITE_NAME=B

1
phpstan.neon.dist Symbolic link
View file

@ -0,0 +1 @@
.trunk/configs/phpstan.neon.dist

62
rector.php Normal file
View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
use PhpStaticAnalysis\RectorRule\AnnotationsToAttributesRector;
use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\AnnotationWithValueToAttributeRector;
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector;
use Rector\PHPUnit\Rector\Class_\PreferPHPUnitSelfCallRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Removing\Rector\FuncCall\RemoveFuncCallRector;
use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Symfony\Set\SensiolabsSetList;
use Rector\Symfony\Set\SymfonySetList;
use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector;
$rectorConfig = RectorConfig::configure();
$rectorConfig->withParallel(30);
$rectorConfig->withPreparedSets(
deadCode: true,
codeQuality: true
);
$rectorConfig->withPaths([
__DIR__ . '/bin',
__DIR__ . '/src',
__DIR__ . '/tests',
]);
// uncomment to reach your current PHP version
$rectorConfig->withPhpSets();
$rectorConfig->withSets([
PHPUnitSetList::PHPUNIT_80,
PHPUnitSetList::PHPUNIT_90,
PHPUnitSetList::PHPUNIT_100,
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES,
// PhpStaticAnalysisSetList::ANNOTATIONS_TO_ATTRIBUTES,// Implied by PHPUNIT_100
DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES,
SensiolabsSetList::ANNOTATIONS_TO_ATTRIBUTES,
__DIR__ . '/vendor/fakerphp/faker/rector-migrate.php',
]);
$rectorConfig->withConfiguredRule(RemoveFuncCallRector::class, [
'var_dump',
]);
$rectorConfig->withRules([
AddVoidReturnTypeWhereNoReturnRector::class,
AnnotationToAttributeRector::class,
AnnotationWithValueToAttributeRector::class,
AnnotationsToAttributesRector::class,
]);
// Prefer self::assert* over $this->assert* in PHPUnit tests
$rectorConfig->withSkip([
PreferPHPUnitThisCallRector::class,
]);
$rectorConfig->withRules([
PreferPHPUnitSelfCallRector::class,
]);
return $rectorConfig;

View file

@ -7,6 +7,7 @@ namespace Bouncer;
use AdamBrett\ShellWrapper\Command\Builder as CommandBuilder; use AdamBrett\ShellWrapper\Command\Builder as CommandBuilder;
use AdamBrett\ShellWrapper\Runners\Exec; use AdamBrett\ShellWrapper\Runners\Exec;
use Aws\S3\S3Client; use Aws\S3\S3Client;
use Bouncer\Logger\AbstractLogger;
use GuzzleHttp\Client as Guzzle; use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException; use GuzzleHttp\Exception\ServerException;
@ -15,7 +16,7 @@ use League\Flysystem\FileAttributes;
use League\Flysystem\Filesystem; use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemException;
use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\Local\LocalFilesystemAdapter;
use Monolog\Logger; use Bouncer\Logger\Logger;
use Bouncer\Logger\Formatter; use Bouncer\Logger\Formatter;
use Spatie\Emoji\Emoji; use Spatie\Emoji\Emoji;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
@ -38,7 +39,7 @@ class Bouncer
private Filesystem $certificateStoreLocal; private Filesystem $certificateStoreLocal;
private ?Filesystem $certificateStoreRemote = null; private ?Filesystem $certificateStoreRemote = null;
private Filesystem $providedCertificateStore; private Filesystem $providedCertificateStore;
private Logger $logger; private AbstractLogger $logger;
private array $previousContainerState = []; private array $previousContainerState = [];
private array $previousSwarmState = []; private array $previousSwarmState = [];
private array $fileHashes; private array $fileHashes;
@ -61,7 +62,7 @@ class Bouncer
$this->settings = new Settings(); $this->settings = new Settings();
$this->logger = new \Bouncer\Logger\Logger( $this->logger = new Logger(
settings: $this->settings, settings: $this->settings,
processIdProcessor: new Processor\ProcessIdProcessor(), processIdProcessor: new Processor\ProcessIdProcessor(),
memoryPeakUsageProcessor: new Processor\MemoryPeakUsageProcessor(), memoryPeakUsageProcessor: new Processor\MemoryPeakUsageProcessor(),
@ -306,6 +307,7 @@ class Bouncer
$bouncerTarget->setUseGlobalCert($this->isUseGlobalCert()); $bouncerTarget->setUseGlobalCert($this->isUseGlobalCert());
// @phpstan-ignore-next-line MB: I'm not sure you're right about ->hasCustomNginxConfig only returning false, Stan..
if ($bouncerTarget->isEndpointValid() || $bouncerTarget->hasCustomNginxConfig()) { if ($bouncerTarget->isEndpointValid() || $bouncerTarget->hasCustomNginxConfig()) {
$bouncerTargets[] = $bouncerTarget; $bouncerTargets[] = $bouncerTarget;
} else { } else {
@ -364,6 +366,7 @@ class Bouncer
exit(1); exit(1);
} }
// @phpstan-ignore-next-line Yes, I know this is a loop, that is desired.
while (true) { while (true) {
$this->runLoop(); $this->runLoop();
} }
@ -471,24 +474,6 @@ class Bouncer
return json_decode($this->docker->request('GET', "containers/{$id}/json")->getBody()->getContents(), true); return json_decode($this->docker->request('GET', "containers/{$id}/json")->getBody()->getContents(), true);
} }
private function dockerEnvHas(string $key, ?array $envs): bool
{
if ($envs === null) {
return false;
}
foreach ($envs as $env) {
if (stripos($env, '=') !== false) {
[$envKey, $envVal] = explode('=', $env, 2);
if ($envKey === $key) {
return true;
}
}
}
return false;
}
private function dockerEnvFilter(?array $envs): array private function dockerEnvFilter(?array $envs): array
{ {
if ($envs === null) { if ($envs === null) {

View file

@ -0,0 +1,417 @@
<?php
declare(strict_types=1);
namespace Bouncer\Logger;
use PhpStaticAnalysis\Attributes\Type;
use PhpStaticAnalysis\Attributes\Param;
use PhpStaticAnalysis\Attributes\Returns;
use Monolog\DateTimeImmutable;
use Monolog\Handler\HandlerInterface;
use Monolog\Level;
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;
use Monolog\ResettableInterface;
use Psr\Log\LoggerInterface;
abstract class AbstractLogger implements LoggerInterface, ResettableInterface
{
protected array $handlers;
protected bool $microsecondTimestamps = true;
protected \DateTimeZone $timezone;
protected ?\Closure $exceptionHandler = null;
/**
* Keeps track of depth to prevent infinite logging loops.
*/
private int $logDepth = 0;
#[Type('\WeakMap<\Fiber<mixed, mixed, mixed, mixed>, int>')] // Keeps track of depth inside fibers to prevent infinite logging loops
private \WeakMap $fiberLogDepth;
/**
* Whether to detect infinite logging loops
* This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this.
*/
private bool $detectCycles = true;
#[Param(name: 'string')] // The logging channel, a simple descriptive name that is attached to all log records
#[Param(handlers: 'HandlerInterface[]')] // optional stack of handlers, the first one in the array is called first, etc
#[Param(processors: 'callable[]')] // Optional array of processors
#[Param(timezone: 'null|\DateTimeZone')] // Optional timezone, if not provided date_default_timezone_get() will be used
#[Param(processors: 'array<(callable(LogRecord):LogRecord | ProcessorInterface)>')]
public function __construct(protected string $name, array $handlers = [], #[Type('array<(callable(LogRecord):LogRecord | ProcessorInterface)>')]
protected array $processors = [], ?\DateTimeZone $timezone = null)
{
$this->setHandlers($handlers);
$this->timezone = $timezone ?? new \DateTimeZone(date_default_timezone_get());
$this->fiberLogDepth = new \WeakMap();
}
public function getName(): string
{
return $this->name;
}
/**
* Return a new cloned instance with the name changed.
*/
#[Returns('static')]
public function withName(string $name): self
{
$new = clone $this;
$new->name = $name;
return $new;
}
public function pushHandler(HandlerInterface $handler): self
{
array_unshift($this->handlers, $handler);
return $this;
}
public function setHandlers(array $handlers): self
{
$this->handlers = [];
foreach (array_reverse($handlers) as $handler) {
$this->pushHandler($handler);
}
return $this;
}
#[Param(callback: 'ProcessorInterface|callable(LogRecord):LogRecord')]
#[Returns('$this')]
public function pushProcessor(callable | ProcessorInterface $callback): self
{
array_unshift($this->processors, $callback);
return $this;
}
#[Returns('$this')]
public function useLoggingLoopDetection(bool $detectCycles): self
{
$this->detectCycles = $detectCycles;
return $this;
}
/**
* Adds a log record.
*/
#[Param(level: 'Level')] // The logging level (a Monolog or RFC 5424 level)
#[Param(message: 'string')] // The log message
#[Param(context: 'mixed[]')] // The log context
#[Param(datetime: 'null|DateTimeImmutable')] // Optional log date to log into the past or future
#[Returns('bool')] // Whether the record has been processed
#[Param(level: 'value-of<Level::VALUES>|Level')]
public function addRecord(Level $level, string $message, array $context = [], ?DateTimeImmutable $datetime = null): bool
{
if ($this->detectCycles) {
if (($fiber = \Fiber::getCurrent()) instanceof \Fiber) {
$logDepth = $this->fiberLogDepth[$fiber] = ($this->fiberLogDepth[$fiber] ?? 0) + 1;
} else {
$logDepth = ++$this->logDepth;
}
} else {
$logDepth = 0;
}
if ($logDepth === 3) {
$this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.');
return false;
}
if ($logDepth >= 5) { // log depth 4 is let through, so we can log the warning above
return false;
}
$trace = debug_backtrace();
$context = array_merge(
[
'file' => basename($trace[1]['file']),
'line' => $trace[1]['line'],
'pid' => getmypid(),
],
$context
);
try {
$recordInitialized = $this->processors === [];
$record = new LogRecord(
datetime: $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
channel: $this->name,
level: $level,
message: $message,
context: $context,
extra: [],
);
$handled = false;
foreach ($this->handlers as $handler) {
if (false === $recordInitialized) {
// skip initializing the record as long as no handler is going to handle it
if (!$handler->isHandling($record)) {
continue;
}
try {
foreach ($this->processors as $processor) {
$record = $processor($record);
}
$recordInitialized = true;
} catch (\Throwable $e) {
$this->handleException($e, $record);
return true;
}
}
// once the record is initialized, send it to all handlers as long as the bubbling chain is not interrupted
try {
$handled = true;
if (true === $handler->handle(clone $record)) {
break;
}
} catch (\Throwable $e) {
$this->handleException($e, $record);
return true;
}
}
return $handled;
} finally {
if ($this->detectCycles) {
if (isset($fiber)) {
--$this->fiberLogDepth[$fiber];
} else {
--$this->logDepth;
}
}
}
}
public function close(): void
{
foreach ($this->handlers as $handler) {
$handler->close();
}
}
public function reset(): void
{
foreach ($this->handlers as $handler) {
if ($handler instanceof ResettableInterface) {
$handler->reset();
}
}
foreach ($this->processors as $processor) {
if ($processor instanceof ResettableInterface) {
$processor->reset();
}
}
}
/**
* Checks whether the Logger has a handler that listens on the given level.
*/
#[Param(level: 'Level')]
public function isHandling(Level $level): bool
{
$record = new LogRecord(
datetime: new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
channel: $this->name,
message: '',
level: $level,
);
foreach ($this->handlers as $handler) {
if ($handler->isHandling($record)) {
return true;
}
}
return false;
}
/**
* Adds a log record at an arbitrary level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(level: 'mixed')] // The log level (a Monolog, PSR-3 or RFC 5424 level)
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
#[Param(level: 'Level|LogLevel::*')]
public function log($level, string | \Stringable $message, array $context = []): void
{
if (!$level instanceof Level) {
$level = Level::Critical;
}
$this->addRecord($level, "A Level that wasn't valid was used to write to a Logger: {level}", ['level' => $level]);
$this->addRecord($level, (string) $message, $context);
}
/**
* Adds a log record at the DEBUG level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function debug(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Debug, (string) $message, $context);
}
/**
* Adds a log record at the INFO level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function info(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Info, (string) $message, $context);
}
/**
* Adds a log record at the NOTICE level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function notice(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Notice, (string) $message, $context);
}
/**
* Adds a log record at the WARNING level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function warning(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Warning, (string) $message, $context);
}
/**
* Adds a log record at the ERROR level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function error(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Error, (string) $message, $context);
}
/**
* Adds a log record at the CRITICAL level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function critical(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Critical, (string) $message, $context);
}
/**
* Adds a log record at the ALERT level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function alert(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Alert, (string) $message, $context);
}
/**
* Adds a log record at the EMERGENCY level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function emergency(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Emergency, (string) $message, $context);
}
/**
* Sets the timezone to be used for the timestamp of log records.
*/
#[Returns('$this')]
public function setTimezone(\DateTimeZone $tz): self
{
$this->timezone = $tz;
return $this;
}
/**
* Returns the timezone to be used for the timestamp of log records.
*/
public function getTimezone(): \DateTimeZone
{
return $this->timezone;
}
/**
* Delegates exception management to the custom exception handler,
* or throws the exception if no custom handler is set.
*/
protected function handleException(\Throwable $e, LogRecord $record): void
{
if (!$this->exceptionHandler instanceof \Closure) {
throw $e;
}
($this->exceptionHandler)($e, $record);
}
#[Returns('array<string, mixed>')]
public function __serialize(): array
{
return [
'name' => $this->name,
'handlers' => $this->handlers,
'processors' => $this->processors,
'microsecondTimestamps' => $this->microsecondTimestamps,
'timezone' => $this->timezone,
'exceptionHandler' => $this->exceptionHandler,
'logDepth' => $this->logDepth,
'detectCycles' => $this->detectCycles,
];
}
#[Param(data: 'array<string, mixed>')]
public function __unserialize(array $data): void
{
foreach (['name', 'handlers', 'processors', 'microsecondTimestamps', 'timezone', 'exceptionHandler', 'logDepth', 'detectCycles'] as $property) {
if (isset($data[$property])) {
$this->{$property} = $data[$property];
}
}
$this->fiberLogDepth = new \WeakMap();
}
}

View file

@ -7,7 +7,7 @@ namespace Bouncer\Logger;
use Bouncer\Settings\Settings; use Bouncer\Settings\Settings;
use Monolog\Processor; use Monolog\Processor;
class Logger extends \Monolog\Logger class Logger extends AbstractLogger
{ {
public function __construct( public function __construct(
private readonly Settings $settings, private readonly Settings $settings,

View file

@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Bouncer; namespace Bouncer;
use Bouncer\Logger\AbstractLogger;
use Bouncer\Logger\Logger; use Bouncer\Logger\Logger;
use Bouncer\Settings\Settings; use Bouncer\Settings\Settings;
use Psr\Log\LoggerInterface;
use Spatie\Emoji\Emoji; use Spatie\Emoji\Emoji;
class Target class Target
@ -27,14 +29,12 @@ class Target
private ?int $proxyTimeoutSeconds = null; private ?int $proxyTimeoutSeconds = null;
private ?string $username = null; private ?string $username = null;
private ?string $password = null; private ?string $password = null;
private ?string $hostOverride = null; private ?string $hostOverride = null;
private ?string $customNginxConfig = null; private ?string $customNginxConfig = null;
private bool $requiresForcedScanning = false; private bool $requiresForcedScanning = false;
public function __construct( public function __construct(
private Logger $logger, private AbstractLogger $logger,
private Settings $settings, private Settings $settings,
) { ) {
$this->allowNonSSL = $this->settings->get('ssl/allow_non_ssl', true); $this->allowNonSSL = $this->settings->get('ssl/allow_non_ssl', true);
@ -94,9 +94,6 @@ class Target
return $this->username; return $this->username;
} }
/**
* @param string
*/
public function setUsername(string $username): self public function setUsername(string $username): self
{ {
$this->username = $username; $this->username = $username;
@ -298,9 +295,6 @@ class Target
return $this; return $this;
} }
/**
* @return string
*/
public function getDomains(): array public function getDomains(): array
{ {
return $this->domains; return $this->domains;
@ -420,10 +414,6 @@ class Target
return $this; return $this;
} }
public function getLogger(): Logger
{
return $this->logger;
}
public function updateLogger(): self public function updateLogger(): self
{ {

View file

@ -1 +0,0 @@
<h1>Website A</h1>

Binary file not shown.

Before

Width: 64px  |  Height: 64px  |  Size: 34 KiB

View file

@ -1 +0,0 @@
<h1>Website B</h1>

Binary file not shown.

Before

Width: 64px  |  Height: 64px  |  Size: 34 KiB

View file

@ -1 +0,0 @@
<h1>Website C</h1>

View file

Before

Width: 64px  |  Height: 64px  |  Size: 34 KiB

After

Width: 64px  |  Height: 64px  |  Size: 34 KiB

View file

@ -0,0 +1,5 @@
<?php
$environment = array_merge($_ENV, $_SERVER);
$site = $environment['SITE_NAME'] ?? 'unknown';
$server = $environment['SERVER_NAME'] ?? gethostname();
printf("<h1>Website %s</h1><p>Running on %s</p>", $site, $server);