Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: pgdog-control
description: PgDog Control
type: application
version: 0.3.1
version: 0.3.2
appVersion: "f477677f"
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ 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 `<release>-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.token` | Shared peer token. Empty generates a token stored in the `<release>-raft` Secret and reused by Helm on upgrades, or reads it from external-secrets when `raft.externalSecrets.enabled` is set. 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. 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`). |
Expand All @@ -115,6 +115,64 @@ Raft configuration is omitted when disabled, preserving the existing Deployment.
Switching an existing release to Raft replaces its control workload and can interrupt service
while the new pods and volumes start.

#### Sourcing the peer token from external-secrets

Instead of the chart's generate-and-reuse token (see above), you can source it
from a Secret managed by the [external-secrets](https://external-secrets.io)
operator. The Secret must carry the token under a `token` key.

**Option 1: Create the ExternalSecret with the chart**

```yaml
raft:
enabled: true
externalSecrets:
enabled: true
create: true
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
remoteRefs:
- secretKey: token
remoteRef:
key: pgdog/raft
property: token
```

**Option 2: Use an existing ExternalSecret**

```yaml
raft:
enabled: true
externalSecrets:
enabled: true
create: false
secretName: "my-secret" # Name of Secret you created/manage
```

In both cases, leave `raft.token` unset — the chart looks up the `token` key of
the target Secret (`raft.externalSecrets.secretName`, default `<release>-raft`)
at render time and inlines it into `control.toml`, the same way it does for its
own generated token. The chart stops rendering its own `<release>-raft` Secret
so it doesn't fight the ExternalSecret for ownership.

Because the value is resolved by a `lookup`, a render that can't reach the
cluster — plain `helm template` — falls back to a freshly generated token, and
so does the first `helm install` if the operator hasn't synced the Secret yet.
The three members always agree, since they share one ConfigMap; the next
`helm upgrade` picks up the synced token and rolls the pods through
`checksum/config`.

| Option | Description |
|-|-|
| `raft.externalSecrets.enabled` | Source the peer token from a Secret instead of the chart's generated one (bool, default `false`). |
| `raft.externalSecrets.create` | Render an `ExternalSecret` resource (bool, default `true`). Set to `false` to reference one you manage yourself. |
| `raft.externalSecrets.name` | Name of the `ExternalSecret` resource (only used when `create: true`; defaults to `<release>-raft`). |
| `raft.externalSecrets.secretName` | Name of the target `Secret` populated by the `ExternalSecret`, expected to contain a `token` key (defaults to `<release>-raft`). |
| `raft.externalSecrets.refreshInterval` | How often the operator resyncs from the external store (only used when `create: true`; default `1h`). |
| `raft.externalSecrets.secretStoreRef` | `{name, kind}` of the `SecretStore`/`ClusterSecretStore` to use (only used when `create: true`). |
| `raft.externalSecrets.remoteRefs` | List of `{secretKey, remoteRef: {key, property}}` entries defining what to fetch. The entry feeding the token must use `secretKey: token` (only used when `create: true`). |

### 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):
Expand Down
33 changes: 23 additions & 10 deletions templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -56,28 +56,41 @@ the same cluster don't collide.
{{- printf "%s-raft" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end }}

{{/*
Name of the Secret expected to hold the raft `token` key: either the chart's
own generated Secret, or the Secret targeted by raft.externalSecrets
(populated by the chart-managed ExternalSecret, or by one the user manages
themselves).
*/}}
{{- define "pgdog-control.raft.secretName" -}}
{{- default (include "pgdog-control.raft.fullname" .) .Values.raft.externalSecrets.secretName -}}
{{- 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" -}}
{{- $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 -}}
{{- $token = .Values.raft.token -}}
{{- else -}}
{{- if not (hasKey .Values.raft "_generatedToken") -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace (include "pgdog-control.raft.fullname" .) | default dict -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace (include "pgdog-control.raft.secretName" .) | default dict -}}
{{- $data := $existing.data | default dict -}}
{{- $token := index $data "token" | default "" | b64dec -}}
{{- if not $token -}}
{{- $token = randAlphaNum 64 -}}
{{- $stored := index $data "token" | default "" | b64dec -}}
{{- if not $stored -}}
{{- $stored = randAlphaNum 64 -}}
{{- end -}}
{{- $_ := set .Values.raft "_generatedToken" $stored -}}
{{- end -}}
{{- $_ := set .Values.raft "_generatedToken" $token -}}
{{- $token = .Values.raft._generatedToken -}}
{{- end -}}
{{- .Values.raft._generatedToken -}}
{{/* Also guards tokens read from a Secret, which often carry a trailing newline. */}}
{{- if not (regexMatch "^[!-~]+$" $token) -}}
{{- fail "raft token must contain only non-whitespace printable ASCII characters" -}}
{{- end -}}
{{- $token -}}
{{- end }}

{{/*
Expand Down
31 changes: 31 additions & 0 deletions templates/raft-externalsecret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{{- if and .Values.raft.enabled .Values.raft.externalSecrets.enabled .Values.raft.externalSecrets.create }}
apiVersion: {{ if .Capabilities.APIVersions.Has "external-secrets.io/v1" }}external-secrets.io/v1{{ else }}external-secrets.io/v1beta1{{ end }}
kind: ExternalSecret
metadata:
name: {{ default (include "pgdog-control.raft.fullname" .) .Values.raft.externalSecrets.name }}
labels:
{{- include "pgdog-control.labels" . | nindent 4 }}
spec:
refreshInterval: {{ .Values.raft.externalSecrets.refreshInterval }}
secretStoreRef:
name: {{ .Values.raft.externalSecrets.secretStoreRef.name }}
kind: {{ .Values.raft.externalSecrets.secretStoreRef.kind }}
target:
name: {{ include "pgdog-control.raft.secretName" . }}
creationPolicy: Owner
{{- if .Values.raft.externalSecrets.remoteRefs }}
data:
{{- range .Values.raft.externalSecrets.remoteRefs }}
- secretKey: {{ .secretKey }}
remoteRef:
key: {{ .remoteRef.key }}
{{- if .remoteRef.property }}
property: {{ .remoteRef.property }}
{{- end }}
{{- end }}
{{- else }}
# Default: fetch the peer token from the external secret store.
# Configure remoteRefs in values.yaml for your specific secret store.
data: []
{{- end }}
{{- end }}
5 changes: 3 additions & 2 deletions templates/raft-secret.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{{- if .Values.raft.enabled }}
{{/* Skip the chart-generated secret when externalSecrets supplies the token instead. */}}
{{- if and .Values.raft.enabled (not .Values.raft.externalSecrets.enabled) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "pgdog-control.raft.fullname" . }}
name: {{ include "pgdog-control.raft.secretName" . }}
labels:
{{- include "pgdog-control.labels" . | nindent 4 }}
type: Opaque
Expand Down
10 changes: 10 additions & 0 deletions test/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,15 @@ grep -q 'image: "registry.example.com/platform/redis:8-alpine"' <<< "$image_rend
grep -q 'imagePullPolicy: Always' <<< "$image_render"
grep -q 'name: registry-credentials' <<< "$image_render"

echo ""
echo "==> Verifying raft token sourced from external-secrets..."
raft_es_render=$(helm template test-release "$CHART_DIR" -f "$TEST_DIR/values-raft-external-secrets.yaml")
grep -q '^kind: ExternalSecret$' <<< "$raft_es_render"
grep -q '^ name: test-release-raft$' <<< "$raft_es_render"
if grep -q '^ token: ' <<< "$raft_es_render"; then
echo "chart-generated raft Secret rendered while raft.externalSecrets.enabled=true" >&2
exit 1
fi

echo ""
echo "==> All chart tests passed!"
13 changes: 13 additions & 0 deletions test/values-raft-external-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
raft:
enabled: true
externalSecrets:
enabled: true
create: true
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
remoteRefs:
- secretKey: token
remoteRef:
key: pgdog/raft
property: token
39 changes: 38 additions & 1 deletion values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,45 @@ 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.
# Shared peer token. Empty generates a token retained in a Kubernetes Secret,
# unless raft.externalSecrets below supplies one instead.
token: ""
# externalSecrets sources the shared peer token from a Secret managed by the
# external-secrets operator (https://external-secrets.io) instead of the
# chart's own generated one. Leave raft.token unset when using it.
externalSecrets:
# enabled switches token sourcing to the Secret named below, instead of
# the chart-managed generated secret.
enabled: false
# create controls whether the chart renders an ExternalSecret resource.
# Set to false to reference an ExternalSecret you already manage
# yourself (only secretName is then used).
create: true
# name of the ExternalSecret resource to create (only used when
# create: true). Defaults to `<release>-raft`.
name: ""
# secretName is the target Secret populated by the ExternalSecret (or by
# your own ExternalSecret when create: false). Must contain a `token`
# key. Defaults to `<release>-raft`, the same name the chart would
# otherwise use for its own generated secret.
secretName: ""
# refreshInterval defines how often to sync secrets from the external
# source (only used when create: true)
refreshInterval: 1h
# secretStoreRef references the SecretStore to use
# (only used when create: true)
secretStoreRef:
name: ""
kind: SecretStore
# remoteRefs defines the external secret references. The entry feeding
# the peer token must use `secretKey: token`
# (only used when create: true)
remoteRefs: []
# Example structure:
# - secretKey: token
# remoteRef:
# key: pgdog/raft
# property: token
# Empty defaults to the Helm release name.
cluster_name: ""
sequence_cache_size: 1000
Expand Down
Loading