diff --git a/docs/reference/contrib/api/configuration.rst b/docs/reference/contrib/api/configuration.rst
index 1bc6aa0c6..2b8d3ea2e 100644
--- a/docs/reference/contrib/api/configuration.rst
+++ b/docs/reference/contrib/api/configuration.rst
@@ -59,7 +59,7 @@ This list also supports child relations (which will be nested inside the returne
Frontend cache invalidation
---------------------------
-If you have a Varnish, Squid or Cloudflare instance in front of your API, the ``wagtailapi`` module can automatically invalidate cached responses for you whenever they are updated in the database.
+If you have a Varnish, Squid, Cloudflare or CloudFront instance in front of your API, the ``wagtailapi`` module can automatically invalidate cached responses for you whenever they are updated in the database.
To enable it, firstly configure the ``wagtail.contrib.wagtailfrontendcache`` module within your project (see [Wagtail frontend cache docs](http://docs.wagtail.io/en/latest/contrib_components/frontendcache.html) for more information).
diff --git a/docs/reference/contrib/frontendcache.rst b/docs/reference/contrib/frontendcache.rst
index 0229fcd79..3271a03e8 100644
--- a/docs/reference/contrib/frontendcache.rst
+++ b/docs/reference/contrib/frontendcache.rst
@@ -8,7 +8,11 @@ Frontend cache invalidator
* Multiple backend support added
* Cloudflare support added
-Many websites use a frontend cache such as Varnish, Squid or Cloudflare to gain extra performance. The downside of using a frontend cache though is that they don't respond well to updating content and will often keep an old version of a page cached after it has been updated.
+.. versionchanged:: 1.6
+
+ * Amazon CloudFront support added
+
+Many websites use a frontend cache such as Varnish, Squid, Cloudflare or CloudFront to gain extra performance. The downside of using a frontend cache though is that they don't respond well to updating content and will often keep an old version of a page cached after it has been updated.
This document describes how to configure Wagtail to purge old versions of pages from a frontend cache whenever a page gets updated.
@@ -76,6 +80,41 @@ Add an item into the ``WAGTAILFRONTENDCACHE`` and set the ``BACKEND`` parameter
}
+Amazon CloudFront
+^^^^^^^^^^^^^^^^^
+
+Within Amazon Web Services you will need at least one CloudFront web distribution. If you don't have one, you can get one here: `CloudFront getting started `_
+
+Add an item into the ``WAGTAILFRONTENDCACHE`` and set the ``BACKEND`` parameter to ``wagtail.contrib.wagtailfrontendcache.backends.CloudfrontBackend``. This backend requires one extra parameter, ``DISTRIBUTION_ID`` (your CloudFront generated distrubition id).
+
+.. code-block:: python
+
+ WAGTAILFRONTENDCACHE = {
+ 'cloudfront': {
+ 'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.CloudfrontBackend',
+ 'DISTRIBUTION_ID': 'your-distribution-id',
+ },
+ }
+
+Configuration of credentials can done in multiple ways. You won't need to store them in your Django settings file. You can read more about this here: `Boto 3 Docs `_
+
+In case you run multiple sites with Wagtail and each site has its CloudFront distribution, provide a mapping instead of a single distribution. Make sure the mapping matches with the hostnames provided in your site settings.
+
+.. code-block:: python
+
+ WAGTAILFRONTENDCACHE = {
+ 'cloudfront': {
+ 'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.CloudfrontBackend',
+ 'DISTRIBUTION_ID': {
+ 'www.wagtail.io': 'your-distribution-id',
+ 'www.madewithwagtail.org': 'your-distribution-id',
+ },
+ },
+ }
+
+ .. note::
+ In most cases, absolute URLs with ``www`` prefixed domain names should be used in your mapping. Only drop the ``www`` prefix if you're absolutely sure you're not using it (e.g. a subdomain).
+
Advanced usage
--------------
diff --git a/docs/reference/contrib/index.rst b/docs/reference/contrib/index.rst
index 7a4e7a2d4..d5a792158 100644
--- a/docs/reference/contrib/index.rst
+++ b/docs/reference/contrib/index.rst
@@ -46,7 +46,7 @@ Provides a view that generates a Google XML sitemap of your public Wagtail conte
:doc:`frontendcache`
--------------------
-A module for automatically purging pages from a cache (Varnish, Squid or Cloudflare) when their content is changed.
+A module for automatically purging pages from a cache (Varnish, Squid, Cloudflare or Cloudfront) when their content is changed.
:doc:`routablepage`
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 46cf57288..35f9f7144 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -4,6 +4,7 @@ backends
callable
callables
Cloudflare
+Cloudfront
contrib
Django
Elasticsearch
diff --git a/wagtail/contrib/wagtailfrontendcache/backends.py b/wagtail/contrib/wagtailfrontendcache/backends.py
index e15459523..ea2b7aa1c 100644
--- a/wagtail/contrib/wagtailfrontendcache/backends.py
+++ b/wagtail/contrib/wagtailfrontendcache/backends.py
@@ -2,7 +2,9 @@ from __future__ import absolute_import, unicode_literals
import json
import logging
+import uuid
+from django.core.exceptions import ImproperlyConfigured
from django.utils.six.moves.urllib.error import HTTPError, URLError
from django.utils.six.moves.urllib.parse import urlencode, urlparse, urlunparse
from django.utils.six.moves.urllib.request import Request, urlopen
@@ -84,3 +86,56 @@ class CloudflareBackend(BaseBackend):
if response_json['result'] == 'error':
logger.error("Couldn't purge '%s' from Cloudflare. Cloudflare error '%s'", url, response_json['msg'])
return
+
+
+class CloudfrontBackend(BaseBackend):
+ def __init__(self, params):
+ import boto3
+
+ self.client = boto3.client('cloudfront')
+ try:
+ self.cloudfront_distribution_id = params.pop('DISTRIBUTION_ID')
+ except KeyError:
+ raise ImproperlyConfigured(
+ "The setting 'WAGTAILFRONTENDCACHE' requires the object 'DISTRIBUTION_ID'."
+ )
+
+ def purge(self, url):
+ url_parsed = urlparse(url)
+ distribution_id = None
+
+ if isinstance(self.cloudfront_distribution_id, dict):
+ host = url_parsed.hostname
+ if host in self.cloudfront_distribution_id:
+ distribution_id = self.cloudfront_distribution_id.get(host)
+ else:
+ logger.info(
+ "Couldn't purge '%s' from CloudFront. Hostname '%s' not found in the DISTRIBUTION_ID mapping",
+ url, host)
+ else:
+ distribution_id = self.cloudfront_distribution_id
+
+ if distribution_id:
+ path = url_parsed.path
+ self._create_invalidation(distribution_id, path)
+
+ def _create_invalidation(self, distribution_id, path):
+ import botocore
+
+ try:
+ self.client.create_invalidation(
+ DistributionId=distribution_id,
+ InvalidationBatch={
+ 'Paths': {
+ 'Quantity': 1,
+ 'Items': [
+ path,
+ ]
+ },
+ 'CallerReference': str(uuid.uuid4())
+ }
+ )
+ except botocore.exceptions.ClientError as e:
+ logger.error(
+ "Couldn't purge '%s' from CloudFront. ClientError: %s %s", path, e.response['Error']['Code'],
+ e.response['Error']['Message'])
diff --git a/wagtail/contrib/wagtailfrontendcache/tests.py b/wagtail/contrib/wagtailfrontendcache/tests.py
index 69d01f2dd..5d3edbae1 100644
--- a/wagtail/contrib/wagtailfrontendcache/tests.py
+++ b/wagtail/contrib/wagtailfrontendcache/tests.py
@@ -1,10 +1,13 @@
from __future__ import absolute_import, unicode_literals
+import mock
+
+from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from django.test.utils import override_settings
from wagtail.contrib.wagtailfrontendcache.backends import (
- BaseBackend, CloudflareBackend, HTTPBackend)
+ BaseBackend, CloudflareBackend, CloudfrontBackend, HTTPBackend)
from wagtail.contrib.wagtailfrontendcache.utils import get_backends
from wagtail.tests.testapp.models import EventIndex
from wagtail.wagtailcore.models import Page
@@ -45,6 +48,42 @@ class TestBackendConfiguration(TestCase):
self.assertEqual(backends['cloudflare'].cloudflare_email, 'test@test.com')
self.assertEqual(backends['cloudflare'].cloudflare_token, 'this is the token')
+ def test_cloudfront(self):
+ backends = get_backends(backend_settings={
+ 'cloudfront': {
+ 'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.CloudfrontBackend',
+ 'DISTRIBUTION_ID': 'frontend',
+ },
+ })
+
+ self.assertEqual(set(backends.keys()), set(['cloudfront']))
+ self.assertIsInstance(backends['cloudfront'], CloudfrontBackend)
+
+ self.assertEqual(backends['cloudfront'].cloudfront_distribution_id, 'frontend')
+
+ def test_cloudfront_validate_distribution_id(self):
+ with self.assertRaises(ImproperlyConfigured):
+ get_backends(backend_settings={
+ 'cloudfront': {
+ 'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.CloudfrontBackend',
+ },
+ })
+
+ @mock.patch('wagtail.contrib.wagtailfrontendcache.backends.CloudfrontBackend._create_invalidation')
+ def test_cloudfront_distribution_id_mapping(self, _create_invalidation):
+ backends = get_backends(backend_settings={
+ 'cloudfront': {
+ 'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.CloudfrontBackend',
+ 'DISTRIBUTION_ID': {
+ 'www.wagtail.io': 'frontend',
+ }
+ },
+ })
+ backends.get('cloudfront').purge('http://www.wagtail.io/home/events/christmas/')
+ backends.get('cloudfront').purge('http://torchbox.com/blog/')
+
+ _create_invalidation.assert_called_once_with('frontend', '/home/events/christmas/')
+
def test_multiple(self):
backends = get_backends(backend_settings={
'varnish': {