1
0
mirror of https://github.com/golang/go synced 2024-11-18 14:04:45 -07:00

dashboard: start of cmd/gomote buildlet client, more packification

Change-Id: I874f4f5ef253cf7f1d6d5073d7c81e76fa1de863
Reviewed-on: https://go-review.googlesource.com/2981
Reviewed-by: Andrew Gerrand <adg@golang.org>
This commit is contained in:
Brad Fitzpatrick 2015-01-16 12:59:14 -08:00
parent 3ecc311976
commit 3aad931e88
23 changed files with 904 additions and 95 deletions

49
dashboard/auth/auth.go Normal file
View File

@ -0,0 +1,49 @@
// 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.
// +build extdep
// Package auth contains shared code related to OAuth2 and obtaining
// tokens for a project.
package auth
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
func homedir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
return os.Getenv("HOME")
}
// ProjectTokenSource returns an OAuth2 TokenSource for the given Google Project ID.
func ProjectTokenSource(proj string, scopes ...string) (oauth2.TokenSource, error) {
// TODO(bradfitz): try different strategies too, like
// three-legged flow if the service account doesn't exist, and
// then cache the token file on disk somewhere. Or maybe that should be an
// option, for environments without stdin/stdout available to the user.
// We'll figure it out as needed.
fileName := filepath.Join(homedir(), "keys", proj+".key.json")
jsonConf, err := ioutil.ReadFile(fileName)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("Missing JSON key configuration. Download the Service Account JSON key from https://console.developers.google.com/project/%s/apiui/credential and place it at %s", proj, fileName)
}
return nil, err
}
conf, err := google.JWTConfigFromJSON(jsonConf, scopes...)
if err != nil {
return nil, fmt.Errorf("reading JSON config from %s: %v", fileName, err)
}
return conf.TokenSource(oauth2.NoContext), nil
}

View File

@ -18,42 +18,44 @@ import (
"strings"
)
// KeyPair is the TLS public certificate PEM file and its associated
// private key PEM file that a builder will use for its HTTPS
// server. The zero value means no HTTPs, which is used by the
// coordinator for machines running within a firewall.
type KeyPair struct {
CertPEM string
KeyPEM string
}
// NoKeyPair is used by the coordinator to speak http directly to buildlets,
// inside their firewall, without TLS.
var NoKeyPair = KeyPair{}
// NewClient returns a *Client that will manipulate ipPort,
// authenticated using the provided keypair.
//
// This constructor returns immediately without testing the host or auth.
func NewClient(ipPort string, tls KeyPair) *Client {
func NewClient(ipPort string, kp KeyPair) *Client {
return &Client{
ipPort: ipPort,
tls: tls,
ipPort: ipPort,
tls: kp,
password: kp.Password(),
httpClient: &http.Client{
Transport: &http.Transport{
DialTLS: kp.tlsDialer(),
},
},
}
}
// A Client interacts with a single buildlet.
type Client struct {
ipPort string
tls KeyPair
ipPort string
tls KeyPair
password string // basic auth password or empty for none
httpClient *http.Client
}
// URL returns the buildlet's URL prefix, without a trailing slash.
func (c *Client) URL() string {
if c.tls != NoKeyPair {
return "http://" + strings.TrimSuffix(c.ipPort, ":80")
if !c.tls.IsZero() {
return "https://" + strings.TrimSuffix(c.ipPort, ":443")
}
return "https://" + strings.TrimSuffix(c.ipPort, ":443")
return "http://" + strings.TrimSuffix(c.ipPort, ":80")
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
if c.password != "" {
req.SetBasicAuth("gomote", c.password)
}
return c.httpClient.Do(req)
}
// PutTarball writes files to the remote buildlet.
@ -63,7 +65,7 @@ func (c *Client) PutTarball(r io.Reader) error {
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
res, err := c.do(req)
if err != nil {
return err
}
@ -95,7 +97,15 @@ type ExecOpts struct {
// seen to completition. If execErr is non-nil, the remoteErr is
// meaningless.
func (c *Client) Exec(cmd string, opts ExecOpts) (remoteErr, execErr error) {
res, err := http.PostForm(c.URL()+"/exec", url.Values{"cmd": {cmd}})
form := url.Values{
"cmd": {cmd},
}
req, err := http.NewRequest("POST", c.URL()+"/exec", strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := c.do(req)
if err != nil {
return nil, err
}
@ -130,6 +140,24 @@ func (c *Client) Exec(cmd string, opts ExecOpts) (remoteErr, execErr error) {
return nil, nil
}
// Destroy shuts down the buildlet, destroying all state immediately.
func (c *Client) Destroy() error {
req, err := http.NewRequest("POST", c.URL()+"/halt", nil)
if err != nil {
return err
}
res, err := c.do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
return fmt.Errorf("buildlet: HTTP status %v: %s", res.Status, slurp)
}
return nil
}
func condRun(fn func()) {
if fn != nil {
fn()

View File

@ -38,6 +38,7 @@ type VMOpts struct {
Meta map[string]string
// DeleteIn optionally specifies a duration at which
// to delete the VM.
DeleteIn time.Duration
@ -102,18 +103,7 @@ func StartNewVM(ts oauth2.TokenSource, instName, builderType string, opts VMOpts
// The https-server is authenticated, though.
Items: []string{"https-server"},
},
Metadata: &compute.Metadata{
Items: []*compute.MetadataItems{
// The buildlet-binary-url is the URL of the buildlet binary
// which the VMs are configured to download at boot and run.
// This lets us/ update the buildlet more easily than
// rebuilding the whole VM image.
{
Key: "buildlet-binary-url",
Value: "http://storage.googleapis.com/go-builder-data/buildlet." + conf.GOOS() + "-" + conf.GOARCH(),
},
},
},
Metadata: &compute.Metadata{},
NetworkInterfaces: []*compute.NetworkInterface{
&compute.NetworkInterface{
AccessConfigs: []*compute.AccessConfig{
@ -126,6 +116,24 @@ func StartNewVM(ts oauth2.TokenSource, instName, builderType string, opts VMOpts
},
},
}
addMeta := func(key, value string) {
instance.Metadata.Items = append(instance.Metadata.Items, &compute.MetadataItems{
Key: key,
Value: value,
})
}
// The buildlet-binary-url is the URL of the buildlet binary
// which the VMs are configured to download at boot and run.
// This lets us/ update the buildlet more easily than
// rebuilding the whole VM image.
addMeta("buildlet-binary-url",
"http://storage.googleapis.com/go-builder-data/buildlet."+conf.GOOS()+"-"+conf.GOARCH())
addMeta("builder-type", builderType)
if !opts.TLS.IsZero() {
addMeta("tls-cert", opts.TLS.CertPEM)
addMeta("tls-key", opts.TLS.KeyPEM)
addMeta("password", opts.TLS.Password())
}
if opts.DeleteIn != 0 {
// In case the VM gets away from us (generally: if the
@ -134,16 +142,11 @@ func StartNewVM(ts oauth2.TokenSource, instName, builderType string, opts VMOpts
// we can kill it later when the coordinator is
// restarted. The cleanUpOldVMs goroutine loop handles
// that killing.
instance.Metadata.Items = append(instance.Metadata.Items, &compute.MetadataItems{
Key: "delete-at",
Value: fmt.Sprint(time.Now().Add(opts.DeleteIn).Unix()),
})
addMeta("delete-at", fmt.Sprint(time.Now().Add(opts.DeleteIn).Unix()))
}
for k, v := range opts.Meta {
instance.Metadata.Items = append(instance.Metadata.Items, &compute.MetadataItems{
Key: k,
Value: v,
})
addMeta(k, v)
}
op, err := computeService.Instances.Insert(projectID, zone, instance).Do()
@ -183,26 +186,24 @@ OpLoop:
return nil, fmt.Errorf("Error getting instance %s details after creation: %v", instName, err)
}
// Find its internal IP.
var ip string
for _, iface := range inst.NetworkInterfaces {
if strings.HasPrefix(iface.NetworkIP, "10.") {
ip = iface.NetworkIP
}
}
if ip == "" {
return nil, errors.New("didn't find its internal IP address")
}
// Finds its internal and/or external IP addresses.
intIP, extIP := instanceIPs(inst)
// Wait for it to boot and its buildlet to come up.
var buildletURL string
var ipPort string
if opts.TLS != NoKeyPair {
buildletURL = "https://" + ip
ipPort = ip + ":443"
if !opts.TLS.IsZero() {
if extIP == "" {
return nil, errors.New("didn't find its external IP address")
}
buildletURL = "https://" + extIP
ipPort = extIP + ":443"
} else {
buildletURL = "http://" + ip
ipPort = ip + ":80"
if intIP == "" {
return nil, errors.New("didn't find its internal IP address")
}
buildletURL = "http://" + intIP
ipPort = intIP + ":80"
}
condRun(opts.OnGotInstanceInfo)
@ -238,3 +239,77 @@ OpLoop:
return NewClient(ipPort, opts.TLS), nil
}
// DestroyVM sends a request to delete a VM. Actual VM description is
// currently (2015-01-19) very slow for no good reason. This function
// returns once it's been requested, not when it's done.
func DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) error {
computeService, _ := compute.New(oauth2.NewClient(oauth2.NoContext, ts))
_, err := computeService.Instances.Delete(proj, zone, instance).Do()
return err
}
type VM struct {
// Name is the name of the GCE VM instance.
// For example, it's of the form "mote-bradfitz-plan9-386-foo",
// and not "plan9-386-foo".
Name string
IPPort string
TLS KeyPair
Type string
}
// ListVMs lists all VMs.
func ListVMs(ts oauth2.TokenSource, proj, zone string) ([]VM, error) {
var vms []VM
computeService, _ := compute.New(oauth2.NewClient(oauth2.NoContext, ts))
// TODO(bradfitz): paging over results if more than 500
list, err := computeService.Instances.List(proj, zone).Do()
if err != nil {
return nil, err
}
for _, inst := range list.Items {
if inst.Metadata == nil {
// Defensive. Not seen in practice.
continue
}
meta := map[string]string{}
for _, it := range inst.Metadata.Items {
meta[it.Key] = it.Value
}
builderType := meta["builder-type"]
if builderType == "" {
continue
}
vm := VM{
Name: inst.Name,
Type: builderType,
TLS: KeyPair{
CertPEM: meta["tls-cert"],
KeyPEM: meta["tls-key"],
},
}
_, extIP := instanceIPs(inst)
if extIP == "" || vm.TLS.IsZero() {
continue
}
vm.IPPort = extIP + ":443"
vms = append(vms, vm)
}
return vms, nil
}
func instanceIPs(inst *compute.Instance) (intIP, extIP string) {
for _, iface := range inst.NetworkInterfaces {
if strings.HasPrefix(iface.NetworkIP, "10.") {
intIP = iface.NetworkIP
}
for _, accessConfig := range iface.AccessConfigs {
if accessConfig.Type == "ONE_TO_ONE_NAT" {
extIP = accessConfig.NatIP
}
}
}
return
}

View File

@ -0,0 +1,132 @@
// 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.
// +build extdep
package buildlet
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"time"
)
// KeyPair is the TLS public certificate PEM file and its associated
// private key PEM file that a builder will use for its HTTPS
// server. The zero value means no HTTPs, which is used by the
// coordinator for machines running within a firewall.
type KeyPair struct {
CertPEM string
KeyPEM string
}
func (kp KeyPair) IsZero() bool { return kp == KeyPair{} }
// Password returns the SHA1 of the KeyPEM. This is used as the HTTP
// Basic Auth password.
func (kp KeyPair) Password() string {
if kp.KeyPEM != "" {
return fmt.Sprintf("%x", sha1.Sum([]byte(kp.KeyPEM)))
}
return ""
}
// tlsDialer returns a TLS dialer for http.Transport.DialTLS that expects
// exactly our TLS cert.
func (kp KeyPair) tlsDialer() func(network, addr string) (net.Conn, error) {
if kp.IsZero() {
// Unused.
return nil
}
wantCert, _ := tls.X509KeyPair([]byte(kp.CertPEM), []byte(kp.KeyPEM))
var wantPubKey *rsa.PublicKey = &wantCert.PrivateKey.(*rsa.PrivateKey).PublicKey
return func(network, addr string) (net.Conn, error) {
if network != "tcp" {
return nil, fmt.Errorf("unexpected network %q", network)
}
plainConn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
tlsConn := tls.Client(plainConn, &tls.Config{InsecureSkipVerify: true})
if err := tlsConn.Handshake(); err != nil {
return nil, err
}
certs := tlsConn.ConnectionState().PeerCertificates
if len(certs) < 1 {
return nil, errors.New("no server peer certificate")
}
cert := certs[0]
peerPubRSA, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("peer cert was a %T; expected RSA", cert.PublicKey)
}
if peerPubRSA.N.Cmp(wantPubKey.N) != 0 {
return nil, fmt.Errorf("unexpected TLS certificate")
}
return tlsConn, nil
}
}
// NoKeyPair is used by the coordinator to speak http directly to buildlets,
// inside their firewall, without TLS.
var NoKeyPair = KeyPair{}
func NewKeyPair() (KeyPair, error) {
fail := func(err error) (KeyPair, error) { return KeyPair{}, err }
failf := func(format string, args ...interface{}) (KeyPair, error) { return fail(fmt.Errorf(format, args...)) }
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return failf("rsa.GenerateKey: %s", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(5 * 365 * 24 * time.Hour) // 5 years
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return failf("failed to generate serial number: %s", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Gopher Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return failf("Failed to create certificate: %s", err)
}
var certOut bytes.Buffer
pem.Encode(&certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
var keyOut bytes.Buffer
pem.Encode(&keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
return KeyPair{
CertPEM: certOut.String(),
KeyPEM: keyOut.String(),
}, nil
}

View File

@ -3,24 +3,24 @@ buildlet: buildlet.go
buildlet.linux-amd64: buildlet.go
GOOS=linux GOARCH=amd64 go build -o $@ --tags=extdep
cat $@ | (cd ../upload && go run upload.go --public go-builder-data/$@)
cat $@ | (cd ../upload && go run --tags=extdep upload.go --public go-builder-data/$@)
buildlet.openbsd-amd64: buildlet.go
GOOS=openbsd GOARCH=amd64 go build -o $@ --tags=extdep
cat $@ | (cd ../upload && go run upload.go --public go-builder-data/$@)
cat $@ | (cd ../upload && go run --tags=extdep upload.go --public go-builder-data/$@)
buildlet.plan9-386: buildlet.go
GOOS=plan9 GOARCH=386 go build -o $@ --tags=extdep
cat $@ | (cd ../upload && go run upload.go --public go-builder-data/$@)
cat $@ | (cd ../upload && go run --tags=extdep upload.go --public go-builder-data/$@)
buildlet.windows-amd64: buildlet.go
GOOS=windows GOARCH=amd64 go build -o $@ --tags=extdep
cat $@ | (cd ../upload && go run upload.go --public go-builder-data/$@)
cat $@ | (cd ../upload && go run --tags=extdep upload.go --public go-builder-data/$@)
buildlet.darwin-amd64: buildlet.go
GOOS=darwin GOARCH=amd64 go build -o $@ --tags=extdep
cat $@ | (cd ../upload && go run upload.go --public go-builder-data/$@)
cat $@ | (cd ../upload && go run --tags=extdep upload.go --public go-builder-data/$@)
buildlet.netbsd-amd64: buildlet.go
GOOS=netbsd GOARCH=amd64 go build -o $@ --tags=extdep
cat $@ | (cd ../upload && go run upload.go --public go-builder-data/$@)
cat $@ | (cd ../upload && go run --tags=extdep upload.go --public go-builder-data/$@)

View File

@ -18,6 +18,7 @@ import (
"archive/tar"
"compress/gzip"
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
@ -37,8 +38,9 @@ import (
)
var (
scratchDir = flag.String("scratchdir", "", "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.")
haltEntireOS = flag.Bool("halt", true, "halt OS in /halt handler. If false, the buildlet process just ends.")
scratchDir = flag.String("scratchdir", "", "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.")
)
func defaultListenAddr() string {
@ -59,6 +61,8 @@ func defaultListenAddr() string {
return ":80"
}
var osHalt func() // set by some machines
func main() {
flag.Parse()
if !metadata.OnGCE() && !strings.HasPrefix(*listenAddr, "localhost:") {
@ -87,8 +91,12 @@ func main() {
http.HandleFunc("/", handleRoot)
password := metadataValue("password")
http.Handle("/writetgz", requirePassword{http.HandlerFunc(handleWriteTGZ), password})
http.Handle("/exec", requirePassword{http.HandlerFunc(handleExec), password})
requireAuth := func(handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
return requirePasswordHandler{http.HandlerFunc(handler), password}
}
http.Handle("/writetgz", requireAuth(handleWriteTGZ))
http.Handle("/exec", requireAuth(handleExec))
http.Handle("/halt", requireAuth(handleHalt))
// TODO: removeall
tlsCert, tlsKey := metadataValue("tls-cert"), metadataValue("tls-key")
@ -293,6 +301,51 @@ func handleExec(w http.ResponseWriter, r *http.Request) {
log.Printf("Run = %s", state)
}
func handleHalt(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "requires POST method", http.StatusBadRequest)
return
}
log.Printf("Halting in 1 second.")
// do the halt in 1 second, to give the HTTP response time to complete:
time.AfterFunc(1*time.Second, haltMachine)
}
func haltMachine() {
if !*haltEntireOS {
log.Printf("Ending buildlet process due to halt.")
os.Exit(0)
return
}
log.Printf("Halting machine.")
time.AfterFunc(5*time.Second, func() { os.Exit(0) })
if osHalt != nil {
// TODO: Windows: http://msdn.microsoft.com/en-us/library/windows/desktop/aa376868%28v=vs.85%29.aspx
osHalt()
os.Exit(0)
}
// Backup mechanism, if exec hangs for any reason:
var err error
switch runtime.GOOS {
case "openbsd":
// Quick, no fs flush, and power down:
err = exec.Command("halt", "-q", "-n", "-p").Run()
case "freebsd":
// Power off (-p), via halt (-o), now.
err = exec.Command("shutdown", "-p", "-o", "now").Run()
case "linux":
// Don't sync (-n), force without shutdown (-f), and power off (-p).
err = exec.Command("/bin/halt", "-n", "-f", "-p").Run()
case "plan9":
err = exec.Command("fshalt").Run()
default:
err = errors.New("No system-specific halt command run; will just end buildlet process.")
}
log.Printf("Shutdown: %v", err)
log.Printf("Ending buildlet process post-halt")
os.Exit(0)
}
// flushWriter is an io.Writer wrapper that writes to w and
// Flushes the output immediately, if w is an http.Flusher.
type flushWriter struct {
@ -336,12 +389,12 @@ func badRequest(msg string) error {
// requirePassword is an http.Handler auth wrapper that enforces a
// HTTP Basic password. The username is ignored.
type requirePassword struct {
type requirePasswordHandler struct {
h http.Handler
password string // empty means no password
}
func (h requirePassword) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h requirePasswordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_, gotPass, _ := r.BasicAuth()
if h.password != "" && h.password != gotPass {
http.Error(w, "invalid password", http.StatusForbidden)

View File

@ -1,3 +1,3 @@
buildlet-stage0.windows-amd64: stage0.go
GOOS=windows GOARCH=amd64 go build -o $@ --tags=extdep
cat $@ | (cd ../../upload && go run upload.go --public go-builder-data/$@)
cat $@ | (cd ../../upload && go run --tags=extdep upload.go --public go-builder-data/$@)

View File

@ -6,4 +6,4 @@ coordinator: coordinator.go
# And watch its logs with:
# sudo journalctl -f -u gobuild.service
upload: coordinator
cat coordinator | (cd ../upload && go run upload.go --public go-builder-data/coordinator)
cat coordinator | (cd ../upload && go run --tags=extdep upload.go --public go-builder-data/coordinator)

View File

@ -1113,7 +1113,7 @@ func hasComputeScope() bool {
return false
}
for _, v := range scopes {
if v == compute.DevstorageFull_controlScope {
if v == compute.ComputeScope {
return true
}
}

View File

@ -0,0 +1,87 @@
// 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.
// +build extdep
package main
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"golang.org/x/oauth2"
"golang.org/x/tools/dashboard/auth"
"golang.org/x/tools/dashboard/buildlet"
"google.golang.org/api/compute/v1"
)
func username() string {
if runtime.GOOS == "windows" {
return os.Getenv("USERNAME")
}
return os.Getenv("USER")
}
func homeDir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
return os.Getenv("HOME")
}
func configDir() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("APPDATA"), "Gomote")
}
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "gomote")
}
return filepath.Join(homeDir(), ".config", "gomote")
}
func projTokenSource() oauth2.TokenSource {
ts, err := auth.ProjectTokenSource(*proj, compute.ComputeScope)
if err != nil {
log.Fatalf("Failed to get OAuth2 token source for project %s: %v", *proj, err)
}
return ts
}
func userKeyPair() buildlet.KeyPair {
keyDir := configDir()
crtFile := filepath.Join(keyDir, "gomote.crt")
keyFile := filepath.Join(keyDir, "gomote.key")
_, crtErr := os.Stat(crtFile)
_, keyErr := os.Stat(keyFile)
if crtErr == nil && keyErr == nil {
return buildlet.KeyPair{
CertPEM: slurpString(crtFile),
KeyPEM: slurpString(keyFile),
}
}
check := func(what string, err error) {
if err != nil {
log.Printf("%s: %v", what, err)
}
}
check("making key dir", os.MkdirAll(keyDir, 0700))
kp, err := buildlet.NewKeyPair()
if err != nil {
log.Fatalf("Error generating new key pair: %v", err)
}
check("writing cert file: ", ioutil.WriteFile(crtFile, []byte(kp.CertPEM), 0600))
check("writing key file: ", ioutil.WriteFile(keyFile, []byte(kp.KeyPEM), 0600))
return kp
}
func slurpString(f string) string {
slurp, err := ioutil.ReadFile(f)
if err != nil {
log.Fatal(err)
}
return string(slurp)
}

View File

@ -0,0 +1,67 @@
// 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.
// +build extdep
package main
import (
"flag"
"fmt"
"log"
"os"
"sort"
"time"
"golang.org/x/tools/dashboard"
"golang.org/x/tools/dashboard/buildlet"
)
func create(args []string) error {
fs := flag.NewFlagSet("create", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "create usage: gomote create [create-opts] <type>\n\n")
fs.PrintDefaults()
os.Exit(1)
}
var timeout time.Duration
fs.DurationVar(&timeout, "timeout", 60*time.Minute, "how long the VM will live before being deleted.")
fs.Parse(args)
if fs.NArg() != 1 {
fs.Usage()
}
builderType := fs.Arg(0)
conf, ok := dashboard.Builders[builderType]
if !ok || !conf.UsesVM() {
var valid []string
for k, conf := range dashboard.Builders {
if conf.UsesVM() {
valid = append(valid, k)
}
}
sort.Strings(valid)
return fmt.Errorf("Invalid builder type %q. Valid options include: %q", builderType, valid)
}
instName := fmt.Sprintf("mote-%s-%s", username(), builderType)
client, err := buildlet.StartNewVM(projTokenSource(), instName, builderType, buildlet.VMOpts{
Zone: *zone,
ProjectID: *proj,
TLS: userKeyPair(),
DeleteIn: timeout,
Description: fmt.Sprintf("gomote buildlet for %s", username()),
OnInstanceRequested: func() {
log.Printf("Sent create request. Waiting for operation.")
},
OnInstanceCreated: func() {
log.Printf("Instance created.")
},
})
if err != nil {
return fmt.Errorf("failed to create VM: %v", err)
}
fmt.Printf("%s\t%s\n", builderType, client.URL())
return nil
}

View File

@ -0,0 +1,42 @@
// 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.
// +build extdep
package main
import (
"flag"
"fmt"
"os"
"golang.org/x/tools/dashboard/buildlet"
)
func destroy(args []string) error {
fs := flag.NewFlagSet("destroy", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "create usage: gomote destroy <instance>\n\n")
fs.PrintDefaults()
os.Exit(1)
}
fs.Parse(args)
if fs.NArg() != 1 {
fs.Usage()
}
name := fs.Arg(0)
bc, err := namedClient(name)
if err != nil {
return err
}
// First ask it to kill itself, and then tell GCE to kill it too:
shutErr := bc.Destroy()
gceErr := buildlet.DestroyVM(projTokenSource(), *proj, *zone, fmt.Sprintf("mote-%s-%s", username(), name))
if shutErr != nil {
return shutErr
}
return gceErr
}

View File

@ -0,0 +1,105 @@
// 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.
// +build extdep
/*
The gomote command is a client for the Go builder infrastructure.
It's a remote control for remote Go builder machines.
Usage:
gomote [global-flags] cmd [cmd-flags]
For example,
$ gomote create openbsd-amd64-gce56
$ gomote push
$ gomote run openbsd-amd64-gce56 src/make.bash
TODO: document more, and figure out the CLI interface more.
*/
package main
import (
"flag"
"fmt"
"os"
"sort"
)
var (
proj = flag.String("project", "symbolic-datum-552", "GCE project owning builders")
zone = flag.String("zone", "us-central1-a", "GCE zone")
)
type command struct {
name string
des string
run func([]string) error
}
var commands = map[string]command{}
func sortedCommands() []string {
s := make([]string, 0, len(commands))
for name := range commands {
s = append(s, name)
}
sort.Strings(s)
return s
}
func usage() {
fmt.Fprintf(os.Stderr, `Usage of gomote: gomote [global-flags] <cmd> [cmd-flags]
Global flags:
`)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "Commands:\n\n")
for _, name := range sortedCommands() {
fmt.Fprintf(os.Stderr, " %-10s %s\n", name, commands[name].des)
}
os.Exit(1)
}
func registerCommand(name, des string, run func([]string) error) {
if _, dup := commands[name]; dup {
panic("duplicate registration of " + name)
}
commands[name] = command{
name: name,
des: des,
run: run,
}
}
func registerCommands() {
registerCommand("create", "create a buildlet", create)
registerCommand("destroy", "destroy a buildlet", destroy)
registerCommand("list", "list buildlets", list)
registerCommand("run", "run a command on a buildlet", run)
registerCommand("put", "put files on a buildlet", put)
registerCommand("puttar", "extract a tar.gz to a buildlet", putTar)
}
func main() {
registerCommands()
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) == 0 {
usage()
}
cmdName := args[0]
cmd, ok := commands[cmdName]
if !ok {
fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmdName)
usage()
}
err := cmd.run(args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "Error running %s: %v\n", cmdName, err)
os.Exit(1)
}
}

View File

@ -0,0 +1,57 @@
// 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.
// +build extdep
package main
import (
"flag"
"fmt"
"os"
"strings"
"golang.org/x/tools/dashboard/buildlet"
)
func list(args []string) error {
fs := flag.NewFlagSet("list", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "list usage: gomote list\n\n")
fs.PrintDefaults()
os.Exit(1)
}
fs.Parse(args)
if fs.NArg() != 0 {
fs.Usage()
}
prefix := fmt.Sprintf("mote-%s-", username())
vms, err := buildlet.ListVMs(projTokenSource(), *proj, *zone)
if err != nil {
return fmt.Errorf("failed to list VMs: %v", err)
}
for _, vm := range vms {
if !strings.HasPrefix(vm.Name, prefix) {
continue
}
fmt.Printf("%s\thttps://%s\n", vm.Type, strings.TrimSuffix(vm.IPPort, ":443"))
}
return nil
}
func namedClient(name string) (*buildlet.Client, error) {
// TODO(bradfitz): cache the list on disk and avoid the API call?
vms, err := buildlet.ListVMs(projTokenSource(), *proj, *zone)
if err != nil {
return nil, fmt.Errorf("error listing VMs while looking up %q: %v", name, err)
}
wantName := fmt.Sprintf("mote-%s-%s", username(), name)
for _, vm := range vms {
if vm.Name == wantName {
return buildlet.NewClient(vm.IPPort, vm.TLS), nil
}
}
return nil, fmt.Errorf("buildlet %q not running", name)
}

View File

@ -0,0 +1,83 @@
// 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.
// +build extdep
package main
import (
"flag"
"fmt"
"io"
"net/http"
"os"
)
// put a .tar.gz
func putTar(args []string) error {
fs := flag.NewFlagSet("put", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "create usage: gomote puttar [put-opts] <buildlet-name> [tar.gz file or '-' for stdin]\n")
fs.PrintDefaults()
os.Exit(1)
}
var rev string
fs.StringVar(&rev, "gorev", "", "If non-empty, git hash to download from gerrit and put to the buildlet. e.g. 886b02d705ff for Go 1.4.1")
fs.Parse(args)
if fs.NArg() < 1 || fs.NArg() > 2 {
fs.Usage()
}
name := fs.Arg(0)
bc, err := namedClient(name)
if err != nil {
return err
}
var tgz io.Reader = os.Stdin
if rev != "" {
if fs.NArg() != 1 {
fs.Usage()
}
// TODO(bradfitz): tell the buildlet to do this
// itself, to avoid network to & from home networks.
// Staying Google<->Google will be much faster.
res, err := http.Get("https://go.googlesource.com/go/+archive/" + rev + ".tar.gz")
if err != nil {
return fmt.Errorf("Error fetching rev %s from Gerrit: %v", rev, err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("Error fetching rev %s from Gerrit: %v", rev, res.Status)
}
tgz = res.Body
} else if fs.NArg() == 2 && fs.Arg(1) != "-" {
f, err := os.Open(fs.Arg(1))
if err != nil {
return err
}
defer f.Close()
tgz = f
}
return bc.PutTarball(tgz)
}
// put single files
func put(args []string) error {
fs := flag.NewFlagSet("put", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "create usage: gomote put [put-opts] <type>\n\n")
fs.PrintDefaults()
os.Exit(1)
}
fs.Parse(args)
if fs.NArg() != 1 {
fs.Usage()
}
return fmt.Errorf("TODO")
builderType := fs.Arg(0)
_ = builderType
return nil
}

View File

@ -0,0 +1,42 @@
// 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.
// +build extdep
package main
import (
"flag"
"fmt"
"os"
"golang.org/x/tools/dashboard/buildlet"
)
func run(args []string) error {
fs := flag.NewFlagSet("run", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "create usage: gomote run [run-opts] <buildlet-name> <cmd> [args...]")
fs.PrintDefaults()
os.Exit(1)
}
fs.Parse(args)
if fs.NArg() < 2 {
fs.Usage()
}
name, cmd := fs.Arg(0), fs.Arg(1)
bc, err := namedClient(name)
if err != nil {
return err
}
remoteErr, execErr := bc.Exec(cmd, buildlet.ExecOpts{
Output: os.Stdout,
})
if execErr != nil {
return fmt.Errorf("Error trying to execute %s: %v", cmd, execErr)
}
return remoteErr
}

View File

@ -15,15 +15,13 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/tools/dashboard/auth"
"google.golang.org/cloud"
"google.golang.org/cloud/storage"
)
@ -111,18 +109,9 @@ var bucketProject = map[string]string{
}
func tokenSource(bucket string) (oauth2.TokenSource, error) {
proj := bucketProject[bucket]
fileName := filepath.Join(os.Getenv("HOME"), "keys", proj+".key.json")
jsonConf, err := ioutil.ReadFile(fileName)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("Missing JSON key configuration. Download the Service Account JSON key from https://console.developers.google.com/project/%s/apiui/credential and place it at %s", proj, fileName)
}
return nil, err
proj, ok := bucketProject[bucket]
if !ok {
return nil, fmt.Errorf("unknown project for bucket %q", bucket)
}
conf, err := google.JWTConfigFromJSON(jsonConf, storage.ScopeReadWrite)
if err != nil {
return nil, fmt.Errorf("reading JSON config from %s: %v", fileName, err)
}
return conf.TokenSource(oauth2.NoContext), nil
return auth.ProjectTokenSource(proj, storage.ScopeReadWrite)
}

View File

@ -6,4 +6,4 @@ docker: Dockerfile
docker build -t go-commit-watcher .
docker-commit-watcher.tar.gz: docker
docker save go-commit-watcher | gzip | (cd ../../cmd/upload && go run upload.go --public go-builder-data/docker-commit-watcher.tar.gz)
docker save go-commit-watcher | gzip | (cd ../../cmd/upload && go run --tags=extdep upload.go --public go-builder-data/docker-commit-watcher.tar.gz)

View File

@ -6,7 +6,7 @@ docker: Dockerfile
docker build -t gobuilders/linux-x86-base .
docker-linux.base.tar.gz: docker
docker save gobuilders/linux-x86-base | gzip | (cd ../../cmd/upload && go run upload.go --public go-builder-data/docker-linux.base.tar.gz)
docker save gobuilders/linux-x86-base | gzip | (cd ../../cmd/upload && go run --tags=extdep upload.go --public go-builder-data/docker-linux.base.tar.gz)
check: docker
docker run -e GOROOT_BOOTSTRAP=/go1.4-amd64/go gobuilders/linux-x86-base /usr/local/bin/builder -rev=20a10e7ddd1 -buildroot=/ -v -report=false linux-amd64-temp

View File

@ -6,7 +6,7 @@ docker: Dockerfile
docker build -t gobuilders/linux-x86-clang .
docker-linux.clang.tar.gz: docker
docker save gobuilders/linux-x86-clang | gzip | (cd ../../cmd/upload && go run upload.go --public go-builder-data/docker-linux.clang.tar.gz)
docker save gobuilders/linux-x86-clang | gzip | (cd ../../cmd/upload && go run --tags=extdep upload.go --public go-builder-data/docker-linux.clang.tar.gz)
check: docker
docker run -e GOROOT_BOOTSTRAP=/go1.4-amd64/go gobuilders/linux-x86-clang /usr/local/bin/builder -rev=20a10e7ddd1b -buildroot=/ -v -report=false linux-amd64-temp

View File

@ -6,7 +6,7 @@ docker: Dockerfile
docker build -t gobuilders/linux-x86-gccgo .
docker-linux.gccgo.tar.gz: docker
docker save gobuilders/linux-x86-gccgo | gzip | (cd ../../cmd/upload && go run upload.go --public go-builder-data/docker-linux.gccgo.tar.gz)
docker save gobuilders/linux-x86-gccgo | gzip | (cd ../../cmd/upload && go run --tags=extdep upload.go --public go-builder-data/docker-linux.gccgo.tar.gz)
check: docker
docker run gobuilders/linux-x86-gccgo /usr/local/bin/builder -tool="gccgo" -rev=b9151e911a54 -v -cmd='make RUNTESTFLAGS="--target_board=unix/-m64" check-go' -report=false linux-amd64-gccgo-temp

View File

@ -6,7 +6,7 @@ docker: Dockerfile
docker build -t gobuilders/linux-x86-nacl .
upload: docker
docker save gobuilders/linux-x86-nacl | gzip | (cd ../../cmd/upload && go run upload.go --public go-builder-data/docker-linux.nacl.tar.gz)
docker save gobuilders/linux-x86-nacl | gzip | (cd ../../cmd/upload && go run --tags=extdep upload.go --public go-builder-data/docker-linux.nacl.tar.gz)
check: docker
docker run gobuilders/linux-x86-nacl /usr/local/bin/builder -rev=77e96c9208d0 -buildroot=/ -v -cmd=/usr/local/bin/build-command.pl -report=false nacl-amd64p32

View File

@ -6,7 +6,7 @@ docker: Dockerfile
docker build -t gobuilders/linux-x86-sid .
docker-linux.sid.tar.gz: docker
docker save gobuilders/linux-x86-sid | gzip | (cd ../../cmd/upload && go run upload.go --public go-builder-data/docker-linux.sid.tar.gz)
docker save gobuilders/linux-x86-sid | gzip | (cd ../../cmd/upload && go run --tags=extdep upload.go --public go-builder-data/docker-linux.sid.tar.gz)
check: docker
docker run -e GOROOT_BOOTSTRAP=/go1.4-amd64/go gobuilders/linux-x86-sid /usr/local/bin/builder -rev=20a10e7ddd1b -buildroot=/ -v -report=false linux-amd64-sid