diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..b4710f9f4e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.build/
+
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..2abd919ae4
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "k8s/vendor/marketplace-tools"]
+ path = k8s/vendor/marketplace-tools
+ url = https://github.com/GoogleCloudPlatform/marketplace-k8s-app-tools
diff --git a/k8s/vendor/marketplace-tools b/k8s/vendor/marketplace-tools
new file mode 160000
index 0000000000..ada1168d43
--- /dev/null
+++ b/k8s/vendor/marketplace-tools
@@ -0,0 +1 @@
+Subproject commit ada1168d43e5b795a66798476c0aff3ad726dc9d
diff --git a/k8s/wordpress/Makefile b/k8s/wordpress/Makefile
new file mode 100644
index 0000000000..ab7b89518b
--- /dev/null
+++ b/k8s/wordpress/Makefile
@@ -0,0 +1,34 @@
+APP_NAME = wordpress
+AGENT_REPO ?= ../../../ubbagent
+
+include ../vendor/marketplace-tools/gcloud.Makefile
+
+include ../vendor/marketplace-tools/crd.Makefile
+include ../vendor/marketplace-tools/app.Makefile
+
+app/build:: .build/deployer .build/init .build/marketplace-ubbagent
+
+.build/deployer: deployer/* manifest/* $(MARKETPLACE_BASE_BUILD)/deployer-kubectl $(MARKETPLACE_BASE_BUILD)/controller $(APP_BUILD)/registry_prefix | app/setup
+ docker build \
+ --build-arg MARKETPLACE_REGISTRY="$(MARKETPLACE_REGISTRY)" \
+ --tag "$(APP_DEPLOYER_IMAGE)" \
+ -f deployer/Dockerfile \
+ .
+ gcloud docker -- push "$(APP_DEPLOYER_IMAGE)"
+ @touch "$@"
+
+.build/init: init/* $(APP_BUILD)/registry_prefix | app/setup
+ cd init \
+ && docker build \
+ --tag "$(APP_REGISTRY)/init" \
+ .
+ gcloud docker -- push "$(APP_REGISTRY)/init"
+ @touch "$@"
+
+.build/marketplace-ubbagent: $(AGENT_REPO)/* $(AGENT_REPO)/**/* $(MARKETPLACE_BASE_BUILD)/registry_prefix | base/setup
+ cd "$(AGENT_REPO)" \
+ && docker build \
+ --tag "$(MARKETPLACE_REGISTRY)/ubbagent" \
+ .
+ gcloud docker -- push "$(MARKETPLACE_REGISTRY)/ubbagent"
+ @touch "$@"
diff --git a/k8s/wordpress/README.md b/k8s/wordpress/README.md
new file mode 100644
index 0000000000..fad9a2ad7d
--- /dev/null
+++ b/k8s/wordpress/README.md
@@ -0,0 +1,62 @@
+*This directory contains an example Kubernetes application (app) based on
+WordPress for the purpose of demonstrating app integration with
+Google Cloud Marketplace. **Not intended for actual use!***
+
+*Content below is intended as a template for end-user documentation. Work in
+progress.*
+
+# Overview
+
+WordPress is a free and open-source content management system (CMS) based on PHP
+and MySQL...
+
+# Installation
+
+## Quick install with Google Cloud Marketplace
+
+Get up and running with a few clicks! Install this WordPress app to a
+Google Kubernetes Engine cluster using Google Cloud Marketplace. Follow the
+on-screen instructions:
+*TODO: link to solution details page*
+
+## Command line instructions
+
+Follow these instructions to install WordPress from the command line.
+
+### Prerequisites
+
+- Setup cluster
+ - Permissions
+- Setup kubectl
+- Install Application Resource
+- Acquire License
+
+*TODO: add details above*
+
+### Commands
+
+Set environment variables (modify if necessary):
+```
+export APP_INSTANCE_NAME=wordpress-1
+export NAMESPACE=default
+```
+
+Expand manifest template:
+```
+cat manifest/* | envsubst > expanded.yaml
+```
+
+Run kubectl:
+```
+kubectl apply -f expanded.yaml
+```
+
+*TODO: fix instructions*
+
+# Backups
+
+*TODO: instructions for backups*
+
+# Upgrades
+
+*TODO: instructions for upgrades*
diff --git a/k8s/wordpress/deployer/Dockerfile b/k8s/wordpress/deployer/Dockerfile
new file mode 100644
index 0000000000..f2bf78f0d6
--- /dev/null
+++ b/k8s/wordpress/deployer/Dockerfile
@@ -0,0 +1,4 @@
+ARG MARKETPLACE_REGISTRY
+FROM ${MARKETPLACE_REGISTRY}/deployer_kubectl_base
+
+ADD manifest/* /data/manifest/
diff --git a/k8s/wordpress/init/Dockerfile b/k8s/wordpress/init/Dockerfile
new file mode 100644
index 0000000000..903bdecf13
--- /dev/null
+++ b/k8s/wordpress/init/Dockerfile
@@ -0,0 +1,7 @@
+FROM launcher.gcr.io/google/debian9
+
+COPY metering.php.tmpl /
+COPY agent-config.yaml /
+COPY init.sh /
+
+CMD ["/init.sh"]
diff --git a/k8s/wordpress/init/agent-config.yaml b/k8s/wordpress/init/agent-config.yaml
new file mode 100644
index 0000000000..f38bafdb7c
--- /dev/null
+++ b/k8s/wordpress/init/agent-config.yaml
@@ -0,0 +1,25 @@
+metrics:
+- name: requests
+ type: int
+ aggregation:
+ bufferSeconds: 60
+ endpoints:
+ - name: disk
+- name: instance_time
+ type: int
+ aggregation:
+ bufferSeconds: 60
+ endpoints:
+ - name: disk
+endpoints:
+- name: disk
+ disk:
+ reportDir: $AGENT_REPORT_DIR
+ expireSeconds: 3600
+sources:
+- name: instance_time
+ heartbeat:
+ metric: instance_time
+ intervalSeconds: 10
+ value:
+ int64Value: 10
diff --git a/k8s/wordpress/init/init.sh b/k8s/wordpress/init/init.sh
new file mode 100755
index 0000000000..7d0502601c
--- /dev/null
+++ b/k8s/wordpress/init/init.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+#
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e
+
+if [ -z "$AGENT_LOCAL_PORT" ]; then
+ echo "AGENT_LOCAL_PORT environment variable must be set"
+ exit 1
+fi
+
+if [ ! -d "/var/www/html" ]; then
+ echo "/var/www/html directory must be mounted"
+ exit 1
+fi
+
+if [ ! -d "/etc/ubbagent" ]; then
+ echo "/etc/ubbagent directory must be mounted"
+ exit 1
+fi
+
+# Expand and copy Wordpress metering plugin.
+mkdir -p /var/www/html/wp-content/mu-plugins
+sed "s/%%AGENT_LOCAL_PORT%%/$AGENT_LOCAL_PORT/g" /metering.php.tmpl > /var/www/html/wp-content/mu-plugins/metering.php
+
+# Copy the metering agent config.
+cp /agent-config.yaml /etc/ubbagent/config.yaml
diff --git a/k8s/wordpress/init/metering.php.tmpl b/k8s/wordpress/init/metering.php.tmpl
new file mode 100644
index 0000000000..2456f05a97
--- /dev/null
+++ b/k8s/wordpress/init/metering.php.tmpl
@@ -0,0 +1,159 @@
+ 'requests',
+ 'startTime' => $now,
+ 'endTime' => $now,
+ 'value' => array( 'int64Value' => 1 )
+ );
+ $args = array(
+ 'headers' => array( 'Content-Type' => 'application/json' ),
+ 'body' => json_encode( $body )
+ );
+
+ $response = wp_remote_post( report_url(), $args);
+ $response_code = wp_remote_retrieve_response_code( $response );
+
+ return $response_code == 200;
+}
+
+/**
+ * A 'wp' action that reports requests for singular post/page views.
+ */
+function handle_view() {
+ if (is_singular()) {
+ report_request();
+ }
+}
+
+/**
+ * Renders the Metering Status widget.
+ */
+function usage_metering_status_widget_display() {
+ $response = wp_remote_get(status_url());
+ $error = '';
+ $status = NULL;
+ if (is_wp_error($request)) {
+ $error = 'Cannot reach agent';
+ } else {
+ $body = wp_remote_retrieve_body($response);
+ $status = json_decode($body, true);
+ if ($status["currentFailureCount"] > 0) {
+ $error = 'Agent is failing to report usage';
+ }
+ $lastSuccess = DateTime::createFromFormat('Y-m-d\TH:i:s+', $status['lastReportSuccess']);
+ $now = new DateTime();
+ $successInterval = $now->diff($lastSuccess)->format('%ad %hh %im %ss');
+ }
+
+ if ($error == '') {
+ $current_status = 'SUCCESS';
+ } else {
+ $current_status = "FAILURE: $error";
+ }
+
+ // Widget HTML
+ ?>
+
Reporting Health
+ = $current_status ?>
+
+ if ($status != null) : ?>
+ Status Data
+
+
+ Last report success |
+ = $successInterval ?> ago |
+
+
+ Current failures |
+ = $status['currentFailureCount'] ?> |
+
+
+ Total failures |
+ = $status['totalFailureCount'] ?> |
+
+
+ endif; ?>
+
+}
+
+/**
+ * Registers the Metering Status widget.
+ */
+function register_usage_metering_status_widget() {
+ wp_add_dashboard_widget(
+ 'usage_metering_status_widget',
+ 'Usage Metering Status',
+ 'usage_metering_status_widget_display'
+ );
+}
+
+// Action registrations.
+add_action('wp', 'handle_view');
+add_action('wp_dashboard_setup', 'register_usage_metering_status_widget');
+
+?>
diff --git a/k8s/wordpress/manifest/controller.yaml.template b/k8s/wordpress/manifest/controller.yaml.template
new file mode 100644
index 0000000000..2347535b2e
--- /dev/null
+++ b/k8s/wordpress/manifest/controller.yaml.template
@@ -0,0 +1,76 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: $APP_INSTANCE_NAME-controller-sa
+ namespace: $NAMESPACE
+---
+# This role is what an application specific controller would
+# typically need.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: $APP_INSTANCE_NAME-controller-approle
+ namespace: $NAMESPACE
+rules:
+- apiGroups: ['marketplace.cloud.google.com']
+ resources: ['applications']
+ verbs: ['*']
+- apiGroups: ['']
+ resources: ['events']
+ verbs: ['*']
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: $APP_INSTANCE_NAME-controller-apprb
+ namespace: $NAMESPACE
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: $APP_INSTANCE_NAME-controller-approle
+subjects:
+- kind: ServiceAccount
+ name: $APP_INSTANCE_NAME-controller-sa
+---
+# We need this binding because the controller currently
+# needs to assign owner references. This functionality
+# will be replaced by the CRD controller.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: $APP_INSTANCE_NAME-controller-editrb
+ namespace: $NAMESPACE
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ # This role is intended for the deployer container. See comment
+ # above about this binding.
+ name: $APP_INSTANCE_NAME-deployer-role
+subjects:
+- kind: ServiceAccount
+ name: $APP_INSTANCE_NAME-controller-sa
+---
+apiVersion: apps/v1beta2
+kind: Deployment
+metadata:
+ name: $APP_INSTANCE_NAME-controller
+ labels: &MysqlDeploymentLabels
+ app: $APP_INSTANCE_NAME
+ component: wordpress-controller
+spec:
+ replicas: 1
+ selector:
+ matchLabels: *MysqlDeploymentLabels
+ template:
+ metadata:
+ labels: *MysqlDeploymentLabels
+ spec:
+ serviceAccountName: $APP_INSTANCE_NAME-controller-sa
+ containers:
+ - image: $MARKETPLACE_REGISTRY/controller
+ name: controller
+ env:
+ - name: APP_INSTANCE_NAME
+ value: "$APP_INSTANCE_NAME"
+ - name: NAMESPACE
+ value: "$NAMESPACE"
diff --git a/k8s/wordpress/manifest/manifests.yaml.template b/k8s/wordpress/manifest/manifests.yaml.template
new file mode 100644
index 0000000000..e64763817b
--- /dev/null
+++ b/k8s/wordpress/manifest/manifests.yaml.template
@@ -0,0 +1,223 @@
+---
+apiVersion: marketplace.cloud.google.com/v1
+kind: Application
+metadata:
+ annotations:
+ marketplace.cloud.google.com: |
+ {"name":"Wordpress","version":"v0.1","description":"The most popular blogging platform","url":"wordpress.org","tagline":"wordpress blog","support_info":"Community support","documentations":[{"url":"https://codex.wordpress.org/Getting_Started_with_WordPress","title":"Getting Started","description":"A quick walkthrough"}]}
+ ApplicationStatus:
+ ready: true
+ generation: 0
+ initializers: null
+ name: "$APP_INSTANCE_NAME"
+ namespace: "$NAMESPACE"
+spec:
+ # TODO(huyhuynh): This list "replaces" the original list created
+ # by the up script. As a result, the deployer service account, for example,
+ # is no longer owned by this application. We have to hackily list such
+ # non-application resources here.
+ # To correctly fix this, we need to use a proper merge stategy. The idea is
+ # that kubectl wouldn't touch the components that it doesn't manage.
+ #
+ # TODO(huyhuynh): Need a good way to do this. Listing the components
+ # manually like this is error-prone and especially hard when there are
+ # many manifest files or when they are modified.
+ components:
+ - $APP_INSTANCE_NAME-controller:
+ kind: Deployment
+ - $APP_INSTANCE_NAME-controller-sa:
+ kind: ServiceAccount
+ - $APP_INSTANCE_NAME-controller-approle:
+ kind: Role
+ - $APP_INSTANCE_NAME-controller-apprb:
+ kind: RoleBinding
+ - $APP_INSTANCE_NAME-controller-editrb:
+ kind: RoleBinding
+ - $APP_INSTANCE_NAME-mysql:
+ kind: Deployment
+ - $APP_INSTANCE_NAME-mysql-pvc:
+ kind: PersistentVolumeClaim
+ - $APP_INSTANCE_NAME-mysql-svc:
+ kind: Service
+ - $APP_INSTANCE_NAME-wordpress:
+ kind: Deployment
+ - $APP_INSTANCE_NAME-wordpress-pvc:
+ kind: PersistentVolumeClaim
+ - $APP_INSTANCE_NAME-wordpress-svc:
+ kind: Service
+ # The following shouldn't be listed here. They are not part of the
+ # application itself, and the solution crafter shouldn't have to worry
+ # about them. See TODOs above.
+ - $APP_INSTANCE_NAME-deployer:
+ kind: Job
+ - $APP_INSTANCE_NAME-deployer-sa:
+ kind: ServiceAccount
+ - $APP_INSTANCE_NAME-deployer-role:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ - $APP_INSTANCE_NAME-deployer-rb:
+ apiGroup: rbac.authorization.k8s.io
+ kind: RoleBinding
+---
+# TODO(huyhuynh): Change this to using StatefulSet
+apiVersion: apps/v1beta2
+kind: Deployment
+metadata:
+ name: $APP_INSTANCE_NAME-mysql
+ labels: &MysqlDeploymentLabels
+ app: $APP_INSTANCE_NAME
+ component: wordpress-mysql
+spec:
+ replicas: 1
+ selector:
+ matchLabels: *MysqlDeploymentLabels
+ template:
+ metadata:
+ labels: *MysqlDeploymentLabels
+ spec:
+ containers:
+ - image: launcher.gcr.io/google/mysql5
+ name: mysql
+ env:
+ - name: "MYSQL_ROOT_PASSWORD"
+ value: "example-password"
+ volumeMounts:
+ - name: data
+ mountPath: /var/lib/mysql
+ subPath: data
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: $APP_INSTANCE_NAME-mysql-pvc
+---
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+ name: $APP_INSTANCE_NAME-mysql-pvc
+ labels:
+ app: $APP_INSTANCE_NAME
+ component: wordpress-mysql
+spec:
+ accessModes: [ReadWriteOnce]
+ storageClassName: standard
+ resources:
+ requests:
+ storage: 5Gi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: $APP_INSTANCE_NAME-mysql-svc
+ labels:
+ app: $APP_INSTANCE_NAME
+ component: wordpress-mysql
+spec:
+ ports:
+ - port: 3306
+ selector:
+ app: $APP_INSTANCE_NAME
+ component: wordpress-mysql
+ clusterIP: None
+---
+# TODO(huyhuynh): Change this to using StatefulSet
+apiVersion: apps/v1beta2
+kind: Deployment
+metadata:
+ name: $APP_INSTANCE_NAME-wordpress
+ labels: &WordpressDeploymentLabels
+ app: $APP_INSTANCE_NAME
+ component: wordpress-webserver
+spec:
+ replicas: 1
+ selector:
+ matchLabels: *WordpressDeploymentLabels
+ template:
+ metadata:
+ labels: *WordpressDeploymentLabels
+ spec:
+ initContainers:
+ - image: $REGISTRY/init
+ name: wordpress-init
+ env:
+ - name: AGENT_LOCAL_PORT
+ value: "6080"
+ volumeMounts:
+ - name: data
+ mountPath: /var/www/html
+ subPath: wp
+ - name: ubbagent-config
+ mountPath: /etc/ubbagent
+ containers:
+ - image: launcher.gcr.io/google/wordpress4-php5-apache
+ name: wordpress
+ env:
+ - name: WORDPRESS_DB_HOST
+ value: $APP_INSTANCE_NAME-mysql-svc
+ # TODO(huyhuynh): Use secrets.
+ - name: WORDPRESS_DB_PASSWORD
+ value: example-password
+ - name: WORDPRESS_DB_USER
+ value: root
+ ports:
+ - name: http
+ containerPort: 80
+ volumeMounts:
+ - name: data
+ mountPath: /var/www/html
+ subPath: wp
+ - image: $MARKETPLACE_REGISTRY/ubbagent
+ name: ubbagent
+ env:
+ - name: AGENT_CONFIG_FILE
+ value: /etc/ubbagent/config.yaml
+ - name: AGENT_LOCAL_PORT
+ value: "6080"
+ - name: AGENT_STATE_DIR
+ value: /var/lib/ubbagent
+ - name: AGENT_REPORT_DIR
+ value: /var/lib/ubbagent/reports
+ volumeMounts:
+ - name: ubbagent-config
+ mountPath: /etc/ubbagent
+ - name: ubbagent-state
+ mountPath: /var/lib/ubbagent
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: $APP_INSTANCE_NAME-wordpress-pvc
+ - name: ubbagent-config
+ emptyDir: {}
+ # TODO(volkman): state directory should maybe be on a PV.
+ - name: ubbagent-state
+ emptyDir: {}
+---
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+ name: $APP_INSTANCE_NAME-wordpress-pvc
+ labels:
+ app: $APP_INSTANCE_NAME
+ component: wordpress-webserver
+spec:
+ accessModes: [ReadWriteOnce]
+ storageClassName: standard
+ resources:
+ requests:
+ storage: 5Gi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: $APP_INSTANCE_NAME-wordpress-svc
+ labels:
+ app: $APP_INSTANCE_NAME
+ component: wordpress-webserver
+spec:
+ ports:
+ - name: http
+ port: 80
+ targetPort: http
+ selector:
+ app: $APP_INSTANCE_NAME
+ component: wordpress-webserver
+ type: LoadBalancer