From a5e53a1e0bc1712f2d4e5ddb11c44d34308c6be5 Mon Sep 17 00:00:00 2001 From: OmkarDeshpande7 Date: Thu, 6 Aug 2026 22:18:45 +0530 Subject: [PATCH 1/6] feat(monitoring): add Loki manifests for pod log storage Co-Authored-By: Claude Sonnet 4.6 --- .../manifests/loki-configMap.yaml | 63 +++++++++++++++ k8s/kube-prometheus/manifests/loki-pvc.yaml | 16 ++++ .../manifests/loki-service.yaml | 20 +++++ .../manifests/loki-serviceAccount.yaml | 10 +++ .../manifests/loki-statefulset.yaml | 79 +++++++++++++++++++ 5 files changed, 188 insertions(+) create mode 100644 k8s/kube-prometheus/manifests/loki-configMap.yaml create mode 100644 k8s/kube-prometheus/manifests/loki-pvc.yaml create mode 100644 k8s/kube-prometheus/manifests/loki-service.yaml create mode 100644 k8s/kube-prometheus/manifests/loki-serviceAccount.yaml create mode 100644 k8s/kube-prometheus/manifests/loki-statefulset.yaml diff --git a/k8s/kube-prometheus/manifests/loki-configMap.yaml b/k8s/kube-prometheus/manifests/loki-configMap.yaml new file mode 100644 index 000000000..99da3b36e --- /dev/null +++ b/k8s/kube-prometheus/manifests/loki-configMap.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki-config + namespace: monitoring +data: + config.yaml: | + auth_enabled: false + + server: + http_listen_port: 3100 + grpc_listen_port: 9095 + log_level: warn + + common: + instance_addr: 127.0.0.1 + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + + query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + + schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + + limits_config: + retention_period: 168h + allow_structured_metadata: false + + compactor: + working_directory: /loki/retention + delete_request_store: filesystem + retention_enabled: true + retention_delete_delay: 2h + retention_delete_worker_count: 150 + + ruler: + alertmanager_url: http://localhost:9093 + + analytics: + reporting_enabled: false diff --git a/k8s/kube-prometheus/manifests/loki-pvc.yaml b/k8s/kube-prometheus/manifests/loki-pvc.yaml new file mode 100644 index 000000000..e2f2655ef --- /dev/null +++ b/k8s/kube-prometheus/manifests/loki-pvc.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki-storage + namespace: monitoring +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 10Gi diff --git a/k8s/kube-prometheus/manifests/loki-service.yaml b/k8s/kube-prometheus/manifests/loki-service.yaml new file mode 100644 index 000000000..62105c3c4 --- /dev/null +++ b/k8s/kube-prometheus/manifests/loki-service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki + namespace: monitoring +spec: + ports: + - name: http + port: 3100 + targetPort: http + - name: grpc + port: 9095 + targetPort: grpc + selector: + app: loki + type: ClusterIP diff --git a/k8s/kube-prometheus/manifests/loki-serviceAccount.yaml b/k8s/kube-prometheus/manifests/loki-serviceAccount.yaml new file mode 100644 index 000000000..b82dde64e --- /dev/null +++ b/k8s/kube-prometheus/manifests/loki-serviceAccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki + namespace: monitoring +automountServiceAccountToken: false diff --git a/k8s/kube-prometheus/manifests/loki-statefulset.yaml b/k8s/kube-prometheus/manifests/loki-statefulset.yaml new file mode 100644 index 000000000..610a89e8a --- /dev/null +++ b/k8s/kube-prometheus/manifests/loki-statefulset.yaml @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: loki + serviceName: loki + template: + metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + spec: + automountServiceAccountToken: false + containers: + - args: + - -config.file=/etc/loki/config.yaml + - -target=all + image: docker.io/grafana/loki:3.5.0 + imagePullPolicy: IfNotPresent + name: loki + ports: + - containerPort: 3100 + name: http + - containerPort: 9095 + name: grpc + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 15 + timeoutSeconds: 1 + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /etc/loki + name: loki-config + readOnly: true + - mountPath: /loki + name: loki-storage + - mountPath: /tmp + name: tmp + nodeSelector: + kubernetes.io/os: linux + securityContext: + fsGroup: 10001 + runAsGroup: 10001 + runAsNonRoot: true + runAsUser: 10001 + serviceAccountName: loki + volumes: + - configMap: + name: loki-config + name: loki-config + - persistentVolumeClaim: + claimName: loki-storage + name: loki-storage + - emptyDir: {} + name: tmp From a6e02550082e8a148fdf44275297f08ea99d44f2 Mon Sep 17 00:00:00 2001 From: OmkarDeshpande7 Date: Thu, 6 Aug 2026 22:21:57 +0530 Subject: [PATCH 2/6] feat(monitoring): add Promtail DaemonSet to ship migration-system logs to Loki --- .../manifests/promtail-clusterRole.yaml | 21 +++++ .../promtail-clusterRoleBinding.yaml | 16 ++++ .../manifests/promtail-configMap.yaml | 52 ++++++++++++ .../manifests/promtail-daemonset.yaml | 79 +++++++++++++++++++ .../manifests/promtail-serviceAccount.yaml | 10 +++ 5 files changed, 178 insertions(+) create mode 100644 k8s/kube-prometheus/manifests/promtail-clusterRole.yaml create mode 100644 k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml create mode 100644 k8s/kube-prometheus/manifests/promtail-configMap.yaml create mode 100644 k8s/kube-prometheus/manifests/promtail-daemonset.yaml create mode 100644 k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml diff --git a/k8s/kube-prometheus/manifests/promtail-clusterRole.yaml b/k8s/kube-prometheus/manifests/promtail-clusterRole.yaml new file mode 100644 index 000000000..971e4981c --- /dev/null +++ b/k8s/kube-prometheus/manifests/promtail-clusterRole.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail +rules: + - apiGroups: + - "" + resources: + - nodes + - nodes/proxy + - services + - endpoints + - pods + verbs: + - get + - watch + - list diff --git a/k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml b/k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml new file mode 100644 index 000000000..586b28174 --- /dev/null +++ b/k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: promtail +subjects: + - kind: ServiceAccount + name: promtail + namespace: monitoring diff --git a/k8s/kube-prometheus/manifests/promtail-configMap.yaml b/k8s/kube-prometheus/manifests/promtail-configMap.yaml new file mode 100644 index 000000000..e4e65e1ae --- /dev/null +++ b/k8s/kube-prometheus/manifests/promtail-configMap.yaml @@ -0,0 +1,52 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail-config + namespace: monitoring +data: + config.yaml: | + server: + http_listen_port: 9080 + grpc_listen_port: 0 + log_level: warn + + positions: + filename: /run/promtail/positions.yaml + + clients: + - url: http://loki.monitoring.svc:3100/loki/api/v1/push + backoff_config: + min_period: 500ms + max_period: 5m + max_retries: 10 + + scrape_configs: + - job_name: kubernetes-pods + kubernetes_sd_configs: + - role: pod + pipeline_stages: + - cri: {} + relabel_configs: + - source_labels: [__meta_kubernetes_namespace] + action: keep + regex: migration-system + - source_labels: [__meta_kubernetes_pod_node_name] + target_label: __host__ + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + - source_labels: [__meta_kubernetes_namespace] + target_label: namespace + - source_labels: [__meta_kubernetes_pod_name] + target_label: pod + - source_labels: [__meta_kubernetes_pod_container_name] + target_label: container + - replacement: /var/log/pods/*$1/*.log + separator: / + source_labels: + - __meta_kubernetes_pod_uid + - __meta_kubernetes_pod_container_name + target_label: __path__ diff --git a/k8s/kube-prometheus/manifests/promtail-daemonset.yaml b/k8s/kube-prometheus/manifests/promtail-daemonset.yaml new file mode 100644 index 000000000..b3fc5b539 --- /dev/null +++ b/k8s/kube-prometheus/manifests/promtail-daemonset.yaml @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail + namespace: monitoring +spec: + selector: + matchLabels: + app: promtail + template: + metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + spec: + containers: + - args: + - -config.file=/etc/promtail/config.yaml + image: docker.io/grafana/promtail:3.5.0 + imagePullPolicy: IfNotPresent + name: promtail + ports: + - containerPort: 9080 + name: http + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 10 + timeoutSeconds: 1 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /etc/promtail + name: promtail-config + readOnly: true + - mountPath: /var/log/pods + name: pods-logs + readOnly: true + - mountPath: /run/promtail + name: positions + nodeSelector: + kubernetes.io/os: linux + securityContext: + runAsGroup: 0 + runAsUser: 0 + serviceAccountName: promtail + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + volumes: + - configMap: + name: promtail-config + name: promtail-config + - hostPath: + path: /var/log/pods + name: pods-logs + - emptyDir: {} + name: positions diff --git a/k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml b/k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml new file mode 100644 index 000000000..6cedc69f1 --- /dev/null +++ b/k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail + namespace: monitoring +automountServiceAccountToken: true From f385c4fd8709d34cc616968f476db371216dbc08 Mon Sep 17 00:00:00 2001 From: OmkarDeshpande7 Date: Thu, 6 Aug 2026 22:24:16 +0530 Subject: [PATCH 3/6] feat(monitoring): add Loki datasource to Grafana Co-Authored-By: Claude Sonnet 4.6 --- .../manifests/grafana-dashboardDatasources.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml b/k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml index ce89f4a6e..a1d4fcce8 100644 --- a/k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml +++ b/k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml @@ -21,6 +21,15 @@ stringData: "type": "prometheus", "url": "http://prometheus-k8s.monitoring.svc:9090", "version": 1 + }, + { + "access": "proxy", + "editable": false, + "name": "loki", + "orgId": 1, + "type": "loki", + "url": "http://loki.monitoring.svc:3100", + "version": 1 } ] } From 0956ca9bc839bbbf92009c0f827fd12aa53c32ca Mon Sep 17 00:00:00 2001 From: OmkarDeshpande7 Date: Thu, 6 Aug 2026 22:25:47 +0530 Subject: [PATCH 4/6] feat(image-builder): pre-bake Loki and Promtail images for airgap support Co-Authored-By: Claude Sonnet 4.6 --- image_builder/scripts/download_images.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/image_builder/scripts/download_images.sh b/image_builder/scripts/download_images.sh index 290e4a21c..2d0d20a8a 100644 --- a/image_builder/scripts/download_images.sh +++ b/image_builder/scripts/download_images.sh @@ -22,6 +22,8 @@ prometheus_config_reloader="quay.io/prometheus-operator/prometheus-config-reload prometheus_operator="quay.io/prometheus-operator/prometheus-operator:v0.89.0" configmap_reload="ghcr.io/jimmidyson/configmap-reload:v0.15.0" grafana="docker.io/grafana/grafana:12.3.3" +loki="docker.io/grafana/loki:3.5.0" +promtail="docker.io/grafana/promtail:3.5.0" v2v_helper="quay.io/platform9/vjailbreak-v2v-helper:$TAG" controller="quay.io/platform9/vjailbreak-controller:$TAG" ui="quay.io/platform9/vjailbreak-ui:$TAG" @@ -65,6 +67,8 @@ images=( "$ui" "$configmap_reload" "$grafana" + "$loki" + "$promtail" "$alpine" "$vpwned" "$ai" From 45501bd9fe5b814c7779255fba9eed09859bf8fc Mon Sep 17 00:00:00 2001 From: OmkarDeshpande7 Date: Fri, 7 Aug 2026 13:36:26 +0530 Subject: [PATCH 5/6] docs: add Loki+Promtail design spec and implementation plan --- .../2026-08-06-loki-promtail-pod-logs.md | 703 ++++++++++++++++++ ...026-08-06-loki-promtail-pod-logs-design.md | 119 +++ 2 files changed, 822 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-06-loki-promtail-pod-logs.md create mode 100644 docs/superpowers/specs/2026-08-06-loki-promtail-pod-logs-design.md diff --git a/docs/superpowers/plans/2026-08-06-loki-promtail-pod-logs.md b/docs/superpowers/plans/2026-08-06-loki-promtail-pod-logs.md new file mode 100644 index 000000000..766b5d24b --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-loki-promtail-pod-logs.md @@ -0,0 +1,703 @@ +# Loki + Promtail Pod Log Visibility Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add Loki + Promtail to the vJailbreak appliance so pod logs from `migration-system` are visible in Grafana, working airgapped on every fresh install. + +**Architecture:** Raw Kubernetes manifests added to `k8s/kube-prometheus/manifests/` — the existing `install.sh` `kubectl apply` loop picks them up at first boot with zero script changes. Loki images are pre-baked into the appliance QCOW2 via `download_images.sh`. Grafana gains a Loki datasource entry alongside the existing Prometheus datasource. + +**Tech Stack:** Loki 3.5.0 (single-binary, filesystem storage), Promtail 3.5.0, k3s `local-path` StorageClass, Grafana 12.3.3 (already deployed) + +## Global Constraints + +- All manifests go in `k8s/kube-prometheus/manifests/` — namespace `monitoring` +- Loki image: `docker.io/grafana/loki:3.5.0` +- Promtail image: `docker.io/grafana/promtail:3.5.0` +- Log scope: `migration-system` namespace only — Promtail must drop all other namespaces +- Retention: 7 days (168h) +- PVC: 10Gi, storageClass `local-path` +- No changes to `install.sh`, `Makefile`, or any Go code +- `grafana-dashboardDatasources.yaml` is a Secret (not ConfigMap) — patch must preserve the existing Prometheus entry + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|----------------| +| `k8s/kube-prometheus/manifests/loki-serviceAccount.yaml` | Create | Loki identity | +| `k8s/kube-prometheus/manifests/loki-configMap.yaml` | Create | Loki server config (storage, retention, schema) | +| `k8s/kube-prometheus/manifests/loki-pvc.yaml` | Create | 10Gi persistent storage for log chunks | +| `k8s/kube-prometheus/manifests/loki-statefulset.yaml` | Create | Loki single-binary pod | +| `k8s/kube-prometheus/manifests/loki-service.yaml` | Create | ClusterIP on 3100 — `loki.monitoring.svc` | +| `k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml` | Create | Promtail identity | +| `k8s/kube-prometheus/manifests/promtail-clusterRole.yaml` | Create | Read pods/nodes for label enrichment | +| `k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml` | Create | Bind ClusterRole to SA | +| `k8s/kube-prometheus/manifests/promtail-configMap.yaml` | Create | Promtail scrape config (migration-system filter) | +| `k8s/kube-prometheus/manifests/promtail-daemonset.yaml` | Create | Promtail pod per node | +| `k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml` | Modify | Add Loki datasource entry | +| `image_builder/scripts/download_images.sh` | Modify | Add loki + promtail to images array | + +--- + +## Task 1: Loki Manifests + +**Files:** +- Create: `k8s/kube-prometheus/manifests/loki-serviceAccount.yaml` +- Create: `k8s/kube-prometheus/manifests/loki-configMap.yaml` +- Create: `k8s/kube-prometheus/manifests/loki-pvc.yaml` +- Create: `k8s/kube-prometheus/manifests/loki-statefulset.yaml` +- Create: `k8s/kube-prometheus/manifests/loki-service.yaml` + +**Interfaces:** +- Produces: Loki HTTP API at `http://loki.monitoring.svc:3100` — consumed by Task 2 (Promtail push URL) and Task 3 (Grafana datasource URL) + +- [ ] **Step 1: Create `loki-serviceAccount.yaml`** + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki + namespace: monitoring +automountServiceAccountToken: false +``` + +- [ ] **Step 2: Create `loki-configMap.yaml`** + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki-config + namespace: monitoring +data: + config.yaml: | + auth_enabled: false + + server: + http_listen_port: 3100 + grpc_listen_port: 9095 + log_level: warn + + common: + instance_addr: 127.0.0.1 + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + + query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + + schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + + limits_config: + retention_period: 168h + allow_structured_metadata: false + + compactor: + working_directory: /loki/retention + delete_request_store: filesystem + retention_enabled: true + retention_delete_delay: 2h + retention_delete_worker_count: 150 + + ruler: + alertmanager_url: http://localhost:9093 + + analytics: + reporting_enabled: false +``` + +- [ ] **Step 3: Create `loki-pvc.yaml`** + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki-storage + namespace: monitoring +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 10Gi +``` + +- [ ] **Step 4: Create `loki-statefulset.yaml`** + +```yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: loki + serviceName: loki + template: + metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + spec: + automountServiceAccountToken: false + containers: + - args: + - -config.file=/etc/loki/config.yaml + - -target=all + image: docker.io/grafana/loki:3.5.0 + imagePullPolicy: IfNotPresent + name: loki + ports: + - containerPort: 3100 + name: http + - containerPort: 9095 + name: grpc + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 15 + timeoutSeconds: 1 + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /etc/loki + name: loki-config + readOnly: true + - mountPath: /loki + name: loki-storage + - mountPath: /tmp + name: tmp + nodeSelector: + kubernetes.io/os: linux + securityContext: + fsGroup: 10001 + runAsGroup: 10001 + runAsNonRoot: true + runAsUser: 10001 + serviceAccountName: loki + volumes: + - configMap: + name: loki-config + name: loki-config + - persistentVolumeClaim: + claimName: loki-storage + name: loki-storage + - emptyDir: {} + name: tmp +``` + +- [ ] **Step 5: Create `loki-service.yaml`** + +```yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app: loki + app.kubernetes.io/name: loki + app.kubernetes.io/part-of: vjailbreak-monitoring + name: loki + namespace: monitoring +spec: + ports: + - name: http + port: 3100 + targetPort: http + - name: grpc + port: 9095 + targetPort: grpc + selector: + app: loki + type: ClusterIP +``` + +- [ ] **Step 6: Validate YAML syntax for all 5 files** + +```bash +python3 -c " +import yaml, sys +files = [ + 'k8s/kube-prometheus/manifests/loki-serviceAccount.yaml', + 'k8s/kube-prometheus/manifests/loki-configMap.yaml', + 'k8s/kube-prometheus/manifests/loki-pvc.yaml', + 'k8s/kube-prometheus/manifests/loki-statefulset.yaml', + 'k8s/kube-prometheus/manifests/loki-service.yaml', +] +for f in files: + yaml.safe_load(open(f)) + print(f'OK: {f}') +" +``` + +Expected: `OK:` printed for each file, no exceptions. + +- [ ] **Step 7: Commit** + +```bash +git add k8s/kube-prometheus/manifests/loki-serviceAccount.yaml \ + k8s/kube-prometheus/manifests/loki-configMap.yaml \ + k8s/kube-prometheus/manifests/loki-pvc.yaml \ + k8s/kube-prometheus/manifests/loki-statefulset.yaml \ + k8s/kube-prometheus/manifests/loki-service.yaml +git commit -m "feat(monitoring): add Loki manifests for pod log storage" +``` + +--- + +## Task 2: Promtail Manifests + +**Files:** +- Create: `k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml` +- Create: `k8s/kube-prometheus/manifests/promtail-clusterRole.yaml` +- Create: `k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml` +- Create: `k8s/kube-prometheus/manifests/promtail-configMap.yaml` +- Create: `k8s/kube-prometheus/manifests/promtail-daemonset.yaml` + +**Interfaces:** +- Consumes: Loki push URL `http://loki.monitoring.svc:3100/loki/api/v1/push` (from Task 1) +- Produces: Log streams labelled `{namespace="migration-system", pod="...", container="..."}` in Loki + +- [ ] **Step 1: Create `promtail-serviceAccount.yaml`** + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail + namespace: monitoring +automountServiceAccountToken: true +``` + +- [ ] **Step 2: Create `promtail-clusterRole.yaml`** + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail +rules: + - apiGroups: + - "" + resources: + - nodes + - nodes/proxy + - services + - endpoints + - pods + verbs: + - get + - watch + - list +``` + +- [ ] **Step 3: Create `promtail-clusterRoleBinding.yaml`** + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: promtail +subjects: + - kind: ServiceAccount + name: promtail + namespace: monitoring +``` + +- [ ] **Step 4: Create `promtail-configMap.yaml`** + +The `relabel_configs` entry with `action: keep` and `regex: migration-system` ensures Promtail only opens log files from pods in `migration-system`. Pods in all other namespaces are dropped before any I/O occurs. + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail-config + namespace: monitoring +data: + config.yaml: | + server: + http_listen_port: 9080 + grpc_listen_port: 0 + log_level: warn + + positions: + filename: /run/promtail/positions.yaml + + clients: + - url: http://loki.monitoring.svc:3100/loki/api/v1/push + backoff_config: + min_period: 500ms + max_period: 5m + max_retries: 10 + + scrape_configs: + - job_name: kubernetes-pods + kubernetes_sd_configs: + - role: pod + pipeline_stages: + - cri: {} + relabel_configs: + - source_labels: [__meta_kubernetes_namespace] + action: keep + regex: migration-system + - source_labels: [__meta_kubernetes_pod_node_name] + target_label: __host__ + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + - source_labels: [__meta_kubernetes_namespace] + target_label: namespace + - source_labels: [__meta_kubernetes_pod_name] + target_label: pod + - source_labels: [__meta_kubernetes_pod_container_name] + target_label: container + - replacement: /var/log/pods/*$1/*.log + separator: / + source_labels: + - __meta_kubernetes_pod_uid + - __meta_kubernetes_pod_container_name + target_label: __path__ +``` + +- [ ] **Step 5: Create `promtail-daemonset.yaml`** + +Promtail needs read access to `/var/log/pods/` on the host. k3s writes pod logs there via containerd CRI. The positions file is written to an emptyDir so it persists across config reloads but not node reboots (acceptable — Promtail re-reads from last known offset or start of file). + +```yaml +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + name: promtail + namespace: monitoring +spec: + selector: + matchLabels: + app: promtail + template: + metadata: + labels: + app: promtail + app.kubernetes.io/name: promtail + app.kubernetes.io/part-of: vjailbreak-monitoring + spec: + containers: + - args: + - -config.file=/etc/promtail/config.yaml + image: docker.io/grafana/promtail:3.5.0 + imagePullPolicy: IfNotPresent + name: promtail + ports: + - containerPort: 9080 + name: http + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 10 + timeoutSeconds: 1 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /etc/promtail + name: promtail-config + readOnly: true + - mountPath: /var/log/pods + name: pods-logs + readOnly: true + - mountPath: /run/promtail + name: positions + nodeSelector: + kubernetes.io/os: linux + securityContext: + runAsGroup: 0 + runAsUser: 0 + serviceAccountName: promtail + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + volumes: + - configMap: + name: promtail-config + name: promtail-config + - hostPath: + path: /var/log/pods + name: pods-logs + - emptyDir: {} + name: positions +``` + +- [ ] **Step 6: Validate YAML syntax for all 5 files** + +```bash +python3 -c " +import yaml, sys +files = [ + 'k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml', + 'k8s/kube-prometheus/manifests/promtail-clusterRole.yaml', + 'k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml', + 'k8s/kube-prometheus/manifests/promtail-configMap.yaml', + 'k8s/kube-prometheus/manifests/promtail-daemonset.yaml', +] +for f in files: + yaml.safe_load(open(f)) + print(f'OK: {f}') +" +``` + +Expected: `OK:` printed for each file, no exceptions. + +- [ ] **Step 7: Commit** + +```bash +git add k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml \ + k8s/kube-prometheus/manifests/promtail-clusterRole.yaml \ + k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml \ + k8s/kube-prometheus/manifests/promtail-configMap.yaml \ + k8s/kube-prometheus/manifests/promtail-daemonset.yaml +git commit -m "feat(monitoring): add Promtail DaemonSet to ship migration-system logs to Loki" +``` + +--- + +## Task 3: Grafana Datasource + +**Files:** +- Modify: `k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml` + +**Interfaces:** +- Consumes: Loki service URL `http://loki.monitoring.svc:3100` (from Task 1) +- Produces: Grafana "loki" datasource available in Explore and dashboards + +- [ ] **Step 1: Read current file** + +Open `k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml`. The `stringData.datasources.yaml` field currently contains one entry for `prometheus`. We must add a second entry for `loki` without removing the prometheus entry. + +- [ ] **Step 2: Add Loki datasource entry** + +The file is a Kubernetes Secret with `stringData`. Edit the `datasources` array to add the Loki entry: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + labels: + app.kubernetes.io/component: grafana + app.kubernetes.io/name: grafana + app.kubernetes.io/part-of: kube-prometheus + app.kubernetes.io/version: 12.3.3 + name: grafana-datasources + namespace: monitoring +stringData: + datasources.yaml: |- + { + "apiVersion": 1, + "datasources": [ + { + "access": "proxy", + "editable": false, + "name": "prometheus", + "orgId": 1, + "type": "prometheus", + "url": "http://prometheus-k8s.monitoring.svc:9090", + "version": 1 + }, + { + "access": "proxy", + "editable": false, + "name": "loki", + "orgId": 1, + "type": "loki", + "url": "http://loki.monitoring.svc:3100", + "version": 1 + } + ] + } +type: Opaque +``` + +- [ ] **Step 3: Validate YAML syntax** + +```bash +python3 -c " +import yaml +yaml.safe_load(open('k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml')) +print('OK') +" +``` + +Expected: `OK` + +- [ ] **Step 4: Commit** + +```bash +git add k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml +git commit -m "feat(monitoring): add Loki datasource to Grafana" +``` + +--- + +## Task 4: Image Pre-Baking for Airgap + +**Files:** +- Modify: `image_builder/scripts/download_images.sh` + +**Interfaces:** +- Produces: `image_builder/images/docker.io_grafana_loki_3.5.0.tar` and `image_builder/images/docker.io_grafana_promtail_3.5.0.tar` baked into appliance QCOW2 + +- [ ] **Step 1: Read current `download_images.sh`** + +Open the file. Find the variable declarations section (around line 8-30) and the `images=(...)` array (around line 40-65). Note the naming pattern: one variable per image, then the variable referenced in the array. + +- [ ] **Step 2: Add loki and promtail image variables** + +After the existing image variables (e.g., after the `grafana=` line), add: + +```bash +loki="docker.io/grafana/loki:3.5.0" +promtail="docker.io/grafana/promtail:3.5.0" +``` + +- [ ] **Step 3: Add variables to the images array** + +In the `images=(...)` array, add after the `"$grafana"` entry: + +```bash + "$loki" + "$promtail" +``` + +- [ ] **Step 4: Verify the diff looks correct** + +```bash +git diff image_builder/scripts/download_images.sh +``` + +Expected diff: two new variable declarations and two new entries in the images array. No other changes. + +- [ ] **Step 5: Commit** + +```bash +git add image_builder/scripts/download_images.sh +git commit -m "feat(image-builder): pre-bake Loki and Promtail images for airgap support" +``` + +--- + +## Verification (on a running vJailbreak appliance) + +After applying all tasks to a running appliance or deploying a newly built image: + +```bash +# 1. Confirm Loki pod is Running +kubectl -n monitoring get pod -l app=loki +# Expected: loki-0 1/1 Running + +# 2. Confirm Promtail pod is Running on each node +kubectl -n monitoring get pod -l app=promtail +# Expected: promtail- 1/1 Running + +# 3. Check Loki is ready +kubectl -n monitoring exec loki-0 -- wget -qO- http://localhost:3100/ready +# Expected: ready + +# 4. Check Promtail is scraping migration-system +kubectl -n monitoring logs -l app=promtail | grep migration-system +# Expected: log lines referencing migration-system pods + +# 5. Verify in Grafana +# Navigate to Grafana → Explore → select "loki" datasource +# Run query: {namespace="migration-system"} +# Expected: log lines from migration-system pods appear +``` diff --git a/docs/superpowers/specs/2026-08-06-loki-promtail-pod-logs-design.md b/docs/superpowers/specs/2026-08-06-loki-promtail-pod-logs-design.md new file mode 100644 index 000000000..977215fcb --- /dev/null +++ b/docs/superpowers/specs/2026-08-06-loki-promtail-pod-logs-design.md @@ -0,0 +1,119 @@ +# Design: Loki + Promtail Pod Log Visibility in Grafana + +**Date:** 2026-08-06 +**Status:** Approved + +## Problem + +vJailbreak Grafana only has a Prometheus datasource (metrics). Pod logs from `migration-system` are not visible in Grafana — operators must `kubectl logs` manually to debug migration failures. + +## Goal + +Show pod logs from the `migration-system` namespace in Grafana using Loki as the log backend and Promtail as the log collector. Must work airgapped and survive appliance upgrades. + +## Non-Goals + +- Logs from namespaces other than `migration-system` +- Log-based alerting +- Long-term log archival beyond 7 days + +## Decisions + +| Question | Decision | Reason | +|----------|----------|--------| +| Log scope | `migration-system` only | Reduce noise and disk use | +| Retention | 7 days | Sufficient for active debugging | +| Storage | PVC via `local-path` provisioner | Survives Loki pod restarts; k3s ships `local-path` | +| Deployment pattern | Raw manifests in `k8s/kube-prometheus/manifests/` | Consistent with existing monitoring stack; zero `install.sh` changes | +| Image pre-baking | Add to `download_images.sh` | Airgap support; existing `ctr import` loop handles load | + +## Architecture + +``` +[migration-system pods] + | stdout/stderr written to /var/log/pods/ + v +[Promtail DaemonSet] -- scrapes /var/log/pods/, filters to migration-system ---> [Loki StatefulSet] + | + [Grafana Loki datasource] + | + [Grafana Explore/Dashboards] +``` + +All components run in the `monitoring` namespace, matching existing kube-prometheus stack. + +## Components + +### Loki + +- **Image:** `docker.io/grafana/loki:3.5.0` +- **Kind:** StatefulSet (1 replica), single-binary mode +- **Storage:** 10Gi PVC via `local-path` storageClass, mounted at `/loki` +- **Retention:** 7 days (`retention_period: 168h`, `retention_deletes_enabled: true`) +- **Resources:** requests 100m CPU / 128Mi RAM; limits 200m CPU / 256Mi RAM +- **Service:** ClusterIP on port 3100, name `loki.monitoring.svc` + +### Promtail + +- **Image:** `docker.io/grafana/promtail:3.5.0` +- **Kind:** DaemonSet (one pod per node) +- **Scope:** Scrapes `/var/log/pods/` but filters to `migration-system` namespace via pipeline stage `drop` on all other namespaces +- **Resources:** requests 50m CPU / 64Mi RAM; limits 100m CPU / 128Mi RAM +- **RBAC:** ClusterRole with `get`/`list`/`watch` on pods and nodes (needed for label enrichment) + +### Grafana datasource + +- Add Loki entry to `grafana-dashboardDatasources.yaml` secret alongside existing Prometheus entry +- URL: `http://loki.monitoring.svc:3100` +- `editable: false` (matches Prometheus pattern) + +## Files Changed + +### New files + +| Path | Purpose | +|------|---------| +| `k8s/kube-prometheus/manifests/loki-serviceAccount.yaml` | ServiceAccount | +| `k8s/kube-prometheus/manifests/loki-configMap.yaml` | Loki config (retention, filesystem storage) | +| `k8s/kube-prometheus/manifests/loki-pvc.yaml` | 10Gi PVC | +| `k8s/kube-prometheus/manifests/loki-statefulset.yaml` | Single-binary StatefulSet | +| `k8s/kube-prometheus/manifests/loki-service.yaml` | ClusterIP service | +| `k8s/kube-prometheus/manifests/promtail-serviceAccount.yaml` | ServiceAccount | +| `k8s/kube-prometheus/manifests/promtail-clusterRole.yaml` | RBAC: read pods/nodes | +| `k8s/kube-prometheus/manifests/promtail-clusterRoleBinding.yaml` | Bind role to SA | +| `k8s/kube-prometheus/manifests/promtail-configMap.yaml` | Scrape config (migration-system filter) | +| `k8s/kube-prometheus/manifests/promtail-daemonset.yaml` | DaemonSet | + +### Modified files + +| Path | Change | +|------|--------| +| `k8s/kube-prometheus/manifests/grafana-dashboardDatasources.yaml` | Add Loki datasource | +| `image_builder/scripts/download_images.sh` | Add `grafana/loki:3.5.0` and `grafana/promtail:3.5.0` to images array | + +### No changes required + +- `image_builder/scripts/install.sh` — existing loops handle image import and manifest apply +- `Makefile` — `docker-build-image` already copies `k8s/kube-prometheus/` into image + +## Airgap + Upgrade Path + +1. `download_images.sh` pulls and exports Loki + Promtail as `.tar` files at build time +2. Packer bakes all `/images/*.tar` into appliance QCOW2 +3. `install.sh` imports all `*.tar` at first boot via `ctr images import` +4. `install.sh` applies all `k8s/kube-prometheus/manifests/` files — picks up new Loki/Promtail manifests automatically +5. New appliance version = new QCOW2 with updated manifests and images baked in + +## Verification + +After deployment, confirm: +```bash +# Loki running +kubectl -n monitoring get pod -l app=loki + +# Promtail running on node +kubectl -n monitoring get pod -l app=promtail + +# Grafana shows Loki datasource +# Grafana → Explore → select "loki" → query: {namespace="migration-system"} +``` From 6eac65b65c66388d9297c12ca15653cac1760ebe Mon Sep 17 00:00:00 2001 From: OmkarDeshpande7 Date: Tue, 18 Aug 2026 16:24:34 +0530 Subject: [PATCH 6/6] PR review skill imporovements --- .claude/skills/vjailbreak-pr-review/SKILL.md | 7 +- .../vjailbreak-pr-review/github_server.py | 326 ++++++++++++++++++ 2 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/vjailbreak-pr-review/github_server.py diff --git a/.claude/skills/vjailbreak-pr-review/SKILL.md b/.claude/skills/vjailbreak-pr-review/SKILL.md index d9f61aa79..76df98289 100644 --- a/.claude/skills/vjailbreak-pr-review/SKILL.md +++ b/.claude/skills/vjailbreak-pr-review/SKILL.md @@ -237,10 +237,9 @@ cat > "$(dirname $RUN_DIR)/eval_metadata.json" << 'JSONEOF' {"prompt": "Review PR #: https://github.com/platform9/vjailbreak/pull/"} JSONEOF -# Launch eval viewer (opens browser automatically) -EVAL_VIEWER="$HOME/.claude/plugins/marketplaces/claude-plugins-official/plugins/skill-creator/skills/skill-creator" -cd "$EVAL_VIEWER" -python3 eval-viewer/generate_review.py "$WORKSPACE" --skill-name vjailbreak-pr-review & +# Launch eval viewer with GitHub posting support (opens browser automatically) +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +python3 "$SKILL_DIR/github_server.py" "$WORKSPACE" --skill-name vjailbreak-pr-review & ``` The viewer starts a local HTTP server and opens `http://localhost:3117` in the browser automatically. The **Post to GitHub** section shows checkboxes for the summary and each finding — check the ones you want, then click **Post Selected to GitHub**. The `send_comments.sh` is a fallback if the server is unavailable. diff --git a/.claude/skills/vjailbreak-pr-review/github_server.py b/.claude/skills/vjailbreak-pr-review/github_server.py new file mode 100644 index 000000000..0e83ef8f4 --- /dev/null +++ b/.claude/skills/vjailbreak-pr-review/github_server.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +"""vjailbreak-pr-review eval viewer with GitHub posting support. + +Wraps the skill-creator eval viewer and adds: + POST /api/github — runs gh pr review / gh pr comment for selected findings + viewer HTML — injects "Post to GitHub" section with checkboxes + +Never modify the shared skill-creator files; keep all customization here. +""" + +import json +import subprocess +import sys +from functools import partial +from http.server import HTTPServer +from pathlib import Path + +# --------------------------------------------------------------------------- +# Locate and import the shared eval viewer +# --------------------------------------------------------------------------- + +_BASE = ( + Path.home() + / ".claude/plugins/marketplaces/claude-plugins-official" + / "plugins/skill-creator/skills/skill-creator/eval-viewer" +) + +if not (_BASE / "generate_review.py").exists(): + sys.exit(f"ERROR: skill-creator eval-viewer not found at {_BASE}") + +sys.path.insert(0, str(_BASE)) +import generate_review # noqa: E402 (must come after sys.path insert) + + +# --------------------------------------------------------------------------- +# GitHub CSS / HTML / JS injected into every served page +# --------------------------------------------------------------------------- + +_GITHUB_CSS = """ + /* ---- Post to GitHub ---- */ + .github-poster { display: flex; flex-direction: column; gap: 0.625rem; } + .github-poster .gh-meta { font-size: 0.8125rem; color: var(--text-muted); margin-bottom: 0.25rem; } + .github-item-label { + display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; + font-size: 0.875rem; line-height: 1.4; padding: 0.5rem 0.625rem; + border: 1px solid var(--border); border-radius: var(--radius); + background: var(--bg); transition: background 0.1s; + } + .github-item-label:hover { background: #f0ede4; } + .github-item-label input[type="checkbox"] { + margin-top: 0.15rem; flex-shrink: 0; accent-color: var(--accent); width: 1rem; height: 1rem; + } + .github-item-label .gh-title { font-weight: 500; } + .github-item-label .gh-tag { + font-size: 0.6875rem; font-family: 'Poppins', sans-serif; font-weight: 600; + text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-left: 0.25rem; + } + .github-actions { display: flex; align-items: center; gap: 1rem; margin-top: 0.375rem; } + .github-post-btn { + font-family: 'Poppins', sans-serif; padding: 0.5rem 1.25rem; + background: var(--accent); color: white; border: none; border-radius: var(--radius); + cursor: pointer; font-size: 0.875rem; font-weight: 600; transition: background 0.15s; + } + .github-post-btn:hover:not(:disabled) { background: var(--accent-hover); } + .github-post-btn:disabled { opacity: 0.5; cursor: not-allowed; } + .github-post-status { font-size: 0.8125rem; color: var(--text-muted); } + .gh-results-list { list-style: none; display: flex; flex-direction: column; gap: 0.375rem; margin-top: 0.375rem; } + .gh-results-list li { font-size: 0.8125rem; padding: 0.375rem 0.625rem; border-radius: 4px; } + .gh-results-list .gh-ok { background: var(--green-bg); color: var(--green); } + .gh-results-list .gh-err { background: var(--red-bg); color: var(--red); } + .gh-results-list .gh-link { color: var(--accent); text-decoration: underline; margin-left: 0.5rem; font-size: 0.75rem; }""" + +_GITHUB_HTML_SECTION = """ + + + """ + +_GITHUB_JS = """ + // ---- Post to GitHub ---- + let currentFindingsData = null; + + function renderGitHub(run) { + const section = document.getElementById("github-section"); + const body = document.getElementById("github-body"); + currentFindingsData = null; + const findingsFile = (run.outputs || []).find(f => f.name === "findings.json" && f.type === "text"); + if (!findingsFile) { section.style.display = "none"; return; } + let fd; + try { fd = JSON.parse(findingsFile.content); } catch (e) { section.style.display = "none"; return; } + currentFindingsData = fd; + section.style.display = "block"; + const verdictLabel = (fd.verdict_flag || "--comment").replace(/^--/, "").replace(/-/g, " "); + let html = '
'; + html += `
PR #${escapeHtml(String(fd.pr || "?"))} · ${escapeHtml(fd.repo || "")}
`; + html += ``; + for (let i = 0; i < (fd.findings || []).length; i++) { + const f = fd.findings[i]; + html += ``; + } + html += `
`; + html += '
    '; + body.innerHTML = html; + } + + async function postToGitHub() { + if (!currentFindingsData) return; + const selected = []; + if (document.getElementById("gh-summary")?.checked) selected.push("summary"); + for (let i = 0; i < (currentFindingsData.findings || []).length; i++) { + if (document.getElementById(`gh-finding-${i}`)?.checked) selected.push(`finding_${i}`); + } + if (selected.length === 0) { document.getElementById("gh-status").textContent = "Nothing selected."; return; } + const btn = document.getElementById("gh-post-btn"); + const status = document.getElementById("gh-status"); + btn.disabled = true; status.textContent = "Posting\u2026"; + try { + const resp = await fetch("/api/github", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ findings_data: currentFindingsData, selected }), + }); + const data = await resp.json(); + if (data.error) { status.textContent = "Error: " + data.error; btn.disabled = false; return; } + const resultsList = document.getElementById("gh-results"); + resultsList.innerHTML = ""; + let allOk = true; + for (const r of (data.results || [])) { + const li = document.createElement("li"); + li.className = r.ok ? "gh-ok" : "gh-err"; + li.textContent = (r.ok ? "\\u2713 " : "\\u2717 ") + r.id + ": " + (r.ok ? "posted" : r.output); + if (r.ok && r.url) { + const a = document.createElement("a"); + a.href = r.url; a.target = "_blank"; a.className = "gh-link"; a.textContent = "View"; + li.appendChild(a); + } + resultsList.appendChild(li); + if (!r.ok) allOk = false; + } + status.textContent = allOk ? "All posted!" : "Some failed \u2014 see below."; + btn.disabled = false; + } catch (e) { status.textContent = "Network error: " + e.message; btn.disabled = false; } + } + + """ + + +def _inject_github(html: str) -> str: + """Inject GitHub CSS, HTML section, and JS into the base viewer HTML.""" + # CSS: before + html = html.replace(" ", _GITHUB_CSS + "\n ", 1) + + # HTML section: before + feedback_anchor = "" + if feedback_anchor in html: + html = html.replace(feedback_anchor, _GITHUB_HTML_SECTION + feedback_anchor, 1) + + # renderGitHub call: after renderOutputs(run); + html = html.replace( + "renderOutputs(run);", + "renderOutputs(run);\n\n // GitHub posting\n renderGitHub(run);", + 1, + ) + + # JS functions: before // ---- Feedback + feedback_js = "// ---- Feedback (saved to server -> feedback.json) ----" + if feedback_js in html: + html = html.replace(feedback_js, _GITHUB_JS + feedback_js, 1) + + return html + + +# --------------------------------------------------------------------------- +# GitHub posting logic +# --------------------------------------------------------------------------- + +def _post_to_github(data: dict) -> list[dict]: + findings_data = data.get("findings_data", {}) + selected = set(data.get("selected", [])) + pr = str(findings_data.get("pr", "")) + repo = findings_data.get("repo", "") + verdict_flag = findings_data.get("verdict_flag", "--comment") + summary_body = findings_data.get("summary_body", "") + findings = findings_data.get("findings", []) + + if not pr or not repo: + raise ValueError("findings.json must contain 'pr' and 'repo' fields") + + results = [] + + if "summary" in selected: + cmd = ["gh", "pr", "review", pr, "--repo", repo, verdict_flag, "--body", summary_body] + r = subprocess.run(cmd, capture_output=True, text=True) + results.append({ + "id": "summary", + "ok": r.returncode == 0, + "url": r.stdout.strip(), + "output": (r.stdout + r.stderr).strip(), + }) + + for i, finding in enumerate(findings): + key = f"finding_{i}" + if key not in selected: + continue + cmd = ["gh", "pr", "comment", pr, "--repo", repo, "--body", finding.get("body", "")] + r = subprocess.run(cmd, capture_output=True, text=True) + results.append({ + "id": key, + "ok": r.returncode == 0, + "url": r.stdout.strip(), + "output": (r.stdout + r.stderr).strip(), + }) + + return results + + +# --------------------------------------------------------------------------- +# Extended handler +# --------------------------------------------------------------------------- + +class GitHubReviewHandler(generate_review.ReviewHandler): + """ReviewHandler extended with /api/github endpoint.""" + + def do_GET(self) -> None: + if self.path in ("/", "/index.html"): + # Generate base HTML then inject GitHub additions + runs = generate_review.find_runs(self.workspace) + benchmark = None + if self.benchmark_path and self.benchmark_path.exists(): + try: + benchmark = json.loads(self.benchmark_path.read_text()) + except (json.JSONDecodeError, OSError): + pass + html = generate_review.generate_html(runs, self.skill_name, self.previous, benchmark) + html = _inject_github(html) + content = html.encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + else: + super().do_GET() + + def do_POST(self) -> None: + if self.path == "/api/github": + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + try: + payload = json.loads(body) + results = _post_to_github(payload) + resp = json.dumps({"ok": True, "results": results}).encode() + self.send_response(200) + except (json.JSONDecodeError, OSError, ValueError) as e: + resp = json.dumps({"error": str(e)}).encode() + self.send_response(500) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(resp))) + self.end_headers() + self.wfile.write(resp) + else: + super().do_POST() + + +# --------------------------------------------------------------------------- +# Entry point (mirrors generate_review.main) +# --------------------------------------------------------------------------- + +def main() -> None: + import argparse + import time + import webbrowser + + parser = argparse.ArgumentParser(description="vjailbreak-pr-review eval viewer with GitHub posting") + parser.add_argument("workspace", type=Path) + parser.add_argument("--port", "-p", type=int, default=3117) + parser.add_argument("--skill-name", "-n", type=str, default="vjailbreak-pr-review") + parser.add_argument("--previous-workspace", type=Path, default=None) + parser.add_argument("--benchmark", type=Path, default=None) + args = parser.parse_args() + + workspace = args.workspace.resolve() + if not workspace.is_dir(): + sys.exit(f"Error: {workspace} is not a directory") + + runs = generate_review.find_runs(workspace) + if not runs: + sys.exit(f"No runs found in {workspace}") + + feedback_path = workspace / "feedback.json" + previous: dict = {} + if args.previous_workspace: + previous = generate_review.load_previous_iteration(args.previous_workspace.resolve()) + benchmark_path = args.benchmark.resolve() if args.benchmark else None + + generate_review._kill_port(args.port) + + handler = partial( + GitHubReviewHandler, + workspace, + args.skill_name, + feedback_path, + previous, + benchmark_path, + ) + try: + server = HTTPServer(("127.0.0.1", args.port), handler) + except OSError: + server = HTTPServer(("127.0.0.1", 0), handler) + + port = server.server_address[1] + url = f"http://localhost:{port}" + print(f"\n vjailbreak-pr-review Eval Viewer (with GitHub posting)") + print(f" URL: {url}\n Workspace: {workspace}\n") + webbrowser.open(url) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nStopped.") + server.server_close() + + +if __name__ == "__main__": + main()