--- # CronJob to sync TeslaMate database password from CNPG secret to Grafana datasource secret # This ensures Grafana always has the current password even when CNPG rotates it apiVersion: batch/v1 kind: CronJob metadata: name: sync-teslamate-password namespace: grafana spec: # Run every 30 minutes to catch password rotations (rotations happen at most daily) schedule: "*/30 * * * *" successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 concurrencyPolicy: Forbid jobTemplate: spec: template: metadata: labels: app: teslamate-password-sync spec: serviceAccountName: teslamate-password-sync restartPolicy: OnFailure containers: - name: sync image: docker.io/alpine/k8s:1.32.13 securityContext: runAsUser: 10000 runAsGroup: 10000 runAsNonRoot: true allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] seccompProfile: type: RuntimeDefault volumeMounts: - name: tmp mountPath: /tmp env: - name: GRAFANA_ADMIN_PASSWORD valueFrom: secretKeyRef: name: grafana-admin-secret key: admin-password optional: true command: - /bin/bash - -c - | set -e echo "Fetching current password from CNPG secret in teslamate namespace..." CURRENT_PASSWORD=$(kubectl get secret -n teslamate pg-teslamate-app -o jsonpath='{.data.password}' | base64 -d) echo "Fetching current password from Grafana datasource secret..." GRAFANA_PASSWORD=$(kubectl get secret -n grafana grafana-teslamate-datasource -o jsonpath='{.data.TESLAMATE_DB_PASSWORD}' | base64 -d) if [ "$CURRENT_PASSWORD" != "$GRAFANA_PASSWORD" ]; then echo "Passwords differ - updating Grafana secret..." kubectl create secret generic grafana-teslamate-datasource \ --from-literal=TESLAMATE_DB_PASSWORD="$CURRENT_PASSWORD" \ -n grafana \ --dry-run=client -o yaml | kubectl apply -f - echo "Password synced to secret" else echo "Passwords match in secrets" fi echo "Finding running Grafana pod..." GRAFANA_POD=$(kubectl get pod -n grafana -l app.kubernetes.io/name=grafana --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') if [ -z "$GRAFANA_POD" ]; then echo "No running Grafana pod found, skipping API update" exit 0 fi echo "Using Grafana pod: $GRAFANA_POD" echo "Updating Grafana datasource password via API (ensures password works after Grafana restart)..." # Get admin password from Grafana secret (fallback to 'admin' if not found) ADMIN_PASS=$(kubectl get secret -n grafana grafana -o jsonpath='{.data.admin-password}' 2>/dev/null | base64 -d || echo "admin") # Update datasource via API with current password RESULT=$(kubectl exec -n grafana "$GRAFANA_POD" -c grafana -- curl -s -X PUT \ -H "Content-Type: application/json" \ -u "admin:$ADMIN_PASS" \ http://localhost:3000/api/datasources/uid/TeslaMate \ -d "{ \"name\": \"TeslaMate\", \"type\": \"grafana-postgresql-datasource\", \"uid\": \"TeslaMate\", \"url\": \"pg-teslamate-rw.teslamate.svc.cluster.local:5432\", \"database\": \"teslamate\", \"user\": \"teslamate\", \"access\": \"proxy\", \"isDefault\": false, \"secureJsonData\": { \"password\": \"$CURRENT_PASSWORD\" }, \"jsonData\": { \"sslmode\": \"disable\", \"postgresVersion\": 1600, \"timescaledb\": false, \"database\": \"teslamate\" } }") echo "API Response: $RESULT" # Test datasource connection echo "Testing datasource connection..." TEST_RESULT=$(kubectl exec -n grafana "$GRAFANA_POD" -c grafana -- curl -s -X POST \ -u "admin:$ADMIN_PASS" \ http://localhost:3000/api/datasources/uid/TeslaMate/health) echo "Health Check: $TEST_RESULT" if echo "$TEST_RESULT" | grep -q '"status":"OK"'; then echo "✅ Datasource password updated and verified successfully!" else echo "⚠️ Datasource updated but connection test failed" exit 1 fi echo "Done!" volumes: - name: tmp emptyDir: {} --- # ServiceAccount for the CronJob apiVersion: v1 kind: ServiceAccount metadata: name: teslamate-password-sync namespace: grafana --- # Role with permissions to read CNPG secret and update Grafana secret apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: teslamate-password-sync namespace: grafana rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "create", "patch", "update"] - apiGroups: [""] resources: ["pods", "pods/exec"] verbs: ["get", "list", "watch", "create"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "patch"] --- # Role to read secret from teslamate namespace apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: teslamate-password-sync namespace: teslamate rules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["pg-teslamate-app"] verbs: ["get"] --- # RoleBinding in grafana namespace apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: teslamate-password-sync namespace: grafana subjects: - kind: ServiceAccount name: teslamate-password-sync namespace: grafana roleRef: kind: Role name: teslamate-password-sync apiGroup: rbac.authorization.k8s.io --- # RoleBinding in teslamate namespace apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: teslamate-password-sync-grafana namespace: teslamate subjects: - kind: ServiceAccount name: teslamate-password-sync namespace: grafana roleRef: kind: Role name: teslamate-password-sync apiGroup: rbac.authorization.k8s.io