cmd/gitmirror: get key from GCE metadata, add a k8s resource controller

Updates golang/go#18817

Change-Id: Iff09e580d720ab1dfc8de0da2c45340467a335db
Reviewed-on: https://go-review.googlesource.com/36473
Reviewed-by: Kevin Burke <kev@inburke.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/gitmirror/gitmirror.go b/cmd/gitmirror/gitmirror.go
index 68dcaaf..7f09e3d 100644
--- a/cmd/gitmirror/gitmirror.go
+++ b/cmd/gitmirror/gitmirror.go
@@ -33,6 +33,8 @@
 	"strings"
 	"sync"
 	"time"
+
+	"cloud.google.com/go/compute/metadata"
 )
 
 const (
@@ -48,7 +50,7 @@
 
 	repoURL  = flag.String("repo", goBase+"go", "main Repository URL (but subrepos are also mirrored)") // TODO: delete this?
 	dashFlag = flag.String("dash", "https://build.golang.org/", "Dashboard URL (must end in /)")
-	keyFile  = flag.String("key", defaultKeyFile, "Build dashboard key file") // TODO: make automatic from metadata/k8s secrets
+	keyFile  = flag.String("key", defaultKeyFile, "Build dashboard key file. If empty, automatic from GCE project metadata")
 
 	pollInterval = flag.Duration("poll", 10*time.Second, "Remote repo poll interval")
 
@@ -63,11 +65,15 @@
 )
 
 var (
-	defaultKeyFile = filepath.Join(homeDir(), ".gobuildkey") // TODO: use k8s secrets or GCE proj metadata
+	defaultKeyFile = filepath.Join(homeDir(), ".gobuildkey")
 	dashboardKey   = ""
 	networkSeen    = make(map[string]bool) // testing mode only (-network=false); known hashes
 )
 
+var httpClient = &http.Client{
+	Timeout: 30 * time.Second, // overkill
+}
+
 func main() {
 	flag.Parse()
 	log.Printf("gitmirror running.")
@@ -114,6 +120,7 @@
 	}
 
 	if *httpAddr != "" {
+		http.HandleFunc("/debug/goroutines", handleDebugGoroutines)
 		ln, err := net.Listen("tcp", *httpAddr)
 		if err != nil {
 			return err
@@ -219,7 +226,7 @@
 		return true
 	}
 	// Else, see if it appears to be a subrepo:
-	r, err := http.Get("https://golang.org/x/" + name)
+	r, err := httpClient.Get("https://golang.org/x/" + name)
 	if err != nil {
 		log.Printf("repo %v doesn't seem to exist: %v", name, err)
 		return false
@@ -778,7 +785,7 @@
 	}
 	v := url.Values{"hash": {hash}, "packagePath": {r.path}}
 	u := *dashFlag + "commit?" + v.Encode()
-	resp, err := http.Get(u)
+	resp, err := httpClient.Get(u)
 	if err != nil {
 		return false, err
 	}
@@ -985,7 +992,14 @@
 				pushRefs = append(pushRefs, ref)
 			}
 		}
-		sort.Sort(refByPriority(pushRefs))
+		sort.Slice(pushRefs, func(i, j int) bool {
+			p1 := priority[refType(pushRefs[i])]
+			p2 := priority[refType(pushRefs[j])]
+			if p1 != p2 {
+				return p1 > p2
+			}
+			return pushRefs[i] <= pushRefs[j]
+		})
 		if len(pushRefs) == 0 {
 			r.setStatus("nothing to sync")
 			return nil
@@ -1133,6 +1147,13 @@
 
 func readKey() (string, error) {
 	c, err := ioutil.ReadFile(*keyFile)
+	if os.IsNotExist(err) && metadata.OnGCE() {
+		key, err := metadata.ProjectAttributeValue("builder-master-key")
+		if err != nil {
+			return "", fmt.Errorf("-key=%s doesn't exist, and key can't be loaded from GCE metadata: %v", err)
+		}
+		return strings.TrimSpace(key), nil
+	}
 	if err != nil {
 		return "", err
 	}
@@ -1147,8 +1168,7 @@
 		return nil, nil
 	}
 
-	// TODO: timeout on all http requests.
-	r, err := http.Get(*dashFlag + "packages?kind=subrepo")
+	r, err := httpClient.Get(*dashFlag + "packages?kind=subrepo")
 	if err != nil {
 		return nil, fmt.Errorf("subrepo list: %v", err)
 	}
@@ -1217,8 +1237,7 @@
 // latest master hash.
 // The returned map is nil on any transient error.
 func gerritMetaMap() map[string]string {
-	// TODO: timeout
-	res, err := http.Get(metaURL)
+	res, err := httpClient.Get(metaURL)
 	if err != nil {
 		return nil
 	}
@@ -1294,19 +1313,6 @@
 	return refHash, bs.Err()
 }
 
-type refByPriority []string
-
-func (s refByPriority) Len() int      { return len(s) }
-func (s refByPriority) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s refByPriority) Less(i, j int) bool {
-	p1 := priority[refType(s[i])]
-	p2 := priority[refType(s[j])]
-	if p1 != p2 {
-		return p1 > p2
-	}
-	return s[i] <= s[j]
-}
-
 func refType(s string) string {
 	s = strings.TrimPrefix(s, "refs/")
 	if i := strings.IndexByte(s, '/'); i != -1 {
@@ -1320,3 +1326,9 @@
 	"tags":    4,
 	"changes": 3,
 }
+
+func handleDebugGoroutines(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	buf := make([]byte, 1<<20)
+	w.Write(buf[:runtime.Stack(buf, true)])
+}
diff --git a/cmd/gitmirror/rc.yaml b/cmd/gitmirror/rc.yaml
new file mode 100644
index 0000000..41aacb2
--- /dev/null
+++ b/cmd/gitmirror/rc.yaml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: ReplicationController
+metadata:
+  name: gitmirror
+spec:
+  replicas: 1
+  selector:
+    app: gitmirror
+  template:
+    metadata:
+      name: gitmirror
+      labels:
+        app: gitmirror
+    spec:
+      containers:
+      - name: gitmirror
+        image: gcr.io/symbolic-datum-552/gitmirror:latest
+        command: ["/go/bin/gitmirror", "-http=:8585", "-mirror=false", "-report=false", "-network=false"]
+        ports:
+        - containerPort: 8585
+        resources:
+          requests:
+            cpu: "1"
+            memory: "2Gi"
+          limits:
+            cpu: "2"
+            memory: "4Gi"