# services/monitoring/oneoffs/grafana-user-dedupe-job.yaml # One-off job for monitoring/grafana-user-dedupe-api-v7. # Purpose: grafana user dedupe api v7 (see container args/env in this file). # Run by setting spec.suspend to false, reconcile, then set it back to true. # Safe to delete the finished Job/pod; it should not run continuously. apiVersion: batch/v1 kind: Job metadata: name: grafana-user-dedupe-api-v7 namespace: monitoring spec: suspend: true backoffLimit: 1 template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/role: "monitoring" vault.hashicorp.com/agent-inject-secret-grafana-env.sh: "kv/data/atlas/monitoring/grafana-admin" vault.hashicorp.com/agent-inject-template-grafana-env.sh: | {{ with secret "kv/data/atlas/monitoring/grafana-admin" }} export GRAFANA_USER="{{ index .Data.data "admin-user" }}" export GRAFANA_PASSWORD="{{ index .Data.data "admin-password" }}" {{ end }} spec: serviceAccountName: monitoring-vault-sync automountServiceAccountToken: true restartPolicy: Never affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role.kubernetes.io/worker operator: Exists preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: kubernetes.io/arch operator: In values: ["arm64"] containers: - name: dedupe image: python:3.12-slim command: - /bin/sh - -c args: - | set -euo pipefail for _ in $(seq 1 30); do if [ -f /vault/secrets/grafana-env.sh ]; then break fi sleep 1 done if [ ! -f /vault/secrets/grafana-env.sh ]; then echo "Vault secret not available" exit 1 fi . /vault/secrets/grafana-env.sh grafana_url="${GRAFANA_URL}" if [ -z "${grafana_url}" ]; then echo "GRAFANA_URL is required" exit 1 fi if [ -z "${GRAFANA_USER}" ] || [ -z "${GRAFANA_PASSWORD}" ]; then echo "Grafana admin credentials missing" exit 1 fi if [ -z "${GRAFANA_DEDUPE_EMAILS}" ]; then echo "GRAFANA_DEDUPE_EMAILS is required" exit 1 fi python - <<'PY' import base64 import json import os import urllib.parse import urllib.error import urllib.request grafana_url = os.environ["GRAFANA_URL"].rstrip("/") user = os.environ["GRAFANA_USER"] password = os.environ["GRAFANA_PASSWORD"] lookups = [e.strip() for e in os.environ["GRAFANA_DEDUPE_EMAILS"].split(",") if e.strip()] token = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("utf-8") headers = {"Authorization": f"Basic {token}"} def request(method: str, url: str): req = urllib.request.Request(url, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=10) as resp: return resp.status, resp.read() except urllib.error.HTTPError as err: body = err.read() return err.code, body for _ in range(60): status, _ = request("GET", f"{grafana_url}/api/health") if status == 200: break else: raise SystemExit("Grafana API did not become ready in time") for lookup in lookups: search_url = f"{grafana_url}/api/users/search?query={urllib.parse.quote(lookup)}" status, body = request("GET", search_url) if status != 200: print(f"search failed for {lookup}: status={status} body={body.decode('utf-8', errors='ignore')}") continue payload = json.loads(body) users = payload.get("users", []) matches = [ user for user in users if user.get("email", "").lower() == lookup.lower() or user.get("login", "").lower() == lookup.lower() ] if not matches: print(f"no grafana user found for {lookup}") continue for user in matches: user_id = user.get("id") if not user_id: continue print(f"deleting grafana user {user_id} ({user.get('email')})") delete_url = f"{grafana_url}/api/admin/users/{user_id}" del_status, del_body = request("DELETE", delete_url) if del_status not in (200, 202, 204): print( "delete failed for", user_id, "status", del_status, "body", del_body.decode("utf-8", errors="ignore"), ) PY echo "done" env: - name: GRAFANA_URL value: http://grafana.monitoring.svc.cluster.local - name: GRAFANA_DEDUPE_EMAILS value: brad.stein@gmail.com,brad@bstein.dev