cmd/gitmirror: cleanups & more Kubernetes work
It's now running, but not hooked up the coordinator yet.
Updates golang/go#18817
Change-Id: I5870af1e0bfe5213886f7faeb138127986a7234c
Reviewed-on: https://go-review.googlesource.com/36801
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/gitmirror/Dockerfile b/cmd/gitmirror/Dockerfile
index d68b5c7..7c96e0e 100644
--- a/cmd/gitmirror/Dockerfile
+++ b/cmd/gitmirror/Dockerfile
@@ -2,7 +2,9 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
-FROM golang:1.8-wheezy
+# Note that OpenSSH 6.5+ is required for the Github SSH private key, which requires
+# at least Debian Jessie (not Wheezy). This uses Jessie:
+FROM golang:1.8-rc
LABEL maintainer "golang-dev@googlegroups.com"
# todo(bradfitz): remove duplication between this and coordinator.
diff --git a/cmd/gitmirror/gitmirror.go b/cmd/gitmirror/gitmirror.go
index 7f09e3d..322b6a0 100644
--- a/cmd/gitmirror/gitmirror.go
+++ b/cmd/gitmirror/gitmirror.go
@@ -46,9 +46,8 @@
var (
httpAddr = flag.String("http", "", "If non-empty, the listen address to run an HTTP server on")
- cacheDir = flag.String("cachedir", "/var/cache/git-mirror", "git cache directory. If empty a temp directory is made.")
+ cacheDir = flag.String("cachedir", "", "git cache directory. If empty a temp directory is made.")
- 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. If empty, automatic from GCE project metadata")
@@ -78,10 +77,6 @@
flag.Parse()
log.Printf("gitmirror running.")
- if err := os.MkdirAll(*cacheDir, 0755); err != nil {
- log.Fatalf("Failed to created watcher's git cache dir: %v", err)
- }
-
go pollGerritAndTickle()
err := runGitMirror()
log.Fatalf("gitmirror exiting after failure: %v", err)
@@ -94,6 +89,26 @@
return errors.New("dashboard URL (-dashboard) must end in /")
}
+ if *mirror {
+ sshDir := filepath.Join(homeDir(), ".ssh")
+ sshKey := filepath.Join(sshDir, "id_ed25519")
+ if _, err := os.Stat(sshKey); err == nil {
+ log.Printf("Using github ssh key at %v", sshKey)
+ } else {
+ if privKey, err := metadata.ProjectAttributeValue("github-ssh"); err == nil && len(privKey) > 0 {
+ if err := os.MkdirAll(sshDir, 0700); err != nil {
+ return err
+ }
+ if err := ioutil.WriteFile(sshKey, []byte(privKey+"\n"), 0600); err != nil {
+ return err
+ }
+ log.Printf("Wrote %s from GCE metadata.", sshKey)
+ } else {
+ return fmt.Errorf("Can't mirror to github without 'github-ssh' GCE metadata or file %v", sshKey)
+ }
+ }
+ }
+
if *cacheDir == "" {
dir, err := ioutil.TempDir("", "gitmirror")
if err != nil {
@@ -103,11 +118,17 @@
*cacheDir = dir
} else {
fi, err := os.Stat(*cacheDir)
- if err != nil {
- return fmt.Errorf("invalid -cachedir: %v", err)
- }
- if !fi.IsDir() {
- return fmt.Errorf("invalid -cachedir=%q; not a directory", *cacheDir)
+ if os.IsNotExist(err) {
+ if err := os.MkdirAll(*cacheDir, 0755); err != nil {
+ return fmt.Errorf("failed to create watcher's git cache dir: %v", err)
+ }
+ } else {
+ if err != nil {
+ return fmt.Errorf("invalid -cachedir: %v", err)
+ }
+ if !fi.IsDir() {
+ return fmt.Errorf("invalid -cachedir=%q; not a directory", *cacheDir)
+ }
}
}
@@ -120,6 +141,7 @@
}
if *httpAddr != "" {
+ http.HandleFunc("/debug/env", handleDebugEnv)
http.HandleFunc("/debug/goroutines", handleDebugGoroutines)
ln, err := net.Listen("tcp", *httpAddr)
if err != nil {
@@ -129,29 +151,13 @@
}
errc := make(chan error)
- dir := *cacheDir
- go func() {
- dst := ""
- if *mirror {
- name := (*repoURL)[strings.LastIndex(*repoURL, "/")+1:]
- dst = "git@github.com:golang/" + name + ".git"
- }
- name := strings.TrimPrefix(*repoURL, goBase)
- r, err := NewRepo(dir, *repoURL, dst, "", true)
- if err != nil {
- errc <- err
- return
- }
- http.Handle("/"+name+".tar.gz", r)
- errc <- r.Watch()
- }()
subrepos, err := subrepoList()
if err != nil {
return err
}
- start := func(name, path string, dash bool) {
+ startRepo := func(name, path string, dash bool) {
log.Printf("Starting watch of repo %s", name)
url := goBase + name
var dst string
@@ -163,20 +169,26 @@
log.Printf("Not mirroring repo %s", name)
}
}
- r, err := NewRepo(dir, url, dst, path, dash)
+ r, err := NewRepo(url, dst, path, dash)
if err != nil {
errc <- err
return
}
http.Handle("/"+name+".tar.gz", r)
- errc <- r.Watch()
+ reposMu.Lock()
+ repos = append(repos, r)
+ sort.Slice(repos, func(i, j int) bool { return repos[i].name() < repos[j].name() })
+ reposMu.Unlock()
+ r.Loop()
}
+ go startRepo("go", "", true)
+
seen := map[string]bool{"go": true}
for _, path := range subrepos {
name := strings.TrimPrefix(path, "golang.org/x/")
seen[name] = true
- go start(name, path, true)
+ go startRepo(name, path, true)
}
if *mirror {
for name := range gerritMetaMap() {
@@ -184,14 +196,35 @@
// Repo already picked up by dashboard list.
continue
}
- go start(name, "golang.org/x/"+name, false)
+ go startRepo(name, "golang.org/x/"+name, false)
}
}
- // Must be non-nil.
+ http.HandleFunc("/", handleRoot)
+
+ // Blocks forever if all the NewRepo calls succeed:
return <-errc
}
+var (
+ reposMu sync.Mutex
+ repos []*Repo
+)
+
+func handleRoot(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ reposMu.Lock()
+ defer reposMu.Unlock()
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprintf(w, "<html><body><pre>\a")
+ for _, r := range repos {
+ fmt.Fprintf(w, "<a href='/debug/watcher/%s'>%s</a> - %s\n", r.name(), r.name(), r.statusLine())
+ }
+}
+
// shouldReport reports whether the named repo should be mirrored from
// Gerrit to Github.
func shouldMirror(name string) bool {
@@ -278,30 +311,34 @@
// Repo represents a repository to be watched.
type Repo struct {
- root string // on-disk location of the git repo
+ root string // on-disk location of the git repo, *cacheDir/name
path string // base import path for repo (blank for main repo)
commits map[string]*Commit // keyed by full commit hash (40 lowercase hex digits)
branches map[string]*Branch // keyed by branch name, eg "release-branch.go1.3" (or empty for default)
dash bool // push new commits to the dashboard
mirror bool // push new commits to 'dest' remote
status statusRing
+
+ mu sync.Mutex
+ err error
+ firstBad time.Time
+ lastBad time.Time
+ firstGood time.Time
+ lastGood time.Time
}
-// NewRepo checks out a new instance of the Mercurial repository
-// specified by srcURL to a new directory inside dir.
+// NewRepo checks out a new instance of the git repository
+// specified by srcURL.
+//
// If dstURL is not empty, changes from the source repository will
// be mirrored to the specified destination repository.
// The importPath argument is the base import path of the repository,
// and should be empty for the main Go repo.
// The dash argument should be set true if commits to this
// repo should be reported to the build dashboard.
-func NewRepo(dir, srcURL, dstURL, importPath string, dash bool) (*Repo, error) {
- var root string
- if importPath == "" {
- root = filepath.Join(dir, "go")
- } else {
- root = filepath.Join(dir, path.Base(importPath))
- }
+func NewRepo(srcURL, dstURL, importPath string, dash bool) (*Repo, error) {
+ name := path.Base(srcURL) // "go", "net", etc
+ root := filepath.Join(*cacheDir, name)
r := &Repo{
path: importPath,
root: root,
@@ -351,11 +388,6 @@
return nil, fmt.Errorf("adding remote: %v", err)
}
r.setStatus("added dest remote")
- r.logf("starting initial push to %v", dstURL)
- if err := r.push(); err != nil {
- return nil, err
- }
- r.logf("did initial push to %v", dstURL)
}
if r.dash {
@@ -369,6 +401,52 @@
return r, nil
}
+func (r *Repo) setErr(err error) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ change := (r.err != nil) != (err != nil)
+ now := time.Now()
+ if err != nil {
+ if change {
+ r.firstBad = now
+ }
+ r.lastBad = now
+ } else {
+ if change {
+ r.firstGood = now
+ }
+ r.lastGood = now
+ }
+ r.err = err
+}
+
+var startTime = time.Now()
+
+func (r *Repo) statusLine() string {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if r.lastGood.IsZero() {
+ if r.err != nil {
+ return fmt.Sprintf("broken; permanently? always failing, for %v", time.Since(r.firstBad))
+ }
+ if time.Since(startTime) < 5*time.Minute {
+ return "ok; starting up, no report yet"
+ }
+ return fmt.Sprintf("hung; hang at start-up? no report since start %v ago", time.Since(startTime))
+ }
+ if r.err == nil {
+ if sinceGood := time.Since(r.lastGood); sinceGood > 6*time.Minute {
+ return fmt.Sprintf("hung? no activity since last success %v ago", sinceGood)
+ }
+ if r.lastBad.After(time.Now().Add(-1 * time.Hour)) {
+ return fmt.Sprintf("ok; recent failure %v ago", time.Since(r.lastBad))
+ }
+ return "ok"
+ }
+ return fmt.Sprintf("broken for %v", time.Since(r.lastGood))
+}
+
func (r *Repo) setStatus(status string) {
r.status.add(status)
}
@@ -438,24 +516,30 @@
// Watch continuously runs "git fetch" in the repo, checks for
// new commits, posts any new commits to the dashboard (if enabled),
// and mirrors commits to a destination repo (if enabled).
-// It only returns a non-nil error.
-func (r *Repo) Watch() error {
+func (r *Repo) Loop() {
tickler := repoTickler(r.name())
for {
if err := r.fetch(); err != nil {
- return err
+ r.setErr(err)
+ time.Sleep(10 * time.Second)
+ continue
}
if r.mirror {
if err := r.push(); err != nil {
- return err
+ r.setErr(err)
+ time.Sleep(10 * time.Second)
+ continue
}
}
if r.dash {
if err := r.updateDashboard(); err != nil {
- return err
+ r.setErr(err)
+ time.Sleep(10 * time.Second)
+ continue
}
}
+ r.setErr(nil)
r.setStatus("waiting")
// We still run a timer but a very slow one, just
// in case the mechanism updating the repo tickler
@@ -976,7 +1060,7 @@
r.setStatus("sync: fetching remote refs")
remote, err := r.getRemoteRefs("dest")
if err != nil {
- r.logf("failed to get local refs: %v", err)
+ r.logf("failed to get remote refs: %v", err)
return err
}
r.setStatus(fmt.Sprintf("sync: got %d remote refs", len(remote)))
@@ -1291,6 +1375,10 @@
func parseRefs(cmd *exec.Cmd) (map[string]string, error) {
refHash := map[string]string{}
+ var errBuf bytes.Buffer
+ if cmd.Stderr == nil {
+ cmd.Stderr = &errBuf
+ }
out, err := cmd.StdoutPipe()
if err != nil {
return nil, err
@@ -1308,9 +1396,9 @@
return nil, err
}
if err := cmd.Wait(); err != nil {
- return nil, err
+ return nil, fmt.Errorf("wait err: %v, stderr: %s", err, errBuf.Bytes())
}
- return refHash, bs.Err()
+ return refHash, nil
}
func refType(s string) string {
@@ -1332,3 +1420,10 @@
buf := make([]byte, 1<<20)
w.Write(buf[:runtime.Stack(buf, true)])
}
+
+func handleDebugEnv(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ for _, kv := range os.Environ() {
+ fmt.Fprintf(w, "%s\n", kv)
+ }
+}
diff --git a/cmd/gitmirror/rc.yaml b/cmd/gitmirror/rc.yaml
index 41aacb2..e8db380 100644
--- a/cmd/gitmirror/rc.yaml
+++ b/cmd/gitmirror/rc.yaml
@@ -3,7 +3,7 @@
metadata:
name: gitmirror
spec:
- replicas: 1
+ replicas: 2
selector:
app: gitmirror
template:
@@ -12,10 +12,17 @@
labels:
app: gitmirror
spec:
+ volumes:
+ - name: cache-volume
+ emptyDir:
+ medium: Memory
containers:
- name: gitmirror
image: gcr.io/symbolic-datum-552/gitmirror:latest
- command: ["/go/bin/gitmirror", "-http=:8585", "-mirror=false", "-report=false", "-network=false"]
+ command: ["/go/bin/gitmirror", "-http=:8585", "-mirror=true", "-report=true", "-network=true", "-cachedir=/cache/gitmirror"]
+ volumeMounts:
+ - mountPath: /cache
+ name: cache-volume
ports:
- containerPort: 8585
resources:
diff --git a/cmd/gitmirror/service.yaml b/cmd/gitmirror/service.yaml
new file mode 100644
index 0000000..3ce19a5
--- /dev/null
+++ b/cmd/gitmirror/service.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: gitmirror
+spec:
+ ports:
+ - port: 8585
+ targetPort: 8585
+ selector:
+ app: gitmirror