From 7a3bd57690ceb4e61615559014be4960afbf60fd Mon Sep 17 00:00:00 2001
From: Arie Peterson <arie@greenhost.nl>
Date: Thu, 8 Jul 2021 17:26:24 +0200
Subject: [PATCH] Cron overhaul, including backup routine

---
 README.md                       |  2 +
 templates/cron-schedule.yaml    | 17 ++++++
 templates/cronjob.yaml          | 94 ---------------------------------
 templates/rbac.yaml             | 29 ----------
 templates/statefulset.yaml      | 28 +++++++---
 values-local.yaml.example       |  5 +-
 values.yaml                     | 24 +--------
 wp-cli-docker/scripts/backup.sh | 17 ------
 8 files changed, 46 insertions(+), 170 deletions(-)
 create mode 100644 templates/cron-schedule.yaml
 delete mode 100644 templates/cronjob.yaml
 delete mode 100644 templates/rbac.yaml
 delete mode 100755 wp-cli-docker/scripts/backup.sh

diff --git a/README.md b/README.md
index 861420e..9303aa9 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,8 @@ $ kubectl logs <pod> -c init-wordpress
 Helm will set up the kubernetes pods that are needed to run your website:
 
 1. A WordPress pod that serves the site
+  - If you enabled `backup.enabled` or `wordpress.mu_cron.enabled`, this pod
+    will also contain a sidecar container that runs cron jobs.
 2. Two MariaDB pods running the database (master-slave setup by default, unless
    you changed this in `values-local.yaml`)
 3. If you configured Redis, a Redis pod is also set up
diff --git a/templates/cron-schedule.yaml b/templates/cron-schedule.yaml
new file mode 100644
index 0000000..88beda8
--- /dev/null
+++ b/templates/cron-schedule.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "wordpress.fullname" . }}-cron-schedule
+  labels:
+    app: {{ include "wordpress.name" . }}
+    chart: {{ include "wordpress.chart" . }}
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+data:
+  www-data: |-
+{{- if .Values.wordpress.mu_cron.enabled }}
+    {{ .Values.wordpress.mu_cron.cronjob.schedule }}  curl -s -w '%{http_code}' {{- if .Values.wordpress.mu_cron.cronjob.curlInsecure }} -k {{- end }} -L 'http://{{ include "wordpress.fullname" . }}:{{ .Values.service.port }}{{ .Values.wordpress.mu_cron.cronjob.path }}?doing_wp_cron&{{ required "Please set wordpress.mu_cron.secret to a random secret" .Values.wordpress.mu_cron.secret }}'
+{{- end }}
+{{- if .Values.backup.enabled }}
+    {{ .Values.backup.schedule }} cd /var/local/ansible && ansible-playbook backup.yml -e @secrets/secret-vars.yaml
+{{- end }}
diff --git a/templates/cronjob.yaml b/templates/cronjob.yaml
deleted file mode 100644
index d344d56..0000000
--- a/templates/cronjob.yaml
+++ /dev/null
@@ -1,94 +0,0 @@
-{{- if .Values.wordpress.mu_cron.enabled }}
-apiVersion: batch/v1beta1
-kind: CronJob
-metadata:
-  name: {{ template "wordpress.fullname" . }}
-  labels:
-    app: {{ include "wordpress.name" . }}
-    chart: {{ include "wordpress.chart" . }}
-    release: {{ .Release.Name }}
-    heritage: {{ .Release.Service }}
-  annotations:
-    {{- toYaml .Values.wordpress.mu_cron.cronjob.annotations | nindent 4 }}
-spec:
-  schedule: "{{ .Values.wordpress.mu_cron.cronjob.schedule }}"
-  concurrencyPolicy: Forbid
-  {{- with .Values.wordpress.mu_cron.cronjob.failedJobsHistoryLimit }}
-  failedJobsHistoryLimit: {{ . }}
-  {{- end }}
-  {{- with .Values.wordpress.mu_cron.cronjob.successfulJobsHistoryLimit }}
-  successfulJobsHistoryLimit: {{ . }}
-  {{- end }}
-  jobTemplate:
-    metadata:
-      labels:
-        app.kubernetes.io/name: {{ include "wordpress.name" . }}
-        app.kubernetes.io/managed-by: {{ .Release.Service }}
-    spec:
-      {{- with .Values.wordpress.mu_cron.cronjob.backoffLimit }}
-      backoffLimit: {{ . }}
-      {{- end }}
-      template:
-        metadata:
-          labels:
-            app.kubernetes.io/name: {{ include "wordpress.name" . }}
-            app.kubernetes.io/managed-by: {{ .Release.Service }}
-        spec:
-          # Set a custom service account which has access to the WordPress
-          # statefulset's state
-          serviceAccountName: {{ include "wordpress.fullname" . }}-cron
-          restartPolicy: Never
-          {{- if (default .Values.image.pullSecrets .Values.wordpress.mu_cron.cronjob.image.pullSecrets) }}
-          imagePullSecrets:
-          {{- range (default .Values.image.pullSecrets .Values.wordpress.mu_cron.cronjob.image.pullSecrets) }}
-            - name: {{ . }}
-          {{- end }}
-          {{- end }}
-          containers:
-            - name: {{ .Chart.Name }}-cron-caller
-              image: "{{ default .Values.image.repository .Values.wordpress.mu_cron.cronjob.image.repository }}:{{ default .Values.image.tag .Values.wordpress.mu_cron.cronjob.image.tag }}"
-              imagePullPolicy: {{ default .Values.image.pullPolicy .Values.wordpress.mu_cron.cronjob.image.pullPolicy }}
-              command: [ "/bin/bash" ]
-              args:
-                - -c
-                - |
-                  # NOTE: we use "{{` ... `}}" to make sure the curly braces are not templated by Helm. Returns <#readyReplicas>,<#replicasWanted>
-                  equation=$(kubectl get statefulset {{ include "wordpress.fullname" . }} --template '{{ `{{.status.readyReplicas}},{{.status.replicas}}` }}')
-                  # Make sure kubectl command did not fail
-                  if [ $? -ne 0 ]; then
-                      echo "Kubernetes command failed";
-                      exit 2;
-                  fi
-                  # Check if part before comma and after comma are equal
-                  if [[ "${equation%,*}" == "${equation#*,}" ]]; then
-                    output=$(curl -s -w '%{http_code}' {{- if .Values.wordpress.mu_cron.cronjob.curlInsecure }} -k {{- end }} -L 'http://{{ include "wordpress.fullname" . }}:{{ .Values.service.port }}{{ .Values.wordpress.mu_cron.cronjob.path }}?doing_wp_cron&{{ required "Please set wordpress.mu_cron.secret to a random secret" .Values.wordpress.mu_cron.secret }}')
-                    # Note that if the output is 200invalid secret string, you
-                    # need to provide the correct secret!
-                    if [[ "$output" == "200" ]]; then
-                      echo "success";
-                      exit 0
-                    else
-                      echo "failed with output '$output'";
-                      exit 1
-                    fi
-                  fi
-                  # If we reach this point, the statefulset is not ready yet
-                  echo "Service is not ready, doing nothing"
-                  exit 0
-              {{- with .Values.wordpress.mu_cron.cronjob.resources }}
-              resources:
-                {{ toYaml . | nindent 16 }}
-              {{- end }}
-    {{- with (default .Values.nodeSelector .Values.wordpress.mu_cron.cronjob.nodeSelector) }}
-          nodeSelector:
-{{ toYaml . | indent 12 }}
-    {{- end }}
-    {{- with (default .Values.affinity .Values.wordpress.mu_cron.cronjob.affinity) }}
-          affinity:
-{{ toYaml . | indent 12 }}
-    {{- end }}
-    {{- with (default .Values.tolerations .Values.wordpress.mu_cron.cronjob.tolerations) }}
-          tolerations:
-{{ toYaml . | indent 12 }}:
-    {{- end }}
-{{- end }}
diff --git a/templates/rbac.yaml b/templates/rbac.yaml
deleted file mode 100644
index 78467f2..0000000
--- a/templates/rbac.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-{{- if .Values.wordpress.mu_cron.enabled }}
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: get-{{ include "wordpress.fullname" . }}-statefulset
-rules:
-  - apiGroups: ["apps"]
-    resources: ["statefulsets"]
-    resourceNames: [{{ include "wordpress.fullname" . }}]
-    verbs: ["get"]
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: read-{{ include "wordpress.fullname" . }}-statefulset
-subjects:
-  - kind: ServiceAccount
-    name: {{ include "wordpress.fullname" . }}-cron
-    namespace: {{ .Release.Namespace }}
-roleRef:
-  kind: Role
-  name: get-{{ include "wordpress.fullname" . }}-statefulset
-  apiGroup: rbac.authorization.k8s.io
----
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: {{ include "wordpress.fullname" . }}-cron
-{{- end }}
diff --git a/templates/statefulset.yaml b/templates/statefulset.yaml
index b0edb70..f2666d5 100644
--- a/templates/statefulset.yaml
+++ b/templates/statefulset.yaml
@@ -128,12 +128,22 @@ spec:
               subPath: .htaccess
           resources:
 {{ toYaml .Values.resources | indent 12 }}
-        {{- if .Values.backup.enabled }}
-        - name: {{ .Chart.Name }}-backup
+        {{- if or .Values.backup.enabled .Values.wordpress.mu_cron.enabled }}
+        - name: {{ .Chart.Name }}-cron
           image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
           imagePullPolicy: {{ .Values.initImage.pullPolicy }}
           command:
-            - "/var/local/ansible/scripts/backup.sh"
+            # Busybox's cron daemon.
+            - "crond"
+            # Run in foreground.
+            - "-f"
+            # Log to stderr, with level 6.
+            - "-d"
+            - "6"
+          # `crond` must be run as root, so we override the pod's security context.
+          securityContext:
+            runAsNonRoot: false
+            runAsUser: 0
           env:
             - name: WORDPRESS_DB_HOST
               value: {{ .Release.Name }}-database
@@ -148,9 +158,6 @@ spec:
               value: {{ .Values.database.db.name }}
             - name: WORDPRESS_TABLE_PREFIX
               value: {{ .Values.wordPressTablePrefix }}
-            - name: BACKUP_INTERVAL_SECONDS
-              # A day's worth of seconds.
-              value: {{ .Values.backup.intervalSeconds | quote }}
           volumeMounts:
             - name: {{ include "wordpress.name" . }}-wp-storage
               mountPath: /var/www/html
@@ -163,6 +170,8 @@ spec:
               subPath: main.yml
             - name: ansible-secrets
               mountPath: /var/local/ansible/secrets
+            - name: cron-schedule
+              mountPath: /etc/crontabs/
             {{- if .Values.backup.sshPrivateKey }}
             - name: ssh-private-key
               mountPath: /var/local/ssh-private-key
@@ -223,3 +232,10 @@ spec:
         - name: htuploads
           configMap:
             name: {{ include "wordpress.fullname" . }}-htuploads
+        - name: cron-schedule
+          configMap:
+            name: {{ include "wordpress.fullname" . }}-cron-schedule
+            items:
+            - key: www-data
+              # This is the name of the user with id 33 in the cli container.
+              path: "xfs"
diff --git a/values-local.yaml.example b/values-local.yaml.example
index dabad8e..396de73 100644
--- a/values-local.yaml.example
+++ b/values-local.yaml.example
@@ -177,8 +177,9 @@ redis:
 #   # If isDate is set to false then backup names are a 2 week cycle of A(day number) or B(day number)
 #   # A monthly database backup and monthly wordpress manifest are always made with monthnumber prefix 
 #   isDate: true
-#   # The interval at which backups occur. Defaults to 86400 seconds (24 hours)
-#   intervalSeconds:
+#   # The cron schedule that determines when backups are made.
+#   # Run at 3:37 every day.
+#   schedule: "37 3 * * *"
 # It's advisable to set resource limits to prevent your K8s cluster from
 # crashing
 # resources:
diff --git a/values.yaml b/values.yaml
index d1bda22..5acf023 100644
--- a/values.yaml
+++ b/values.yaml
@@ -169,32 +169,11 @@ wordpress:
     slug: wp-cron-control
     version: cecdec276f086aafb6765ea77ce8d2ce0948e01c
     phpfile: wp-cron-control.php
-    # Optional annotations to add to the cronjob object
     cronjob:
-      image:
-        repository: bitnami/kubectl
-        tag: 1.18
-        pullPolicy: IfNotPresent
       # Every 3 minutes
       schedule: "*/3 * * * *"
       # We use the internal DNS, so there is no TLS certificate
       curlInsecure: true
-      # resources:
-        # We usually recommend not to specify default resources and to leave this as a conscious
-        # choice for the user. This also increases chances charts run on environments with little
-        # resources, such as Minikube. If you do want to specify resources, uncomment the following
-        # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
-        # limits:
-        #  cpu: 100m
-        #  memory: 128Mi
-        # requests:
-        #  cpu: 100m
-        #  memory: 128Mi
-      annotations: {}
-      failedJobsHistoryLimit: 3
-      successfulJobsHistoryLimit: 1
-      # Maximum number of times a failing Job is retried.
-      backoffLimit: 1
       # Path to the cronjob PHP file (gets appended to the wordpress URL)
       path: /wp-cron.php
       # You can override this key for the cronjobs. If you don't change the
@@ -336,7 +315,8 @@ redis:
 
 backup:
   enabled: false
-  intervalSeconds: 86400
+  # Daily at 2:00.
+  schedule: "0 2 * * *"
   isDate: true
 
 wpSalts: {}
diff --git a/wp-cli-docker/scripts/backup.sh b/wp-cli-docker/scripts/backup.sh
deleted file mode 100755
index 341c321..0000000
--- a/wp-cli-docker/scripts/backup.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-
-backupCommand="ansible-playbook backup.yml -e @secrets/secret-vars.yaml"
-
-while true
-do
-    date
-    echo "Waiting for $BACKUP_INTERVAL_SECONDS seconds before starting next backup."
-    sleep $BACKUP_INTERVAL_SECONDS
-    $backupCommand
-    exitCode=$?
-    if [ $exitCode -ne 0 ]
-    then
-        echo "Backup failed, exiting!"
-        exit $exitCode
-    fi
-done
-- 
GitLab