mirror of
https://github.com/golang/go
synced 2024-11-06 01:46:12 -07:00
130914b004
This comment was written and has not been changed since this package was created: https://www.youtube.com/watch?v=1rZ-JorHJEY It is no longer called 'tipgodoc', and it is no longer all that 'new'. This change request updates that text to reflect the current state of the 'tip' command. Change-Id: I4ce56fb9a3bd617cf92f8d53df5a2d4726085a9a Reviewed-on: https://go-review.googlesource.com/24860 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
346 lines
7.7 KiB
Go
346 lines
7.7 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by the Apache 2.0
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Command tip is the tip.golang.org server,
|
|
// serving the latest HEAD straight from the Git oven.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
repoURL = "https://go.googlesource.com/"
|
|
metaURL = "https://go.googlesource.com/?b=master&format=JSON"
|
|
startTimeout = 10 * time.Minute
|
|
)
|
|
|
|
func main() {
|
|
const k = "TIP_BUILDER"
|
|
var b Builder
|
|
switch os.Getenv(k) {
|
|
case "godoc":
|
|
b = godocBuilder{}
|
|
case "talks":
|
|
b = talksBuilder{}
|
|
default:
|
|
log.Fatalf("Unknown %v value: %q", k, os.Getenv(k))
|
|
}
|
|
|
|
p := &Proxy{builder: b}
|
|
go p.run()
|
|
http.Handle("/", httpsOnlyHandler{p})
|
|
http.HandleFunc("/_ah/health", p.serveHealthCheck)
|
|
|
|
log.Print("Starting up")
|
|
|
|
if err := http.ListenAndServe(":8080", nil); err != nil {
|
|
p.stop()
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Proxy implements the tip.golang.org server: a reverse-proxy
|
|
// that builds and runs godoc instances showing the latest docs.
|
|
type Proxy struct {
|
|
builder Builder
|
|
|
|
mu sync.Mutex // protects the followin'
|
|
proxy http.Handler
|
|
cur string // signature of gorepo+toolsrepo
|
|
cmd *exec.Cmd // live godoc instance, or nil for none
|
|
side string
|
|
hostport string // host and port of the live instance
|
|
err error
|
|
}
|
|
|
|
type Builder interface {
|
|
Signature(heads map[string]string) string
|
|
Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error)
|
|
HealthCheck(hostport string) error
|
|
}
|
|
|
|
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/_tipstatus" {
|
|
p.serveStatus(w, r)
|
|
return
|
|
}
|
|
p.mu.Lock()
|
|
proxy := p.proxy
|
|
err := p.err
|
|
p.mu.Unlock()
|
|
if proxy == nil {
|
|
s := "starting up"
|
|
if err != nil {
|
|
s = err.Error()
|
|
}
|
|
http.Error(w, s, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
proxy.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (p *Proxy) serveStatus(w http.ResponseWriter, r *http.Request) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
fmt.Fprintf(w, "side=%v\ncurrent=%v\nerror=%v\n", p.side, p.cur, p.err)
|
|
}
|
|
|
|
func (p *Proxy) serveHealthCheck(w http.ResponseWriter, r *http.Request) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
// NOTE: Status 502, 503, 504 are the only status codes that signify an unhealthy app.
|
|
// So long as this handler returns one of those codes, this instance will not be sent any requests.
|
|
if p.proxy == nil {
|
|
log.Printf("Health check: not ready")
|
|
http.Error(w, "Not ready", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
if err := p.builder.HealthCheck(p.hostport); err != nil {
|
|
log.Printf("Health check failed: %v", err)
|
|
http.Error(w, "Health check failed", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
io.WriteString(w, "ok")
|
|
}
|
|
|
|
// run runs in its own goroutine.
|
|
func (p *Proxy) run() {
|
|
p.side = "a"
|
|
for {
|
|
p.poll()
|
|
time.Sleep(30 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (p *Proxy) stop() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.cmd != nil {
|
|
p.cmd.Process.Kill()
|
|
}
|
|
}
|
|
|
|
// poll runs from the run loop goroutine.
|
|
func (p *Proxy) poll() {
|
|
heads := gerritMetaMap()
|
|
if heads == nil {
|
|
return
|
|
}
|
|
|
|
sig := p.builder.Signature(heads)
|
|
|
|
p.mu.Lock()
|
|
changes := sig != p.cur
|
|
curSide := p.side
|
|
p.cur = sig
|
|
p.mu.Unlock()
|
|
|
|
if !changes {
|
|
return
|
|
}
|
|
|
|
newSide := "b"
|
|
if curSide == "b" {
|
|
newSide = "a"
|
|
}
|
|
|
|
dir := filepath.Join(os.TempDir(), "tip", newSide)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
p.err = err
|
|
return
|
|
}
|
|
hostport := "localhost:8081"
|
|
if newSide == "b" {
|
|
hostport = "localhost:8082"
|
|
}
|
|
cmd, err := p.builder.Init(dir, hostport, heads)
|
|
if err != nil {
|
|
err = fmt.Errorf("builder.Init: %v", err)
|
|
} else {
|
|
go func() {
|
|
// TODO(adg,bradfitz): be smarter about dead processes
|
|
if err := cmd.Wait(); err != nil {
|
|
log.Printf("process in %v exited: %v", dir, err)
|
|
}
|
|
}()
|
|
err = waitReady(p.builder, hostport)
|
|
if err != nil {
|
|
cmd.Process.Kill()
|
|
err = fmt.Errorf("waitReady: %v", err)
|
|
}
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if err != nil {
|
|
log.Println(err)
|
|
p.err = err
|
|
return
|
|
}
|
|
|
|
u, err := url.Parse(fmt.Sprintf("http://%v/", hostport))
|
|
if err != nil {
|
|
err = fmt.Errorf("parsing hostport: %v", err)
|
|
log.Println(err)
|
|
p.err = err
|
|
return
|
|
}
|
|
p.proxy = httputil.NewSingleHostReverseProxy(u)
|
|
p.side = newSide
|
|
p.hostport = hostport
|
|
if p.cmd != nil {
|
|
p.cmd.Process.Kill()
|
|
}
|
|
p.cmd = cmd
|
|
}
|
|
|
|
func waitReady(b Builder, hostport string) error {
|
|
var err error
|
|
deadline := time.Now().Add(startTimeout)
|
|
for time.Now().Before(deadline) {
|
|
if err = b.HealthCheck(hostport); err == nil {
|
|
return nil
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
return fmt.Errorf("timed out waiting for process at %v: %v", hostport, err)
|
|
}
|
|
|
|
func runErr(cmd *exec.Cmd) error {
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if len(out) == 0 {
|
|
return err
|
|
}
|
|
return fmt.Errorf("%s\n%v", out, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkout(repo, hash, path string) error {
|
|
// Clone git repo if it doesn't exist.
|
|
if _, err := os.Stat(filepath.Join(path, ".git")); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return fmt.Errorf("mkdir: %v", err)
|
|
}
|
|
if err := runErr(exec.Command("git", "clone", repo, path)); err != nil {
|
|
return fmt.Errorf("clone: %v", err)
|
|
}
|
|
} else if err != nil {
|
|
return fmt.Errorf("stat .git: %v", err)
|
|
}
|
|
|
|
// Pull down changes and update to hash.
|
|
cmd := exec.Command("git", "fetch")
|
|
cmd.Dir = path
|
|
if err := runErr(cmd); err != nil {
|
|
return fmt.Errorf("fetch: %v", err)
|
|
}
|
|
cmd = exec.Command("git", "reset", "--hard", hash)
|
|
cmd.Dir = path
|
|
if err := runErr(cmd); err != nil {
|
|
return fmt.Errorf("reset: %v", err)
|
|
}
|
|
cmd = exec.Command("git", "clean", "-d", "-f", "-x")
|
|
cmd.Dir = path
|
|
if err := runErr(cmd); err != nil {
|
|
return fmt.Errorf("clean: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// gerritMetaMap returns the map from repo name (e.g. "go") to its
|
|
// latest master hash.
|
|
// The returned map is nil on any transient error.
|
|
func gerritMetaMap() map[string]string {
|
|
res, err := http.Get(metaURL)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer res.Body.Close()
|
|
defer io.Copy(ioutil.Discard, res.Body) // ensure EOF for keep-alive
|
|
if res.StatusCode != 200 {
|
|
return nil
|
|
}
|
|
var meta map[string]struct {
|
|
Branches map[string]string
|
|
}
|
|
br := bufio.NewReader(res.Body)
|
|
// For security reasons or something, this URL starts with ")]}'\n" before
|
|
// the JSON object. So ignore that.
|
|
// Shawn Pearce says it's guaranteed to always be just one line, ending in '\n'.
|
|
for {
|
|
b, err := br.ReadByte()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if b == '\n' {
|
|
break
|
|
}
|
|
}
|
|
if err := json.NewDecoder(br).Decode(&meta); err != nil {
|
|
log.Printf("JSON decoding error from %v: %s", metaURL, err)
|
|
return nil
|
|
}
|
|
m := map[string]string{}
|
|
for repo, v := range meta {
|
|
if master, ok := v.Branches["master"]; ok {
|
|
m[repo] = master
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func getOK(url string) (body []byte, err error) {
|
|
res, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, err = ioutil.ReadAll(res.Body)
|
|
res.Body.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, errors.New(res.Status)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
// httpsOnlyHandler redirects requests to "http://example.com/foo?bar"
|
|
// to "https://example.com/foo?bar"
|
|
type httpsOnlyHandler struct {
|
|
h http.Handler
|
|
}
|
|
|
|
func (h httpsOnlyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("X-Appengine-Https") == "off" {
|
|
r.URL.Scheme = "https"
|
|
r.URL.Host = r.Host
|
|
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
|
return
|
|
}
|
|
if r.Header.Get("X-Appengine-Https") == "on" {
|
|
// Only set this header when we're actually in production.
|
|
w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
|
|
}
|
|
h.h.ServeHTTP(w, r)
|
|
}
|