Skip to content
Merged
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.4.2
version: 0.4.3
appVersion: "main-ent"
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,12 @@ 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 `<release>-control` (string, default `""`). |
| `control.rbac.existingServiceAccount` | Run the pod as this existing ServiceAccount. Requires `create: false`; see [Disabling RBAC](#disabling-rbac) (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 `<release>-control` (string, default `""`). |
| `control.rbac.existingServiceAccount` | Run the pod as this existing ServiceAccount. Requires `create: false`; see [Disabling RBAC](#disabling-rbac) (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

Expand Down Expand Up @@ -559,6 +559,27 @@ If `allowed_cidrs` is omitted, the control plane defaults to private IPv4 ranges
| `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). |

### MCP

Set `control.config.mcp` to enable the `/mcp` endpoint for MCP clients. With Raft enabled, tokens can be generated and removed in System / MCP without configuring any tokens at startup:

```yaml
raft:
enabled: true
control:
config:
mcp:
enabled: true
tokens: []
```

Clients authenticate with `Authorization: Bearer <token>`. These tokens are separate from `control.config.auth.tokens`, which protect the PgDog API. The MCP endpoint uses the control plane's existing port and ingress.

| Option | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `mcp.enabled` | Enables the `/mcp` endpoint (bool, default `false`). |
| `mcp.tokens` | Optional configured MCP bearer tokens. When omitted, defaults to comma-separated `MCP_TOKENS`, or an empty list if unset. |

### Authentication

Set `control.config.auth.tokens` to restrict requests to the PgDog API to the listed Bearer tokens:
Expand Down Expand Up @@ -610,6 +631,7 @@ Secrets are injected as environment variables. Leave corresponding inline values
| Environment variable | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| `AUTH_TOKENS` | Comma-separated Bearer token allowlist for `/api/v2`, sourced through `control.config.auth.secret.tokensKey`. |
| `MCP_TOKENS` | Comma-separated MCP bearer tokens, sourced through `control.config.mcp.secret.tokensKey`. |
| `GITHUB_CLIENT_ID` | GitHub OAuth application ID. |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth application secret. |
| `GOOGLE_CLIENT_ID` | Google OAuth application ID. |
Expand All @@ -628,6 +650,7 @@ metadata:
type: Opaque
stringData:
auth-tokens: "token-1,token-2"
mcp-tokens: "mcp-token-1,mcp-token-2"
github-client-id: "your-github-client-id"
github-client-secret: "your-github-client-secret"
google-client-id: "your-google-client-id"
Expand All @@ -642,6 +665,11 @@ Apply it in the release namespace with `kubectl apply -n <release-namespace> -f
```yaml
control:
config:
mcp:
enabled: true
secret:
name: control-secrets
tokensKey: mcp-tokens
auth:
secret:
name: control-secrets
Expand Down
11 changes: 11 additions & 0 deletions templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ data:
{{- end }}
{{- end }}

{{- with $config.mcp }}

[mcp]
{{- if hasKey . "enabled" }}
enabled = {{ .enabled }}
{{- end }}
{{- if hasKey . "tokens" }}
tokens = [{{ range $i, $token := .tokens }}{{ if $i }}, {{ end }}{{ $token | quote }}{{ end }}]
{{- end }}
{{- end }}

{{- with $config.rds }}
[rds]
{{- with .refresh_interval_secs }}
Expand Down
9 changes: 9 additions & 0 deletions templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ spec:
value: {{ . | quote }}
{{- end }}
{{- $auth := (.Values.control.config | default dict).auth | default dict }}
{{- with ((.Values.control.config | default dict).mcp | default dict).secret }}
{{- if .tokensKey }}
- name: MCP_TOKENS
valueFrom:
secretKeyRef:
name: {{ required "control.config.mcp.secret.name is required when tokensKey is set" .name | quote }}
key: {{ .tokensKey | quote }}
{{- end }}
{{- end }}
{{- with $auth.secret }}
{{- if .tokensKey }}
- name: AUTH_TOKENS
Expand Down
66 changes: 66 additions & 0 deletions test/test-mcp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/bash
set -eo pipefail

TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CHART_DIR="$TEST_DIR/.."

mcp_config() {
helm template test-release "$CHART_DIR" --show-only templates/configmap.yaml "$@" \
| yq -r '.data["control.toml"]'
}

echo "==> Verifying MCP configuration..."
default_config=$(mcp_config)
if grep -q '^\[mcp\]$' <<< "$default_config"; then
echo "MCP configuration rendered with default values" >&2
exit 1
fi

for raft in false true; do
enabled_config=$(mcp_config -f "$TEST_DIR/values-mcp.yaml" --set "raft.enabled=$raft")
grep -q '^\[mcp\]$' <<< "$enabled_config"
grep -q '^enabled = true$' <<< "$enabled_config"
grep -Fqx 'tokens = ["test-mcp-token", "test-mcp._~+/=token"]' <<< "$enabled_config"

empty_config=$(mcp_config -f "$TEST_DIR/values-mcp.yaml" --set "raft.enabled=$raft" \
--set-json 'control.config.mcp.tokens=[]')
grep -q '^\[mcp\]$' <<< "$empty_config"
grep -q '^enabled = true$' <<< "$empty_config"
grep -Fqx 'tokens = []' <<< "$empty_config"

omitted_config=$(mcp_config --set control.config.mcp.enabled=true --set "raft.enabled=$raft")
grep -q '^\[mcp\]$' <<< "$omitted_config"
grep -q '^enabled = true$' <<< "$omitted_config"
if grep -q '^tokens = ' <<< "$omitted_config"; then
echo "MCP tokens rendered when omitted from values" >&2
exit 1
fi

secret_render=$(helm template test-release "$CHART_DIR" -f "$TEST_DIR/values-secrets.yaml" \
--set "raft.enabled=$raft")
secret_ref=$(yq -o=json -I=0 'select(.kind == "Deployment" or .kind == "StatefulSet") | .spec.template.spec.containers[] | select(.name == "control") | .env[] | select(.name == "MCP_TOKENS") | .valueFrom.secretKeyRef' <<< "$secret_render")
if [ "$secret_ref" != '{"name":"control-secrets","key":"mcp-tokens"}' ]; then
echo "MCP_TOKENS does not reference the configured Secret: $secret_ref" >&2
exit 1
fi
secret_config=$(mcp_config -f "$TEST_DIR/values-secrets.yaml" --set "raft.enabled=$raft")
grep -q '^\[mcp\]$' <<< "$secret_config"
if grep -q '^tokens = ' <<< "$secret_config"; then
echo "Rendered tokens would override MCP_TOKENS from the Secret" >&2
exit 1
fi
done

if helm template test-release "$CHART_DIR" -f "$TEST_DIR/values-secrets.yaml" \
--set control.config.mcp.secret.name= >/dev/null 2>&1; then
echo "MCP Secret reference rendered without a Secret name" >&2
exit 1
fi

disabled_config=$(mcp_config -f "$TEST_DIR/values-mcp.yaml" \
--set control.config.mcp.enabled=false --set-json 'control.config.mcp.tokens=[]')
grep -q '^\[mcp\]$' <<< "$disabled_config"
grep -q '^enabled = false$' <<< "$disabled_config"
grep -Fqx 'tokens = []' <<< "$disabled_config"

echo "==> MCP configuration tests passed!"
2 changes: 2 additions & 0 deletions test/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,6 @@ if [ "$raft_annotation" != "24h" ]; then
fi

echo ""
bash "$TEST_DIR/test-mcp.sh"

echo "==> All chart tests passed!"
7 changes: 7 additions & 0 deletions test/values-mcp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
control:
config:
mcp:
enabled: true
tokens:
- test-mcp-token
- test-mcp._~+/=token
5 changes: 5 additions & 0 deletions test/values-secrets.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
control:
config:
mcp:
enabled: true
secret:
name: control-secrets
tokensKey: mcp-tokens
auth:
secret:
name: control-secrets
Expand Down
10 changes: 10 additions & 0 deletions values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ control:
# # - ::1/128
# # - fc00::/7
# allowed_cidrs: []
mcp: {}
# Enable the bearer-authenticated /mcp endpoint. Disabled by default.
# enabled: false
# Optional configured tokens, written to the control.toml ConfigMap.
# Omit or leave empty to use tokens managed through Raft in System → MCP.
# Authentication remains required when the list is empty.
# tokens: []
# secret:
# name: "" # existing Secret in the release namespace
# tokensKey: "" # comma-separated list -> MCP_TOKENS; leave tokens unset
rds: {}
# refresh_interval_secs: 60
# Experimental: do not enable in production yet.
Expand Down
Loading