1
0
mirror of https://github.com/golang/go synced 2024-10-01 11:28:34 -06:00
go/cmd/tip/tip.go
Parker Moore 130914b004 tip: update package doc to refer to stable code
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>
2016-07-11 22:47:40 +00:00

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)
}