cmd/coordinator: add health item for gitmirror

Change-Id: I2e58f30a635bad22df8d7ec0d7b4e515c471aa05
Reviewed-on: https://go-review.googlesource.com/c/build/+/179877
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/coordinator/kube.go b/cmd/coordinator/kube.go
index 1baa7f0..22e2ecc 100644
--- a/cmd/coordinator/kube.go
+++ b/cmd/coordinator/kube.go
@@ -77,6 +77,7 @@
 	sourcecache.RegisterGitMirrorDial(func(ctx context.Context) (net.Conn, error) {
 		return goKubeClient.DialServicePort(ctx, "gitmirror", "")
 	})
+	go monitorGitMirror() // requires goKubeClient
 
 	go kubePool.pollCapacityLoop()
 	return nil
diff --git a/cmd/coordinator/status.go b/cmd/coordinator/status.go
index 34466df..bafef2a 100644
--- a/cmd/coordinator/status.go
+++ b/cmd/coordinator/status.go
@@ -7,7 +7,9 @@
 package main
 
 import (
+	"bufio"
 	"bytes"
+	"context"
 	"fmt"
 	"html"
 	"html/template"
@@ -15,6 +17,7 @@
 	"net/http"
 	"os"
 	"os/exec"
+	"regexp"
 	"runtime"
 	"sort"
 	"strings"
@@ -92,7 +95,7 @@
 type healthChecker struct {
 	ID     string
 	Title  string
-	EnvURL string
+	DocURL string
 	Check  func(*checkWriter)
 }
 
@@ -130,13 +133,14 @@
 	addHealthChecker(newJoyentSolarisChecker())
 	addHealthChecker(newJoyentIllumosChecker())
 	addHealthChecker(newBasepinChecker())
+	addHealthChecker(newGitMirrorChecker())
 }
 
 func newBasepinChecker() *healthChecker {
 	return &healthChecker{
 		ID:     "basepin",
 		Title:  "VM snapshots",
-		EnvURL: "https://golang.org/issue/21305",
+		DocURL: "https://golang.org/issue/21305",
 		Check: func(w *checkWriter) {
 			v := basePinErr.Load()
 			if v == nil {
@@ -151,6 +155,73 @@
 	}
 }
 
+var lastGitMirrorErrors atomic.Value // of []string
+
+func monitorGitMirror() {
+	for {
+		lastGitMirrorErrors.Store(gitMirrorErrors())
+		time.Sleep(30 * time.Second)
+	}
+}
+
+// $1 is repo; $2 is error message
+var gitMirrorLineRx = regexp.MustCompile(`/debug/watcher/([\w-]+).?>.+</a> - (.*)`)
+
+func gitMirrorErrors() (errs []string) {
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	req, _ := http.NewRequest("GET", "http://gitmirror/", nil)
+	req = req.WithContext(ctx)
+	res, err := watcherProxy.Transport.RoundTrip(req)
+	if err != nil {
+		return []string{err.Error()}
+	}
+	defer res.Body.Close()
+	if res.StatusCode != 200 {
+		return []string{res.Status}
+	}
+	// TODO: add a JSON mode to gitmirror so we don't need to parse HTML.
+	// This works for now. We control its output.
+	bs := bufio.NewScanner(res.Body)
+	for bs.Scan() {
+		// Lines look like:
+		//    <html><body><pre><a href='/debug/watcher/arch'>arch</a> - ok
+		// or:
+		//    <a href='/debug/watcher/arch'>arch</a> - ok
+		// (See https://farmer.golang.org/debug/watcher/)
+		line := bs.Text()
+		if strings.HasSuffix(line, " - ok") {
+			continue
+		}
+		m := gitMirrorLineRx.FindStringSubmatch(line)
+		if m == nil {
+			if strings.Contains(line, "</html>") {
+				break
+			}
+			return []string{fmt.Sprintf("error parsing line %q", line)}
+		}
+		errs = append(errs, fmt.Sprintf("repo %s: %s"))
+	}
+	if err := bs.Err(); err != nil {
+		errs = append(errs, err.Error())
+	}
+	return errs
+}
+
+func newGitMirrorChecker() *healthChecker {
+	return &healthChecker{
+		ID:     "gitmirror",
+		Title:  "Git mirroring",
+		DocURL: "https://github.com/golang/build/tree/master/cmd/gitmirror",
+		Check: func(w *checkWriter) {
+			ee, _ := lastGitMirrorErrors.Load().([]string)
+			for _, v := range ee {
+				w.error(v)
+			}
+		},
+	}
+}
+
 func newMacHealthChecker() *healthChecker {
 	var hosts []string
 	const numMacHosts = 10 // physical Mac minis, not reverse buildlet connections
@@ -196,7 +267,7 @@
 	return &healthChecker{
 		ID:     "macs",
 		Title:  "MacStadium Mac VMs",
-		EnvURL: "https://github.com/golang/build/tree/master/env/darwin/macstadium",
+		DocURL: "https://github.com/golang/build/tree/master/env/darwin/macstadium",
 		Check: func(w *checkWriter) {
 			// Check hosts.
 			checkHosts(w)
@@ -214,7 +285,7 @@
 	return &healthChecker{
 		ID:     "joyent-solaris",
 		Title:  "Joyent solaris/amd64 machines",
-		EnvURL: "https://github.com/golang/build/tree/master/env/solaris-amd64/joyent",
+		DocURL: "https://github.com/golang/build/tree/master/env/solaris-amd64/joyent",
 		Check:  hostTypeChecker("host-solaris-amd64"),
 	}
 }
@@ -223,7 +294,7 @@
 	return &healthChecker{
 		ID:     "joyent-illumos",
 		Title:  "Joyent illumos/amd64 machines",
-		EnvURL: "https://github.com/golang/build/tree/master/env/illumos-amd64-joyent",
+		DocURL: "https://github.com/golang/build/tree/master/env/illumos-amd64-joyent",
 		Check:  hostTypeChecker("host-illumos-amd64-joyent"),
 	}
 }
@@ -263,7 +334,7 @@
 	return &healthChecker{
 		ID:     "scaleway",
 		Title:  "Scaleway linux/arm machines",
-		EnvURL: "https://github.com/golang/build/tree/master/env/linux-arm/scaleway",
+		DocURL: "https://github.com/golang/build/tree/master/env/linux-arm/scaleway",
 		Check:  reverseHostChecker(hosts),
 	}
 }
@@ -277,7 +348,7 @@
 	return &healthChecker{
 		ID:     "packet",
 		Title:  "Packet linux/arm64 machines",
-		EnvURL: "https://github.com/golang/build/tree/master/env/linux-arm64/packet",
+		DocURL: "https://github.com/golang/build/tree/master/env/linux-arm64/packet",
 		Check:  reverseHostChecker(hosts),
 	}
 }
@@ -291,7 +362,7 @@
 	return &healthChecker{
 		ID:     "osuppc64",
 		Title:  "OSU linux/ppc64 machines",
-		EnvURL: "https://github.com/golang/build/tree/master/env/linux-ppc64/osuosl",
+		DocURL: "https://github.com/golang/build/tree/master/env/linux-ppc64/osuosl",
 		Check:  reverseHostChecker(hosts),
 	}
 }
@@ -305,7 +376,7 @@
 	return &healthChecker{
 		ID:     "osuppc64le",
 		Title:  "OSU linux/ppc64le machines",
-		EnvURL: "https://github.com/golang/build/tree/master/env/linux-ppc64le/osuosl",
+		DocURL: "https://github.com/golang/build/tree/master/env/linux-ppc64le/osuosl",
 		Check:  reverseHostChecker(hosts),
 	}
 }
@@ -388,8 +459,8 @@
 			return
 		}
 		fmt.Fprintf(w, "# %q status: %s\n", hc.ID, hc.Title)
-		if hc.EnvURL != "" {
-			fmt.Fprintf(w, "# Notes: %v\n", hc.EnvURL)
+		if hc.DocURL != "" {
+			fmt.Fprintf(w, "# Notes: %v\n", hc.DocURL)
 		}
 		for _, v := range cw.Out {
 			fmt.Fprintf(w, "%s: %s\n", v.Level, v.Text)
@@ -556,7 +627,7 @@
 
 <h2 id=health>Health <a href='#health'>¶</a></h2>
 <ul>{{range .HealthCheckers}}
-  <li><a href="/status/{{.ID}}">{{.Title}}</a>{{if .EnvURL}} [<a href="{{.EnvURL}}">docs</a>]{{end -}}: {{with .DoCheck.Out}}
+  <li><a href="/status/{{.ID}}">{{.Title}}</a>{{if .DocURL}} [<a href="{{.DocURL}}">docs</a>]{{end -}}: {{with .DoCheck.Out}}
       <ul>
         {{- range .}}
           <li>{{ .AsHTML}}</li>