diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..50c8cc1 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,10 @@ +[bumpversion] +current_version = 0.1.0 +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P\w+))? +serialize = + {major}.{minor}.{patch}-{suffix} + {major}.{minor}.{patch} + +[bumpversion:file:charts/cnpg-database/Chart.yaml] +search = version: {current_version} +replace = version: {new_version} diff --git a/.github/lint-config.yaml b/.github/lint-config.yaml new file mode 100644 index 0000000..21095dd --- /dev/null +++ b/.github/lint-config.yaml @@ -0,0 +1 @@ +target-branch: "main" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b521c75 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + +jobs: + get-version: + name: Get Version + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.get-version.outputs.version }} + steps: + - name: Check out repository + uses: bakdata/ci-templates/actions/checkout@1.69.2 + + - name: Get version + id: get-version + shell: bash + env: + DEFAULT_BRANCH: refs/heads/${{ github.event.repository.default_branch }} + run: | + version=$(sed -n 's/^current_version = \(\.*\)/\1/p' < .bumpversion.cfg) + if [[ "${GITHUB_REF}" != refs/tags/* ]]; then + version="${version}-dev" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + + build-and-publish: + name: Helm + uses: bakdata/ci-templates/.github/workflows/helm-multi-release.yaml@1.69.2 + needs: get-version + with: + charts-path: "charts" + subdirs: "['cnpg-database']" + version: ${{ needs.get-version.outputs.version }} + secrets: + github-username: ${{ secrets.GH_USERNAME }} + github-email: ${{ secrets.GH_EMAIL }} + github-token: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..5baac10 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,25 @@ +name: Release + +on: + workflow_dispatch: + inputs: + release-type: + description: "Scope of the release." + type: choice + required: true + default: patch + options: + - patch + - minor + - major + +jobs: + release: + name: Release + uses: bakdata/ci-templates/.github/workflows/bump-version-release.yaml@1.35.1 + with: + release-type: "${{ github.event.inputs.release-type }}" + secrets: + github-username: "${{ secrets.GH_USERNAME }}" + github-email: "${{ secrets.GH_EMAIL }}" + github-token: "${{ secrets.GH_TOKEN }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6e029a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.* +!.github +!.gitignore +!.bumpversion.cfg diff --git a/README.md b/README.md index 2373188..5d19f3f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # cnpg-database-helm-chart -Helm chart for a CNPG database + +Helm chart for deploying a CNPG database + +## Install + +```shell +helm repo add cnpg-database https://bakdata.github.io/cnpg-database-helm-chart/ +helm repo update +helm install cnpg-database/cnpg-database +``` diff --git a/charts/cnpg-database/.helmignore b/charts/cnpg-database/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/cnpg-database/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/cnpg-database/Chart.yaml b/charts/cnpg-database/Chart.yaml new file mode 100644 index 0000000..154a133 --- /dev/null +++ b/charts/cnpg-database/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +name: cnpg-database +description: A Helm chart for deploying a CNPG database +version: 0.1.0 +maintainers: + - name: bakdata + email: opensource@bakdata.com + url: bakdata.com diff --git a/charts/cnpg-database/templates/_helpers.tpl b/charts/cnpg-database/templates/_helpers.tpl new file mode 100644 index 0000000..868d14d --- /dev/null +++ b/charts/cnpg-database/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cnpg-database.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 "cnpg-database.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 "cnpg-database.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cnpg-database.labels" -}} +helm.sh/chart: {{ include "cnpg-database.chart" . }} +{{ include "cnpg-database.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cnpg-database.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cnpg-database.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cnpg-database.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cnpg-database.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/cnpg-database/templates/cluster.yaml b/charts/cnpg-database/templates/cluster.yaml new file mode 100644 index 0000000..66ee7f3 --- /dev/null +++ b/charts/cnpg-database/templates/cluster.yaml @@ -0,0 +1,127 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/postgresql.cnpg.io/cluster_v1.json +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: {{ include "cnpg-database.fullname" . }} + labels: + {{- include "cnpg-database.labels" . | nindent 4 }} + {{- with .Values.postgres.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.postgres.description }} + description: {{ .Values.postgres.description }} + {{- end }} + instances: {{ .Values.postgres.instances }} + imageName: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" + enablePDB: {{ .Values.postgres.enablePDB }} + enableSuperuserAccess: {{ .Values.postgres.enableSuperuserAccess }} + {{- with .Values.postgres.resources }} + resources: + {{- toYaml . | nindent 4 }} + {{- end }} + storage: + storageClass: {{ .Values.postgres.storage.class }} + resizeInUseVolumes: {{ .Values.postgres.storage.data.resizeInUseVolume }} + size: {{ .Values.postgres.storage.data.size }} + walStorage: + storageClass: {{ .Values.postgres.storage.class }} + size: {{ .Values.postgres.storage.wal.size }} + resizeInUseVolumes: {{ .Values.postgres.storage.wal.resizeInUseVolume }} + {{- if .Values.postgres.bootstrap.recovery.enabled }} + bootstrap: + recovery: + source: {{ .Values.postgres.bootstrap.recovery.name | default (include "cnpg-database.fullname" .) | quote }} + database: {{ .Values.postgres.bootstrap.database }} + owner: {{ .Values.postgres.bootstrap.owner }} + externalClusters: + - name: {{ .Values.postgres.bootstrap.recovery.name | default (include "cnpg-database.fullname" .) | quote }} + barmanObjectStore: + serverName: {{ .Values.postgres.bootstrap.recovery.name | default (include "cnpg-database.fullname" .) | quote }} + destinationPath: {{ .Values.backup.s3.destinationPath | quote }} + endpointURL: {{ .Values.backup.s3.endpointURL | quote }} + {{- if and .Values.backup.trustBundleName .Values.backup.trustBundleKey }} + endpointCA: + name: {{ .Values.backup.trustBundleName }} + key: {{ .Values.backup.trustBundleKey }} + {{- end }} + s3Credentials: + accessKeyId: + key: S3_ACCESS_KEY + name: {{ include "cnpg-database.fullname" . }}-s3 + secretAccessKey: + key: S3_SECRET_KEY + name: {{ include "cnpg-database.fullname" . }}-s3 + {{- else }} + bootstrap: + initdb: + database: {{ .Values.postgres.bootstrap.database }} + owner: {{ .Values.postgres.bootstrap.owner }} + {{- end }} + {{- with .Values.postgres.parameters }} + postgresql: + parameters: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- if or .Values.postgres.groups .Values.postgres.users }} + managed: + roles: + {{- range .Values.postgres.groups }} + - name: {{ . | quote }} + ensure: present + {{- end }} + {{- range .Values.postgres.users }} + - name: {{ .username | quote }} + ensure: present + login: true + passwordSecret: + name: {{ printf "%s-creds-%s" (include "cnpg-database.fullname" $) .username | replace "." "-" | trunc 63 | quote }} + {{- with .inRoles }} + inRoles: + {{- range . }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.backup.enabled }} + backup: + retentionPolicy: {{ .Values.backup.retentionPolicy }} + target: {{ .Values.backup.target }} + barmanObjectStore: + serverName: {{ .Values.backup.name | default (include "cnpg-database.fullname" .) | quote }} + data: + compression: {{ .Values.backup.data.compression }} + destinationPath: {{ .Values.backup.s3.destinationPath }} + endpointURL: {{ .Values.backup.s3.endpointURL }} + {{- if and .Values.backup.trustBundleName .Values.backup.trustBundleKey }} + endpointCA: + name: {{ .Values.backup.trustBundleName }} + key: {{ .Values.backup.trustBundleKey }} + {{- end }} + s3Credentials: + accessKeyId: + key: S3_ACCESS_KEY + name: {{ include "cnpg-database.fullname" . }}-s3 + secretAccessKey: + key: S3_SECRET_KEY + name: {{ include "cnpg-database.fullname" . }}-s3 + wal: + compression: {{ .Values.backup.wal.compression }} + {{- with .Values.backup.wal.maxParallel }} + maxParallel: {{ . }} + {{- end }} + {{- with .Values.backup.wal.encryption }} + encryption: {{ . }} + {{- end }} + {{- end }} + {{- if or .Values.postgres.serverCASecret .Values.postgres.serverTLSSecret }} + certificates: + {{- with .Values.postgres.serverCASecret }} + serverCASecret: {{ . }} + {{- end }} + {{- with .Values.postgres.serverTLSSecret }} + serverTLSSecret: {{ . }} + {{- end }} + {{- end }} diff --git a/charts/cnpg-database/templates/pod-monitor.yaml b/charts/cnpg-database/templates/pod-monitor.yaml new file mode 100644 index 0000000..c73079e --- /dev/null +++ b/charts/cnpg-database/templates/pod-monitor.yaml @@ -0,0 +1,13 @@ +{{- if .Values.podMonitor.enabled }} +# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/monitoring.coreos.com/podmonitor_v1.json +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: {{ include "cnpg-database.fullname" . }} +spec: + selector: + matchLabels: + cnpg.io/cluster: {{ include "cnpg-database.fullname" . }} + podMetricsEndpoints: + - port: metrics +{{- end }} diff --git a/charts/cnpg-database/templates/scheduled-backup.yaml b/charts/cnpg-database/templates/scheduled-backup.yaml new file mode 100644 index 0000000..b7ce788 --- /dev/null +++ b/charts/cnpg-database/templates/scheduled-backup.yaml @@ -0,0 +1,18 @@ +{{- if .Values.backup.enabled }} +# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/postgresql.cnpg.io/scheduledbackup_v1.json +apiVersion: postgresql.cnpg.io/v1 +kind: ScheduledBackup +metadata: + name: {{ include "cnpg-database.fullname" . }} + {{- with .Values.backup.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + suspend: {{.Values.backup.suspend }} + schedule: {{ .Values.backup.schedule | quote }} + immediate: {{ .Values.backup.immediate }} + backupOwnerReference: self + cluster: + name: {{ include "cnpg-database.fullname" . }} +{{- end }} diff --git a/charts/cnpg-database/templates/secret-s3.yaml b/charts/cnpg-database/templates/secret-s3.yaml new file mode 100644 index 0000000..2a93afe --- /dev/null +++ b/charts/cnpg-database/templates/secret-s3.yaml @@ -0,0 +1,19 @@ +{{- if .Values.backup.enabled }} +# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/bitnami.com/sealedsecret_v1alpha1.json +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: {{ include "cnpg-database.fullname" . }}-s3 + annotations: + sealedsecrets.bitnami.com/namespace-wide: "true" + labels: + {{- include "cnpg-database.labels" . | nindent 4 }} +spec: + encryptedData: + S3_ACCESS_KEY: {{.Values.backup.s3.accessKey }} + S3_SECRET_KEY: {{.Values.backup.s3.secretKey }} + template: + metadata: + annotations: + cnpg.io/reload: "true" +{{- end }} diff --git a/charts/cnpg-database/templates/secrets-users.yaml b/charts/cnpg-database/templates/secrets-users.yaml new file mode 100644 index 0000000..6023d1c --- /dev/null +++ b/charts/cnpg-database/templates/secrets-users.yaml @@ -0,0 +1,22 @@ +{{- with .Values.postgres.users }} +{{- range . }} +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/bitnami.com/sealedsecret_v1alpha1.json +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: {{ printf "%s-creds-%s" (include "cnpg-database.fullname" $) .username | replace "." "-" | trunc 63 | quote }} + annotations: + sealedsecrets.bitnami.com/namespace-wide: "true" +spec: + encryptedData: + password: {{ .password | quote }} + template: + metadata: + annotations: + cnpg.io/reload: "true" + type: kubernetes.io/basic-auth + data: + username: {{ .username | quote }} +{{- end }} +{{- end }} diff --git a/charts/cnpg-database/templates/service.yaml b/charts/cnpg-database/templates/service.yaml new file mode 100644 index 0000000..b1538b2 --- /dev/null +++ b/charts/cnpg-database/templates/service.yaml @@ -0,0 +1,43 @@ +{{- if .Values.additionalService.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cnpg-database.fullname" . }}-additional + {{- with .Values.additionalService.labels }} + labels: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.additionalService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if eq .Values.additionalService.type "ClusterIP" }} + type: ClusterIP + {{- with .Values.additionalService.clusterIP }} + clusterIP: {{ . }} + {{- end }} + {{- else if eq .Values.additionalService.type "LoadBalancer" }} + type: LoadBalancer + {{- with .Values.additionalService.loadBalancerIP }} + loadBalancerIP: {{ . }} + {{- end }} + {{- with .Values.additionalService.loadBalancerClass }} + loadBalancerClass: {{ . }} + {{- end }} + {{- with .Values.additionalService.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- else }} + type: {{ .Values.additionalService.type }} + {{- end }} + ports: + - name: postgres + protocol: TCP + port: {{ .Values.additionalService.port }} + targetPort: {{ .Values.additionalService.targetPort }} + selector: + cnpg.io/cluster: {{ include "cnpg-database.fullname" . }} + cnpg.io/instanceRole: {{ .Values.additionalService.instanceRole }} +{{- end }} diff --git a/charts/cnpg-database/values.yaml b/charts/cnpg-database/values.yaml new file mode 100644 index 0000000..9dc1037 --- /dev/null +++ b/charts/cnpg-database/values.yaml @@ -0,0 +1,109 @@ +postgres: + description: "" + annotations: {} + + instances: 1 + image: + repository: ghcr.io/cloudnative-pg/postgresql + tag: "18-standard-trixie" + + enablePDB: true + enableSuperuserAccess: true + + resources: + requests: + memory: "512Mi" + cpu: "50m" + limits: + memory: "512Mi" + cpu: "50m" + + # Storage configuration, see https://cloudnative-pg.io/docs/current/storage/ + storage: + class: standard + data: + resizeInUseVolume: true + size: 10Gi + wal: + resizeInUseVolume: true + size: 10Gi + + # Bootstrap the PostgreSQL: https://cloudnative-pg.io/docs/current/bootstrap/ + bootstrap: + database: db + owner: user + recovery: + enabled: false + # Name of the S3 backup to recover from. Uses S3 settings from backup section. + name: "" + + # Parameters passed to PostgreSQL: https://cloudnative-pg.io/docs/current/postgresql_conf/ + parameters: {} + + groups: [] + # - "my_group_role" + + # Usernames are unsealed and passwords are sealed (refer to https://github.com/bitnami-labs/sealed-secrets) + users: [] + # - username: "john.doe" + # password: "MySealedPassword" + # inRoles: + # - my_group_role + + # Secret name for root CA certificate + serverCASecret: "" + # Secret name for TLS + serverTLSSecret: "" + +backup: + enabled: false + + # Name of the current instance backup into the S3 bucket. + # Cannot be set to a backup name that already exists inside the bucket. + # For more details see https://github.com/cloudnative-pg/cloudnative-pg/issues/5203 + name: "" + annotations: {} + + # Schedule of the backup job + schedule: "0 0 0 * * *" + suspend: false + immediate: false + + retentionPolicy: 14d + target: "prefer-standby" + + trustBundleName: "" + trustBundleKey: "" + + # access and secret keys are sealed (refer to https://github.com/bitnami-labs/sealed-secrets) + s3: + destinationPath: s3://my-bucket + endpointURL: https://my-s3-endpoint.example + accessKey: "MyAccessKeyId" + secretKey: "MySecretKeyId" + data: + compression: gzip + wal: + compression: gzip + maxParallel: 8 + encryption: AES256 + +# See https://cloudnative-pg.io/docs/current/monitoring/#monitoring-with-the-prometheus-operator +podMonitor: + enabled: true + +# CNPG deploys services for the database by default. This is just here if you require an extra service. +additionalService: + enabled: false + + type: ClusterIP + clusterIP: "" + loadBalancerIP: "" + loadBalancerClass: "" + loadBalancerSourceRanges: [] + + port: 5432 + targetPort: 5432 + + # Whether to chose the primary or the readonly replica instance + instanceRole: replica