From 7319e0953fbb095ba6cb280653a0b38690b6f948 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 8 Sep 2026 21:52:21 -0700 Subject: [PATCH 1/5] feat: support raft --- README.md | 40 +++++++++++++++++++++++++++- templates/_helpers.tpl | 29 ++++++++++++++++++++ templates/configmap.yaml | 15 +++++++++++ templates/deployment.yaml | 51 +++++++++++++++++++++++++++++++++--- templates/networkpolicy.yaml | 18 +++++++++++++ templates/raft-secret.yaml | 11 ++++++++ templates/raft-service.yaml | 19 ++++++++++++++ test/values-raft.yaml | 2 ++ values.yaml | 14 ++++++++++ 9 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 templates/raft-secret.yaml create mode 100644 templates/raft-service.yaml create mode 100644 test/values-raft.yaml diff --git a/README.md b/README.md index a0da432..7e80116 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The PgDog deployment contains the following components: | Components | Description | |-|-| -| Deployment | PgDog control plane deployment, with one replica. | +| Deployment / StatefulSet | PgDog control plane deployment with one replica, or a three-replica StatefulSet when `raft.enabled` is true. | | Service | Service pointing to the deployment. Selector labels are configured automatically. | | Ingress / HTTPRoute | Four (4) routing modes are supported: Nginx, AWS ALB, Gateway API, and Default. See [ingress](#ingress) for more details. | | ConfigMap | Configuration for the control plane. | @@ -77,6 +77,44 @@ redis: | `redis.image.pullPolicy` | Redis image pull policy (string, default `IfNotPresent`). | | `redis.image.pullSecrets` | Image pull secrets attached to the Redis pod (list, default `[]`). | +### Raft + +Enable Raft with a top-level setting: + +```yaml +raft: + enabled: true +``` + +Use a control image containing the Raft implementation. Enabling Raft replaces the +control Deployment with a StatefulSet of exactly three replicas, regardless of +`control.replicas`. Preferred pod anti-affinity spreads the replicas across +machines using `kubernetes.io/hostname` when possible. Replicas can share a node, +so single-node clusters such as Minikube are supported. No zone separation is +required. The top-level `nodeSelector` and `tolerations` still apply. + +Each replica gets its own ReadWriteOnce PVC mounted at +`/var/lib/pgdog-control/raft`. The generated `[raft]` section sets `storage_path` +to `/var/lib/pgdog-control/raft/raft.redb`. Claims are retained when the StatefulSet +is deleted. The chart supplies node IDs from pod names, three stable peer addresses +through a headless Service, and peer ingress/egress rules when NetworkPolicy is +enabled. Pods start in parallel and update one at a time. + +| Option | Description | +|-|-| +| `raft.enabled` | Enable the three-member Raft StatefulSet (default `false`). | +| `raft.token` | Shared peer token. Empty generates a token stored in `-raft` Secret and reused by Helm on upgrades. The token is also written to `control.toml` in the ConfigMap. For offline/GitOps rendering, supply a stable token explicitly (default `""`). | +| `raft.cluster_name` | Raft cluster name (default `control2`). | +| `raft.sequence_cache_size` | Positive number of sequence values reserved per Raft write (default `1000`). | +| `raft.persistence.size` | Storage requested by each of the three PVCs (default `1Gi`). | +| `raft.persistence.storageClass` | StorageClass for each PVC. Empty uses the cluster default; `"-"` selects no StorageClass (default `""`). | +| `raft.persistence.mountPath` | PVC mount directory; `storage_path` is this directory plus `/raft.redb` (default `/var/lib/pgdog-control/raft`). | + +Raft configuration is omitted when disabled, preserving the existing Deployment. +Legacy `control.config.leader` settings are omitted when Raft is enabled. Switching +an existing release to Raft replaces its control workload and can interrupt service +while the new pods and volumes start. + ### Ingress The PgDog control plane has a web dashboard. It can be accessed through the Ingress or HTTPRoute the chart creates. The chart supports 4 presets (called modes): diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index ac02274..ae10bac 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -51,6 +51,35 @@ the same cluster don't collide. {{- printf "%s-redis" .Release.Name | trunc 63 | trimSuffix "-" }} {{- end }} +{{/* Stable DNS service and token Secret for the Raft members. */}} +{{- define "pgdog-control.raft.fullname" -}} +{{- printf "%s-raft" .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* +Reuse the installed token on upgrades. Cache a newly generated token in the +render context so the Secret, ConfigMap, and pod checksum all agree. +*/}} +{{- define "pgdog-control.raft.token" -}} +{{- if .Values.raft.token -}} +{{- if not (regexMatch "^[!-~]+$" .Values.raft.token) -}} +{{- fail "raft.token must contain only non-whitespace printable ASCII characters" -}} +{{- end -}} +{{- .Values.raft.token -}} +{{- else -}} +{{- if not (hasKey .Values.raft "_generatedToken") -}} +{{- $existing := lookup "v1" "Secret" .Release.Namespace (include "pgdog-control.raft.fullname" .) | default dict -}} +{{- $data := $existing.data | default dict -}} +{{- $token := index $data "token" | default "" | b64dec -}} +{{- if not $token -}} +{{- $token = randAlphaNum 64 -}} +{{- end -}} +{{- $_ := set .Values.raft "_generatedToken" $token -}} +{{- end -}} +{{- .Values.raft._generatedToken -}} +{{- end -}} +{{- end }} + {{/* Redis URL used by the control plane. redis.url is the public chart setting; control.config.redis.url remains supported for backwards compatibility. diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 5e1374c..918ca04 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -112,6 +112,20 @@ data: {{- end }} {{- end }} + {{- if .Values.raft.enabled }} + + [raft] + token = {{ include "pgdog-control.raft.token" . | quote }} + storage_path = {{ printf "%s/raft.redb" (trimSuffix "/" .Values.raft.persistence.mountPath) | quote }} + cluster_name = {{ .Values.raft.cluster_name | quote }} + sequence_cache_size = {{ .Values.raft.sequence_cache_size }} + {{- range $id := until 3 }} + + [[raft.members]] + id = {{ $id }} + address = {{ printf "http://%s-%d.%s.%s.svc:%v" (include "pgdog-control.control.fullname" $) $id (include "pgdog-control.raft.fullname" $) $.Release.Namespace $.Values.control.port | quote }} + {{- end }} + {{- else }} {{- with $config.leader }} [leader] @@ -131,6 +145,7 @@ data: release_timeout_secs = {{ . }} {{- end }} {{- end }} + {{- end }} {{- with $config.helm }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 69c97d1..46bc62d 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -1,14 +1,35 @@ apiVersion: apps/v1 -kind: Deployment +kind: {{ ternary "StatefulSet" "Deployment" .Values.raft.enabled }} metadata: name: {{ include "pgdog-control.control.fullname" . }} labels: {{- include "pgdog-control.labels" . | nindent 4 }} spec: - replicas: {{ .Values.control.replicas | default 1 }} + replicas: {{ ternary 3 (int (.Values.control.replicas | default 1)) .Values.raft.enabled }} + {{- if .Values.raft.enabled }} + serviceName: {{ include "pgdog-control.raft.fullname" . }} + # Start all members without waiting for an initial quorum to become ready. + podManagementPolicy: Parallel + updateStrategy: + type: RollingUpdate + volumeClaimTemplates: + - metadata: + name: raft + spec: + accessModes: ["ReadWriteOnce"] + {{- if eq .Values.raft.persistence.storageClass "-" }} + storageClassName: "" + {{- else if .Values.raft.persistence.storageClass }} + storageClassName: {{ .Values.raft.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.raft.persistence.size | quote }} + {{- end }} selector: matchLabels: {{- include "pgdog-control.selectorLabels" . | nindent 6 }} + {{- if not .Values.raft.enabled }} # Readiness is leader-aware so only the elected control pod receives # Service traffic. New pods normally start as followers, so they do not # become Ready while the old leader still holds the Lease. Allowing the @@ -20,12 +41,13 @@ spec: rollingUpdate: maxSurge: 0 maxUnavailable: 100% + {{- end }} template: metadata: annotations: meta.helm.sh/release-name: {{ .Release.Name | quote }} meta.helm.sh/release-namespace: {{ .Release.Namespace | quote }} - # Roll the deployment when any rendered config / secret content + # Roll the workload when any rendered config / secret content # changes. Each annotation hashes the *template output*, not # values directly, so transformations applied inside the # template (defaults, lookups, randAlphaNum on first install) @@ -46,7 +68,17 @@ spec: labels: {{- include "pgdog-control.selectorLabels" . | nindent 8 }} spec: - {{- if gt (int (.Values.control.replicas | default 1)) 1 }} + {{- if .Values.raft.enabled }} + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + {{- include "pgdog-control.selectorLabels" . | nindent 18 }} + {{- else if gt (int (.Values.control.replicas | default 1)) 1 }} topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -88,6 +120,13 @@ spec: env: - name: CONTROL_CONFIG value: /etc/pgdog-control/control.toml + {{- if .Values.raft.enabled }} + # control2 extracts the StatefulSet ordinal from the pod name. + - name: RAFT_NODE_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + {{- end }} - name: HOME value: /var/lib/pgdog-control - name: XDG_CACHE_HOME @@ -168,6 +207,10 @@ spec: readOnly: true - name: runtime mountPath: /var/lib/pgdog-control + {{- if .Values.raft.enabled }} + - name: raft + mountPath: {{ .Values.raft.persistence.mountPath | quote }} + {{- end }} - name: tmp mountPath: /tmp ports: diff --git a/templates/networkpolicy.yaml b/templates/networkpolicy.yaml index bdd7cbb..ffcf3f7 100644 --- a/templates/networkpolicy.yaml +++ b/templates/networkpolicy.yaml @@ -13,6 +13,15 @@ spec: - Ingress - Egress ingress: + {{- if .Values.raft.enabled }} + - from: + - podSelector: + matchLabels: + {{- include "pgdog-control.selectorLabels" . | nindent 10 }} + ports: + - protocol: TCP + port: {{ .Values.control.port }} + {{- end }} - from: - namespaceSelector: matchLabels: @@ -24,6 +33,15 @@ spec: {{- toYaml . | nindent 2 }} {{- end }} egress: + {{- if .Values.raft.enabled }} + - to: + - podSelector: + matchLabels: + {{- include "pgdog-control.selectorLabels" . | nindent 10 }} + ports: + - protocol: TCP + port: {{ .Values.control.port }} + {{- end }} {{- if .Values.redis.enabled }} - to: - podSelector: diff --git a/templates/raft-secret.yaml b/templates/raft-secret.yaml new file mode 100644 index 0000000..57f9fe8 --- /dev/null +++ b/templates/raft-secret.yaml @@ -0,0 +1,11 @@ +{{- if .Values.raft.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "pgdog-control.raft.fullname" . }} + labels: + {{- include "pgdog-control.labels" . | nindent 4 }} +type: Opaque +data: + token: {{ include "pgdog-control.raft.token" . | b64enc | quote }} +{{- end }} diff --git a/templates/raft-service.yaml b/templates/raft-service.yaml new file mode 100644 index 0000000..920d06f --- /dev/null +++ b/templates/raft-service.yaml @@ -0,0 +1,19 @@ +{{- if .Values.raft.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "pgdog-control.raft.fullname" . }} + labels: + {{- include "pgdog-control.labels" . | nindent 4 }} +spec: + clusterIP: None + # Peers must be discoverable while the cluster is starting or recovering. + publishNotReadyAddresses: true + ports: + - port: {{ .Values.control.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "pgdog-control.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/test/values-raft.yaml b/test/values-raft.yaml new file mode 100644 index 0000000..6eabefe --- /dev/null +++ b/test/values-raft.yaml @@ -0,0 +1,2 @@ +raft: + enabled: true diff --git a/values.yaml b/values.yaml index 624e5d7..b823dcd 100644 --- a/values.yaml +++ b/values.yaml @@ -11,6 +11,20 @@ nodeSelector: {} # tolerations allows pods to be scheduled on nodes with matching taints tolerations: [] +raft: + # Run three control pods with stable identities and durable Raft storage. + # Prefer separate machines, but allow pods to share a node when needed. + enabled: false + # Shared peer token. Empty generates a token retained in a Kubernetes Secret. + token: "" + cluster_name: control2 + sequence_cache_size: 1000 + persistence: + size: 1Gi + # Empty uses the cluster's default StorageClass; "-" disables dynamic provisioning. + storageClass: "" + mountPath: /var/lib/pgdog-control/raft + control: port: 8080 aws: From daffecf7dc6adf1714a92a17952ce027754165b8 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 8 Sep 2026 22:00:20 -0700 Subject: [PATCH 2/5] chart version --- Chart.yaml | 2 +- README.md | 269 ++++++++++++++++++++++++++--------------------------- 2 files changed, 135 insertions(+), 136 deletions(-) diff --git a/Chart.yaml b/Chart.yaml index 58c8b85..f3669f5 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: pgdog-control description: PgDog Control type: application -version: 0.2.18 +version: 0.3.0 appVersion: "84c7c56a" diff --git a/README.md b/README.md index 7e80116..2d9522e 100644 --- a/README.md +++ b/README.md @@ -40,22 +40,22 @@ This chart installs the PgDog control plane and, by default, a Redis instance. The PgDog deployment contains the following components: -| Components | Description | -|-|-| -| Deployment / StatefulSet | PgDog control plane deployment with one replica, or a three-replica StatefulSet when `raft.enabled` is true. | -| Service | Service pointing to the deployment. Selector labels are configured automatically. | -| Ingress / HTTPRoute | Four (4) routing modes are supported: Nginx, AWS ALB, Gateway API, and Default. See [ingress](#ingress) for more details. | -| ConfigMap | Configuration for the control plane. | -| Secret | Secret that stores the key used to encrypt authentication cookies. | -| Service account, Cluster role, Cluster role bindings | Service account with RBAC to access select Kube APIs. See [RBAC](#rbac) for more details. | -| NetworkPolicy | Optional; restricts ingress/egress traffic. See [NetworkPolicy](#networkpolicy) for more details. | +| Components | Description | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Deployment / StatefulSet | PgDog control plane deployment with one replica, or a three-replica StatefulSet when `raft.enabled` is true. | +| Service | Service pointing to the deployment. Selector labels are configured automatically. | +| Ingress / HTTPRoute | Four (4) routing modes are supported: Nginx, AWS ALB, Gateway API, and Default. See [ingress](#ingress) for more details. | +| ConfigMap | Configuration for the control plane. | +| Secret | Secret that stores the key used to encrypt authentication cookies. | +| Service account, Cluster role, Cluster role bindings | Service account with RBAC to access select Kube APIs. See [RBAC](#rbac) for more details. | +| NetworkPolicy | Optional; restricts ingress/egress traffic. See [NetworkPolicy](#networkpolicy) for more details. | By default, the chart also deploys a single-replica Redis instance. The control plane uses Redis for storing metrics. Set `redis.enabled: false` and provide `redis.url` to use an external Redis instead. The chart-managed Redis has the following components: -| Components | Description | -|-|-| -| Deployment | Redis deployment with one replica. | -| Service | Redis service pointing to the deployment, with selector labels configured automatically. | +| Components | Description | +| ---------- | ---------------------------------------------------------------------------------------- | +| Deployment | Redis deployment with one replica. | +| Service | Redis service pointing to the deployment, with selector labels configured automatically. | ```yaml redis: @@ -68,14 +68,14 @@ redis: pullSecrets: [] ``` -| Option | Description | -|-|-| -| `redis.enabled` | Deploy the chart-managed Redis resources (bool, default `true`). | -| `redis.url` | Redis connection string written to `[redis].url` in `control.toml`. When empty, defaults to the chart-managed Redis Service (string, default `""`). | -| `redis.image.repository` | Redis image repository (string, default `redis`). | -| `redis.image.tag` | Redis image tag (string, default `7-alpine`). | -| `redis.image.pullPolicy` | Redis image pull policy (string, default `IfNotPresent`). | -| `redis.image.pullSecrets` | Image pull secrets attached to the Redis pod (list, default `[]`). | +| Option | Description | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `redis.enabled` | Deploy the chart-managed Redis resources (bool, default `true`). | +| `redis.url` | Redis connection string written to `[redis].url` in `control.toml`. When empty, defaults to the chart-managed Redis Service (string, default `""`). | +| `redis.image.repository` | Redis image repository (string, default `redis`). | +| `redis.image.tag` | Redis image tag (string, default `7-alpine`). | +| `redis.image.pullPolicy` | Redis image pull policy (string, default `IfNotPresent`). | +| `redis.image.pullSecrets` | Image pull secrets attached to the Redis pod (list, default `[]`). | ### Raft @@ -86,29 +86,30 @@ raft: enabled: true ``` -Use a control image containing the Raft implementation. Enabling Raft replaces the -control Deployment with a StatefulSet of exactly three replicas, regardless of +Enabling Raft replaces the control `Deployment` with a `StatefulSet` of exactly 3 replicas, regardless of `control.replicas`. Preferred pod anti-affinity spreads the replicas across -machines using `kubernetes.io/hostname` when possible. Replicas can share a node, -so single-node clusters such as Minikube are supported. No zone separation is -required. The top-level `nodeSelector` and `tolerations` still apply. +machines using `kubernetes.io/hostname`, when possible. + +The top-level `nodeSelector` and `tolerations` still apply. Each replica gets its own ReadWriteOnce PVC mounted at `/var/lib/pgdog-control/raft`. The generated `[raft]` section sets `storage_path` to `/var/lib/pgdog-control/raft/raft.redb`. Claims are retained when the StatefulSet is deleted. The chart supplies node IDs from pod names, three stable peer addresses through a headless Service, and peer ingress/egress rules when NetworkPolicy is -enabled. Pods start in parallel and update one at a time. - -| Option | Description | -|-|-| -| `raft.enabled` | Enable the three-member Raft StatefulSet (default `false`). | -| `raft.token` | Shared peer token. Empty generates a token stored in `-raft` Secret and reused by Helm on upgrades. The token is also written to `control.toml` in the ConfigMap. For offline/GitOps rendering, supply a stable token explicitly (default `""`). | -| `raft.cluster_name` | Raft cluster name (default `control2`). | -| `raft.sequence_cache_size` | Positive number of sequence values reserved per Raft write (default `1000`). | -| `raft.persistence.size` | Storage requested by each of the three PVCs (default `1Gi`). | -| `raft.persistence.storageClass` | StorageClass for each PVC. Empty uses the cluster default; `"-"` selects no StorageClass (default `""`). | -| `raft.persistence.mountPath` | PVC mount directory; `storage_path` is this directory plus `/raft.redb` (default `/var/lib/pgdog-control/raft`). | +enabled. + +Pods start in parallel and update one at a time. + +| Option | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `raft.enabled` | Enable the three-member Raft StatefulSet (default `false`). | +| `raft.token` | Shared peer token. Empty generates a token stored in `-raft` Secret and reused by Helm on upgrades. The token is also written to `control.toml` in the ConfigMap. For offline/GitOps rendering, supply a stable token explicitly (default `""`). | +| `raft.cluster_name` | Raft cluster name (default `control2`). | +| `raft.sequence_cache_size` | Positive number of sequence values reserved per Raft write (default `1000`). | +| `raft.persistence.size` | Storage requested by each of the three PVCs (default `1Gi`). | +| `raft.persistence.storageClass` | StorageClass for each PVC. Empty uses the cluster default; `"-"` selects no StorageClass (default `""`). | +| `raft.persistence.mountPath` | PVC mount directory; `storage_path` is this directory plus `/raft.redb` (default `/var/lib/pgdog-control/raft`). | Raft configuration is omitted when disabled, preserving the existing Deployment. Legacy `control.config.leader` settings are omitted when Raft is enabled. Switching @@ -130,12 +131,12 @@ The mode is selected by `ingress.mode`. In `nginx`, `aws`, and `default` modes, All three modes share the options below: -| Option | Description | -|-|-| -| `ingress.enabled` | Enable/disable the Ingress (bool, default `true`). | -| `ingress.mode` | One of `nginx`, `aws`, `gateway`, or `default`. Defaults to `nginx`. | -| `ingress.host` | External hostname, e.g. pgdog.acme.com. Required for Nginx and AWS ALB; optional for Default. | -| `ingress.labels` | Extra `metadata.labels` merged on top of the chart's standard labels (map, default `{}`). | +| Option | Description | +| ----------------- | --------------------------------------------------------------------------------------------- | +| `ingress.enabled` | Enable/disable the Ingress (bool, default `true`). | +| `ingress.mode` | One of `nginx`, `aws`, `gateway`, or `default`. Defaults to `nginx`. | +| `ingress.host` | External hostname, e.g. pgdog.acme.com. Required for Nginx and AWS ALB; optional for Default. | +| `ingress.labels` | Extra `metadata.labels` merged on top of the chart's standard labels (map, default `{}`). | #### Nginx @@ -153,11 +154,11 @@ ingress: sslRedirect: "true" ``` -| Option | Description | -|-|-| -| `ingress.nginx.tls.enabled` | When `true`, emits the cert-manager and ssl-redirect annotations and a `tls` block referencing `-control-tls` (bool, default `true`). | -| `ingress.nginx.clusterIssuer` | Value of the `cert-manager.io/cluster-issuer` annotation (string, default `letsencrypt-prod`). | -| `ingress.nginx.sslRedirect` | Value of the `nginx.ingress.kubernetes.io/ssl-redirect` annotation. Quoted because nginx expects a string (string, default `"true"`). | +| Option | Description | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `ingress.nginx.tls.enabled` | When `true`, emits the cert-manager and ssl-redirect annotations and a `tls` block referencing `-control-tls` (bool, default `true`). | +| `ingress.nginx.clusterIssuer` | Value of the `cert-manager.io/cluster-issuer` annotation (string, default `letsencrypt-prod`). | +| `ingress.nginx.sslRedirect` | Value of the `nginx.ingress.kubernetes.io/ssl-redirect` annotation. Quoted because nginx expects a string (string, default `"true"`). | ##### Finding an existing ClusterIssuer @@ -259,12 +260,12 @@ ingress: sslRedirect: true ``` -| Option | Description | -|-|-| -| `ingress.aws.scheme` | `alb.ingress.kubernetes.io/scheme`. Either `internet-facing` or `internal` (string, default `internet-facing`). | -| `ingress.aws.subnets` | Optional comma-separated subnet IDs rendered as `alb.ingress.kubernetes.io/subnets`. Empty = controller auto-discovers subnets from AWS tags (string, default `""`). | -| `ingress.aws.certificateArn` | ACM cert ARN attached to the HTTPS listener. Empty = HTTP-only ALB, no 443 listener (string, default `""`). | -| `ingress.aws.sslRedirect` | When `true` and `certificateArn` is set, the ALB redirects HTTP:80 → HTTPS:443. Ignored when `certificateArn` is empty (bool, default `true`). | +| Option | Description | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ingress.aws.scheme` | `alb.ingress.kubernetes.io/scheme`. Either `internet-facing` or `internal` (string, default `internet-facing`). | +| `ingress.aws.subnets` | Optional comma-separated subnet IDs rendered as `alb.ingress.kubernetes.io/subnets`. Empty = controller auto-discovers subnets from AWS tags (string, default `""`). | +| `ingress.aws.certificateArn` | ACM cert ARN attached to the HTTPS listener. Empty = HTTP-only ALB, no 443 listener (string, default `""`). | +| `ingress.aws.sslRedirect` | When `true` and `certificateArn` is set, the ALB redirects HTTP:80 → HTTPS:443. Ignored when `certificateArn` is empty (bool, default `true`). | #### Gateway API @@ -281,10 +282,10 @@ ingress: sectionName: web ``` -| Option | Description | -|-|-| -| `ingress.gateway.name` | Name of the Gateway resource the HTTPRoute attaches to (string, required). | -| `ingress.gateway.namespace` | Namespace of the Gateway resource (string, required). | +| Option | Description | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `ingress.gateway.name` | Name of the Gateway resource the HTTPRoute attaches to (string, required). | +| `ingress.gateway.namespace` | Namespace of the Gateway resource (string, required). | | `ingress.gateway.sectionName` | Selects a specific listener on the Gateway. Leave empty to attach to all listeners that match the hostname (string, optional). | The chart does not create or manage the Gateway itself; that's expected to exist already. The HTTPRoute routes all paths (`/`) to the control Service on port 80, scoped to the hostname in `ingress.host`. TLS, certificates, and load balancer configuration are handled by the Gateway and its associated resources. @@ -307,11 +308,11 @@ ingress: secretName: control-tls ``` -| Option | Description | -|-|-| -| `ingress.ingressClassName` | Rendered as `spec.ingressClassName` when non-empty (string, default `""`). | -| `ingress.annotations` | Rendered verbatim as `metadata.annotations` (map, default `{}`). | -| `ingress.tls` | Rendered verbatim under `spec.tls`. Supply the full `[{hosts, secretName}]` list (list, default `[]`). | +| Option | Description | +| -------------------------- | ------------------------------------------------------------------------------------------------------ | +| `ingress.ingressClassName` | Rendered as `spec.ingressClassName` when non-empty (string, default `""`). | +| `ingress.annotations` | Rendered verbatim as `metadata.annotations` (map, default `{}`). | +| `ingress.tls` | Rendered verbatim under `spec.tls`. Supply the full `[{hosts, secretName}]` list (list, default `[]`). | ### DNS @@ -341,11 +342,11 @@ control: In the above example, the dashboard can see workloads in every namespace, but it can only spin up or tear down PgDog deployments in `pgdog-prod` and `pgdog-staging`. Leaving `writeNamespaces` empty produces a fully read-only install. The dashboard still works, but the "deploy" actions will be rejected by the API server. -| Option | Description | -|-|-| -| `control.rbac.create` | Render the ServiceAccount and the RBAC bindings. When `false`, no RBAC is rendered and the pod runs without a mounted API token. The Kubernetes views in the dashboard will be empty (bool, default `true`). | -| `control.rbac.serviceAccountName` | Override the generated ServiceAccount name. Empty falls back to `-control` (string, default `""`). | -| `control.rbac.writeNamespaces` | Namespaces where the control plane is allowed to manage PgDog workloads. Each entry produces one Role + RoleBinding pair. Empty means the install is read-only everywhere (list, default `[]`). | +| Option | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `control.rbac.create` | Render the ServiceAccount and the RBAC bindings. When `false`, no RBAC is rendered and the pod runs without a mounted API token. The Kubernetes views in the dashboard will be empty (bool, default `true`). | +| `control.rbac.serviceAccountName` | Override the generated ServiceAccount name. Empty falls back to `-control` (string, default `""`). | +| `control.rbac.writeNamespaces` | Namespaces where the control plane is allowed to manage PgDog workloads. Each entry produces one Role + RoleBinding pair. Empty means the install is read-only everywhere (list, default `[]`). | ### Disabling RBAC @@ -377,9 +378,9 @@ networkPolicy: port: 8080 ``` -| Option | Description | -|-|-| -| `networkPolicy.enabled` | Render the control and, when enabled, Redis `NetworkPolicy` resources (bool, default `false`). | +| Option | Description | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `networkPolicy.enabled` | Render the control and, when enabled, Redis `NetworkPolicy` resources (bool, default `false`). | | `networkPolicy.extraIngress` | Additional ingress rules appended to the control `NetworkPolicy`, on top of the built-in ingress-nginx rule. Each entry follows the standard `NetworkPolicyIngressRule` schema (`from`/`ports`) and is passed through verbatim (list, default `[]`). | ## AWS access (EKS / IRSA) @@ -503,9 +504,7 @@ The control plane only reads from AWS. It never creates, modifies, or deletes an { "Sid": "EC2InstanceTypes", "Effect": "Allow", - "Action": [ - "ec2:DescribeInstanceTypes" - ], + "Action": ["ec2:DescribeInstanceTypes"], "Resource": "*" }, { @@ -539,11 +538,11 @@ control: `region` is emitted as `AWS_REGION` on the container and is required unless the pod runs on a node whose IMDS already exposes one. For clusters without IRSA (kind, minikube, a non-EKS managed cluster), set `control.aws.accessKeyId` / `secretAccessKey` instead. The chart will render a `-aws-creds` Secret and load it via `envFrom`. Don't do this on EKS; IRSA is strictly better. -| Option | Description | -|-|-| -| `control.aws.roleArn` | IAM role ARN. When non-empty, annotates the ServiceAccount with `eks.amazonaws.com/role-arn` so the EKS pod-identity webhook can inject `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE` (string, default `""`). | -| `control.aws.region` | AWS region the SDK targets. Rendered as `AWS_REGION` on the container (string, default `""`). | -| `control.aws.accessKeyId` / `secretAccessKey` / `sessionToken` | Static IAM-user credentials. Only for non-EKS clusters. Don't set these alongside `roleArn`; pick one (string, default `""`). | +| Option | Description | +| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `control.aws.roleArn` | IAM role ARN. When non-empty, annotates the ServiceAccount with `eks.amazonaws.com/role-arn` so the EKS pod-identity webhook can inject `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE` (string, default `""`). | +| `control.aws.region` | AWS region the SDK targets. Rendered as `AWS_REGION` on the container (string, default `""`). | +| `control.aws.accessKeyId` / `secretAccessKey` / `sessionToken` | Static IAM-user credentials. Only for non-EKS clusters. Don't set these alongside `roleArn`; pick one (string, default `""`). | ## Configuration @@ -573,9 +572,9 @@ control: If `allowed_cidrs` is omitted, the control plane defaults to private IPv4 ranges, IPv4/IPv6 loopback, and IPv6 ULA. The check intentionally uses the direct TCP peer address and ignores forwarded headers such as `X-Forwarded-For`; configure the CIDRs for the address the control plane actually sees from your ingress, load balancer, sidecar, or PgDog caller. -| Option | Description | -|-|-| -| `api.pgdog.ip_allowlist.enabled` | Enables source-IP checks for `/api/v2/*` PgDog endpoints (bool, default `false`). | +| Option | Description | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `api.pgdog.ip_allowlist.enabled` | Enables source-IP checks for `/api/v2/*` PgDog endpoints (bool, default `false`). | | `api.pgdog.ip_allowlist.allowed_cidrs` | CIDR ranges allowed to call `/api/v2/*`. Invalid CIDRs cause protected requests to be rejected until the config is fixed (list of strings, default private IPv4 ranges, loopback, and IPv6 ULA). | ### Authentication @@ -599,17 +598,17 @@ control: allowed_domains: [acme.com] ``` -| Option | Description | -|-|-| -| `redirect_base_url` | Public base URL of the dashboard. Used to build the OAuth redirect URI registered with each provider, e.g. `https://control.acme.com/auth/github/callback`. Defaults to `http://localhost:8080` (string, optional). | -| `cookie_secret` | Master key used to sign the session and CSRF cookies. **Leave empty in production.** The chart generates a random 64-character key on first install and stores it in a `-secrets` Secret, then reuses it on every `helm upgrade` via a `lookup` call so sessions survive rollouts. Setting this explicitly disables the helper Secret (string, optional). | -| `cookie_secure` | Set the `Secure` flag on cookies. Disable only for local HTTP testing (bool, default `true`). | -| `session_max_age_days` | Lifetime of the signed session cookie (int, default `30`). | -| `state_max_age_min` | Lifetime of the per-request CSRF state cookie. Has to outlive the user clicking through the provider's consent screen (int, default `10`). | -| `github.client_id` / `github.client_secret` | OAuth credentials from the GitHub App. Required to enable the GitHub login route. | -| `github.allowed_orgs` | If non-empty, only users whose membership the GitHub API reports in one of these orgs are allowed to log in. The `read:org` scope is added automatically when this list is non-empty (list of strings, default `[]`). | -| `google.client_id` / `google.client_secret` | OAuth credentials from the Google Cloud OAuth client. Required to enable the Google login route. | -| `google.allowed_domains` | If non-empty, only users whose verified Google email's domain (the part after `@`, compared case-insensitively) appears in this list are allowed to log in (list of strings, default `[]`). | +| Option | Description | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `redirect_base_url` | Public base URL of the dashboard. Used to build the OAuth redirect URI registered with each provider, e.g. `https://control.acme.com/auth/github/callback`. Defaults to `http://localhost:8080` (string, optional). | +| `cookie_secret` | Master key used to sign the session and CSRF cookies. **Leave empty in production.** The chart generates a random 64-character key on first install and stores it in a `-secrets` Secret, then reuses it on every `helm upgrade` via a `lookup` call so sessions survive rollouts. Setting this explicitly disables the helper Secret (string, optional). | +| `cookie_secure` | Set the `Secure` flag on cookies. Disable only for local HTTP testing (bool, default `true`). | +| `session_max_age_days` | Lifetime of the signed session cookie (int, default `30`). | +| `state_max_age_min` | Lifetime of the per-request CSRF state cookie. Has to outlive the user clicking through the provider's consent screen (int, default `10`). | +| `github.client_id` / `github.client_secret` | OAuth credentials from the GitHub App. Required to enable the GitHub login route. | +| `github.allowed_orgs` | If non-empty, only users whose membership the GitHub API reports in one of these orgs are allowed to log in. The `read:org` scope is added automatically when this list is non-empty (list of strings, default `[]`). | +| `google.client_id` / `google.client_secret` | OAuth credentials from the Google Cloud OAuth client. Required to enable the Google login route. | +| `google.allowed_domains` | If non-empty, only users whose verified Google email's domain (the part after `@`, compared case-insensitively) appears in this list are allowed to log in (list of strings, default `[]`). | #### Sourcing OAuth credentials from a Secret @@ -627,7 +626,7 @@ control: auth: redirect_base_url: https://control.acme.com github: - client_id: Iv1.0123456789abcdef # not sensitive — fine to inline + client_id: Iv1.0123456789abcdef # not sensitive — fine to inline allowed_orgs: [acme-corp] secret: name: oauth-secrets @@ -640,10 +639,10 @@ control: clientSecretKey: google-client-secret ``` -| Option | Description | -|-|-| -| `.secret.name` | Name of an existing `Secret` in the release namespace holding the credentials. Required when either key below is set (string, optional). | -| `.secret.clientIdKey` | Key in that Secret to inject as `GITHUB_CLIENT_ID` / `GOOGLE_CLIENT_ID`. Leave `client_id` unset when this is set (string, optional). | +| Option | Description | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.secret.name` | Name of an existing `Secret` in the release namespace holding the credentials. Required when either key below is set (string, optional). | +| `.secret.clientIdKey` | Key in that Secret to inject as `GITHUB_CLIENT_ID` / `GOOGLE_CLIENT_ID`. Leave `client_id` unset when this is set (string, optional). | | `.secret.clientSecretKey` | Key in that Secret to inject as `GITHUB_CLIENT_SECRET` / `GOOGLE_CLIENT_SECRET`. Leave `client_secret` unset when this is set (string, optional). | The provider's `[auth.]` section still has to render for the login route to be enabled, so keep at least one inline field (`client_id`, `allowed_orgs`/`allowed_domains`) or the `secret` block set under the provider. Env vars sourced this way are not hashed into the deployment's `checksum/config` annotation — rotating the referenced Secret needs a manual `kubectl rollout restart deployment/-control`. @@ -661,10 +660,10 @@ control: repo_url: https://helm.pgdog.dev ``` -| Option | Description | -|-|-| -| `chart` | Chart name within the repo. The control plane installs `{repo}/{chart}` (string, default `pgdog`). | -| `repo` | Locally-registered repo name. Used both as the prefix in the chart reference and as the name passed to `helm repo add` (string, default `pgdogdev`). | +| Option | Description | +| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `chart` | Chart name within the repo. The control plane installs `{repo}/{chart}` (string, default `pgdog`). | +| `repo` | Locally-registered repo name. Used both as the prefix in the chart reference and as the name passed to `helm repo add` (string, default `pgdogdev`). | | `repo_url` | Repo index URL. This is what `helm repo add ` is pointed at on boot, so the dashboard doesn't need an out-of-band `helm repo add` step (string, default `https://helm.pgdog.dev`). | ### Background polling @@ -688,15 +687,15 @@ control: period_secs: 60 ``` -| Option | Description | -|-|-| -| `rds.refresh_interval_secs` | How often to poll AWS RDS for cluster and instance topology (int, default `60`). | -| `rds.autodiscovery` | **Experimental. Do not enable in production yet.** Automatically reconcile Helm-managed PgDog database entries from discovered RDS topology (bool, default `false`). | -| `kube.refresh_interval_secs` | How often to poll Kubernetes for PgDog workloads. Independent of the `watch` streams, which fire on events (int, default `15`). | -| `dns.refresh_interval_secs` | How often to re-resolve every known RDS hostname (int, default `30`). | -| `cloudwatch.refresh_interval_secs` | How often to poll CloudWatch for per-instance metrics (int, default `60`). | -| `cloudwatch.lookback_secs` | How far back each fetch reaches. A fresh deploy pulls the full window on its first tick (int, default `3600`). | -| `cloudwatch.period_secs` | CloudWatch aggregation period. The smallest bucket the metric API returns (int, default `60`). | +| Option | Description | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `rds.refresh_interval_secs` | How often to poll AWS RDS for cluster and instance topology (int, default `60`). | +| `rds.autodiscovery` | **Experimental. Do not enable in production yet.** Automatically reconcile Helm-managed PgDog database entries from discovered RDS topology (bool, default `false`). | +| `kube.refresh_interval_secs` | How often to poll Kubernetes for PgDog workloads. Independent of the `watch` streams, which fire on events (int, default `15`). | +| `dns.refresh_interval_secs` | How often to re-resolve every known RDS hostname (int, default `30`). | +| `cloudwatch.refresh_interval_secs` | How often to poll CloudWatch for per-instance metrics (int, default `60`). | +| `cloudwatch.lookback_secs` | How far back each fetch reaches. A fresh deploy pulls the full window on its first tick (int, default `3600`). | +| `cloudwatch.period_secs` | CloudWatch aggregation period. The smallest bucket the metric API returns (int, default `60`). | ### Alerting @@ -717,15 +716,15 @@ control: api_key: inc_live_xxx ``` -| Option | Description | -|-|-| -| `evaluation_window_secs` | How long metrics must remain at or above threshold before creating an alert (int, default `300`). | -| `thresholds.clients_waiting` | Number of clients waiting on a server connection (int, optional). | -| `thresholds.cpu` | CPU usage percentage. Must be between `0.0` and `100.0`, inclusive (float, optional). | -| `thresholds.memory` | Memory used, in megabytes (int, optional). | -| `thresholds.server_connections` | Number of open server connections (int, optional). | -| `thresholds.slow_queries` | Create incidents for queries whose duration reaches `store.slow_queries_threshold` (bool, default `false`). | -| `incident_io.api_key` | incident.io API key with permission to create incidents. Missing `incident_io` disables the integration (string, optional). | +| Option | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `evaluation_window_secs` | How long metrics must remain at or above threshold before creating an alert (int, default `300`). | +| `thresholds.clients_waiting` | Number of clients waiting on a server connection (int, optional). | +| `thresholds.cpu` | CPU usage percentage. Must be between `0.0` and `100.0`, inclusive (float, optional). | +| `thresholds.memory` | Memory used, in megabytes (int, optional). | +| `thresholds.server_connections` | Number of open server connections (int, optional). | +| `thresholds.slow_queries` | Create incidents for queries whose duration reaches `store.slow_queries_threshold` (bool, default `false`). | +| `incident_io.api_key` | incident.io API key with permission to create incidents. Missing `incident_io` disables the integration (string, optional). | ### State store @@ -745,16 +744,16 @@ control: autoreload: immediately # or in_sync, or off ``` -| Option | Description | -|-|-| -| `tick_secs` | How often the sweep task wakes up. Sets the shortest possible reaction time for stale and evict transitions (int, default `1`). | -| `stale_after_secs` | Instance is marked stale if its newest metric is older than this. The UI dims it but keeps it visible (int, default `5`). | -| `evict_after_secs` | Instance is dropped from the store entirely if its newest metric is older than this (int, default `60`). | -| `metrics_retention_secs` | How much per-instance metric history is kept in memory. Older points are dropped as new ones arrive (int, default `300`). | -| `query_history_limit` | Per-token historical query store capacity. Oldest deduped query entries are evicted first once the limit is reached (int, default `1000`). | -| `query_plans_limit` | Per-token query-plan capacity. Plans with the oldest creation time are evicted first once the limit is reached; `0` disables plan storage (int, default `100`). | -| `slow_queries_threshold` | Minimum query duration, in milliseconds, for classifying a query as slow (int, default `5000`). | -| `autoreload` | Automatically enqueue `reload_configuration` for instances that report config drift (enum, default `off`, available options: `off`, `immediately`, `in_sync`). | +| Option | Description | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tick_secs` | How often the sweep task wakes up. Sets the shortest possible reaction time for stale and evict transitions (int, default `1`). | +| `stale_after_secs` | Instance is marked stale if its newest metric is older than this. The UI dims it but keeps it visible (int, default `5`). | +| `evict_after_secs` | Instance is dropped from the store entirely if its newest metric is older than this (int, default `60`). | +| `metrics_retention_secs` | How much per-instance metric history is kept in memory. Older points are dropped as new ones arrive (int, default `300`). | +| `query_history_limit` | Per-token historical query store capacity. Oldest deduped query entries are evicted first once the limit is reached (int, default `1000`). | +| `query_plans_limit` | Per-token query-plan capacity. Plans with the oldest creation time are evicted first once the limit is reached; `0` disables plan storage (int, default `100`). | +| `slow_queries_threshold` | Minimum query duration, in milliseconds, for classifying a query as slow (int, default `5000`). | +| `autoreload` | Automatically enqueue `reload_configuration` for instances that report config drift (enum, default `off`, available options: `off`, `immediately`, `in_sync`). | ### Slack Notifications @@ -768,10 +767,10 @@ control: channel: C0123456789 ``` -| Option | Description | -|-|-| +| Option | Description | +| ----------- | ---------------------------------------------------------------- | | `bot_token` | Slack bot token with `chat:write` permission (string, optional). | -| `channel` | Slack channel ID or name for status updates (string, optional). | +| `channel` | Slack channel ID or name for status updates (string, optional). | ### Redis persistence @@ -784,8 +783,8 @@ control: save_interval_secs: 60 ``` -| Option | Description | -|-|-| +| Option | Description | +| -------------------- | ------------------------------------------------------------------------------- | | `save_interval_secs` | How often the background task snapshots the store to Redis (int, default `60`). | To use an external Redis, disable all chart-managed Redis resources and set its URL: From f004c6cfabcbcfb452d162fb6fe9fc74e6b850ee Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 8 Sep 2026 22:02:44 -0700 Subject: [PATCH 3/5] edits --- README.md | 2 +- templates/configmap.yaml | 2 +- values.yaml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2d9522e..3c7c7e9 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ Pods start in parallel and update one at a time. | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `raft.enabled` | Enable the three-member Raft StatefulSet (default `false`). | | `raft.token` | Shared peer token. Empty generates a token stored in `-raft` Secret and reused by Helm on upgrades. The token is also written to `control.toml` in the ConfigMap. For offline/GitOps rendering, supply a stable token explicitly (default `""`). | -| `raft.cluster_name` | Raft cluster name (default `control2`). | +| `raft.cluster_name` | Raft cluster name. Empty defaults to the Helm release name (default `""`). | | `raft.sequence_cache_size` | Positive number of sequence values reserved per Raft write (default `1000`). | | `raft.persistence.size` | Storage requested by each of the three PVCs (default `1Gi`). | | `raft.persistence.storageClass` | StorageClass for each PVC. Empty uses the cluster default; `"-"` selects no StorageClass (default `""`). | diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 918ca04..526ebb7 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -117,7 +117,7 @@ data: [raft] token = {{ include "pgdog-control.raft.token" . | quote }} storage_path = {{ printf "%s/raft.redb" (trimSuffix "/" .Values.raft.persistence.mountPath) | quote }} - cluster_name = {{ .Values.raft.cluster_name | quote }} + cluster_name = {{ .Values.raft.cluster_name | default .Release.Name | quote }} sequence_cache_size = {{ .Values.raft.sequence_cache_size }} {{- range $id := until 3 }} diff --git a/values.yaml b/values.yaml index b823dcd..52eb92d 100644 --- a/values.yaml +++ b/values.yaml @@ -17,7 +17,8 @@ raft: enabled: false # Shared peer token. Empty generates a token retained in a Kubernetes Secret. token: "" - cluster_name: control2 + # Empty defaults to the Helm release name. + cluster_name: "" sequence_cache_size: 1000 persistence: size: 1Gi From d0d2f133d30edcb218909f86ea7debb3ff91cbc3 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 8 Sep 2026 22:25:12 -0700 Subject: [PATCH 4/5] remove leader --- README.md | 4 +--- templates/configmap.yaml | 20 -------------------- templates/deployment.yaml | 6 ------ templates/rbac.yaml | 29 ----------------------------- test/values-full.yaml | 6 ------ values.yaml | 6 ------ 6 files changed, 1 insertion(+), 70 deletions(-) diff --git a/README.md b/README.md index 3c7c7e9..1f169b4 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,7 @@ Pods start in parallel and update one at a time. | `raft.persistence.mountPath` | PVC mount directory; `storage_path` is this directory plus `/raft.redb` (default `/var/lib/pgdog-control/raft`). | Raft configuration is omitted when disabled, preserving the existing Deployment. -Legacy `control.config.leader` settings are omitted when Raft is enabled. Switching -an existing release to Raft replaces its control workload and can interrupt service +Switching an existing release to Raft replaces its control workload and can interrupt service while the new pods and volumes start. ### Ingress @@ -326,7 +325,6 @@ When `control.rbac.create` is `true` (default), the chart renders: - A `ServiceAccount` for the control pod. If `control.aws.roleArn` is set, the ServiceAccount also carries the `eks.amazonaws.com/role-arn` annotation, which is what EKS IRSA looks for when handing the pod temporary AWS credentials. - A `ClusterRole` and `ClusterRoleBinding` granting **read-only** access cluster-wide. This is enough for the dashboard to list namespaces and read deployments, statefulsets, pods, services, configmaps, and secrets in any namespace. It cannot change anything. Pod logs are included so the deployment log view works. -- A namespace-scoped `Role` and `RoleBinding` in the release namespace granting access to `coordination.k8s.io` `Lease` objects for control-plane leader election. - For each namespace you list in `control.rbac.writeNamespaces`, a namespace-scoped `Role` and `RoleBinding` granting **write** access (create, update, patch, delete) on the resources PgDog actually manages: deployments, statefulsets, services, configmaps, secrets, service accounts, roles, role bindings, and pod disruption budgets. Namespaces not on the list stay strictly read-only. A typical setup grants write access only to the namespaces where you want PgDog clusters to live: diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 526ebb7..77aba4e 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -125,26 +125,6 @@ data: id = {{ $id }} address = {{ printf "http://%s-%d.%s.%s.svc:%v" (include "pgdog-control.control.fullname" $) $id (include "pgdog-control.raft.fullname" $) $.Release.Namespace $.Values.control.port | quote }} {{- end }} - {{- else }} - {{- with $config.leader }} - - [leader] - {{- if hasKey . "enabled" }} - enabled = {{ .enabled }} - {{- end }} - {{- with .lease_name }} - lease_name = {{ . | quote }} - {{- end }} - {{- with .lease_duration_secs }} - lease_duration_secs = {{ . }} - {{- end }} - {{- with .renew_interval_secs }} - renew_interval_secs = {{ . }} - {{- end }} - {{- with .release_timeout_secs }} - release_timeout_secs = {{ . }} - {{- end }} - {{- end }} {{- end }} {{- with $config.helm }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 46bc62d..6b0219d 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -30,12 +30,6 @@ spec: matchLabels: {{- include "pgdog-control.selectorLabels" . | nindent 6 }} {{- if not .Values.raft.enabled }} - # Readiness is leader-aware so only the elected control pod receives - # Service traffic. New pods normally start as followers, so they do not - # become Ready while the old leader still holds the Lease. Allowing the - # whole old ReplicaSet to be unavailable lets Kubernetes terminate the - # old leader, release the Lease, and promote a new-version pod instead - # of waiting forever for a follower to become Ready. strategy: type: RollingUpdate rollingUpdate: diff --git a/templates/rbac.yaml b/templates/rbac.yaml index bf7112d..443f0b4 100644 --- a/templates/rbac.yaml +++ b/templates/rbac.yaml @@ -54,35 +54,6 @@ subjects: - kind: ServiceAccount name: {{ include "pgdog-control.control.serviceAccountName" . }} namespace: {{ .Release.Namespace }} ---- -# Namespace-scoped access for control-plane leader election. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ include "pgdog-control.control.clusterFullname" . }}-leader - namespace: {{ .Release.Namespace }} - labels: - {{- include "pgdog-control.labels" . | nindent 4 }} -rules: - - apiGroups: ["coordination.k8s.io"] - resources: ["leases"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "pgdog-control.control.clusterFullname" . }}-leader - namespace: {{ .Release.Namespace }} - labels: - {{- include "pgdog-control.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ include "pgdog-control.control.clusterFullname" . }}-leader -subjects: - - kind: ServiceAccount - name: {{ include "pgdog-control.control.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} {{- range $ns := .Values.control.rbac.writeNamespaces }} --- # Namespace-scoped write access for workloads the control plane manages. diff --git a/test/values-full.yaml b/test/values-full.yaml index 02e301a..e071069 100644 --- a/test/values-full.yaml +++ b/test/values-full.yaml @@ -37,12 +37,6 @@ control: autoreload: "immediately" autoscaling: pool_size: true - leader: - enabled: true - lease_name: pgdog-control-leader - lease_duration_secs: 20 - renew_interval_secs: 7 - release_timeout_secs: 4 helm: chart: pgdog repo: pgdogdev diff --git a/values.yaml b/values.yaml index 52eb92d..10c49b1 100644 --- a/values.yaml +++ b/values.yaml @@ -130,12 +130,6 @@ control: # autoreload: off autoscaling: {} # pool_size: false - leader: {} - # enabled: true - # lease_name: "" # Empty means derive from Helm release. - # lease_duration_secs: 15 - # renew_interval_secs: 5 - # release_timeout_secs: 3 helm: {} # chart: pgdog # repo: pgdogdev From 884f2df09c1710e2fd304e6d225a420aded39ab5 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 8 Sep 2026 22:28:53 -0700 Subject: [PATCH 5/5] app version --- Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chart.yaml b/Chart.yaml index f3669f5..c3d2996 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -3,4 +3,4 @@ name: pgdog-control description: PgDog Control type: application version: 0.3.0 -appVersion: "84c7c56a" +appVersion: "f477677f"