nandi/atop-analyzepublic Fork 0
1126d97
Commits
Clone
git clone https://git.rickub.com/nandi/atop-analyze.git
git clone ssh://git@rickub.com/nandi/atop-analyze.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Document automated Grafana secret creation

nandithebull committed 2026-09-21T19:54:32-07:00 Browse files
1126d97 parent: c8c2d90
modified README.md +5 -0
@@ -173,6 +173,11 @@ modal secret create atop-grafana \
173173 modal deploy modal_grafana.py
174174 ```
175175
176+Or run `python create_grafana_secret.py` to fetch those four public endpoint
177+values from Pocket ID's discovery document and securely prompt for the client
178+credentials and Modal token. It creates the same `atop-grafana` secret without
179+printing the private values.
180+
176181 Grafana redirects visitors to Pocket ID and does not expose a local password
177182 form. It uses PKCE, validates the signed ID token with Pocket ID’s JWKS, and
178183 requests a refresh token so an active Grafana session does not end at access
@@ -173,6 +173,11 @@ modal secret create atop-grafana \
173 modal deploy modal_grafana.py173 modal deploy modal_grafana.py
174 ```174 ```
175 175
176+Or run `python create_grafana_secret.py` to fetch those four public endpoint
177+values from Pocket ID's discovery document and securely prompt for the client
178+credentials and Modal token. It creates the same `atop-grafana` secret without
179+printing the private values.
180+
176 Grafana redirects visitors to Pocket ID and does not expose a local password181 Grafana redirects visitors to Pocket ID and does not expose a local password
177 form. It uses PKCE, validates the signed ID token with Pocket ID’s JWKS, and182 form. It uses PKCE, validates the signed ID token with Pocket ID’s JWKS, and
178 requests a refresh token so an active Grafana session does not end at access183 requests a refresh token so an active Grafana session does not end at access
added create_grafana_secret.py +111 -0
new file mode 100644
@@ -0,0 +1,111 @@
1+#!/usr/bin/env python3
2+"""Create the Modal secret used by Grafana's Pocket ID login.
3+
4+Pocket ID exposes public OIDC endpoint metadata through its discovery document,
5+but intentionally never exposes an OAuth client secret. This helper fetches the
6+public metadata and prompts locally for the credentials that must remain private.
7+"""
8+
9+import argparse
10+import getpass
11+import json
12+import os
13+import subprocess
14+import sys
15+import tempfile
16+from pathlib import Path
17+from urllib.parse import urlparse
18+from urllib.request import urlopen
19+
20+
21+DEFAULT_POCKET_ID_URL = "https://codegod100--pocket-id-serve.modal.run"
22+DEFAULT_GRAFANA_URL = "https://codegod100--atop-grafana-grafana.modal.run"
23+DEFAULT_CLICKHOUSE_HOST = "codegod100--atop-clickhouse-clickhouse.modal.run"
24+REQUIRED_DISCOVERY_FIELDS = {
25+ "authorization_endpoint": "GF_AUTH_GENERIC_OAUTH_AUTH_URL",
26+ "token_endpoint": "GF_AUTH_GENERIC_OAUTH_TOKEN_URL",
27+ "userinfo_endpoint": "GF_AUTH_GENERIC_OAUTH_API_URL",
28+ "jwks_uri": "GF_AUTH_GENERIC_OAUTH_JWK_SET_URL",
29+}
30+
31+
32+def https_url(value: str, name: str) -> str:
33+ parsed = urlparse(value)
34+ if parsed.scheme != "https" or not parsed.netloc:
35+ raise ValueError(f"{name} must be an HTTPS URL")
36+ return value.rstrip("/")
37+
38+
39+def get_value(prompt: str, environment_name: str, *, secret: bool = False) -> str:
40+ value = os.environ.get(environment_name)
41+ if not value:
42+ reader = getpass.getpass if secret else input
43+ value = reader(f"{prompt}: ").strip()
44+ if not value:
45+ raise ValueError(f"{prompt} is required")
46+ return value
47+
48+
49+def fetch_discovery(pocket_id_url: str) -> dict[str, str]:
50+ discovery_url = f"{pocket_id_url}/.well-known/openid-configuration"
51+ with urlopen(discovery_url, timeout=15) as response: # noqa: S310: URL is HTTPS-validated above.
52+ document = json.load(response)
53+ missing = set(REQUIRED_DISCOVERY_FIELDS) - document.keys()
54+ if missing:
55+ raise ValueError(f"Pocket ID discovery document is missing: {', '.join(sorted(missing))}")
56+ return document
57+
58+
59+def main() -> int:
60+ parser = argparse.ArgumentParser(description=__doc__)
61+ parser.add_argument("--pocket-id-url", default=DEFAULT_POCKET_ID_URL)
62+ parser.add_argument("--grafana-url", default=DEFAULT_GRAFANA_URL)
63+ parser.add_argument("--clickhouse-host", default=DEFAULT_CLICKHOUSE_HOST)
64+ parser.add_argument("--secret-name", default="atop-grafana")
65+ parser.add_argument("--force", action="store_true", help="replace an existing Modal secret")
66+ parser.add_argument("--yes", action="store_true", help="skip the final confirmation")
67+ args = parser.parse_args()
68+
69+ pocket_id_url = https_url(args.pocket_id_url, "Pocket ID URL")
70+ grafana_url = https_url(args.grafana_url, "Grafana URL")
71+ discovery = fetch_discovery(pocket_id_url)
72+ values = {
73+ "CLICKHOUSE_HOST": args.clickhouse_host,
74+ "MODAL_KEY": get_value("Modal token key (wk-...)", "MODAL_KEY", secret=True),
75+ "MODAL_SECRET": get_value("Modal token secret (ws-...)", "MODAL_SECRET", secret=True),
76+ "GF_SERVER_ROOT_URL": grafana_url,
77+ "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": get_value("Pocket ID Grafana client ID", "GF_AUTH_GENERIC_OAUTH_CLIENT_ID"),
78+ "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": get_value(
79+ "Pocket ID Grafana client secret", "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET", secret=True
80+ ),
81+ }
82+ values.update({environment_name: discovery[field] for field, environment_name in REQUIRED_DISCOVERY_FIELDS.items()})
83+
84+ print(f"Creating Modal secret {args.secret_name!r} for {grafana_url}.")
85+ print(f"Pocket ID discovery: {pocket_id_url}/.well-known/openid-configuration")
86+ if not args.yes and input("Continue? [y/N] ").strip().lower() not in {"y", "yes"}:
87+ print("Cancelled.")
88+ return 0
89+
90+ temporary_path: Path | None = None
91+ try:
92+ descriptor, filename = tempfile.mkstemp(prefix="atop-grafana-", suffix=".json")
93+ temporary_path = Path(filename)
94+ with os.fdopen(descriptor, "w", encoding="utf-8") as temporary_file:
95+ json.dump(values, temporary_file)
96+ command = ["modal", "secret", "create", args.secret_name, "--from-json", str(temporary_path)]
97+ if args.force:
98+ command.append("--force")
99+ subprocess.run(command, check=True)
100+ finally:
101+ if temporary_path:
102+ temporary_path.unlink(missing_ok=True)
103+ return 0
104+
105+
106+if __name__ == "__main__":
107+ try:
108+ raise SystemExit(main())
109+ except (OSError, ValueError, subprocess.CalledProcessError) as error:
110+ print(f"Error: {error}", file=sys.stderr)
111+ raise SystemExit(1)
new file mode 100644
@@ -0,0 +1,111 @@
1+#!/usr/bin/env python3
2+"""Create the Modal secret used by Grafana's Pocket ID login.
3+
4+Pocket ID exposes public OIDC endpoint metadata through its discovery document,
5+but intentionally never exposes an OAuth client secret. This helper fetches the
6+public metadata and prompts locally for the credentials that must remain private.
7+"""
8+
9+import argparse
10+import getpass
11+import json
12+import os
13+import subprocess
14+import sys
15+import tempfile
16+from pathlib import Path
17+from urllib.parse import urlparse
18+from urllib.request import urlopen
19+
20+
21+DEFAULT_POCKET_ID_URL = "https://codegod100--pocket-id-serve.modal.run"
22+DEFAULT_GRAFANA_URL = "https://codegod100--atop-grafana-grafana.modal.run"
23+DEFAULT_CLICKHOUSE_HOST = "codegod100--atop-clickhouse-clickhouse.modal.run"
24+REQUIRED_DISCOVERY_FIELDS = {
25+ "authorization_endpoint": "GF_AUTH_GENERIC_OAUTH_AUTH_URL",
26+ "token_endpoint": "GF_AUTH_GENERIC_OAUTH_TOKEN_URL",
27+ "userinfo_endpoint": "GF_AUTH_GENERIC_OAUTH_API_URL",
28+ "jwks_uri": "GF_AUTH_GENERIC_OAUTH_JWK_SET_URL",
29+}
30+
31+
32+def https_url(value: str, name: str) -> str:
33+ parsed = urlparse(value)
34+ if parsed.scheme != "https" or not parsed.netloc:
35+ raise ValueError(f"{name} must be an HTTPS URL")
36+ return value.rstrip("/")
37+
38+
39+def get_value(prompt: str, environment_name: str, *, secret: bool = False) -> str:
40+ value = os.environ.get(environment_name)
41+ if not value:
42+ reader = getpass.getpass if secret else input
43+ value = reader(f"{prompt}: ").strip()
44+ if not value:
45+ raise ValueError(f"{prompt} is required")
46+ return value
47+
48+
49+def fetch_discovery(pocket_id_url: str) -> dict[str, str]:
50+ discovery_url = f"{pocket_id_url}/.well-known/openid-configuration"
51+ with urlopen(discovery_url, timeout=15) as response: # noqa: S310: URL is HTTPS-validated above.
52+ document = json.load(response)
53+ missing = set(REQUIRED_DISCOVERY_FIELDS) - document.keys()
54+ if missing:
55+ raise ValueError(f"Pocket ID discovery document is missing: {', '.join(sorted(missing))}")
56+ return document
57+
58+
59+def main() -> int:
60+ parser = argparse.ArgumentParser(description=__doc__)
61+ parser.add_argument("--pocket-id-url", default=DEFAULT_POCKET_ID_URL)
62+ parser.add_argument("--grafana-url", default=DEFAULT_GRAFANA_URL)
63+ parser.add_argument("--clickhouse-host", default=DEFAULT_CLICKHOUSE_HOST)
64+ parser.add_argument("--secret-name", default="atop-grafana")
65+ parser.add_argument("--force", action="store_true", help="replace an existing Modal secret")
66+ parser.add_argument("--yes", action="store_true", help="skip the final confirmation")
67+ args = parser.parse_args()
68+
69+ pocket_id_url = https_url(args.pocket_id_url, "Pocket ID URL")
70+ grafana_url = https_url(args.grafana_url, "Grafana URL")
71+ discovery = fetch_discovery(pocket_id_url)
72+ values = {
73+ "CLICKHOUSE_HOST": args.clickhouse_host,
74+ "MODAL_KEY": get_value("Modal token key (wk-...)", "MODAL_KEY", secret=True),
75+ "MODAL_SECRET": get_value("Modal token secret (ws-...)", "MODAL_SECRET", secret=True),
76+ "GF_SERVER_ROOT_URL": grafana_url,
77+ "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": get_value("Pocket ID Grafana client ID", "GF_AUTH_GENERIC_OAUTH_CLIENT_ID"),
78+ "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": get_value(
79+ "Pocket ID Grafana client secret", "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET", secret=True
80+ ),
81+ }
82+ values.update({environment_name: discovery[field] for field, environment_name in REQUIRED_DISCOVERY_FIELDS.items()})
83+
84+ print(f"Creating Modal secret {args.secret_name!r} for {grafana_url}.")
85+ print(f"Pocket ID discovery: {pocket_id_url}/.well-known/openid-configuration")
86+ if not args.yes and input("Continue? [y/N] ").strip().lower() not in {"y", "yes"}:
87+ print("Cancelled.")
88+ return 0
89+
90+ temporary_path: Path | None = None
91+ try:
92+ descriptor, filename = tempfile.mkstemp(prefix="atop-grafana-", suffix=".json")
93+ temporary_path = Path(filename)
94+ with os.fdopen(descriptor, "w", encoding="utf-8") as temporary_file:
95+ json.dump(values, temporary_file)
96+ command = ["modal", "secret", "create", args.secret_name, "--from-json", str(temporary_path)]
97+ if args.force:
98+ command.append("--force")
99+ subprocess.run(command, check=True)
100+ finally:
101+ if temporary_path:
102+ temporary_path.unlink(missing_ok=True)
103+ return 0
104+
105+
106+if __name__ == "__main__":
107+ try:
108+ raise SystemExit(main())
109+ except (OSError, ValueError, subprocess.CalledProcessError) as error:
110+ print(f"Error: {error}", file=sys.stderr)
111+ raise SystemExit(1)