Files
Agent-System/internal/engineeringdashboard/source_runtime.go
T
weichenandCursor a07ba1065b feat(engineering-dashboard): U2-U4 docs git and runtime collectors
Add allowlisted markdown indexing, read-only git plumbing via injected
runners, and docker/readiness collection with path and loopback guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 07:03:32 +00:00

352 lines
9.2 KiB
Go

package engineeringdashboard
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
// ReadyEndpoint maps an environment name to a loopback readiness URL.
type ReadyEndpoint struct {
Env string
URL string
}
var defaultReadyEndpoints = []ReadyEndpoint{
{Env: "dev", URL: "http://127.0.0.1:8080/ready"},
{Env: "test", URL: "http://127.0.0.1:8081/ready"},
{Env: "main", URL: "http://127.0.0.1:8000/ready"},
}
var allowedComposeProjects = map[string]string{
"agent-dev": "dev",
"agent-test": "test",
"agent-main": "main",
"agent-infra": "shared",
}
// RuntimeCollectResult is the runtime collector output.
type RuntimeCollectResult struct {
Source SourceState
Environments []EnvironmentState
Facts []Fact
Warnings []string
}
// CollectRuntime gathers docker + readiness + receipt observations.
func CollectRuntime(ctx context.Context, repoRoot string, runner CommandRunner, now time.Time) RuntimeCollectResult {
start := time.Now()
res := RuntimeCollectResult{}
if runner == nil {
runner = ExecRunner{}
}
envs := map[string]*EnvironmentState{
"shared": {Name: "shared"},
"dev": {Name: "dev"},
"test": {Name: "test"},
"main": {Name: "main"},
}
psOut, stderr, err := runner.Run(ctx, repoRoot, "docker", "ps", "-a", "--format", "{{json .}}")
if err != nil {
res.Warnings = append(res.Warnings, "docker unavailable: "+strings.TrimSpace(stderr+" "+err.Error()))
for _, e := range envs {
e.Facets = DeploymentFacets{
ContainerStatus: "unknown", AppReadiness: "unknown", ServicesOK: "unknown",
IdentityNote: "docker unavailable",
}
}
} else {
for _, line := range strings.Split(psOut, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
cv, envName, ok := parseDockerLine(line)
if !ok {
continue
}
e := envs[envName]
if e == nil {
continue
}
e.Containers = append(e.Containers, cv)
}
}
client := &http.Client{
Timeout: 2 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if err := assertLoopbackReadyURL(req.URL.String()); err != nil {
return err
}
if len(via) >= 3 {
return fmt.Errorf("too many redirects")
}
return nil
},
}
for _, ep := range defaultReadyEndpoints {
if err := assertLoopbackReadyURL(ep.URL); err != nil {
res.Warnings = append(res.Warnings, err.Error())
continue
}
status, body, err := getReady(ctx, client, ep.URL)
e := envs[ep.Env]
if e == nil {
continue
}
if err != nil {
e.Readiness = "error"
e.Facets.AppReadiness = "error"
res.Warnings = append(res.Warnings, ep.Env+" ready: "+err.Error())
continue
}
if status >= 200 && status < 300 {
e.Readiness = "ok"
e.Facets.AppReadiness = "ok"
} else {
e.Readiness = fmt.Sprintf("http_%d", status)
e.Facets.AppReadiness = e.Readiness
}
if strings.Contains(body, "schema_version") {
e.Schema = extractJSONField(body, "schema_version")
}
res.Facts = append(res.Facts, Fact{
ID: FactID("env:"+ep.Env, "readiness", "ready"), Subject: "env:" + ep.Env, Field: "readiness",
Value: e.Readiness, Provenance: ProvenanceObserved, SourceRef: ep.URL, ObservedAt: now,
})
}
for name, e := range envs {
e.Facets.ContainerStatus = summarizeContainers(e.Containers)
e.Facets.ServicesOK = summarizeServices(e.Containers, e.Facets.AppReadiness)
if e.Facets.AppReadiness == "" {
e.Facets.AppReadiness = "unknown"
}
receipt := findLatestReceipt(repoRoot)
if receipt != "" {
e.Receipt = receipt
e.Facets.ArtifactID = filepath.Base(receipt)
}
if e.Facets.ContainerStatus == "partial" && e.Facets.AppReadiness == "ok" {
e.DriftNotes = append(e.DriftNotes, "app readiness ok coexists with partial container health — no aggregate green")
e.Facets.IdentityNote = "running visible; service completeness partial"
}
if e.Facets.ContainerStatus != "unknown" && e.Facets.ArtifactID == "" {
e.Facets.IdentityNote = "running visible, version unverified"
}
res.Facts = append(res.Facts, Fact{
ID: FactID("env:"+name, "container_status", "docker"), Subject: "env:" + name, Field: "container_status",
Value: e.Facets.ContainerStatus, Provenance: ProvenanceObserved, SourceRef: "docker", ObservedAt: now,
})
}
for _, name := range []string{"shared", "dev", "test", "main"} {
res.Environments = append(res.Environments, *envs[name])
}
status := SourceOK
fresh := FreshnessFresh
if len(res.Warnings) > 0 {
status = SourceDegraded
}
res.Source = SourceState{
Name: "runtime", Status: status, Freshness: fresh,
ObservedAt: now, LastSuccessAt: &now, DurationMS: time.Since(start).Milliseconds(),
}
return res
}
func parseDockerLine(line string) (ContainerView, string, bool) {
var raw map[string]any
if err := json.Unmarshal([]byte(line), &raw); err != nil {
return ContainerView{}, "", false
}
// docker --format {{json .}} fields vary; support both Names and names
name := strField(raw, "Names", "names", "Name")
image := strField(raw, "Image", "image")
status := strField(raw, "Status", "status", "State")
labels := strField(raw, "Labels", "labels")
project, service := parseComposeLabels(labels)
envName := allowedComposeProjects[project]
if envName == "" {
// try infer from name
lname := strings.ToLower(name)
switch {
case strings.Contains(lname, "-dev"), strings.Contains(lname, "_dev"):
envName = "dev"
case strings.Contains(lname, "-test"), strings.Contains(lname, "_test"):
envName = "test"
case strings.Contains(lname, "-main"), strings.Contains(lname, "_main"):
envName = "main"
default:
return ContainerView{}, "", false
}
}
health := ""
lowerStatus := strings.ToLower(status)
switch {
case strings.Contains(lowerStatus, "unhealthy"):
health = "unhealthy"
case strings.Contains(lowerStatus, "healthy"):
health = "healthy"
case strings.Contains(lowerStatus, "restarting"):
health = "unhealthy"
}
restarts := 0
if strings.Contains(lowerStatus, "restarting") {
restarts = 1
}
cv := ContainerView{
Name: name, Image: image, Status: status, Health: health,
RestartCount: restarts, Project: project, Service: service,
}
return cv, envName, true
}
func strField(m map[string]any, keys ...string) string {
for _, k := range keys {
if v, ok := m[k]; ok {
switch t := v.(type) {
case string:
return t
default:
return fmt.Sprint(t)
}
}
}
return ""
}
func parseComposeLabels(labels string) (project, service string) {
for _, part := range strings.Split(labels, ",") {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "com.docker.compose.project":
project = kv[1]
case "com.docker.compose.service":
service = kv[1]
}
}
return project, service
}
func summarizeContainers(cs []ContainerView) string {
if len(cs) == 0 {
return "unknown"
}
bad := 0
for _, c := range cs {
ls := strings.ToLower(c.Status + " " + c.Health)
if strings.Contains(ls, "restart") || strings.Contains(ls, "unhealthy") || strings.Contains(ls, "exited") {
bad++
}
}
if bad == 0 {
return "ok"
}
if bad < len(cs) {
return "partial"
}
return "unhealthy"
}
func summarizeServices(cs []ContainerView, readiness string) string {
cstat := summarizeContainers(cs)
if cstat == "ok" && readiness == "ok" {
return "complete"
}
if cstat == "unknown" && (readiness == "" || readiness == "unknown") {
return "unknown"
}
if cstat == "partial" || readiness == "ok" {
return "partial"
}
return "partial"
}
func assertLoopbackReadyURL(raw string) error {
u, err := url.Parse(raw)
if err != nil {
return err
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("ready scheme denied: %s", u.Scheme)
}
host := u.Hostname()
ip := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() {
return fmt.Errorf("ready host must be loopback: %s", host)
}
port := u.Port()
switch port {
case "8080", "8081", "8000", "":
// ok (empty only if default — still require explicit in our list)
default:
return fmt.Errorf("ready port not allowlisted: %s", port)
}
return nil
}
func getReady(ctx context.Context, client *http.Client, rawURL string) (int, string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil {
return 0, "", err
}
resp, err := client.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return resp.StatusCode, string(body), nil
}
func extractJSONField(body, key string) string {
var m map[string]any
if err := json.Unmarshal([]byte(body), &m); err != nil {
return ""
}
if v, ok := m[key]; ok {
return fmt.Sprint(v)
}
return ""
}
func findLatestReceipt(repoRoot string) string {
dir := filepath.Join(repoRoot, ".doc", "test-reports", "assets", "promotion-receipts")
entries, err := os.ReadDir(dir)
if err != nil {
return ""
}
var latest string
var latestMod time.Time
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
info, err := e.Info()
if err != nil {
continue
}
if info.ModTime().After(latestMod) {
latestMod = info.ModTime()
latest = filepath.ToSlash(filepath.Join(".doc/test-reports/assets/promotion-receipts", e.Name()))
}
}
return latest
}