diff --git a/Chart.yaml b/Chart.yaml index c917891..d5c21c4 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: pgdog-control description: PgDog Control type: application -version: 0.4.0 +version: 0.4.1 appVersion: "main-ent" diff --git a/README.md b/README.md index 8edb52f..517cdcb 100644 --- a/README.md +++ b/README.md @@ -317,11 +317,23 @@ In the above example, the dashboard can see workloads in every namespace, but it | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `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.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 -If your cluster manages RBAC out-of-band (a platform team's controller, GitOps, an admission policy), set `control.rbac.create: false`. The chart then renders no ServiceAccount, no ClusterRole/Binding, and no Role/Bindings, and the deployment runs the pod with `automountServiceAccountToken: false`. The dashboard still serves the UI, but every Kubernetes-backed view will be empty until you bind an externally-managed ServiceAccount with equivalent permissions to the pod yourself. +If your cluster manages RBAC out-of-band (a platform team's controller, GitOps, an admission policy), set `control.rbac.create: false`. The chart then renders no ServiceAccount, no ClusterRole/Binding, and no Role/Bindings, and the deployment runs the pod with `automountServiceAccountToken: false`. The dashboard still serves the UI, but every Kubernetes-backed view will be empty. + +To run the pod as a ServiceAccount you manage, with RBAC bound out of band, an IRSA role, or both, name it in `control.rbac.existingServiceAccount`: + +```yaml +control: + rbac: + create: false + existingServiceAccount: pgdog-control +``` + +The ServiceAccount must exist before `helm install`. The chart leaves `automountServiceAccountToken` unset on the pod, so the ServiceAccount's own setting decides whether the pod gets a Kubernetes API token: keep it enabled for the Kubernetes views, or set it to `false` when the pod only needs IRSA. `control.aws.roleArn` is rejected in this mode; annotate the ServiceAccount with `eks.amazonaws.com/role-arn` yourself. ## NetworkPolicy @@ -365,7 +377,7 @@ This needs three things, only one of which is in the chart: ### Trust policy -The role must trust the cluster's OIDC provider and scope the trust to the control plane's ServiceAccount. That subject is `system:serviceaccount::-control`, or whatever `control.rbac.serviceAccountName` is if you overrode it. Confirm the exact subject after `helm install` with: +The role must trust the cluster's OIDC provider and scope the trust to the control plane's ServiceAccount. That subject is `system:serviceaccount::-control`, or whatever `control.rbac.serviceAccountName` or `control.rbac.existingServiceAccount` names. Confirm the exact subject after `helm install` with: ```sh kubectl -n get sa \ diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 600920f..1795aa2 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -89,7 +89,15 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.control.rbac.existingServiceAccount }} {{- if .Values.control.rbac.create }} + {{- fail "control.rbac.existingServiceAccount runs the pod without the chart's RBAC; set control.rbac.create=false" }} + {{- end }} + {{- if .Values.control.aws.roleArn }} + {{- fail "control.aws.roleArn annotates the chart's ServiceAccount; annotate control.rbac.existingServiceAccount instead" }} + {{- end }} + serviceAccountName: {{ .Values.control.rbac.existingServiceAccount }} + {{- else if .Values.control.rbac.create }} serviceAccountName: {{ include "pgdog-control.control.serviceAccountName" . }} automountServiceAccountToken: true {{- else }} diff --git a/test/test.sh b/test/test.sh index 7e8d5af..ac69db1 100755 --- a/test/test.sh +++ b/test/test.sh @@ -21,5 +21,32 @@ grep -q '^ query_plans_limit = 75$' <<< "$full_render" grep -q '^ slow_queries_threshold = 2500$' <<< "$full_render" grep -q '^ slow_queries = true$' <<< "$full_render" +echo "" +echo "==> Verifying an existing ServiceAccount without RBAC..." +control_sa() { + helm template test-release "$CHART_DIR" "$@" \ + | yq -o=json -I=0 'select(.kind == "Deployment" and .metadata.name == "test-release-control") | .spec.template.spec | {"sa": .serviceAccountName, "automount": .automountServiceAccountToken}' +} +sa_render=$(helm template test-release "$CHART_DIR" -f "$TEST_DIR/values-existing-service-account.yaml") +if [ "$(control_sa -f "$TEST_DIR/values-existing-service-account.yaml")" != '{"sa":"pgdog-control-aws","automount":null}' ]; then + echo "control pod does not run as the existing ServiceAccount, or overrides its token setting" >&2 + exit 1 +fi +if grep -qE '^kind: (ServiceAccount|ClusterRole|ClusterRoleBinding)$' <<< "$sa_render"; then + echo "RBAC resources rendered while control.rbac.create=false" >&2 + exit 1 +fi +# Opt-in only: without existingServiceAccount, create=false keeps the default SA and ignores roleArn. +if [ "$(control_sa --set control.rbac.create=false --set control.rbac.serviceAccountName=foo --set control.aws.roleArn=arn:aws:iam::123456789012:role/x)" != '{"sa":null,"automount":false}' ]; then + echo "create=false without existingServiceAccount changed the control pod's ServiceAccount" >&2 + exit 1 +fi +for conflict in "control.rbac.create=true" "control.aws.roleArn=arn:aws:iam::123456789012:role/x"; do + if helm template test-release "$CHART_DIR" -f "$TEST_DIR/values-existing-service-account.yaml" --set "$conflict" >/dev/null 2>&1; then + echo "existingServiceAccount rendered with $conflict" >&2 + exit 1 + fi +done + echo "" echo "==> All chart tests passed!" diff --git a/test/values-existing-service-account.yaml b/test/values-existing-service-account.yaml new file mode 100644 index 0000000..485b8b1 --- /dev/null +++ b/test/values-existing-service-account.yaml @@ -0,0 +1,5 @@ +# Run as an existing ServiceAccount (e.g. IRSA) without the chart's RBAC. +control: + rbac: + create: false + existingServiceAccount: pgdog-control-aws diff --git a/values.yaml b/values.yaml index d1103ae..f4a5dba 100644 --- a/values.yaml +++ b/values.yaml @@ -62,6 +62,12 @@ control: # Override the ServiceAccount name. When empty, falls back to # `-control`. serviceAccountName: "" + # Run the control pod as this existing ServiceAccount instead, e.g. one + # with RBAC managed out of band or annotated for EKS IRSA. Requires + # create: false; the ServiceAccount must exist before install and carry + # its own role annotation (roleArn is not used). Its own + # automountServiceAccountToken decides whether the pod gets an API token. + existingServiceAccount: "" # Namespaces where the control plane is allowed to create/update/patch/delete # workloads (deployments, statefulsets, configmaps, secrets, services). # A Role + RoleBinding is generated in each listed namespace. Empty