apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
  labels:
    app: vibecarbon-grafana
    component: observability
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: vibecarbon-grafana
  template:
    metadata:
      labels:
        app: vibecarbon-grafana
        component: observability
    spec:
      securityContext:
        fsGroup: 472
        runAsUser: 472
        runAsGroup: 472
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: grafana
        image: grafana/grafana:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 3000
          name: http
        env:
        - name: GF_SECURITY_ADMIN_USER
          valueFrom:
            secretKeyRef:
              name: grafana-secrets
              key: ADMIN_EMAIL
        - name: GF_SECURITY_ADMIN_PASSWORD
          valueFrom:
            secretKeyRef:
              name: grafana-secrets
              key: ADMIN_PASSWORD
        # H-9: anonymous access OFF. Grafana is reachable only through Traefik's
        # admin-auth ForwardAuth (super_admin), which injects the verified identity.
        - name: GF_AUTH_ANONYMOUS_ENABLED
          value: 'false'
        # Auth-proxy: trust the ForwardAuth-injected X-Authenticated-User header as
        # the logged-in user so super_admins skip a second Grafana login.
        - name: GF_AUTH_PROXY_ENABLED
          value: 'true'
        - name: GF_AUTH_PROXY_HEADER_NAME
          value: X-Authenticated-User
        - name: GF_AUTH_PROXY_HEADER_PROPERTY
          value: username
        - name: GF_AUTH_PROXY_AUTO_SIGN_UP
          value: 'true'
        - name: GF_USERS_AUTO_ASSIGN_ORG_ROLE
          value: Admin
        # SECURITY (header trust boundary): only requests whose SOURCE IP is in the
        # whitelist may present X-Authenticated-User (Grafana checks the direct TCP
        # peer, not X-Forwarded-For). Set to the k3s pod CIDR as DEFENSE-IN-DEPTH —
        # pod IPs are ephemeral and Grafana's whitelist is IP-based while k8s
        # identity is label-based, so Traefik can't be named individually here. The
        # REAL control is the default-deny NetworkPolicy in this dedicated
        # vibecarbon-observability namespace (network-policy.yaml): only {traefik,
        # app} can reach grafana:3000. Requests WITHOUT the header (the app's
        # unauthenticated /api/health poll) are unaffected — the auth-proxy path is
        # skipped when the header is absent. RESIDUAL RISK: the app pod is allowed
        # to reach :3000 for that health poll, so a COMPROMISED app pod could forge
        # X-Authenticated-User → Grafana admin. Bounded (that pod already holds the
        # service-role key), and much smaller than before isolation, when EVERY
        # vibecarbon-namespace pod (n8n/metabase/storage/…) could reach :3000.
        # Closing even the app-pod vector needs the app's Grafana liveness check
        # moved off :3000 (documented follow-up).
        - name: GF_AUTH_PROXY_WHITELIST
          value: 10.42.0.0/16
        # H-9: scoped config/secret local to vibecarbon-observability. Grafana gets
        # ONLY the three keys it needs (SITE_URL here; ADMIN_EMAIL/ADMIN_PASSWORD
        # above) — NOT the whole vibecarbon-secrets bundle (no DB / service-role
        # creds cross into the observability namespace). Both are provisioned into
        # this namespace at deploy time (applyObservabilitySecrets in k3s.js).
        - name: SITE_URL
          valueFrom:
            configMapKeyRef:
              name: grafana-config
              key: SITE_URL
        - name: GF_SERVER_ROOT_URL
          value: $(SITE_URL)/admin/grafana/
        - name: GF_SERVER_SERVE_FROM_SUB_PATH
          value: 'true'
        volumeMounts:
        - name: data
          mountPath: /var/lib/grafana
        - name: datasources
          mountPath: /etc/grafana/provisioning/datasources
          readOnly: true
        - name: dashboards-provisioning
          mountPath: /etc/grafana/provisioning/dashboards
          readOnly: true
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /api/health
            port: http
          initialDelaySeconds: 15
          periodSeconds: 20
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /api/health
            port: http
          initialDelaySeconds: 5
          periodSeconds: 10
          timeoutSeconds: 3
          failureThreshold: 3
        securityContext:
          allowPrivilegeEscalation: false
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: grafana-pvc
      - name: datasources
        configMap:
          name: grafana-provisioning
          items:
          - key: datasources.yml
            path: datasources.yml
      - name: dashboards-provisioning
        configMap:
          name: grafana-provisioning
          items:
          - key: dashboards.yml
            path: dashboards.yml
