Upstreaming changes from downstream project

This commit is contained in:
Greyscale 2022-08-09 01:28:22 +01:00
parent d62725db5f
commit 9d437e266f
No known key found for this signature in database
GPG key ID: CEC4822A25BA80C5
7 changed files with 304 additions and 109 deletions

7
bouncer/.gitignore vendored
View file

@ -1,3 +1,4 @@
/docker-compose.override.yml .idea
/vendor docker-compose.override.yml
/.php-cs-fixer.cache vendor
.php-cs-fixer.cache

View file

@ -49,11 +49,13 @@ COPY composer.* /app/
COPY public /app/public COPY public /app/public
RUN composer install && \ RUN composer install && \
chmod +x /app/bouncer && \ chmod +x /app/bouncer && \
mkdir -p /var/log/bouncer mkdir -p /var/log/bouncer && \
rm /etc/nginx/sites-enabled/default && \
cp /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
FROM benzine/php:nginx-8.0 as test-app-a FROM benzine/php:nginx-8.1 as test-app-a
COPY ./test/public-web-a /app/public COPY ./test/public-web-a /app/public
FROM benzine/php:nginx-8.0 as test-app-b FROM benzine/php:nginx-8.1 as test-app-b
COPY ./test/public-web-b /app/public COPY ./test/public-web-b /app/public

View file

@ -12,6 +12,9 @@ server {
{% if useTemporaryCert %} {% if useTemporaryCert %}
ssl_certificate /certs/example.crt; ssl_certificate /certs/example.crt;
ssl_certificate_key /certs/example.key; ssl_certificate_key /certs/example.key;
{% elseif useGlobalCert %}
ssl_certificate /certs/global.crt;
ssl_certificate_key /certs/global.key;
{% else %} {% else %}
ssl_certificate /etc/letsencrypt/live/{{ name }}/fullchain.pem; ssl_certificate /etc/letsencrypt/live/{{ name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ name }}/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/{{ name }}/privkey.pem;

48
bouncer/Readme.md Normal file
View file

@ -0,0 +1,48 @@
# Automatic Swarm Nginx Loadbalancer
## Environment variables
This container has its own environment variables, AS WELL AS scanning for some environment variables associated with your services.
These should not be confused.
### Load balancer Configuration
#### Main configuration
| Key | Default | Options | Behaviour |
|----------------------------------------|---------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DOCKER_HOST | false | | Define a http endpoint representing your docker socket. If this is null, it connects to /var/lib/docker.sock |
| GLOBAL_CERT | false | Contents of an ssl certificate | If you want to provide a single cert for all endpoints, perhaps with a catch-all that may be later overriden, you can provide the whole contents of a certificates file here. |
| GLOBAL_CERT_KEY | false | Contents of an ssl certificates private key | The private key related to GLOBAL CERT. These must be provided in tandem. |
| BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS | false | positive numbers | To force the bouncer to update on a schedule even if no changes are detected, measured in seconds |
#### For using with lets encrypt
| Key | Default | Options | Behaviour |
|---------------------------|-----------|---------------------------|--------------------------------------------------------------------------------------|
| BOUNCER_LETSENCRYPT_MODE | 'staging' | 'staging' or 'production' | Determine if this is going to connect to a production or staging Lets Encrypt server |
| BOUNCER_LETSENCRYPT_EMAIL | | 'bob@example.com' | Email address to associate with lets encrypt |
#### For using S3 for generated cert synchronisation with Lets Encrypt
| Key | Default | Options | Behaviour |
|------------------------------------|---------|-----------------|---------------------------------------------------------------------------------------|
| BOUNCER_S3_BUCKET | false | | enable S3 behaviour to store lets-encrypt generated certs |
| BOUNCER_S3_ENDPOINT | false | | define s3 endpoint to override default AWS s3 implementation, for example, with minio |
| BOUNCER_S3_KEY_ID | false | | S3 API Key ID | |
| BOUNCER_s3_KEY_SECRET | false | | S3 API Key Secret |
| BOUNCER_S3_REGION | false | | S3 API Region |
| BOUNCER_S3_USE_PATH_STYLE_ENDPOINT | false | `true or false` | Needed for minio |
| BOUNCER_S3_PREFIX | false | | Prefix file path in s3 bucket |
### Served Instance Configuration
These environment variables need to be applied to the CONSUMING SERVICE and not the loadbalancer container itself.
| Key | Example | Behaviour |
|--------------------------------|-------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| BOUNCER_DOMAIN | "a.example.com" | The domain that should be directed to this container |
| BOUNCER_LETSENCRYPT | Values are "yes" or "true", anything else is false | To enable, or disable Lets Encrypt service for this hostname |
| BOUNCER_TARGET_PORT | 9000 | Explicitly define the port you want to hit the service on, in case of ambiguity |
| BOUNCER_ALLOW_NON_SSL | Defaults to enabled. Values are "yes" or "true", anything else is false | Should HTTP only traffic be allowed to hit this service? If disabled, http traffic is forwarded towards https |
| BOUNCER_ALLOW_WEBSOCKETS | Defaults to enabled. Values are "yes" or "true", anything else is false | Enable websocket behaviour |
| BOUNCER_ALLOW_LARGE_PAYLOADS | Defaults to disabled. | Allows overriding the default nginx payload size. Related to BOUNCER_MAX_PAYLOADS_MEGABYTES |
| BOUNCER_MAX_PAYLOADS_MEGABYTES | numbers | Size of max payload to allow, in megabytes. Requires BOUNCER_ALLOW_LARGE_PAYLOADS to be enabled |
## Security considerations
If you're putting this behind access control to the docker socket, it will need access to the /swarm /services and /containers endpoints of the docker api.

View file

@ -21,16 +21,22 @@ class BouncerTarget
{ {
private string $id; private string $id;
private array $domains; private array $domains;
private string $ip; private string $endpointHostnameOrIp;
private int $port = 80; private ?int $port = null;
private bool $letsEncrypt = false; private bool $letsEncrypt = false;
private string $targetPath; private string $targetPath;
private bool $allowNonSSL = true; private bool $allowNonSSL = true;
private bool $useTemporaryCert = true; private bool $useTemporaryCert = true;
private bool $useGlobalCert = false;
private bool $allowWebsocketSupport = true; private bool $allowWebsocketSupport = true;
private bool $allowLargePayloads = false; private bool $allowLargePayloads = false;
private int $maxPayloadSizeMegabytes = 256; private int $maxPayloadSizeMegabytes = 256;
public function __construct(
private Logger $logger
) {
}
public function __toArray() public function __toArray()
{ {
return [ return [
@ -40,6 +46,7 @@ class BouncerTarget
'letsEncrypt' => $this->isLetsEncrypt(), 'letsEncrypt' => $this->isLetsEncrypt(),
'targetPath' => $this->getTargetPath(), 'targetPath' => $this->getTargetPath(),
'useTemporaryCert' => $this->isUseTemporaryCert(), 'useTemporaryCert' => $this->isUseTemporaryCert(),
'useGlobalCert' => $this->isUseGlobalCert(),
'allowNonSSL' => $this->isAllowNonSSL(), 'allowNonSSL' => $this->isAllowNonSSL(),
'allowWebsocketSupport' => $this->isAllowWebsocketSupport(), 'allowWebsocketSupport' => $this->isAllowWebsocketSupport(),
'allowLargePayloads' => $this->isAllowLargePayloads(), 'allowLargePayloads' => $this->isAllowLargePayloads(),
@ -59,6 +66,21 @@ class BouncerTarget
return $this; return $this;
} }
public function isUseGlobalCert(): bool
{
return $this->useGlobalCert;
}
public function setUseGlobalCert(bool $useGlobalCert): BouncerTarget
{
$this->useGlobalCert = $useGlobalCert;
// Global cert overrides temporary certs.
$this->setUseTemporaryCert(false);
return $this;
}
public function isAllowWebsocketSupport(): bool public function isAllowWebsocketSupport(): bool
{ {
return $this->allowWebsocketSupport; return $this->allowWebsocketSupport;
@ -149,23 +171,28 @@ class BouncerTarget
return $this; return $this;
} }
public function getIp(): string public function getEndpointHostnameOrIp(): string
{ {
return $this->ip; return $this->endpointHostnameOrIp;
} }
public function setIp(string $ip): BouncerTarget public function setEndpointHostnameOrIp(string $endpointHostnameOrIp): BouncerTarget
{ {
$this->ip = $ip; $this->endpointHostnameOrIp = $endpointHostnameOrIp;
return $this; return $this;
} }
public function getPort(): int public function getPort(): ?int
{ {
return $this->port; return $this->port;
} }
public function isPortSet(): bool
{
return $this->port !== null;
}
public function setPort(int $port): BouncerTarget public function setPort(int $port): BouncerTarget
{ {
$this->port = $port; $this->port = $port;
@ -189,6 +216,28 @@ class BouncerTarget
return $this; return $this;
} }
public function isEndpointValid(): bool
{
// Is it just an IP?
if (filter_var($this->getEndpointHostnameOrIp(), FILTER_VALIDATE_IP)) {
// $this->logger->debug(sprintf('%s isEndpointValid: %s is a normal IP', Emoji::magnifyingGlassTiltedRight(), $this->getEndpointHostnameOrIp()));
return true;
}
// Is it a Hostname that resolves?
$resolved = gethostbyname($this->getEndpointHostnameOrIp());
if (filter_var($resolved, FILTER_VALIDATE_IP)) {
// $this->logger->debug(sprintf('%s isEndpointValid: %s is a hostname that resolves to a normal IP %s', Emoji::magnifyingGlassTiltedRight(), $this->getEndpointHostnameOrIp(), $resolved));
return true;
}
// $this->logger->debug(sprintf('%s isEndpointValid: %s is a hostname does not resolve', Emoji::magnifyingGlassTiltedRight(), $this->getEndpointHostnameOrIp()));
return false;
}
} }
class Bouncer class Bouncer
@ -200,10 +249,14 @@ class Bouncer
private Filesystem $configFilesystem; private Filesystem $configFilesystem;
private Filesystem $certificateStoreLocal; private Filesystem $certificateStoreLocal;
private ?Filesystem $certificateStoreRemote = null; private ?Filesystem $certificateStoreRemote = null;
private Filesystem $providedCertificateStore;
private Logger $logger; private Logger $logger;
private string $instanceStateHash = ''; private string $instanceStateHash = '';
private array $fileHashes; private array $fileHashes;
private bool $swarmMode = false; private bool $swarmMode = false;
private bool $useGlobalCert = false;
private int $forcedUpdateIntervalSeconds = 600;
private ?int $lastUpdateEpoch = null;
public function __construct() public function __construct()
{ {
@ -216,14 +269,13 @@ class Bouncer
$stdout->setFormatter(new ColoredLineFormatter(null, "%level_name%: %message% \n")); $stdout->setFormatter(new ColoredLineFormatter(null, "%level_name%: %message% \n"));
$this->logger->pushHandler($stdout); $this->logger->pushHandler($stdout);
$this->client = new Guzzle( if (isset($this->environment['DOCKER_HOST'])) {
[ $this->logger->info(sprintf('%s Connecting to %s', Emoji::electricPlug(), $this->environment['DOCKER_HOST']));
'base_uri' => 'http://localhost', $this->client = new Guzzle(['base_uri' => $this->environment['DOCKER_HOST']]);
'curl' => [ } else {
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock', $this->logger->info(sprintf('%s Connecting to /var/run/docker.sock', Emoji::electricPlug()));
], $this->client = new Guzzle(['base_uri' => 'http://localhost', 'curl' => [CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock']]);
] }
);
$this->loader = new FilesystemLoader([ $this->loader = new FilesystemLoader([
__DIR__, __DIR__,
@ -236,6 +288,9 @@ class Bouncer
// Set up Local certificate store // Set up Local certificate store
$this->certificateStoreLocal = new Filesystem(new LocalFilesystemAdapter('/etc/letsencrypt')); $this->certificateStoreLocal = new Filesystem(new LocalFilesystemAdapter('/etc/letsencrypt'));
// Set up Local certificate store for certificates provided to us
$this->providedCertificateStore = new Filesystem(new LocalFilesystemAdapter('/certs'));
// Set up Remote certificate store, if configured // Set up Remote certificate store, if configured
if (isset($this->environment['BOUNCER_S3_BUCKET'])) { if (isset($this->environment['BOUNCER_S3_BUCKET'])) {
$this->certificateStoreRemote = new Filesystem( $this->certificateStoreRemote = new Filesystem(
@ -255,6 +310,20 @@ class Bouncer
) )
); );
} }
// Allow defined global cert if set
if (isset($this->environment['GLOBAL_CERT'], $this->environment['GLOBAL_CERT_KEY'])) {
$this->setUseGlobalCert(true);
$this->providedCertificateStore->write('global.crt', str_replace('\\n', "\n", trim($this->environment['GLOBAL_CERT'], '"')));
$this->providedCertificateStore->write('global.key', str_replace('\\n', "\n", trim($this->environment['GLOBAL_CERT_KEY'], '"')));
$this->logger->info(sprintf("%s GLOBAL_CERT was set, so we're going to use a defined certificate!", Emoji::globeShowingEuropeAfrica()));
}
// Determine forced update interval.
if (isset($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS']) && is_numeric($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS'])) {
$this->setForcedUpdateIntervalSeconds($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS']);
}
$this->logger->info(sprintf('%s Forced update interval is every %d seconds', Emoji::watch(), $this->getForcedUpdateIntervalSeconds()));
} }
public function isSwarmMode(): bool public function isSwarmMode(): bool
@ -269,6 +338,30 @@ class Bouncer
return $this; return $this;
} }
public function isUseGlobalCert(): bool
{
return $this->useGlobalCert;
}
public function setUseGlobalCert(bool $useGlobalCert): Bouncer
{
$this->useGlobalCert = $useGlobalCert;
return $this;
}
public function getForcedUpdateIntervalSeconds(): int
{
return $this->forcedUpdateIntervalSeconds;
}
public function setForcedUpdateIntervalSeconds(int $forcedUpdateIntervalSeconds): Bouncer
{
$this->forcedUpdateIntervalSeconds = $forcedUpdateIntervalSeconds;
return $this;
}
/** /**
* @throws \GuzzleHttp\Exception\GuzzleException * @throws \GuzzleHttp\Exception\GuzzleException
* *
@ -288,29 +381,40 @@ class Bouncer
[$envKey, $envVal] = explode('=', $environmentItem, 2); [$envKey, $envVal] = explode('=', $environmentItem, 2);
$envs[$envKey] = $envVal; $envs[$envKey] = $envVal;
} else { } else {
$envs[$envKey] = true; $envs[$environmentItem] = true;
} }
} }
} }
if (isset($envs['BOUNCER_DOMAIN'])) { if (isset($envs['BOUNCER_DOMAIN'])) {
$bouncerTarget = (new BouncerTarget()) $bouncerTarget = (new BouncerTarget($this->logger))
->setId($inspect['Id']) ->setId($inspect['Id'])
; ;
$bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget); $bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget);
if (isset($inspect['NetworkSettings']['IPAddress']) && !empty($inspect['NetworkSettings']['IPAddress'])) { if (isset($inspect['NetworkSettings']['IPAddress']) && !empty($inspect['NetworkSettings']['IPAddress'])) {
// As per docker service // As per docker service
$bouncerTarget->setIp($inspect['NetworkSettings']['IPAddress']); $bouncerTarget->setEndpointHostnameOrIp($inspect['NetworkSettings']['IPAddress']);
} else { } else {
// As per docker compose // As per docker compose
$networks = array_values($inspect['NetworkSettings']['Networks']); $networks = array_values($inspect['NetworkSettings']['Networks']);
$bouncerTarget->setIp($networks[0]['IPAddress']); $bouncerTarget->setEndpointHostnameOrIp($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())); $bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getEndpointHostnameOrIp(), $bouncerTarget->getPort() >= 0 ? $bouncerTarget->getPort() : 80));
$bouncerTargets[] = $bouncerTarget; $bouncerTarget->setUseGlobalCert($this->isUseGlobalCert());
$valid = $bouncerTarget->isEndpointValid();
// $this->logger->debug(sprintf(
// '%s Decided that %s has the endpoint %s and it %s.',
// Emoji::magnifyingGlassTiltedLeft(),
// $bouncerTarget->getName(),
// $bouncerTarget->getEndpointHostnameOrIp(),
// $valid ? 'is valid' : 'is not valid'
// ));
if ($valid) {
$bouncerTargets[] = $bouncerTarget;
}
} }
} }
@ -340,19 +444,39 @@ class Bouncer
$envs[$eKey] = $eVal; $envs[$eKey] = $eVal;
} }
if (isset($envs['BOUNCER_DOMAIN'])) { if (isset($envs['BOUNCER_DOMAIN'])) {
$bouncerTarget = (new BouncerTarget()) $bouncerTarget = (new BouncerTarget($this->logger))
->setId($service['ID']) ->setId($service['ID'])
; ;
$bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget); $bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget);
$bouncerTarget->setIp('172.17.0.1'); if ($bouncerTarget->isPortSet()) {
$bouncerTarget->setPort($service['Endpoint']['Ports'][0]['PublishedPort']); $bouncerTarget->setEndpointHostnameOrIp($service['Spec']['Name']);
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getIp(), $bouncerTarget->getPort())); // $this->logger->info(sprintf('Ports for %s has been explicitly set to %s:%d.', $bouncerTarget->getName(), $bouncerTarget->getEndpointHostnameOrIp(), $bouncerTarget->getPort()));
} elseif (isset($service['Endpoint']['Ports'])) {
$bouncerTarget->setEndpointHostnameOrIp('172.17.0.1');
$bouncerTarget->setPort($service['Endpoint']['Ports'][0]['PublishedPort']);
} else {
$this->logger->warning(sprintf('Ports block missing for %s.', $bouncerTarget->getName()));
continue;
}
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getEndpointHostnameOrIp(), $bouncerTarget->getPort()));
$bouncerTarget->setUseGlobalCert($this->isUseGlobalCert());
// $this->logger->debug(sprintf('Decided that %s has the target path %s', $bouncerTarget->getName(), $bouncerTarget->getTargetPath())); // $this->logger->debug(sprintf('Decided that %s has the target path %s', $bouncerTarget->getName(), $bouncerTarget->getTargetPath()));
$bouncerTargets[] = $bouncerTarget; $valid = $bouncerTarget->isEndpointValid();
// $this->logger->debug(sprintf(
// '%s Decided that %s has the endpoint %s and it %s.',
// Emoji::magnifyingGlassTiltedLeft(),
// $bouncerTarget->getName(),
// $bouncerTarget->getEndpointHostnameOrIp(),
// $valid ? 'is valid' : 'is not valid'
// ));
if ($valid) {
$bouncerTargets[] = $bouncerTarget;
}
} }
} }
} }
@ -431,6 +555,9 @@ class Bouncer
*/ */
private function stateHasChanged(): bool private function stateHasChanged(): bool
{ {
if ($this->lastUpdateEpoch === null || $this->lastUpdateEpoch <= time() - $this->forcedUpdateIntervalSeconds) {
return true;
}
$newInstanceStates = []; $newInstanceStates = [];
// Standard Containers // Standard Containers
@ -486,6 +613,8 @@ class Bouncer
$this->logger->info(sprintf('%s Swarm mode is %s.', Emoji::CHARACTER_HONEYBEE, $this->isSwarmMode() ? 'enabled' : 'disabled')); $this->logger->info(sprintf('%s Swarm mode is %s.', Emoji::CHARACTER_HONEYBEE, $this->isSwarmMode() ? 'enabled' : 'disabled'));
$targets = $this->isSwarmMode() ? $this->findContainersSwarmMode() : $this->findContainersContainerMode(); $targets = $this->isSwarmMode() ? $this->findContainersSwarmMode() : $this->findContainersContainerMode();
$this->wipeNginxConfig();
$this->logger->info(sprintf('%s Found %d services with BOUNCER_DOMAIN set', Emoji::CHARACTER_MAGNIFYING_GLASS_TILTED_LEFT, count($targets))); $this->logger->info(sprintf('%s Found %d services with BOUNCER_DOMAIN set', Emoji::CHARACTER_MAGNIFYING_GLASS_TILTED_LEFT, count($targets)));
foreach ($targets as $target) { foreach ($targets as $target) {
$this->generateNginxConfig($target); $this->generateNginxConfig($target);
@ -502,6 +631,7 @@ class Bouncer
while ($this->stateHasChanged() === false) { while ($this->stateHasChanged() === false) {
sleep(5); sleep(5);
} }
$this->lastUpdateEpoch = time();
$this->logger->info(sprintf('%s Host Container state has changed', Emoji::CHARACTER_WARNING)); $this->logger->info(sprintf('%s Host Container state has changed', Emoji::CHARACTER_WARNING));
} }
@ -567,10 +697,8 @@ class Bouncer
private function generateNginxConfig(BouncerTarget $target): self private function generateNginxConfig(BouncerTarget $target): self
{ {
$this->configFilesystem->write( $configData = $this->twig->render('NginxTemplate.twig', $target->__toArray());
$target->getName(), $this->configFilesystem->write($target->getName(), $configData);
$this->twig->render('NginxTemplate.twig', $target->__toArray())
);
$this->logger->info(sprintf('%s Created Nginx config for http://%s', Emoji::CHARACTER_PENCIL, $target->getName())); $this->logger->info(sprintf('%s Created Nginx config for http://%s', Emoji::CHARACTER_PENCIL, $target->getName()));
return $this; return $this;
@ -644,6 +772,19 @@ class Bouncer
$this->logger->info(sprintf('%s Restarting nginx', Emoji::CHARACTER_TIMER_CLOCK)); $this->logger->info(sprintf('%s Restarting nginx', Emoji::CHARACTER_TIMER_CLOCK));
$shell->run($command); $shell->run($command);
} }
private function wipeNginxConfig(): void
{
$this->logger->debug('Purging existing config files ...');
foreach ($this->configFilesystem->listContents('') as $file) {
/** @var FileAttributes $file */
if ($file->isFile() && $file->path() != 'default' && $file->path() != 'default-ssl') {
$this->configFilesystem->delete($file->path());
// $this->logger->debug(sprintf(' > %s', $file->path()));
}
}
// $this->logger->debug('Purge complete!');
}
} }
(new Bouncer())->run(); (new Bouncer())->run();

View file

@ -1,13 +1,13 @@
{ {
"name": "benzine/bouncer", "name": "benzine/bouncer",
"description": "Nginx Configuration Management", "description": "Automated Docker-swarm aware Nginx configuration management",
"type": "project", "type": "project",
"config": { "config": {
"sort-packages": true "sort-packages": true
}, },
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"require": { "require": {
"php": "^8.0", "php": "^8.1",
"ext-json": "*", "ext-json": "*",
"ext-curl": "*", "ext-curl": "*",
"kint-php/kint": "^3.3", "kint-php/kint": "^3.3",

138
bouncer/composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bb6894396a4d90d304b95586f326ea71", "content-hash": "afde4d97f684020724ec24b67577d3ae",
"packages": [ "packages": [
{ {
"name": "adambrett/shell-wrapper", "name": "adambrett/shell-wrapper",
@ -108,16 +108,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.224.0", "version": "3.228.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "bc5eb18414ef703c5f39a5a009a437c74c228306" "reference": "4dad57c95c7ff1dfcea29a7877ce64720b3318c3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bc5eb18414ef703c5f39a5a009a437c74c228306", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4dad57c95c7ff1dfcea29a7877ce64720b3318c3",
"reference": "bc5eb18414ef703c5f39a5a009a437c74c228306", "reference": "4dad57c95c7ff1dfcea29a7877ce64720b3318c3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -125,9 +125,9 @@
"ext-json": "*", "ext-json": "*",
"ext-pcre": "*", "ext-pcre": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"guzzlehttp/guzzle": "^5.3.3 || ^6.2.1 || ^7.0", "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"guzzlehttp/promises": "^1.4.0", "guzzlehttp/promises": "^1.4.0",
"guzzlehttp/psr7": "^1.7.0 || ^2.1.1", "guzzlehttp/psr7": "^1.8.5 || ^2.3",
"mtdowling/jmespath.php": "^2.6", "mtdowling/jmespath.php": "^2.6",
"php": ">=5.5" "php": ">=5.5"
}, },
@ -193,9 +193,9 @@
"support": { "support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.224.0" "source": "https://github.com/aws/aws-sdk-php/tree/3.228.3"
}, },
"time": "2022-05-27T20:23:28+00:00" "time": "2022-06-24T20:24:09+00:00"
}, },
{ {
"name": "bramus/ansi-php", "name": "bramus/ansi-php",
@ -883,16 +883,16 @@
}, },
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "2.6.0", "version": "2.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Seldaek/monolog.git", "url": "https://github.com/Seldaek/monolog.git",
"reference": "247918972acd74356b0a91dfaa5adcaec069b6c0" "reference": "5579edf28aee1190a798bfa5be8bc16c563bd524"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/247918972acd74356b0a91dfaa5adcaec069b6c0", "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5579edf28aee1190a798bfa5be8bc16c563bd524",
"reference": "247918972acd74356b0a91dfaa5adcaec069b6c0", "reference": "5579edf28aee1190a798bfa5be8bc16c563bd524",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -971,7 +971,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Seldaek/monolog/issues", "issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/2.6.0" "source": "https://github.com/Seldaek/monolog/tree/2.7.0"
}, },
"funding": [ "funding": [
{ {
@ -983,7 +983,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-05-10T09:36:00+00:00" "time": "2022-06-09T08:59:12+00:00"
}, },
{ {
"name": "mtdowling/jmespath.php", "name": "mtdowling/jmespath.php",
@ -1435,16 +1435,16 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.25.0", "version": "v1.26.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab" "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"reference": "30885182c981ab175d4d034db0f6f469898070ab", "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1459,7 +1459,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.23-dev" "dev-main": "1.26-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -1497,7 +1497,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
}, },
"funding": [ "funding": [
{ {
@ -1513,20 +1513,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-10-20T20:35:02+00:00" "time": "2022-05-24T11:49:31+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.25.0", "version": "v1.26.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1541,7 +1541,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.23-dev" "dev-main": "1.26-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -1580,7 +1580,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
}, },
"funding": [ "funding": [
{ {
@ -1596,7 +1596,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-11-30T18:21:41+00:00" "time": "2022-05-24T11:49:31+00:00"
}, },
{ {
"name": "twig/twig", "name": "twig/twig",
@ -2337,16 +2337,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v6.1.0", "version": "v6.1.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170" "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c9646197ef43b0e2ff44af61e7f0571526fd4170", "url": "https://api.github.com/repos/symfony/console/zipball/7a86c1c42fbcb69b59768504c7bca1d3767760b7",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170", "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2413,7 +2413,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v6.1.0" "source": "https://github.com/symfony/console/tree/v6.1.2"
}, },
"funding": [ "funding": [
{ {
@ -2429,7 +2429,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-05-27T06:34:22+00:00" "time": "2022-06-26T13:01:30+00:00"
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
@ -2789,16 +2789,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-grapheme", "name": "symfony/polyfill-intl-grapheme",
"version": "v1.25.0", "version": "v1.26.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783" "reference": "433d05519ce6990bf3530fba6957499d327395c2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783", "reference": "433d05519ce6990bf3530fba6957499d327395c2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2810,7 +2810,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.23-dev" "dev-main": "1.26-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -2850,7 +2850,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0"
}, },
"funding": [ "funding": [
{ {
@ -2866,20 +2866,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-11-23T21:10:46+00:00" "time": "2022-05-24T11:49:31+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.25.0", "version": "v1.26.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" "reference": "219aa369ceff116e673852dce47c3a41794c14bd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", "reference": "219aa369ceff116e673852dce47c3a41794c14bd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2891,7 +2891,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.23-dev" "dev-main": "1.26-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -2934,7 +2934,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0"
}, },
"funding": [ "funding": [
{ {
@ -2950,20 +2950,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-02-19T12:13:01+00:00" "time": "2022-05-24T11:49:31+00:00"
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.25.0", "version": "v1.26.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2972,7 +2972,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.23-dev" "dev-main": "1.26-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -3017,7 +3017,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
}, },
"funding": [ "funding": [
{ {
@ -3033,20 +3033,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-03-04T08:16:47+00:00" "time": "2022-05-10T07:21:04+00:00"
}, },
{ {
"name": "symfony/polyfill-php81", "name": "symfony/polyfill-php81",
"version": "v1.25.0", "version": "v1.26.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php81.git", "url": "https://github.com/symfony/polyfill-php81.git",
"reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1",
"reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3055,7 +3055,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.23-dev" "dev-main": "1.26-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -3096,7 +3096,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0"
}, },
"funding": [ "funding": [
{ {
@ -3112,7 +3112,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-09-13T13:58:11+00:00" "time": "2022-05-24T11:49:31+00:00"
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
@ -3324,16 +3324,16 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v6.1.0", "version": "v6.1.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529" "reference": "1903f2879875280c5af944625e8246d81c2f0604"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/d3edc75baf9f1d4f94879764dda2e1ac33499529", "url": "https://api.github.com/repos/symfony/string/zipball/1903f2879875280c5af944625e8246d81c2f0604",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529", "reference": "1903f2879875280c5af944625e8246d81c2f0604",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3389,7 +3389,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v6.1.0" "source": "https://github.com/symfony/string/tree/v6.1.2"
}, },
"funding": [ "funding": [
{ {
@ -3405,7 +3405,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-04-22T08:18:23+00:00" "time": "2022-06-26T16:35:04+00:00"
} }
], ],
"aliases": [], "aliases": [],