{"id":51788533,"url":"https://github.com/pgdogdev/helm-ee","last_synced_at":"2026-07-20T20:32:55.181Z","repository":{"id":349636983,"uuid":"1199638867","full_name":"pgdogdev/helm-ee","owner":"pgdogdev","description":"PgDog Enterprise control plane Helm chart.","archived":false,"fork":false,"pushed_at":"2026-06-22T19:59:51.000Z","size":111,"stargazers_count":0,"open_issues_count":1,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-22T21:14:35.455Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pgdogdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-02T14:53:19.000Z","updated_at":"2026-06-18T22:14:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pgdogdev/helm-ee","commit_stats":null,"previous_names":["pgdogdev/helm-ee"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/pgdogdev/helm-ee","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdogdev%2Fhelm-ee","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdogdev%2Fhelm-ee/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdogdev%2Fhelm-ee/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdogdev%2Fhelm-ee/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pgdogdev","download_url":"https://codeload.github.com/pgdogdev/helm-ee/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdogdev%2Fhelm-ee/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35699589,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"ssl_error","status_checked_at":"2026-07-20T02:08:09.736Z","response_time":111,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-20T20:32:50.419Z","updated_at":"2026-07-20T20:32:55.172Z","avatar_url":"https://github.com/pgdogdev.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PgDog EE Helm Chart\n\nProduction-ready [Helm](https://helm.sh) chart for the PgDog Enterprise\n[Control Plane](https://docs.pgdog.dev/enterprise_edition/control_plane/).\n\n## Installation\n\n### Guided installation\n\nThis chart has a few dependencies, like having a valid `Ingress` controller and an IAM role\nto allow the web dashboard read-only access to RDS and CloudWatch.\n\nTo quickly check that your EKS cluster has everything, you can run this Bash script:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/pgdogdev/helm-ee/main/install.sh | bash\n```\n\nIt's strictly read-only and will print out warnings or errors. It can also help generate a valid\nIAM role with a Trust Policy.\n\n### Manual install\n\nInstall the chart with Helm (read below for configuration options):\n\n```sh\nhelm repo add pgdogdev-ee https://helm-ee.pgdog.dev\nhelm install control pgdogdev-ee/pgdog-control\n```\n\nThe three somewhat complex steps are:\n\n1. Configuring AWS RDS/CloudWatch permissions (IAM)\n2. Setting up an Ingress with TLS termination (nginx, ALB and Gateway API supported)\n3. Configuring OAuth for the control plane dashboard (GitHub and Google auth supported)\n\n## Chart summary\n\nThis chart installs two deployments: PgDog control plane and Redis.\n\nThe PgDog deployment contains the following components:\n\n| Components | Description |\n|-|-|\n| Deployment | PgDog control plane deployment, with one replica. |\n| Service | Service pointing to the deployment. Selector labels are configured automatically. |\n| Ingress / HTTPRoute | Four (4) routing modes are supported: Nginx, AWS ALB, Gateway API, and Default. See [ingress](#ingress) for more details. |\n| ConfigMap | Configuration for the control plane. |\n| Secret | Secret that stores the key used to encrypt authentication cookies. |\n| Service account, Cluster role, Cluster role bindings | Service account with RBAC to access select Kube APIs. See [RBAC](#rbac) for more details. |\n\nIn addition to installing the PgDog control plane, this chart will deploy a Redis deployment (with one replica). The control plane uses Redis for storing\nmetrics. The Redis deployment has the following components:\n\n| Components | Description |\n|-|-|\n| Deployment | Redis deployment with one replica. |\n| Service | Redis service pointing to the deployment, with selector labels configured automatically. |\n\n### Ingress\n\nThe 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):\n\n- Nginx\n- AWS ALB\n- Gateway API\n- Default\n\nNginx and AWS ALB are Ingress-based presets with set annotations that should work for most deployments. Gateway API renders an HTTPRoute instead of an Ingress; use it when traffic enters through a Gateway controller. The Default mode allows the user to configure all Ingress options (class, annotations, etc.).\n\nThe mode is selected by `ingress.mode`. In `nginx`, `aws`, and `default` modes, the chart renders exactly one Ingress, whose rule always routes `/` to the control Service on port 80. In `gateway` mode, the chart renders an HTTPRoute instead. Only one of these resources is created per install.\n\nAll three modes share the options below:\n\n| Option | Description |\n|-|-|\n| `ingress.enabled` | Enable/disable the Ingress (bool, default `true`). |\n| `ingress.mode` | One of `nginx`, `aws`, `gateway`, or `default`. Defaults to `nginx`. |\n| `ingress.host` | External hostname, e.g. pgdog.acme.com. Required for Nginx and AWS ALB; optional for Default. |\n| `ingress.labels` | Extra `metadata.labels` merged on top of the chart's standard labels (map, default `{}`). |\n\n#### Nginx\n\nThe Nginx preset targets [ingress-nginx](https://kubernetes.github.io/ingress-nginx/) with [cert-manager](https://cert-manager.io/) handling certificate issuance. The chart hardcodes `ingressClassName: nginx`, emits the cert-manager and ssl-redirect annotations, and renders a `tls` block that references `\u003crelease\u003e-control-tls`. The cert-manager fills that Secret in response to the cluster issuer.\n\n```yaml\ningress:\n  enabled: true\n  mode: nginx\n  host: pgdog.acme.com\n  nginx:\n    tls:\n      enabled: true\n    clusterIssuer: letsencrypt-prod\n    sslRedirect: \"true\"\n```\n\n| Option | Description |\n|-|-|\n| `ingress.nginx.tls.enabled` | When `true`, emits the cert-manager and ssl-redirect annotations and a `tls` block referencing `\u003crelease\u003e-control-tls` (bool, default `true`). |\n| `ingress.nginx.clusterIssuer` | Value of the `cert-manager.io/cluster-issuer` annotation (string, default `letsencrypt-prod`). |\n| `ingress.nginx.sslRedirect` | Value of the `nginx.ingress.kubernetes.io/ssl-redirect` annotation. Quoted because nginx expects a string (string, default `\"true\"`). |\n\n##### Finding an existing ClusterIssuer\n\nIf cert-manager is already installed, list the available issuers:\n\n```sh\nkubectl get clusterissuers\n```\n\nThe output looks like:\n\n```\nNAME                  READY   AGE\nletsencrypt-prod      True    42d\nletsencrypt-staging   True    42d\n```\n\nUse the `NAME` column verbatim as `ingress.nginx.clusterIssuer`. `ClusterIssuer` is cluster-scoped, so you don't need `-n`. The issuer doesn't have to live in the release namespace.\n\nCheck that `READY` is `True`. If it isn't, run `kubectl describe clusterissuer \u003cname\u003e` and fix the issuer first. Otherwise the cert request will stay in `Pending` state.\n\nIf the command returns `error: the server doesn't have a resource type \"clusterissuers\"`, cert-manager isn't installed. See below.\n\n##### Installing `ingress-nginx` and `cert-manager`\n\nOn a cluster with neither component, install both before installing this chart. Order matters. Install ingress-nginx first. Then cert-manager. Then create a ClusterIssuer.\n\n**1. ingress-nginx**\n\n```sh\nhelm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx\nhelm repo update\nhelm install ingress-nginx ingress-nginx/ingress-nginx \\\n  --namespace ingress-nginx --create-namespace\n```\n\nWait for the controller's Service to get an external address. On most managed clusters that's a `LoadBalancer`. You'll need its hostname or IP for DNS:\n\n```sh\nkubectl -n ingress-nginx get svc ingress-nginx-controller -w\n```\n\nPoint `control.acme.com` (or whatever `ingress.host` you'll use) at that address before continuing. Let's Encrypt's HTTP-01 challenge fails if the hostname doesn't resolve to the controller.\n\n**2. cert-manager**\n\n```sh\nhelm repo add jetstack https://charts.jetstack.io\nhelm repo update\nhelm install cert-manager jetstack/cert-manager \\\n  --namespace cert-manager --create-namespace \\\n  --set crds.enabled=true\n```\n\n**3. A ClusterIssuer**\n\ncert-manager doesn't ship issuers. You create them. Here's a minimal Let's Encrypt production issuer that solves HTTP-01 through ingress-nginx:\n\n```yaml\n# letsencrypt-prod.yaml\napiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n  name: letsencrypt-prod\nspec:\n  acme:\n    server: https://acme-v02.api.letsencrypt.org/directory\n    email: you@acme.com\n    privateKeySecretRef:\n      name: letsencrypt-prod-account-key\n    solvers:\n      - http01:\n          ingress:\n            ingressClassName: nginx\n```\n\n```sh\nkubectl apply -f letsencrypt-prod.yaml\nkubectl get clusterissuer letsencrypt-prod -w   # wait for READY=True\n```\n\nFor first-time setup, point `server` at `https://acme-staging-v02.api.letsencrypt.org/directory` and create a separate `letsencrypt-staging` issuer. Staging has much higher rate limits. You can iterate on the install without burning prod issuance quota. Once it works, re-issue against the prod issuer.\n\nOnce the issuer is `READY=True`, set `ingress.nginx.clusterIssuer: letsencrypt-prod` in `values.yaml` and install the chart.\n\n#### AWS ALB\n\nThe AWS ALB preset targets the [AWS Load Balancer Controller](https://kubernetes-sigs.github.io/aws-load-balancer-controller/). The chart hardcodes `ingressClassName: alb` and `alb.ingress.kubernetes.io/target-type: ip`, and lets ACM terminate TLS at the load balancer. Supply an ACM cert ARN to get the HTTPS listener; leave it empty for HTTP-only.\n\n```yaml\ningress:\n  enabled: true\n  mode: aws\n  host: control.example.com\n  aws:\n    scheme: internet-facing\n    subnets: subnet-aaa,subnet-bbb\n    certificateArn: arn:aws:acm:us-east-1:111111111111:certificate/abc-123\n    sslRedirect: true\n```\n\n| Option | Description |\n|-|-|\n| `ingress.aws.scheme` | `alb.ingress.kubernetes.io/scheme`. Either `internet-facing` or `internal` (string, default `internet-facing`). |\n| `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 `\"\"`). |\n| `ingress.aws.certificateArn` | ACM cert ARN attached to the HTTPS listener. Empty = HTTP-only ALB, no 443 listener (string, default `\"\"`). |\n| `ingress.aws.sslRedirect` | When `true` and `certificateArn` is set, the ALB redirects HTTP:80 → HTTPS:443. Ignored when `certificateArn` is empty (bool, default `true`). |\n\n#### Gateway API\n\nThe Gateway API mode is selected with `ingress.mode: gateway`. Instead of an Ingress, the chart renders an [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) that attaches to an existing Gateway resource. Use this when your cluster routes traffic through a Gateway controller (Traefik, Envoy Gateway, AWS ALB via `gateway.k8s.aws`, etc.) and TLS is terminated at the Gateway or its backing load balancer.\n\n```yaml\ningress:\n  enabled: true\n  mode: gateway\n  host: control.example.com\n  gateway:\n    name: traefik-gw\n    namespace: traefik\n    sectionName: web\n```\n\n| Option | Description |\n|-|-|\n| `ingress.gateway.name` | Name of the Gateway resource the HTTPRoute attaches to (string, required). |\n| `ingress.gateway.namespace` | Namespace of the Gateway resource (string, required). |\n| `ingress.gateway.sectionName` | Selects a specific listener on the Gateway. Leave empty to attach to all listeners that match the hostname (string, optional). |\n\nThe 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.\n\n### Default\n\nThe Default mode is selected with `ingress.mode: default`. The chart adds nothing on top: no annotations, no `ingressClassName`, no `tls` block. You can route through any controller (Traefik, HAProxy, Contour, GKE, etc.) by supplying the keys it expects, for example:\n\n```yaml\ningress:\n  enabled: true\n  mode: default\n  host: control.example.com\n  ingressClassName: traefik\n  annotations:\n    traefik.ingress.kubernetes.io/router.entrypoints: websecure\n    cert-manager.io/cluster-issuer: letsencrypt-prod\n  tls:\n    - hosts: [control.example.com]\n      secretName: control-tls\n```\n\n| Option | Description |\n|-|-|\n| `ingress.ingressClassName` | Rendered as `spec.ingressClassName` when non-empty (string, default `\"\"`). |\n| `ingress.annotations` | Rendered verbatim as `metadata.annotations` (map, default `{}`). |\n| `ingress.tls` | Rendered verbatim under `spec.tls`. Supply the full `[{hosts, secretName}]` list (list, default `[]`). |\n\n### DNS\n\nIf you need TLS, you will also need to setup DNS. In AWS, you can create a Route53 CNAME record pointing to the ALB and issue a cert for it in ACM. If using the Nginx controller, `cert-manager` will issue the certificate, but you still need to create a DNS record manually.\n\n## RBAC\n\nThe control plane talks to the Kubernetes API in two distinct ways: it **reads** workloads from every namespace so the dashboard can render them, and it **writes** to a short list of namespaces where you actually want it to manage PgDog deployments.\n\nWhen `control.rbac.create` is `true` (default), the chart renders:\n\n- 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.\n- 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.\n- A namespace-scoped `Role` and `RoleBinding` in the release namespace granting access to `coordination.k8s.io` `Lease` objects for control-plane leader election.\n- 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.\n\nA typical setup grants write access only to the namespaces where you want PgDog clusters to live:\n\n```yaml\ncontrol:\n  rbac:\n    create: true\n    writeNamespaces:\n      - pgdog-prod\n      - pgdog-staging\n```\n\nIn 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.\n\n| Option | Description |\n|-|-|\n| `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`). |\n| `control.rbac.serviceAccountName` | Override the generated ServiceAccount name. Empty falls back to `\u003crelease\u003e-control` (string, default `\"\"`). |\n| `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 `[]`). |\n\n### Disabling RBAC\n\nIf 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.\n\n## AWS access (EKS / IRSA)\n\nThe control plane reads RDS topology and CloudWatch metrics so the dashboard can show your databases alongside the PgDog workloads. To do that in EKS without baking long-lived keys into the cluster, the recommended path is **IRSA** (IAM Roles for Service Accounts).\n\nThis needs three things, only one of which is in the chart:\n\n1. **An OIDC provider for the cluster, registered in IAM.** This is a one-time per-cluster setup (`eksctl utils associate-iam-oidc-provider --cluster \u003cname\u003e --approve`, or the equivalent Terraform / console steps).\n2. **An IAM role** whose trust policy lets the pod's ServiceAccount assume it, with a permissions policy granting read access to RDS and CloudWatch. Details below.\n3. **`control.aws.roleArn`** set to that role's ARN. The chart annotates the ServiceAccount with `eks.amazonaws.com/role-arn: \u003croleArn\u003e`, and the rest happens automatically inside the pod.\n\n### Trust policy\n\nThe role must trust the cluster's OIDC provider and scope the trust to the control plane's ServiceAccount. That subject is `system:serviceaccount:\u003crelease-namespace\u003e:\u003crelease\u003e-control`, or whatever `control.rbac.serviceAccountName` is if you overrode it. Confirm the exact subject after `helm install` with:\n\n```sh\nkubectl -n \u003crelease-namespace\u003e get sa \\\n  -l app.kubernetes.io/instance=\u003crelease\u003e,app.kubernetes.io/component=control \\\n  -o name\n```\n\nThe role's trust policy has to match the SA name byte-for-byte. An off-by-one here surfaces as `AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity` in the pod logs. Replace the account ID, region, and OIDC ID with your own:\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Federated\": \"arn:aws:iam::111111111111:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE\"\n      },\n      \"Action\": \"sts:AssumeRoleWithWebIdentity\",\n      \"Condition\": {\n        \"StringEquals\": {\n          \"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub\": \"system:serviceaccount:default:control-control\",\n          \"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud\": \"sts.amazonaws.com\"\n        }\n      }\n    }\n  ]\n}\n```\n\nThe `:sub` condition is what keeps any other pod in the cluster from assuming this role. Leaving it off would let any ServiceAccount with the OIDC trust pick it up.\n\n#### Generating the trust policy\n\nRather than hand-edit the JSON, you can derive every field from the live cluster with `aws` and `kubectl`. Set the four inputs at the top, then pipe the output straight into `aws iam create-role` or save it to a file:\n\n```sh\nCLUSTER=eks-prod\nREGION=us-west-2\nNAMESPACE=pgdog\nRELEASE=pgdog-control\n\nOIDC_HOST=$(aws eks describe-cluster --name \"$CLUSTER\" --region \"$REGION\" \\\n  --query 'cluster.identity.oidc.issuer' --output text | sed 's|^https://||')\nACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)\nSA=$(kubectl -n \"$NAMESPACE\" get sa \\\n  -l app.kubernetes.io/instance=\"$RELEASE\",app.kubernetes.io/component=control \\\n  -o jsonpath='{.items[0].metadata.name}')\n\ncat \u003e trust-policy.json \u003c\u003cEOF\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Federated\": \"arn:aws:iam::${ACCOUNT_ID}:oidc-provider/${OIDC_HOST}\"\n      },\n      \"Action\": \"sts:AssumeRoleWithWebIdentity\",\n      \"Condition\": {\n        \"StringEquals\": {\n          \"${OIDC_HOST}:sub\": \"system:serviceaccount:${NAMESPACE}:${SA}\",\n          \"${OIDC_HOST}:aud\": \"sts.amazonaws.com\"\n        }\n      }\n    }\n  ]\n}\nEOF\n```\n\nThen create (or update) the role:\n\n```sh\n# First-time creation\naws iam create-role \\\n  --role-name pgdog-control \\\n  --assume-role-policy-document file://trust-policy.json\n\n# Updating an existing role's trust policy in place\naws iam update-assume-role-policy \\\n  --role-name pgdog-control \\\n  --policy-document file://trust-policy.json\n```\n\nThe `kubectl` lookup only works after `helm install` has run. The SA doesn't exist yet on a fresh cluster. If you're bootstrapping in the other order (role first, then chart), substitute the SA name manually: `SA=\"${RELEASE}-control\"`.\n\n### Permissions policy\n\nThe control plane only reads from AWS. It never creates, modifies, or deletes anything. A minimal policy covering the APIs it actually calls:\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"RDSTopology\",\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"rds:DescribeDBClusters\",\n        \"rds:DescribeDBInstances\",\n        \"rds:DescribeDBParameters\"\n      ],\n      \"Resource\": \"*\"\n    },\n    {\n      \"Sid\": \"EC2InstanceTypes\",\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"ec2:DescribeInstanceTypes\"\n      ],\n      \"Resource\": \"*\"\n    },\n    {\n      \"Sid\": \"CloudWatchMetrics\",\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"cloudwatch:GetMetricData\",\n        \"cloudwatch:GetMetricStatistics\",\n        \"cloudwatch:ListMetrics\"\n      ],\n      \"Resource\": \"*\"\n    }\n  ]\n}\n```\n\nThe control plane calls `rds:DescribeDBParameters` to display parameter-group settings for each instance, and `ec2:DescribeInstanceTypes` to look up the vCPU/memory specs of the underlying instance class (e.g. `db.r6g.xlarge` → 4 vCPU, 32 GiB). Without these two actions the RDS refresh fails with `AccessDenied` / `UnauthorizedOperation` and the database panel stays empty.\n\nIf you want to lock it down further, both `rds:Describe*` actions support resource-level ARNs and you can scope CloudWatch via the `cloudwatch:namespace` condition key set to `AWS/RDS`.\n\n### Wiring it up\n\nOnce the role exists, point the chart at it:\n\n```yaml\ncontrol:\n  aws:\n    roleArn: arn:aws:iam::111111111111:role/pgdog-control\n    region: us-east-1\n```\n\n`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 `\u003crelease\u003e-aws-creds` Secret and load it via `envFrom`. Don't do this on EKS; IRSA is strictly better.\n\n| Option | Description |\n|-|-|\n| `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 `\"\"`). |\n| `control.aws.region` | AWS region the SDK targets. Rendered as `AWS_REGION` on the container (string, default `\"\"`). |\n| `control.aws.accessKeyId` / `secretAccessKey` / `sessionToken` | Static IAM-user credentials. Only for non-EKS clusters. Don't set these alongside `roleArn`; pick one (string, default `\"\"`). |\n\n## Configuration\n\nThe control plane reads its runtime configuration from a TOML file at `/etc/pgdog-control/control.toml`. The chart materializes that file from `control.config` in `values.yaml`. Every nested key under `control.config` becomes a TOML table, and field names map straight through. Every section and every field is optional; anything you omit falls back to a hardcoded default, so a minimal install only sets the handful of values.\n\nEach subsection below covers one TOML section.\n\n### PgDog API IP Allowlist\n\n`control.config.api.pgdog.ip_allowlist` adds an optional source-IP gate in front of the PgDog machine API endpoints under `/api/v2/*`. It is disabled by default. When enabled, the control plane accepts those requests only when the direct TCP peer address falls inside one of the configured CIDR ranges:\n\n```yaml\ncontrol:\n  config:\n    api:\n      pgdog:\n        ip_allowlist:\n          enabled: true\n          allowed_cidrs:\n            - 10.0.0.0/8\n            - 172.16.0.0/12\n            - 192.168.0.0/16\n            - 127.0.0.0/8\n            - ::1/128\n            - fc00::/7\n```\n\nIf `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.\n\n| Option | Description |\n|-|-|\n| `api.pgdog.ip_allowlist.enabled` | Enables source-IP checks for `/api/v2/*` PgDog endpoints (bool, default `false`). |\n| `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). |\n\n### Authentication\n\n`control.config.auth` wires up the OAuth-backed login flow for the dashboard. GitHub and Google are supported and can be enabled side by side. At least one needs to be configured, or the dashboard will be **accessible by anyone with the URL**:\n\n```yaml\ncontrol:\n  config:\n    auth:\n      redirect_base_url: https://control.acme.com\n      cookie_secure: true\n      session_max_age_days: 30\n      github:\n        client_id: Iv1.0123456789abcdef\n        client_secret: shhh\n        allowed_orgs: [acme-corp]\n      google:\n        client_id: 0123456789-abc.apps.googleusercontent.com\n        client_secret: shhh\n        allowed_domains: [acme.com]\n```\n\n| Option | Description |\n|-|-|\n| `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). |\n| `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 `\u003crelease\u003e-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). |\n| `cookie_secure` | Set the `Secure` flag on cookies. Disable only for local HTTP testing (bool, default `true`). |\n| `session_max_age_days` | Lifetime of the signed session cookie (int, default `30`). |\n| `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`). |\n| `github.client_id` / `github.client_secret` | OAuth credentials from the GitHub App. Required to enable the GitHub login route. |\n| `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 `[]`). |\n| `google.client_id` / `google.client_secret` | OAuth credentials from the Google Cloud OAuth client. Required to enable the Google login route. |\n| `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 `[]`). |\n\n#### Sourcing OAuth credentials from a Secret\n\nInlining `client_id` / `client_secret` above writes them in plaintext into the `\u003crelease\u003e-control-config` ConfigMap. To keep the client secrets out of the ConfigMap (and out of your values), reference an existing `Secret` in the release namespace instead. The chart injects each referenced key as an environment variable (`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) via `secretKeyRef`; the control plane reads these when the corresponding field is absent from `control.toml`.\n\n```sh\nkubectl create secret generic oauth-secrets \\\n  --from-literal=github-client-secret=shhh \\\n  --from-literal=google-client-secret=shhh\n```\n\n```yaml\ncontrol:\n  config:\n    auth:\n      redirect_base_url: https://control.acme.com\n      github:\n        client_id: Iv1.0123456789abcdef   # not sensitive — fine to inline\n        allowed_orgs: [acme-corp]\n        secret:\n          name: oauth-secrets\n          clientSecretKey: github-client-secret\n      google:\n        client_id: 0123456789-abc.apps.googleusercontent.com\n        allowed_domains: [acme.com]\n        secret:\n          name: oauth-secrets\n          clientSecretKey: google-client-secret\n```\n\n| Option | Description |\n|-|-|\n| `\u003cprovider\u003e.secret.name` | Name of an existing `Secret` in the release namespace holding the credentials. Required when either key below is set (string, optional). |\n| `\u003cprovider\u003e.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). |\n| `\u003cprovider\u003e.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). |\n\nThe provider's `[auth.\u003cprovider\u003e]` 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/\u003crelease\u003e-control`.\n\n### Helm\n\nWhen the dashboard provisions a new PgDog cluster, it shells out to `helm upgrade --install` against a chart fetched from our Helm repository. `control.config.helm` controls which chart and which repository. The defaults point at the public `pgdogdev` chart on `helm.pgdog.dev`, which is what you want unless you mirror the chart internally.\n\n```yaml\ncontrol:\n  config:\n    helm:\n      chart: pgdog\n      repo: pgdogdev\n      repo_url: https://helm.pgdog.dev\n```\n\n| Option | Description |\n|-|-|\n| `chart` | Chart name within the repo. The control plane installs `{repo}/{chart}` (string, default `pgdog`). |\n| `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`). |\n| `repo_url` | Repo index URL. This is what `helm repo add \u003crepo\u003e \u003crepo_url\u003e` 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`). |\n\n### Background polling\n\nThe dashboard refreshes its view of the world by polling each backing system on a fixed cadence. Defaults are tuned for production; lower them if you want faster updates at the cost of more API calls, or raise them if you're trying to stay under a rate limit. CloudWatch and RDS settings are no-ops unless AWS credentials are configured.\n\n```yaml\ncontrol:\n  config:\n    rds:\n      refresh_interval_secs: 60\n      # Experimental: do not enable in production yet.\n      autodiscovery: false\n    kube:\n      refresh_interval_secs: 15\n    dns:\n      refresh_interval_secs: 30\n    cloudwatch:\n      refresh_interval_secs: 60\n      lookback_secs: 3600\n      period_secs: 60\n```\n\n| Option | Description |\n|-|-|\n| `rds.refresh_interval_secs` | How often to poll AWS RDS for cluster and instance topology (int, default `60`). |\n| `rds.autodiscovery` | **Experimental. Do not enable in production yet.** Automatically reconcile Helm-managed PgDog database entries from discovered RDS topology (bool, default `false`). |\n| `kube.refresh_interval_secs` | How often to poll Kubernetes for PgDog workloads. Independent of the `watch` streams, which fire on events (int, default `15`). |\n| `dns.refresh_interval_secs` | How often to re-resolve every known RDS hostname (int, default `30`). |\n| `cloudwatch.refresh_interval_secs` | How often to poll CloudWatch for per-instance metrics (int, default `60`). |\n| `cloudwatch.lookback_secs` | How far back each fetch reaches. A fresh deploy pulls the full window on its first tick (int, default `3600`). |\n| `cloudwatch.period_secs` | CloudWatch aggregation period. The smallest bucket the metric API returns (int, default `60`). |\n\n### Alerting\n\n`control.config.alerts` enables outbound alert integrations. Leave `incident_io` unset to disable incident.io. Thresholds are optional and only configured metrics create alerts.\n\n```yaml\ncontrol:\n  config:\n    alerts:\n      evaluation_window_secs: 300\n      thresholds:\n        clients_waiting: 10\n        cpu: 90.0\n        memory: 2048\n        server_connections: 100\n      incident_io:\n        api_key: inc_live_xxx\n```\n\n| Option | Description |\n|-|-|\n| `evaluation_window_secs` | How long metrics must remain at or above threshold before creating an alert (int, default `300`). |\n| `thresholds.clients_waiting` | Number of clients waiting on a server connection (int, optional). |\n| `thresholds.cpu` | CPU usage percentage. Must be between `0.0` and `100.0`, inclusive (float, optional). |\n| `thresholds.memory` | Memory used, in megabytes (int, optional). |\n| `thresholds.server_connections` | Number of open server connections (int, optional). |\n| `incident_io.api_key` | incident.io API key with permission to create incidents. Missing `incident_io` disables the integration (string, optional). |\n\n### State store\n\n`control.config.store` governs the in-memory metric store: how often it sweeps for stale data, when an instance is marked stale or evicted, and how long per-instance metric history is retained. The defaults are tight enough for an interactive dashboard; widen them if you keep the UI open against a cluster that's intentionally idle, or if you want a longer historical window in memory.\n\n```yaml\ncontrol:\n  config:\n    store:\n      tick_secs: 1\n      stale_after_secs: 5\n      evict_after_secs: 60\n      metrics_retention_secs: 300\n      query_history_limit: 1000\n      autoreload: immediately # or in_sync, or off\n```\n\n| Option | Description |\n|-|-|\n| `tick_secs` | How often the sweep task wakes up. Sets the shortest possible reaction time for stale and evict transitions (int, default `1`). |\n| `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`). |\n| `evict_after_secs` | Instance is dropped from the store entirely if its newest metric is older than this (int, default `60`). |\n| `metrics_retention_secs` | How much per-instance metric history is kept in memory. Older points are dropped as new ones arrive (int, default `300`). |\n| `query_history_limit` | Per-token historical query store capacity. Oldest deduped query entries are evicted first once the limit is reached (int, default `1000`). |\n| `autoreload` | Automatically enqueue `reload_configuration` for instances that report config drift (enum, default `off`, available options: `off`, `immediately`, `in_sync`). |\n\n### Slack Notifications\n\n`control.config.slack` enables Slack status updates for long-running deployment and maintenance work. Leave either field empty to disable Slack. If the section is omitted, the control plane falls back to the `SLACK_BOT_TOKEN` and `SLACK_CHANNEL` environment variables.\n\n```yaml\ncontrol:\n  config:\n    slack:\n      bot_token: xoxb-...\n      channel: C0123456789\n```\n\n| Option | Description |\n|-|-|\n| `bot_token` | Slack bot token with `chat:write` permission (string, optional). |\n| `channel` | Slack channel ID or name for status updates (string, optional). |\n\n### Redis persistence\n\n`control.config.redis` controls how the in-memory store is snapshotted to Redis between process restarts. The chart already provisions an in-cluster Redis (`\u003crelease\u003e-redis`) and the control plane points at it by default, so most installs leave this section alone.\n\n```yaml\ncontrol:\n  config:\n    redis:\n      url: redis://my-redis.cache:6379\n      save_interval_secs: 60\n```\n\n| Option | Description |\n|-|-|\n| `url` | Redis connection string. Leave empty to use the in-cluster Redis the chart installs; set it only to point at an external Redis (string, optional). |\n| `save_interval_secs` | How often the background task snapshots the store to Redis (int, default `60`). |\n\n## Examples\n\n```sh\nhelm install control pgdogdev-ee/pgdog-control -f values.yaml\n```\n\n### EKS with the AWS Load Balancer Controller\n\nThis example deploys into an EKS cluster that already has the AWS Load Balancer Controller, and TLS certificate set up via ACM. AWS credentials come from IRSA.\n\n```yaml\ncontrol:\n  aws:\n    # IAM role assumed by the pod via IRSA. The role's trust policy must\n    # allow system:serviceaccount:\u003crelease-ns\u003e:\u003crelease\u003e-control.\n    roleArn: arn:aws:iam::111111111111:role/pgdog-control\n    region: us-east-1\n  rbac:\n    create: true\n    # Namespaces where the control plane is allowed to manage PgDog\n    # clusters. The dashboard still sees workloads in every namespace.\n    writeNamespaces:\n      - pgdog-prod\n      - pgdog-staging\n  config:\n    auth:\n      redirect_base_url: https://control.acme.com\n      github:\n        client_id: Iv1.0123456789abcdef\n        client_secret: shhh-store-this-in-a-secret\n        allowed_orgs: [acme-corp]\n\ningress:\n  enabled: true\n  mode: aws\n  host: control.acme.com\n  aws:\n    scheme: internet-facing\n    certificateArn: arn:aws:acm:us-east-1:111111111111:certificate/abc-123-def-456\n    sslRedirect: true\n```\n\n### Generic Kubernetes with ingress-nginx and cert-manager\n\nThis example targets a generic cluster (kubeadm, on-prem, or any managed Kubernetes) with [ingress-nginx](https://kubernetes.github.io/ingress-nginx/) handling traffic and [cert-manager](https://cert-manager.io/) issuing Let's Encrypt certificates.\n\n```yaml\ncontrol:\n  rbac:\n    create: true\n    writeNamespaces:\n      - pgdog-prod\n      - pgdog-staging\n  config:\n    auth:\n      redirect_base_url: https://control.acme.com\n      google:\n        client_id: 0123456789-abc.apps.googleusercontent.com\n        client_secret: shhh-store-this-in-a-secret\n        allowed_domains: [acme.com]\n\ningress:\n  enabled: true\n  mode: nginx\n  host: control.acme.com\n  nginx:\n    tls:\n      enabled: true\n    clusterIssuer: letsencrypt-prod\n    sslRedirect: \"true\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgdogdev%2Fhelm-ee","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpgdogdev%2Fhelm-ee","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgdogdev%2Fhelm-ee/lists"}