diff --git a/charts/dgraph/Chart.yaml b/charts/dgraph/Chart.yaml index edc4489aa..8bf2b8517 100644 --- a/charts/dgraph/Chart.yaml +++ b/charts/dgraph/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 name: dgraph type: application -version: 25.3.1-preview2 -appVersion: v25.3.1 +version: 25.3.8 +appVersion: v25.3.8 description: Dgraph is a horizontally scalable and distributed graph database, providing ACID transactions, consistent replication and linearizable reads. keywords: - dgraph diff --git a/charts/dgraph/README.md b/charts/dgraph/README.md index d21724d5b..dd082a4f0 100644 --- a/charts/dgraph/README.md +++ b/charts/dgraph/README.md @@ -44,6 +44,12 @@ No manual intervention is required. Also review the [additional breaking changes **Backup admin password now required**: When `alpha.acl.enabled` is true and backups are enabled, `backups.admin.password` must be explicitly set. Previously the chart would silently render an empty secret, which would cause backup failures at runtime. The chart now fails at install/upgrade time with a clear error message if the password is missing. +**ACL and encryption flags now auto-activate**: Setting `alpha.acl.enabled: true` (or `alpha.encryption.enabled: true`) now synthesizes the matching `--acl` (or `--encryption`) superflag onto the Alpha command automatically; previously these flags had to be added by hand through `alpha.extraFlags`. If you already pass `--acl` or `--encryption` through `alpha.extraFlags`, remove it — the chart fails rendering rather than pass the flag twice. The chart points the flag at `/dgraph/acl/` and `/dgraph/enc/`, which default to `hmac_secret_file` and `enc_key_file`; override those keys if your Secret stores the file under a different name. + +**TLS now activates from the tls block**: Setting `alpha.tls.enabled: true` (or `zero.tls.enabled: true`) now synthesizes the `--tls` superflag onto the Alpha (or Zero) command; previously the flag had to be added by hand through `extraFlags`. If you already pass `--tls` through `alpha.extraFlags` or `zero.extraFlags`, remove it — the chart fails rendering rather than pass the flag twice. Configure TLS through the new `tls.internalPort`, `tls.clientName`, and `tls.clientAuthType` keys; the chart reads the cert files from `/dgraph/tls` (`ca.crt`, `node.crt`, `node.key`, and `client..crt`/`.key`). + +**Health probes use HTTPS when TLS is enabled**: With `alpha.tls.enabled: true` (or `zero.tls.enabled: true`), the built-in httpGet startup, liveness, and readiness probes switch to `scheme: HTTPS`. A cert-requiring `clientAuthType` (`REQUIREANY` or `REQUIREANDVERIFY`) makes every client present a certificate, which the kubelet's probes cannot; the chart fails rendering in that case. Set `clientAuthType: VERIFYIFGIVEN`, or supply exec probes through `customStartupProbe`/`customLivenessProbe`/`customReadinessProbe`. A cert-requiring `clientAuthType` also requires `clientName` so in-cluster callers (inter-node TLS) can present a client cert. + ### Installing the Chart To install the chart with the release name `my-release`: @@ -82,13 +88,15 @@ The following table lists the configurable parameters of the `dgraph` chart and | Parameter | Description | Default | | ---------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------- | | `commonLabels` | Labels to add to all resources and pod templates | `{}` | +| `imagePullSecrets` | Array of imagePullSecrets applied to every Pod (plain strings or `{name: ...}` objects); takes precedence over `global.imagePullSecrets` and `image.pullSecrets` | `[]` | | `image.registry` | Container registry name | `docker.io` | | `image.repository` | Container image name | `dgraph/dgraph` | -| `image.tag` | Container image tag | `v25.3.1` | +| `image.tag` | Container image tag | `v25.3.8` | | `image.pullPolicy` | Container pull policy | `IfNotPresent` | | `nameOverride` | Deployment name override (will append the release name) | `nil` | | `namespaceOverride` | Deployment namespace override if specified. | `nil` | | `fullnameOverride` | Deployment full name override (the release name is ignored) | `nil` | +| `preUpgradeHook.enabled` | Run the v24-to-v25 StatefulSet selector migration Job on `helm upgrade` | `true` | | `preUpgradeHook.image.registry` | Pre-upgrade hook image registry | `docker.io` | | `preUpgradeHook.image.repository` | Pre-upgrade hook image repository | `bitnami/kubectl` | | `preUpgradeHook.image.tag` | Pre-upgrade hook image tag | `1.31` | @@ -105,6 +113,9 @@ The following table lists the configurable parameters of the `dgraph` chart and | `zero.updateStrategy` | Strategy for upgrading zero nodes | `RollingUpdate` | | `zero.schedulerName` | Configure an explicit scheduler | `nil` | | `zero.monitorLabel` | "monitor" label on the zero Service (for Prometheus service discovery) | `zero-dgraph-io` | +| `zero.pdb.enabled` | Enable a PodDisruptionBudget for the zero StatefulSet | `false` | +| `zero.pdb.minAvailable` | Minimum available zero pods during voluntary disruptions | `2` | +| `zero.pdb.maxUnavailable` | Maximum unavailable zero pods (alternative to `minAvailable`) | `nil` | | `zero.rollingUpdatePartition` | Partition update strategy | `nil` | | `zero.podManagementPolicy` | Pod management policy for zero nodes | `OrderedReady` | | `zero.replicaCount` | Number of zero nodes | `3` | @@ -116,6 +127,11 @@ The following table lists the configurable parameters of the `dgraph` chart and | `zero.envFrom` | Extra environment variables loaded from configmap(s) and/or secret(s) | `[]` | | `zero.extraEnvs` | extra env vars | `[]` | | `zero.extraFlags` | Zero extra flags for command line | `""` | +| `zero.logLevel` | Verbosity (glog `-v`): `normal`/`verbose`/`debug`/`trace`, or a raw integer | `normal` | +| `zero.vmodule` | Per-module glog verbosity (`--vmodule`), e.g. `server=3,raft=2` | `""` | +| `zero.logtostderr` | Log to the container's stderr (glog `--logtostderr`) | `true` | +| `zero.alsologtostderr` | Also write logs under `logDir` in addition to stderr | `false` | +| `zero.logDir` | Directory for glog file output (`--log_dir`); used when `logtostderr=false` or `alsologtostderr=true` | `""` | | `zero.configFile` | Zero config file | `{}` | | `zero.automountServiceAccountToken` | automatically mount a ServiceAccount API credentials | `true` | | `zero.service.type` | Zero service type | `ClusterIP` | @@ -129,10 +145,15 @@ The following table lists the configurable parameters of the `dgraph` chart and | `zero.securityContext.enabled` | Security context for zero nodes enabled | `false` | | `zero.securityContext.fsGroup` | Group id of the zero container | `1001` | | `zero.securityContext.runAsUser` | User ID for the zero container | `1001` | +| `zero.containerSecurityContext.enabled` | Enable the zero container securityContext (drop ALL capabilities, forbid privilege escalation) | `false` | +| `zero.containerSecurityContext.allowPrivilegeEscalation` | Allow privilege escalation for the zero container | `false` | +| `zero.containerSecurityContext.readOnlyRootFilesystem` | Mount the zero container's root filesystem read-only | `false` | +| `zero.containerSecurityContext.capabilities.drop` | Linux capabilities dropped from the zero container | `['ALL']` | | `zero.persistence.enabled` | Enable persistence for zero using PVC | `true` | | `zero.persistence.storageClass` | PVC Storage Class for zero volume | `nil` | | `zero.persistence.accessModes` | PVC Access Mode for zero volume | `['ReadWriteOnce']` | | `zero.persistence.size` | PVC Storage Request for zero volume | `32Gi` | +| `zero.persistence.persistentVolumeClaimRetentionPolicy` | PVC retention policy for the zero StatefulSet (`whenDeleted`/`whenScaled`, each `Retain` or `Delete`); requires Kubernetes >= 1.27. Unset keeps the Kubernetes default. | _unset_ | | `zero.nodeSelector` | Node labels for zero pod assignment | `{}` | | `zero.tolerations` | Zero tolerations | `[]` | | `zero.resources.requests.memory` | Zero pod resources memory requests | `100Mi` | @@ -142,11 +163,17 @@ The following table lists the configurable parameters of the `dgraph` chart and | `zero.customStartupProbe` | Zero custom startup probes (if `zero.startupProbe` not enabled) | `{}` | | `zero.customLivenessProbe` | Zero custom liveness probes (if `zero.livenessProbe` not enabled) | `{}` | | `zero.customReadinessProbe` | Zero custom readiness probes (if `zero.readinessProbe` not enabled) | `{}` | +| `zero.tls.internalPort` | Enable TLS on Zero's internal gRPC port (synthesized into `--tls`) | `true` | +| `zero.tls.clientName` | Client cert basename for Zero `--tls` (empty omits the client cert) | `""` | +| `zero.tls.clientAuthType` | Zero `--tls` client-auth-type, e.g. `REQUIREANDVERIFY` (empty omits) | `""` | | `alpha.name` | Alpha component name | `alpha` | | `alpha.metrics.enabled` | Add annotations for Prometheus metric scraping | `true` | | `alpha.extraAnnotations` | Specify annotations for template metadata | `{}` | | `alpha.podLabels` | Specify additional labels for template metadata | `{}` | | `alpha.monitorLabel` | "monitor" label on the alpha Service (for Prometheus service discovery) | `alpha-dgraph-io` | +| `alpha.pdb.enabled` | Enable a PodDisruptionBudget for the alpha StatefulSet | `false` | +| `alpha.pdb.minAvailable` | Minimum available alpha pods during voluntary disruptions | `2` | +| `alpha.pdb.maxUnavailable` | Maximum unavailable alpha pods (alternative to `minAvailable`) | `nil` | | `alpha.updateStrategy` | Strategy for upgrading alpha nodes | `RollingUpdate` | | `alpha.schedulerName` | Configure an explicit scheduler | `nil` | | `alpha.rollingUpdatePartition` | Partition update strategy | `nil` | @@ -159,6 +186,11 @@ The following table lists the configurable parameters of the `dgraph` chart and | `alpha.envFrom` | Extra environment variables loaded from configmap(s) and/or secret(s) | `[]` | | `alpha.extraEnvs` | extra env vars | `[]` | | `alpha.extraFlags` | Alpha extra flags for command | `""` | +| `alpha.logLevel` | Verbosity (glog `-v`): `normal`/`verbose`/`debug`/`trace`, or a raw integer | `normal` | +| `alpha.vmodule` | Per-module glog verbosity (`--vmodule`), e.g. `server=3,raft=2` | `""` | +| `alpha.logtostderr` | Log to the container's stderr (glog `--logtostderr`) | `true` | +| `alpha.alsologtostderr` | Also write logs under `logDir` in addition to stderr | `false` | +| `alpha.logDir` | Directory for glog file output (`--log_dir`); used when `logtostderr=false` or `alsologtostderr=true` | `""` | | `alpha.configFile` | Alpha config file | `{}` | | `alpha.automountServiceAccountToken` | automatically mount a ServiceAccount API credentials | `true` | | `alpha.service.type` | Alpha node service type | `ClusterIP` | @@ -181,16 +213,35 @@ The following table lists the configurable parameters of the `dgraph` chart and | `alpha.securityContext.enabled` | Security context for Alpha nodes enabled | `false` | | `alpha.securityContext.fsGroup` | Group id of the Alpha container | `1001` | | `alpha.securityContext.runAsUser` | User ID for the Alpha container | `1001` | +| `alpha.containerSecurityContext.enabled` | Enable the alpha container securityContext (drop ALL capabilities, forbid privilege escalation) | `false` | +| `alpha.containerSecurityContext.allowPrivilegeEscalation` | Allow privilege escalation for the alpha container | `false` | +| `alpha.containerSecurityContext.readOnlyRootFilesystem` | Mount the alpha container's root filesystem read-only | `false` | +| `alpha.containerSecurityContext.capabilities.drop` | Linux capabilities dropped from the alpha container | `['ALL']` | | `alpha.tls.enabled` | Alpha service TLS enabled | `false` | | `alpha.tls.files` | Alpha service TLS key and certificate files stored as secrets | `false` | -| `alpha.encryption.enabled` | Alpha Encryption at Rest enabled | `false` | +| `alpha.tls.internalPort` | Enable TLS on Alpha's internal gRPC port (synthesized into `--tls`) | `true` | +| `alpha.tls.clientName` | Client cert basename for Alpha `--tls` (empty omits the client cert) | `""` | +| `alpha.tls.clientAuthType` | Alpha `--tls` client-auth-type, e.g. `REQUIREANDVERIFY` (empty omits) | `""` | +| `alpha.encryption.enabled` | Alpha Encryption at Rest enabled (auto-adds `--encryption`) | `false` | +| `alpha.encryption.keyFile` | Filename/key of the encryption key within the mounted Secret | `enc_key_file` | +| `alpha.encryption.existingSecret` | Name of a pre-created Secret holding the encryption key (suppresses the chart's own) | `""` | | `alpha.encryption.file` | Alpha Encryption at Rest key file | `nil` | -| `alpha.acl.enabled` | Alpha ACL enabled | `false` | +| `alpha.acl.enabled` | Alpha ACL enabled (auto-adds `--acl`) | `false` | +| `alpha.acl.secretFile` | Filename/key of the HMAC secret within the mounted Secret | `hmac_secret_file` | +| `alpha.acl.existingSecret` | Name of a pre-created Secret holding the HMAC key (suppresses the chart's own) | `""` | | `alpha.acl.file` | Alpha ACL secret file | `nil` | +| `alpha.acl.bootstrap.enabled` | Enable the post-install/post-upgrade ACL bootstrap reconciler Job | `false` | +| `alpha.acl.bootstrap.existingSecret` | Secret holding the credentials the bootstrap Job reads (defaults to `acl.existingSecret`, else the chart-managed ACL Secret) | `""` | +| `alpha.acl.bootstrap.grootPasswordSecretKey` | Key in the credentials Secret holding groot's rotated password | `groot_password` | +| `alpha.acl.bootstrap.rotation` | Opaque token rendered as a Job pod annotation; change it to force a re-run without touching Alpha | `""` | +| `alpha.acl.bootstrap.image` | Image override for the bootstrap Job (empty reuses the deployed dgraph image) | `{}` | +| `alpha.acl.bootstrap.groups` | Declarative ACL groups (`name`, `rules: [{predicate, permission}]`) the reconciler converges | `[]` | +| `alpha.acl.bootstrap.users` | Declarative ACL users (`name`, `passwordSecretKey`, `groups`) the reconciler converges | `[]` | | `alpha.persistence.enabled` | Enable persistence for alpha using PVC | `true` | | `alpha.persistence.storageClass` | PVC Storage Class for alpha volume | `nil` | | `alpha.persistence.accessModes` | PVC Access Mode for alpha volume | `['ReadWriteOnce']` | | `alpha.persistence.size` | PVC Storage Request for alpha volume | `100Gi` | +| `alpha.persistence.persistentVolumeClaimRetentionPolicy` | PVC retention policy for the alpha StatefulSet (`whenDeleted`/`whenScaled`, each `Retain` or `Delete`); requires Kubernetes >= 1.27. Unset keeps the Kubernetes default. | _unset_ | | `alpha.nodeSelector` | Node labels for alpha pod assignment | `{}` | | `alpha.tolerations` | Alpha tolerations | `[]` | | `alpha.resources.requests.memory` | Zero pod resources memory request | `100Mi` | @@ -238,6 +289,10 @@ The following table lists the configurable parameters of the `dgraph` chart and | `ratel.securityContext.enabled` | Security context for ratel nodes enabled | `false` | | `ratel.securityContext.fsGroup` | Group id of the ratel container | `1001` | | `ratel.securityContext.runAsUser` | User ID for the ratel container | `1001` | +| `ratel.containerSecurityContext.enabled` | Enable the ratel container securityContext (drop ALL capabilities, forbid privilege escalation) | `false` | +| `ratel.containerSecurityContext.allowPrivilegeEscalation` | Allow privilege escalation for the ratel container | `false` | +| `ratel.containerSecurityContext.readOnlyRootFilesystem` | Mount the ratel container's root filesystem read-only | `false` | +| `ratel.containerSecurityContext.capabilities.drop` | Linux capabilities dropped from the ratel container | `['ALL']` | | `ratel.resources.requests` | Ratel pod resources requests | `nil` | | `ratel.livenessProbe` | Ratel liveness probes | See `values.yaml` for defaults | | `ratel.readinessProbe` | Ratel readiness probes | See `values.yaml` for defaults | @@ -249,6 +304,8 @@ The following table lists the configurable parameters of the `dgraph` chart and | `backups.schedulerName` | Configure an explicit scheduler for Backups Kubernetes CronJobs | `nil` | | `backups.admin.user` | Login user for backups (required if ACL enabled) | `groot` | | `backups.admin.password` | Login user password for backups (required if ACL enabled) | `nil` | +| `backups.admin.existingSecret` | Pre-created Secret holding the backup admin password (chart mints none of its own when set); ignored unless `alpha.acl.enabled` | `""` | +| `backups.admin.passwordSecretKey` | Key within `existingSecret` (or the chart's own backups Secret) holding the backup admin password | `backup_admin_password` | | `backups.admin.tls_client` | TLS Client Name (requried if `REQUIREANY` or `REQUIREANDVERIFY` set) | `nil` | | `backups.admin.auth_token` | Auth Token | `nil` | | `backups.image.registry` | Container registry name | `docker.io` | @@ -278,6 +335,34 @@ The following table lists the configurable parameters of the `dgraph` chart and | `backups.keys.minio.secret` | Alpha env variable `MINIO_SECRET_KEY` fetched from secrets | "" | | `backups.keys.s3.access` | Alpha env variable `AWS_ACCESS_KEY_ID` fetched from secrets | "" | | `backups.keys.s3.secret` | Alpha env variable `AWS_SECRET_ACCESS_KEY` fetched from secrets | "" | +| `serviceMonitor.enabled` | Create a Prometheus Operator ServiceMonitor for the alpha and zero metrics endpoints (requires the Prometheus Operator CRDs) | `false` | +| `serviceMonitor.namespace` | Namespace for the ServiceMonitor (defaults to the release namespace) | `nil` | +| `serviceMonitor.labels` | Extra labels for the ServiceMonitor (for Prometheus Operator `serviceMonitorSelector`) | `{}` | +| `serviceMonitor.interval` | Scrape interval | `30s` | +| `serviceMonitor.scrapeTimeout` | Scrape timeout | `10s` | +| `serviceMonitor.path` | Metrics endpoint path | `/debug/prometheus_metrics` | +| `prometheusRule.enabled` | Create a Prometheus Operator PrometheusRule with default alerts (requires the Prometheus Operator CRDs) | `false` | +| `prometheusRule.labels` | Extra labels for the PrometheusRule (for Prometheus Operator `ruleSelector`) | `{}` | +| `prometheusRule.defaultRules` | Include the chart's conservative default alerting rules | `true` | +| `prometheusRule.extraRules` | Additional Prometheus alerting rules appended to the PrometheusRule | `[]` | +| `networkPolicy.enabled` | Create a NetworkPolicy restricting ingress to the dgraph pods | `false` | +| `networkPolicy.clientPodLabels` | Pod selector labels allowed as NetworkPolicy ingress clients | `{}` | +| `networkPolicy.extraIngress` | Additional NetworkPolicy ingress rules | `[]` | +| `validation.enabled` | Master switch for the post-install validation subsystem (ConfigMap, test Pod, Job, CronJob, RBAC) | `false` | +| `validation.image` | Validator image override (empty reuses the deployed dgraph image) | `{}` | +| `validation.adminUser` | Account the validator logs in as for auth-dependent checks | `groot` | +| `validation.adminPasswordSecretKey` | Secret key holding `adminUser`'s password (empty derives it) | `""` | +| `validation.job.enabled` | Run validation as a post-install/upgrade hook Job that gates the release | `false` | +| `validation.job.backoffLimit` | Job `backoffLimit` (also used by the manual CronJob's jobTemplate) | `1` | +| `validation.cronjob.enabled` | Create a suspended, manually-triggered CronJob for on-demand validation | `false` | +| `validation.rbac.enabled` | Create the validator ServiceAccount/Role/RoleBinding (required by `checkBackups`) | `false` | +| `validation.checkBackups` | Also assert the backup CronJobs exist with their expected schedules (requires `rbac.enabled`) | `false` | +| `validation.backupRoundtrip` | Trigger a live backup round-trip to S3 (side-effecting, slow; reserved for future use) | `false` | +| `validation.retries` | Per-check retry attempts before failing | `10` | +| `validation.retrySleep` | Seconds between retries | `12` | +| `validation.podAnnotations` | Extra annotations for validator pods | `{}` | +| `validation.nodeSelector` | nodeSelector for validator pods (empty falls back to `alpha.nodeSelector`) | `{}` | +| `validation.tolerations` | tolerations for validator pods (empty falls back to `alpha.tolerations`) | `[]` | | `global.ingress.enabled` | Enable global ingress resource (overrides Alpha/Ratel ingress) | `false` | | `global.ingress.annotations` | global ingress annotations | `{}` | | `global.ingress.tls` | global ingress tls settings | `{}` | diff --git a/charts/dgraph/files/validation/validate.sh b/charts/dgraph/files/validation/validate.sh new file mode 100644 index 000000000..4e914805b --- /dev/null +++ b/charts/dgraph/files/validation/validate.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash +# dgraph post-install validator. +# +# Asserts the running dgraph cluster matches what the chart rendered, read +# from /config/expected.json. Runs in-cluster three ways: a `helm test` Pod, a +# post-install hook Job (gates the release), or the suspended manual CronJob +# (kubectl create job --from=cronjob/...). +# +# Transport is taken from env the pod template sets (mirrors the ACL bootstrap +# reconciler): plaintext by default, or HTTPS with the chart's CA and optional +# client cert under native TLS. The logical expected state comes from +# expected.json, which the chart templates from its own values, so the validator +# cannot drift from what was deployed. +# +# Checks: health, cluster membership, ACL enforcement, admin login, an +# authenticated query, per-user logins, group predicate rules, and (optionally) +# backup CronJob scheduling. When ACL is disabled, the login and auth-dependent +# checks are skipped. No `set -e`: each check aggregates into FAILURES so one +# failure still lets the rest report. Exit 0 = all pass; non-zero = a failure. +# +# check_* helpers are dispatched indirectly through retry()/run() (via "$@"), +# which shellcheck cannot trace; silence the false "never invoked" (SC2329) and +# "unreachable command" (SC2317) it infers for them. +# shellcheck disable=SC2329,SC2317 +set -u + +EXPECTED_JSON="${EXPECTED_JSON:-/config/expected.json}" +CREDS_DIR="${CREDS_DIR:-/creds}" +RETRIES="${RETRIES:-10}" +RETRY_SLEEP="${RETRY_SLEEP:-12}" +K8S_API="${K8S_API:-https://kubernetes.default.svc}" +SA_DIR="/var/run/secrets/kubernetes.io/serviceaccount" +: "${ALPHA_HOST:?ALPHA_HOST not set}" + +# TLS: when the pod supplies a CA path (alpha.tls.enabled), talk HTTPS and present +# the client cert if one was provided. Empty CERTOPTS keeps plaintext. Mirrors the +# ACL bootstrap and backup CronJob curl handling. +SCHEME="http" +CERTOPTS=() +if [ -n "${CACERT_PATH:-}" ]; then + SCHEME="https" + CERTOPTS+=('--cacert' "${CACERT_PATH}") + if [ -n "${CLIENT_CERT_PATH:-}" ] && [ -n "${CLIENT_KEY_PATH:-}" ]; then + CERTOPTS+=('--cert' "${CLIENT_CERT_PATH}" '--key' "${CLIENT_KEY_PATH}") + fi +fi +ALPHA="${SCHEME}://${ALPHA_HOST}:8080" + +NS="$(jq -r '.namespace' "$EXPECTED_JSON")" +ACL_ENABLED="$(jq -r '.aclEnabled' "$EXPECTED_JSON")" + +FAILURES=0 +pass() { printf 'PASS %s\n' "$1"; } +fail() { + printf 'FAIL %s\n' "$1" + FAILURES=$((FAILURES + 1)) +} +log() { printf '%s\n' "$1"; } + +# Alpha-facing curl with the resolved TLS options applied. +curl_alpha() { /usr/bin/curl -fsS "${CERTOPTS[@]}" "$@"; } + +# retry LABEL CMD... : run until exit 0 or RETRIES exhausted, logging each wait so +# a slow first deploy shows progress instead of going silent. +retry() { + _label="$1" + shift + _i=1 + while true; do + if "$@"; then return 0; fi + [ "$_i" -ge "$RETRIES" ] && return 1 + log "$_label: not ready; retry $_i/$RETRIES in ${RETRY_SLEEP}s" + _i=$((_i + 1)) + sleep "$RETRY_SLEEP" + done +} + +# login USER PASSWORD -> accessJWT on stdout, non-zero on failure. Credentials go +# in as GraphQL variables (jq --arg) so any generated password is injection-safe. +login() { + _payload="$(jq -n --arg u "$1" --arg p "$2" \ + '{query:"mutation($u:String!,$p:String!){login(userId:$u,password:$p){response{accessJWT}}}",variables:{u:$u,p:$p}}')" + _resp="$(curl_alpha -X POST "$ALPHA/admin" -H 'Content-Type: application/json' -d "$_payload")" || return 1 + _jwt="$(printf '%s' "$_resp" | jq -r '.data.login.response.accessJWT // empty')" + [ -n "$_jwt" ] || return 1 + printf '%s' "$_jwt" +} + +read_pw() { + _f="$CREDS_DIR/$1" + [ -f "$_f" ] || return 1 + cat "$_f" +} + +# A. Health: /health is public; every instance must report "healthy". +check_health() { + _h="$(curl_alpha "$ALPHA/health")" || return 1 + _total="$(printf '%s' "$_h" | jq 'length')" + _ok="$(printf '%s' "$_h" | jq '[.[] | select(.status == "healthy")] | length')" + [ "$_total" -gt 0 ] && [ "$_total" = "$_ok" ] +} + +# Admin JWT underpins membership/query/groups; fetch with retry before they run. +ADMIN_JWT="" +# Admin auth header args, populated once we hold a JWT. Stays empty when ACL is +# disabled so we never send an empty X-Dgraph-AccessToken, which an auth proxy +# could treat as a failed auth attempt and reject. +AUTH_HDR=() +get_admin_jwt() { + _u="$(jq -r '.adminUser' "$EXPECTED_JSON")" + _k="$(jq -r '.adminPasswordKey' "$EXPECTED_JSON")" + _pw="$(read_pw "$_k")" || return 1 + ADMIN_JWT="$(login "$_u" "$_pw")" || return 1 + [ -n "$ADMIN_JWT" ] || return 1 + AUTH_HDR=(-H "X-Dgraph-AccessToken: $ADMIN_JWT") +} + +# B. Membership: counted Alphas/Zeros in /state match expected. As a +# post-install/upgrade gate this compares against the just-rendered replicaCount, +# so counts match. Note: the zeros count assumes /state lists only live members; +# if a decommissioned zero lingered in /state after a scale-down it would +# over-count and FAIL. Not a concern for the install/upgrade gate, but a manual +# CronJob run after a scale-down could surface it. +check_membership() { + _state="$(curl_alpha "$ALPHA/state" "${AUTH_HDR[@]}")" || return 1 + _a="$(printf '%s' "$_state" | jq '[.groups[].members | length] | add // 0')" + _z="$(printf '%s' "$_state" | jq '(.zeros // {}) | length')" + [ "$_a" = "$(jq -r '.expectedAlphas' "$EXPECTED_JSON")" ] && + [ "$_z" = "$(jq -r '.expectedZeros' "$EXPECTED_JSON")" ] +} + +# C. ACL enforcing: an unauthenticated admin query is rejected. Rejection can be +# a GraphQL 200 with a non-empty errors[] (current Dgraph) OR an HTTP 401/403 +# (an auth proxy or a future Dgraph version). Deliberately not +# curl_alpha here: its -f turns a 4xx rejection into a non-zero exit that would +# read as "not enforcing" — exactly backwards. Assert on the status/body instead. +check_acl_enforcing() { + _out="$(/usr/bin/curl -sS -w '\n%{http_code}' "${CERTOPTS[@]}" \ + -X POST "$ALPHA/admin" -H 'Content-Type: application/json' \ + -d '{"query":"{ queryGroup { name } }"}')" || return 1 + _code="${_out##*$'\n'}" + _body="${_out%$'\n'*}" + # A 401/403 is an explicit rejection => enforcing. + case "$_code" in 401 | 403) return 0 ;; esac + # A 200 counts only if the GraphQL response carries a non-empty errors[]. + [ "$_code" = "200" ] && + [ "$(printf '%s' "$_body" | jq -r '(.errors // []) | length' 2>/dev/null || echo 0)" -gt 0 ] +} + +# D. Authenticated query: a DQL schema read through the auth path returns data. +check_query() { + _resp="$(curl_alpha -X POST "$ALPHA/query" \ + -H 'Content-Type: application/dql' \ + "${AUTH_HDR[@]}" \ + --data-binary 'schema {}')" || return 1 + printf '%s' "$_resp" | jq -e '.data' >/dev/null 2>&1 +} + +# E. Per-user login: every declared account logs in with its stored password. +check_user_logins() { + _rc=0 + _n="$(jq -r '.users | length' "$EXPECTED_JSON")" + _i=0 + while [ "$_i" -lt "$_n" ]; do + _u="$(jq -r ".users[$_i].name" "$EXPECTED_JSON")" + _k="$(jq -r ".users[$_i].passwordKey" "$EXPECTED_JSON")" + _i=$((_i + 1)) + _pw="$(read_pw "$_k")" || { + fail "user-login: $_u (no password key $_k)" + _rc=1 + continue + } + if login "$_u" "$_pw" >/dev/null; then + pass "user-login: $_u" + else + fail "user-login: $_u" + _rc=1 + fi + done + return "$_rc" +} + +# F. Groups + rules: each expected group exists AND carries every expected +# predicate rule (matching predicate and permission). +check_groups() { + _ng="$(jq -r '.groups | length' "$EXPECTED_JSON")" + [ "$_ng" -eq 0 ] && return 0 + _resp="$(curl_alpha -X POST "$ALPHA/admin" -H 'Content-Type: application/json' \ + "${AUTH_HDR[@]}" \ + -d '{"query":"{ queryGroup { name rules { predicate permission } } }"}')" || return 1 + _rc=0 + _i=0 + while [ "$_i" -lt "$_ng" ]; do + _g="$(jq -r ".groups[$_i].name" "$EXPECTED_JSON")" + _exp_rules="$(jq -c ".groups[$_i].rules // []" "$EXPECTED_JSON")" + _i=$((_i + 1)) + _present="$(printf '%s' "$_resp" | jq --arg g "$_g" '[.data.queryGroup[] | select(.name == $g)] | length')" + if [ "${_present:-0}" -lt 1 ]; then + fail "group: $_g (absent)" + _rc=1 + continue + fi + _act_rules="$(printf '%s' "$_resp" | jq -c --arg g "$_g" '([.data.queryGroup[] | select(.name == $g)][0].rules) // []')" + _missing="$(jq -n --argjson e "$_exp_rules" --argjson a "$_act_rules" \ + '[$e[] | . as $r | select(($a | any(.predicate == $r.predicate and .permission == $r.permission)) | not)] | length')" + if [ "${_missing:-0}" -eq 0 ]; then + pass "group: $_g (rules ok)" + else + fail "group: $_g ($_missing expected rule(s) missing)" + _rc=1 + fi + done + return "$_rc" +} + +# G. Backups: each expected backup CronJob exists with the expected schedule. +# Reads the Kubernetes API with the pod's ServiceAccount token (requires the +# validation RBAC). Only runs when expected.json marks backups.check true. +check_backups() { + _token="$(cat "$SA_DIR/token")" + _n="$(jq -r '.backups.cronjobs | length' "$EXPECTED_JSON")" + _rc=0 + _i=0 + while [ "$_i" -lt "$_n" ]; do + _name="$(jq -r ".backups.cronjobs[$_i].name" "$EXPECTED_JSON")" + _sched="$(jq -r ".backups.cronjobs[$_i].schedule" "$EXPECTED_JSON")" + _i=$((_i + 1)) + _cj="$(/usr/bin/curl -fsS --cacert "$SA_DIR/ca.crt" -H "Authorization: Bearer $_token" \ + "$K8S_API/apis/batch/v1/namespaces/$NS/cronjobs/$_name")" || { + fail "backup-cronjob: $_name (absent)" + _rc=1 + continue + } + _got="$(printf '%s' "$_cj" | jq -r '.spec.schedule')" + if [ "$_got" = "$_sched" ]; then + pass "backup-cronjob: $_name ($_got)" + else + fail "backup-cronjob: $_name ($_got != $_sched)" + _rc=1 + fi + done + return "$_rc" +} + +run() { + _name="$1" + shift + if retry "$_name" "$@"; then pass "$_name"; else fail "$_name"; fi +} + +# run_reported LABEL FN...: retry wrapper for checks that emit their own per-item +# PASS/FAIL and increment FAILURES (check_user_logins, check_groups, +# check_backups). Bare, these get one shot and can FAIL on a still-propagating +# ACL rule or a not-yet-visible CronJob while the run()-wrapped checks above them +# would retry past the same window. Probe quietly in a subshell (its FAILURES +# increments and output are discarded) until it passes or attempts run out, then +# run once authoritatively so per-item detail and the failure count land exactly +# once. +run_reported() { + _name="$1" + shift + _i=1 + while [ "$_i" -lt "$RETRIES" ]; do + if ("$@") >/dev/null 2>&1; then break; fi + log "$_name: not ready; retry $_i/$RETRIES in ${RETRY_SLEEP}s" + _i=$((_i + 1)) + sleep "$RETRY_SLEEP" + done + "$@" +} + +log "dgraph validator -> $ALPHA (ns=$NS, aclEnabled=$ACL_ENABLED); up to $RETRIES x ${RETRY_SLEEP}s per check" +run "health" check_health + +if [ "$ACL_ENABLED" = "true" ]; then + if retry "admin-login" get_admin_jwt; then pass "admin-login"; else fail "admin-login"; fi + run "acl-enforcing" check_acl_enforcing + run "membership" check_membership + run "query" check_query + run_reported "user-logins" check_user_logins + run_reported "groups" check_groups +else + log "ACL disabled (expected.json aclEnabled != true); skipping login, ACL-enforcement, user-login, and group checks" + run "membership" check_membership + run "query" check_query +fi + +if [ "$(jq -r '.backups.check' "$EXPECTED_JSON")" = "true" ]; then + run_reported "backups" check_backups +fi + +echo "----" +if [ "$FAILURES" -eq 0 ]; then + echo "dgraph validation: PASS" + exit 0 +fi +echo "dgraph validation: FAIL ($FAILURES check(s))" +exit 1 diff --git a/charts/dgraph/templates/_helpers.tpl b/charts/dgraph/templates/_helpers.tpl index 66137b39e..bf1383183 100644 --- a/charts/dgraph/templates/_helpers.tpl +++ b/charts/dgraph/templates/_helpers.tpl @@ -14,7 +14,7 @@ We truncate at 24 chars because some Kubernetes name fields are limited to this {{- .Values.fullnameOverride | trunc 24 | trimSuffix "-" -}} {{- else -}} {{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 24 -}} +{{- printf "%s-%s" .Release.Name $name | trunc 24 | trimSuffix "-" -}} {{- end -}} {{- end -}} {{/* @@ -147,25 +147,23 @@ Also, we can't use a single if because lazy evaluation is not an option {{/* Return the proper Docker Image Registry Secret Names +Priority: imagePullSecrets (Kubernetes object list) > global.imagePullSecrets (string list) > image.pullSecrets (string list) */}} {{- define "dgraph.imagePullSecrets" -}} -{{/* -Helm 2.11 supports the assignment of a value to a variable defined in a different scope, -but Helm 2.9 and 2.10 doesn't support it, so we need to implement this if-else logic. -Also, we can't use a single if because lazy evaluation is not an option -*/}} -{{- if .Values.global }} -{{- if .Values.global.imagePullSecrets }} +{{- if .Values.imagePullSecrets }} imagePullSecrets: -{{- range .Values.global.imagePullSecrets }} +{{- range .Values.imagePullSecrets }} +{{- if kindIs "map" . }} + - name: {{ .name }} +{{- else }} - name: {{ . }} {{- end }} -{{- else if .Values.image.pullSecrets }} +{{- end }} +{{- else if and .Values.global .Values.global.imagePullSecrets }} imagePullSecrets: -{{- range .Values.image.pullSecrets }} +{{- range .Values.global.imagePullSecrets }} - name: {{ . }} {{- end }} -{{- end -}} {{- else if .Values.image.pullSecrets }} imagePullSecrets: {{- range .Values.image.pullSecrets }} @@ -264,3 +262,114 @@ Allow overriding namespace {{- define "dgraph.namespace" -}} {{- default .Release.Namespace .Values.namespaceOverride -}} {{- end -}} + +{{/* +Generate the ingress path. Emits "/*" for ingress classes that need a wildcard +prefix (gce, alb, nsx) and "/" otherwise, checking global.ingress.ingressClassName +first and falling back to the kubernetes.io/ingress.class annotation. +*/}} +{{- define "dgraph.ingressPath" -}} + {{- $path := "/" -}} + {{- if .Values.global.ingress.ingressClassName -}} + {{- if eq .Values.global.ingress.ingressClassName "gce" "alb" "nsx" }} + {{- $path = "/*" -}} + {{- else }} + {{- $path = "/" -}} + {{- end }} + {{- else if index $.Values.global.ingress "annotations" -}} + {{- if eq (index $.Values.global.ingress.annotations "kubernetes.io/ingress.class" | default "") "gce" "alb" "nsx" }} + {{- $path = "/*" -}} + {{- else }} + {{- $path = "/" -}} + {{- end }} + {{- end -}} + {{- printf "%s" $path -}} +{{- end -}} + +{{/* +Cluster-domain suffix for in-cluster FQDNs: "." with the leading +dot, or empty when global.domain is unset. Trims stray leading/trailing dots so +a host never renders "...svc." or "...svc..cluster.local". +Use as: ...svc{{ include "dgraph.domainSuffix" . }} +*/}} +{{- define "dgraph.domainSuffix" -}} +{{- with (.Values.global.domain | default "" | trimAll ".") }}.{{ . }}{{ end -}} +{{- end -}} + +{{/* +Map a named log level to its glog -v integer; pass any other value (e.g. a raw +integer) through unchanged. Names are lowercase. +*/}} +{{- define "dgraph.verbosity" -}} + {{- $m := dict "normal" "0" "verbose" "1" "debug" "2" "trace" "3" -}} + {{- $k := toString . -}} + {{- index $m $k | default $k -}} +{{- end -}} + +{{/* +Render the glog flag fragment for a role. "." is a role value map (.Values.alpha +or .Values.zero). Emits nothing when every value is a glog default (logLevel +normal/0, empty vmodule, alsologtostderr false, empty logDir, logtostderr true), +so the default command line is unchanged. When any value differs it emits, with a +leading space, "-v= --logtostderr=" plus --vmodule / --alsologtostderr / +--log_dir when those are set. +*/}} +{{- define "dgraph.logFlags" -}} +{{- $v := include "dgraph.verbosity" .logLevel -}} +{{- /* logtostderr defaults to true (values.yaml), but Helm's `default` treats a + boolean false as empty, so an explicit `false` would be flipped back to the + default. Use a nil check so nil -> true while honoring an explicit false. */}} +{{- $logtostderr := .logtostderr -}} +{{- if kindIs "invalid" $logtostderr -}}{{- $logtostderr = true -}}{{- end -}} +{{- if or (ne $v "0") .vmodule .alsologtostderr .logDir (not $logtostderr) -}} +{{- printf " -v=%s --logtostderr=%v" $v $logtostderr -}} +{{- if .vmodule }}{{ printf " --vmodule=%s" .vmodule }}{{ end -}} +{{- if .alsologtostderr }} --alsologtostderr{{ end -}} +{{- if .logDir }}{{ printf " --log_dir=%s" .logDir }}{{ end -}} +{{- end -}} +{{- end -}} + +{{/* +Does this tls block force every client to present a certificate? + +client-auth-type mirrors Go's crypto/tls.ClientAuthType, which separates +"require" from "verify". REQUIREANY and REQUIREANDVERIFY are the only values +that force a cert (REQUIREANY never verifies it, REQUIREANDVERIFY does). +VERIFYIFGIVEN leaves the cert optional but verifies one that is presented; +REQUEST asks for a cert and neither requires nor verifies it; "" and OFF +disable client auth outright. + +Returns the STRING "true" or "" (empty) -- NOT a boolean. Compare it as a +string: eq (include "dgraph.tls.certRequired" (dict "tls" .Values.alpha.tls)) "true". + +Single source of truth for the alpha/zero probe guards. Keep them reading from +here: an inline re-derivation drifts. +*/}} +{{- define "dgraph.tls.certRequired" -}} +{{- $t := .tls.clientAuthType | default "" -}} +{{- if or (eq $t "REQUIREANDVERIFY") (eq $t "REQUIREANY") -}}true{{- end -}} +{{- end -}} + +{{/* +Compose Dgraph's --tls superflag from a tier's tls map. Pass a dict +{"tls": .Values.alpha.tls, "path": "/dgraph/tls"}. Filenames follow the output +of scripts/make_tls_secrets.sh (ca.crt, node.crt, node.key, +client..crt/.key). client-cert/key and client-auth-type are emitted only +when the corresponding values are set. +*/}} +{{- define "dgraph.tlsFlag" -}} +{{- /* internalPort defaults to true (values.yaml), but Helm's `default` treats a + boolean false as empty, so an explicit `false` would be flipped back to the + default. Use a nil check so nil -> true while honoring an explicit false. */}} +{{- $ip := .tls.internalPort -}} +{{- if kindIs "invalid" $ip -}}{{- $ip = true -}}{{- end -}} +{{- $opts := list (printf "ca-cert=%s/ca.crt" .path) (printf "server-cert=%s/node.crt" .path) (printf "server-key=%s/node.key" .path) (printf "internal-port=%v" $ip) -}} +{{- if .tls.clientName -}} +{{- $opts = append $opts (printf "client-cert=%s/client.%s.crt" .path .tls.clientName) -}} +{{- $opts = append $opts (printf "client-key=%s/client.%s.key" .path .tls.clientName) -}} +{{- end -}} +{{- if .tls.clientAuthType -}} +{{- $opts = append $opts (printf "client-auth-type=%s" .tls.clientAuthType) -}} +{{- end -}} +{{- printf "--tls \"%s;\"" (join "; " $opts) -}} +{{- end -}} diff --git a/charts/dgraph/templates/acl/bootstrap-configmap.yaml b/charts/dgraph/templates/acl/bootstrap-configmap.yaml new file mode 100644 index 000000000..424a373b0 --- /dev/null +++ b/charts/dgraph/templates/acl/bootstrap-configmap.yaml @@ -0,0 +1,126 @@ +{{- if and .Values.alpha.acl.enabled .Values.alpha.acl.bootstrap.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-acl-bootstrap + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "acl-bootstrap") | nindent 4 }} +data: + reconcile.sh: | + #!/usr/bin/env bash + ## Idempotent ACL reconciler. + ## 1. Rotates `groot` off Dgraph's default password to the supplied per-env one. + ## 2. Converges the declared groups (with predicate rules) and users (with group + ## membership), creating or updating as needed. + ## Safe to re-run on every helm upgrade. Authenticates via the GraphQL /admin + ## endpoint with the same login pattern the backup CronJob uses. The dgraph + ## image provides jq and /usr/bin/curl; all parsing goes through jq. + set -euo pipefail + + : "${ALPHA_HOST:?ALPHA_HOST not set}" + : "${GROOT_PASSWORD_KEY:?GROOT_PASSWORD_KEY not set}" + CREDS_DIR="${CREDS_DIR:-/creds}" + USERS_JSON="${USERS_JSON:-[]}" + GROUPS_JSON="${GROUPS_JSON:-[]}" + + ## TLS: when the Job supplies a CA path (alpha.tls.enabled), talk HTTPS and + ## present the client cert if one was provided. Empty CERTOPTS keeps plaintext. + ## Mirrors the backup CronJob curl handling. + SCHEME="http" + CERTOPTS=() + if [ -n "${CACERT_PATH:-}" ]; then + SCHEME="https" + CERTOPTS+=('--cacert' "${CACERT_PATH}") + if [ -n "${CLIENT_CERT_PATH:-}" ] && [ -n "${CLIENT_KEY_PATH:-}" ]; then + CERTOPTS+=('--cert' "${CLIENT_CERT_PATH}" '--key' "${CLIENT_KEY_PATH}") + fi + fi + ADMIN="${SCHEME}://${ALPHA_HOST}:8080/admin" + HEALTH="${SCHEME}://${ALPHA_HOST}:8080/health" + + groot_password="$(cat "${CREDS_DIR}/${GROOT_PASSWORD_KEY}")" + + ## post -> response body. Empty token = anonymous (login). + post() { + local token="$1" body="$2" + if [ -n "${token}" ]; then + /usr/bin/curl -fsS "${CERTOPTS[@]}" "${ADMIN}" -H 'Content-Type: application/json' \ + -H "X-Dgraph-AccessToken: ${token}" --data "${body}" + else + /usr/bin/curl -fsS "${CERTOPTS[@]}" "${ADMIN}" -H 'Content-Type: application/json' --data "${body}" + fi + } + + ## login -> accessJWT on stdout, non-zero on failure. + ## GraphQL variables (not string interpolation) keep credentials injection-safe. + login() { + local body resp jwt + body="$(jq -n --arg u "$1" --arg p "$2" \ + '{query:"mutation($u:String!,$p:String!){login(userId:$u,password:$p){response{accessJWT}}}",variables:{u:$u,p:$p}}')" + resp="$(post "" "${body}")" || return 1 + jwt="$(printf '%s' "${resp}" | jq -r '.data.login.response.accessJWT // empty')" + [ -n "${jwt}" ] || return 1 + printf '%s' "${jwt}" + } + + echo "Waiting for Alpha at ${HEALTH} ..." + until /usr/bin/curl -fsS "${CERTOPTS[@]}" "${HEALTH}" >/dev/null 2>&1; do sleep 5; done + + ## --- groot rotation (idempotent) --- + if TOKEN="$(login groot "${groot_password}" 2>/dev/null)"; then + echo "groot already rotated." + else + echo "Rotating groot from its default password ..." + TOKEN="$(login groot password)" || { + echo "FATAL: groot is neither the target nor the default password; refusing to guess." >&2 + exit 1 + } + body="$(jq -n --arg p "${groot_password}" \ + '{query:"mutation($p:String!){updateUser(input:{filter:{name:{eq:\"groot\"}},set:{password:$p}}){user{name}}}",variables:{p:$p}}')" + post "${TOKEN}" "${body}" | jq -e '.data.updateUser.user[0].name=="groot"' >/dev/null || { + echo "FATAL: groot password rotation failed." >&2; exit 1; } + TOKEN="$(login groot "${groot_password}")" || { + echo "FATAL: cannot log in as groot after rotation." >&2; exit 1; } + echo "groot rotated." + fi + + ## --- groups: create if absent, then converge rules --- + printf '%s' "${GROUPS_JSON}" | jq -c '.[]' | while read -r g; do + name="$(printf '%s' "${g}" | jq -r '.name')" + rules="$(printf '%s' "${g}" | jq -c '.rules // []')" + echo "Ensuring group ${name} ..." + add="$(jq -n --arg n "${name}" \ + '{query:"mutation($n:String!){addGroup(input:[{name:$n}]){group{name}}}",variables:{n:$n}}')" + post "${TOKEN}" "${add}" >/dev/null 2>&1 || true # ignore "already exists" + if [ "${rules}" != "[]" ]; then + upd="$(jq -n --arg n "${name}" --argjson r "${rules}" \ + '{query:"mutation($n:String!,$r:[RuleRef!]!){updateGroup(input:{filter:{name:{eq:$n}},set:{rules:$r}}){group{name}}}",variables:{n:$n,r:$r}}')" + post "${TOKEN}" "${upd}" | jq -e '.data.updateGroup.group[0].name' >/dev/null || { + echo "FATAL: failed to set rules on group ${name}." >&2; exit 1; } + fi + done + + ## --- users: create if absent, else reset password + group membership --- + printf '%s' "${USERS_JSON}" | jq -c '.[]' | while read -r u; do + name="$(printf '%s' "${u}" | jq -r '.name')" + pwkey="$(printf '%s' "${u}" | jq -r '.passwordSecretKey')" + groupsref="$(printf '%s' "${u}" | jq -c '[.groups[]? | {name: .}]')" + pw="$(cat "${CREDS_DIR}/${pwkey}")" + echo "Ensuring user ${name} ..." + add="$(jq -n --arg n "${name}" --arg p "${pw}" --argjson g "${groupsref}" \ + '{query:"mutation($n:String!,$p:String!,$g:[GroupRef]){addUser(input:[{name:$n,password:$p,groups:$g}]){user{name}}}",variables:{n:$n,p:$p,g:$g}}')" + resp="$(post "${TOKEN}" "${add}")" + if printf '%s' "${resp}" | jq -e '.data.addUser.user[0].name' >/dev/null 2>&1; then + echo " created ${name}" + else + upd="$(jq -n --arg n "${name}" --arg p "${pw}" --argjson g "${groupsref}" \ + '{query:"mutation($n:String!,$p:String!,$g:[GroupRef]){updateUser(input:{filter:{name:{eq:$n}},set:{password:$p,groups:$g}}){user{name}}}",variables:{n:$n,p:$p,g:$g}}')" + post "${TOKEN}" "${upd}" | jq -e '.data.updateUser.user[0].name' >/dev/null || { + echo "FATAL: could not upsert user ${name}." >&2; exit 1; } + echo " updated ${name}" + fi + done + + echo "ACL bootstrap complete." +{{- end }} diff --git a/charts/dgraph/templates/acl/bootstrap-job.yaml b/charts/dgraph/templates/acl/bootstrap-job.yaml new file mode 100644 index 000000000..b2ebf0a80 --- /dev/null +++ b/charts/dgraph/templates/acl/bootstrap-job.yaml @@ -0,0 +1,120 @@ +{{- if and .Values.alpha.acl.enabled .Values.alpha.acl.bootstrap.enabled }} +{{- /* + Resolve the Secret that holds the credentials the reconciler reads (groot and + per-user passwords). Defaults to the bootstrap-specific existingSecret, then the + ACL existingSecret, then the chart-created ACL Secret. +*/}} +{{- $bootstrap := .Values.alpha.acl.bootstrap }} +{{- $secretName := $bootstrap.existingSecret | default .Values.alpha.acl.existingSecret | default (printf "%s-acl-secret" (include "dgraph.alpha.fullname" .)) }} +{{- /* native TLS active (alpha.tls.enabled): the reconciler talks HTTPS to + Alpha's /admin and mounts the client cert. Reused at each TLS branch below. */}} +{{- $nativeTLS := .Values.alpha.tls.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-acl-bootstrap + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "acl-bootstrap") | nindent 4 }} + annotations: + ## Run after Alpha/Zero are up, on both first install and every upgrade. + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + backoffLimit: 6 + template: + metadata: + {{- with .Values.alpha.acl.bootstrap.rotation }} + annotations: + ## Changing acl.bootstrap.rotation changes this annotation, so a rotation + ## (e.g. a Terraform counter bump) produces a release diff that re-runs this + ## Job and its reconciler. The annotation lands only on the Job, so Alpha is + ## never restarted by a rotation. + dgraph.io/acl-rotation: {{ . | quote }} + {{- end }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "acl-bootstrap") | nindent 8 }} + spec: + restartPolicy: Never +{{- include "dgraph.imagePullSecrets" . | indent 6 }} + automountServiceAccountToken: false + ## Run alongside Alpha (same taint/label) so the Job can reach the service. + {{- if .Values.alpha.nodeSelector }} + nodeSelector: +{{ toYaml .Values.alpha.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.alpha.tolerations }} + tolerations: +{{ toYaml .Values.alpha.tolerations | indent 8 }} + {{- end }} + {{- if .Values.alpha.securityContext.enabled }} + securityContext: + {{- omit .Values.alpha.securityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + containers: + - name: acl-bootstrap + {{- /* Use the override only when registry, repository, and tag are all set; a + partial override would render an invalid image reference, so fall back to + the shared dgraph image instead. */}} + {{- if and .Values.alpha.acl.bootstrap.image .Values.alpha.acl.bootstrap.image.registry .Values.alpha.acl.bootstrap.image.repository .Values.alpha.acl.bootstrap.image.tag }} + image: {{ printf "%s/%s:%s" .Values.alpha.acl.bootstrap.image.registry .Values.alpha.acl.bootstrap.image.repository (.Values.alpha.acl.bootstrap.image.tag | toString) }} + {{- else }} + image: {{ include "dgraph.image" . }} + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + command: ["/usr/bin/bash", "/scripts/reconcile.sh"] + env: + - name: ALPHA_HOST + {{- if $nativeTLS }} + ## Native TLS: target the alpha-0 headless FQDN (…svc.) that + ## scripts/make_tls_secrets.sh lists in the cert SANs, so curl's hostname + ## verification of Alpha's server cert passes. The bare ClusterIP Service + ## name is not a SAN. + value: {{ printf "%s-0.%s-headless.%s.svc%s" (include "dgraph.alpha.fullname" .) (include "dgraph.alpha.fullname" .) (include "dgraph.namespace" .) (include "dgraph.domainSuffix" .) | quote }} + {{- else }} + value: {{ include "dgraph.alpha.fullname" . | quote }} + {{- end }} + - name: GROOT_PASSWORD_KEY + value: {{ .Values.alpha.acl.bootstrap.grootPasswordSecretKey | quote }} + - name: USERS_JSON + value: {{ .Values.alpha.acl.bootstrap.users | toJson | quote }} + - name: GROUPS_JSON + value: {{ .Values.alpha.acl.bootstrap.groups | toJson | quote }} + {{- if $nativeTLS }} + ## Native TLS (alpha.tls.enabled): reconcile.sh reads these to talk HTTPS + ## to Alpha's /admin and present a client cert under mutual TLS. + - name: CACERT_PATH + value: /dgraph/tls/ca.crt + {{- if .Values.alpha.tls.clientName }} + - name: CLIENT_CERT_PATH + value: /dgraph/tls/client.{{ .Values.alpha.tls.clientName }}.crt + - name: CLIENT_KEY_PATH + value: /dgraph/tls/client.{{ .Values.alpha.tls.clientName }}.key + {{- end }} + {{- end }} + volumeMounts: + - name: scripts + mountPath: /scripts + - name: creds + mountPath: /creds + readOnly: true + {{- if $nativeTLS }} + - name: tls-volume + mountPath: /dgraph/tls + readOnly: true + {{- end }} + volumes: + - name: scripts + configMap: + name: {{ include "dgraph.alpha.fullname" . }}-acl-bootstrap + defaultMode: 0555 + - name: creds + secret: + secretName: {{ $secretName }} + {{- if $nativeTLS }} + - name: tls-volume + secret: + secretName: {{ include "dgraph.alpha.fullname" . }}-tls-secret + {{- end }} +{{- end }} diff --git a/charts/dgraph/templates/alpha/ingress.yaml b/charts/dgraph/templates/alpha/ingress.yaml index 3b89464e2..3a03ad2fb 100644 --- a/charts/dgraph/templates/alpha/ingress.yaml +++ b/charts/dgraph/templates/alpha/ingress.yaml @@ -1,21 +1,3 @@ -{{- /* Generate ingress path */}} -{{- define "path" -}} - {{- $path := "/" -}} - {{- if .Values.global.ingress.ingressClassName -}} - {{- if eq .Values.global.ingress.ingressClassName "gce" "alb" "nsx" }} - {{- $path = "/*" -}} - {{- else }} - {{- $path = "/" -}} - {{- end }} - {{- else if index $.Values.global.ingress "annotations" -}} - {{- if eq (index $.Values.global.ingress.annotations "kubernetes.io/ingress.class" | default "") "gce" "alb" "nsx" }} - {{- $path = "/*" -}} - {{- else }} - {{- $path = "/" -}} - {{- end }} - {{- end -}} - {{- printf "%s" $path -}} -{{- end -}} {{- /* Alpha ingress resource */}} {{- if and (eq .Values.alpha.ingress.enabled true) (eq .Values.global.ingress.enabled false) -}} apiVersion: networking.k8s.io/v1 @@ -52,7 +34,7 @@ spec: port: number: 8080 pathType: ImplementationSpecific - path: {{ template "path" . }} + path: {{ template "dgraph.ingressPath" . }} {{- if .Values.alpha.ingress.hostname }} host: {{ .Values.alpha.ingress.hostname }} {{- end }} @@ -94,7 +76,7 @@ spec: port: number: 9080 pathType: ImplementationSpecific - path: {{ template "path" . }} + path: {{ template "dgraph.ingressPath" . }} {{- if .Values.alpha.ingress_grpc.hostname }} host: {{ .Values.alpha.ingress_grpc.hostname }} {{- end }} diff --git a/charts/dgraph/templates/alpha/secret-acl.yaml b/charts/dgraph/templates/alpha/secret-acl.yaml index fdf73c065..6dd27f3af 100644 --- a/charts/dgraph/templates/alpha/secret-acl.yaml +++ b/charts/dgraph/templates/alpha/secret-acl.yaml @@ -1,4 +1,4 @@ -{{ if and .Values.alpha.acl.enabled .Values.alpha.acl.file }} +{{ if and .Values.alpha.acl.enabled (not .Values.alpha.acl.existingSecret) .Values.alpha.acl.file }} apiVersion: v1 kind: Secret metadata: @@ -6,10 +6,6 @@ metadata: namespace: {{ include "dgraph.namespace" . }} labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.alpha.name) | nindent 4 }} - {{- with .Values.alpha.tls.annotations }} - annotations: - {{- toYaml . | trimSuffix "\n" | nindent 4 }} - {{- end }} type: Opaque data: {{- with .Values.alpha.acl.file }} diff --git a/charts/dgraph/templates/alpha/secret-enc.yaml b/charts/dgraph/templates/alpha/secret-enc.yaml index 8b06afc14..24fc70aae 100644 --- a/charts/dgraph/templates/alpha/secret-enc.yaml +++ b/charts/dgraph/templates/alpha/secret-enc.yaml @@ -1,4 +1,4 @@ -{{ if and .Values.alpha.encryption.enabled .Values.alpha.encryption.file }} +{{ if and .Values.alpha.encryption.enabled (not .Values.alpha.encryption.existingSecret) .Values.alpha.encryption.file }} apiVersion: v1 kind: Secret metadata: @@ -6,10 +6,6 @@ metadata: namespace: {{ include "dgraph.namespace" . }} labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.alpha.name) | nindent 4 }} - {{- with .Values.alpha.tls.annotations }} - annotations: - {{- toYaml . | trimSuffix "\n" | nindent 4 }} - {{- end }} type: Opaque data: {{- with .Values.alpha.encryption.file }} diff --git a/charts/dgraph/templates/alpha/secret-tls.yaml b/charts/dgraph/templates/alpha/secret-tls.yaml index 141e14a8e..4b9fa6e90 100644 --- a/charts/dgraph/templates/alpha/secret-tls.yaml +++ b/charts/dgraph/templates/alpha/secret-tls.yaml @@ -6,10 +6,6 @@ metadata: namespace: {{ include "dgraph.namespace" . }} labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.alpha.name) | nindent 4 }} - {{- with .Values.alpha.tls.annotations }} - annotations: - {{- toYaml . | trimSuffix "\n" | nindent 4 }} - {{- end }} type: Opaque data: {{- with .Values.alpha.tls.files }} diff --git a/charts/dgraph/templates/alpha/statefulset.yaml b/charts/dgraph/templates/alpha/statefulset.yaml index 0bd3c702c..68b82db98 100644 --- a/charts/dgraph/templates/alpha/statefulset.yaml +++ b/charts/dgraph/templates/alpha/statefulset.yaml @@ -8,11 +8,8 @@ {{- $max = 1 -}} {{- end -}} - {{- /* Append domain suffix if domain is used */}} - {{- $domainSuffix := "" -}} - {{- if .Values.global.domain -}} - {{- $domainSuffix = printf ".%s" .Values.global.domain -}} - {{- end -}} + {{- /* Append the cluster-domain suffix (trimmed, omitted when empty). */}} + {{- $domainSuffix := include "dgraph.domainSuffix" . -}} {{- /* Create comma-separated list of zeros */}} {{- range $idx := until $max }} @@ -26,6 +23,35 @@ {{- $hasMinioKeys := include "dgraph.backups.keys.minio.enabled" . -}} {{- $backupsEnabled := or .Values.backups.full.enabled .Values.backups.incremental.enabled }} {{- $initContainerEnabled := or .Values.alpha.initContainers.init.enabled .Values.alpha.extraInitContainers }} +{{- /* native-TLS is active for alpha when alpha.tls is on. Computed once here (a + bool) and reused for the --tls superflag, the HTTPS probe scheme, and the + extraFlags/clientAuthType guards below. */}} +{{- $nativeTLS := .Values.alpha.tls.enabled }} +{{- /* ACL and encryption-at-rest activate from `enabled` below by synthesizing the + matching superflag onto the alpha command. Fail rather than emit a duplicate + flag if the operator also hand-set it in extraFlags. */}} +{{- if and .Values.alpha.acl.enabled (contains "--acl" (.Values.alpha.extraFlags | default "")) }} +{{- fail "alpha.acl.enabled synthesizes the --acl superflag, but alpha.extraFlags already contains --acl. Remove --acl from alpha.extraFlags (configure alpha.acl.secretFile / alpha.acl.existingSecret instead) so the flag is not passed twice." }} +{{- end }} +{{- if and .Values.alpha.encryption.enabled (contains "--encryption" (.Values.alpha.extraFlags | default "")) }} +{{- fail "alpha.encryption.enabled synthesizes the --encryption superflag, but alpha.extraFlags already contains --encryption. Remove --encryption from alpha.extraFlags (configure alpha.encryption.keyFile / alpha.encryption.existingSecret instead) so the flag is not passed twice." }} +{{- end }} +{{- /* native TLS synthesizes --tls from alpha.tls; a hand-set --tls in extraFlags + would be passed twice. */}} +{{- if and $nativeTLS (contains "--tls" (.Values.alpha.extraFlags | default "")) }} +{{- fail "alpha.extraFlags contains --tls, but the chart synthesizes it when alpha.tls.enabled=true. Remove --tls from alpha.extraFlags and set alpha.tls.internalPort / clientName / clientAuthType instead." }} +{{- end }} +{{- /* client-auth-type applies to the external ports the built-in probes hit, so a + cert-requiring mode breaks the certless kubelet probe handshake. */}} +{{- $certRequiringAuth := eq (include "dgraph.tls.certRequired" (dict "tls" .Values.alpha.tls)) "true" }} +{{- if and $nativeTLS $certRequiringAuth }} +{{- if not .Values.alpha.tls.clientName }} +{{- fail (printf "alpha.tls.clientAuthType=%s requires alpha.tls.clientName so in-cluster callers (inter-node TLS) can present a client certificate." .Values.alpha.tls.clientAuthType) }} +{{- end }} +{{- if or (and .Values.alpha.startupProbe.enabled (not .Values.alpha.customStartupProbe)) (and .Values.alpha.livenessProbe.enabled (not .Values.alpha.customLivenessProbe)) (and .Values.alpha.readinessProbe.enabled (not .Values.alpha.customReadinessProbe)) }} +{{- fail (printf "alpha.tls.clientAuthType=%s forces every client to present a certificate, which the built-in httpGet probes cannot do (the kubelet has no client cert). Relax alpha.tls.clientAuthType (e.g. VERIFYIFGIVEN) on the external ports, or supply alpha.customStartupProbe / customLivenessProbe / customReadinessProbe (exec probes that present the client cert)." .Values.alpha.tls.clientAuthType) }} +{{- end }} +{{- end }} apiVersion: apps/v1 kind: StatefulSet metadata: @@ -50,6 +76,13 @@ spec: app: {{ template "dgraph.name" . }} release: {{ .Release.Name }} component: {{ .Values.alpha.name }} + {{- if .Values.alpha.persistence.enabled }} + {{- with .Values.alpha.persistence.persistentVolumeClaimRetentionPolicy }} + persistentVolumeClaimRetentionPolicy: + whenDeleted: {{ .whenDeleted | default "Retain" }} + whenScaled: {{ .whenScaled | default "Retain" }} + {{- end }} + {{- end }} template: metadata: name: {{ template "dgraph.alpha.fullname" . }} @@ -67,10 +100,10 @@ spec: labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.alpha.name "podLabels" .Values.alpha.podLabels) | nindent 8 }} spec: - {{- if .Values.serviceAccount.create }} - serviceAccountName: {{ .Values.serviceAccount.name }} - automountServiceAccountToken: {{ .Values.alpha.automountServiceAccountToken }} + {{- if or .Values.serviceAccount.create .Values.serviceAccount.name }} + serviceAccountName: {{ include "dgraph.serviceAccountName" . }} {{- end }} + automountServiceAccountToken: {{ .Values.alpha.automountServiceAccountToken }} {{- if .Values.alpha.schedulerName }} schedulerName: {{ .Values.alpha.schedulerName }} {{- end }} @@ -107,8 +140,7 @@ spec: {{- include "dgraph.imagePullSecrets" . | indent 6 }} {{- if .Values.alpha.securityContext.enabled }} securityContext: - fsGroup: {{ .Values.alpha.securityContext.fsGroup }} - runAsUser: {{ .Values.alpha.securityContext.runAsUser }} + {{- omit .Values.alpha.securityContext "enabled" | toYaml | nindent 8 }} {{- end }} {{- if .Values.alpha.nodeSelector }} nodeSelector: @@ -127,6 +159,10 @@ spec: - name: {{ template "dgraph.alpha.fullname" . }}-init image: {{ template "dgraph.initContainers.init.image" . }} imagePullPolicy: {{ .Values.alpha.initContainers.init.image.pullPolicy | quote }} + {{- if .Values.alpha.containerSecurityContext.enabled }} + securityContext: + {{- omit .Values.alpha.containerSecurityContext "enabled" | toYaml | nindent 10 }} + {{- end }} {{- with .Values.alpha.initContainers.init.env }} env: {{- tpl (toYaml .) $ | nindent 10 }} @@ -153,6 +189,10 @@ spec: containers: - name: {{ template "dgraph.alpha.fullname" . }} image: {{ template "dgraph.image" . }} + {{- if .Values.alpha.containerSecurityContext.enabled }} + securityContext: + {{- omit .Values.alpha.containerSecurityContext "enabled" | toYaml | nindent 10 }} + {{- end }} ports: - containerPort: 7080 name: grpc-alpha-int @@ -210,7 +250,7 @@ spec: {{- /* TODO: Remove awk-gsub once dgraph-io/dgraph#6837 is merged and back-ported. */}} - | set -ex - dgraph alpha --my=$(hostname -f | awk '{gsub(/\.$/,""); print $0}'):7080 --zero {{ template "multi_zeros" . }} {{ .Values.alpha.extraFlags }} + exec dgraph alpha --my=$(hostname -f | awk '{gsub(/\.$/,""); print $0}'):7080 --zero {{ template "multi_zeros" . }}{{ template "dgraph.logFlags" .Values.alpha }} {{ .Values.alpha.extraFlags }}{{- if .Values.alpha.acl.enabled }} --acl "secret-file=/dgraph/acl/{{ .Values.alpha.acl.secretFile }};"{{- end }}{{- if .Values.alpha.encryption.enabled }} --encryption "key-file=/dgraph/enc/{{ .Values.alpha.encryption.keyFile }};"{{- end }}{{- if $nativeTLS }} {{ include "dgraph.tlsFlag" (dict "tls" .Values.alpha.tls "path" "/dgraph/tls") }}{{- end }} resources: {{ toYaml .Values.alpha.resources | indent 10 }} {{- if .Values.alpha.startupProbe.enabled }} @@ -218,6 +258,9 @@ spec: httpGet: port: {{ .Values.alpha.startupProbe.port }} path: {{ .Values.alpha.startupProbe.path }} + {{- if $nativeTLS }} + scheme: HTTPS + {{- end }} periodSeconds: {{ .Values.alpha.startupProbe.periodSeconds }} timeoutSeconds: {{ .Values.alpha.startupProbe.timeoutSeconds }} successThreshold: {{ .Values.alpha.startupProbe.successThreshold }} @@ -230,6 +273,9 @@ spec: httpGet: port: {{ .Values.alpha.livenessProbe.port }} path: {{ .Values.alpha.livenessProbe.path }} + {{- if $nativeTLS }} + scheme: HTTPS + {{- end }} initialDelaySeconds: {{ .Values.alpha.livenessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.alpha.livenessProbe.periodSeconds }} timeoutSeconds: {{ .Values.alpha.livenessProbe.timeoutSeconds }} @@ -243,6 +289,9 @@ spec: httpGet: port: {{ .Values.alpha.readinessProbe.port }} path: {{ .Values.alpha.readinessProbe.path }} + {{- if $nativeTLS }} + scheme: HTTPS + {{- end }} initialDelaySeconds: {{ .Values.alpha.readinessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.alpha.readinessProbe.periodSeconds }} timeoutSeconds: {{ .Values.alpha.readinessProbe.timeoutSeconds }} @@ -252,10 +301,8 @@ spec: readinessProbe: {{- toYaml .Values.alpha.customReadinessProbe | nindent 10 }} {{- end }} volumeMounts: - {{- if .Values.alpha.persistence.enabled }} - name: datadir mountPath: /dgraph - {{- end }} {{- if .Values.alpha.configFile }} - name: config-volume mountPath: /dgraph/config @@ -282,11 +329,12 @@ spec: {{- end }} terminationGracePeriodSeconds: {{ .Values.alpha.terminationGracePeriodSeconds }} volumes: + {{- if not .Values.alpha.persistence.enabled }} + # When persistence is enabled the volumeClaimTemplates entry below supplies + # the "datadir" volume - the StatefulSet controller injects a per-pod PVC + # (datadir--). Only declare an emptyDir fallback when + # persistence is disabled. - name: datadir - {{- if .Values.alpha.persistence.enabled }} - persistentVolumeClaim: - claimName: datadir - {{- else }} emptyDir: {} {{- end }} {{- if and $backupsEnabled (or $hasS3Keys $hasMinioKeys) }} @@ -317,12 +365,18 @@ spec: {{- if .Values.alpha.encryption.enabled }} - name: enc-volume secret: - secretName: {{ template "dgraph.alpha.fullname" . }}-encryption-secret + # existingSecret lets a pre-created Secret supply the encryption key, so it + # never has to be rendered through Helm values; otherwise mount the Secret + # the chart renders from `encryption.file`. + secretName: {{ .Values.alpha.encryption.existingSecret | default (printf "%s-encryption-secret" (include "dgraph.alpha.fullname" .)) }} {{- end }} - {{- if or .Values.alpha.acl.enabled }} + {{- if .Values.alpha.acl.enabled }} - name: acl-volume secret: - secretName: {{ template "dgraph.alpha.fullname" . }}-acl-secret + # existingSecret lets a pre-created Secret (e.g. one managed by Terraform or an + # external secrets operator) supply the HMAC key, so it never has to be rendered + # through Helm values; otherwise mount the Secret the chart renders from `acl.file`. + secretName: {{ .Values.alpha.acl.existingSecret | default (printf "%s-acl-secret" (include "dgraph.alpha.fullname" .)) }} {{- end }} {{- if .Values.alpha.persistence.enabled }} volumeClaimTemplates: diff --git a/charts/dgraph/templates/alpha/svc-headless.yaml b/charts/dgraph/templates/alpha/svc-headless.yaml index 00422a63c..cbbbc4e86 100644 --- a/charts/dgraph/templates/alpha/svc-headless.yaml +++ b/charts/dgraph/templates/alpha/svc-headless.yaml @@ -12,6 +12,20 @@ spec: - name: grpc-alpha-int port: 7080 targetPort: 7080 + ## http-alpha (8080) and grpc-alpha (9080) are exposed here only when backups are + ## enabled (backups.full.enabled or backups.incremental.enabled): the backup CronJobs + ## are their consumer, addressing a single pinned pod (alpha-0) through this headless + ## Service, and under a STRICT mTLS service mesh (e.g. Istio) the client sidecar only + ## builds an mTLS route for ports the Service declares. Without 8080 here, the backup + ## /admin login falls through to plaintext and Alpha's sidecar resets the connection. + {{- if or .Values.backups.full.enabled .Values.backups.incremental.enabled }} + - name: http-alpha + port: 8080 + targetPort: 8080 + - name: grpc-alpha + port: 9080 + targetPort: 9080 + {{- end }} selector: app: {{ template "dgraph.name" . }} component: {{ .Values.alpha.name }} diff --git a/charts/dgraph/templates/backups/configs.yaml b/charts/dgraph/templates/backups/configs.yaml index 6a1d693db..413b9f867 100644 --- a/charts/dgraph/templates/backups/configs.yaml +++ b/charts/dgraph/templates/backups/configs.yaml @@ -9,51 +9,69 @@ metadata: data: backup.sh: | ###### - # get_token_rest - get accessJWT token with REST command for Dgraph 1.x + # require_json - validate a curl response before parsing it as JSON + # params: + # 1: body - the response body to validate + # 2: curl_status - exit status of the curl that produced the body + # 3: url - request URL, for the error message + # A failed connection through a service mesh (or any network error) returns a + # non-JSON body, e.g. Envoy's "upstream connect error ...", which the field + # extraction below cannot parse. Surface the curl status, the URL, and the raw + # body instead of failing silently with an empty token. Only bodies that fail + # validation are echoed, so a successful login response is never printed. ########################## - get_token_rest() { - JSON="{\"userid\": \"${USER}\", \"password\": \"${PASSWORD}\" }" - RESULT=$( - /usr/bin/curl --silent \ - "${HEADERS[@]}" \ - "${CERTOPTS[@]}" \ - --request POST \ - ${ALPHA_HOST}:8080/login \ - --data "${JSON}" - ) - - if grep -q errors <<< "$RESULT"; then - ERROR=$(grep -oP '(?<=message":")[^"]*' <<< $RESULT) - echo "ERROR: $ERROR" + require_json() { + BODY=${1} + CURL_STATUS=${2} + URL=${3} + if [[ "$CURL_STATUS" -ne 0 ]]; then + echo "ERROR: curl to ${URL} failed with exit ${CURL_STATUS}: ${BODY}" >&2 return 1 fi + if [[ -z "$BODY" ]]; then + echo "ERROR: empty response from ${URL}" >&2 + return 1 + fi + if ! jq -e . >/dev/null 2>&1 <<< "$BODY"; then + echo "ERROR: non-JSON response from ${URL}: ${BODY}" >&2 + return 1 + fi + } - grep -oP '(?<=accessJWT":")[^"]*' <<< "$RESULT" - + ###### + # get_token_rest - get accessJWT token with REST command for Dgraph 1.x + ########################## + get_token_rest() { + JSON=$(jq -n --arg userid "$USER" --arg password "$PASSWORD" '{userid: $userid, password: $password}') + CURL_STATUS=0 + RESULT=$( + /usr/bin/curl --silent "${HEADERS[@]}" "${CERTOPTS[@]}" \ + --request POST ${ALPHA_HOST}:8080/login --data "${JSON}" + ) || CURL_STATUS=$? + require_json "$RESULT" "$CURL_STATUS" "${ALPHA_HOST}:8080/login" || return 1 + ERROR=$(jq -r '.errors[0].message // empty' <<< "$RESULT") + if [[ -n "$ERROR" ]]; then echo "ERROR: $ERROR"; return 1; fi + TOKEN=$(jq -r '.data.accessJWT // empty' <<< "$RESULT") + [[ -n "$TOKEN" ]] || { echo "ERROR: could not parse accessJWT from /login response" >&2; return 1; } + echo "$TOKEN" } ###### # get_token_graphql - get accessJWT token using GraphQL for Dgraph 20.03.1+ ########################## get_token_graphql() { - GQL="{\"query\": \"mutation { login(userId: \\\"${USER}\\\" password: \\\"${PASSWORD}\\\") { response { accessJWT } } }\"}" + GQL=$(jq -n --arg userId "$USER" --arg password "$PASSWORD" '{query: "mutation ($userId: String, $password: String) { login(userId: $userId, password: $password) { response { accessJWT } } }", variables: {userId: $userId, password: $password}}') + CURL_STATUS=0 RESULT=$( - /usr/bin/curl --silent \ - "${HEADERS[@]}" \ - "${CERTOPTS[@]}" \ - --request POST \ - ${ALPHA_HOST}:8080/admin \ - --data "${GQL}" - ) - - if grep -q errors <<< "$RESULT"; then - ERROR=$(grep -oP '(?<=message":")[^"]*' <<< $RESULT) - echo "ERROR: $ERROR" - return 1 - fi - - grep -oP '(?<=accessJWT":")[^"]*' <<< "$RESULT" - + /usr/bin/curl --silent "${HEADERS[@]}" "${CERTOPTS[@]}" \ + --request POST ${ALPHA_HOST}:8080/admin --data "${GQL}" + ) || CURL_STATUS=$? + require_json "$RESULT" "$CURL_STATUS" "${ALPHA_HOST}:8080/admin" || return 1 + ERROR=$(jq -r '.errors[0].message // empty' <<< "$RESULT") + if [[ -n "$ERROR" ]]; then echo "ERROR: $ERROR"; return 1; fi + TOKEN=$(jq -r '.data.login.response.accessJWT // empty' <<< "$RESULT") + [[ -n "$TOKEN" ]] || { echo "ERROR: could not parse accessJWT from /admin login response" >&2; return 1; } + echo "$TOKEN" } ###### @@ -74,6 +92,8 @@ data: CACERT_PATH=${CACERT_PATH:-""} CLIENT_CERT_PATH=${CLIENT_CERT_PATH:-""} CLIENT_KEY_PATH=${CLIENT_KEY_PATH:-""} + HEADERS=() + CERTOPTS=() ## user/password required for login if [[ -z "$USER" || -z "$PASSWORD" ]]; then @@ -125,6 +145,8 @@ data: CACERT_PATH=${CACERT_PATH:-""} CLIENT_CERT_PATH=${CLIENT_CERT_PATH:-""} CLIENT_KEY_PATH=${CLIENT_KEY_PATH:-""} + HEADERS=() + CERTOPTS=() API_TYPE=${API_TYPE:-"graphql"} @@ -188,24 +210,22 @@ data: backup_rest() { URL_PATH="admin/backup?force_full=$FORCE_FULL" + CURL_STATUS=0 RESULT=$(/usr/bin/curl --silent \ "${HEADERS[@]}" \ "${CERTOPTS[@]}" \ --request POST \ ${ALPHA_HOST}:8080/$URL_PATH \ --data "destination=$BACKUP_DESTINATION" - ) - - if grep -q errors <<< "$RESULT"; then - ERROR=$(grep -oP '(?<=message":")[^"]*' <<< $RESULT) - MESSAGE="ERROR: $ERROR" - if grep -q code <<< "$RESULT"; then - CODE=$(grep -oP '(?<=code":")[^"]*' <<< $RESULT) - echo "$MESSAGE REASON='$CODE'" - fi + ) || CURL_STATUS=$? + require_json "$RESULT" "$CURL_STATUS" "${ALPHA_HOST}:8080/${URL_PATH}" || return 1 + + ERROR=$(jq -r '.errors[0].message // empty' <<< "$RESULT") + if [[ -n "$ERROR" ]]; then + CODE=$(jq -r '.errors[0].code // empty' <<< "$RESULT") + if [[ -n "$CODE" ]]; then echo "ERROR: $ERROR REASON='$CODE'"; else echo "ERROR: $ERROR"; fi return 1 fi - echo $RESULT } @@ -216,20 +236,18 @@ data: backup_graphql() { GQL="{\"query\": \"mutation { backup(input: {destination: \\\"${BACKUP_DESTINATION}\\\" forceFull: $FORCE_FULL }) { response { message code } } }\"}" + CURL_STATUS=0 RESULT=$(/usr/bin/curl --silent \ "${HEADERS[@]}" \ "${CERTOPTS[@]}" \ --request POST \ $ALPHA_HOST:8080/admin \ --data "$GQL" - ) - - if grep -q errors <<< "$RESULT"; then - ERROR=$(grep -oP '(?<=message":")[^"]*' <<< $RESULT) - echo "ERROR: $ERROR" - return 1 - fi + ) || CURL_STATUS=$? + require_json "$RESULT" "$CURL_STATUS" "${ALPHA_HOST}:8080/admin" || return 1 + ERROR=$(jq -r '.errors[0].message // empty' <<< "$RESULT") + if [[ -n "$ERROR" ]]; then echo "ERROR: $ERROR"; return 1; fi echo $RESULT } {{- end }} diff --git a/charts/dgraph/templates/backups/cronjob-full.yaml b/charts/dgraph/templates/backups/cronjob-full.yaml index 8f7f89e91..df4228254 100644 --- a/charts/dgraph/templates/backups/cronjob-full.yaml +++ b/charts/dgraph/templates/backups/cronjob-full.yaml @@ -73,7 +73,7 @@ spec: {{- end }} {{- if .Values.alpha.acl.enabled }} - ACCESS_TOKEN=$(get_token {{ .Values.backups.admin.user }} $(cat /backup_secrets/backup_admin_password) $AUTH_TOKEN ) + ACCESS_TOKEN=$(get_token "{{ .Values.backups.admin.user }}" "$(cat /backup_secrets/backup_admin_password)" "${AUTH_TOKEN:-}") {{- end }} ## Full Backup with optional access and auth tokens @@ -126,9 +126,26 @@ spec: {{- end }} {{- if or .Values.alpha.acl.enabled .Values.backups.admin.auth_token }} - name: backup-secret-volume + {{- if and .Values.alpha.acl.enabled .Values.backups.admin.existingSecret }} + projected: + sources: + - secret: + name: {{ .Values.backups.admin.existingSecret }} + items: + - key: {{ .Values.backups.admin.passwordSecretKey }} + path: backup_admin_password + {{- if .Values.backups.admin.auth_token }} + - secret: + name: {{ template "dgraph.backups.fullname" . }}-secret + items: + - key: backup_auth_token + path: backup_auth_token + {{- end }} + {{- else }} secret: secretName: {{ template "dgraph.backups.fullname" . }}-secret {{- end }} + {{- end }} {{- if .Values.backups.nfs.enabled }} - name: backups-nfs-volume persistentVolumeClaim: diff --git a/charts/dgraph/templates/backups/cronjob-inc.yaml b/charts/dgraph/templates/backups/cronjob-inc.yaml index 8a591e794..549773d38 100644 --- a/charts/dgraph/templates/backups/cronjob-inc.yaml +++ b/charts/dgraph/templates/backups/cronjob-inc.yaml @@ -73,7 +73,7 @@ spec: {{- end }} {{- if .Values.alpha.acl.enabled }} - ACCESS_TOKEN=$(get_token {{ .Values.backups.admin.user }} $(cat /backup_secrets/backup_admin_password) $AUTH_TOKEN ) + ACCESS_TOKEN=$(get_token "{{ .Values.backups.admin.user }}" "$(cat /backup_secrets/backup_admin_password)" "${AUTH_TOKEN:-}") {{- end }} ## Incremental Backup with optional access and auth tokens @@ -126,9 +126,26 @@ spec: {{- end }} {{- if or .Values.alpha.acl.enabled .Values.backups.admin.auth_token }} - name: backup-secret-volume + {{- if and .Values.alpha.acl.enabled .Values.backups.admin.existingSecret }} + projected: + sources: + - secret: + name: {{ .Values.backups.admin.existingSecret }} + items: + - key: {{ .Values.backups.admin.passwordSecretKey }} + path: backup_admin_password + {{- if .Values.backups.admin.auth_token }} + - secret: + name: {{ template "dgraph.backups.fullname" . }}-secret + items: + - key: backup_auth_token + path: backup_auth_token + {{- end }} + {{- else }} secret: secretName: {{ template "dgraph.backups.fullname" . }}-secret {{- end }} + {{- end }} {{- if .Values.backups.nfs.enabled }} - name: backups-nfs-volume persistentVolumeClaim: diff --git a/charts/dgraph/templates/backups/pv.yaml b/charts/dgraph/templates/backups/pv.yaml index 5d19a6dde..88cd6792d 100644 --- a/charts/dgraph/templates/backups/pv.yaml +++ b/charts/dgraph/templates/backups/pv.yaml @@ -3,7 +3,6 @@ apiVersion: v1 kind: PersistentVolume metadata: name: {{ template "dgraph.backups.fullname" . }}-fileserver - namespace: {{ include "dgraph.namespace" . }} labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.backups.name) | nindent 4 }} spec: diff --git a/charts/dgraph/templates/backups/secrets.yaml b/charts/dgraph/templates/backups/secrets.yaml index 92f3c0ff8..8a860afcb 100644 --- a/charts/dgraph/templates/backups/secrets.yaml +++ b/charts/dgraph/templates/backups/secrets.yaml @@ -1,7 +1,8 @@ {{- $hasS3Keys := include "dgraph.backups.keys.s3.enabled" . -}} {{- $hasMinioKeys := include "dgraph.backups.keys.minio.enabled" . -}} {{- $backupsEnabled := or .Values.backups.full.enabled .Values.backups.incremental.enabled }} -{{- if and $backupsEnabled (or .Values.alpha.acl.enabled .Values.backups.admin.auth_token $hasS3Keys $hasMinioKeys) -}} +{{- $aclInlinePassword := and .Values.alpha.acl.enabled (not .Values.backups.admin.existingSecret) }} +{{- if and $backupsEnabled (or $aclInlinePassword .Values.backups.admin.auth_token $hasS3Keys $hasMinioKeys) -}} apiVersion: v1 kind: Secret metadata: @@ -9,14 +10,10 @@ metadata: namespace: {{ include "dgraph.namespace" . }} labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.backups.name) | nindent 4 }} - {{- with .Values.alpha.tls.annotations }} - annotations: - {{- toYaml . | trimSuffix "\n" | nindent 4 }} - {{- end }} type: Opaque data: - {{- if .Values.alpha.acl.enabled }} - backup_admin_password: {{ required "backups.admin.password must be set when alpha.acl.enabled is true and backups are enabled" .Values.backups.admin.password | toString | b64enc | quote }} + {{- if $aclInlinePassword }} + backup_admin_password: {{ required "backups.admin.password must be set when alpha.acl.enabled is true, backups are enabled, and backups.admin.existingSecret is not set" .Values.backups.admin.password | toString | b64enc | quote }} {{- end }} {{- if .Values.backups.admin.auth_token }} backup_auth_token: {{ .Values.backups.admin.auth_token | toString | b64enc | quote }} diff --git a/charts/dgraph/templates/global-ingress.yaml b/charts/dgraph/templates/global-ingress.yaml index c7b35f0f0..8452b3a36 100644 --- a/charts/dgraph/templates/global-ingress.yaml +++ b/charts/dgraph/templates/global-ingress.yaml @@ -1,21 +1,3 @@ -{{- /* Generate ingress path */}} -{{- define "path" -}} - {{- $path := "/" -}} - {{- if .Values.global.ingress.ingressClassName -}} - {{- if eq .Values.global.ingress.ingressClassName "gce" "alb" "nsx" }} - {{- $path = "/*" -}} - {{- else }} - {{- $path = "/" -}} - {{- end }} - {{- else if index $.Values.global.ingress "annotations" -}} - {{- if eq (index $.Values.global.ingress.annotations "kubernetes.io/ingress.class" | default "") "gce" "alb" "nsx" }} - {{- $path = "/*" -}} - {{- else }} - {{- $path = "/" -}} - {{- end }} - {{- end -}} - {{- printf "%s" $path -}} -{{- end -}} {{- /* Global ingress resource */}} {{- if (eq .Values.global.ingress.enabled true) -}} apiVersion: networking.k8s.io/v1 @@ -52,7 +34,7 @@ spec: port: number: 8080 pathType: ImplementationSpecific - path: {{ template "path" . }} + path: {{ template "dgraph.ingressPath" . }} {{- if .Values.global.ingress.alpha_hostname }} host: {{ .Values.global.ingress.alpha_hostname }} {{- end }} @@ -65,7 +47,7 @@ spec: port: number: 80 pathType: ImplementationSpecific - path: {{ template "path" . }} + path: {{ template "dgraph.ingressPath" . }} {{- if .Values.global.ingress.ratel_hostname }} host: {{ .Values.global.ingress.ratel_hostname }} {{- end }} @@ -107,7 +89,7 @@ spec: port: number: 9080 pathType: ImplementationSpecific - path: {{ template "path" . }} + path: {{ template "dgraph.ingressPath" . }} {{- if .Values.global.ingress_grpc.alpha_grpc_hostname }} host: {{ .Values.global.ingress_grpc.alpha_grpc_hostname }} {{- end }} diff --git a/charts/dgraph/templates/networkpolicy.yaml b/charts/dgraph/templates/networkpolicy.yaml new file mode 100644 index 000000000..d5690fb4d --- /dev/null +++ b/charts/dgraph/templates/networkpolicy.yaml @@ -0,0 +1,49 @@ +{{- /* +Optional NetworkPolicy restricting ingress to the dgraph pods. Default off so +it can't silently break connectivity on clusters whose CNI does not enforce +NetworkPolicy. When enabled it: + - always allows intra-cluster dgraph traffic (alpha<->zero, peer gossip) by + selecting pods that carry this release's own app/release labels; + - grants client access to the alpha HTTP (8080) and gRPC (9080) ports only to + pods carrying every label in networkPolicy.clientPodLabels; + - appends any rules supplied in networkPolicy.extraIngress verbatim. +Egress is intentionally not restricted (backups reach S3/MinIO/NFS, etc.). +*/ -}} +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ template "dgraph.fullname" . }} + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" .) | nindent 4 }} +spec: + podSelector: + matchLabels: + app: {{ template "dgraph.name" . }} + release: {{ .Release.Name }} + policyTypes: + - Ingress + ingress: + # Intra-cluster: any dgraph pod from this release may reach any other. + - from: + - podSelector: + matchLabels: + app: {{ template "dgraph.name" . }} + release: {{ .Release.Name }} + {{- with .Values.networkPolicy.clientPodLabels }} + # Clients: pods carrying these labels may reach the alpha client ports. + - from: + - podSelector: + matchLabels: + {{- toYaml . | nindent 14 }} + ports: + - protocol: TCP + port: 8080 + - protocol: TCP + port: 9080 + {{- end }} + {{- with .Values.networkPolicy.extraIngress }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/dgraph/templates/pdb.yaml b/charts/dgraph/templates/pdb.yaml new file mode 100644 index 000000000..2e47e9bb6 --- /dev/null +++ b/charts/dgraph/templates/pdb.yaml @@ -0,0 +1,53 @@ +{{- /* +PodDisruptionBudgets for the alpha and zero Raft groups. Default off: setting +minAvailable (or maxUnavailable) too tight relative to replicaCount can block +voluntary evictions entirely. In particular, minAvailable >= replicaCount on a +single-replica group makes `kubectl drain` (and cluster-autoscaler scale-down) +hang forever, since Kubernetes will never evict a Pod that would violate the +budget. Review minAvailable/maxUnavailable against each group's replicaCount +before enabling. +Set maxUnavailable instead of minAvailable to express the budget the other way. +*/ -}} +{{- if .Values.zero.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ template "dgraph.zero.fullname" . }} + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" .Values.zero.name) | nindent 4 }} +spec: + {{- if .Values.zero.pdb.maxUnavailable }} + maxUnavailable: {{ .Values.zero.pdb.maxUnavailable }} + {{- else }} + minAvailable: {{ .Values.zero.pdb.minAvailable }} + {{- end }} + selector: + matchLabels: + app: {{ template "dgraph.name" . }} + release: {{ .Release.Name }} + component: {{ .Values.zero.name }} +{{- end }} +{{- if .Values.alpha.pdb.enabled }} +{{- if .Values.zero.pdb.enabled }} +--- +{{- end }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ template "dgraph.alpha.fullname" . }} + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" .Values.alpha.name) | nindent 4 }} +spec: + {{- if .Values.alpha.pdb.maxUnavailable }} + maxUnavailable: {{ .Values.alpha.pdb.maxUnavailable }} + {{- else }} + minAvailable: {{ .Values.alpha.pdb.minAvailable }} + {{- end }} + selector: + matchLabels: + app: {{ template "dgraph.name" . }} + release: {{ .Release.Name }} + component: {{ .Values.alpha.name }} +{{- end }} diff --git a/charts/dgraph/templates/pre-upgrade-statefulset-cleanup.yaml b/charts/dgraph/templates/pre-upgrade-statefulset-cleanup.yaml index 30724f5df..0e1180cbc 100644 --- a/charts/dgraph/templates/pre-upgrade-statefulset-cleanup.yaml +++ b/charts/dgraph/templates/pre-upgrade-statefulset-cleanup.yaml @@ -3,7 +3,13 @@ Pre-upgrade hook: delete StatefulSets with --cascade=orphan so that Helm can recreate them with updated immutable fields (e.g. selector.matchLabels) while keeping existing pods running. The pods are adopted by the new StatefulSets once the upgrade completes. + +Gated by preUpgradeHook.enabled. This only matters for clusters migrating from +the v24-era selector labels; once a cluster has upgraded past that point the +hook is pure per-upgrade overhead (a Job plus transient SA/RBAC on every +helm upgrade) and can be turned off. */}} +{{- if .Values.preUpgradeHook.enabled }} apiVersion: v1 kind: ServiceAccount metadata: @@ -92,22 +98,36 @@ spec: # StatefulSets (Alpha and Zero) for STS in {{ template "dgraph.alpha.fullname" . }} {{ template "dgraph.zero.fullname" . }}; do - SELECTOR=$(kubectl get statefulset "$STS" -n "$NS" --ignore-not-found -o jsonpath='{.spec.selector.matchLabels.chart}') + # --ignore-not-found: NotFound exits 0 with empty output (handled + # below); any other kubectl error exits non-zero and fails the Job + # via sh -e, so RBAC or connection problems stay visible. + EXISTS=$(kubectl get statefulset "$STS" -n "$NS" --ignore-not-found -o name) + if [ -z "$EXISTS" ]; then + echo "StatefulSet $STS does not exist in namespace $NS, skipping." + continue + fi + SELECTOR=$(kubectl get statefulset "$STS" -n "$NS" -o jsonpath='{.spec.selector.matchLabels.chart}') if [ -n "$SELECTOR" ]; then echo "StatefulSet $STS has stale 'chart' selector label ($SELECTOR), deleting with --cascade=orphan..." kubectl delete statefulset "$STS" --cascade=orphan -n "$NS" else - echo "StatefulSet $STS does not have 'chart' selector label (or does not exist), skipping." + echo "StatefulSet $STS does not have 'chart' selector label, skipping." fi done # Deployments (Ratel) for DEPLOY in {{ template "dgraph.ratel.fullname" . }}; do - SELECTOR=$(kubectl get deployment "$DEPLOY" -n "$NS" --ignore-not-found -o jsonpath='{.spec.selector.matchLabels.chart}') + EXISTS=$(kubectl get deployment "$DEPLOY" -n "$NS" --ignore-not-found -o name) + if [ -z "$EXISTS" ]; then + echo "Deployment $DEPLOY does not exist in namespace $NS, skipping." + continue + fi + SELECTOR=$(kubectl get deployment "$DEPLOY" -n "$NS" -o jsonpath='{.spec.selector.matchLabels.chart}') if [ -n "$SELECTOR" ]; then echo "Deployment $DEPLOY has stale 'chart' selector label ($SELECTOR), deleting with --cascade=orphan..." kubectl delete deployment "$DEPLOY" --cascade=orphan -n "$NS" else - echo "Deployment $DEPLOY does not have 'chart' selector label (or does not exist), skipping." + echo "Deployment $DEPLOY does not have 'chart' selector label, skipping." fi done +{{- end }} diff --git a/charts/dgraph/templates/prometheusrule.yaml b/charts/dgraph/templates/prometheusrule.yaml new file mode 100644 index 000000000..a101608ce --- /dev/null +++ b/charts/dgraph/templates/prometheusrule.yaml @@ -0,0 +1,57 @@ +{{- /* +Prometheus Operator PrometheusRule. Default off. The default alerts below are +deliberately conservative and built only on metrics that are stable regardless +of this chart's internal metric names: + - `up`, emitted by Prometheus for every scrape target (works with the + ServiceMonitor above), keyed to the exact alpha/zero Service names; and + - `kube_job_status_failed` from kube-state-metrics, for backup CronJob failures. +Richer dgraph-specific alerts (raft leader churn, replication lag, predicate +move failures) depend on this image's exact metric names — add them via +prometheusRule.extraRules once you have validated the names against a live +target. Set prometheusRule.defaultRules=false to ship only your extraRules. +*/ -}} +{{- if .Values.prometheusRule.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: {{ template "dgraph.fullname" . }} + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" .) | nindent 4 }} + {{- with .Values.prometheusRule.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + groups: + - name: {{ template "dgraph.fullname" . }} + rules: + {{- if .Values.prometheusRule.defaultRules }} + - alert: DgraphAlphaInstanceDown + expr: up{service="{{ template "dgraph.alpha.fullname" . }}"} == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Dgraph alpha instance {{`{{ $labels.instance }}`}} is down" + description: "An alpha pod has been unreachable to Prometheus for 5m. With a 3-node group, losing two alphas drops the group below quorum." + - alert: DgraphZeroInstanceDown + expr: up{service="{{ template "dgraph.zero.fullname" . }}"} == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Dgraph zero instance {{`{{ $labels.instance }}`}} is down" + description: "A zero pod has been unreachable to Prometheus for 5m. Zero is the cluster's Raft coordinator; losing quorum stalls the cluster." + - alert: DgraphBackupJobFailed + expr: kube_job_status_failed{namespace="{{ include "dgraph.namespace" . }}", job_name=~"{{ template "dgraph.backups.fullname" . }}-.*"} > 0 + for: 15m + labels: + severity: warning + annotations: + summary: "Dgraph backup job {{`{{ $labels.job_name }}`}} failed" + description: "A binary-backup CronJob run has failed. Backups may be stale — investigate before relying on point-in-time recovery." + {{- end }} + {{- with .Values.prometheusRule.extraRules }} + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/dgraph/templates/ratel/deployment.yaml b/charts/dgraph/templates/ratel/deployment.yaml index 08edba72d..c2659543e 100644 --- a/charts/dgraph/templates/ratel/deployment.yaml +++ b/charts/dgraph/templates/ratel/deployment.yaml @@ -33,14 +33,17 @@ spec: {{- end }} {{- if .Values.ratel.securityContext.enabled }} securityContext: - fsGroup: {{ .Values.ratel.securityContext.fsGroup }} - runAsUser: {{ .Values.ratel.securityContext.runAsUser }} + {{- omit .Values.ratel.securityContext "enabled" | toYaml | nindent 8 }} {{- end }} {{- include "dgraph.imagePullSecrets" . | indent 6 }} containers: - name: "{{ template "dgraph.ratel.fullname" . }}" image: "{{ template "dgraph.ratel.image" . }}" imagePullPolicy: {{ .Values.ratel.image.pullPolicy | quote }} + {{- if .Values.ratel.containerSecurityContext.enabled }} + securityContext: + {{- omit .Values.ratel.containerSecurityContext "enabled" | toYaml | nindent 10 }} + {{- end }} {{- if .Values.ratel.extraEnvs }} env: {{- with .Values.ratel.extraEnvs }} diff --git a/charts/dgraph/templates/ratel/ingress.yaml b/charts/dgraph/templates/ratel/ingress.yaml index 109847e4f..46fb64090 100644 --- a/charts/dgraph/templates/ratel/ingress.yaml +++ b/charts/dgraph/templates/ratel/ingress.yaml @@ -1,21 +1,3 @@ -{{- /* Generate ingress path */}} -{{- define "path" -}} - {{- $path := "/" -}} - {{- if .Values.global.ingress.ingressClassName -}} - {{- if eq .Values.global.ingress.ingressClassName "gce" "alb" "nsx" }} - {{- $path = "/*" -}} - {{- else }} - {{- $path = "/" -}} - {{- end }} - {{- else if index $.Values.global.ingress "annotations" -}} - {{- if eq (index $.Values.global.ingress.annotations "kubernetes.io/ingress.class" | default "") "gce" "alb" "nsx" }} - {{- $path = "/*" -}} - {{- else }} - {{- $path = "/" -}} - {{- end }} - {{- end -}} - {{- printf "%s" $path -}} -{{- end -}} {{- /* Ratel ingress resource */}} {{- if and (eq .Values.ratel.ingress.enabled true) (eq .Values.global.ingress.enabled false) (eq .Values.ratel.enabled true) -}} apiVersion: networking.k8s.io/v1 @@ -52,7 +34,7 @@ spec: port: number: 80 pathType: ImplementationSpecific - path: {{ template "path" . }} + path: {{ template "dgraph.ingressPath" . }} {{- if .Values.ratel.ingress.hostname }} host: {{ .Values.ratel.ingress.hostname }} {{- end }} diff --git a/charts/dgraph/templates/servicemonitor.yaml b/charts/dgraph/templates/servicemonitor.yaml new file mode 100644 index 000000000..6e291c024 --- /dev/null +++ b/charts/dgraph/templates/servicemonitor.yaml @@ -0,0 +1,44 @@ +{{- /* +Prometheus Operator ServiceMonitor for the alpha and zero metrics endpoints. +Default off. The alpha Service exposes the HTTP port as `http-alpha` (8080) and +the zero Service as `http-zero` (6080); dgraph serves Prometheus metrics on the +HTTP port at serviceMonitor.path. Listing both port names in one ServiceMonitor +is fine — an endpoint whose named port a given Service lacks is simply skipped. + +The selector requires the `monitor` label, which only the ClusterIP Services +carry (set from alpha.monitorLabel / zero.monitorLabel). The headless Services +share the same app/release labels but do not carry `monitor`, so they are +never selected. +*/ -}} +{{- if .Values.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "dgraph.fullname" . }} + namespace: {{ default (include "dgraph.namespace" .) .Values.serviceMonitor.namespace }} + labels: + {{- include "dgraph.labels" (dict "ctx" .) | nindent 4 }} + {{- with .Values.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + namespaceSelector: + matchNames: + - {{ include "dgraph.namespace" . }} + selector: + matchLabels: + app: {{ template "dgraph.name" . }} + release: {{ .Release.Name }} + matchExpressions: + - key: monitor + operator: Exists + endpoints: + - port: http-alpha + path: {{ .Values.serviceMonitor.path }} + interval: {{ .Values.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} + - port: http-zero + path: {{ .Values.serviceMonitor.path }} + interval: {{ .Values.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} +{{- end }} diff --git a/charts/dgraph/templates/validation/_pod.tpl b/charts/dgraph/templates/validation/_pod.tpl new file mode 100644 index 000000000..763452a37 --- /dev/null +++ b/charts/dgraph/templates/validation/_pod.tpl @@ -0,0 +1,111 @@ +{{/* +Shared pod spec for the dgraph validator, used by the `helm test` Pod, the +post-install hook Job, and the suspended manual CronJob. Emits the body of a pod +`spec:` at column 0; each caller includes it with `nindent` for its nesting. + +Transport mirrors the ACL bootstrap reconciler: under native TLS +(alpha.tls.enabled) the validator targets the alpha-0 headless FQDN (a cert SAN) +over HTTPS with the chart CA and optional client cert; otherwise it talks +plaintext to the ClusterIP Service. +*/}} +{{- define "dgraph.validation.podSpec" -}} +{{- $nativeTLS := .Values.alpha.tls.enabled -}} +{{- $credsSecret := .Values.alpha.acl.bootstrap.existingSecret | default .Values.alpha.acl.existingSecret | default (printf "%s-acl-secret" (include "dgraph.alpha.fullname" .)) -}} +restartPolicy: Never +{{- include "dgraph.imagePullSecrets" . | nindent 0 }} +{{- if .Values.validation.rbac.enabled }} +serviceAccountName: {{ include "dgraph.alpha.fullname" . }}-validate +{{- else }} +automountServiceAccountToken: false +{{- end }} +{{- $nodeSelector := .Values.validation.nodeSelector | default .Values.alpha.nodeSelector }} +{{- with $nodeSelector }} +nodeSelector: +{{- toYaml . | nindent 2 }} +{{- end }} +{{- $tolerations := .Values.validation.tolerations | default .Values.alpha.tolerations }} +{{- with $tolerations }} +tolerations: +{{- toYaml . | nindent 2 }} +{{- end }} +{{- if .Values.alpha.securityContext.enabled }} +securityContext: +{{- omit .Values.alpha.securityContext "enabled" | toYaml | nindent 2 }} +{{- end }} +containers: +- name: validate +{{- /* Use the override only when registry, repository, and tag are all set; a + partial override would render an invalid image reference, so fall back to + the shared dgraph image instead. */}} +{{- if and .Values.validation.image .Values.validation.image.registry .Values.validation.image.repository .Values.validation.image.tag }} + image: {{ printf "%s/%s:%s" .Values.validation.image.registry .Values.validation.image.repository (.Values.validation.image.tag | toString) }} +{{- else }} + image: {{ include "dgraph.image" . }} +{{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + command: ["/usr/bin/bash", "/scripts/validate.sh"] + env: + - name: ALPHA_HOST +{{- if $nativeTLS }} + value: {{ printf "%s-0.%s-headless.%s.svc%s" (include "dgraph.alpha.fullname" .) (include "dgraph.alpha.fullname" .) (include "dgraph.namespace" .) (include "dgraph.domainSuffix" .) | quote }} +{{- else }} + value: {{ include "dgraph.alpha.fullname" . | quote }} +{{- end }} + - name: EXPECTED_JSON + value: /config/expected.json + - name: CREDS_DIR + value: /creds + - name: RETRIES + value: {{ .Values.validation.retries | quote }} + - name: RETRY_SLEEP + value: {{ .Values.validation.retrySleep | quote }} +{{- if $nativeTLS }} + - name: CACERT_PATH + value: /dgraph/tls/ca.crt +{{- if .Values.alpha.tls.clientName }} + - name: CLIENT_CERT_PATH + value: /dgraph/tls/client.{{ .Values.alpha.tls.clientName }}.crt + - name: CLIENT_KEY_PATH + value: /dgraph/tls/client.{{ .Values.alpha.tls.clientName }}.key +{{- end }} +{{- end }} + volumeMounts: + - name: scripts + mountPath: /scripts + - name: config + mountPath: /config +{{- if .Values.alpha.acl.enabled }} + - name: creds + mountPath: /creds + readOnly: true +{{- end }} +{{- if $nativeTLS }} + - name: tls-volume + mountPath: /dgraph/tls + readOnly: true +{{- end }} +volumes: +- name: scripts + configMap: + name: {{ include "dgraph.alpha.fullname" . }}-validate + defaultMode: 0555 + items: + - key: validate.sh + path: validate.sh +- name: config + secret: + secretName: {{ include "dgraph.alpha.fullname" . }}-validate + items: + - key: expected.json + path: expected.json +{{- if .Values.alpha.acl.enabled }} +- name: creds + secret: + secretName: {{ $credsSecret }} +{{- end }} +{{- if $nativeTLS }} +- name: tls-volume + secret: + secretName: {{ include "dgraph.alpha.fullname" . }}-tls-secret +{{- end }} +{{- end -}} diff --git a/charts/dgraph/templates/validation/configmap.yaml b/charts/dgraph/templates/validation/configmap.yaml new file mode 100644 index 000000000..34a8e5d21 --- /dev/null +++ b/charts/dgraph/templates/validation/configmap.yaml @@ -0,0 +1,26 @@ +{{- if .Values.validation.enabled }} +{{- /* + checkBackups reads the backup CronJobs via the Kubernetes API, which needs the + validator ServiceAccount/Role. Fail at render time rather than deploy a + validator that cannot satisfy the check it was asked to run. +*/}} +{{- if and .Values.validation.checkBackups (not .Values.validation.rbac.enabled) }} +{{- fail "validation.checkBackups requires validation.rbac.enabled: the backup-CronJob check reads the Kubernetes API with the validator ServiceAccount." }} +{{- end }} +{{- /* + The validator script only. The expected-state document it reads lives in a + Secret (templates/validation/secret.yaml), not here, because it names the + Secret keys that hold the admin/user passwords and so is kept out of a + world-readable ConfigMap. +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} +data: + validate.sh: | +{{ .Files.Get "files/validation/validate.sh" | indent 4 }} +{{- end }} diff --git a/charts/dgraph/templates/validation/cronjob.yaml b/charts/dgraph/templates/validation/cronjob.yaml new file mode 100644 index 000000000..a8edf4520 --- /dev/null +++ b/charts/dgraph/templates/validation/cronjob.yaml @@ -0,0 +1,39 @@ +{{- if and .Values.validation.enabled .Values.validation.cronjob.enabled }} +{{- /* + Suspended manual-trigger template. It never fires on its own (suspended + + a never-occurring schedule); an operator runs it on demand via + `kubectl create job --from=cronjob/-alpha-validate ... -manual`. + NOTES.txt renders the exact command (with the release's name and namespace + substituted) on install; this is a template comment, so it is not the place + for a rendered command. +*/}} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} +spec: + schedule: "0 0 31 2 *" # 31 February: never fires. + suspend: true + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + metadata: + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 8 }} + spec: + backoffLimit: {{ .Values.validation.job.backoffLimit }} + template: + metadata: + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 12 }} + {{- with .Values.validation.podAnnotations }} + annotations: + {{- toYaml . | nindent 12 }} + {{- end }} + spec: + {{- include "dgraph.validation.podSpec" . | nindent 10 }} +{{- end }} diff --git a/charts/dgraph/templates/validation/job.yaml b/charts/dgraph/templates/validation/job.yaml new file mode 100644 index 000000000..61cc7dfe0 --- /dev/null +++ b/charts/dgraph/templates/validation/job.yaml @@ -0,0 +1,32 @@ +{{- if and .Values.validation.enabled .Values.validation.job.enabled }} +{{- /* + Post-install/upgrade hook Job that gates the release: a failed validation fails + the hook, which fails `helm install/upgrade` (and, under Terraform, the + helm_release with wait). Weight 10 runs it after the ACL bootstrap (weight 5) + so accounts exist first. A failed Job is kept (only hook-succeeded deletes) for + inspection; before-hook-creation clears the prior one on the next run. +*/}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + backoffLimit: {{ .Values.validation.job.backoffLimit }} + template: + metadata: + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 8 }} + {{- with .Values.validation.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "dgraph.validation.podSpec" . | nindent 6 }} +{{- end }} diff --git a/charts/dgraph/templates/validation/rbac.yaml b/charts/dgraph/templates/validation/rbac.yaml new file mode 100644 index 000000000..912e0f142 --- /dev/null +++ b/charts/dgraph/templates/validation/rbac.yaml @@ -0,0 +1,44 @@ +{{- if and .Values.validation.enabled .Values.validation.rbac.enabled }} +{{- /* + ServiceAccount + Role for the validator's backup-CronJob check (Check G reads + CronJobs via the Kubernetes API). Regular release resources, not hooks, so the + ServiceAccount exists before the post-install hook Job and persists for `helm + test`. Off by default (validation.rbac.enabled=false) for consumers who do not + want the validator granted API access. +*/}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} +rules: +- apiGroups: ["batch"] + resources: ["cronjobs"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "dgraph.alpha.fullname" . }}-validate +subjects: +- kind: ServiceAccount + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} +{{- end }} diff --git a/charts/dgraph/templates/validation/secret.yaml b/charts/dgraph/templates/validation/secret.yaml new file mode 100644 index 000000000..cd1ee0831 --- /dev/null +++ b/charts/dgraph/templates/validation/secret.yaml @@ -0,0 +1,60 @@ +{{- if .Values.validation.enabled }} +{{- /* + The validator's expected state, derived from the chart's own values so it + cannot drift from what was deployed. Rendered into a Secret rather than a + ConfigMap: expected.json names the Secret keys that hold the admin/user + passwords (adminPasswordKey, users[].passwordKey), so it is credential-adjacent + and kept out of a world-readable ConfigMap. adminPasswordKey defaults to + groot's key (the always-present superadmin); set validation.adminUser / + adminPasswordSecretKey to validate as a different account. users carry their + bootstrap passwordSecretKey. +*/}} +{{- $adminUser := .Values.validation.adminUser | default "groot" }} +{{- $adminKey := .Values.validation.adminPasswordSecretKey }} +{{- if not $adminKey }} + {{- if eq $adminUser "groot" }} + {{- $adminKey = .Values.alpha.acl.bootstrap.grootPasswordSecretKey }} + {{- else }} + {{- $adminKey = printf "%s_password" $adminUser }} + {{- end }} +{{- end }} +{{- $users := list }} +{{- range .Values.alpha.acl.bootstrap.users }} + {{- $users = append $users (dict "name" .name "passwordKey" .passwordSecretKey) }} +{{- end }} +{{- $groups := list }} +{{- range .Values.alpha.acl.bootstrap.groups }} + {{- $groups = append $groups (dict "name" .name "rules" (.rules | default (list))) }} +{{- end }} +{{- $cronjobs := list }} +{{- if .Values.validation.checkBackups }} + {{- if .Values.backups.full.enabled }} + {{- $cronjobs = append $cronjobs (dict "name" (printf "%s-full" (include "dgraph.backups.fullname" .)) "schedule" .Values.backups.full.schedule) }} + {{- end }} + {{- if .Values.backups.incremental.enabled }} + {{- $cronjobs = append $cronjobs (dict "name" (printf "%s-inc" (include "dgraph.backups.fullname" .)) "schedule" .Values.backups.incremental.schedule) }} + {{- end }} +{{- end }} +{{- $expected := dict + "namespace" (include "dgraph.namespace" .) + "expectedAlphas" (.Values.alpha.replicaCount | int) + "expectedZeros" (.Values.zero.replicaCount | int) + "aclEnabled" .Values.alpha.acl.enabled + "adminUser" $adminUser + "adminPasswordKey" $adminKey + "users" $users + "groups" $groups + "backups" (dict "check" .Values.validation.checkBackups "roundtrip" .Values.validation.backupRoundtrip "cronjobs" $cronjobs) +}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} +type: Opaque +stringData: + expected.json: | + {{ $expected | toJson }} +{{- end }} diff --git a/charts/dgraph/templates/validation/test.yaml b/charts/dgraph/templates/validation/test.yaml new file mode 100644 index 000000000..e70b74ca0 --- /dev/null +++ b/charts/dgraph/templates/validation/test.yaml @@ -0,0 +1,21 @@ +{{- if .Values.validation.enabled }} +{{- /* + `helm test` entrypoint. Runs the validator on demand (helm test ) + without gating install/upgrade. +*/}} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "dgraph.alpha.fullname" . }}-validate-test + namespace: {{ include "dgraph.namespace" . }} + labels: + {{- include "dgraph.labels" (dict "ctx" . "component" "validate") | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + {{- with .Values.validation.podAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- include "dgraph.validation.podSpec" . | nindent 2 }} +{{- end }} diff --git a/charts/dgraph/templates/zero/secret-tls.yaml b/charts/dgraph/templates/zero/secret-tls.yaml index c92a8490f..596008d9b 100644 --- a/charts/dgraph/templates/zero/secret-tls.yaml +++ b/charts/dgraph/templates/zero/secret-tls.yaml @@ -6,10 +6,6 @@ metadata: namespace: {{ include "dgraph.namespace" . }} labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.zero.name) | nindent 4 }} - {{- with .Values.zero.tls.annotations }} - annotations: - {{- toYaml . | trimSuffix "\n" | nindent 4 }} - {{- end }} type: Opaque data: {{- with .Values.zero.tls.files }} diff --git a/charts/dgraph/templates/zero/statefulset.yaml b/charts/dgraph/templates/zero/statefulset.yaml index 27c5b9e61..9ba9ff0cb 100644 --- a/charts/dgraph/templates/zero/statefulset.yaml +++ b/charts/dgraph/templates/zero/statefulset.yaml @@ -2,11 +2,8 @@ {{- define "peer_zero" -}} {{- $zeroFullName := include "dgraph.zero.fullname" . -}} - {{- /* Append domain suffix if domain is used */}} - {{- $domainSuffix := "" -}} - {{- if .Values.global.domain -}} - {{- $domainSuffix = printf ".%s" .Values.global.domain -}} - {{- end -}} + {{- /* Append the cluster-domain suffix (trimmed, omitted when empty). */}} + {{- $domainSuffix := include "dgraph.domainSuffix" . -}} {{- printf "%s-%d.%s-headless.${POD_NAMESPACE}.svc%s:5080" $zeroFullName 0 $zeroFullName $domainSuffix -}} {{- end -}} @@ -19,6 +16,26 @@ {{- printf "--idx " -}} {{- end -}} {{- end -}} +{{- /* native-TLS is active for zero when zero.tls is on. Computed once here (a + bool) and reused for the --tls superflag, the HTTPS probe scheme, and the + extraFlags/clientAuthType guards below. */}} +{{- $nativeTLS := .Values.zero.tls.enabled }} +{{- /* native TLS synthesizes --tls from zero.tls; a hand-set --tls in extraFlags + would be passed twice. */}} +{{- if and $nativeTLS (contains "--tls" (.Values.zero.extraFlags | default "")) }} +{{- fail "zero.extraFlags contains --tls, but the chart synthesizes it when zero.tls.enabled=true. Remove --tls from zero.extraFlags and set zero.tls.internalPort / clientName / clientAuthType instead." }} +{{- end }} +{{- /* client-auth-type applies to the external ports the built-in probes hit, so a + cert-requiring mode breaks the certless kubelet probe handshake. */}} +{{- $certRequiringAuth := eq (include "dgraph.tls.certRequired" (dict "tls" .Values.zero.tls)) "true" }} +{{- if and $nativeTLS $certRequiringAuth }} +{{- if not .Values.zero.tls.clientName }} +{{- fail (printf "zero.tls.clientAuthType=%s requires zero.tls.clientName so inter-node TLS can present a client certificate." .Values.zero.tls.clientAuthType) }} +{{- end }} +{{- if or (and .Values.zero.startupProbe.enabled (not .Values.zero.customStartupProbe)) (and .Values.zero.livenessProbe.enabled (not .Values.zero.customLivenessProbe)) (and .Values.zero.readinessProbe.enabled (not .Values.zero.customReadinessProbe)) }} +{{- fail (printf "zero.tls.clientAuthType=%s forces every client to present a certificate, which the built-in httpGet probes cannot do (the kubelet has no client cert). Relax zero.tls.clientAuthType (e.g. VERIFYIFGIVEN) on the external ports, or supply zero.customStartupProbe / customLivenessProbe / customReadinessProbe (exec probes that present the client cert)." .Values.zero.tls.clientAuthType) }} +{{- end }} +{{- end }} apiVersion: apps/v1 kind: StatefulSet metadata: @@ -43,6 +60,13 @@ spec: app: {{ template "dgraph.name" . }} release: {{ .Release.Name }} component: {{ .Values.zero.name }} + {{- if .Values.zero.persistence.enabled }} + {{- with .Values.zero.persistence.persistentVolumeClaimRetentionPolicy }} + persistentVolumeClaimRetentionPolicy: + whenDeleted: {{ .whenDeleted | default "Retain" }} + whenScaled: {{ .whenScaled | default "Retain" }} + {{- end }} + {{- end }} template: metadata: name: {{ template "dgraph.zero.fullname" . }} @@ -60,10 +84,10 @@ spec: labels: {{- include "dgraph.labels" (dict "ctx" . "component" .Values.zero.name "podLabels" .Values.zero.podLabels) | nindent 8 }} spec: - {{- if .Values.serviceAccount.create }} - serviceAccountName: {{ .Values.serviceAccount.name }} - automountServiceAccountToken: {{ .Values.zero.automountServiceAccountToken }} + {{- if or .Values.serviceAccount.create .Values.serviceAccount.name }} + serviceAccountName: {{ include "dgraph.serviceAccountName" . }} {{- end }} + automountServiceAccountToken: {{ .Values.zero.automountServiceAccountToken }} {{- if .Values.zero.schedulerName }} schedulerName: {{ .Values.zero.schedulerName }} {{- end }} @@ -100,8 +124,7 @@ spec: {{- include "dgraph.imagePullSecrets" . | indent 6 }} {{- if .Values.zero.securityContext.enabled }} securityContext: - fsGroup: {{ .Values.zero.securityContext.fsGroup }} - runAsUser: {{ .Values.zero.securityContext.runAsUser }} + {{- omit .Values.zero.securityContext "enabled" | toYaml | nindent 8 }} {{- end }} {{- if .Values.zero.nodeSelector }} nodeSelector: @@ -114,6 +137,10 @@ spec: containers: - name: {{ template "dgraph.zero.fullname" . }} image: {{ template "dgraph.image" . }} + {{- if .Values.zero.containerSecurityContext.enabled }} + securityContext: + {{- omit .Values.zero.containerSecurityContext "enabled" | toYaml | nindent 10 }} + {{- end }} ports: - containerPort: 5080 name: grpc-zero @@ -145,9 +172,9 @@ spec: ordinal=${BASH_REMATCH[1]} idx=$(($ordinal + 1)) if [[ $ordinal -eq 0 ]]; then - exec dgraph zero --my=$(hostname -f | awk '{gsub(/\.$/,""); print $0}'):5080 {{ template "raft_index_flag" . }}$idx --replicas {{ .Values.zero.shardReplicaCount }} {{ .Values.zero.extraFlags }} + exec dgraph zero --my=$(hostname -f | awk '{gsub(/\.$/,""); print $0}'):5080 {{ template "raft_index_flag" . }}$idx --replicas {{ .Values.zero.shardReplicaCount }}{{ template "dgraph.logFlags" .Values.zero }} {{ .Values.zero.extraFlags }}{{- if $nativeTLS }} {{ include "dgraph.tlsFlag" (dict "tls" .Values.zero.tls "path" "/dgraph/tls") }}{{- end }} else - exec dgraph zero --my=$(hostname -f | awk '{gsub(/\.$/,""); print $0}'):5080 --peer {{ template "peer_zero" . }} {{ template "raft_index_flag" . }}$idx --replicas {{ .Values.zero.shardReplicaCount }} {{ .Values.zero.extraFlags }} + exec dgraph zero --my=$(hostname -f | awk '{gsub(/\.$/,""); print $0}'):5080 --peer {{ template "peer_zero" . }} {{ template "raft_index_flag" . }}$idx --replicas {{ .Values.zero.shardReplicaCount }}{{ template "dgraph.logFlags" .Values.zero }} {{ .Values.zero.extraFlags }}{{- if $nativeTLS }} {{ include "dgraph.tlsFlag" (dict "tls" .Values.zero.tls "path" "/dgraph/tls") }}{{- end }} fi resources: {{ toYaml .Values.zero.resources | indent 10 }} @@ -156,6 +183,9 @@ spec: httpGet: port: {{ .Values.zero.startupProbe.port }} path: {{ .Values.zero.startupProbe.path }} + {{- if $nativeTLS }} + scheme: HTTPS + {{- end }} periodSeconds: {{ .Values.zero.startupProbe.periodSeconds }} timeoutSeconds: {{ .Values.zero.startupProbe.timeoutSeconds }} successThreshold: {{ .Values.zero.startupProbe.successThreshold }} @@ -168,6 +198,9 @@ spec: httpGet: port: {{ .Values.zero.livenessProbe.port }} path: {{ .Values.zero.livenessProbe.path }} + {{- if $nativeTLS }} + scheme: HTTPS + {{- end }} initialDelaySeconds: {{ .Values.zero.livenessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.zero.livenessProbe.periodSeconds }} timeoutSeconds: {{ .Values.zero.livenessProbe.timeoutSeconds }} @@ -181,6 +214,9 @@ spec: httpGet: port: {{ .Values.zero.readinessProbe.port }} path: {{ .Values.zero.readinessProbe.path }} + {{- if $nativeTLS }} + scheme: HTTPS + {{- end }} initialDelaySeconds: {{ .Values.zero.readinessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.zero.readinessProbe.periodSeconds }} timeoutSeconds: {{ .Values.zero.readinessProbe.timeoutSeconds }} diff --git a/charts/dgraph/templates/zero/svc.yaml b/charts/dgraph/templates/zero/svc.yaml index fb5fd35bb..e0eb1811c 100644 --- a/charts/dgraph/templates/zero/svc.yaml +++ b/charts/dgraph/templates/zero/svc.yaml @@ -16,8 +16,8 @@ spec: {{- if .Values.zero.service.loadBalancerIP }} loadBalancerIP: {{ .Values.zero.service.loadBalancerIP }} {{- end }} - {{- if .Values.zero.externalTrafficPolicy }} - externalTrafficPolicy: {{ .Values.zero.externalTrafficPolicy }} + {{- if .Values.zero.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.zero.service.externalTrafficPolicy }} {{- end }} ports: - port: 5080 diff --git a/charts/dgraph/values.yaml b/charts/dgraph/values.yaml index 1743d5419..9e214a257 100644 --- a/charts/dgraph/values.yaml +++ b/charts/dgraph/values.yaml @@ -12,10 +12,16 @@ ## and per-component podLabels will override commonLabels on key conflicts. commonLabels: {} +## Optionally specify an array of imagePullSecrets applied to every Pod. +## Entries may be plain secret-name strings or Kubernetes {name: secret-name} +## objects. Takes precedence over global.imagePullSecrets and image.pullSecrets. +## Secrets must be manually created in the namespace. +imagePullSecrets: [] + image: &image registry: docker.io repository: dgraph/dgraph - tag: v25.3.1 + tag: v25.3.8 ## Specify a imagePullPolicy ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images @@ -37,6 +43,10 @@ image: &image ## Runs automatically on helm upgrade — detects and recreates StatefulSets ## with stale selector labels using --cascade=orphan to avoid downtime. preUpgradeHook: + ## Run the v24-to-v25 StatefulSet selector migration Job on helm upgrade. + ## Once every cluster has migrated past the v24-to-v25 selector change this + ## can be set to false (it then becomes pure per-upgrade overhead). + enabled: true image: registry: docker.io repository: bitnami/kubectl @@ -72,6 +82,15 @@ zero: ## Value for the "monitor" label on the zero Service (not on pods or other resources). ## Used by Prometheus for service discovery. monitorLabel: zero-dgraph-io + + ## PodDisruptionBudget for the zero StatefulSet. Default off — see templates/pdb.yaml + ## for why minAvailable >= replicaCount can deadlock `kubectl drain` on a + ## single-replica group. Review against replicaCount before enabling. + pdb: + enabled: false + minAvailable: 2 + # maxUnavailable: + ## StatefulSet controller supports automated updates. There are two valid update strategies: RollingUpdate and OnDelete ## ref: https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/#updating-statefulsets ## @@ -126,6 +145,27 @@ zero: ## Extra flags for command line flags in command extraFlags: "" + ## Verbosity (glog -v). INFO/WARNING/ERROR are ALWAYS written to stderr; this + ## only adds finer V-logs on top. Named level OR a raw -v integer: + ## normal->0 (prod default) verbose->1 debug->2 trace->3 + logLevel: normal + + ## Per-module verbosity (glog --vmodule), e.g. "server=3,raft=2". Empty = none. + vmodule: "" + + ## Log to the container's stderr (glog --logtostderr). Keep true in Kubernetes + ## so logs reach `kubectl logs` / the node collector. Setting false routes logs + ## to files under logDir instead — only safe with a writable, mounted logDir. + logtostderr: true + + ## Also write logs to files under logDir IN ADDITION to stderr + ## (glog --alsologtostderr). Requires a writable logDir. + alsologtostderr: false + + ## Directory for glog file output (glog --log_dir); used when logtostderr=false + ## or alsologtostderr=true. Must be a writable mount. Empty = glog default. + logDir: "" + ## Configuration file for dgraph zero used as an alternative to command-line options ## Ref: https://docs.dgraph.io/cli/config configFile: {} @@ -173,12 +213,38 @@ zero: fsGroup: 1001 runAsUser: 1001 + ## dgraph container Security Context. + ## Drops all Linux capabilities and forbids privilege escalation. Every key + ## except `enabled` is rendered verbatim into the container securityContext. + ## readOnlyRootFilesystem is left false because dgraph writes scratch data + ## outside its mounted data dir; flip it on only after verifying on a cluster. + containerSecurityContext: + enabled: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + ## TLS Configuration tls: enabled: false ## Files created from './tls' directory set with `dgraph cert` command ## Can use make_tls_secrets.sh to generate secrets.yaml files: {} + ## Read only when tls.enabled=true; the chart then builds Dgraph's --tls + ## superflag from the keys below. + ## Enable TLS on the internal gRPC port for inter-node traffic (Dgraph's + ## --tls "internal-port="). A boolean, not a port number. + internalPort: true + ## Client cert basename; selects client..crt/.key at /dgraph/tls. + ## Empty omits the client cert from --tls. + clientName: "" + ## Dgraph client-auth-type for the external ports (e.g. REQUIREANDVERIFY). + ## Empty omits the field. REQUIREANY/REQUIREANDVERIFY require a clientName and + ## are incompatible with the default httpGet probes (use VERIFYIFGIVEN or + ## custom exec probes). + clientAuthType: "" ## dgraph data Persistent Volume Storage Class @@ -194,6 +260,13 @@ zero: accessModes: - ReadWriteOnce size: 32Gi + ## Optionally control PVC lifecycle on StatefulSet deletion (whenDeleted) and + ## scale-down (whenScaled). Each is "Retain" (the Kubernetes default) or + ## "Delete". Leaving this unset preserves current behavior. Requires + ## Kubernetes >= 1.27. + # persistentVolumeClaimRetentionPolicy: + # whenDeleted: Retain + # whenScaled: Retain ## Node labels and tolerations for pod assignment ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector @@ -261,6 +334,15 @@ alpha: ## Value for the "monitor" label on the alpha Service (not on pods or other resources). ## Used by Prometheus for service discovery. monitorLabel: alpha-dgraph-io + + ## PodDisruptionBudget for the alpha StatefulSet. Default off — see templates/pdb.yaml + ## for why minAvailable >= replicaCount can deadlock `kubectl drain` on a + ## single-replica group. Review against replicaCount before enabling. + pdb: + enabled: false + minAvailable: 2 + # maxUnavailable: + ## StatefulSet controller supports automated updates. There are two valid update strategies: RollingUpdate and OnDelete ## ref: https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/#updating-statefulsets ## @@ -314,6 +396,27 @@ alpha: ## Extra flags for command line flags in command extraFlags: "" + ## Verbosity (glog -v). INFO/WARNING/ERROR are ALWAYS written to stderr; this + ## only adds finer V-logs on top. Named level OR a raw -v integer: + ## normal->0 (prod default) verbose->1 debug->2 trace->3 + logLevel: normal + + ## Per-module verbosity (glog --vmodule), e.g. "server=3,raft=2". Empty = none. + vmodule: "" + + ## Log to the container's stderr (glog --logtostderr). Keep true in Kubernetes + ## so logs reach `kubectl logs` / the node collector. Setting false routes logs + ## to files under logDir instead — only safe with a writable, mounted logDir. + logtostderr: true + + ## Also write logs to files under logDir IN ADDITION to stderr + ## (glog --alsologtostderr). Requires a writable logDir. + alsologtostderr: false + + ## Directory for glog file output (glog --log_dir); used when logtostderr=false + ## or alsologtostderr=true. Must be a writable mount. Empty = glog default. + logDir: "" + ## Configuration file for dgraph alpha used as an alternative to command-line options ## Ref: https://docs.dgraph.io/cli/config configFile: {} @@ -377,6 +480,19 @@ alpha: fsGroup: 1001 runAsUser: 1001 + ## dgraph container Security Context. + ## Drops all Linux capabilities and forbids privilege escalation. Every key + ## except `enabled` is rendered verbatim into the container securityContext. + ## readOnlyRootFilesystem is left false because dgraph writes scratch data + ## outside its mounted data dir; flip it on only after verifying on a cluster. + containerSecurityContext: + enabled: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + ## TLS Configuration ## Documentation on dgraph alpha TLS options, ## see https://docs.dgraph.io/admin/security/tls-configuration#tls-options @@ -385,24 +501,88 @@ alpha: ## Files created from './tls' directory set with `dgraph cert` command ## Can use make_tls_secrets.sh to generate secrets.yaml files: {} + ## Read only when tls.enabled=true; the chart then builds Dgraph's --tls + ## superflag from the keys below. + ## Enable TLS on the internal gRPC port for inter-node traffic (Dgraph's + ## --tls "internal-port="). A boolean, not a port number. + internalPort: true + ## Client cert basename; selects client..crt/.key at /dgraph/tls. + ## Empty omits the client cert from --tls. + clientName: "" + ## Dgraph client-auth-type for the external ports (e.g. REQUIREANDVERIFY). + ## Empty omits the field. REQUIREANY/REQUIREANDVERIFY require a clientName and + ## are incompatible with the default httpGet probes (use VERIFYIFGIVEN or + ## custom exec probes). + clientAuthType: "" ## ACL Configuration ## ref: https://docs.dgraph.io/installation/configuration/enable-acl acl: enabled: false + ## Filename (and Secret key) of the HMAC secret mounted at /dgraph/acl/. + secretFile: hmac_secret_file + ## Name of a pre-created Secret holding the HMAC key. When set, the chart does NOT + ## render its own ACL Secret from `file` below; the alpha pod mounts this Secret + ## instead, so the HMAC never passes through Helm values or release state. The + ## Secret must contain the `secretFile` key. + existingSecret: "" ## The values in `file: {}` will be the filename as key and the file data as the value. - ## Make sure that the filename key matches the dgraph configuration of `acl_secret_file` + ## Make sure that the filename key matches the dgraph configuration of `acl_secret_file`. + ## Ignored when `existingSecret` is set. # file: # ## value for 1234567890123456789012345678901 # ## Note that Kubernetes secrets must be base64-encoded # hmac_secret_file: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMQo= + ## Bootstrap: a run-once-per-deploy Job (post-install/post-upgrade hook) that + ## rotates `groot` off its default password and converges the declared users and + ## groups. Idempotent, so it is safe on every helm upgrade. Requires acl.enabled + ## and a credentials Secret (acl.existingSecret or the chart-created ACL Secret, + ## holding groot and per-user passwords). Default off. + ## + ## Add an engineer: append a `users` entry with `groups: [guardians]` and add + ## their password under a new key in the credentials Secret (`passwordSecretKey`). + ## Add a service: add a `groups` entry with predicate `rules`, then a `users` + ## entry placed in that group. + bootstrap: + enabled: false + ## Secret holding the credentials the Job reads; defaults to acl.existingSecret + ## (else the chart-created ACL Secret). + existingSecret: "" + ## Key in the credentials Secret holding groot's target (rotated) password. + grootPasswordSecretKey: groot_password + ## Opaque rotation token rendered as a Job pod annotation. Change it (e.g. from + ## a Terraform counter) to force a helm upgrade that re-runs the reconciler, + ## without touching Alpha. Empty = no annotation. + rotation: "" + ## Image for the bootstrap Job. Empty (the default) reuses the deployed dgraph + ## image, which provides bash, curl, and jq. Set registry/repository/tag to pin + ## an explicit override: + # image: + # registry: docker.io + # repository: dgraph/dgraph + # tag: v25.3.8 + image: {} + ## Each group: { name, rules: [{ predicate, permission }] }. + ## permission is OR-summed: 1=read, 2=write, 4=modify. + groups: [] + ## Each user: { name, passwordSecretKey, groups: [ ... ] }. + ## `guardians` is the built-in superadmin group. + users: [] + ## Encryption at Rest Configuration ## ref: https://docs.dgraph.io/installation/configuration/encryption-at-rest encryption: enabled: false + ## Filename (and Secret key) of the encryption key mounted at /dgraph/enc/. + keyFile: enc_key_file + ## Name of a pre-created Secret holding the encryption key. When set, the chart does + ## NOT render its own encryption Secret from `file` below; the alpha pod mounts this + ## Secret instead. The Secret must contain the `keyFile` key. + existingSecret: "" ## The values in `file: {}` will be the filename as key and the file data as the value. - ## Make sure that the filename key matches the dgraph configuration of `encryption_key_file` + ## Make sure that the filename key matches the dgraph configuration of `encryption_key_file`. + ## Ignored when `existingSecret` is set. # file: # ## value for 123456789012345 # ## Note that Kubernetes secrets must be base64-encoded @@ -422,6 +602,13 @@ alpha: - ReadWriteOnce size: 100Gi annotations: {} + ## Optionally control PVC lifecycle on StatefulSet deletion (whenDeleted) and + ## scale-down (whenScaled). Each is "Retain" (the Kubernetes default) or + ## "Delete". Leaving this unset preserves current behavior. Requires + ## Kubernetes >= 1.27. + # persistentVolumeClaimRetentionPolicy: + # whenDeleted: Retain + # whenScaled: Retain ## Node labels and tolerations for pod assignment ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector @@ -591,6 +778,17 @@ ratel: fsGroup: 1001 runAsUser: 1001 + ## ratel container Security Context. + ## Drops all Linux capabilities and forbids privilege escalation. Every key + ## except `enabled` is rendered verbatim into the container securityContext. + containerSecurityContext: + enabled: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + ## Configure resource requests ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ ## @@ -645,6 +843,14 @@ backups: ## ref: https://docs.dgraph.io/installation/configuration/enable-acl user: groot # password: password + ## Name of a pre-created Secret holding the backup admin password. When set, the + ## chart does NOT mint `backup_admin_password` into its own backups Secret; the + ## CronJob mounts this Secret's key instead, so the password never passes through + ## Helm values or release state. Ignored unless alpha.acl.enabled is true. + existingSecret: "" + ## Key within existingSecret (or the chart's own backups Secret) that holds the + ## backup admin password. + passwordSecretKey: backup_admin_password ## Mutual TLS client certificate and key can be used to secure transaction that was created with ## dgraph cert --client should be specified here. ## ref. https://docs.dgraph.io/admin/security/tls-configuration#tls-options @@ -721,6 +927,90 @@ backups: ## AWS_SECRET_ACCESS_KEY env var secret: "" +## Prometheus Operator ServiceMonitor for the alpha and zero metrics endpoints. +## Default off — requires the Prometheus Operator CRDs to be installed; a +## stock install with this disabled imposes no CRD dependency. +serviceMonitor: + enabled: false + # namespace: monitoring + labels: {} + interval: 30s + scrapeTimeout: 10s + path: /debug/prometheus_metrics + +## Prometheus Operator PrometheusRule with conservative default alerts for +## alpha/zero instance availability and backup CronJob failures. +## Default off — requires the Prometheus Operator CRDs to be installed; a +## stock install with this disabled imposes no CRD dependency. +prometheusRule: + enabled: false + labels: {} + defaultRules: true + extraRules: [] + +## Optional NetworkPolicy restricting ingress to the dgraph pods. Default off +## so it can't silently break connectivity on clusters whose CNI does not +## enforce NetworkPolicy. +networkPolicy: + enabled: false + clientPodLabels: {} + extraIngress: [] + +## Post-install validation subsystem. A validator asserts the running cluster +## matches what the chart rendered (health, membership, ACL enforcement, admin +## and per-user logins, group rules, and optionally backup CronJob schedules). +## Default off and inert on a stock install: nothing renders unless enabled. +validation: + ## Master switch for all validator resources (ConfigMap, test Pod, Job, CronJob, RBAC). + enabled: false + ## Validator image. Empty (the default) reuses the deployed dgraph image (which + ## bundles bash, curl, jq) so the validator always matches the running version. + ## Set all three fields below only to pin an explicit override: + # image: + # registry: docker.io + # repository: dgraph/dgraph + # tag: v25.3.8 + image: {} + ## Account the validator logs in as for auth-dependent checks. Defaults to the + ## groot superadmin. + adminUser: groot + ## Secret key holding adminUser's password. Empty derives it (groot's key, else + ## _password). + adminPasswordSecretKey: "" + ## The validator reads passwords from the ACL creds Secret (alpha.acl.bootstrap.existingSecret / + ## alpha.acl.existingSecret, else the chart-managed -acl-secret). An external creds Secret + ## must carry the password keys the validator reads — adminPasswordSecretKey (or groot's key) and each + ## bootstrap user's passwordSecretKey — otherwise the admin/user-login checks FAIL the gating hook. + ## Post-install/upgrade hook Job that gates the release on a passing validation. + ## Default off: a failed check would fail `helm install/upgrade`. + job: + enabled: false + ## Job backoffLimit (also used by the manual CronJob's jobTemplate). + backoffLimit: 1 + ## Suspended manual-trigger CronJob (kubectl create job --from=cronjob/...). + ## Default off. + cronjob: + enabled: false + ## RBAC for the backup-CronJob check (the validator reads CronJobs via the + ## Kubernetes API). Create the validator ServiceAccount/Role/RoleBinding. + ## Required by checkBackups. + rbac: + enabled: false + ## Also assert the backup CronJobs exist with their expected schedules (requires rbac.enabled). + checkBackups: false + ## Trigger a live backup round-trip to S3 (side-effecting, slow; reserved for future use). Default off. + backupRoundtrip: false + ## Per-check retry attempts before failing. + retries: 10 + ## Seconds between retries. + retrySleep: 12 + ## Extra annotations for validator pods. + podAnnotations: {} + ## nodeSelector for validator pods. Empty falls back to alpha.nodeSelector. + nodeSelector: {} + ## tolerations for validator pods. Empty falls back to alpha.tolerations. + tolerations: [] + global: domain: cluster.local ## Combined ingress resource for alpha and ratel services