From 424eecd274a055a350d8b41154d854bb4285efc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 27 Mar 2026 19:54:33 +0100 Subject: [PATCH] Add PostgreSQL datasource to Grafana with read-only user - Create grafana_reader role with SELECT-only access on all schemas - Init script runs on postgres first boot (docker-entrypoint-initdb.d) - Grafana PostgreSQL datasource provisioned with read-only credentials - Enables SQL queries in dashboards (user count, routes, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/cd-infra.yml | 7 ++++ infrastructure/docker-compose.yml | 2 ++ .../provisioning/datasources/datasources.yml | 15 +++++++++ infrastructure/postgres/init-grafana-user.sql | 32 +++++++++++++++++++ infrastructure/secrets.infra.env | 15 +++++---- 5 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 infrastructure/postgres/init-grafana-user.sql diff --git a/.github/workflows/cd-infra.yml b/.github/workflows/cd-infra.yml index 021fdee..dc6ffc2 100644 --- a/.github/workflows/cd-infra.yml +++ b/.github/workflows/cd-infra.yml @@ -68,6 +68,13 @@ jobs: GHCR_TOKEN=$(grep DEPLOY_GHCR_TOKEN .env | cut -d= -f2-) echo "$GHCR_TOKEN" | docker login ghcr.io -u stigi --password-stdin 2>/dev/null || true + # Setup Grafana read-only DB user (idempotent) + docker compose exec -T postgres psql -U trails -d trails -f /docker-entrypoint-initdb.d/init-grafana-user.sql 2>/dev/null || true + GRAFANA_DB_PW=$(grep GRAFANA_DB_PASSWORD .env | cut -d= -f2-) + if [ -n "$GRAFANA_DB_PW" ]; then + docker compose exec -T postgres psql -U trails -d trails -c "ALTER ROLE grafana_reader PASSWORD '$GRAFANA_DB_PW'" 2>/dev/null || true + fi + # Full restart: gh workflow run cd-infra.yml -f restart_all=true if [ "${{ github.event.inputs.restart_all }}" = "true" ]; then docker compose --env-file .env up -d --remove-orphans diff --git a/infrastructure/docker-compose.yml b/infrastructure/docker-compose.yml index 8955e99..e537029 100644 --- a/infrastructure/docker-compose.yml +++ b/infrastructure/docker-compose.yml @@ -78,6 +78,7 @@ services: POSTGRES_DB: trails volumes: - pgdata:/var/lib/postgresql/data + - ./postgres/init-grafana-user.sql:/docker-entrypoint-initdb.d/init-grafana-user.sql:ro command: - "postgres" - "-c" @@ -154,6 +155,7 @@ services: GF_AUTH_GITHUB_ALLOWED_ORGANIZATIONS: trails-cool GF_AUTH_GITHUB_SCOPES: user:email,read:org GF_AUTH_DISABLE_LOGIN_FORM: "true" + GRAFANA_DB_PASSWORD: ${GRAFANA_DB_PASSWORD:-} volumes: - ./grafana/provisioning:/etc/grafana/provisioning:ro - ./grafana/dashboards:/var/lib/grafana/dashboards:ro diff --git a/infrastructure/grafana/provisioning/datasources/datasources.yml b/infrastructure/grafana/provisioning/datasources/datasources.yml index 522d4fc..d78a2cc 100644 --- a/infrastructure/grafana/provisioning/datasources/datasources.yml +++ b/infrastructure/grafana/provisioning/datasources/datasources.yml @@ -15,3 +15,18 @@ datasources: access: proxy url: http://loki:3100 editable: false + + - name: PostgreSQL + uid: postgres + type: postgres + access: proxy + url: postgres:5432 + user: grafana_reader + editable: false + jsonData: + database: trails + sslmode: disable + maxOpenConns: 2 + maxIdleConns: 1 + secureJsonData: + password: $__env{GRAFANA_DB_PASSWORD} diff --git a/infrastructure/postgres/init-grafana-user.sql b/infrastructure/postgres/init-grafana-user.sql new file mode 100644 index 0000000..099f5dc --- /dev/null +++ b/infrastructure/postgres/init-grafana-user.sql @@ -0,0 +1,32 @@ +-- Read-only user for Grafana dashboards +-- Password is set via: ALTER ROLE grafana_reader PASSWORD '' +-- Run by cd-infra deploy script after container start + +DO $$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'grafana_reader') THEN + CREATE ROLE grafana_reader WITH LOGIN; + END IF; +END +$$; + +GRANT CONNECT ON DATABASE trails TO grafana_reader; +GRANT USAGE ON SCHEMA public TO grafana_reader; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO grafana_reader; + +-- Also grant on planner/journal schemas if they exist +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'planner') THEN + GRANT USAGE ON SCHEMA planner TO grafana_reader; + GRANT SELECT ON ALL TABLES IN SCHEMA planner TO grafana_reader; + ALTER DEFAULT PRIVILEGES IN SCHEMA planner GRANT SELECT ON TABLES TO grafana_reader; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'journal') THEN + GRANT USAGE ON SCHEMA journal TO grafana_reader; + GRANT SELECT ON ALL TABLES IN SCHEMA journal TO grafana_reader; + ALTER DEFAULT PRIVILEGES IN SCHEMA journal GRANT SELECT ON TABLES TO grafana_reader; + END IF; +END +$$; diff --git a/infrastructure/secrets.infra.env b/infrastructure/secrets.infra.env index e41e191..bda2951 100644 --- a/infrastructure/secrets.infra.env +++ b/infrastructure/secrets.infra.env @@ -1,10 +1,11 @@ -#ENC[AES256_GCM,data:IbWmlFr0kUJNYnRkD9Rt6tPAgtE+8TrECPY3MstykO8EPIZOvkg0T1hjGsDLca5NA8cdWqWWEmSF4o7ewvk=,iv:UD1YxHEH9PxnSrkVI/Nm9WVazGk9Kh+B8uzfNqjmf3I=,tag:+mWHdGvo+OeQFa4Ak/S3Pg==,type:comment] -#ENC[AES256_GCM,data:BDOEQHz+QsQcQCzrD8XWcxREuNWELctcjsZahTmwEAYolct7EaW4G9bGI1K8BL8V5OejHvqtxgd34LHBAzTrTL66ATW/pWGY2lF/G69J4Q==,iv:4RyNDeZUZOgibkI7+aIyCJDxd/7jZMMaBIepQCbbHFA=,tag:v/mPe5ILLVS2bLUKFpOoJg==,type:comment] -GF_AUTH_GITHUB_CLIENT_ID=ENC[AES256_GCM,data:4a1+g2U6a+XlfwSLLoJFWruvOLg=,iv:h1yDpE21XPxqOGBsE+E1Xunr0eAC2ALLP9x11XMPd6Q=,tag:zKWMUrCi1b2Ir8g1BcsAcA==,type:str] -GF_AUTH_GITHUB_CLIENT_SECRET=ENC[AES256_GCM,data:LGbZHhQeLcS01UJXtZefsGUi0P8lscZRrho0HGq+hKRuoy5RO1HssA==,iv:NATDvm3fKe4zjJ36PWb/iJLVA2lJsognRLg0TZY5yw4=,tag:JYatVnyvrc0dYue2aTh55Q==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UiszMUJMYzFlWE5Qc3Zk\nUDF2V1BIYmIwbjVydHI5OTJoRVJXdFhDZDEwCmlEQTFKRUZwYVlQbVRJMEpHL2Vj\nd21pS3BwZHc3QklCMVl4ZHFzZUJYRWMKLS0tIEtQeFQvNGxRUGhqNnBzQ0ZvaXRY\nMFZjQlhobXBKT08yc0dLVGNwd0pHbmcKEAS/YRu2NhGCZLX+nkVjo7PEu7ZV4iCW\nyO9GIsaIQcN6FWmp5IkjdeM6jkHxSk+qHCL5bKx78EgPMgzKobjgzQ==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:0mZOnEJyXdCzF0HYzuVFkOmGOpfsTzfHbrwmCwU7NyeaDKFpoTvH1fQW/XCcflGaWPRrKRyCEYOa4sdPnag=,iv:iiEYiIqvmvOFQTEDPFeu7fkluWf0z/v+tvDmY7L0cFE=,tag:Ds/EHF6XSGfnv8ZCa5OU6w==,type:comment] +#ENC[AES256_GCM,data:+au0EOdh5q7IY7qcP4Jko5dJz4AdDCF8ilBqKUCHb9hB8xlM2FHiyLrcf3Q0eYbAyg28xaTm+wewKDivvuoOv4grGohqezJ4Ypffm1Yg6g==,iv:zXrneKQUu+bL5oRFmz5nVdUcUolH0HDFuOSK460rmpc=,tag:WDybda6Q9WBEp+SuzfKQqw==,type:comment] +GF_AUTH_GITHUB_CLIENT_ID=ENC[AES256_GCM,data:mik8yBpOAkJXHQ8vEeMZV2Y+KY4=,iv:DNZLDWxnichM9E4fitJSQdf0lxNsneYJxleIs6CPJgs=,tag:aXCt7Ff5PggBWaXvRYv9tw==,type:str] +GF_AUTH_GITHUB_CLIENT_SECRET=ENC[AES256_GCM,data:jcGhMeBwOLZotvfExZWqyMtql1C7AC0hlus7Vl/RLvEjshPmNtO9yg==,iv:20HLImbrFMkWLQ+chInq0Ig9j2Q4NSLXFN+72jXxM/M=,tag:INKt4Icg9QI1PTD+KEfCHg==,type:str] +GRAFANA_DB_PASSWORD=ENC[AES256_GCM,data:o1G5rDo1G4RiOGBbd7KDcqEIKG7UuwTlVx7BI7eVHWI=,iv:Fd1wA+4uB7Ypix8n4aTGKJo6V+uiCkr4yZ3wkGZ5rQA=,tag:c0qpH/v7lgmU5yXZo7n+RA==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBbnY1VndkeE9hYnByK3RU\nRDFUYU5DcWhCRXYrdStoallzU0ZnWkJJeEY4CnBUYUlIK2ZhcUV2aWhqSDFSYnhD\nb1hkOGdkSGFyajRXNVg5a1FZcE9mQzQKLS0tIEVKZThiV1RMdkhhaWlvM05vY1g4\nYTNYUkFaYUJzbVUvMi83eHg1NXhXcGMKShBIdPKho4WmWiykqFXdmQ/NRAKoUpUw\ntUQKcGlrhHRhpgaFTyeKj+UVD1y52cXWZX9xhFdJ7TFxEO1opNc9LQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1vukt8py0s2mm47fx6qh6vykckfkdqslf9w68ekytfzvc8xp7e3rqn6p64n -sops_lastmodified=2026-03-27T15:40:55Z -sops_mac=ENC[AES256_GCM,data:pkYMdGTyjc6/ig/hdBD4GGeyiOkJBp/z9+w3vlT/bfHeje4FBZxjdmgykfCmNFJqov1MoysEsP0z8VZpdcSa+cjcJXVIhXcSpl4Uo/NXZuYRIuPkQztSGna+tStrpus4Zxk2SLe8IF5AFgPtuCwZvS7R7JE4KKbkKG/kfcTMrR8=,iv:SXzctdK5QEV6RZuSSxDor7MYPgEJGvSih4l3G2+lwYI=,tag:umAgJ94DM7DKVPPniCdRvw==,type:str] +sops_lastmodified=2026-03-27T19:07:05Z +sops_mac=ENC[AES256_GCM,data:hKi5AD4+FFL5/bDHnBfNvWipsUA0GOLVF0/l5o2I6bh5aD4l/ZJbL1hOdxq/2MO7eJPjej5AFqREEDbMKHwY7s9nvKJhNAjtg8ejRK/y1x8r8Skm790cjcT04hIbpIgc2yaM131eyOuz39q9HODAmCoLqrVBpkStEQYhId5gQcY=,iv:fFSYntpfurMfD5vzRgaVONMZe3NyxuJPw0w2lqqCyQs=,tag:8gPfB9sJpwUaG8cc43kesQ==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.9.4