Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add remote-download command to CLI #8375

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/cli_command/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ go_library(
"//cli/login",
"//cli/plugin",
"//cli/printlog",
"//cli/remote_download",
"//cli/remotebazel",
"//cli/update",
"//cli/upload",
Expand Down
6 changes: 6 additions & 0 deletions cli/cli_command/cli_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/buildbuddy-io/buildbuddy/cli/login"
"github.com/buildbuddy-io/buildbuddy/cli/plugin"
"github.com/buildbuddy-io/buildbuddy/cli/printlog"
"github.com/buildbuddy-io/buildbuddy/cli/remote_download"
"github.com/buildbuddy-io/buildbuddy/cli/remotebazel"
"github.com/buildbuddy-io/buildbuddy/cli/update"
"github.com/buildbuddy-io/buildbuddy/cli/upload"
Expand Down Expand Up @@ -83,6 +84,11 @@ var Commands = []Command{
Help: "Runs a bazel command in the cloud with BuildBuddy's hosted bazel service.",
Handler: remotebazel.HandleRemoteBazel,
},
{
Name: "remote-download",
Help: "Fetches a remote asset via an intermediate cache.",
Handler: remote_download.HandleRemoteDownload,
},
{
Name: "update",
Help: "Updates the bb CLI to the latest version.",
Expand Down
23 changes: 23 additions & 0 deletions cli/remote_download/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "remote_download",
srcs = ["remote_download.go"],
importpath = "github.com/buildbuddy-io/buildbuddy/cli/remote_download",
visibility = ["//visibility:public"],
deps = [
"//cli/arg",
"//cli/login",
"//proto:remote_asset_go_proto",
"//proto:remote_execution_go_proto",
"//proto:resource_go_proto",
"//server/remote_cache/digest",
"//server/util/flag",
"//server/util/grpc_client",
"@com_github_mattn_go_isatty//:go-isatty",
"@org_golang_google_grpc//status",
"@org_golang_google_protobuf//types/known/durationpb",
],
)

package(default_visibility = ["//cli:__subpackages__"])
128 changes: 128 additions & 0 deletions cli/remote_download/remote_download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package remote_download

import (
"context"
"fmt"
"log"
"os"
"strings"

"github.com/buildbuddy-io/buildbuddy/cli/arg"
"github.com/buildbuddy-io/buildbuddy/cli/login"
"github.com/buildbuddy-io/buildbuddy/server/remote_cache/digest"
"github.com/buildbuddy-io/buildbuddy/server/util/flag"
"github.com/buildbuddy-io/buildbuddy/server/util/grpc_client"
"github.com/mattn/go-isatty"
"google.golang.org/protobuf/types/known/durationpb"

rapb "github.com/buildbuddy-io/buildbuddy/proto/remote_asset"
repb "github.com/buildbuddy-io/buildbuddy/proto/remote_execution"
rspb "github.com/buildbuddy-io/buildbuddy/proto/resource"
gstatus "google.golang.org/grpc/status"
)

var (
flags = flag.NewFlagSet("remote-download", flag.ContinueOnError)

target = flags.String("target", login.DefaultApiTarget, "Remote gRPC target. Must support bytestream and remote asset APIs.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate getting an API key flag here to target specific tenants.
Or if we can get the existing API key from .git/config by default, that would be nice as well.

remoteInstanceName = flags.String("remote_instance_name", "", "Remote instance name - typically acts as a cache namespace.")
digestFunction = flags.String("digest_function", "", "Digest function specifying how the blob is cached. Must be supported by the remote server.")
timeout = flags.Duration("timeout", 0, "Fetch timeout.")
qualifiers = flag.New(flags, "qualifier", []string{}, "Qualifiers in NAME=VALUE format.")

usage = `
usage: bb ` + flags.Name() + ` <url>

Fetches a file via an intermediate cache server and prints the resulting
cache resource name to stdout.

The resource can be downloaded using 'bb download':

bb remote-download <url> | xargs bb download

The "checksum.sri" qualifier can be used to specify a checksum.
Example:

SHA256=$(curl -fsSL https://example.com | sha256sum | awk '{print $1}')
SRI="sha256-$(echo "$SHA256" | xxd -r -p | base64 -w 0)"
bb remote-download --qualifier=checksum.sri="$SRI" https://example.com
`
)

func HandleRemoteDownload(args []string) (int, error) {
if err := arg.ParseFlagSet(flags, args); err != nil {
if err == flag.ErrHelp {
log.Print(usage)
return 1, nil
}
return -1, err
}

uris := flags.Args()
if len(uris) == 0 {
return -1, fmt.Errorf("missing <url> argument")
}
if len(uris) != 1 {
return -1, fmt.Errorf("only one URL argument is supported")
}

parsedDigestFunction := repb.DigestFunction_SHA256
if *digestFunction != "" {
df, err := digest.ParseFunction(*digestFunction)
if err != nil {
return -1, fmt.Errorf("invalid --digest_function %q", *digestFunction)
}
parsedDigestFunction = df
}

conn, err := grpc_client.DialSimpleWithoutPooling(*target)
if err != nil {
return -1, fmt.Errorf("dial %q: %s", *target, err)
}
defer conn.Close()

ctx := context.Background()

var timeoutProto *durationpb.Duration
if *timeout > 0 {
timeoutProto = durationpb.New(*timeout)
}
req := &rapb.FetchBlobRequest{
InstanceName: *remoteInstanceName,
Uris: uris,
DigestFunction: parsedDigestFunction,
Timeout: timeoutProto,
// TODO: OldestContentAccepted
}
for _, q := range *qualifiers {
name, value, ok := strings.Cut(q, "=")
if !ok {
return -1, fmt.Errorf("invalid qualifier (expected NAME=VALUE)")
}
req.Qualifiers = append(req.Qualifiers, &rapb.Qualifier{
Name: name,
Value: value,
})
fmt.Printf("Qualifier name: %q value: %q\n", name, value)
}
client := rapb.NewFetchClient(conn)
resp, err := client.FetchBlob(ctx, req)
if err != nil {
return -1, fmt.Errorf("fetch blob: %w", err)
}

if err := gstatus.ErrorProto(resp.GetStatus()); err != nil {
return -1, err
}

rn := digest.NewResourceName(resp.GetBlobDigest(), *remoteInstanceName, rspb.CacheType_CAS, resp.GetDigestFunction())
dl, err := rn.DownloadString()
if err != nil {
return -1, fmt.Errorf("convert resource name to string: %w", err)
}
fmt.Print(dl)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println()
}
return 0, nil
}
2 changes: 1 addition & 1 deletion server/remote_asset/fetch_server/fetch_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ func mirrorToCache(
}
defer rsp.Body.Close()
if rsp.StatusCode < 200 || rsp.StatusCode >= 400 {
return nil, status.UnavailableErrorf("failed to fetch %q: HTTP %s", uri, err)
return nil, status.UnavailableErrorf("failed to fetch %q: HTTP %s", uri, rsp.Status)
}

// If we know what the hash should be and the content length is known,
Expand Down