Skip to content

Commit

Permalink
Add remote-download command to CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
bduffany committed Feb 12, 2025
1 parent 5fe1abc commit 72e6882
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 1 deletion.
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.")
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 fetch <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

0 comments on commit 72e6882

Please sign in to comment.