diff --git a/packages/apps/ferretdb/Chart.yaml b/packages/apps/ferretdb/Chart.yaml new file mode 100644 index 00000000..6d35a52b --- /dev/null +++ b/packages/apps/ferretdb/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +name: ferretdb +description: Managed FerretDB service +icon: ferretdb.svg + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.22.0" diff --git a/packages/apps/ferretdb/Makefile b/packages/apps/ferretdb/Makefile new file mode 100644 index 00000000..207e2133 --- /dev/null +++ b/packages/apps/ferretdb/Makefile @@ -0,0 +1,2 @@ +generate: + readme-generator -v values.yaml -s values.schema.json -r README.md diff --git a/packages/apps/ferretdb/README.md b/packages/apps/ferretdb/README.md new file mode 100644 index 00000000..afa4f961 --- /dev/null +++ b/packages/apps/ferretdb/README.md @@ -0,0 +1,34 @@ +# Managed FerretDB Service + +## Parameters + +### Common parameters + +| Name | Description | Value | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------- | +| `external` | Enable external access from outside the cluster | `false` | +| `size` | Persistent Volume size | `10Gi` | +| `replicas` | Number of Postgres replicas | `2` | +| `quorum.minSyncReplicas` | Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed. | `0` | +| `quorum.maxSyncReplicas` | Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the number of instances). | `0` | + +### Configuration parameters + +| Name | Description | Value | +| ------- | ------------------- | ----- | +| `users` | Users configuration | `{}` | + +### Backup parameters + +| Name | Description | Value | +| ------------------------ | ---------------------------------------------- | ------------------------------------------------------ | +| `backup.enabled` | Enable pereiodic backups | `false` | +| `backup.s3Region` | The AWS S3 region where backups are stored | `us-east-1` | +| `backup.s3Bucket` | The S3 bucket used for storing backups | `s3.example.org/postgres-backups` | +| `backup.schedule` | Cron schedule for automated backups | `0 2 * * *` | +| `backup.cleanupStrategy` | The strategy for cleaning up old backups | `--keep-last=3 --keep-daily=3 --keep-within-weekly=1m` | +| `backup.s3AccessKey` | The access key for S3, used for authentication | `oobaiRus9pah8PhohL1ThaeTa4UVa7gu` | +| `backup.s3SecretKey` | The secret key for S3, used for authentication | `ju3eum4dekeich9ahM1te8waeGai0oog` | +| `backup.resticPassword` | The password for Restic backup encryption | `ChaXoveekoh6eigh4siesheeda2quai0` | + + diff --git a/packages/apps/ferretdb/ferretdb.svg b/packages/apps/ferretdb/ferretdb.svg new file mode 100644 index 00000000..196871e1 --- /dev/null +++ b/packages/apps/ferretdb/ferretdb.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/packages/apps/ferretdb/templates/.gitkeep b/packages/apps/ferretdb/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/apps/ferretdb/templates/backup-cronjob.yaml b/packages/apps/ferretdb/templates/backup-cronjob.yaml new file mode 100644 index 00000000..84a6aee7 --- /dev/null +++ b/packages/apps/ferretdb/templates/backup-cronjob.yaml @@ -0,0 +1,99 @@ +{{- if .Values.backup.enabled }} +{{ $image := .Files.Get "images/backup.json" | fromJson }} + +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ .Release.Name }}-backup +spec: + schedule: "{{ .Values.backup.schedule }}" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + spec: + restartPolicy: OnFailure + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/backup-script.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/backup-secret.yaml") . | sha256sum }} + spec: + restartPolicy: Never + containers: + - name: mysqldump + image: "{{ index $image "image.name" }}@{{ index $image "containerimage.digest" }}" + command: + - /bin/sh + - /scripts/backup.sh + env: + - name: REPO_PREFIX + value: {{ required "s3Bucket is not specified!" .Values.backup.s3Bucket | quote }} + - name: CLEANUP_STRATEGY + value: {{ required "cleanupStrategy is not specified!" .Values.backup.cleanupStrategy | quote }} + - name: PGUSER + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgres-superuser + key: username + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgres-superuser + key: password + - name: PGHOST + value: {{ .Release.Name }}-postgres-rw + - name: PGPORT + value: "5432" + - name: PGDATABASE + value: postgres + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-backup + key: s3AccessKey + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-backup + key: s3SecretKey + - name: AWS_DEFAULT_REGION + value: {{ .Values.backup.s3Region }} + - name: RESTIC_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-backup + key: resticPassword + volumeMounts: + - mountPath: /scripts + name: scripts + - mountPath: /tmp + name: tmp + - mountPath: /.cache + name: cache + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true + volumes: + - name: scripts + secret: + secretName: {{ .Release.Name }}-backup-script + - name: tmp + emptyDir: {} + - name: cache + emptyDir: {} + securityContext: + runAsNonRoot: true + runAsUser: 9000 + runAsGroup: 9000 + seccompProfile: + type: RuntimeDefault +{{- end }} diff --git a/packages/apps/ferretdb/templates/backup-script.yaml b/packages/apps/ferretdb/templates/backup-script.yaml new file mode 100644 index 00000000..362bdc01 --- /dev/null +++ b/packages/apps/ferretdb/templates/backup-script.yaml @@ -0,0 +1,50 @@ +{{- if .Values.backup.enabled }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-backup-script +stringData: + backup.sh: | + #!/bin/sh + set -e + set -o pipefail + + JOB_ID="job-$(uuidgen|cut -f1 -d-)" + DB_LIST=$(psql -Atq -c 'SELECT datname FROM pg_catalog.pg_database;' | grep -v '^\(postgres\|app\|template.*\)$') + echo DB_LIST=$(echo "$DB_LIST" | shuf) # shuffle list + echo "Job ID: $JOB_ID" + echo "Target repo: $REPO_PREFIX" + echo "Cleanup strategy: $CLEANUP_STRATEGY" + echo "Start backup for:" + echo "$DB_LIST" + echo + echo "Backup started at `date +%Y-%m-%d\ %H:%M:%S`" + for db in $DB_LIST; do + ( + set -x + restic -r "s3:${REPO_PREFIX}/$db" cat config >/dev/null 2>&1 || \ + restic -r "s3:${REPO_PREFIX}/$db" init --repository-version 2 + restic -r "s3:${REPO_PREFIX}/$db" unlock --remove-all >/dev/null 2>&1 || true # no locks, k8s takes care of it + pg_dump -Z0 -Ft -d "$db" | \ + restic -r "s3:${REPO_PREFIX}/$db" backup --tag "$JOB_ID" --stdin --stdin-filename dump.tar + restic -r "s3:${REPO_PREFIX}/$db" tag --tag "$JOB_ID" --set "completed" + ) + done + echo "Backup finished at `date +%Y-%m-%d\ %H:%M:%S`" + + echo + echo "Run cleanup:" + echo + + echo "Cleanup started at `date +%Y-%m-%d\ %H:%M:%S`" + for db in $DB_LIST; do + ( + set -x + restic forget -r "s3:${REPO_PREFIX}/$db" --group-by=tags --keep-tag "completed" # keep completed snapshots only + restic forget -r "s3:${REPO_PREFIX}/$db" --group-by=tags $CLEANUP_STRATEGY + restic prune -r "s3:${REPO_PREFIX}/$db" + ) + done + echo "Cleanup finished at `date +%Y-%m-%d\ %H:%M:%S`" +{{- end }} diff --git a/packages/apps/ferretdb/templates/backup-secret.yaml b/packages/apps/ferretdb/templates/backup-secret.yaml new file mode 100644 index 00000000..be221e2f --- /dev/null +++ b/packages/apps/ferretdb/templates/backup-secret.yaml @@ -0,0 +1,11 @@ +{{- if .Values.backup.enabled }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-backup +stringData: + s3AccessKey: {{ required "s3AccessKey is not specified!" .Values.backup.s3AccessKey }} + s3SecretKey: {{ required "s3SecretKey is not specified!" .Values.backup.s3SecretKey }} + resticPassword: {{ required "resticPassword is not specified!" .Values.backup.resticPassword }} +{{- end }} diff --git a/packages/apps/ferretdb/templates/external-svc.yaml b/packages/apps/ferretdb/templates/external-svc.yaml new file mode 100644 index 00000000..2e0501c8 --- /dev/null +++ b/packages/apps/ferretdb/templates/external-svc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} +spec: + type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }} + {{- if .Values.external }} + externalTrafficPolicy: Local + allocateLoadBalancerNodePorts: false + {{- end }} + ports: + - name: ferretdb + port: 27017 + selector: + app: {{ .Release.Name }} diff --git a/packages/apps/ferretdb/templates/ferretdb.yaml b/packages/apps/ferretdb/templates/ferretdb.yaml new file mode 100644 index 00000000..8d72787b --- /dev/null +++ b/packages/apps/ferretdb/templates/ferretdb.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ .Release.Name }} + spec: + containers: + - name: ferretdb + image: ghcr.io/ferretdb/ferretdb:1.22.0 + ports: + - containerPort: 27017 + env: + - name: FERRETDB_POSTGRESQL_URL + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgres-app + key: uri diff --git a/packages/apps/ferretdb/templates/init-job.yaml b/packages/apps/ferretdb/templates/init-job.yaml new file mode 100644 index 00000000..b7b03133 --- /dev/null +++ b/packages/apps/ferretdb/templates/init-job.yaml @@ -0,0 +1,66 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-init-job + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + template: + metadata: + name: {{ .Release.Name }}-init-job + annotations: + checksum/config: {{ include (print $.Template.BasePath "/init-script.yaml") . | sha256sum }} + spec: + restartPolicy: Never + containers: + - name: postgres + image: ghcr.io/cloudnative-pg/postgresql:15.3 + command: + - bash + - /scripts/init.sh + env: + - name: PGUSER + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgres-superuser + key: username + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgres-superuser + key: password + - name: PGHOST + value: {{ .Release.Name }}-postgres-rw + - name: PGPORT + value: "5432" + - name: PGDATABASE + value: postgres + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true + volumeMounts: + - mountPath: /etc/secret + name: secret + - mountPath: /scripts + name: scripts + securityContext: + fsGroup: 26 + runAsGroup: 26 + runAsNonRoot: true + runAsUser: 26 + seccompProfile: + type: RuntimeDefault + volumes: + - name: secret + secret: + secretName: {{ .Release.Name }}-postgres-superuser + - name: scripts + secret: + secretName: {{ .Release.Name }}-init-script diff --git a/packages/apps/ferretdb/templates/init-script.yaml b/packages/apps/ferretdb/templates/init-script.yaml new file mode 100644 index 00000000..3917f8fd --- /dev/null +++ b/packages/apps/ferretdb/templates/init-script.yaml @@ -0,0 +1,104 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-init-script +stringData: + init.sh: | + #!/bin/bash + set -e + echo "== create users" + {{- if .Values.users }} + psql -v ON_ERROR_STOP=1 <<\EOT + {{- range $user, $u := .Values.users }} + SELECT 'CREATE ROLE {{ $user }} LOGIN INHERIT;' + WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '{{ $user }}')\gexec + ALTER ROLE {{ $user }} WITH PASSWORD '{{ $u.password }}' LOGIN INHERIT {{ ternary "REPLICATION" "NOREPLICATION" (default false $u.replication) }}; + COMMENT ON ROLE {{ $user }} IS 'user managed by helm'; + {{- end }} + EOT + {{- end }} + + echo "== delete users" + MANAGED_USERS=$(echo '\du+' | psql | awk -F'|' '$4 == " user managed by helm" {print $1}' | awk NF=NF RS= OFS=' ') + DEFINED_USERS="{{ join " " (keys .Values.users) }}" + DELETE_USERS=$(for user in $MANAGED_USERS; do case " $DEFINED_USERS " in *" $user "*) :;; *) echo $user;; esac; done) + + echo "users to delete: $DELETE_USERS" + for user in $DELETE_USERS; do + # https://stackoverflow.com/a/51257346/2931267 + psql -v ON_ERROR_STOP=1 --echo-all <