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': {