Skip to content

Commit

Permalink
Merge pull request #589 from Jean-Beru/add-cloudfront-support
Browse files Browse the repository at this point in the history
Add CloudFront support
  • Loading branch information
dbu committed May 4, 2023
2 parents ef1bab7 + 4b1c865 commit 0d22d7e
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
- php-version: '7.4'
- php-version: '8.0'
- php-version: '8.1'
dependencies: 'jean-beru/fos-http-cache-cloudfront'
- php-version: '7.4'
symfony-version: '4.*'
- php-version: '7.4'
Expand Down
50 changes: 50 additions & 0 deletions Resources/doc/reference/configuration/proxy-client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,55 @@ endpoint for testing purposes.

.. _configuration_noop_proxy_client:

cloudfront
----------
Talking to AWS cloudfront requires the ``jean-beru/fos-http-cache-cloudfront`` library. You need to require this dependency before you can configure the ``cloudfront`` proxy client.

.. code-block:: yaml
# config/packages/fos_http_cache.yaml
fos_http_cache:
proxy_client:
cloudfront:
distribution_id: '<my-distribution-id>'
configuration:
accessKeyId: '<my-access-key-id>'
accessKeySecret: '<my-access-key-secret>'
.. code-block:: yaml
# config/packages/fos_http_cache.yaml
fos_http_cache:
proxy_client:
cloudfront:
distribution_id: '<my-distribution-id>'
client: '<my.custom.client>'
``distribution_id``
"""""""""""""""""""

**type**: ``string``

Identifier for the CloudFront distribution you want to purge the cache for.

``configuration``
"""""""""""""""""

**type**: ``array`` **default**: ``[]``

Configuration used to instantiate the `AsyncAws\CloudFront\CloudFrontClient` client. More information is available on
the `AWS Async documentation_`. It can not be used with the ``client`` option.

``client``
"""""""""""""""""

**type**: ``string`` **default**: ``null``

Service identifier of a `AsyncAws\CloudFront\CloudFrontClient` client. More information is available on the
`AWS Async documentation_`. It can not be used with the ``configuration`` option.

.. _configuration_noop_proxy_client:

noop
----

Expand Down Expand Up @@ -328,3 +377,4 @@ bundle. Please refer to the :ref:`FOSHttpCache library’s documentation <foshtt
for more information.

.. _xkey vmod: https://github.com/varnish/varnish-modules/blob/master/docs/vmod_xkey.rst
.. _AWS Async documentation_: https://async-aws.com/configuration.html
1 change: 1 addition & 0 deletions Resources/doc/spelling_word_list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ autoconfigured
backend
cacheable
cloudflare
cloudfront
ETag
friendsofsymfony
github
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"sebastian/exporter": "^2.0"
},
"suggest": {
"jean-beru/fos-http-cache-cloudfront": "To use CloudFront proxy",
"sensio/framework-extra-bundle": "For Tagged Cache Invalidation",
"symfony/expression-language": "For Tagged Cache Invalidation",
"symfony/console": "To send invalidation requests from the command line"
Expand Down
34 changes: 32 additions & 2 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\PurgeTagsListener;
use FOS\HttpCache\TagHeaderFormatter\TagHeaderFormatter;
use JeanBeru\HttpCacheCloudFront\Proxy\CloudFront;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
Expand Down Expand Up @@ -412,7 +413,7 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode)
->arrayNode('proxy_client')
->children()
->enumNode('default')
->values(['varnish', 'nginx', 'symfony', 'cloudflare', 'noop'])
->values(['varnish', 'nginx', 'symfony', 'cloudflare', 'cloudfront', 'noop'])
->info('If you configure more than one proxy client, you need to specify which client is the default.')
->end()
->arrayNode('varnish')
Expand Down Expand Up @@ -494,6 +495,29 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode)
->end()
->end()

->arrayNode('cloudfront')
->info('Configure a client to interact with AWS cloudfront. You need to install jean-beru/fos-http-cache-cloudfront to work with cloudfront')
->children()
->scalarNode('distribution_id')
->info('Identifier for your CloudFront distribution you want to purge the cache for')
->end()
->scalarNode('client')
->info('AsyncAws\CloudFront\CloudFrontClient client to use')
->defaultNull()
->end()
->variableNode('configuration')
->defaultValue([])
->info('Client configuration from https://async-aws.com/configuration.html')
->end()
->end()
->validate()
->ifTrue(function ($v) {
return null !== $v['client'] && count($v['configuration']) > 0;
})
->thenInvalid('You can not set both cloudfront.client and cloudfront.configuration')
->end()
->end()

->booleanNode('noop')->end()
->end()
->validate()
Expand All @@ -512,7 +536,7 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode)
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));
}

if (!\in_array($proxyName, ['noop', 'default', 'symfony', 'cloudflare'])) {
if (!\in_array($proxyName, ['noop', 'default', 'symfony', 'cloudflare', 'cloudfront'])) {
if (!$arrayServersConfigured && !$jsonServersConfigured) {
throw new InvalidConfigurationException(sprintf('The "http.servers" or "http.servers_from_jsonenv" section must be defined for the proxy "%s"', $proxyName));
}
Expand All @@ -525,6 +549,12 @@ private function addProxyClientSection(ArrayNodeDefinition $rootNode)
throw new InvalidConfigurationException('Either configure the "http.servers" or "http.servers_from_jsonenv" section or enable "proxy_client.symfony.use_kernel_dispatcher"');
}
}

if ('cloudfront' === $proxyName) {
if (!class_exists(CloudFront::class)) {
throw new InvalidConfigurationException('For the cloudfront proxy client, you need to install jean-beru/fos-http-cache-cloudfront. Class '.CloudFront::class.' does not exist.');
}
}
}

return $config;
Expand Down
31 changes: 31 additions & 0 deletions src/DependencyInjection/FOSHttpCacheExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace FOS\HttpCacheBundle\DependencyInjection;

use AsyncAws\CloudFront\CloudFrontClient;
use FOS\HttpCache\ProxyClient\HttpDispatcher;
use FOS\HttpCache\ProxyClient\ProxyClient;
use FOS\HttpCache\SymfonyCache\KernelDispatcher;
Expand Down Expand Up @@ -90,6 +91,8 @@ public function load(array $configs, ContainerBuilder $container)
if ('noop' !== $defaultClient
&& array_key_exists('base_url', $config['proxy_client'][$defaultClient])) {
$generateUrlType = UrlGeneratorInterface::ABSOLUTE_PATH;
} elseif ('cloudfront' === $defaultClient) {
$generateUrlType = UrlGeneratorInterface::ABSOLUTE_PATH;
} else {
$generateUrlType = UrlGeneratorInterface::ABSOLUTE_URL;
}
Expand Down Expand Up @@ -340,6 +343,9 @@ private function loadProxyClient(ContainerBuilder $container, XmlFileLoader $loa
if (isset($config['cloudflare'])) {
$this->loadCloudflare($container, $loader, $config['cloudflare']);
}
if (isset($config['cloudfront'])) {
$this->loadCloudfront($container, $loader, $config['cloudfront']);
}
if (isset($config['noop'])) {
$loader->load('noop.xml');
}
Expand Down Expand Up @@ -471,6 +477,27 @@ private function loadCloudflare(ContainerBuilder $container, XmlFileLoader $load
$loader->load('cloudflare.xml');
}

private function loadCloudfront(ContainerBuilder $container, XmlFileLoader $loader, array $config)
{
if (null !== $config['client']) {
$container->setAlias(
'fos_http_cache.proxy_client.cloudfront.cloudfront_client',
$config['client']
);
} else {
$container->setDefinition(
'fos_http_cache.proxy_client.cloudfront.cloudfront_client',
new Definition(CloudFrontClient::class, [$config['configuration']])
);
}

$container->setParameter('fos_http_cache.proxy_client.cloudfront.options', [
'distribution_id' => $config['distribution_id'],
]);

$loader->load('cloudfront.xml');
}

/**
* @param array $config Configuration section for the tags node
* @param string $client Name of the client used with the cache manager,
Expand Down Expand Up @@ -629,6 +656,10 @@ private function getDefaultProxyClient(array $config)
return 'cloudflare';
}

if (isset($config['cloudfront'])) {
return 'cloudfront';
}

if (isset($config['noop'])) {
return 'noop';
}
Expand Down
16 changes: 16 additions & 0 deletions src/Resources/config/cloudfront.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="fos_http_cache.proxy_client.cloudfront"
class="JeanBeru\HttpCacheCloudFront\Proxy\CloudFront"
public="true">
<argument type="service" id="fos_http_cache.proxy_client.cloudfront.cloudfront_client"/>
<argument>%fos_http_cache.proxy_client.cloudfront.options%</argument>
</service>
</services>

</container>
19 changes: 19 additions & 0 deletions tests/Resources/Fixtures/config/cloudfront.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the FOSHttpCacheBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

$container->loadFromExtension('fos_http_cache', [
'proxy_client' => [
'cloudfront' => [
'distribution_id' => 'my_distribution',
'configuration' => ['accessKeyId' => 'AwsAccessKeyId', 'accessKeySecret' => 'AwsAccessKeySecret'],
],
],
]);
15 changes: 15 additions & 0 deletions tests/Resources/Fixtures/config/cloudfront.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services">

<config xmlns="http://example.org/schema/dic/fos_http_cache">
<proxy-client>
<cloudfront distribution-id="my_distribution">
<configuration>
<accessKeyId>AwsAccessKeyId</accessKeyId>
<accessKeySecret>AwsAccessKeySecret</accessKeySecret>
</configuration>
</cloudfront>
</proxy-client>

</config>
</container>
8 changes: 8 additions & 0 deletions tests/Resources/Fixtures/config/cloudfront.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fos_http_cache:

proxy_client:
cloudfront:
distribution_id: 'my_distribution'
configuration:
accessKeyId: 'AwsAccessKeyId'
accessKeySecret: 'AwsAccessKeySecret'
55 changes: 55 additions & 0 deletions tests/Unit/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use FOS\HttpCacheBundle\DependencyInjection\Configuration;
use FOS\HttpCacheBundle\DependencyInjection\FOSHttpCacheExtension;
use JeanBeru\HttpCacheCloudFront\Proxy\CloudFront;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionConfigurationTestCase;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
Expand Down Expand Up @@ -327,6 +328,60 @@ public function testSupportsCloudflare()
}
}

public function testSupportsCloudfront()
{
if (!class_exists(CloudFront::class)) {
$this->markTestSkipped('jean-beru/fos-http-cache-cloudfront not available');
}

$expectedConfiguration = $this->getEmptyConfig();
$expectedConfiguration['proxy_client'] = [
'cloudfront' => [
'distribution_id' => 'my_distribution',
'configuration' => ['accessKeyId' => 'AwsAccessKeyId', 'accessKeySecret' => 'AwsAccessKeySecret'],
'client' => null,
],
];
$expectedConfiguration['cache_manager']['enabled'] = 'auto';
$expectedConfiguration['cache_manager']['generate_url_type'] = 'auto';
$expectedConfiguration['tags']['enabled'] = 'auto';
$expectedConfiguration['invalidation']['enabled'] = 'auto';

$formats = array_map(function ($path) {
return __DIR__.'/../../Resources/Fixtures/'.$path;
}, [
'config/cloudfront.yml',
'config/cloudfront.xml',
'config/cloudfront.php',
]);

foreach ($formats as $format) {
$this->assertProcessedConfigurationEquals($expectedConfiguration, [$format]);
}
}

public function testCloudfrontConfigurationWithClientIsNotAllowed()
{
if (!class_exists(CloudFront::class)) {
$this->markTestSkipped('jean-beru/fos-http-cache-cloudfront not available');
}

$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('You can not set both cloudfront.client and cloudfront.configuration');

$params = $this->getEmptyConfig();
$params['proxy_client'] = [
'cloudfront' => [
'distribution_id' => 'my_distribution',
'configuration' => ['accessKeyId' => 'AwsAccessKeyId', 'accessKeySecret' => 'AwsAccessKeySecret'],
'client' => 'my.client',
],
];

$configuration = new Configuration(false);
(new Processor())->processConfiguration($configuration, ['fos_http_cache' => $params]);
}

public function testEmptyServerConfigurationIsNotAllowed()
{
$this->expectException(InvalidConfigurationException::class);
Expand Down
Loading

0 comments on commit 0d22d7e

Please sign in to comment.