Repository: buchgr/bazel-remote
Pull request #276 (2020-05-18 05:34:37)

add experimental remote asset api support
This is disabled by default, and can be enabled with the following flag:
`--experimental_remote_asset_api`

This currently handles some simple FetchBlob requests: when there's an sha256 SRI qualifier and a recognisable file type with http URL.


Patch:
diff --git a/README.md b/README.md
index cc7fd43..5baa79c 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,15 @@ and the corresponding parts of the [Byte Stream API](https://github.com/googleap
 
 To query endpoint metrics see [github.com/grpc-ecosystem/go-grpc-prometheus's metrics documentation](https://github.com/grpc-ecosystem/go-grpc-prometheus#metrics).
 
+### Experimental Remote Asset API Support
+
+There is (very) experimental support for a subset of the Fetch service in the
+[Remote Asset API](https://github.com/bazelbuild/remote-apis/blob/master/build/bazel/remote/asset/v1/remote_asset.proto)
+which can be enabled with the `--experimental_remote_asset_api` flag.
+
+To use this with Bazel, specify
+[--experimental_remote_downloader=grpc://replace-with-your.host:port](https://docs.bazel.build/versions/master/command-line-reference.html#flag--experimental_remote_downloader).
+
 ## Usage
 
 If a YAML configuration file is specified by the `--config_file` command line
@@ -95,31 +104,32 @@ COMMANDS:
    help, h  Shows a list of commands or help for one command
 
 GLOBAL OPTIONS:
-   --config_file value           Path to a YAML configuration file. If this flag is specified then all other flags are ignored. [$BAZEL_REMOTE_CONFIG_FILE]
-   --dir value                   Directory path where to store the cache contents. This flag is required. [$BAZEL_REMOTE_DIR]
-   --max_size value              The maximum size of the remote cache in GiB. This flag is required. (default: -1) [$BAZEL_REMOTE_MAX_SIZE]
-   --host value                  Address to listen on. Listens on all network interfaces by default. [$BAZEL_REMOTE_HOST]
-   --port value                  The port the HTTP server listens on. (default: 8080) [$BAZEL_REMOTE_PORT]
-   --grpc_port value             The port the EXPERIMENTAL gRPC server listens on. Set to 0 to disable. (default: 9092) [$BAZEL_REMOTE_GRPC_PORT]
-   --profile_host value          A host address to listen on for profiling, if enabled by a valid --profile_port setting. (default: "127.0.0.1") [$BAZEL_REMOTE_PROFILE_HOST]
-   --profile_port value          If a positive integer, serve /debug/pprof/* URLs from http://profile_host:profile_port. (default: 0, ie profiling disabled) [$BAZEL_REMOTE_PROFILE_PORT]
-   --htpasswd_file value         Path to a .htpasswd file. This flag is optional. Please read https://httpd.apache.org/docs/2.4/programs/htpasswd.html. [$BAZEL_REMOTE_HTPASSWD_FILE]
-   --tls_enabled                 This flag has been deprecated. Specify tls_cert_file and tls_key_file instead. (default: false) [$BAZEL_REMOTE_TLS_ENABLED]
-   --tls_cert_file value         Path to a pem encoded certificate file. [$BAZEL_REMOTE_TLS_CERT_FILE]
-   --tls_key_file value          Path to a pem encoded key file. [$BAZEL_REMOTE_TLS_KEY_FILE]
-   --idle_timeout value          The maximum period of having received no request after which the server will shut itself down. (default: 0s, ie disabled) [$BAZEL_REMOTE_IDLE_TIMEOUT]
-   --s3.endpoint value           The S3/minio endpoint to use when using S3 cache backend. [$BAZEL_REMOTE_S3_ENDPOINT]
-   --s3.bucket value             The S3/minio bucket to use when using S3 cache backend. [$BAZEL_REMOTE_S3_BUCKET]
-   --s3.prefix value             The S3/minio object prefix to use when using S3 cache backend. [$BAZEL_REMOTE_S3_PREFIX]
-   --s3.access_key_id value      The S3/minio access key to use when using S3 cache backend. [$BAZEL_REMOTE_S3_ACCESS_KEY_ID]
-   --s3.secret_access_key value  The S3/minio secret access key to use when using S3 cache backend. [$BAZEL_REMOTE_S3_SECRET_ACCESS_KEY]
-   --s3.disable_ssl              Whether to disable TLS/SSL when using the S3 cache backend. (default: false, ie enable TLS/SSL) [$BAZEL_REMOTE_S3_DISABLE_SSL]
-   --s3.iam_role_endpoint value  Endpoint for using IAM security credentials, eg http://169.254.169.254 for EC2, http://169.254.170.2 for ECS. [$BAZEL_REMOTE_S3_IAM_ROLE_ENDPOINT]
-   --s3.region value             The AWS region. Required when using s3.iam_role_endpoint. [$BAZEL_REMOTE_S3_REGION]
-   --disable_http_ac_validation  Whether to disable ActionResult validation for HTTP requests. (default: false, ie enable validation) [$BAZEL_REMOTE_DISABLE_HTTP_AC_VALIDATION]
-   --disable_grpc_ac_deps_check  Whether to disable ActionResult dependency check for gRPC GetActionResult requests. (default: false, ie enable ActionCache dependency checks) [$BAZEL_REMOTE_DISABLE_GRPS_AC_DEPS_CHECK]
-   --enable_endpoint_metrics     Whether to enable metrics for each HTTP/gRPC endpoint. (default: false, ie disable metrics) [$BAZEL_REMOTE_ENABLE_ENDPOINT_METRICS]
-   --help, -h                    show help (default: false)
+   --config_file value              Path to a YAML configuration file. If this flag is specified then all other flags are ignored. [$BAZEL_REMOTE_CONFIG_FILE]
+   --dir value                      Directory path where to store the cache contents. This flag is required. [$BAZEL_REMOTE_DIR]
+   --max_size value                 The maximum size of the remote cache in GiB. This flag is required. (default: -1) [$BAZEL_REMOTE_MAX_SIZE]
+   --host value                     Address to listen on. Listens on all network interfaces by default. [$BAZEL_REMOTE_HOST]
+   --port value                     The port the HTTP server listens on. (default: 8080) [$BAZEL_REMOTE_PORT]
+   --grpc_port value                The port the EXPERIMENTAL gRPC server listens on. Set to 0 to disable. (default: 9092) [$BAZEL_REMOTE_GRPC_PORT]
+   --profile_host value             A host address to listen on for profiling, if enabled by a valid --profile_port setting. (default: "127.0.0.1") [$BAZEL_REMOTE_PROFILE_HOST]
+   --profile_port value             If a positive integer, serve /debug/pprof/* URLs from http://profile_host:profile_port. (default: 0, ie profiling disabled) [$BAZEL_REMOTE_PROFILE_PORT]
+   --htpasswd_file value            Path to a .htpasswd file. This flag is optional. Please read https://httpd.apache.org/docs/2.4/programs/htpasswd.html. [$BAZEL_REMOTE_HTPASSWD_FILE]
+   --tls_enabled                    This flag has been deprecated. Specify tls_cert_file and tls_key_file instead. (default: false) [$BAZEL_REMOTE_TLS_ENABLED]
+   --tls_cert_file value            Path to a pem encoded certificate file. [$BAZEL_REMOTE_TLS_CERT_FILE]
+   --tls_key_file value             Path to a pem encoded key file. [$BAZEL_REMOTE_TLS_KEY_FILE]
+   --idle_timeout value             The maximum period of having received no request after which the server will shut itself down. (default: 0s, ie disabled) [$BAZEL_REMOTE_IDLE_TIMEOUT]
+   --s3.endpoint value              The S3/minio endpoint to use when using S3 cache backend. [$BAZEL_REMOTE_S3_ENDPOINT]
+   --s3.bucket value                The S3/minio bucket to use when using S3 cache backend. [$BAZEL_REMOTE_S3_BUCKET]
+   --s3.prefix value                The S3/minio object prefix to use when using S3 cache backend. [$BAZEL_REMOTE_S3_PREFIX]
+   --s3.access_key_id value         The S3/minio access key to use when using S3 cache backend. [$BAZEL_REMOTE_S3_ACCESS_KEY_ID]
+   --s3.secret_access_key value     The S3/minio secret access key to use when using S3 cache backend. [$BAZEL_REMOTE_S3_SECRET_ACCESS_KEY]
+   --s3.disable_ssl                 Whether to disable TLS/SSL when using the S3 cache backend. (default: false, ie enable TLS/SSL) [$BAZEL_REMOTE_S3_DISABLE_SSL]
+   --s3.iam_role_endpoint value     Endpoint for using IAM security credentials, eg http://169.254.169.254 for EC2, http://169.254.170.2 for ECS. [$BAZEL_REMOTE_S3_IAM_ROLE_ENDPOINT]
+   --s3.region value                The AWS region. Required when using s3.iam_role_endpoint. [$BAZEL_REMOTE_S3_REGION]
+   --disable_http_ac_validation     Whether to disable ActionResult validation for HTTP requests. (default: false, ie enable validation) [$BAZEL_REMOTE_DISABLE_HTTP_AC_VALIDATION]
+   --disable_grpc_ac_deps_check     Whether to disable ActionResult dependency checks for gRPC GetActionResult requests. (default: false, ie enable ActionCache dependency checks) [$BAZEL_REMOTE_DISABLE_GRPS_AC_DEPS_CHECK]
+   --enable_endpoint_metrics        Whether to enable metrics for each HTTP/gRPC endpoint. (default: false, ie disable metrics) [$BAZEL_REMOTE_ENABLE_ENDPOINT_METRICS]
+   --experimental_remote_asset_api  Whether to enable the experimental remote asset API implementation. (default: false, ie remote asset API disabled)
+   --help, -h                       show help (default: false)
 ```
 
 ### Example configuration file
diff --git a/main.go b/main.go
index f080a4f..309d27f 100644
--- a/main.go
+++ b/main.go
@@ -206,6 +206,11 @@ func main() {
 			DefaultText: "false, ie disable metrics",
 			EnvVars:     []string{"BAZEL_REMOTE_ENABLE_ENDPOINT_METRICS"},
 		},
+		&cli.BoolFlag{
+			Name:        "experimental_remote_asset_api",
+			Usage:       "Whether to enable the experimental remote asset API implementation.",
+			DefaultText: "false, ie disable remote asset API",
+		},
 	}
 
 	app.Action = func(ctx *cli.Context) error {
@@ -383,8 +388,16 @@ func main() {
 				}
 				log.Println("gRPC AC dependency checks:", validateStatus)
 
+				enableRemoteAssetAPI := ctx.Bool("experimental_remote_asset_api")
+				remoteAssetStatus := "disabled"
+				if enableRemoteAssetAPI {
+					remoteAssetStatus = "enabled"
+				}
+				log.Println("experimental gRPC remote asset API:", remoteAssetStatus)
+
 				err3 := server.ListenAndServeGRPC(addr, opts,
 					validateAC,
+					enableRemoteAssetAPI,
 					diskCache, accessLogger, errorLogger)
 				if err3 != nil {
 					log.Fatal(err3)
diff --git a/server/BUILD.bazel b/server/BUILD.bazel
index 3e213f7..e569339 100644
--- a/server/BUILD.bazel
+++ b/server/BUILD.bazel
@@ -5,6 +5,7 @@ go_library(
     srcs = [
         "grpc.go",
         "grpc_ac.go",
+        "grpc_asset.go",
         "grpc_basic_auth.go",
         "grpc_bytestream.go",
         "grpc_cas.go",
@@ -18,6 +19,7 @@ go_library(
         "//cache/disk:go_default_library",
         "//utils/idle:go_default_library",
         "@com_github_abbot_go_http_auth//:go_default_library",
+        "@com_github_bazelbuild_remote_apis//build/bazel/remote/asset/v1:go_default_library",
         "@com_github_bazelbuild_remote_apis//build/bazel/remote/execution/v2:go_default_library",
         "@com_github_bazelbuild_remote_apis//build/bazel/semver:go_default_library",
         "@com_github_golang_protobuf//jsonpb:go_default_library_gen",
@@ -37,6 +39,7 @@ go_library(
 go_test(
     name = "go_default_test",
     srcs = [
+        "grpc_asset_test.go",
         "grpc_test.go",
         "http_test.go",
     ],
@@ -45,6 +48,7 @@ go_test(
         "//cache:go_default_library",
         "//cache/disk:go_default_library",
         "//utils:go_default_library",
+        "@com_github_bazelbuild_remote_apis//build/bazel/remote/asset/v1:go_default_library",
         "@com_github_bazelbuild_remote_apis//build/bazel/remote/execution/v2:go_default_library",
         "@com_github_golang_protobuf//proto:go_default_library",
         "@com_github_google_uuid//:go_default_library",
diff --git a/server/grpc.go b/server/grpc.go
index 0b0b137..a3886c9 100644
--- a/server/grpc.go
+++ b/server/grpc.go
@@ -12,6 +12,7 @@ import (
 	_ "google.golang.org/grpc/encoding/gzip" // Register gzip support.
 	"google.golang.org/grpc/status"
 
+	asset "github.com/bazelbuild/remote-apis/build/bazel/remote/asset/v1"
 	pb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
 	"github.com/bazelbuild/remote-apis/build/bazel/semver"
 
@@ -41,6 +42,7 @@ type grpcServer struct {
 // blocking call to https://godoc.org/google.golang.org/grpc#Server.Serve
 func ListenAndServeGRPC(addr string, opts []grpc.ServerOption,
 	validateACDeps bool,
+	enableRemoteAssetAPI bool,
 	c *disk.Cache, a cache.Logger, e cache.Logger) error {
 
 	listener, err := net.Listen("tcp", addr)
@@ -48,11 +50,12 @@ func ListenAndServeGRPC(addr string, opts []grpc.ServerOption,
 		return err
 	}
 
-	return serveGRPC(listener, opts, validateACDeps, c, a, e)
+	return serveGRPC(listener, opts, validateACDeps, enableRemoteAssetAPI, c, a, e)
 }
 
 func serveGRPC(l net.Listener, opts []grpc.ServerOption,
 	validateACDepsCheck bool,
+	enableRemoteAssetAPI bool,
 	c *disk.Cache, a cache.Logger, e cache.Logger) error {
 
 	srv := grpc.NewServer(opts...)
@@ -64,6 +67,9 @@ func serveGRPC(l net.Listener, opts []grpc.ServerOption,
 	pb.RegisterCapabilitiesServer(srv, s)
 	pb.RegisterContentAddressableStorageServer(srv, s)
 	bytestream.RegisterByteStreamServer(srv, s)
+	if enableRemoteAssetAPI {
+		asset.RegisterFetchServer(srv, s)
+	}
 	return srv.Serve(l)
 }
 
diff --git a/server/grpc_asset.go b/server/grpc_asset.go
new file mode 100644
index 0000000..6cc2f8e
--- /dev/null
+++ b/server/grpc_asset.go
@@ -0,0 +1,197 @@
+package server
+
+import (
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/hex"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"path/filepath"
+	"strings"
+
+	"google.golang.org/genproto/googleapis/rpc/status"
+	"google.golang.org/grpc/codes"
+
+	asset "github.com/bazelbuild/remote-apis/build/bazel/remote/asset/v1"
+	pb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
+
+	"github.com/buchgr/bazel-remote/cache"
+)
+
+// FetchServer implementation
+
+func (s *grpcServer) FetchBlob(ctx context.Context, req *asset.FetchBlobRequest) (*asset.FetchBlobResponse, error) {
+
+	var sha256Str string
+
+	// Q: which combinations of qualifiers to support?
+	// * simple file, identified by sha256 SRI AND/OR recognisable URL
+	// * git repository, identified by ???
+	// * go repository, identified by tag/branch/???
+
+	// "strong" identifiers:
+	// checksum.sri -> direct lookup for sha256 (easy), indirect lookup for
+	//     others (eg sha256 of the SRI hash).
+	// vcs.commit + .git extension -> indirect lookup? or sha1 lookup?
+	//     But this could waste a lot of space.
+	//
+	// "weak" identifiers:
+	// vcs.branch + .git extension -> indirect lookup, with timeout check
+	//    directory: limit one of the vcs.* returns
+	//               insert to tree into the CAS?
+	//
+	//    git archive --format=tar --remote=http://foo/bar.git ref dir...
+
+	// For TTL items, we need another (persistent) index, eg BadgerDB?
+	// key -> CAS sha256 + timestamp
+	// Should we place a limit on the size of the index?
+
+	for _, q := range req.GetQualifiers() {
+		if q.Name == "checksum.sri" && strings.HasPrefix(q.Value, "sha256-") {
+			// Ref: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+
+			b64hash := strings.TrimPrefix(q.Value, "sha256-")
+
+			decoded, err := base64.StdEncoding.DecodeString(b64hash)
+			if err != nil {
+				s.errorLogger.Printf("failed to base64 decode \"%s\": %v",
+					b64hash, err)
+				continue
+			}
+
+			sha256Str = hex.EncodeToString(decoded)
+
+			found, size := s.cache.Contains(cache.CAS, sha256Str)
+			if !found {
+				continue
+			}
+
+			if size < 0 {
+				// We don't know the size yet (bad http backend?).
+				r, size, err := s.cache.Get(cache.CAS, sha256Str)
+				if r != nil {
+					defer r.Close()
+				}
+				if err != nil || size < 0 {
+					s.errorLogger.Printf("failed to get CAS %s from proxy backend size: %d err: %v",
+						size, sha256Str, err)
+					continue
+				}
+			}
+
+			return &asset.FetchBlobResponse{
+				Status: &status.Status{Code: int32(codes.OK)},
+				BlobDigest: &pb.Digest{
+					Hash:      sha256Str,
+					SizeBytes: size,
+				},
+			}, nil
+		}
+	}
+
+	// Cache miss. See if we can download one of the URIs.
+
+	for _, uri := range req.GetUris() {
+		ok, actualHash, size := s.fetchItem(uri, sha256Str)
+		if ok {
+			return &asset.FetchBlobResponse{
+				Status: &status.Status{Code: int32(codes.OK)},
+				BlobDigest: &pb.Digest{
+					Hash:      actualHash,
+					SizeBytes: size,
+				},
+				Uri: uri,
+			}, nil
+		}
+
+		// Not a simple file. Not yet handled...
+	}
+
+	return &asset.FetchBlobResponse{
+		Status: &status.Status{Code: int32(codes.NotFound)},
+	}, nil
+}
+
+// Simple files (not eg git).
+var simpleExtensions = map[string]struct{}{
+	".gz":  {},
+	".xz":  {},
+	".bz2": {},
+	".tar": {},
+	".zip": {},
+}
+
+func (s *grpcServer) fetchItem(uri string, expectedHash string) (bool, string, int64) {
+	u, err := url.Parse(uri)
+	if err != nil {
+		s.errorLogger.Printf("unable to parse URI: %s err: %v", uri, err)
+		return false, "", int64(-1)
+	}
+
+	if u.Scheme != "http" && u.Scheme != "https" {
+		s.errorLogger.Printf("unsupported URI: %s", uri)
+		return false, "", int64(-1)
+	}
+
+	_, simple := simpleExtensions[filepath.Ext(u.Path)]
+	if !simple {
+		return false, "", int64(-1)
+	}
+
+	resp, err := http.Get(uri)
+	if err != nil {
+		s.errorLogger.Printf("failed to get URI: %s err: %v", uri, err)
+		return false, "", int64(-1)
+	}
+	defer resp.Body.Close()
+	rc := resp.Body
+
+	expectedSize := resp.ContentLength
+	if expectedHash == "" || expectedSize < 0 {
+		// We can't call Put until we know the hash and size.
+
+		data, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			s.errorLogger.Printf("failed to read data: %v", uri)
+			return false, "", int64(-1)
+		}
+
+		expectedSize = int64(len(data))
+		hashBytes := sha256.Sum256(data)
+		hashStr := hex.EncodeToString(hashBytes[:])
+
+		if hashStr != expectedHash {
+			s.errorLogger.Printf("URI data has hash %s, expected %s",
+				hashStr, expectedHash)
+			return false, "", int64(-1)
+		}
+
+		expectedHash = hashStr
+		rc = ioutil.NopCloser(bytes.NewReader(data))
+	}
+
+	err = s.cache.Put(cache.CAS, expectedHash, expectedSize, rc)
+	if err != nil {
+		s.errorLogger.Printf("failed to Put %s: %v", expectedHash, err)
+		return false, "", int64(-1)
+	}
+
+	return true, expectedHash, expectedSize
+}
+
+func (s *grpcServer) FetchDirectory(context.Context, *asset.FetchDirectoryRequest) (*asset.FetchDirectoryResponse, error) {
+	return nil, nil
+}
+
+/* PushServer implementation
+func (s *grpcServer) PushBlob(context.Context, *asset.PushBlobRequest) (*asset.PushBlobResponse, error) {
+	return nil, nil
+}
+
+func (s *grpcServer) PushDirectory(context.Context, *asset.PushDirectoryRequest) (*asset.PushDirectoryResponse, error) {
+	return nil, nil
+}
+*/


Test patch:
diff --git a/server/grpc_asset_test.go b/server/grpc_asset_test.go
new file mode 100644
index 0000000..ab78018
--- /dev/null
+++ b/server/grpc_asset_test.go
@@ -0,0 +1,103 @@
+package server
+
+import (
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	asset "github.com/bazelbuild/remote-apis/build/bazel/remote/asset/v1"
+	//pb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
+
+	"google.golang.org/grpc/codes"
+
+	testutils "github.com/buchgr/bazel-remote/utils"
+)
+
+// These tests rely on TestMain in grpc_test.go.
+
+func TestAssetFetchBlob(t *testing.T) {
+
+	ts := newTestGetServer()
+
+	hexSha256 := strings.TrimSuffix(ts.path, ".tar.gz")
+	hashBytes, err := hex.DecodeString(hexSha256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := asset.FetchBlobRequest{
+		Uris: []string{
+			ts.srv.URL + "/404.unrecognisedextension",
+			ts.srv.URL + "/404.tar.gz",
+			ts.srv.URL + "/" + ts.path, // This URL should work.
+		},
+		Qualifiers: []*asset.Qualifier{
+			{
+				Name: "checksum.sri",
+				Value: "sha256-" +
+					base64.StdEncoding.EncodeToString([]byte(hashBytes)),
+			},
+		},
+	}
+
+	resp, err := assetClient.FetchBlob(ctx, &req)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if resp.Status.GetCode() != int32(codes.OK) {
+		t.Fatal("expected successful fetch")
+	}
+	if resp.BlobDigest == nil {
+		t.Fatal("expected non-bil BlobDigest")
+	}
+	if resp.BlobDigest.Hash != hexSha256 {
+		t.Fatal("mismatching BlobDigest hash returned")
+	}
+}
+
+type testGetServer struct {
+	srv *httptest.Server
+
+	blob []byte
+	path string
+}
+
+func (s *testGetServer) handler(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet && r.Method != http.MethodHead {
+		http.Error(w, "Unsupported method for this test",
+			http.StatusMethodNotAllowed)
+		return
+	}
+
+	if r.URL.Path != "/"+s.path {
+		http.Error(w, "Not found", http.StatusNotFound)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if r.Method == http.MethodHead {
+		w.Header().Set("ContentLength", fmt.Sprintf("%d", len(s.blob)))
+	}
+
+	if r.Method == http.MethodGet {
+		w.Write(s.blob)
+	}
+}
+
+func newTestGetServer() *testGetServer {
+	blob, hash := testutils.RandomDataAndHash(256)
+
+	ts := testGetServer{
+		blob: blob,
+		path: hash + ".tar.gz",
+	}
+	ts.srv = httptest.NewServer(http.HandlerFunc(ts.handler))
+
+	return &ts
+}
diff --git a/server/grpc_test.go b/server/grpc_test.go
index 84b4f69..f17147c 100644
--- a/server/grpc_test.go
+++ b/server/grpc_test.go
@@ -13,6 +13,7 @@ import (
 	"testing"
 	"time"
 
+	asset "github.com/bazelbuild/remote-apis/build/bazel/remote/asset/v1"
 	pb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
 	"github.com/golang/protobuf/proto"
 	"github.com/google/uuid"
@@ -38,11 +39,12 @@ const bufSize = 1024 * 1024
 var (
 	listener *bufconn.Listener
 
-	acClient  pb.ActionCacheClient
-	casClient pb.ContentAddressableStorageClient
-	bsClient  bytestream.ByteStreamClient
-	ctx       = context.Background()
-	diskCache *disk.Cache
+	acClient    pb.ActionCacheClient
+	casClient   pb.ContentAddressableStorageClient
+	bsClient    bytestream.ByteStreamClient
+	assetClient asset.FetchClient
+	ctx         = context.Background()
+	diskCache   *disk.Cache
 
 	badDigestTestCases = []badDigest{
 		{digest: pb.Digest{Hash: ""}, reason: "empty hash"},
@@ -75,12 +77,14 @@ func TestMain(m *testing.M) {
 	listener = bufconn.Listen(bufSize)
 
 	validateAC := true
+	enableRemoteAssetAPI := true
 
 	go func() {
 		err2 := serveGRPC(
 			listener,
 			[]grpc.ServerOption{},
 			validateAC,
+			enableRemoteAssetAPI,
 			diskCache, accessLogger, errorLogger)
 		if err2 != nil {
 			fmt.Println(err2)
@@ -98,6 +102,7 @@ func TestMain(m *testing.M) {
 	casClient = pb.NewContentAddressableStorageClient(conn)
 	acClient = pb.NewActionCacheClient(conn)
 	bsClient = bytestream.NewByteStreamClient(conn)
+	assetClient = asset.NewFetchClient(conn)
 
 	os.Exit(m.Run())
 }