Skip to content

Commit 466dd92

Browse files
tft7000dbu
andauthored
feat: support env var in varnish config for servers key (#564)
* feat: new config options servers_from_jsonenv to support a variable amount of proxy servers defined via an env var Co-authored-by: Tim Kask <[email protected]> Co-authored-by: David Buchmann <[email protected]>
1 parent e5e1e42 commit 466dd92

File tree

9 files changed

+214
-13
lines changed

9 files changed

+214
-13
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
2.11.0
5+
------
6+
7+
### Added
8+
9+
* New configuration option `servers_from_jsonenv` to support a variable amount of proxy servers defined via an environment variable.
10+
411
2.10.3
512
------
613

Resources/doc/reference/configuration/proxy-client.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ varnish
3737
servers:
3838
- 123.123.123.1:6060
3939
- 123.123.123.2
40+
# alternatively, if you configure the varnish servers in an environment variable:
41+
# servers_from_jsonenv: '%env(json:VARNISH_SERVERS)%'
4042
base_url: yourwebsite.com
4143
4244
``header_length``
@@ -69,6 +71,26 @@ When using a multi-server setup, make sure to include **all** proxy servers in
6971
this list. Invalidation must happen on all systems or you will end up with
7072
inconsistent caches.
7173

74+
.. note::
75+
76+
When using a variable amount of proxy servers that are defined via environment
77+
variable, use the ``http.servers_from_jsonenv`` option below.
78+
79+
``http.servers_from_jsonenv``
80+
"""""""""""""""""""""""""""""
81+
82+
**type**: ``string``
83+
84+
Json encoded servers array as string. The servers array has the same specs as ``http.servers``.
85+
86+
Use this option only when using a variable amount of proxy servers that shall be defined via
87+
environment variable. Otherwise use the regular ``http.servers`` option.
88+
89+
Usage:
90+
* fos_http_cache.yaml: ``servers_from_jsonenv: '%env(json:VARNISH_SERVERS)%'``
91+
* environment definition: ``VARNISH_SERVERS='["123.123.123.1:6060","123.123.123.2"]'``
92+
93+
7294
``http.base_url``
7395
"""""""""""""""""
7496

src/DependencyInjection/Configuration.php

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -488,19 +488,29 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode)
488488
->always()
489489
->then(function ($config) {
490490
foreach ($config as $proxyName => $proxyConfig) {
491-
$serversConfigured = isset($proxyConfig['http']) && isset($proxyConfig['http']['servers']) && \is_array($proxyConfig['http']['servers']);
491+
// we only want either the servers config or the servers_from_jsonenv config
492+
if (isset($proxyConfig['http']['servers']) && !count($proxyConfig['http']['servers'])) {
493+
unset($proxyConfig['http']['servers'], $config[$proxyName]['http']['servers']);
494+
}
495+
496+
$arrayServersConfigured = isset($proxyConfig['http']['servers']) && \is_array($proxyConfig['http']['servers']);
497+
$jsonServersConfigured = isset($proxyConfig['http']['servers_from_jsonenv']) && \is_string($proxyConfig['http']['servers_from_jsonenv']);
498+
499+
if ($arrayServersConfigured && $jsonServersConfigured) {
500+
throw new InvalidConfigurationException(sprintf('You can only set one of "http.servers" or "http.servers_from_jsonenv" but not both to avoid ambiguity for the proxy "%s"', $proxyName));
501+
}
492502

493503
if (!\in_array($proxyName, ['noop', 'default', 'symfony'])) {
494-
if (!$serversConfigured) {
495-
throw new \InvalidArgumentException(sprintf('The "http.servers" section must be defined for the proxy "%s"', $proxyName));
504+
if (!$arrayServersConfigured && !$jsonServersConfigured) {
505+
throw new InvalidConfigurationException(sprintf('The "http.servers" or "http.servers_from_jsonenv" section must be defined for the proxy "%s"', $proxyName));
496506
}
497507

498508
return $config;
499509
}
500510

501511
if ('symfony' === $proxyName) {
502-
if (!$serversConfigured && false === $proxyConfig['use_kernel_dispatcher']) {
503-
throw new \InvalidArgumentException('Either configure the "http.servers" section or enable "proxy_client.symfony.use_kernel_dispatcher"');
512+
if (!$arrayServersConfigured && !$jsonServersConfigured && false === $proxyConfig['use_kernel_dispatcher']) {
513+
throw new InvalidConfigurationException('Either configure the "http.servers" or "http.servers_from_jsonenv" section or enable "proxy_client.symfony.use_kernel_dispatcher"');
504514
}
505515
}
506516
}
@@ -532,12 +542,14 @@ private function getHttpDispatcherNode()
532542
->fixXmlConfig('server')
533543
->children()
534544
->arrayNode('servers')
535-
->info('Addresses of the hosts the caching proxy is running on. May be hostname or ip, and with :port if not the default port 80.')
545+
->info('Addresses of the hosts the caching proxy is running on. The values may be hostnames or ips, and with :port if not the default port 80.')
536546
->useAttributeAsKey('name')
537-
->isRequired()
538547
->requiresAtLeastOneElement()
539548
->prototype('scalar')->end()
540549
->end()
550+
->scalarNode('servers_from_jsonenv')
551+
->info('Addresses of the hosts the caching proxy is running on (env var that contains a json array as a string). The values may be hostnames or ips, and with :port if not the default port 80.')
552+
->end()
541553
->scalarNode('base_url')
542554
->defaultNull()
543555
->info('Default host name and optional path for path based invalidation.')

src/DependencyInjection/FOSHttpCacheExtension.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,12 +365,23 @@ private function loadProxyClient(ContainerBuilder $container, XmlFileLoader $loa
365365
*/
366366
private function createHttpDispatcherDefinition(ContainerBuilder $container, array $config, $serviceName)
367367
{
368-
foreach ($config['servers'] as $url) {
368+
if (array_key_exists('servers', $config)) {
369+
foreach ($config['servers'] as $url) {
370+
$usedEnvs = [];
371+
$container->resolveEnvPlaceholders($url, null, $usedEnvs);
372+
if (0 === \count($usedEnvs)) {
373+
$this->validateUrl($url, 'Not a valid Varnish server address: "%s"');
374+
}
375+
}
376+
}
377+
if (array_key_exists('servers_from_jsonenv', $config) && is_string($config['servers_from_jsonenv'])) {
378+
// check that the config contains an env var
369379
$usedEnvs = [];
370-
$container->resolveEnvPlaceholders($url, null, $usedEnvs);
380+
$container->resolveEnvPlaceholders($config['servers_from_jsonenv'], null, $usedEnvs);
371381
if (0 === \count($usedEnvs)) {
372-
$this->validateUrl($url, 'Not a valid Varnish server address: "%s"');
382+
throw new InvalidConfigurationException('Not a valid Varnish servers_from_jsonenv configuration: '.$config['servers_from_jsonenv']);
373383
}
384+
$config['servers'] = $config['servers_from_jsonenv'];
374385
}
375386
if (!empty($config['base_url'])) {
376387
$baseUrl = $config['base_url'];
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCacheBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
$container->loadFromExtension('fos_http_cache', [
13+
'proxy_client' => [
14+
'varnish' => [
15+
'http' => [
16+
'servers_from_jsonenv' => '%env(json:VARNISH_SERVERS)%',
17+
'base_url' => '/test',
18+
'http_client' => 'acme.guzzle.nginx',
19+
],
20+
],
21+
],
22+
]);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services">
3+
4+
<config xmlns="http://example.org/schema/dic/fos_http_cache">
5+
<proxy-client>
6+
<varnish>
7+
<http base-url="/test" http-client="acme.guzzle.nginx" servers-from-jsonenv="%env(json:VARNISH_SERVERS)%" />
8+
</varnish>
9+
</proxy-client>
10+
11+
</config>
12+
</container>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
fos_http_cache:
2+
3+
proxy_client:
4+
varnish:
5+
http:
6+
servers_from_jsonenv: '%env(json:VARNISH_SERVERS)%'
7+
base_url: /test
8+
http_client: acme.guzzle.nginx

tests/Unit/DependencyInjection/ConfigurationTest.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ public function testSupportsSymfony()
301301
public function testEmptyServerConfigurationIsNotAllowed()
302302
{
303303
$this->expectException(InvalidConfigurationException::class);
304-
$this->expectExceptionMessage('Either configure the "http.servers" section or enable "proxy_client.symfony.use_kernel_dispatcher');
304+
$this->expectExceptionMessage('Either configure the "http.servers" or "http.servers_from_jsonenv" section or enable "proxy_client.symfony.use_kernel_dispatcher');
305305

306306
$params = $this->getEmptyConfig();
307307
$params['proxy_client'] = [
@@ -734,4 +734,37 @@ private function getEmptyConfig()
734734
],
735735
];
736736
}
737+
738+
public function testSupportsServersFromJsonEnv(): void
739+
{
740+
$expectedConfiguration = $this->getEmptyConfig();
741+
$expectedConfiguration['proxy_client'] = [
742+
'varnish' => [
743+
'http' => [
744+
'servers_from_jsonenv' => '%env(json:VARNISH_SERVERS)%',
745+
'base_url' => '/test',
746+
'http_client' => 'acme.guzzle.nginx',
747+
],
748+
'tag_mode' => 'ban',
749+
'tags_header' => 'X-Cache-Tags',
750+
],
751+
];
752+
$expectedConfiguration['cache_manager']['enabled'] = 'auto';
753+
$expectedConfiguration['cache_manager']['generate_url_type'] = 'auto';
754+
$expectedConfiguration['tags']['enabled'] = 'auto';
755+
$expectedConfiguration['invalidation']['enabled'] = 'auto';
756+
$expectedConfiguration['user_context']['logout_handler']['enabled'] = true;
757+
758+
$formats = array_map(function ($path) {
759+
return __DIR__.'/../../Resources/Fixtures/'.$path;
760+
}, [
761+
'config/servers_from_jsonenv.yml',
762+
'config/servers_from_jsonenv.xml',
763+
'config/servers_from_jsonenv.php',
764+
]);
765+
766+
foreach ($formats as $format) {
767+
$this->assertProcessedConfigurationEquals($expectedConfiguration, [$format]);
768+
}
769+
}
737770
}

tests/Unit/DependencyInjection/FOSHttpCacheExtensionTest.php

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
use PHPUnit\Framework\TestCase;
1818
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1919
use Symfony\Component\DependencyInjection\ChildDefinition;
20+
use Symfony\Component\DependencyInjection\Compiler\ResolveEnvPlaceholdersPass;
21+
use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass;
2022
use Symfony\Component\DependencyInjection\ContainerBuilder;
2123
use Symfony\Component\DependencyInjection\Definition;
2224
use Symfony\Component\DependencyInjection\DefinitionDecorator;
23-
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
25+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
26+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
2427
use Symfony\Component\DependencyInjection\Reference;
2528
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
2629
use Symfony\Component\HttpKernel\Kernel;
@@ -662,10 +665,81 @@ public function testVarnishCustomTagsHeader()
662665
$this->assertEquals(['tag_mode' => 'ban', 'tags_header' => 'myheader'], $container->getParameter('fos_http_cache.proxy_client.varnish.options'));
663666
}
664667

668+
/**
669+
* @param array|null $serversValue array that contains servers, `null` if not set
670+
* @param string|null $serversFromJsonEnvValue string that should contain an env var (use `VARNISH_SERVERS` for this test), `null` if not set
671+
* @param string|mixed|null $envValue _ENV['VARNISH_SERVERS'] will be set to this value; only used if `$serversFromJsonEnvValue` is used; should be a string, otherwise an error will show up
672+
* @param array|null $expectedServersValue expected servers value the http dispatcher receives
673+
* @param string|null $expectExceptionClass the exception class the configuration might throw, `null` if no exception is thrown
674+
* @param string|null $expectExceptionMessage the message the exception throws, anything if no exception is thrown
675+
*
676+
* @dataProvider dataVarnishServersConfig
677+
*/
678+
public function testVarnishServersConfig($serversValue, $serversFromJsonEnvValue, $envValue, $expectedServersValue, $expectExceptionClass, $expectExceptionMessage): void
679+
{
680+
$_ENV['VARNISH_SERVERS'] = $envValue;
681+
$container = $this->createContainer();
682+
683+
// workaround to get the possible env string into the EnvPlaceholderParameterBag
684+
$container->setParameter('triggerServersValue', $serversValue);
685+
$container->setParameter('triggerServersFromJsonEnvValue', $serversFromJsonEnvValue);
686+
(new ResolveParameterPlaceHoldersPass())->process($container);
687+
688+
$config = $this->getBaseConfig();
689+
690+
if (null === $serversValue) {
691+
unset($config['proxy_client']['varnish']['http']['servers']);
692+
} else {
693+
$config['proxy_client']['varnish']['http']['servers'] = $container->getParameter('triggerServersValue');
694+
}
695+
if (null !== $serversFromJsonEnvValue) {
696+
$config['proxy_client']['varnish']['http']['servers_from_jsonenv'] = $container->getParameter('triggerServersFromJsonEnvValue');
697+
}
698+
699+
if ($expectExceptionClass) {
700+
$this->expectException($expectExceptionClass);
701+
$this->expectExceptionMessage($expectExceptionMessage);
702+
}
703+
704+
$this->extension->load([$config], $container);
705+
706+
// Note: until here InvalidConfigurationException should be thrown
707+
if (InvalidConfigurationException::class === $expectExceptionClass) {
708+
return;
709+
}
710+
711+
(new ResolveEnvPlaceholdersPass())->process($container);
712+
713+
// Note: now all expected exceptions should be thrown
714+
if ($expectExceptionClass) {
715+
return;
716+
}
717+
718+
$definition = $container->getDefinition('fos_http_cache.proxy_client.varnish.http_dispatcher');
719+
static::assertEquals($expectedServersValue, $definition->getArgument(0));
720+
}
721+
722+
public function dataVarnishServersConfig()
723+
{
724+
return [
725+
// working case before implementing the feature 'env vars in servers key'
726+
'regular array as servers value allowed' => [['my-server-1', 'my-server-2'], null, null, ['my-server-1', 'my-server-2'], null, null],
727+
// testing the feature 'env vars in servers_from_jsonenv key'
728+
'env var with json array as servers value allowed' => [null, '%env(json:VARNISH_SERVERS)%', '["my-server-1","my-server-2"]', ['my-server-1', 'my-server-2'], null, null],
729+
// not allowed cases (servers_from_jsonenv)
730+
'plain string as servers value is forbidden' => [null, 'plain_string_not_allowed_as_servers_from_jsonenv_value', null, null, InvalidConfigurationException::class, 'Not a valid Varnish servers_from_jsonenv configuration: plain_string_not_allowed_as_servers_from_jsonenv_value'],
731+
'an int as servers value is forbidden' => [null, 1, 'env_value_not_used', null, InvalidConfigurationException::class, 'The "http.servers" or "http.servers_from_jsonenv" section must be defined for the proxy "varnish"'],
732+
'env var with string as servers value is forbidden (at runtime)' => [null, '%env(json:VARNISH_SERVERS)%', 'wrong_usage_of_env_value', 'no_servers_value', RuntimeException::class, 'Invalid JSON in env var "VARNISH_SERVERS": Syntax error'],
733+
// more cases
734+
'no definition leads to error' => [null, null, 'not_used', 'not_used', InvalidConfigurationException::class, 'The "http.servers" or "http.servers_from_jsonenv" section must be defined for the proxy "varnish"'],
735+
'both servers and servers_from_jsonenv defined leads to error' => [['my-server-1', 'my-server-2'], '%env(json:VARNISH_SERVERS)%', 'not_used', 'not_used', InvalidConfigurationException::class, 'You can only set one of "http.servers" or "http.servers_from_jsonenv" but not both to avoid ambiguity for the proxy "varnish"'],
736+
];
737+
}
738+
665739
private function createContainer()
666740
{
667741
$container = new ContainerBuilder(
668-
new ParameterBag(['kernel.debug' => false])
742+
new EnvPlaceholderParameterBag(['kernel.debug' => false])
669743
);
670744

671745
// The cache_manager service depends on the router service

0 commit comments

Comments
 (0)