diff --git a/charts/cnpg-cluster/CHANGELOG.md b/charts/cnpg-cluster/CHANGELOG.md new file mode 100644 index 0000000..a4bc8bd --- /dev/null +++ b/charts/cnpg-cluster/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog for the 'cnpg-cluster' Helm-Chart + +# v0.1.0 + +- Initial version +- Helm-Untittests for **CronJob**- and **Cluster**-Resources \ No newline at end of file diff --git a/charts/cnpg-cluster/Chart.yaml b/charts/cnpg-cluster/Chart.yaml new file mode 100644 index 0000000..195f328 --- /dev/null +++ b/charts/cnpg-cluster/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: cnpg-cluster +description: Custom Helm-Chart for a CNPG-Cluster with db-dump CronJob functionality +version: 0.1.0 \ No newline at end of file diff --git a/charts/cnpg-cluster/README.md b/charts/cnpg-cluster/README.md new file mode 100644 index 0000000..6181ac8 --- /dev/null +++ b/charts/cnpg-cluster/README.md @@ -0,0 +1,47 @@ +# cnpg-cluster + +Custom Helm-Chart for a CNPG-Cluster with db-dump CronJob functionality + +> [!IMPORTANT] +> This Helm chart assumes that the required CNPG Custom Resource Definitions (CRDs) are already installed in the target Kubernetes cluster. +> It deploys a CronJob that performs full database dumps for the configured CNPG Database. +> The dumped data is written to a Persistent Volume (PV), and your PV must be backed up using your chosen backup solution (for example, Velero with a remote S3 destination). +> The chart does not configure or guarantee the backup of the PV - this responsibility remains with the user. +> This chart is tailored to a specific CNPG backup use case. If you try to use it in your cluster, please verify that it fits your environment and requirements (including PV provisioning and backup/retention setup). + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) + +## Values + +### Backup-Cronjob specifications + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| backupCronjob.containerImage | string | `"postgres:16-alpine"` | Container-Image with 'pg_dumpall'-binary at PATH | +| backupCronjob.enabled | bool | `false` | Whether to enable backups | +| backupCronjob.persistentVolumeClaimName | string | `"cnpg-db-backup-pvc"` | Name of existing PVC where the database-dump is being stored at | +| backupCronjob.restartPolicy | string | `"OnFailure"` | Policy to restart Backup-Cronjob | +| backupCronjob.schedule | string | `"10 * * * *"` | Schedule of the Cronjob (NOTE: Align with the actual PV-Backup-Schedule) | +| backupCronjob.targetDatabaseHost | string | `"cnpg-cluster-app-rw.cnpg-system"` | Target CNPG-database Host | +| backupCronjob.targetDatabasePort | int | `5432` | Target CNPG-database Port | +| backupCronjob.timeZone | string | `"Europe/Brussels"` | Timezone of Cronjob-Schedule | + +### CNPG-Cluster specifications + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| postgresCluster.annotations | object | `{}` | Annotations | +| postgresCluster.cnpgSuperUserSecretName | string | `""` | Name of the Secret for the superUser of the CNPG-Cluster | +| postgresCluster.enableSuperuserAccess | bool | `false` | Whether to allow superuser-access (NOTE: Required to be set 'true' for backups) | +| postgresCluster.imageName | string | `"ghcr.io/cloudnative-pg/postgresql:16"` | Container-Image | +| postgresCluster.imagePullPolicy | string | `"IfNotPresent"` | When to pull Container-Image | +| postgresCluster.instances | int | `1` | Number of CNPG-Cluster instances | +| postgresCluster.managedRoles | list | `[]` | Add managed Roles to database | +| postgresCluster.postgresqlParameters | object | `{}` | Additional parameters for the postgres-database | +| postgresCluster.resources | object | `{}` | Resource requests and limits | +| postgresCluster.sharedPreloadLibraries | list | `[]` | Preload additional libraries | +| postgresCluster.storageClass | string | `"local-path"` | StorageClass of requested PVC | +| postgresCluster.storageSize | string | `"10Gi"` | Storage-Size of requested PVC | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) \ No newline at end of file diff --git a/charts/cnpg-cluster/README.md.gotmpl b/charts/cnpg-cluster/README.md.gotmpl new file mode 100644 index 0000000..2894d27 --- /dev/null +++ b/charts/cnpg-cluster/README.md.gotmpl @@ -0,0 +1,15 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +> [!IMPORTANT] +> This Helm chart assumes that the required CNPG Custom Resource Definitions (CRDs) are already installed in the target Kubernetes cluster. +> It deploys a CronJob that performs full database dumps for the configured CNPG Database. +> The dumped data is written to a Persistent Volume (PV), and your PV must be backed up using your chosen backup solution (for example, Velero with a remote S3 destination). +> The chart does not configure or guarantee the backup of the PV - this responsibility remains with the user. +> This chart is tailored to a specific CNPG backup use case. If you try to use it in your cluster, please verify that it fits your environment and requirements (including PV provisioning and backup/retention setup). + +{{ template "chart.versionBadge" . }}{{ template "chart.typeBadge" . }}{{ template "chart.appVersionBadge" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} \ No newline at end of file diff --git a/charts/cnpg-cluster/templates/_helpers.tpl b/charts/cnpg-cluster/templates/_helpers.tpl new file mode 100644 index 0000000..9b1a6ed --- /dev/null +++ b/charts/cnpg-cluster/templates/_helpers.tpl @@ -0,0 +1,85 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cnpgdb.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cnpgdb.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cnpgdb.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cnpgdb.labels" -}} +helm.sh/chart: {{ include "cnpgdb.chart" . }} +{{ include "cnpgdb.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cnpgdb.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cnpgdb.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Compile all warnings into a single message, and call fail. +See e.g.: https://github.com/bitnami/charts/blob/d9f6e8974fc9c8cbc64146e1632f70476529e720/bitnami/airflow/templates/_helpers.tpl#L434 +*/}} +{{- define "cnpgdb.validateValues" -}} +{{- $messages := list -}} +{{- $messages := append $messages (include "cnpgdb.validateValues.superUserAccess" .) -}} +{{- $messages := without $messages "" -}} +{{- $message := join "\n" $messages -}} + +{{- if $message -}} +{{- printf "\nVALUES VALIDATION:\n%s" $message | fail -}} +{{- end -}} +{{- end -}} + +{{/* +Validate values of cnpgdb - superUserAccess + +> Whether `enableSuperuserAccess: true` when backup is enabled +> Whether `cnpgSuperUserSecretName` is set when `enableSuperuserAccess: true` +*/}} +{{- define "cnpgdb.validateValues.superUserAccess" -}} +{{- $postgres := .Values.postgresCluster -}} +{{- $backup := .Values.backupCronjob -}} +{{- if and $backup.enabled (not $postgres.enableSuperuserAccess) -}} +cnpgdb: enableSuperuserAccess + You have to enable external superUserAccess when enabling Backups +{{- end -}} +{{- if and $postgres.enableSuperuserAccess (not $postgres.cnpgSuperUserSecretName) -}} +cnpgdb: cnpgSuperUserSecretName + You have to provide a superUserAccess-Name when enabling superUserAccess +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/charts/cnpg-cluster/templates/backup-cronjob.yaml b/charts/cnpg-cluster/templates/backup-cronjob.yaml new file mode 100644 index 0000000..b23a29e --- /dev/null +++ b/charts/cnpg-cluster/templates/backup-cronjob.yaml @@ -0,0 +1,50 @@ +{{ include "cnpgdb.validateValues" . }} +{{- $cronjob := .Values.backupCronjob -}} +{{- $postgres := .Values.postgresCluster -}} +{{- if and $cronjob.enabled $postgres.enableSuperuserAccess }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "cnpgdb.fullname" . }}-backup-dump + namespace: {{ .Release.Namespace | quote }} +spec: + schedule: "{{ $cronjob.schedule }}" + timeZone: "{{ $cronjob.timeZone }}" + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + restartPolicy: {{ $cronjob.restartPolicy }} + containers: + - name: postgres-backup + image: {{ $cronjob.containerImage }} + env: + - name: PGUSER + valueFrom: + secretKeyRef: + name: {{ $postgres.cnpgSuperUserSecretName }} + key: username + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ $postgres.cnpgSuperUserSecretName }} + key: password + command: + - /bin/bash + - -c + - | + set -euo pipefail; + pg_dumpall \ + -h {{ $cronjob.targetDatabaseHost }} \ + -p {{ $cronjob.targetDatabasePort }} \ + > /backup/cnpg-sqlbkp.sql + ls -lah /backup/cnpg-sqlbkp.sql + volumeMounts: + - name: backup-volume + mountPath: /backup + volumes: + - name: backup-volume + persistentVolumeClaim: + claimName: {{ $cronjob.persistentVolumeClaimName }} +{{- end -}} \ No newline at end of file diff --git a/charts/cnpg-cluster/templates/cnpg-cluster.yaml b/charts/cnpg-cluster/templates/cnpg-cluster.yaml new file mode 100644 index 0000000..652a067 --- /dev/null +++ b/charts/cnpg-cluster/templates/cnpg-cluster.yaml @@ -0,0 +1,45 @@ +{{ include "cnpgdb.validateValues" . }} +{{- $postgres := .Values.postgresCluster -}} +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: {{ include "cnpgdb.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + {{- with $postgres.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "cnpgdb.labels" . }} +spec: + instances: {{ $postgres.instances }} + storage: + size: {{ $postgres.storageSize }} + storageClass: {{ $postgres.storageClass }} + imageName: {{ $postgres.imageName }} + imagePullPolicy: {{ $postgres.imagePullPolicy }} + enableSuperuserAccess: {{ $postgres.enableSuperuserAccess }} + {{- if $postgres.cnpgSuperUserSecretName }} + superuserSecret: + name: {{ $postgres.cnpgSuperUserSecretName }} + {{- end }} + {{- if $postgres.managedRoles }} + managed: + roles: + {{- toYaml $postgres.managedRoles | nindent 6 }} + {{- end }} + {{- if or $postgres.postgresqlParameters $postgres.sharedPreloadLibraries }} + postgresql: + {{- if $postgres.postgresqlParameters }} + parameters: + {{- toYaml $postgres.postgresqlParameters | nindent 6 }} + {{- end }} + {{- if $postgres.sharedPreloadLibraries }} + shared_preload_libraries: + {{- toYaml $postgres.sharedPreloadLibraries | nindent 6 }} + {{- end }} + {{- end }} + {{- if $postgres.resources }} + resources: + {{- toYaml $postgres.resources | nindent 4 }} + {{- end }} \ No newline at end of file diff --git a/charts/cnpg-cluster/tests/backup-cronjob_test.yaml b/charts/cnpg-cluster/tests/backup-cronjob_test.yaml new file mode 100644 index 0000000..0088115 --- /dev/null +++ b/charts/cnpg-cluster/tests/backup-cronjob_test.yaml @@ -0,0 +1,98 @@ +#------------------------------------------------------------------------------ +# +# CronJob-Unittests Test-Suite for CNPG-Cluster +# +# > https://github.com/helm-unittest/helm-unittest/blob/main/DOCUMENT.md +# +#------------------------------------------------------------------------------ +# +# Test suite name to show on test result output +suite: Test CronJob +# +# The template files scope to test in this suite +# Only the selected files will be rendered +templates: + - templates/backup-cronjob.yaml +# +tests: + - it: should not render enabled CronJob with disabled superUserAccess + # + set: + backupCronjob.enabled: true + postgresCluster.enableSuperuserAccess: false + # + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\ncnpgdb: enableSuperuserAccess\n You have to enable external superUserAccess when enabling Backups" + + - it: should render enabled CronJob with enabled superUserAccess + # + set: + postgresCluster: + enableSuperuserAccess: true + cnpgSuperUserSecretName: "test-cnpg-superuser-secret" + backupCronjob.enabled: true + # + asserts: + - isKind: + of: CronJob + + - it: should render enabled CronJob and superUserAccess with adjusted specs + # + set: + postgresCluster: + enableSuperuserAccess: true + cnpgSuperUserSecretName: "test-cnpg-superuser-secret" + backupCronjob: + enabled: true + schedule: "1337 1337 1337" + timeZone: "EARTH" + restartPolicy: OnFailure + containerImage: "postgres:1337-alpine" + persistentVolumeClaimName: "test-cronjob-backup-pvc" + targetDatabaseHost: "cnpg-db.1337-db-host" + targetDatabasePort: 1337 + # + asserts: + - isKind: + of: CronJob + + - equal: + path: spec.schedule + value: "1337 1337 1337" + + - equal: + path: spec.timeZone + value: "EARTH" + + - equal: + path: spec.jobTemplate.spec.template.spec.restartPolicy + value: "OnFailure" + + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].image + value: "postgres:1337-alpine" + + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].env[0].valueFrom.secretKeyRef.name + value: "test-cnpg-superuser-secret" + + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.name + value: "test-cnpg-superuser-secret" + + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].command + value: + - /bin/bash + - -c + - | + set -euo pipefail; + pg_dumpall \ + -h cnpg-db.1337-db-host \ + -p 1337 \ + > /backup/cnpg-sqlbkp.sql + ls -lah /backup/cnpg-sqlbkp.sql + + + \ No newline at end of file diff --git a/charts/cnpg-cluster/tests/cnpg-cluster_test.yaml b/charts/cnpg-cluster/tests/cnpg-cluster_test.yaml new file mode 100644 index 0000000..7b25dfa --- /dev/null +++ b/charts/cnpg-cluster/tests/cnpg-cluster_test.yaml @@ -0,0 +1,113 @@ +#------------------------------------------------------------------------------ +# +# Cluster-Unittests Test-Suite for CNPG-Cluster +# +# > https://github.com/helm-unittest/helm-unittest/blob/main/DOCUMENT.md +# +#------------------------------------------------------------------------------ +# +# Test suite name to show on test result output +suite: Test Cluster +# +# The template files scope to test in this suite +# Only the selected files will be rendered +templates: + - templates/cnpg-cluster.yaml +# +tests: + - it: should render Cluster with enabled superUserAccess + # + set: + postgresCluster.enableSuperuserAccess: true + postgresCluster.cnpgSuperUserSecretName: "test-cnpg-db-superuser-secret" + # + asserts: + - isKind: + of: Cluster + + - equal: + path: spec.enableSuperuserAccess + value: true + + - equal: + path: spec.superuserSecret.name + value: "test-cnpg-db-superuser-secret" + + - it: should render Cluster with disabled superUserAccess + # + # Use default Helm Values (superUserAccess is disabled) + # + asserts: + - isKind: + of: Cluster + + - equal: + path: spec.enableSuperuserAccess + value: false + + - notExists: + path: spec.superuserSecret.name + + - it: should render Cluster with custom app-user managedRoles + # + set: + postgresCluster.managedRoles: + - name: testuser1 + superuser: false + login: true + inherit: true + connectionLimit: -1 + ensure: present + passwordSecret: + name: cnpg-appuser-test1-secret + key: password + + - name: testuser2 + superuser: false + login: true + createdb: true + inherit: true + ensure: present + connectionLimit: -1 + passwordSecret: + name: cnpg-appuser-test2-secret + key: password + # + asserts: + - isKind: + of: Cluster + - equal: + path: spec.managed.roles + value: + - name: testuser1 + superuser: false + login: true + inherit: true + connectionLimit: -1 + ensure: present + passwordSecret: + name: cnpg-appuser-test1-secret + key: password + + - name: testuser2 + superuser: false + login: true + createdb: true + inherit: true + ensure: present + connectionLimit: -1 + passwordSecret: + name: cnpg-appuser-test2-secret + key: password + + - it: should render without any managedRoles + # + set: + managedRoles: [] + # + asserts: + - isKind: + of: Cluster + + - notExists: + path: spec.managed.roles \ No newline at end of file diff --git a/charts/cnpg-cluster/values.schema.json b/charts/cnpg-cluster/values.schema.json new file mode 100644 index 0000000..06cf7f4 --- /dev/null +++ b/charts/cnpg-cluster/values.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "properties": { + "postgresCluster": { + "type": "object", + "properties": { + "annotations": {"type": "object"}, + "instances": {"type": "integer", "minimum": 1}, + "storageSize": {"type": "string"}, + "storageClass": {"type": "string"}, + "imageName": {"type": "string"}, + "imagePullPolicy": {"type": "string"}, + "enableSuperuserAccess": {"type": "boolean"}, + "cnpgSuperUserSecretName": {"type": "string"}, + "managedRoles": {"type": "array"}, + "postgresqlParameters": {"type": "object"}, + "sharedPreloadLibraries": {"type": "array"}, + "resources": {"type": "object"} + } + }, + + "backupCronjob": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "schedule": {"type": "string"}, + "timeZone": {"type": "string"}, + "restartPolicy": {"type": "string"}, + "containerImage": {"type": "string"}, + "persistentVolumeClaimName": {"type": "string"}, + "targetDatabaseHost": {"type": "string"}, + "targetDatabasePort": {"type": "integer"} + } + } + }, + + "required": [ + "postgresCluster", + "backupCronjob" + ], + + "title": "Values", + "type": "object" +} \ No newline at end of file diff --git a/charts/cnpg-cluster/values.yaml b/charts/cnpg-cluster/values.yaml new file mode 100644 index 0000000..6474728 --- /dev/null +++ b/charts/cnpg-cluster/values.yaml @@ -0,0 +1,100 @@ +#----------------------------------------------------------------------- +# +# Default Helm-Values for custom CNPG-Cluster Helm-Chart with +# integrated db-dump CronJob-Resource for backups +# +#----------------------------------------------------------------------- + + +# ====================================================================== +# CNPG-Cluster specifications +# ====================================================================== +postgresCluster: + # + # -- Annotations + # @section -- CNPG-Cluster specifications + annotations: {} + # + # -- Number of CNPG-Cluster instances + # @section -- CNPG-Cluster specifications + instances: 1 + # + # -- Storage-Size of requested PVC + # @section -- CNPG-Cluster specifications + storageSize: 10Gi + # + # -- StorageClass of requested PVC + # @section -- CNPG-Cluster specifications + storageClass: local-path + # + # -- Container-Image + # @section -- CNPG-Cluster specifications + imageName: ghcr.io/cloudnative-pg/postgresql:16 + # + # -- When to pull Container-Image + # @section -- CNPG-Cluster specifications + imagePullPolicy: IfNotPresent + # + # -- Whether to allow superuser-access + # (NOTE: Required to be set 'true' for backups) + # @section -- CNPG-Cluster specifications + enableSuperuserAccess: false + # + # -- Name of the Secret for the superUser of the CNPG-Cluster + # @section -- CNPG-Cluster specifications + cnpgSuperUserSecretName: "" + # + # -- Add managed Roles to database + # @section -- CNPG-Cluster specifications + managedRoles: [] + # + # -- Additional parameters for the postgres-database + # @section -- CNPG-Cluster specifications + postgresqlParameters: {} + # + # -- Preload additional libraries + # @section -- CNPG-Cluster specifications + sharedPreloadLibraries: [] + # + # -- Resource requests and limits + # @section -- CNPG-Cluster specifications + resources: {} + + +# ====================================================================== +# Backup-Cronjob specifications +# ====================================================================== +backupCronjob: + # + # -- Whether to enable backups + # @section -- Backup-Cronjob specifications + enabled: false + # + # -- Schedule of the Cronjob + # (NOTE: Align with the actual PV-Backup-Schedule) + # @section -- Backup-Cronjob specifications + schedule: "10 * * * *" + # + # -- Timezone of Cronjob-Schedule + # @section -- Backup-Cronjob specifications + timeZone: "Europe/Brussels" + # + # -- Policy to restart Backup-Cronjob + # @section -- Backup-Cronjob specifications + restartPolicy: OnFailure + # + # -- Container-Image with 'pg_dumpall'-binary at PATH + # @section -- Backup-Cronjob specifications + containerImage: "postgres:16-alpine" + # + # -- Name of existing PVC where the database-dump is being stored at + # @section -- Backup-Cronjob specifications + persistentVolumeClaimName: "cnpg-db-backup-pvc" + # + # -- Target CNPG-database Host + # @section -- Backup-Cronjob specifications + targetDatabaseHost: "cnpg-cluster-app-rw.cnpg-system" + # + # -- Target CNPG-database Port + # @section -- Backup-Cronjob specifications + targetDatabasePort: 5432 \ No newline at end of file