blob: 6b70565cb818ac395358ddd718dd5c1c5ef80fe5 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
package main
import (
"bytes"
"fmt"
"net/http"
"net/url"
"path"
"path/filepath"
"runtime"
"strings"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
storage "google.golang.org/api/storage/v1"
"v.io/jiri/collect"
"v.io/jiri/retry"
"v.io/jiri/tool"
"v.io/x/devtools/vbinary/exitcode"
"v.io/x/lib/cmdline"
)
var (
archFlag string
attemptsFlag int
datePrefixFlag string
keyFileFlag string
osFlag string
outputDirFlag string
releaseFlag bool
maxParallelDownloads int
waitTimeBetweenAttempts = 3 * time.Minute
createClientAttempts = 5
waitTimeBetweenCreateClientAttempts = 1 * time.Minute
)
const (
binariesBucketName = "vanadium-binaries"
releaseBinariesBucketName = "vanadium-release"
gceUser = "veyron"
)
func bucketName() string {
if releaseFlag {
return releaseBinariesBucketName
}
return binariesBucketName
}
func dateLayout() string {
if releaseFlag {
return "2006-01-02.15:04"
}
return "2006-01-02T15:04:05-07:00"
}
func osArchDir() string {
if releaseFlag {
return ""
}
return fmt.Sprintf("%s_%s", osFlag, archFlag)
}
func stripOsArchDir(name string) string {
if releaseFlag {
return name
}
return strings.Split(name, "/")[1]
}
// TODO(suharshs): Add tests that mock out google.Storage.
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
cmdRoot.Flags.BoolVar(&releaseFlag, "release", false, "Operate on vanadium-release bucket instead of vanadium-binaries.")
cmdRoot.Flags.StringVar(&archFlag, "arch", runtime.GOARCH, "Target architecture. The default is the value of runtime.GOARCH.")
cmdRoot.Flags.Lookup("arch").DefValue = "<runtime.GOARCH>"
cmdRoot.Flags.StringVar(&osFlag, "os", runtime.GOOS, "Target operating system. The default is the value of runtime.GOOS.")
cmdRoot.Flags.Lookup("os").DefValue = "<runtime.GOOS>"
cmdRoot.Flags.StringVar(&keyFileFlag, "key-file", "", "Google Developers service account JSON key file.")
cmdRoot.Flags.StringVar(&datePrefixFlag, "date-prefix", "", "Date prefix to match daily build timestamps. Must be a prefix of YYYY-MM-DD.")
cmdDownload.Flags.IntVar(&attemptsFlag, "attempts", 1, "Number of attempts before failing.")
cmdDownload.Flags.StringVar(&outputDirFlag, "output-dir", "", "Directory for storing downloaded binaries.")
cmdDownload.Flags.IntVar(&maxParallelDownloads, "max-parallel-downloads", 8, "Maximum number of downloads that can happen at the same time.")
tool.InitializeRunFlags(&cmdRoot.Flags)
}
func main() {
cmdline.Main(cmdRoot)
}
// cmdRoot represents the "vbinary" command.
var cmdRoot = &cmdline.Command{
Name: "vbinary",
Short: "Access daily builds of Vanadium binaries",
Long: `
Command vbinary retrieves daily builds of Vanadium binaries stored in
a Google Storage bucket.
`,
Children: []*cmdline.Command{cmdList, cmdDownload},
}
// cmdList represents the "vbinary list" command.
var cmdList = &cmdline.Command{
Runner: cmdline.RunnerFunc(runList),
Name: "list",
Short: "List existing daily builds of Vanadium binaries",
Long: `
List existing daily builds of Vanadium binaries. The displayed dates
can be limited with the --date-prefix flag. An exit code of 3 indicates
that no snapshot was found.
`,
}
func runList(env *cmdline.Env, _ []string) error {
ctx := tool.NewContextFromEnv(env)
client, err := createClient(ctx)
if err != nil {
return err
}
service, err := storage.New(client)
if err != nil {
return err
}
binaries, err := binarySnapshots(ctx, service)
if err != nil {
return err
}
for _, name := range binaries {
fmt.Fprintf(ctx.Stdout(), "%s\n", name)
}
return nil
}
// cmdDownload represents the "vbinary download" command.
var cmdDownload = &cmdline.Command{
Runner: cmdline.RunnerFunc(runDownload),
Name: "download",
Short: "Download an existing daily build of Vanadium binaries",
Long: `
Download an existing daily build of Vanadium binaries. The latest
snapshot within the --date-prefix range will be downloaded. If no
--date-prefix flag is provided, the overall latest snapshot will be
downloaded. An exit code of 3 indicates that no snapshot was found.
`,
}
func runDownload(env *cmdline.Env, args []string) error {
ctx := tool.NewContextFromEnv(env)
s := ctx.NewSeq()
client, err := createClient(ctx)
if err != nil {
return err
}
binaries, timestamp, err := latestBinaries(ctx, client)
if err != nil {
return err
}
if len(outputDirFlag) == 0 {
outputDirFlag = fmt.Sprintf("./v23_%s_%s_%s", osFlag, archFlag, timestamp)
}
numBinaries := len(binaries)
downloadBinaries := func() error {
downloadFn := func() error {
if err := ctx.NewSeq().MkdirAll(outputDirFlag, 0755).Done(); err != nil {
return err
}
errChan := make(chan error, numBinaries)
downloadingChan := make(chan struct{}, maxParallelDownloads)
for _, name := range binaries {
downloadingChan <- struct{}{}
go downloadBinary(ctx, client, name, errChan, downloadingChan)
}
gotError := false
for i := 0; i < numBinaries; i++ {
if err := <-errChan; err != nil {
fmt.Fprintf(ctx.Stderr(), "failed to download binary: %v\n", err)
gotError = true
}
}
if gotError {
if err := ctx.NewSeq().RemoveAll(outputDirFlag).Done(); err != nil {
fmt.Fprintf(ctx.Stderr(), "%v", err)
}
return fmt.Errorf("Failed to download some binaries")
}
return nil
}
if err := retry.Function(ctx, downloadFn, retry.AttemptsOpt(attemptsFlag), retry.IntervalOpt(waitTimeBetweenAttempts)); err != nil {
return fmt.Errorf("operation failed")
}
// Remove the .done file from the snapshot.
if err := ctx.NewSeq().RemoveAll(path.Join(outputDirFlag, ".done")).Done(); err != nil {
return err
}
return nil
}
return s.Call(downloadBinaries, "Downloading binaries to %s", outputDirFlag).Done()
}
// latestBinaries returns the binaries of the latest snapshot whose timestamp
// matches the datePrefixFlag, along with the matching timestamp.
func latestBinaries(ctx *tool.Context, client *http.Client) ([]string, string, error) {
service, err := storage.New(client)
if err != nil {
return nil, "", err
}
timestamp, err := latestTimestamp(ctx, client, service)
if err != nil {
return nil, "", err
}
binaryPrefix := path.Join(osArchDir(), timestamp)
res, err := service.Objects.List(bucketName()).Fields("nextPageToken", "items/name").Prefix(binaryPrefix).Do()
if err != nil {
return nil, "", err
}
objs := res.Items
for res.NextPageToken != "" {
res, err = service.Objects.List(bucketName()).PageToken(res.NextPageToken).Do()
if err != nil {
return nil, "", err
}
objs = append(objs, res.Items...)
}
if len(objs) == 0 {
return nil, "", fmt.Errorf("no binaries found (OS: %s, Arch: %s, Date: %s)", osFlag, archFlag, timestamp)
}
ret := make([]string, len(objs))
for i, obj := range objs {
ret[i] = obj.Name
}
return ret, timestamp, nil
}
// latestTimestamp returns the time of the latest snapshot within the
// date-prefix range.
func latestTimestamp(ctx *tool.Context, client *http.Client, service *storage.Service) (string, error) {
// If no datePrefixFlag is provided, we just want to get the latest snapshot.
if datePrefixFlag == "" {
latestFile := path.Join(osArchDir(), "latest")
b, err := downloadFileBytes(client, latestFile)
if err != nil {
return "", err
}
return string(b), nil
}
// Otherwise, we get the snapshots that match datePrefixFlag and choose the latest.
snapshots, err := binarySnapshots(ctx, service)
if err != nil {
return "", err
}
layout := dateLayout()
var latest string
var latestTime time.Time
for _, name := range snapshots {
timestamp := stripOsArchDir(name)
t, err := time.Parse(layout, timestamp)
if err != nil {
return "", err
}
if t.After(latestTime) {
latest = timestamp
latestTime = t
}
}
return latest, nil
}
func binarySnapshots(ctx *tool.Context, service *storage.Service) ([]string, error) {
filterSnapshots := func(call *storage.ObjectsListCall) (*storage.Objects, error) {
binaryPrefix := path.Join(osArchDir(), datePrefixFlag)
// We delimit results by the ".done" file to ensure that only successfully completed snapshots are considered.
return call.Fields("nextPageToken", "prefixes").Prefix(binaryPrefix).Delimiter("/.done").Do()
}
res, err := filterSnapshots(service.Objects.List(bucketName()))
if err != nil {
return nil, err
}
snapshots := res.Prefixes
for res.NextPageToken != "" {
res, err = filterSnapshots(service.Objects.List(bucketName()).PageToken(res.NextPageToken))
if err != nil {
return nil, err
}
snapshots = append(snapshots, res.Prefixes...)
}
if len(snapshots) == 0 {
fmt.Fprintf(ctx.Stderr(), "no snapshots found (OS: %s, Arch: %s, Date: %s)\n", osFlag, archFlag, datePrefixFlag)
return nil, cmdline.ErrExitCode(exitcode.NoSnapshotExitCode)
}
ret := make([]string, len(snapshots))
for i, snapshot := range snapshots {
ret[i] = strings.TrimSuffix(snapshot, "/.done")
}
return ret, nil
}
func createClient(ctx *tool.Context) (*http.Client, error) {
if len(keyFileFlag) > 0 {
data, err := ctx.NewSeq().ReadFile(keyFileFlag)
if err != nil {
return nil, err
}
conf, err := google.JWTConfigFromJSON(data, storage.CloudPlatformScope)
if err != nil {
return nil, fmt.Errorf("failed to create JWT config file: %v", err)
}
return conf.Client(oauth2.NoContext), nil
}
var defaultClient *http.Client
createDefaultClientFn := func() error {
var err error
defaultClient, err = google.DefaultClient(oauth2.NoContext, storage.CloudPlatformScope)
if err != nil {
return err
}
return nil
}
if err := retry.Function(ctx, createDefaultClientFn, retry.AttemptsOpt(createClientAttempts), retry.IntervalOpt(waitTimeBetweenCreateClientAttempts)); err != nil {
return nil, fmt.Errorf("failed to create default client")
}
return defaultClient, nil
}
func downloadBinary(ctx *tool.Context, client *http.Client, binaryPath string, errChan chan<- error, downloadingChan chan struct{}) {
helper := func() error {
b, err := downloadFileBytes(client, binaryPath)
if err != nil {
return fmt.Errorf("failed to download file %v: %v", binaryPath, err)
}
fileName := filepath.Join(outputDirFlag, path.Base(binaryPath))
if err := ctx.NewSeq().WriteFile(fileName, b, 0755).Done(); err != nil {
return err
}
return nil
}
errChan <- helper()
<-downloadingChan
}
func downloadFileBytes(client *http.Client, filePath string) (b []byte, e error) {
// This roundabout request is required because of the issue detailed here:
// https://plus.sandbox.google.com/+IanRose/posts/Tzw3QZqEQZk
// and here:
// https://groups.google.com/forum/#!msg/Golang-nuts/juguXl-ss2Q/oOVFvHYqoSgJ.
urls := "https://www.googleapis.com/download/storage/v1/b/{bucket}/o/{object}?alt=media"
req, err := http.NewRequest("GET", urls, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request to %s: %v\n", urls, err)
}
req.URL.Path = strings.Replace(req.URL.Path, "{bucket}", url.QueryEscape(bucketName()), 1)
req.URL.Path = strings.Replace(req.URL.Path, "{object}", url.QueryEscape(filePath), 1)
googleapi.SetOpaque(req.URL)
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to download %v: %v\n", req.URL.RequestURI(), err)
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("got StatusCode %v for download %v", req.URL.RequestURI(), res.StatusCode)
}
defer collect.Error(func() error { return res.Body.Close() }, &e)
var buf bytes.Buffer
if _, err := buf.ReadFrom(res.Body); err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
return buf.Bytes(), nil
}