diff --git a/README.md b/README.md index 6f084bc..9c73ac2 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,94 @@ This setup is only suitable for development and testing. > > However, please note that provisioning the Persistent Volume (PV) and PVC for storage backends like NFS is outside the scope of this Chart. You will need to provision the PV and PVC separately according to your storage backend's documentation before using the `dictionaries.existingClaim` parameter. +## Database service provider + +By default WProofreader Server runs **without a database**. The chart exposes the +DB feature as **two independent concerns**: + +- **`database`** — *does WProofreader talk to a DB, and how.* When `database.enabled=true` + the chart injects `WPR_ENABLE_DATABASE_PROVIDER` + `WPR_DATABASE_*` env into the + AppServer (it reads them directly at startup since v6.0.0), with the password from a Secret. +- **`databaseProvisioning`** — *do we also create/migrate that DB with db-manager.* + A separate operational decision (usually driven by CI/CD via `--set`). When + `databaseProvisioning.enabled=true` the chart runs the + [`db-manager`](https://github.com/WebSpellChecker/db-manager) image as a + `pre-install`/`pre-upgrade` Helm hook Job that bootstraps the database, applies the + versioned migrations, and creates the `appserver` and `app_manager` users + with per-table grants — **before** the AppServer pods start. + +The two are independent: point `database` at an already-provisioned DB and leave +`databaseProvisioning` off, or run provisioning on its own. + +> The MySQL server itself is **not** created by this chart. Point `database.host` +> at an existing MySQL (in-cluster, RDS, etc.). The provisioning Job connects as +> a privileged user (`databaseProvisioning.rootUser`, default `root`) to bootstrap +> the schema and create the application users. + +### Minimal example (connect + provision) + +```shell +helm install wproofreader-app ./wproofreader --namespace wsc --create-namespace \ + --set licenseTicketID=$WPR_LICENSE_TICKET_ID \ + --set database.enabled=true \ + --set database.host=my-mysql-primary.wsc.svc.cluster.local \ + --set database.appServerPassword=$APPSERVER_PASSWORD \ + --set databaseProvisioning.enabled=true \ + --set databaseProvisioning.rootPassword=$MYSQL_ROOT_PASSWORD \ + --set databaseProvisioning.appManagerPassword=$APP_MANAGER_PASSWORD +``` + +### Connect to an already-provisioned DB (no provisioning) + +```shell +helm install wproofreader-app ./wproofreader --namespace wsc \ + --set licenseTicketID=$WPR_LICENSE_TICKET_ID \ + --set database.enabled=true \ + --set database.host=my-mysql-primary.wsc.svc.cluster.local \ + --set database.appServerPassword=$APPSERVER_PASSWORD +``` + +### Using an existing Secret + +Pre-create a Secret with key `appserver-password` (and, when provisioning, +`root-password` + `app-manager-password`), then reference it via +`database.existingSecret` — the chart renders no Secret of its own: + +```shell +kubectl -n wsc create secret generic wpr-db \ + --from-literal=appserver-password=... \ + --from-literal=root-password=... \ + --from-literal=app-manager-password=... + +helm install wproofreader-app ./wproofreader --namespace wsc \ + --set licenseTicketID=$WPR_LICENSE_TICKET_ID \ + --set database.enabled=true \ + --set database.host=my-mysql-primary.wsc.svc.cluster.local \ + --set database.existingSecret=wpr-db \ + --set databaseProvisioning.enabled=true +``` + +### Key parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `database.enabled` | `false` | WProofreader uses a DB service provider | +| `database.host` | `""` | MySQL host (required when enabled or provisioning) | +| `database.port` | `3306` | MySQL port | +| `database.name` | `cloud_service` | Service database/schema name | +| `database.user` | `appserver` | User WProofreader authenticates as | +| `database.appServerPassword` | `""` | Password for `database.user` (or use `existingSecret`) | +| `database.existingSecret` | `""` | Pre-made Secret instead of inline passwords | +| `databaseProvisioning.enabled` | `false` | Run the db-manager provisioning Job | +| `databaseProvisioning.rootPassword` | `""` | Privileged password to bootstrap + create users | +| `databaseProvisioning.appManagerPassword` | `""` | Password for the `app_manager` user | +| `databaseProvisioning.image.repository` | `…/db-manager` | db-manager image (ECR) | +| `databaseProvisioning.contexts` | `internal,seed` | Migration contexts (`internal` = schema only for prod) | +| `databaseProvisioning.migrationMode` | `bootstrap` | `bootstrap` creates the DB if missing | + +> With `databaseProvisioning.enabled=false`, the chart only wires the connection +> env — you must ensure the schema and the `appserver` user already exist. + ## Use in production For production deployments, it is highly recommended **to specify resource requests and limits for your Kubernetes pods**. This helps ensure that your applications have the necessary resources to run efficiently while preventing them from consuming excessive resources on the cluster which can impact other applications. diff --git a/manifests/db-credentials-secret.yaml b/manifests/db-credentials-secret.yaml new file mode 100644 index 0000000..9abd06e --- /dev/null +++ b/manifests/db-credentials-secret.yaml @@ -0,0 +1,33 @@ +# Example DB credentials Secret for the `database.existingSecret` option. +# +# Use this when you prefer to manage the database passwords yourself (sealed-secrets, +# SOPS, Vault, or plain kubectl) instead of passing them through Helm values. +# +# Usage: +# 1. Edit the passwords below (stringData is plaintext for convenience). +# 2. kubectl apply -n -f manifests/db-credentials-secret.yaml +# 3. helm install ... \ +# --set database.enabled=true \ +# --set database.host= \ +# --set database.existingSecret=wproofreader-db-credentials \ +# --set databaseProvisioning.enabled=true # if you also provision with db-manager +# +# Required keys: +# - appserver-password ALWAYS (WProofreader logs in as the appserver user) +# - root-password only when databaseProvisioning.enabled (db-manager bootstraps the DB) +# - app-manager-password only when databaseProvisioning.enabled (creates the app_manager user) +# +# When databaseProvisioning is OFF you can drop root-password and app-manager-password. +apiVersion: v1 +kind: Secret +metadata: + name: wproofreader-db-credentials + # namespace: wsc # uncomment / set, or pass -n to kubectl +type: Opaque +stringData: + # privileged password db-manager uses to bootstrap + create users + root-password: "change-me-root" + # password WProofreader authenticates with (database.user, default "appserver") + appserver-password: "change-me-appserver" + # password for the app_manager user db-manager creates (App-Manager login) + app-manager-password: "change-me-app-manager" diff --git a/wproofreader/Chart.yaml b/wproofreader/Chart.yaml index 27922b1..c6e4f64 100644 --- a/wproofreader/Chart.yaml +++ b/wproofreader/Chart.yaml @@ -4,6 +4,6 @@ description: A Helm chart for deploying webspellchecker/wproofreader in Kubernet type: application -version: 1.3.1 +version: 1.4.0 -appVersion: "6.11.0.0" +appVersion: "6.13.1.0" diff --git a/wproofreader/templates/NOTES.txt b/wproofreader/templates/NOTES.txt index d7bf662..97216c1 100644 --- a/wproofreader/templates/NOTES.txt +++ b/wproofreader/templates/NOTES.txt @@ -13,4 +13,14 @@ Version: Port forwarding to the app server can be enabled with the following command (sudo might be required on some platforms) kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "wproofreader.fullname" . }} {{ .Values.webPort | default (include "wproofreader.defaultWebPort" .) }}:{{ include "wproofreader.servicePort" . }} +{{- if .Values.database.enabled }} + +Database service provider is ENABLED (schema "{{ .Values.database.name }}" on {{ .Values.database.host }}). +{{- end }} +{{- if .Values.databaseProvisioning.enabled }} + db-manager provisioning ran as a pre-install/pre-upgrade hook (schema "{{ .Values.database.name }}" on {{ .Values.database.host }}). Check it with: + kubectl logs --namespace {{ .Release.Namespace }} job/{{ include "wproofreader.fullname" . }}-db-provision +{{- else if .Values.database.enabled }} + Provisioning is OFF (databaseProvisioning.enabled=false) — ensure the schema and the "{{ .Values.database.user }}" user already exist in {{ .Values.database.name }}. +{{- end }} diff --git a/wproofreader/templates/_helpers.tpl b/wproofreader/templates/_helpers.tpl index 8551493..da5fcca 100644 --- a/wproofreader/templates/_helpers.tpl +++ b/wproofreader/templates/_helpers.tpl @@ -70,6 +70,18 @@ Create the name of the PersistentVolumeClaim. {{- end }} {{- end }} +{{/* +Name of the Secret holding database credentials. +Uses database.existingSecret when provided, otherwise a chart-managed Secret. +*/}} +{{- define "wproofreader.dbSecretName" -}} +{{- if .Values.database.existingSecret -}} +{{- .Values.database.existingSecret -}} +{{- else -}} +{{- printf "%s-db" (include "wproofreader.fullname" .) -}} +{{- end -}} +{{- end }} + {{/* Prints key-value pairs encoded in base 64. */}} diff --git a/wproofreader/templates/db-provision-job.yaml b/wproofreader/templates/db-provision-job.yaml new file mode 100644 index 0000000..e6f8ee4 --- /dev/null +++ b/wproofreader/templates/db-provision-job.yaml @@ -0,0 +1,100 @@ +{{- if .Values.databaseProvisioning.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "wproofreader.fullname" . }}-db-provision + labels: + {{- include "wproofreader.labels" . | nindent 4 }} + app.kubernetes.io/component: db-provision + annotations: + # Runs after the db-secret (weight -10) and before WProofreader pods start, + # so the schema and users exist before the AppServer connects. + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + backoffLimit: {{ .Values.databaseProvisioning.backoffLimit }} + {{- with .Values.databaseProvisioning.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ . }} + {{- end }} + template: + metadata: + labels: + {{- include "wproofreader.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: db-provision + spec: + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: db-manager + # tag defaults to the chart appVersion (the WProofreader version) so the + # db-manager image always carries the matching per-version schema migrations. + image: "{{ .Values.databaseProvisioning.image.repository }}:{{ .Values.databaseProvisioning.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.databaseProvisioning.image.pullPolicy }} + # 'args' overrides the image CMD; the entrypoint (/usr/local/bin/entrypoint.sh) + # still runs and receives "migrate" to execute the full provisioning lifecycle. + args: ["migrate"] + env: + # --- target database (connection comes from the `database` block) --- + - name: DB_HOST + value: {{ required "database.host is required when databaseProvisioning.enabled" .Values.database.host | quote }} + - name: DB_PORT + value: {{ .Values.database.port | quote }} + - name: DB_NAME + value: {{ .Values.database.name | quote }} + # --- privileged login db-manager uses to bootstrap + create users --- + - name: DB_USER + value: {{ .Values.databaseProvisioning.rootUser | quote }} + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "wproofreader.dbSecretName" . }} + key: root-password + # --- migration behaviour --- + - name: MIGRATION_MODE + value: {{ .Values.databaseProvisioning.migrationMode | quote }} + - name: DB_CONTEXTS + value: {{ .Values.databaseProvisioning.contexts | quote }} + - name: PROVISION_PREDEFINED_USERS + value: {{ .Values.databaseProvisioning.provisionPredefinedUsers | quote }} + # --- appserver user (WProofreader login) — name from `database`, password shared --- + - name: APPSERVER_USERNAME + value: {{ .Values.database.user | quote }} + - name: APPSERVER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "wproofreader.dbSecretName" . }} + key: appserver-password + # --- app_manager user (App-Manager login) --- + - name: APP_MANAGER_USERNAME + value: {{ .Values.databaseProvisioning.appManagerUsername | quote }} + - name: APP_MANAGER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "wproofreader.dbSecretName" . }} + key: app-manager-password + optional: true + # --- safety + resilience --- + - name: CREATE_BACKUP + value: {{ .Values.databaseProvisioning.createBackup | quote }} + - name: AUTO_ROLLBACK_ON_FAILURE + value: {{ .Values.databaseProvisioning.autoRollbackOnFailure | quote }} + - name: DB_MAX_RETRIES + value: {{ .Values.databaseProvisioning.maxRetries | quote }} + - name: DB_RETRY_INTERVAL + value: {{ .Values.databaseProvisioning.retryInterval | quote }} + - name: CHANGELOG_LOCK_STALE_MINUTES + value: {{ .Values.databaseProvisioning.changelogLockStaleMinutes | quote }} + - name: LOG_LEVEL + value: {{ .Values.databaseProvisioning.logLevel | quote }} + {{- with .Values.databaseProvisioning.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.databaseProvisioning.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} diff --git a/wproofreader/templates/db-secret.yaml b/wproofreader/templates/db-secret.yaml new file mode 100644 index 0000000..52994d4 --- /dev/null +++ b/wproofreader/templates/db-secret.yaml @@ -0,0 +1,28 @@ +{{- if and (or .Values.database.enabled .Values.databaseProvisioning.enabled) (not .Values.database.existingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "wproofreader.dbSecretName" . }} + labels: + {{- include "wproofreader.labels" . | nindent 4 }} + {{- if .Values.databaseProvisioning.enabled }} + annotations: + # When provisioning is enabled this Secret must exist before the db-manager + # Job (lower weight). before-hook-creation only (no hook-succeeded) so it + # persists for the long-running WProofreader Deployment to reference. + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation + {{- end }} +type: Opaque +stringData: + # WProofreader logs in as the appserver user; db-manager also needs this to + # create that user, so it is required whenever the DB feature is on. + appserver-password: {{ required "database.appServerPassword is required when the database feature is enabled (or provide database.existingSecret)" .Values.database.appServerPassword | quote }} + {{- if .Values.databaseProvisioning.enabled }} + root-password: {{ required "databaseProvisioning.rootPassword is required when databaseProvisioning.enabled (or provide database.existingSecret)" .Values.databaseProvisioning.rootPassword | quote }} + {{- with .Values.databaseProvisioning.appManagerPassword }} + app-manager-password: {{ . | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/wproofreader/templates/deployment.yaml b/wproofreader/templates/deployment.yaml index 57106d1..f468b86 100644 --- a/wproofreader/templates/deployment.yaml +++ b/wproofreader/templates/deployment.yaml @@ -74,6 +74,35 @@ spec: value: {{ .Values.webPort | default (include "wproofreader.defaultWebPort" .) | quote }} - name: WPR_VIRTUAL_DIR value: {{ .Values.virtualDir }} + {{- if .Values.database.enabled }} + - name: WPR_ENABLE_DATABASE_PROVIDER + value: "true" + - name: WPR_DATABASE_HOST + value: {{ required "database.host is required when database.enabled" .Values.database.host | quote }} + - name: WPR_DATABASE_PORT + value: {{ .Values.database.port | quote }} + - name: WPR_DATABASE_SCHEMA + value: {{ .Values.database.name | quote }} + - name: WPR_DATABASE_USER + value: {{ .Values.database.user | quote }} + - name: WPR_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "wproofreader.dbSecretName" . }} + key: appserver-password + {{- if .Values.database.enableRequestStatistic }} + - name: WPR_ENABLE_REQUEST_STATISTIC + value: "true" + {{- end }} + {{- if .Values.database.enableUserActionStatistic }} + - name: WPR_ENABLE_USER_ACTION_STATISTIC + value: "true" + {{- end }} + {{- if .Values.database.enableRequestValidation }} + - name: WPR_ENABLE_REQUEST_VALIDATION + value: "true" + {{- end }} + {{- end }} {{- with .Values.extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/wproofreader/values.yaml b/wproofreader/values.yaml index 3030290..d22663e 100644 --- a/wproofreader/values.yaml +++ b/wproofreader/values.yaml @@ -180,3 +180,92 @@ strategy: # See "Taints and Tolerations": https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ tolerations: [] + +#### Database connection (optional) +# Connection config for WProofreader's database service provider (WPR_DATABASE_* env). +# Does NOT create the database — point `host` at an existing MySQL. +database: + # master switch — does WProofreader use a database service provider? + enabled: false + + # MySQL host holding the WProofreader service schema + # e.g. wpr-mysql-primary..svc.cluster.local + host: "" + # MySQL port + port: 3306 + # service database/schema name + name: cloud_service + # user WProofreader authenticates as + user: appserver + + # password for `user` above (WPR_DATABASE_PASSWORD); ignored if existingSecret is set + appServerPassword: "" + + # use a pre-existing Secret instead of inline passwords; the chart then renders + # no Secret of its own. Required keys: + # - appserver-password (always) + # - root-password, app-manager-password (also, when databaseProvisioning.enabled) + existingSecret: "" + + # WProofreader DB-backed features (only applied when enabled) + enableRequestStatistic: true + enableUserActionStatistic: true + enableRequestValidation: true + +#### Database provisioning (optional) +# Provision the database above with db-manager (schema, migrations, users) via a +# pre-install/pre-upgrade hook Job. Independent of `database.enabled`; often set by CI/CD. +databaseProvisioning: + enabled: false + + image: + # db-manager image repository (no tag) + repository: webspellchecker/db-manager + # image tag; empty defaults to the chart appVersion (must match the WProofreader version) + tag: "" + # options: IfNotPresent, Never, Always + pullPolicy: IfNotPresent + + # privileged user db-manager connects as to bootstrap the database + rootUser: root + # privileged password; ignored if database.existingSecret is set + rootPassword: "" + # App-Manager database user db-manager creates + appManagerUsername: app_manager + # App-Manager login password; ignored if database.existingSecret is set + appManagerPassword: "" + + # migration mode: bootstrap (create DB if missing) | adopt | schema-only + migrationMode: bootstrap + # migration contexts + contexts: "external,seed" + # create the appserver + app_manager users (with grants) during provisioning + provisionPredefinedUsers: true + # take a backup before migrating + createBackup: false + # restore from backup if the migration fails + autoRollbackOnFailure: false + + # connection retry — db-manager waits for MySQL to be reachable + maxRetries: 60 + retryInterval: 2 + # minutes before a stale changelog lock is released (K8s crash resilience) + changelogLockStaleMinutes: 10 + # log level: ERROR, WARN, INFO, DEBUG, TRACE + logLevel: INFO + # Job backoffLimit (retries before the install fails) + backoffLimit: 3 + # delete the finished Job after this many seconds + ttlSecondsAfterFinished: 3600 + + # extra environment variables to pass to the db-manager container + extraEnv: [] + + # Resource requests/limits for the provisioning Job + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi