Files
Homelabv4/infrastructure/grafana/manifests/teslamate-password-sync-cronjob.yaml
T
2026-03-09 20:21:35 -05:00

196 lines
6.8 KiB
YAML

---
# 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