← Back to team overview

wordpress-charmers team mailing list archive

[Merge] ~tcuthbert/charm-k8s-wordpress:merge_stuff into charm-k8s-wordpress:master

 

Thomas Cuthbert has proposed merging ~tcuthbert/charm-k8s-wordpress:merge_stuff into charm-k8s-wordpress:master.

Requested reviews:
  Wordpress Charmers (wordpress-charmers)

For more details, see:
https://code.launchpad.net/~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress-1/+merge/394793
-- 
Your team Wordpress Charmers is requested to review the proposed merge of ~tcuthbert/charm-k8s-wordpress:merge_stuff into charm-k8s-wordpress:master.
diff --git a/.jujuignore b/.jujuignore
index 739b280..71eca3f 100644
--- a/.jujuignore
+++ b/.jujuignore
@@ -2,4 +2,6 @@
 /image
 *.py[cod]
 *.charm
+image-builder
+Dockerfile
 Makefile
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ace57f9
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,101 @@
+ARG DIST_RELEASE
+
+FROM ubuntu:${DIST_RELEASE}
+
+LABEL maintainer="wordpress-charmers@xxxxxxxxxxxxxxxxxxx"
+
+# HTTPS_PROXY used when we RUN curl to download Wordpress itself
+ARG BUILD_DATE
+ARG HTTPS_PROXY
+
+# Launchpad OCI image builds don't support dynamic arg parsing. Skip until
+# https://bugs.launchpad.net/launchpad/+bug/1902010 is resolved.
+#LABEL org.label-schema.build-date=${BUILD_DATE}
+
+ENV APACHE_CONFDIR=/etc/apache2
+ENV APACHE_ENVVARS=/etc/apache2/envvars
+
+# Avoid interactive prompts
+RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
+
+# Update all packages, remove cruft, install required packages, configure apache
+RUN apt-get update && apt-get -y dist-upgrade \
+        && apt-get --purge autoremove -y \
+        && apt-get install -y apache2 \
+            bzr \
+            curl \
+            git \
+            libapache2-mod-php \
+            libgmp-dev \
+            php \
+            php-curl \
+            php-gd \
+            php-gmp \
+            php-mysql \
+            php-symfony-yaml \
+            php-xml \
+            pwgen \
+            python3 \
+            python3-yaml \
+            ssl-cert \
+        && sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS" \
+        && . "$APACHE_ENVVARS" \
+        && for dir in "$APACHE_LOCK_DIR" "$APACHE_RUN_DIR" "$APACHE_LOG_DIR"; do rm -rvf "$dir"; mkdir -p "$dir"; chown "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$dir"; chmod 777 "$dir";  done \
+        && ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log" \
+        && ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log" \
+        && ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log" \
+        && chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR"
+
+# Configure PHP and apache2 - mod_php requires us to use mpm_prefork
+COPY ./image-builder/files/docker-php.conf $APACHE_CONFDIR/conf-available/docker-php.conf
+COPY ./image-builder/files/docker-php-swift-proxy.conf $APACHE_CONFDIR/conf-available/docker-php-swift-proxy.conf
+RUN a2enconf docker-php \
+    && a2dismod mpm_event \
+    && a2enmod headers \
+    && a2enmod mpm_prefork \
+    && a2enmod proxy \
+    && a2enmod proxy_http \
+    && a2enmod rewrite
+
+# Install the main Wordpress code, this will be our only site so /var/www/html is fine
+RUN curl -o wordpress.tar.gz -fSL "https://wordpress.org/latest.tar.gz"; \
+    && tar -xzf wordpress.tar.gz -C /usr/src/ \
+    && rm wordpress.tar.gz \
+    && chown -R www-data:www-data /usr/src/wordpress \
+    && rm -rf /var/www/html \
+    && mv /usr/src/wordpress /var/www/html
+
+COPY ./image-builder/files/ /files/
+COPY ./image-builder/fetcher.py .
+RUN mkdir -p /files/themes /files/plugins
+RUN ./fetcher.py
+# Copy our collected themes and plugins into the appropriate paths
+RUN cp -r /files/plugins/* /var/www/html/wp-content/plugins/
+RUN cp -r /files/themes/* /var/www/html/wp-content/themes/
+
+# wp-info.php contains template variables which our ENTRYPOINT script will populate
+RUN install -D /files/wp-info.php /var/www/html/wp-info.php
+RUN install -D /files/wp-config.php /var/www/html/wp-config.php
+RUN chown -R www-data:www-data /var/www/html
+
+# Copy our helper scripts and their wrapper into their own directory
+RUN install /files/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
+
+RUN install -t /srv/wordpress-helpers/ -D /files/_add_option.php \
+    /files/_enable_plugin.php \
+    /files/_get_option.php \
+    /files/plugin_handler.py \
+    /files/ready.sh
+
+# Make the wrapper executable
+RUN chmod 0755 /srv/wordpress-helpers/plugin_handler.py
+RUN chmod 0755 /srv/wordpress-helpers/ready.sh
+RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh
+
+RUN rm -r /files
+
+# Port 80 only, TLS will terminate elsewhere
+EXPOSE 80
+
+ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+CMD apachectl -D FOREGROUND
diff --git a/image-builder/Makefile b/image-builder/Makefile
new file mode 100644
index 0000000..4a7a40c
--- /dev/null
+++ b/image-builder/Makefile
@@ -0,0 +1,64 @@
+DIST_RELEASE ?= bionic
+VERSION ?= latest
+DOCKER_DEPS = \
+	apache2 \
+	curl \
+	libapache2-mod-php \
+	libgmp-dev \
+	php \
+	php-curl \
+	php-gd \
+	php-gmp \
+	php-mysql \
+	php-symfony-yaml \
+	php-xml \
+	pwgen \
+	python3 \
+	python3-yaml \
+	ssl-cert
+
+build-image:
+	@echo "Building the image."
+	@docker build \
+		--no-cache=true \
+		--build-arg BUILD_DATE=$$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
+		--build-arg PKGS_TO_INSTALL='$(DOCKER_DEPS)' \
+		--build-arg DIST_RELEASE=$(DIST_RELEASE) \
+		--build-arg HTTPS_PROXY=$(HTTPS_PROXY) \
+		-t wordpress:$(DIST_RELEASE)-$(VERSION) \
+		.
+
+build: lint deps fetch build-image
+	@echo "Pushing to the prod-is-external registry."
+	@docker tag wordpress:$(DIST_RELEASE)-$(VERSION) prod-is-external.docker-registry.canonical.com/wordpress:$(DIST_RELEASE)-$(VERSION)
+	@docker push prod-is-external.docker-registry.canonical.com/wordpress:$(DIST_RELEASE)-$(VERSION)
+
+deps:
+	@echo "Checking dependencies are present"
+	@command -v bzr >/dev/null 2>&1 || { echo "I require bzr but it's not installed. Aborting." >&2; exit 1; }
+	@command -v git >/dev/null 2>&1 || { echo "I require git but it's not installed. Aborting." >&2; exit 1; }
+
+fetch:
+	@echo "Fetching plugins and themes."
+	@tox -e fetch
+
+lint: clean
+	@echo "Running flake8"
+	@tox -e lint
+
+test: lint
+	@echo "Running unit tests"
+	@tox -e unit
+
+clean:
+	@echo "Cleaning files"
+	@rm -rf ./.tox
+	@rm -rf ./.pytest_cache
+	@rm -rf ./files/plugins/*
+	@rm -rf ./files/themes/*
+	@rm -rf ./files/__pycache__
+	@rm -rf ./tests/unit/__pycache__
+	@mkdir -p ./files/plugins
+	@mkdir -p ./files/themes
+
+.PHONY: build lint clean
diff --git a/image-builder/fetcher.py b/image-builder/fetcher.py
new file mode 100755
index 0000000..5ce0b50
--- /dev/null
+++ b/image-builder/fetcher.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+
+import os
+import shutil
+import subprocess
+import urllib.request
+import zipfile
+
+
+zip_plugins_to_get = {
+    # please keep these in alphabetical order
+    '404page',
+    'all-in-one-event-calendar',
+    'coschedule-by-todaymade',
+    'elementor',
+    'essential-addons-for-elementor-lite',
+    'favicon-by-realfavicongenerator',
+    'feedwordpress',
+    'fruitful-shortcodes',
+    'genesis-columns-advanced',
+    'line-break-shortcode',
+    'no-category-base-wpml',
+    'openid',
+    'post-grid',
+    'powerpress',
+    'redirection',
+    'relative-image-urls',
+    'rel-publisher',
+    'safe-svg',
+    'show-current-template',
+    'simple-301-redirects',
+    'simple-custom-css',
+    'social-media-buttons-toolbar',
+    'so-widgets-bundle',
+    'svg-support',
+    'syntaxhighlighter',
+    'wordpress-importer',
+    'wordpress-seo',
+    'wp-font-awesome',
+    'wp-lightbox-2',
+    'wp-markdown',
+    'wp-mastodon-share',
+    'wp-polls',
+    'wp-statistics',
+}
+
+branch_plugins_to_get = {
+    # please keep these in alphabetical order
+    'launchpad-integration': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress-launchpad-integration/+git/wordpress-launchpad-integration'},
+    'openstack-objectstorage': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/openstack-objectstorage-k8s'},
+    'teams-integration': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress-teams-integration/+git/wordpress-teams-integration'},
+    'xubuntu-team-members': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-plugin-xubuntu-team-members'},
+}
+
+branch_themes_to_get = {
+    # please keep these in alphabetical order
+    'fruitful': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-fruitful'},
+    'light-wordpress-theme': {'url': 'https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-webthemes/+git/light-wordpress-theme'},
+    'mscom': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-mscom'},
+    'twentyeleven': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-twentyeleven'},
+    'ubuntu-cloud-website': {'url': 'https://git.launchpad.net/~canonical-sysadmins/ubuntu-cloud-website/+git/ubuntu-cloud-website'},
+    'ubuntu-community': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-community'},
+    'ubuntu-community-wordpress-theme': {'url': 'https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-wordpress-theme/+git/ubuntu-community-wordpress-theme'},
+    'ubuntu-fi-new': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-fi'},
+    'ubuntu-light': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-light'},
+    'ubuntustudio-wp': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntustudio-wp'},
+    'wordpress_launchpad': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-launchpad'},
+    'xubuntu-theme': {'url': 'https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website'},
+}
+
+
+def get_plugins(zip_plugins, branch_plugins):
+    total_zips = len(zip_plugins)
+    current_zip = 0
+    for zip_plugin in zip_plugins:
+        current_zip = current_zip + 1
+        print('Downloading {} of {} zipped plugins: {} ...'.format(current_zip, total_zips, zip_plugin))
+        url = 'https://downloads.wordpress.org/plugin/{}.latest-stable.zip'.format(zip_plugin)
+        file_name = os.path.join(os.getcwd(), 'files/plugins', os.path.basename(url))
+        with urllib.request.urlopen(url) as response, open(file_name, 'wb') as out_file:
+            shutil.copyfileobj(response, out_file)
+        with zipfile.ZipFile(file_name, 'r') as zip_ref:
+            zip_ref.extractall(os.path.join(os.getcwd(), 'files/plugins'))
+        os.remove(file_name)
+
+    total_branches = len(branch_plugins)
+    current_branch = 0
+    for branch_plugin in branch_plugins:
+        current_branch = current_branch + 1
+        print('Downloading {} of {} branched plugins: {} ...'.format(current_branch, total_branches, branch_plugin))
+        url = branch_plugins[branch_plugin].get('url')
+        basename = os.path.basename(url)
+        if basename.startswith('lp:'):
+            basename = basename[3:]
+        if basename.startswith('wp-plugin-'):
+            basename = basename[10:]
+        dest = os.path.join(os.getcwd(), 'files/plugins', basename)
+        if url.startswith('lp:'):
+            cmd = ['bzr', 'branch', url, dest]
+        elif url.startswith('https://git'):
+            cmd = ['git', 'clone', url, dest]
+        else:
+            print("ERROR: Don't know how to clone {}".format(url))
+            exit(1)
+        _ = subprocess.check_output(cmd, universal_newlines=True, stderr=subprocess.STDOUT)
+
+
+def get_themes(branch_themes):
+    total_branches = len(branch_themes)
+    current_branch = 0
+    for branch_theme in branch_themes:
+        current_branch = current_branch + 1
+        print('Downloading {} of {} branched themes: {} ...'.format(current_branch, total_branches, branch_theme))
+        url = branch_themes[branch_theme].get('url')
+        basename = os.path.basename(url)
+        if basename.startswith('lp:'):
+            basename = basename[3:]
+        if basename.startswith('wp-theme-'):
+            basename = basename[9:]
+        dest = os.path.join(os.getcwd(), 'files/themes/', basename)
+        if url.startswith('lp:'):
+            cmd = ['bzr', 'branch', url, dest]
+        elif url.startswith('https://git'):
+            cmd = ['git', 'clone', url, dest]
+        else:
+            print("ERROR: Don't know how to clone {}".format(url))
+            exit(1)
+        _ = subprocess.check_output(cmd, universal_newlines=True, stderr=subprocess.STDOUT)
+
+
+if __name__ == '__main__':
+    get_plugins(zip_plugins_to_get, branch_plugins_to_get)
+    get_themes(branch_themes_to_get)
diff --git a/image-builder/files/_add_option.php b/image-builder/files/_add_option.php
new file mode 100644
index 0000000..cbfe8a8
--- /dev/null
+++ b/image-builder/files/_add_option.php
@@ -0,0 +1,11 @@
+<?php
+
+# PHP cli helper which adds new option to wp_settings table
+#
+# Example use:
+# php ./_add_option.php akismet_strictness 0
+
+@include "wp-config.php";
+@include_once "wp-includes/option.php";
+@include_once "wp-includes/functions.php";
+add_option($argv[1], maybe_unserialize($argv[2]));
diff --git a/image-builder/files/_enable_plugin.php b/image-builder/files/_enable_plugin.php
new file mode 100644
index 0000000..11ed3e7
--- /dev/null
+++ b/image-builder/files/_enable_plugin.php
@@ -0,0 +1,14 @@
+<?php
+
+# PHP cli helper which enables a plugin. Plugin name is in
+# format returned by _list_inactive_plugins.php helper
+#
+# Example use:
+# php ./_enable_plugin.php akismet/akismet.php openid/openid.php
+
+@include "wp-config.php";
+@include_once "wp-admin/includes/plugin.php";
+$result = activate_plugins(array_slice($argv, 1));
+if ( is_wp_error( $result ) ) {
+    throw new Exception($result);
+}
diff --git a/image-builder/files/_get_option.php b/image-builder/files/_get_option.php
new file mode 100644
index 0000000..4168301
--- /dev/null
+++ b/image-builder/files/_get_option.php
@@ -0,0 +1,15 @@
+<?php
+
+# PHP cli helper which returns option value if present
+#
+# Example use:
+# php ./_get_option.php akismet_strictness
+
+@include "wp-config.php";
+@include_once "wp-includes/option.php";
+@include_once "/usr/share/php/Symfony/Component/Yaml/autoload.php";
+
+use Symfony\Component\Yaml\Yaml;
+
+$value = get_option($argv[1]);
+print Yaml::dump($value);
diff --git a/image-builder/files/docker-entrypoint.sh b/image-builder/files/docker-entrypoint.sh
new file mode 100644
index 0000000..9cfdba9
--- /dev/null
+++ b/image-builder/files/docker-entrypoint.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -eu
+
+sed -i -e "s/%%%WORDPRESS_DB_HOST%%%/$WORDPRESS_DB_HOST/" /var/www/html/wp-info.php
+sed -i -e "s/%%%WORDPRESS_DB_NAME%%%/$WORDPRESS_DB_NAME/" /var/www/html/wp-info.php
+sed -i -e "s/%%%WORDPRESS_DB_USER%%%/$WORDPRESS_DB_USER/" /var/www/html/wp-info.php
+sed -i -e "s/%%%WORDPRESS_DB_PASSWORD%%%/$WORDPRESS_DB_PASSWORD/" /var/www/html/wp-info.php
+
+for key in AUTH_KEY SECURE_AUTH_KEY LOGGED_IN_KEY NONCE_KEY AUTH_SALT SECURE_AUTH_SALT LOGGED_IN_SALT NONCE_SALT;
+do
+    sed -i -e "s/%%%${key}%%%/$(printenv ${key})/" /var/www/html/wp-info.php
+done
+
+# If we have passed in SWIFT_URL, then append swift proxy config.
+[ -z "${SWIFT_URL-}" ] || a2enconf docker-php-swift-proxy
+
+nohup bash -c "/srv/wordpress-helpers/plugin_handler.py &"
+
+sed -i 's/max_execution_time = 30/max_execution_time = 300/' /etc/php/7.2/apache2/php.ini
+sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 10M/' /etc/php/7.2/apache2/php.ini
+
+exec "$@"
diff --git a/image-builder/files/docker-php-swift-proxy.conf b/image-builder/files/docker-php-swift-proxy.conf
new file mode 100644
index 0000000..871df91
--- /dev/null
+++ b/image-builder/files/docker-php-swift-proxy.conf
@@ -0,0 +1,4 @@
+PassEnv SWIFT_URL
+ProxyPass /wp-content/uploads/ ${SWIFT_URL}
+ProxyPassReverse /wp-content/uploads/ ${SWIFT_URL}
+Timeout 300
diff --git a/image-builder/files/docker-php.conf b/image-builder/files/docker-php.conf
new file mode 100644
index 0000000..00d1f36
--- /dev/null
+++ b/image-builder/files/docker-php.conf
@@ -0,0 +1,21 @@
+<FilesMatch \.php$>
+	SetHandler application/x-httpd-php
+</FilesMatch>
+
+<Location "/wp-admin">
+    Header Set Cache-Control "max-age=0, no-store"
+</Location>
+
+DirectoryIndex disabled
+DirectoryIndex index.php index.html
+
+<Directory /var/www/>
+	Options -Indexes
+	AllowOverride All
+	RewriteEngine On
+	RewriteBase /
+	RewriteRule ^index\.php$ - [L]
+	RewriteCond %{REQUEST_FILENAME} !-f
+	RewriteCond %{REQUEST_FILENAME} !-d
+	RewriteRule . /index.php [L]
+</Directory>
diff --git a/image-builder/files/plugin_handler.py b/image-builder/files/plugin_handler.py
new file mode 100644
index 0000000..94403ec
--- /dev/null
+++ b/image-builder/files/plugin_handler.py
@@ -0,0 +1,152 @@
+#!/usr/bin/python3
+
+import logging
+import os
+import subprocess
+import urllib.request
+from time import sleep
+from yaml import safe_load
+
+helpers_path = "/srv/wordpress-helpers"
+install_path = "/var/www/html"
+
+
+def call_php_helper(helper, stdin="", *args):
+    path = os.path.join(helpers_path, helper)
+    cmd = ["php", path]
+    cmd.extend([str(arg) for arg in args])
+    logging.info(cmd)
+    process = subprocess.Popen(
+        cmd,
+        cwd=install_path,
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    )
+    return process.communicate(stdin)[0]  # spit back stdout+stderr combined
+
+
+def enable_plugin(*plugins):
+    logging.info("Enabling plugins: {}".format(plugins))
+    logging.info(call_php_helper("_enable_plugin.php", "", *plugins))
+
+
+def get_option(key):
+    value = call_php_helper("_get_option.php", "", key)
+    return safe_load(value)
+
+
+def add_option(key, value):
+    # Ensure we don't overwrite settings
+    if not get_option(key):
+        logging.info("Adding option: {}".format(key))
+        call_php_helper("_add_option.php", "", key, value)
+    else:
+        logging.info('Option "{}" already in place, skipping.'.format(key))
+
+
+def encode_team_map(team_map):
+    # example: site-sysadmins=administrator,site-editors=editor,site-executives=editor
+    team_map_lines = []
+    i = 0
+    team_map_lines.append("a:{}:{{".format(len(team_map.split(","))))
+    for mapping in team_map.split(","):
+        i = i + 1
+        team, role = mapping.split("=", 2)
+        team_map_lines.append("i:{};".format(i))
+        team_map_lines.append('O:8:"stdClass":4:{')
+        team_map_lines.append('s:2:"id";')
+        team_map_lines.append("i:{};".format(i))
+        team_map_lines.append('s:4:"team";')
+        team_map_lines.append('s:{}:"{}";'.format(len(team), team))
+        team_map_lines.append('s:4:"role";')
+        team_map_lines.append('s:{}:"{}";'.format(len(role), role))
+        team_map_lines.append('s:6:"server";')
+        team_map_lines.append('s:1:"0";')
+        team_map_lines.append("}")
+    team_map_lines.append("}")
+
+    return "".join(team_map_lines)
+
+
+def enable_akismet(key):
+    enable_plugin("akismet/akismet.php")
+    add_option("akismet_strictness", "0")
+    add_option("akismet_show_user_comments_approved", "0")
+    add_option("wordpress_api_key", key)
+
+
+def enable_openid(team_map):
+    encoded_team_map = encode_team_map(team_map)
+    enable_plugin("openid/openid.php")
+    add_option("openid_required_for_registration", "1")
+    add_option("openid_teams_trust_list", encoded_team_map)
+
+
+def enable_swift(swift_config):
+    enable_plugin("openstack-objectstorage/objectstorage.php")
+    for k, v in swift_config.items():
+        add_option("object_storage_{}".format(k), v)
+
+
+def configure_wordpress():
+    url = "http://localhost";
+    sleep_time = 10
+    total_sleep_time = 0
+    max_sleep_time = 600
+    success = False
+    while success is not True:
+        if total_sleep_time > max_sleep_time:
+            return False
+        try:
+            response = urllib.request.urlopen(url, timeout=sleep_time)
+        except Exception:
+            logging.info("Waiting for Wordpress to accept connections")
+            sleep(sleep_time)
+            total_sleep_time = total_sleep_time + sleep_time
+        else:
+            if response.status == 200:
+                success = True
+            else:
+                logging.info(
+                    "Waiting for Wordpress to return HTTP 200 (got {})".format(
+                        response.status
+                    )
+                )
+                sleep(sleep_time)
+    return True
+
+
+if __name__ == "__main__":
+    logger = logging.getLogger(__name__)
+    logging.basicConfig(
+        filename="/var/log/wordpress-plugin-handler.log", level=logging.DEBUG
+    )
+
+    if configure_wordpress():
+        # create the file to satisfy the readinessProbe
+        open(os.path.join(helpers_path, '.ready'), 'a').close()
+
+        key = os.getenv("WP_PLUGIN_AKISMET_KEY")
+        if key:
+            enable_akismet(key)
+
+        team_map = os.getenv("WP_PLUGIN_OPENID_TEAM_MAP")
+        if team_map:
+            enable_openid(team_map)
+
+        swift_url = os.getenv("SWIFT_URL")
+        if swift_url:
+            swift_config = {}
+            swift_config['url'] = swift_url
+            swift_config['auth_url'] = os.getenv("SWIFT_AUTH_URL")
+            swift_config['bucket'] = os.getenv("SWIFT_BUCKET")
+            swift_config['password'] = os.getenv("SWIFT_PASSWORD")
+            swift_config['prefix'] = os.getenv("SWIFT_PREFIX")
+            swift_config['region'] = os.getenv("SWIFT_REGION")
+            swift_config['tenant'] = os.getenv("SWIFT_TENANT")
+            swift_config['username'] = os.getenv("SWIFT_USERNAME")
+            swift_config['copy_to_swift'] = os.getenv("SWIFT_COPY_TO_SWIFT")
+            swift_config['serve_from_swift'] = os.getenv("SWIFT_SERVE_FROM_SWIFT")
+            swift_config['remove_local_file'] = os.getenv("SWIFT_REMOVE_LOCAL_FILE")
+            enable_swift(swift_config)
diff --git a/image-builder/files/ready.sh b/image-builder/files/ready.sh
new file mode 100644
index 0000000..a676bc2
--- /dev/null
+++ b/image-builder/files/ready.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+READY=/srv/wordpress-helpers/.ready
+
+# This script is designed to be called by the Kubernetes
+# readinessProbe. If the WP plugins haven't been enabled
+# which we know due to the $READY file not existing then
+# return a failure, replace the shell with whatever curl
+# returns for checking that the website is alive.
+if [ -f "$READY" ]; then
+    exec /usr/bin/curl --silent http://localhost
+fi
+
+exit 1
diff --git a/image-builder/files/wp-config.php b/image-builder/files/wp-config.php
new file mode 100644
index 0000000..b47b161
--- /dev/null
+++ b/image-builder/files/wp-config.php
@@ -0,0 +1,52 @@
+<?php
+#
+#    "             "
+#  mmm   m   m   mmm   m   m
+#    #   #   #     #   #   #
+#    #   #   #     #   #   #
+#    #   "mm"#     #   "mm"#
+#    #             #
+#  ""            ""
+# This file is managed by Juju. Do not make local changes.
+#
+
+/* That's all, stop editing! Happy blogging. */
+
+/** Enable container debug level logging. **/
+if ( getenv("WORDPRESS_DEBUG") ) {
+        define( 'WP_DEBUG', true );
+        define( 'WP_DEBUG_DISPLAY', false );
+        define( 'WP_DEBUG_LOG', '/dev/stderr' );
+}
+
+/** Fixes for mixed content when WordPress is behind nginx TLS reverse proxy.
+ * https://ahenriksson.com/2020/01/27/how-to-set-up-wordpress-behind-a-reverse-proxy-when-using-nginx/
+ * */
+define('FORCE_SSL_ADMIN', true);
+if ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
+ $_SERVER['HTTPS']='on';
+
+/** Absolute path to the WordPress directory. */
+if ( !defined('ABSPATH') )
+        define('ABSPATH', dirname(__FILE__) . '/');
+
+/** Pull in the config information */
+require_once(ABSPATH . 'wp-info.php');
+
+/** Sets up WordPress vars and included files. */
+require_once(ABSPATH . 'wp-settings.php');
+
+/** Prevent wordpress from attempting to update and make external requests.
+ *
+ * Our firewalls do not allow WordPress to communicate externally to wordpress.org
+ * for auto-updates, this causes our kubernetes pods to timeout during initial configuration,
+ * preventing the site from ever becoming available.
+ * */
+define( 'AUTOMATIC_UPDATER_DISABLED', true );
+define( 'WP_AUTO_UPDATE_CORE', false );
+
+$http_host = $_SERVER['HTTP_HOST'];
+define('WP_HOME',"https://$http_host";);
+define('WP_SITEURL',"https://$http_host";);
+
+remove_filter('template_redirect', 'redirect_canonical');
diff --git a/image-builder/files/wp-info.php b/image-builder/files/wp-info.php
new file mode 100644
index 0000000..5a891a2
--- /dev/null
+++ b/image-builder/files/wp-info.php
@@ -0,0 +1,53 @@
+<?php
+#
+#    "             "
+#  mmm   m   m   mmm   m   m
+#    #   #   #     #   #   #
+#    #   #   #     #   #   #
+#    #   "mm"#     #   "mm"#
+#    #             #
+#  ""            ""
+# This file is managed by Juju. Do not make local changes.
+#
+
+// We have to cheat a little because frontend service can terminate SSL
+// If it does it should set X-Edge-Https header to "on" to tell us original
+// request came on https
+
+if (!empty($_SERVER['HTTP_X_EDGE_HTTPS']) && 'off' != $_SERVER['HTTP_X_EDGE_HTTPS']) {
+        $_SERVER['HTTPS'] = 'on';
+}
+
+if (!empty($_SERVER['HTTPS']) && 'off' != $_SERVER['HTTPS']) {
+    define('WP_PLUGIN_URL', 'https://' . $_SERVER['HTTP_HOST'] . '/wp-content/plugins');
+    define('WP_CONTENT_URL', 'https://' . $_SERVER['HTTP_HOST'] . '/wp-content');
+    define('WP_SITEURL', 'https://' . $_SERVER['HTTP_HOST']);
+    define('WP_URL', 'https://' . $_SERVER['HTTP_HOST']);
+    define('WP_HOME', 'https://' . $_SERVER['HTTP_HOST']);
+}
+else {
+    define('WP_PLUGIN_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/wp-content/plugins');
+    define('WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/wp-content');
+    define('WP_SITEURL', 'http://' . $_SERVER['HTTP_HOST']);
+    define('WP_URL', 'http://' . $_SERVER['HTTP_HOST']);
+    define('WP_HOME', 'http://' . $_SERVER['HTTP_HOST']);
+}
+
+define('DB_NAME', '%%%WORDPRESS_DB_NAME%%%');
+define('DB_USER', '%%%WORDPRESS_DB_USER%%%');
+define('DB_HOST', '%%%WORDPRESS_DB_HOST%%%');
+
+define('DB_PASSWORD', '%%%WORDPRESS_DB_PASSWORD%%%');
+
+define('WP_CACHE', true);
+
+define('AUTH_KEY', '%%%AUTH_KEY%%%');
+define('SECURE_AUTH_KEY', '%%%SECURE_AUTH_KEY%%%');
+define('LOGGED_IN_KEY', '%%%LOGGED_IN_KEY%%%');
+define('NONCE_KEY', '%%%NONCE_KEY%%%');
+define('AUTH_SALT', '%%%AUTH_SALT%%%');
+define('SECURE_AUTH_SALT', '%%%SECURE_AUTH_SALT%%%');
+define('LOGGED_IN_SALT', '%%%LOGGED_IN_SALT%%%');
+define('NONCE_SALT', '%%%NONCE_SALT%%%');
+
+$table_prefix  = 'wp_';
diff --git a/image-builder/tests/unit/requirements.txt b/image-builder/tests/unit/requirements.txt
new file mode 100644
index 0000000..092d46a
--- /dev/null
+++ b/image-builder/tests/unit/requirements.txt
@@ -0,0 +1,3 @@
+pytest
+pytest-cov
+PyYAML
diff --git a/image-builder/tests/unit/test_plugin_hander.py b/image-builder/tests/unit/test_plugin_hander.py
new file mode 100644
index 0000000..86f353b
--- /dev/null
+++ b/image-builder/tests/unit/test_plugin_hander.py
@@ -0,0 +1,37 @@
+import os
+import sys
+import unittest
+from unittest import mock
+
+sys.path.append(os.path.join("..", "..", os.path.dirname(__file__)))
+from files import plugin_handler  # NOQA: E402
+
+
+class testWrapper(unittest.TestCase):
+    def setUp(self):
+        self.maxDiff = None
+
+    @mock.patch("files.plugin_handler.logging")
+    def test_team_mapper(self, foo):
+        given = ",".join(
+            [
+                "canonical-sysadmins=administrator",
+                "canonical-website-editors=editor",
+                "canonical-website-admins=administrator",
+                "launchpad=editor",
+            ]
+        )
+        want = "".join(
+            [
+                """a:4:{i:1;O:8:"stdClass":4:{s:2:"id";i:1;s:4:"team";s:19:"canonical-sysadmins";""",
+                """s:4:"role";s:13:"administrator";s:6:"server";s:1:"0";}""",
+                """i:2;O:8:"stdClass":4:{s:2:"id";i:2;s:4:"team";s:25:"canonical-website-editors";""",
+                """s:4:"role";s:6:"editor";s:6:"server";s:1:"0";}""",
+                """i:3;O:8:"stdClass":4:{s:2:"id";i:3;s:4:"team";s:24:"canonical-website-admins";""",
+                """s:4:"role";s:13:"administrator";s:6:"server";s:1:"0";}""",
+                """i:4;O:8:"stdClass":4:{s:2:"id";i:4;s:4:"team";s:9:"launchpad";""",
+                """s:4:"role";s:6:"editor";s:6:"server";s:1:"0";}}""",
+            ]
+        )
+        got = plugin_handler.encode_team_map(given)
+        self.assertEqual(got, want)
diff --git a/image-builder/tox.ini b/image-builder/tox.ini
new file mode 100644
index 0000000..c03998c
--- /dev/null
+++ b/image-builder/tox.ini
@@ -0,0 +1,40 @@
+[tox]
+skipsdist=True
+envlist = build
+skip_missing_interpreters = True
+
+[testenv]
+basepython = python3
+setenv =
+  PYTHONPATH = .
+
+[testenv:black]
+commands = {envbindir}/black --skip-string-normalization --line-length=120 .
+deps = black
+
+[testenv:lint]
+commands = {envbindir}/flake8
+deps = flake8
+
+[testenv:fetch]
+commands = ./fetcher.py
+passenv = HTTP_PROXY HTTPS_PROXY
+setenv =
+  BZR_HOME = /tmp
+
+[testenv:unit]
+commands =
+    pytest {posargs:-v  --cov=. --cov-report=term-missing --cov-branch}
+deps = -r{toxinidir}/tests/unit/requirements.txt
+setenv =
+  PYTHONPATH={toxinidir}/lib
+  TZ=UTC
+
+[flake8]
+exclude =
+    .git,
+    __pycache__,
+    .tox,
+    files/
+max-line-length = 120
+max-complexity = 10

Follow ups