feat: support existing persistent volume (#105)

Statefulset `volumeClaimTemplates` create new volumes and `PersistentVolumeClaims` which cannot be mutated. This adds the ability to declare a `persistentVolume` as existing and provide the required PV and PVC data for binding a volume to the statefulset. This is especially useful for restoring backup snapshots during disaster recovery efforts, but could also be used for initial data migrations or other scenarios where users would like to deploy this chart with existing CouchDB data.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1377554
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.swp
diff --git a/couchdb/Chart.yaml b/couchdb/Chart.yaml
index e2bda0d..0cdd6ed 100644
--- a/couchdb/Chart.yaml
+++ b/couchdb/Chart.yaml
@@ -1,6 +1,6 @@
 apiVersion: v1
 name: couchdb
-version: 4.1.1
+version: 4.2.0
 appVersion: 3.2.1
 description: A database featuring seamless multi-master sync, that scales from
   big data to mobile, with an intuitive HTTP/JSON API and designed for
diff --git a/couchdb/README.md b/couchdb/README.md
index 2cb7c73..3e6f468 100644
--- a/couchdb/README.md
+++ b/couchdb/README.md
@@ -1,6 +1,6 @@
 # CouchDB
 
-![Version: 4.1.1](https://img.shields.io/badge/Version-4.1.1-informational?style=flat-square) ![AppVersion: 3.2.1](https://img.shields.io/badge/AppVersion-3.2.1-informational?style=flat-square)
+![Version: 4.2.0](https://img.shields.io/badge/Version-4.2.0-informational?style=flat-square) ![AppVersion: 3.2.1](https://img.shields.io/badge/AppVersion-3.2.1-informational?style=flat-square)
 
 Apache CouchDB is a database featuring seamless multi-master sync, that scales
 from big data to mobile, with an intuitive HTTP/JSON API and designed for
@@ -18,7 +18,7 @@
 ```bash
 $ helm repo add couchdb https://apache.github.io/couchdb-helm
 $ helm install couchdb/couchdb \
-  --version=4.1.1 \
+  --version=4.2.0 \
   --set allowAdminParty=true \
   --set couchdbConfig.couchdb.uuid=$(curl https://www.uuidgenerator.net/api/version4 2>/dev/null | tr -d -)
 ```
@@ -44,7 +44,7 @@
 ```bash
 $ helm install \
   --name my-release \
-  --version=4.1.1 \
+  --version=4.2.0 \
   --set couchdbConfig.couchdb.uuid=decafbaddecafbaddecafbaddecafbad \
   couchdb/couchdb
 ```
@@ -78,7 +78,7 @@
 ```bash
 $ helm install \
   --name my-release \
-  --version=4.1.1 \
+  --version=4.2.0 \
   --set createAdminSecret=false \
   --set couchdbConfig.couchdb.uuid=decafbaddecafbaddecafbaddecafbad \
   couchdb/couchdb
@@ -133,7 +133,7 @@
 
 ```bash
 $ helm repo add couchdb https://apache.github.io/couchdb-helm
-$ helm upgrade my-release --version=4.1.1 couchdb/couchdb
+$ helm upgrade my-release --version=4.2.0 couchdb/couchdb
 ```
 
 ## Configuration
@@ -164,68 +164,72 @@
 A variety of other parameters are also configurable. See the comments in the
 `values.yaml` file for further details:
 
-| Parameter                            | Default                                                                                                                                                      |
-|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `adminUsername`                      | admin                                                                                                                                                        |
-| `adminPassword`                      | auto-generated                                                                                                                                               |
-| `adminHash`                          |                                                                                                                                                              |
-| `cookieAuthSecret`                   | auto-generated                                                                                                                                               |
-| `image.repository`                   | couchdb                                                                                                                                                      |
-| `image.tag`                          | 3.2.1                                                                                                                                                        |
-| `image.pullPolicy`                   | IfNotPresent                                                                                                                                                 |
-| `searchImage.repository`             | kocolosk/couchdb-search                                                                                                                                      |
-| `searchImage.tag`                    | 0.1.0                                                                                                                                                        |
-| `searchImage.pullPolicy`             | IfNotPresent                                                                                                                                                 |
-| `initImage.repository`               | busybox                                                                                                                                                      |
-| `initImage.tag`                      | latest                                                                                                                                                       |
-| `initImage.pullPolicy`               | Always                                                                                                                                                       |
-| `ingress.enabled`                    | false                                                                                                                                                        |
-| `ingress.hosts`                      | chart-example.local                                                                                                                                          |
-| `ingress.annotations`                |                                                                                                                                                              |
-| `ingress.path`                       | /                                                                                                                                                            |
-| `ingress.tls`                        |                                                                                                                                                              |
-| `persistentVolume.accessModes`       | ReadWriteOnce                                                                                                                                                |
-| `persistentVolume.storageClass`      | Default for the Kube cluster                                                                                                                                 |
-| `persistentVolume.annotations`       | {}                                                                                                                                                           |
-| `podManagementPolicy`                | Parallel                                                                                                                                                     |
-| `affinity`                           |                                                                                                                                                              |
-| `topologySpreadConstraints`          |                                                                                                                                                              |
-| `annotations`                        |                                                                                                                                                              |
-| `tolerations`                        |                                                                                                                                                              |
-| `resources`                          |                                                                                                                                                              |
-| `autoSetup.enabled`                  | false (if set to true, must have `service.enabled` set to true and a correct `adminPassword` - deploy it with the `--wait` flag to avoid first jobs failure) |
-| `autoSetup.image.repository`         | curlimages/curl                                                                                                                                                  |
-| `autoSetup.image.tag`                | latest                                                                                                                                                       |
-| `autoSetup.image.pullPolicy`         | Always                                                                                                                                                       |
-| `autoSetup.defaultDatabases`         | [`_global_changes`]                                                                                                                                          |
-| `service.annotations`                |                                                                                                                                                              |
-| `service.enabled`                    | true                                                                                                                                                         |
-| `service.type`                       | ClusterIP                                                                                                                                                    |
-| `service.externalPort`               | 5984                                                                                                                                                         |
-| `dns.clusterDomainSuffix`            | cluster.local                                                                                                                                                |
-| `networkPolicy.enabled`              | true                                                                                                                                                         |
-| `serviceAccount.enabled`             | true                                                                                                                                                         |
-| `serviceAccount.create`              | true                                                                                                                                                         |
-| `serviceAccount.imagePullSecrets`    |                                                                                                                                                              |
-| `sidecars`                           | {}                                                                                                                                                           |
-| `livenessProbe.enabled`              | true                                                                                                                                                         |
-| `livenessProbe.failureThreshold`     | 3                                                                                                                                                            |
-| `livenessProbe.initialDelaySeconds`  | 0                                                                                                                                                            |
-| `livenessProbe.periodSeconds`        | 10                                                                                                                                                           |
-| `livenessProbe.successThreshold`     | 1                                                                                                                                                            |
-| `livenessProbe.timeoutSeconds`       | 1                                                                                                                                                            |
-| `readinessProbe.enabled`             | true                                                                                                                                                         |
-| `readinessProbe.failureThreshold`    | 3                                                                                                                                                            |
-| `readinessProbe.initialDelaySeconds` | 0                                                                                                                                                            |
-| `readinessProbe.periodSeconds`       | 10                                                                                                                                                           |
-| `readinessProbe.successThreshold`    | 1                                                                                                                                                            |
-| `readinessProbe.timeoutSeconds`      | 1                                                                                                                                                            |
-| `prometheusPort.enabled`             | false                                                                                                                                                        |
-| `prometheusPort.port`                | 17896                                                                                                                                                        |
-| `prometheusPort.bind_address`        | 0.0.0.0                                                                                                                                                      |
-| `placementConfig.enabled`            | false                                                                                                                                                        |
-| `placementConfig.image.repository`   | caligrafix/couchdb-autoscaler-placement-manager                                                                                                              |
-| `placementConfig.image.tag`          | 0.1.0                                                                                                                                                        |
+| Parameter                              | Default                                          |
+| -------------------------------------- | ------------------------------------------------ |
+| `adminUsername`                        | admin                                            |
+| `adminPassword`                        | auto-generated                                   |
+| `adminHash`                            |                                                  |
+| `cookieAuthSecret`                     | auto-generated                                   |
+| `image.repository`                     | couchdb                                          |
+| `image.tag`                            | 3.2.1                                            |
+| `image.pullPolicy`                     | IfNotPresent                                     |
+| `searchImage.repository`               | kocolosk/couchdb-search                          |
+| `searchImage.tag`                      | 0.1.0                                            |
+| `searchImage.pullPolicy`               | IfNotPresent                                     |
+| `initImage.repository`                 | busybox                                          |
+| `initImage.tag`                        | latest                                           |
+| `initImage.pullPolicy`                 | Always                                           |
+| `ingress.enabled`                      | false                                            |
+| `ingress.hosts`                        | chart-example.local                              |
+| `ingress.annotations`                  |                                                  |
+| `ingress.path`                         | /                                                |
+| `ingress.tls`                          |                                                  |
+| `persistentVolume.accessModes`         | ReadWriteOnce                                    |
+| `persistentVolume.storageClass`        | Default for the Kube cluster                     |
+| `persistentVolume.annotations`         | {}                                               |
+| `persistentVolume.existingClaims`      | [] (a list of existing PV/PVC volume value objects with `volumeName`, `claimName`, `persistentVolumeName` and `volumeSource` defined)                                                                |
+| `persistentVolume.volumeName`          |                                                  |
+| `persistentVolume.claimName`           |                                                  |
+| `persistentVolume.volumeSource`        |                                                  |
+| `podManagementPolicy`                  | Parallel                                         |
+| `affinity`                             |                                                  |
+| `topologySpreadConstraints`            |                                                  |
+| `annotations`                          |                                                  |
+| `tolerations`                          |                                                  |
+| `resources`                            |                                                  |
+| `autoSetup.enabled`                    | false (if set to true, must have `service.enabled` set to true and a correct `adminPassword` - deploy it with the `--wait` flag to avoid first jobs failure)                                         |
+| `autoSetup.image.repository`           | curlimages/curl                                  |
+| `autoSetup.image.tag`                  | latest                                           |
+| `autoSetup.image.pullPolicy`           | Always                                           |
+| `autoSetup.defaultDatabases`           | [`_global_changes`]                              |
+| `service.annotations`                  |                                                  |
+| `service.enabled`                      | true                                             |
+| `service.type`                         | ClusterIP                                        |
+| `service.externalPort`                 | 5984                                             |
+| `dns.clusterDomainSuffix`              | cluster.local                                    |
+| `networkPolicy.enabled`                | true                                             |
+| `serviceAccount.enabled`               | true                                             |
+| `serviceAccount.create`                | true                                             |
+| `serviceAccount.imagePullSecrets`      |                                                  |
+| `sidecars`                             | {}                                               |
+| `livenessProbe.enabled`                | true                                             |
+| `livenessProbe.failureThreshold`       | 3                                                |
+| `livenessProbe.initialDelaySeconds`    | 0                                                |
+| `livenessProbe.periodSeconds`          | 10                                               |
+| `livenessProbe.successThreshold`       | 1                                                |
+| `livenessProbe.timeoutSeconds`         | 1                                                |
+| `readinessProbe.enabled`               | true                                             |
+| `readinessProbe.failureThreshold`      | 3                                                |
+| `readinessProbe.initialDelaySeconds`   | 0                                                |
+| `readinessProbe.periodSeconds`         | 10                                               |
+| `readinessProbe.successThreshold`      | 1                                                |
+| `readinessProbe.timeoutSeconds`        | 1                                                |
+| `prometheusPort.enabled`               | false                                            |
+| `prometheusPort.port`                  | 17896                                            |
+| `prometheusPort.bind_address`          | 0.0.0.0                                          |
+| `placementConfig.enabled`              | false                                            |
+| `placementConfig.image.repository`     | caligrafix/couchdb-autoscaler-placement-manager  |
+| `placementConfig.image.tag`            | 0.1.0                                            |
 
 ## Feedback, Issues, Contributing
 
diff --git a/couchdb/templates/_helpers.tpl b/couchdb/templates/_helpers.tpl
index 2f5c1a4..e33f85c 100644
--- a/couchdb/templates/_helpers.tpl
+++ b/couchdb/templates/_helpers.tpl
@@ -64,8 +64,6 @@
   {{- end -}}
 {{- end -}}
 
-
-
 {{/*
 Labels used to define Pods in the CouchDB statefulset
 */}}
@@ -102,3 +100,46 @@
 {{- define "couchdb.uuid" -}}
 {{- required "A value for couchdbConfig.couchdb.uuid must be set" (.Values.couchdbConfig.couchdb | default dict).uuid -}}
 {{- end -}}
+
+{{/*
+Repurpose volume claim metadata whether using the new volume claim template
+or existing volume claims.
+*/}}
+{{- define "persistentVolume.metadata" -}}
+{{- $context := index . "context" -}}
+{{- $claim := index . "claim" -}}
+name: {{ $claim.claimName | default "database-storage" }}
+labels:
+  app: {{ template "couchdb.name" $context }}
+  release: {{ $context.Release.Name }}
+{{- with $context.Values.persistentVolume.annotations }}
+annotations:
+  {{- toYaml . | nindent 6 }}
+{{- end }}
+{{- end -}}
+
+{{/*
+Repurpose volume claim spec whether using the new volume claim template
+or an existing volume claim.
+*/}}
+{{- define "persistentVolume.spec" -}}
+{{- $context := index . "context" -}}
+{{- $claim := index . "claim" -}}
+accessModes:
+{{- range $context.Values.persistentVolume.accessModes }}
+- {{ . | quote }}
+{{- end }}
+resources:
+  requests:
+    storage: {{ $context.Values.persistentVolume.size | quote }}
+{{- if $context.Values.persistentVolume.storageClass }}
+{{- if (eq "-" $context.Values.persistentVolume.storageClass) }}
+storageClassName: ""
+{{- else }}
+storageClassName: "{{ $context.Values.persistentVolume.storageClass }}"
+{{- end }}
+{{- end }}
+{{- if $claim.persistentVolumeName }}
+volumeName: {{ $claim.persistentVolumeName }}
+{{- end }}
+{{- end -}}
diff --git a/couchdb/templates/persistentvolume.yaml b/couchdb/templates/persistentvolume.yaml
new file mode 100644
index 0000000..3349b83
--- /dev/null
+++ b/couchdb/templates/persistentvolume.yaml
@@ -0,0 +1,24 @@
+{{- if and .Values.persistentVolume.enabled .Values.persistentVolume.existingClaims -}}
+{{- range $claim := .Values.persistentVolume.existingClaims }}
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+  name: {{ $claim.persistentVolumeName }}
+spec:
+{{- if $.Values.persistentVolume.storageClass }}
+{{- if (eq "-" $.Values.persistentVolume.storageClass) }}
+  storageClassName: ""
+{{- else }}
+  storageClassName: "{{ $.Values.persistentVolume.storageClass }}"
+{{- end }}
+{{- end }}
+  accessModes:
+  {{- range $.Values.persistentVolume.accessModes }}
+    - {{ . | quote }}
+  {{- end }}
+  capacity:
+    storage: {{ $.Values.persistentVolume.size }}
+{{ toYaml $claim.volumeSource | indent 2 }}
+---
+{{- end }}
+{{- end }}
diff --git a/couchdb/templates/persistentvolumeclaim.yaml b/couchdb/templates/persistentvolumeclaim.yaml
new file mode 100644
index 0000000..5371682
--- /dev/null
+++ b/couchdb/templates/persistentvolumeclaim.yaml
@@ -0,0 +1,12 @@
+{{- if and .Values.persistentVolume.enabled .Values.persistentVolume.existingClaims -}}
+{{- $context := . }}
+{{- range $claim := .Values.persistentVolume.existingClaims }}
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  {{- include "persistentVolume.metadata" (dict "context" $context "claim" $claim) | nindent 2 }}
+spec:
+  {{- include "persistentVolume.spec" (dict "context" $context "claim" $claim) | nindent 2 }}
+---
+{{- end }}
+{{- end }}
diff --git a/couchdb/templates/statefulset.yaml b/couchdb/templates/statefulset.yaml
index a71f6bc..a844ada 100644
--- a/couchdb/templates/statefulset.yaml
+++ b/couchdb/templates/statefulset.yaml
@@ -207,30 +207,16 @@
 {{- if not .Values.persistentVolume.enabled }}
         - name: database-storage
           emptyDir: {}
+{{- else if and .Values.persistentVolume.enabled .Values.persistentVolume.existingClaims }}
+        {{- range $claim := .Values.persistentVolume.existingClaims }}
+        - name: {{ $claim.volumeName }}
+          persistentVolumeClaim:
+            claimName: {{ $claim.claimName }}
+        {{- end }}
 {{- else }}
   volumeClaimTemplates:
     - metadata:
-        name: database-storage
-        labels:
-          app: {{ template "couchdb.name" . }}
-          release: {{ .Release.Name }}
-        {{- with .Values.persistentVolume.annotations }}
-        annotations:
-          {{- toYaml . | nindent 10 }}
-        {{- end }}
+        {{- include "persistentVolume.metadata" (dict "context" .) | nindent 8 }}
       spec:
-        accessModes:
-        {{- range .Values.persistentVolume.accessModes }}
-          - {{ . | quote }}
-        {{- end }}
-        resources:
-          requests:
-            storage: {{ .Values.persistentVolume.size | quote }}
-      {{- if .Values.persistentVolume.storageClass }}
-      {{- if (eq "-" .Values.persistentVolume.storageClass) }}
-        storageClassName: ""
-      {{- else }}
-        storageClassName: "{{ .Values.persistentVolume.storageClass }}"
-      {{- end }}
-      {{- end }}
+        {{- include "persistentVolume.spec" (dict "context" .) | nindent 8 }}
 {{- end }}
diff --git a/couchdb/values.yaml b/couchdb/values.yaml
index a1db0a5..6c0667e 100644
--- a/couchdb/values.yaml
+++ b/couchdb/values.yaml
@@ -67,6 +67,8 @@
 # provisioner.
 persistentVolume:
   enabled: false
+  # NOTE: the number of existing claims must match the cluster size
+  existingClaims: []
   annotations: {}
   accessModes:
     - ReadWriteOnce