all: add buildlet reverse mode

In -reverse mode, the buildlet dials the coordinator. The connection
is then turned around so the coordinator can control the buildlet.
This lets us start buildlets on machines without APIs behind
firewalls.

Also add the -mode flag to the coordinator, which defaults to dev
mode when running off GCE. In prod mode the coordinator attempts to
become farmer.golang.org and pick up its real certificates. In the
new mode, builtin certificates are used allowing you to start a
local coordinator and buildlet for testing.

A simple connection test will be in a followup CL, as soon as key
checking is implemented.

Change-Id: I2a7dcdfbb4efda71df31b571788945e9ce1f3365
Reviewed-on: https://go-review.googlesource.com/8490
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
David Crawshaw 2015-04-06 08:09:20 -04:00
Родитель 9d86d3da60
Коммит 581ddd17e2
6 изменённых файлов: 429 добавлений и 2 удалений

Просмотреть файл

@ -8,6 +8,7 @@ package buildlet // import "golang.org/x/build/buildlet"
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
@ -62,6 +63,11 @@ func (c *Client) SetDescription(v string) {
c.desc = v
}
// SetHTTPClient replaces the underlying HTTP client.
func (c *Client) SetHTTPClient(httpClient *http.Client) {
c.httpClient = httpClient
}
// defaultDialer returns the net/http package's default Dial function.
// Notably, this sets TCP keep-alive values, so when we kill VMs
// (whose TCP stacks stop replying, forever), we don't leak file
@ -341,6 +347,40 @@ func (c *Client) DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) e
return retErr
}
// Info provides configuration information about the buildlet.
//
// A coordinator can use the provided information to decide what, if anything,
// to do with a buildlet.
type Info struct {
Version int // buildlet version, coordinator rejects any value less than 1.
Targets []string // list of "goos-goarch[-extra]"
}
// Info returns an Info value describing this buildlet.
func (c *Client) Info() (Info, error) {
req, err := http.NewRequest("GET", c.URL()+"/info", nil)
if err != nil {
return Info{}, err
}
resp, err := c.do(req)
if err != nil {
return Info{}, err
}
if resp.StatusCode != http.StatusOK {
return Info{}, errors.New(resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return Info{}, err
}
var info Info
if err := json.Unmarshal(b, &info); err != nil {
return Info{}, err
}
return info, nil
}
// WorkDir returns the absolute path to the buildlet work directory.
func (c *Client) WorkDir() (string, error) {
req, err := http.NewRequest("GET", c.URL()+"/workdir", nil)

Просмотреть файл

@ -18,6 +18,7 @@ import (
"compress/gzip"
"crypto/sha1"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
@ -36,13 +37,16 @@ import (
"strings"
"time"
"golang.org/x/build/buildlet"
"google.golang.org/cloud/compute/metadata"
)
var (
haltEntireOS = flag.Bool("halt", true, "halt OS in /halt handler. If false, the buildlet process just ends.")
workDir = flag.String("workdir", "", "Temporary directory to use. The contents of this directory may be deleted at any time. If empty, TempDir is used to create one.")
listenAddr = flag.String("listen", defaultListenAddr(), "address to listen on. Warning: this service is inherently insecure and offers no protection of its own. Do not expose this port to the world.")
listenAddr = flag.String("listen", defaultListenAddr(), "address to listen on. Unused in reverse mode. Warning: this service is inherently insecure and offers no protection of its own. Do not expose this port to the world.")
reverse = flag.Bool("reverse", false, "instead of listening for a coordinator, work in reverse. The buildlet dials the coordinator and awaits instructions.")
coordinator = flag.String("coordinator", "localhost:8119", "address of coordinator, in production use farmer.golang.org. Only used in reverse mode.")
)
func defaultListenAddr() string {
@ -119,8 +123,17 @@ func main() {
http.Handle("/tgz", requireAuth(handleGetTGZ))
http.Handle("/removeall", requireAuth(handleRemoveAll))
http.Handle("/workdir", requireAuth(handleWorkDir))
http.Handle("/info", requireAuth(handleInfo))
http.Handle("/ls", requireAuth(handleLs))
if *reverse {
dialCoordinator()
} else {
listenForCoordinator()
}
}
func listenForCoordinator() {
tlsCert, tlsKey := metadataValue("tls-cert"), metadataValue("tls-key")
if (tlsCert == "") != (tlsKey == "") {
log.Fatalf("tls-cert and tls-key must both be supplied, or neither.")
@ -782,6 +795,27 @@ func handleWorkDir(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, *workDir)
}
func handleInfo(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "requires GET method", http.StatusBadRequest)
return
}
// TODO(crawshaw): make targets configurable
info := buildlet.Info{
Version: 1,
Targets: []string{
runtime.GOOS + "-" + runtime.GOARCH,
},
}
b, err := json.Marshal(info)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(b)
}
func handleLs(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "requires GET method", http.StatusBadRequest)

142
cmd/buildlet/reverse.go Normal file
Просмотреть файл

@ -0,0 +1,142 @@
package main
import (
"bufio"
"crypto/tls"
"crypto/x509"
"log"
"net"
"net/http"
"strings"
)
func dialCoordinator() {
caCert := testCoordinatorCA
addr := *coordinator
if strings.HasPrefix(addr, "farmer.golang.org") {
if addr == "farmer.golang.org" {
addr = "farmer.golang.org:443"
}
caCert = coordinatorCA
}
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM([]byte(caCert)) {
log.Fatal("failed to append coordinator CA certificate")
}
log.Printf("Dialing coordinator %s...", addr)
tcpConn, err := net.Dial("tcp", addr)
if err != nil {
log.Fatalf("Could not dial %s: %v", addr, err)
}
config := &tls.Config{
ServerName: "go",
RootCAs: caPool,
}
conn := tls.Client(tcpConn, config)
if err := conn.Handshake(); err != nil {
log.Fatalf("failed to handshake with coordinator: %v", err)
}
bufr := bufio.NewReader(conn)
// TODO(crawshaw): include build key as part of initial request.
req, err := http.NewRequest("GET", "/reverse", nil)
if err != nil {
log.Fatal(err)
}
if err := req.Write(conn); err != nil {
log.Fatalf("coordinator /reverse request failed: %v", err)
}
if _, err := http.ReadResponse(bufr, req); err != nil {
log.Fatalf("coordinator /reverse response failed: %v", err)
}
// The client becomes the simple http server.
log.Printf("Connected to coordinator, serving HTTP back at them.")
srv := &http.Server{}
srv.Serve(newReverseListener(conn))
}
func newReverseListener(c net.Conn) net.Listener {
rl := &reverseListener{c: c}
return rl
}
// reverseListener serves out a single underlying conn, once.
// After that it blocks. A one-connection, boring net.Listener.
type reverseListener struct {
c net.Conn
}
func (rl *reverseListener) Accept() (net.Conn, error) {
if rl.c != nil {
c := rl.c
rl.c = nil
return c, nil
}
// TODO(crawshaw): return error when the connection is closed and
// make sure the function calling srv.Serve redials the communicator.
select {}
}
func (rl *reverseListener) Close() error { return nil }
func (rl *reverseListener) Addr() net.Addr { return reverseAddr("buildlet") }
// reverseAddr implements net.Addr for reverseListener.
type reverseAddr string
func (a reverseAddr) Network() string { return "reverse" }
func (a reverseAddr) String() string { return "reverse:" + string(a) }
/*
Certificate authority and the coordinator SSL key were created with:
openssl genrsa -out ca_key.pem 2048
openssl req -x509 -new -key ca_key.pem -out ca_cert.pem -days 1068 -subj /CN="go"
openssl genrsa -out key.pem 2048
openssl req -new -out cert_req.pem -key key.pem -subj /CN="go"
openssl x509 -req -in cert_req.pem -out cert.pem -CAkey ca_key.pem -CA ca_cert.pem -days 730 -CAcreateserial -CAserial serial
*/
// coordinatorCA is the production CA cert for farmer.golang.org.
const coordinatorCA = `-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIJANl4KOv9Cj4UMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNV
BAMTAmdvMB4XDTE1MDQwNTIwMTE0OFoXDTE4MDMwODIwMTE0OFowDTELMAkGA1UE
AxMCZ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ/oLb+ksvNScl
zIweMGv2ZWRdWW3o9vWIMpOhkiYuBOZjp7zvs89OuKNdC1ylJs3ENnNtD8QOG1Ze
kM3s6MTjCLVZUX4218HAenGifaunTNfbW1/q/tTnZh4Kri00vgq9jFtYnlqFLYhT
PlmDMdpgOY4ligc/1bSPWVsI7CKCbh3fAz67m++opVE0M7LFp8bhkyFv/dnhZFxo
s9ei3ZKFLjYJdZUNRMZ+HcqBzXMQR7HeCOD2pZ1yoHJw1b3Ebe4YOcQCHq4moW7W
DavISKSXl7DKZYX1QlFUmEMkl5aMIEHUJ0oI2wnL9+u5s1NU2/k8sSxbH7Y/cKio
cFPwuMt7AgMBAAGjbjBsMB0GA1UdDgQWBBS5f/j+8YL9B8THnoAXIhQty3vDZjA9
BgNVHSMENjA0gBS5f/j+8YL9B8THnoAXIhQty3vDZqERpA8wDTELMAkGA1UEAxMC
Z2+CCQDZeCjr/Qo+FDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBU
EOOl2ChJyxFg8b4OrG/EC0HMxic2CakRsj6GWQlAwNU8+3o2u2+zYqKhuREDazsZ
1+0f54iU4TXPgPLiOVLQT8AOM6BDDeZfugAopAf0QaIXW5AmM5hnkhW035aXZgx9
rYageMGnnkK2H7E7WlcFbGcPjZtbpZyFnGoAvxcUfOzdnm/LLuvFg6YWf1ynXsNI
aOx5LNVDhzcQlHZ26ueOLoyIpTQxqvo+hwmIOVDLlZ9bz2BS6FevFjsciJmcDL8N
cmY1/5cC/4NzpnN95cvZxp3FX8Ka7YFun03ubjXzXttoeyrxP2WFXuc2D2hkTJPE
Co9z2+Nue1JHG9JcDaeW
-----END CERTIFICATE-----`
// testCoordinatorCA is a dev mode cert, not used in production.
const testCoordinatorCA = `-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIJAPvaWgVSI9PaMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNV
BAMTAmdvMB4XDTE1MDQwNjAzMTc0MVoXDTE4MDMwOTAzMTc0MVowDTELMAkGA1UE
AxMCZ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ6t6PGkTk5CnR
+ZVkHq8w9VgDutnTIED3fWQLZLlc7oyexY4wLqmB/fYxINtmtWg7tUon8Y6SMPBF
51bam7qc69iWYuSUVkhHcQSGYM/OUKXmtl5V2W9HqfHT+Kcqi8Vm2E946LPMCtKJ
JUuzSYYLkXFl8JZw0bi8CROZ23LY7FTZTK/lGUun65bDCTB9AuB/BlclBBtT7pDg
6hSc73tMDWRZZ2c4rY0LXYgqbW9Zs0E8ePrKjHGFKxwQlDu0EKhjN/v6HWwq4qXD
Zlcx8tiPdFIpUOPN5SkpJq80XiDLy1Cqxxc0gdbM1uxIxYwNzlJqwybVqx8E9H/E
y4NAdg0xAgMBAAGjbjBsMB0GA1UdDgQWBBSXjKSDNj0jnlgUsb7lQU6K7CvUGjA9
BgNVHSMENjA0gBSXjKSDNj0jnlgUsb7lQU6K7CvUGqERpA8wDTELMAkGA1UEAxMC
Z2+CCQD72loFUiPT2jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCl
YGLMKAAXgqr4Wj3sCOHfzeZR7fD0ngJ45eP08woXyc6Lg+2kcaOjNVIQ7k91XacP
XeoWexeVnaNNxc0B3uWGqy54AF+6ZuJ8Ybtm3KiFrjYd4iuvQUS4wYYh8Iu83chX
TjB7sEliFX8+KNSWONw3vULfggMugyTnRilW8qOWd0Xx729NlsvC+OFJc2RVkGoq
bmE4LZKjOf0SAh32d1Ye4hH1lPjWkGnVtXiBZbtqk9Ctc1bn6Vq2UxsE/BbZHBlc
0iKSFmwBiTqOyCs9q9Hpb012HqZYV+4CMBDsR21yAtecSuY8Rse9Vc+POuyRuY25
oObGb36g+BHVuGJxjbFo
-----END CERTIFICATE-----`

Просмотреть файл

@ -21,6 +21,7 @@ import (
"log"
"net"
"net/http"
"os"
"os/exec"
"path"
"runtime"
@ -52,6 +53,8 @@ var (
cleanZones = flag.String("zones", "us-central1-a,us-central1-b,us-central1-f", "Comma-separated list of zones to periodically clean of stale build VMs (ones that failed to shut themselves down)")
buildLogBucket = flag.String("logbucket", "go-build-log", "GCS bucket to put trybot build failures in")
mode = flag.String("mode", "", "valid modes are 'dev', 'prod', or '' for auto-detect")
)
// LOCK ORDER:
@ -108,6 +111,18 @@ const (
)
func readGCSFile(name string) ([]byte, error) {
if *mode == "dev" {
b, ok := testFiles[name]
if !ok {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
return []byte(b), nil
}
r, err := storage.NewReader(serviceCtx, "go-builder-data", name)
if err != nil {
return nil, err
@ -116,6 +131,53 @@ func readGCSFile(name string) ([]byte, error) {
return ioutil.ReadAll(r)
}
// Fake keys signed by a fake CA.
var testFiles = map[string]string{
"farmer-cert.pem": `-----BEGIN CERTIFICATE-----
MIICljCCAX4CCQCoS+/smvkG2TANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDEwJn
bzAeFw0xNTA0MDYwMzE3NDJaFw0xNzA0MDUwMzE3NDJaMA0xCzAJBgNVBAMTAmdv
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1NMaVxX8RfCMtQB18azV
hL6/U7C8W2G+8WXYeFuOpgP2SHnMbsUeTiUYWS1xqAxUh3Vl/TT1HIASRDL7kBis
yj+drspafnCr4Yp9oJx1xlIhVXGD/SyHk5oewkjkNEmrFtUT07mT2lmZqD3XJ+6V
aQslRxhPEkLGsXIA/hCucPIplI9jgLY8TmOBhQ7RzXAnk/ayAzDkCgkWB4k/zaFy
LiHjEkE7O7PIjjY51btCLep9QSts98zojY5oYNj2RdQOZa56MHAlh9hbdpm+P1vp
2QBpsDbVpHYv2VPCPvkdOGU1/nzumsxHy17DcirKP8Tuf6zMf9obeuSlMvUUPptl
hwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBxvUMKsX+DEhZSmc164IuSVJ9ucZ97
+KWn4nCwnVkI/RrsJpiTj3pZNRkAxq2vmZTpUdU0CgGHdZNXp/6s/GX4cSzFphSf
WZQN0CG/O50SQ39m7fz/dZ2Xse6EH2grr6KN0QsDhK/RVxecQv57rY9nLFHnC60t
vJBDC739lWlnsGDxylJNxEk2l5c2rJdn82yGw2G9pQ/LDVAtO1G2rxGkpi4FcpGk
rNAa6MiwcyFHcAr3OsigLm4Q9bCS6YXfQDvCZGAR91ADXVWDFC1sgBgM3U3+1bGp
tgXUVKymUvoVq0BiY4BCCYDluoErgZDytLmnUOxrykYi532VpRbbK2ja
-----END CERTIFICATE-----`,
"farmer-key.pem": `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA1NMaVxX8RfCMtQB18azVhL6/U7C8W2G+8WXYeFuOpgP2SHnM
bsUeTiUYWS1xqAxUh3Vl/TT1HIASRDL7kBisyj+drspafnCr4Yp9oJx1xlIhVXGD
/SyHk5oewkjkNEmrFtUT07mT2lmZqD3XJ+6VaQslRxhPEkLGsXIA/hCucPIplI9j
gLY8TmOBhQ7RzXAnk/ayAzDkCgkWB4k/zaFyLiHjEkE7O7PIjjY51btCLep9QSts
98zojY5oYNj2RdQOZa56MHAlh9hbdpm+P1vp2QBpsDbVpHYv2VPCPvkdOGU1/nzu
msxHy17DcirKP8Tuf6zMf9obeuSlMvUUPptlhwIDAQABAoIBAAJOPyzOWitPzdZw
KNbzbmS/xEbd1UyQJIds+QlkxIjb5iEm4KYakJd8I2Vj7qVJbOkCxpYVqsoiQRBo
FP2cptKSGd045/4SrmoFHBNPXp9FaIMKdcmaX+Wjd83XCFHgsm/O4yYaDpYA/n8q
HFicZxX6Pu8kPkcOXiSx/XzDJYCnuec0GIfiJfbrQEwNLA+Ck2HnFfLy6LyrgCqi
eqaxyBoLolzjW7guWV6e/ECsnLXx2n/Pj4l1aqIFKlYxOjBIKRqeUsqzMFpOCbrx
z/scaBuH88hO96jbGZWUAm3R6ZslocQ6TaENYWNVKN1SeGISiE3hRoMAUIu1eHVu
mEzOjvECgYEA9Ypu04NzVjAHdZRwrP7IiX3+CmbyNatdZXIoagp8boPBYWw7QeL8
TPwvc3PCSIjxcT+Jv2hHTZ9Ofz9vAm/XJx6Ios9o/uAbytA+RAolQJWtLGuFLKv1
wxq78iDFcIWq3iPwpl8FJaXeCb/bsNP9jruPhwWWbJVvD1eTif09ZzsCgYEA3ePo
aQ5S0YrPtaf5r70eSBloe5vveG/kW3EW0QMrN6YlOhGSX+mjdAJk7XI/JW6vVPYS
aK+g+ZnzV7HL421McuVH8mmwPHi48l5o2FewF54qYfOoTAJS1cjV08j8WtQsrEax
HHom4m4joQEm0o4QEnTxJDS8/u7T/hhMALxeziUCgYANwevjvgHAWoCQffiyOLRT
v9N0EcCQcUGSZYsOJfhC2O8E3mOTlXw9dAPUnC/OkJ22krDNILKeDsb/Kja2FD4h
2vwc4zIm1be47WIPveHIdJp3Wq7jid8DR4QwVNW7MEIaoDjjmX9YVKrUMQPGLJqQ
XMH19sIu41CNs4J4wM+n8QKBgBiIcFPdP47neBuvnM2vbT+vf3vbO9jnFip+EHW/
kfGvLwKCmtp77JSRBzOxpAWxfTU5l8N3V6cBPIR/pflZRlCVxSSqRtAI0PoLMjBp
UZDq7eiylfMBdsMoV2v5Ft28A8xwbHinkNEMOGg+xloVVvWTdG36XsMZCNtZOF4E
db75AoGBAIk6IW5O2lk9Vc537TCyLpl2HYCP0jI3v6xIkFFolnfHPEgsXLJo9YU8
crVtB0zy4jzjN/SClc/iaeOzk5Ot+iwSRFBZu2jdt0TRxbG+cd+6vKLs0Baw6kB1
gpRUwP6i5yhi838rMgurGVFr3O/0Sv7wMx5UNEJ/RopbQ2K/bnwn
-----END RSA PRIVATE KEY-----`,
}
func listenAndServeTLS() {
certPEM, err := readGCSFile("farmer-cert.pem")
if err != nil {
@ -134,6 +196,9 @@ func listenAndServeTLS() {
}
server := &http.Server{Addr: ":443"}
if *mode == "dev" {
server.Addr = ":8119"
}
config := &tls.Config{
NextProtos: []string{"http/1.1"},
Certificates: []tls.Certificate{cert},
@ -166,15 +231,33 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
func main() {
flag.Parse()
if err := initGCE(); err != nil {
err := initGCE()
if err != nil {
if *mode == "" {
*mode = "dev"
}
log.Printf("VM support disabled due to error initializing GCE: %v", err)
} else {
if *mode == "" {
*mode = "prod"
}
}
switch *mode {
case "dev", "prod":
log.Printf("Running in %s mode", *mode)
default:
log.Fatalf("Unknown mode: %q", *mode)
}
http.HandleFunc("/", handleStatus)
http.HandleFunc("/try", handleTryStatus)
http.HandleFunc("/logs", handleLogs)
http.HandleFunc("/debug/goroutines", handleDebugGoroutines)
http.HandleFunc("/reverse", handleReverse)
go func() {
if *mode == "dev" {
return
}
err := http.ListenAndServe(":80", nil)
if err != nil {
log.Fatalf("http.ListenAndServe:80: %v", err)
@ -182,6 +265,11 @@ func main() {
}()
go listenAndServeTLS()
if *mode == "dev" {
// TODO(crawshaw): do more in test mode
select {}
}
go gcePool.cleanUpOldVMs()
// Start the Docker processes on this host polling Gerrit and

Просмотреть файл

@ -225,6 +225,9 @@ func (p *gceBuildletPool) instanceUsed(instName string) bool {
// "delete-at" metadata attribute on them when created to some time
// that's well beyond their expected lifetime.
func (p *gceBuildletPool) cleanUpOldVMs() {
if *mode == "dev" {
return
}
if computeService == nil {
return
}

120
cmd/coordinator/reverse.go Normal file
Просмотреть файл

@ -0,0 +1,120 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"fmt"
"io"
"log"
"net/http"
"sync"
"golang.org/x/build/buildlet"
)
const minBuildletVersion = 1
var reversePool = &reverseBuildletPool{}
type reverseBuildletPool struct {
mu sync.Mutex
buildlets []reverseBuildlet
}
type reverseBuildlet struct {
info buildlet.Info
client *buildlet.Client
}
func handleReverse(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
http.Error(w, "buildlet registration requires SSL", http.StatusInternalServerError)
return
}
// TODO(crawshaw): check key
conn, bufrw, err := w.(http.Hijacker).Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Registering reverse buildlet %s", r.RemoteAddr)
// The server becomes a (very simple) http client.
(&http.Response{Status: "200 OK", Proto: "HTTP/1.1"}).Write(conn)
client := buildlet.NewClient("none", buildlet.NoKeyPair)
client.SetHTTPClient(&http.Client{
Transport: newRoundTripper(bufrw),
})
info, err := client.Info()
if err != nil {
log.Printf("Reverse connection did not answer /info: %v", err)
conn.Close()
return
}
if info.Version < minBuildletVersion {
log.Printf("Buildlet too old: %s, %+v", r.RemoteAddr, info)
conn.Close()
return
}
log.Printf("Buildlet %s: %+v", r.RemoteAddr, info)
// TODO(crawshaw): register buildlet with pool, pass conn
// TODO(crawshaw): add connection test
select {}
}
func newRoundTripper(bufrw *bufio.ReadWriter) *reverseRoundTripper {
return &reverseRoundTripper{
bufrw: bufrw,
sema: make(chan bool, 1),
}
}
// reverseRoundTripper is an http client that serializes all requests
// over a *bufio.ReadWriter.
//
// Attempts at concurrent requests return an error.
type reverseRoundTripper struct {
bufrw *bufio.ReadWriter
sema chan bool
}
func (c *reverseRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
select {
case c.sema <- true:
default:
return nil, fmt.Errorf("reverseRoundTripper: line busy")
}
if err := req.Write(c.bufrw); err != nil {
return nil, err
}
if err := c.bufrw.Flush(); err != nil {
return nil, err
}
resp, err = http.ReadResponse(c.bufrw.Reader, req)
if err != nil {
return nil, err
}
resp.Body = &reverseLockedBody{resp.Body, c.sema}
return resp, err
}
type reverseLockedBody struct {
body io.ReadCloser
sema chan bool
}
func (b *reverseLockedBody) Read(p []byte) (n int, err error) {
return b.body.Read(p)
}
func (b *reverseLockedBody) Close() error {
err := b.body.Close()
<-b.sema
b.body = nil // prevent double close
return err
}