all: use buildlet client interface to enable testing

This change introduces the use of a buildlet client interface in
existing functions. This is being introduced in order to facilitate
testing.

Change-Id: I41cd5a372fc31edbd9bcba1859cdf84308360174
Reviewed-on: https://go-review.googlesource.com/c/build/+/371818
Trust: Carlos Amedee <carlos@golang.org>
Run-TryBot: Carlos Amedee <carlos@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alex Rakoczy <alex@golang.org>
diff --git a/buildenv/envs.go b/buildenv/envs.go
index bfe2b50..b9b2d6e 100644
--- a/buildenv/envs.go
+++ b/buildenv/envs.go
@@ -158,7 +158,7 @@
 // SnapshotURL returns the absolute URL of the .tar.gz containing a
 // built Go tree for the builderType and Go rev (40 character Git
 // commit hash). The tarball is suitable for passing to
-// (*buildlet.Client).PutTarFromURL.
+// (buildlet.Client).PutTarFromURL.
 func (e Environment) SnapshotURL(builderType, rev string) string {
 	return fmt.Sprintf("https://storage.googleapis.com/%s/go/%s/%s.tar.gz", e.SnapBucket, builderType, rev)
 }
diff --git a/buildlet/buildlet.go b/buildlet/buildlet.go
index d29b76f..c9711cb 100644
--- a/buildlet/buildlet.go
+++ b/buildlet/buildlet.go
@@ -86,7 +86,7 @@
 // "https://<ip>". The ipPort field is in the form of "<ip>:<port>". The function
 // will attempt to connect to the buildlet for the lesser of: the default timeout period
 // (10 minutes) or the timeout set in the passed in context.
-func buildletClient(ctx context.Context, buildletURL, ipPort string, opts *VMOpts) (*Client, error) {
+func buildletClient(ctx context.Context, buildletURL, ipPort string, opts *VMOpts) (Client, error) {
 	ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
 	defer cancel()
 	try := 0
diff --git a/buildlet/buildletclient.go b/buildlet/buildletclient.go
index 3af666e..3b8b5b5 100644
--- a/buildlet/buildletclient.go
+++ b/buildlet/buildletclient.go
@@ -24,19 +24,19 @@
 	"golang.org/x/oauth2"
 )
 
-var _ clientBuildlet = (*Client)(nil)
+var _ Client = (*client)(nil)
 
-// NewClient returns a *Client that will manipulate ipPort,
+// 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, kp KeyPair) *Client {
+func NewClient(ipPort string, kp KeyPair) Client {
 	tr := &http.Transport{
 		Dial:            defaultDialer(),
 		DialTLS:         kp.tlsDialer(),
 		IdleConnTimeout: time.Minute,
 	}
-	c := &Client{
+	c := &client{
 		ipPort:     ipPort,
 		tls:        kp,
 		password:   kp.Password(),
@@ -47,7 +47,7 @@
 	return c
 }
 
-func (c *Client) setCommon() {
+func (c *client) setCommon() {
 	c.peerDead = make(chan struct{})
 	c.ctx, c.ctxCancel = context.WithCancel(context.Background())
 }
@@ -56,7 +56,7 @@
 // against this builder fail, or when the client is destroyed with
 // Close. The function fn is never called more than once.
 // SetOnHeartbeatFailure must be set before any use of the buildlet.
-func (c *Client) SetOnHeartbeatFailure(fn func()) {
+func (c *client) SetOnHeartbeatFailure(fn func()) {
 	c.heartbeatFailure = fn
 }
 
@@ -64,7 +64,7 @@
 
 // Closes destroys and closes down the buildlet, destroying all state
 // immediately.
-func (c *Client) Close() error {
+func (c *client) Close() error {
 	// TODO(bradfitz): have a Client-wide Done channel we set on
 	// all outbound HTTP Requests and close it in the once here?
 	// Then if something was in-flight and somebody else Closes,
@@ -92,7 +92,7 @@
 	return nil
 }
 
-func (c *Client) setPeerDead(err error) {
+func (c *client) setPeerDead(err error) {
 	c.setPeerDeadOnce.Do(func() {
 		c.MarkBroken()
 		if err == nil {
@@ -106,26 +106,26 @@
 // SetDescription sets a short description of where the buildlet
 // connection came from.  This is used by the build coordinator status
 // page, mostly for debugging.
-func (c *Client) SetDescription(v string) {
+func (c *client) SetDescription(v string) {
 	c.desc = v
 }
 
 // SetGCEInstanceName sets an instance name for GCE buildlets.
 // This value differs from the buildlet name used in the CLI and web interface.
-func (c *Client) SetGCEInstanceName(v string) {
+func (c *client) SetGCEInstanceName(v string) {
 	c.gceInstanceName = v
 }
 
 // GCEInstanceName gets an instance name for GCE buildlets.
 // This value differs from the buildlet name used in the CLI and web interface.
 // For non-GCE buildlets, this will return an empty string.
-func (c *Client) GCEInstanceName() string {
+func (c *client) GCEInstanceName() string {
 	return c.gceInstanceName
 }
 
 // SetHTTPClient replaces the underlying HTTP client.
 // It should only be called before the Client is used.
-func (c *Client) SetHTTPClient(httpClient *http.Client) {
+func (c *client) SetHTTPClient(httpClient *http.Client) {
 	c.httpClient = httpClient
 }
 
@@ -138,7 +138,7 @@
 // upgrade request. But now that the net/http client supports
 // read/write bodies for protocol upgrades, we could change how ssh
 // works and delete this.
-func (c *Client) SetDialer(dialer func(context.Context) (net.Conn, error)) {
+func (c *client) SetDialer(dialer func(context.Context) (net.Conn, error)) {
 	c.dialer = dialer
 }
 
@@ -153,8 +153,8 @@
 	return net.Dial
 }
 
-// A Client interacts with a single buildlet.
-type Client struct {
+// A client interacts with a single buildlet.
+type client struct {
 	ipPort          string // required, unless remoteBuildlet+baseURL is set
 	tls             KeyPair
 	httpClient      *http.Client
@@ -190,13 +190,13 @@
 // files. See golang.org/issue/19052.
 //
 // SetReleaseMode must be set before using the client.
-func (c *Client) SetReleaseMode(v bool) {
+func (c *client) SetReleaseMode(v bool) {
 	c.releaseMode = v
 }
 
-func (c *Client) String() string {
+func (c *client) String() string {
 	if c == nil {
-		return "(nil *buildlet.Client)"
+		return "(nil buildlet.Client)"
 	}
 	return strings.TrimSpace(c.URL() + " " + c.desc)
 }
@@ -204,12 +204,12 @@
 // RemoteName returns the name of this client's buildlet on the
 // coordinator. If this buildlet isn't a remote buildlet created via
 // gomote, this returns the empty string.
-func (c *Client) RemoteName() string {
+func (c *client) RemoteName() string {
 	return c.remoteBuildlet
 }
 
 // URL returns the buildlet's URL prefix, without a trailing slash.
-func (c *Client) URL() string {
+func (c *client) URL() string {
 	if c.baseURL != "" {
 		return strings.TrimRight(c.baseURL, "/")
 	}
@@ -219,15 +219,15 @@
 	return "http://" + strings.TrimSuffix(c.ipPort, ":80")
 }
 
-func (c *Client) IPPort() string { return c.ipPort }
+func (c *client) IPPort() string { return c.ipPort }
 
-func (c *Client) SetName(name string) { c.name = name }
+func (c *client) SetName(name string) { c.name = name }
 
 // Name returns the name of this buildlet.
 // It returns the first non-empty string from the name given to
 // SetName, its remote buildlet name, its ip:port, or "(unnamed-buildlet)" in the case where
 // ip:port is empty because there's a custom dialer.
-func (c *Client) Name() string {
+func (c *client) Name() string {
 	if c.name != "" {
 		return c.name
 	}
@@ -241,7 +241,7 @@
 }
 
 // MarkBroken marks this client as broken in some way.
-func (c *Client) MarkBroken() {
+func (c *client) MarkBroken() {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 	c.broken = true
@@ -249,20 +249,20 @@
 }
 
 // IsBroken reports whether this client is broken in some way.
-func (c *Client) IsBroken() bool {
+func (c *client) IsBroken() bool {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 	return c.broken
 }
 
-func (c *Client) authUsername() string {
+func (c *client) authUsername() string {
 	if c.authUser != "" {
 		return c.authUser
 	}
 	return "gomote"
 }
 
-func (c *Client) do(req *http.Request) (*http.Response, error) {
+func (c *client) do(req *http.Request) (*http.Response, error) {
 	c.initHeartbeatOnce.Do(c.initHeartbeats)
 	if c.password != "" {
 		req.SetBasicAuth(c.authUsername(), c.password)
@@ -278,7 +278,7 @@
 // and the target type must be a VM type running on GCE. This was primarily
 // created for RDP to Windows machines, but it might get reused for other
 // purposes in the future.
-func (c *Client) ProxyTCP(port int) (io.ReadWriteCloser, error) {
+func (c *client) ProxyTCP(port int) (io.ReadWriteCloser, error) {
 	if c.RemoteName() == "" {
 		return nil, errors.New("ProxyTCP currently only supports gomote-created buildlets")
 	}
@@ -307,23 +307,23 @@
 // ProxyRoundTripper returns a RoundTripper that sends HTTP requests directly
 // through to the underlying buildlet, adding auth and X-Buildlet-Proxy headers
 // as necessary. This is really only intended for use by the coordinator.
-func (c *Client) ProxyRoundTripper() http.RoundTripper {
+func (c *client) ProxyRoundTripper() http.RoundTripper {
 	return proxyRoundTripper{c}
 }
 
 type proxyRoundTripper struct {
-	c *Client
+	c *client
 }
 
 func (p proxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 	return p.c.do(req)
 }
 
-func (c *Client) initHeartbeats() {
+func (c *client) initHeartbeats() {
 	go c.heartbeatLoop()
 }
 
-func (c *Client) heartbeatLoop() {
+func (c *client) heartbeatLoop() {
 	failInARow := 0
 	for {
 		select {
@@ -355,7 +355,7 @@
 
 // doHeaderTimeout calls c.do(req) and returns its results, or
 // errHeaderTimeout if max elapses first.
-func (c *Client) doHeaderTimeout(req *http.Request, max time.Duration) (res *http.Response, err error) {
+func (c *client) doHeaderTimeout(req *http.Request, max time.Duration) (res *http.Response, err error) {
 	type resErr struct {
 		res *http.Response
 		err error
@@ -403,7 +403,7 @@
 }
 
 // doOK sends the request and expects a 200 OK response.
-func (c *Client) doOK(req *http.Request) error {
+func (c *client) doOK(req *http.Request) error {
 	res, err := c.do(req)
 	if err != nil {
 		return err
@@ -421,7 +421,7 @@
 // If dir is empty, they're placed at the root of the buildlet's work directory.
 // The dir is created if necessary.
 // The Reader must be of a tar.gz file.
-func (c *Client) PutTar(ctx context.Context, r io.Reader, dir string) error {
+func (c *client) PutTar(ctx context.Context, r io.Reader, dir string) error {
 	req, err := http.NewRequest("PUT", c.URL()+"/writetgz?dir="+url.QueryEscape(dir), r)
 	if err != nil {
 		return err
@@ -434,7 +434,7 @@
 // If dir is empty, they're placed at the root of the buildlet's work directory.
 // The dir is created if necessary.
 // The url must be of a tar.gz file.
-func (c *Client) PutTarFromURL(ctx context.Context, tarURL, dir string) error {
+func (c *client) PutTarFromURL(ctx context.Context, tarURL, dir string) error {
 	form := url.Values{
 		"url": {tarURL},
 	}
@@ -447,7 +447,7 @@
 }
 
 // Put writes the provided file to path (relative to workdir) and sets mode.
-func (c *Client) Put(ctx context.Context, r io.Reader, path string, mode os.FileMode) error {
+func (c *client) Put(ctx context.Context, r io.Reader, path string, mode os.FileMode) error {
 	param := url.Values{
 		"path": {path},
 		"mode": {fmt.Sprint(int64(mode))},
@@ -461,7 +461,7 @@
 
 // GetTar returns a .tar.gz stream of the given directory, relative to the buildlet's work dir.
 // The provided dir may be empty to get everything.
-func (c *Client) GetTar(ctx context.Context, dir string) (io.ReadCloser, error) {
+func (c *client) GetTar(ctx context.Context, dir string) (io.ReadCloser, error) {
 	var args string
 	if c.releaseMode {
 		args = "&pargzip=0"
@@ -536,7 +536,7 @@
 //
 // If the context's deadline is exceeded, the returned execErr is
 // ErrTimeout.
-func (c *Client) Exec(ctx context.Context, cmd string, opts ExecOpts) (remoteErr, execErr error) {
+func (c *client) Exec(ctx context.Context, cmd string, opts ExecOpts) (remoteErr, execErr error) {
 	var mode string
 	if opts.SystemLevel {
 		mode = "sys"
@@ -629,7 +629,7 @@
 }
 
 // RemoveAll deletes the provided paths, relative to the work directory.
-func (c *Client) RemoveAll(ctx context.Context, paths ...string) error {
+func (c *client) RemoveAll(ctx context.Context, paths ...string) error {
 	if len(paths) == 0 {
 		return nil
 	}
@@ -643,7 +643,7 @@
 }
 
 // DestroyVM shuts down the buildlet and destroys the VM instance.
-func (c *Client) DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) error {
+func (c *client) DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) error {
 	// TODO(bradfitz): move GCE stuff out of this package?
 	gceErrc := make(chan error, 1)
 	buildletErrc := make(chan error, 1)
@@ -696,7 +696,7 @@
 }
 
 // Status returns an Status value describing this buildlet.
-func (c *Client) Status(ctx context.Context) (Status, error) {
+func (c *client) Status(ctx context.Context) (Status, error) {
 	select {
 	case <-c.peerDead:
 		return Status{}, c.deadErr
@@ -728,7 +728,7 @@
 }
 
 // WorkDir returns the absolute path to the buildlet work directory.
-func (c *Client) WorkDir(ctx context.Context) (string, error) {
+func (c *client) WorkDir(ctx context.Context) (string, error) {
 	req, err := http.NewRequest("GET", c.URL()+"/workdir", nil)
 	if err != nil {
 		return "", err
@@ -817,7 +817,7 @@
 // ListDir lists the contents of a directory.
 // The fn callback is run for each entry.
 // The directory dir itself is not included.
-func (c *Client) ListDir(ctx context.Context, dir string, opts ListDirOpts, fn func(DirEntry)) error {
+func (c *client) ListDir(ctx context.Context, dir string, opts ListDirOpts, fn func(DirEntry)) error {
 	param := url.Values{
 		"dir":       {dir},
 		"recursive": {fmt.Sprint(opts.Recursive)},
@@ -845,7 +845,7 @@
 	return sc.Err()
 }
 
-func (c *Client) getDialer() func(context.Context) (net.Conn, error) {
+func (c *client) getDialer() func(context.Context) (net.Conn, error) {
 	if !c.tls.IsZero() {
 		return func(_ context.Context) (net.Conn, error) {
 			return c.tls.tlsDialer()("tcp", c.ipPort)
@@ -857,7 +857,7 @@
 	return c.dialWithNetDial
 }
 
-func (c *Client) dialWithNetDial(ctx context.Context) (net.Conn, error) {
+func (c *client) dialWithNetDial(ctx context.Context) (net.Conn, error) {
 	var d net.Dialer
 	return d.DialContext(ctx, "tcp", c.ipPort)
 }
@@ -865,7 +865,7 @@
 // ConnectSSH opens an SSH connection to the buildlet for the given username.
 // The authorizedPubKey must be a line from an ~/.ssh/authorized_keys file
 // and correspond to the private key to be used to communicate over the net.Conn.
-func (c *Client) ConnectSSH(user, authorizedPubKey string) (net.Conn, error) {
+func (c *client) ConnectSSH(user, authorizedPubKey string) (net.Conn, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
 	defer cancel()
 	conn, err := c.getDialer()(ctx)
@@ -903,6 +903,11 @@
 	return conn, nil
 }
 
+// AddCloseFunc adds an optional extra code to run on close.
+func (c *client) AddCloseFunc(fn func()) {
+	c.closeFuncs = append(c.closeFuncs, fn)
+}
+
 func condRun(fn func()) {
 	if fn != nil {
 		fn()
diff --git a/buildlet/buildletclient_test.go b/buildlet/buildletclient_test.go
index 36b5e18..289daca 100644
--- a/buildlet/buildletclient_test.go
+++ b/buildlet/buildletclient_test.go
@@ -78,7 +78,7 @@
 			}
 			ts.StartTLS()
 			defer ts.Close()
-			c := Client{
+			c := client{
 				ipPort:   strings.TrimPrefix(ts.URL, "https://"),
 				tls:      tc.keyPair,
 				password: tc.password,
@@ -150,7 +150,7 @@
 				w.WriteHeader(http.StatusSwitchingProtocols)
 			}))
 			defer ts.Close()
-			c := Client{
+			c := client{
 				ipPort:   strings.TrimPrefix(ts.URL, "http://"),
 				password: tc.password,
 				authUser: tc.authUser,
diff --git a/buildlet/ec2.go b/buildlet/ec2.go
index b6e6b45..c1a094d 100644
--- a/buildlet/ec2.go
+++ b/buildlet/ec2.go
@@ -39,7 +39,7 @@
 
 // StartNewVM boots a new VM on EC2, waits until the client is accepting connections
 // on the configured port and returns a buildlet client configured communicate with it.
-func (c *EC2Client) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) (*Client, error) {
+func (c *EC2Client) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) (Client, error) {
 	// check required params
 	if opts == nil || opts.TLS.IsZero() {
 		return nil, errors.New("TLS keypair is not set")
diff --git a/buildlet/fakebuildletclient.go b/buildlet/fakebuildletclient.go
index a8522b8..381ed68 100644
--- a/buildlet/fakebuildletclient.go
+++ b/buildlet/fakebuildletclient.go
@@ -15,7 +15,11 @@
 	"golang.org/x/oauth2"
 )
 
-type clientBuildlet interface {
+// Client is an interface that represent the methods exposed by client. The
+// fake buildlet client should be used instead of client when testing things that
+// use the client interface.
+type Client interface {
+	AddCloseFunc(fn func())
 	Close() error
 	ConnectSSH(user, authorizedPubKey string) (net.Conn, error)
 	DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) error
@@ -33,10 +37,13 @@
 	PutTar(ctx context.Context, r io.Reader, dir string) error
 	PutTarFromURL(ctx context.Context, tarURL, dir string) error
 	RemoteName() string
+	RemoveAll(ctx context.Context, paths ...string) error
 	SetDescription(v string)
+	SetDialer(dialer func(context.Context) (net.Conn, error))
 	SetGCEInstanceName(v string)
 	SetHTTPClient(httpClient *http.Client)
 	SetName(name string)
+	SetOnHeartbeatFailure(fn func())
 	SetReleaseMode(v bool)
 	Status(ctx context.Context) (Status, error)
 	String() string
@@ -46,16 +53,27 @@
 
 var errUnimplemented = errors.New("unimplemented function")
 
-var _ clientBuildlet = (*FakeClient)(nil)
+var _ Client = (*FakeClient)(nil)
 
 // FakeClient is a fake buildlet client used for testing. Not all functions are implemented.
 type FakeClient struct {
 	name         string
 	instanceName string
+	closeFuncs   []func()
+}
+
+// AddCloseFunc adds optional extra code to run on close for the fake buildlet.
+func (fc *FakeClient) AddCloseFunc(fn func()) {
+	fc.closeFuncs = append(fc.closeFuncs, fn)
 }
 
 // Close is a fake client closer.
-func (fc *FakeClient) Close() error { return nil }
+func (fc *FakeClient) Close() error {
+	for _, f := range fc.closeFuncs {
+		f()
+	}
+	return nil
+}
 
 // ConnectSSH connects to a fake SSH server.
 func (fc *FakeClient) ConnectSSH(user, authorizedPubKey string) (net.Conn, error) {
@@ -124,6 +142,9 @@
 // SetDescription sets the description on a fake client.
 func (fc *FakeClient) SetDescription(v string) {}
 
+// SetDialer sets the function that creates a new connection to the fake buildlet.
+func (fc *FakeClient) SetDialer(dialer func(context.Context) (net.Conn, error)) {}
+
 // SetGCEInstanceName sets the GCE instance name on a fake client.
 func (fc *FakeClient) SetGCEInstanceName(v string) {
 	fc.instanceName = v
@@ -137,6 +158,9 @@
 	fc.name = name
 }
 
+// SetOnHeartbeatFailure sets a function to be called when heartbeats against this fake buildlet fail.
+func (fc *FakeClient) SetOnHeartbeatFailure(fn func()) {}
+
 // SetReleaseMode sets the release mode on a fake client.
 func (fc *FakeClient) SetReleaseMode(v bool) {}
 
@@ -151,3 +175,6 @@
 
 // WorkDir is the working directory for the fake buildlet.
 func (fc *FakeClient) WorkDir(ctx context.Context) (string, error) { return "", errUnimplemented }
+
+// RemoveAll deletes the provided paths, relative to the work directory for a fake buildlet.
+func (fc *FakeClient) RemoveAll(ctx context.Context, paths ...string) error { return errUnimplemented }
diff --git a/buildlet/gce.go b/buildlet/gce.go
index d1b0533..9847e5f 100644
--- a/buildlet/gce.go
+++ b/buildlet/gce.go
@@ -36,7 +36,7 @@
 
 // StartNewVM boots a new VM on GCE and returns a buildlet client
 // configured to speak to it.
-func StartNewVM(creds *google.Credentials, buildEnv *buildenv.Environment, instName, hostType string, opts VMOpts) (*Client, error) {
+func StartNewVM(creds *google.Credentials, buildEnv *buildenv.Environment, instName, hostType string, opts VMOpts) (Client, error) {
 	ctx := context.TODO()
 	computeService, _ := compute.New(oauth2.NewClient(ctx, creds.TokenSource))
 
@@ -271,7 +271,9 @@
 	if err != nil {
 		return nil, err
 	}
-	client.closeFuncs = append(client.closeFuncs, closeFuncs...)
+	for _, cf := range closeFuncs {
+		client.AddCloseFunc(cf)
+	}
 	return client, nil
 }
 
diff --git a/buildlet/kube.go b/buildlet/kube.go
index 8f9d1ef..509b66c 100644
--- a/buildlet/kube.go
+++ b/buildlet/kube.go
@@ -68,7 +68,7 @@
 
 // StartPod creates a new pod on a Kubernetes cluster and returns a buildlet client
 // configured to speak to it.
-func StartPod(ctx context.Context, kubeClient *kubernetes.Client, podName, hostType string, opts PodOpts) (*Client, error) {
+func StartPod(ctx context.Context, kubeClient *kubernetes.Client, podName, hostType string, opts PodOpts) (Client, error) {
 	conf, ok := dashboard.Hosts[hostType]
 	if !ok || conf.ContainerImage == "" {
 		return nil, fmt.Errorf("invalid builder type %q", hostType)
diff --git a/buildlet/remote.go b/buildlet/remote.go
index 179844f..e8b3676 100644
--- a/buildlet/remote.go
+++ b/buildlet/remote.go
@@ -77,7 +77,7 @@
 //
 // It may expire at any time.
 // To release it, call Client.Close.
-func (cc *CoordinatorClient) CreateBuildlet(builderType string) (*Client, error) {
+func (cc *CoordinatorClient) CreateBuildlet(builderType string) (Client, error) {
 	return cc.CreateBuildletWithStatus(builderType, nil)
 }
 
@@ -90,7 +90,7 @@
 )
 
 // CreateBuildletWithStatus is like CreateBuildlet but accepts an optional status callback.
-func (cc *CoordinatorClient) CreateBuildletWithStatus(builderType string, status func(types.BuildletWaitStatus)) (*Client, error) {
+func (cc *CoordinatorClient) CreateBuildletWithStatus(builderType string, status func(types.BuildletWaitStatus)) (Client, error) {
 	hc, err := cc.client()
 	if err != nil {
 		return nil, err
@@ -197,13 +197,13 @@
 
 // NamedBuildlet returns a buildlet client for the named remote buildlet.
 // Names are not validated. Use Client.Status to check whether the client works.
-func (cc *CoordinatorClient) NamedBuildlet(name string) (*Client, error) {
+func (cc *CoordinatorClient) NamedBuildlet(name string) (Client, error) {
 	hc, err := cc.client()
 	if err != nil {
 		return nil, err
 	}
 	ipPort, _ := cc.instance().TLSHostPort() // must succeed if client did
-	c := &Client{
+	c := &client{
 		baseURL:        "https://" + ipPort,
 		remoteBuildlet: name,
 		httpClient:     hc,
diff --git a/cmd/coordinator/buildstatus.go b/cmd/coordinator/buildstatus.go
index 7ca4873..42f2656 100644
--- a/cmd/coordinator/buildstatus.go
+++ b/cmd/coordinator/buildstatus.go
@@ -97,7 +97,7 @@
 	branch     string    // non-empty for post-submit work
 
 	onceInitHelpers sync.Once // guards call of onceInitHelpersFunc
-	helpers         <-chan *buildlet.Client
+	helpers         <-chan buildlet.Client
 	ctx             context.Context    // used to start the build
 	cancel          context.CancelFunc // used to cancel context; for use by setDone only
 
@@ -107,7 +107,7 @@
 	canceled        bool                // whether this build was forcefully canceled, so errors should be ignored
 	schedItem       *schedule.SchedItem // for the initial buildlet (ignoring helpers for now)
 	logURL          string              // if non-empty, permanent URL of log
-	bc              *buildlet.Client    // nil initially, until pool returns one
+	bc              buildlet.Client     // nil initially, until pool returns one
 	done            time.Time           // finished running
 	succeeded       bool                // set when done
 	output          livelog.Buffer      // stdout and stderr
@@ -288,7 +288,7 @@
 
 // getHelpers returns a channel of buildlet test helpers, with an item
 // sent as they become available. The channel is closed at the end.
-func (st *buildStatus) getHelpers() <-chan *buildlet.Client {
+func (st *buildStatus) getHelpers() <-chan buildlet.Client {
 	st.onceInitHelpers.Do(st.onceInitHelpersFunc)
 	return st.helpers
 }
@@ -377,7 +377,7 @@
 
 var errSkipBuildDueToDeps = errors.New("build was skipped due to missing deps")
 
-func (st *buildStatus) getBuildlet() (*buildlet.Client, error) {
+func (st *buildStatus) getBuildlet() (buildlet.Client, error) {
 	schedItem := &schedule.SchedItem{
 		HostType:   st.conf.HostType,
 		IsTry:      st.trySet != nil,
@@ -790,7 +790,7 @@
 	return nil, nil
 }
 
-func (st *buildStatus) doSnapshot(bc *buildlet.Client) error {
+func (st *buildStatus) doSnapshot(bc buildlet.Client) error {
 	// If we're using a pre-built snapshot, don't make another.
 	if st.useSnapshot() {
 		return nil
@@ -815,7 +815,7 @@
 	return st.writeGoSourceTo(st.bc)
 }
 
-func (st *buildStatus) writeGoSourceTo(bc *buildlet.Client) error {
+func (st *buildStatus) writeGoSourceTo(bc buildlet.Client) error {
 	// Write the VERSION file.
 	sp := st.CreateSpan("write_version_tar")
 	if err := bc.PutTar(st.ctx, buildgo.VersionTgz(st.Rev), "go"); err != nil {
@@ -843,7 +843,7 @@
 	return sp.Done(st.bc.PutTarFromURL(st.ctx, u, bootstrapDir))
 }
 
-func (st *buildStatus) cleanForSnapshot(bc *buildlet.Client) error {
+func (st *buildStatus) cleanForSnapshot(bc buildlet.Client) error {
 	sp := st.CreateSpan("clean_for_snapshot")
 	return sp.Done(bc.RemoveAll(st.ctx,
 		"go/doc/gopher",
@@ -851,7 +851,7 @@
 	))
 }
 
-func (st *buildStatus) writeSnapshot(bc *buildlet.Client) (err error) {
+func (st *buildStatus) writeSnapshot(bc buildlet.Client) (err error) {
 	sp := st.CreateSpan("write_snapshot_to_gcs")
 	defer func() { sp.Done(err) }()
 	// This should happen in 15 seconds or so, but I saw timeouts
@@ -1305,7 +1305,7 @@
 //
 // After runTests completes, the caller must assume that st.bc might be invalid
 // (It's possible that only one of the helper buildlets survived).
-func (st *buildStatus) runTests(helpers <-chan *buildlet.Client) (remoteErr, err error) {
+func (st *buildStatus) runTests(helpers <-chan buildlet.Client) (remoteErr, err error) {
 	testNames, remoteErr, err := st.distTestList()
 	if remoteErr != nil {
 		return fmt.Errorf("distTestList remote: %v", remoteErr), nil
@@ -1358,7 +1358,7 @@
 		defer buildletActivity.Done() // for the per-goroutine Add(2) above
 		for helper := range helpers {
 			buildletActivity.Add(1)
-			go func(bc *buildlet.Client) {
+			go func(bc buildlet.Client) {
 				defer buildletActivity.Done() // for the per-helper Add(1) above
 				defer st.LogEventTime("closed_helper", bc.Name())
 				defer bc.Close()
@@ -1478,7 +1478,7 @@
 const maxTestExecErrors = 3
 
 // runTestsOnBuildlet runs tis on bc, using the optional goroot & gopath environment variables.
-func (st *buildStatus) runTestsOnBuildlet(bc *buildlet.Client, tis []*testItem, goroot, gopath string) {
+func (st *buildStatus) runTestsOnBuildlet(bc buildlet.Client, tis []*testItem, goroot, gopath string) {
 	names := make([]string, len(tis))
 	for i, ti := range tis {
 		names[i] = ti.name
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index c667402..3a9176e 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -1747,8 +1747,8 @@
 
 // getBuildlets creates up to n buildlets and sends them on the returned channel
 // before closing the channel.
-func getBuildlets(ctx context.Context, n int, schedTmpl *schedule.SchedItem, lg pool.Logger) <-chan *buildlet.Client {
-	ch := make(chan *buildlet.Client) // NOT buffered
+func getBuildlets(ctx context.Context, n int, schedTmpl *schedule.SchedItem, lg pool.Logger) <-chan buildlet.Client {
+	ch := make(chan buildlet.Client) // NOT buffered
 	var wg sync.WaitGroup
 	wg.Add(n)
 	for i := 0; i < n; i++ {
diff --git a/cmd/coordinator/remote.go b/cmd/coordinator/remote.go
index 1694dda..c69f2fb 100644
--- a/cmd/coordinator/remote.go
+++ b/cmd/coordinator/remote.go
@@ -73,7 +73,7 @@
 	Created     time.Time
 	Expires     time.Time
 
-	buildlet *buildlet.Client
+	buildlet buildlet.Client
 }
 
 // renew renews rb's idle timeout if ctx hasn't expired.
@@ -183,7 +183,7 @@
 		ticker = t.C
 	}
 
-	resc := make(chan *buildlet.Client)
+	resc := make(chan buildlet.Client)
 	errc := make(chan error)
 
 	hconf := bconf.HostConfig()
diff --git a/cmd/coordinator/remote_test.go b/cmd/coordinator/remote_test.go
index be59c8b..9ee0207 100644
--- a/cmd/coordinator/remote_test.go
+++ b/cmd/coordinator/remote_test.go
@@ -27,13 +27,13 @@
 )
 
 type TestBuildletPool struct {
-	clients map[string]*buildlet.Client
+	clients map[string]buildlet.Client
 	mu      sync.Mutex
 }
 
 // GetBuildlet finds the first available buildlet for the hostType and returns
 // it, or an error if no buildlets are available for that hostType.
-func (tp *TestBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg pool.Logger) (*buildlet.Client, error) {
+func (tp *TestBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg pool.Logger) (buildlet.Client, error) {
 	tp.mu.Lock()
 	defer tp.mu.Unlock()
 	c, ok := tp.clients[hostType]
@@ -45,10 +45,10 @@
 
 // Add sets the given client for the given hostType, overriding any previous
 // entries.
-func (tp *TestBuildletPool) Add(hostType string, client *buildlet.Client) {
+func (tp *TestBuildletPool) Add(hostType string, client buildlet.Client) {
 	tp.mu.Lock()
 	if tp.clients == nil {
-		tp.clients = make(map[string]*buildlet.Client)
+		tp.clients = make(map[string]buildlet.Client)
 	}
 	tp.clients[hostType] = client
 	tp.mu.Unlock()
@@ -103,7 +103,7 @@
 		HostType: "test-host",
 		Owners:   []*gophers.Person{{Emails: []string{"test@golang.org"}}},
 	}
-	testPool.Add("test-host", &buildlet.Client{})
+	testPool.Add("test-host", &buildlet.FakeClient{})
 }
 
 func removeBuilder(name string) {
diff --git a/cmd/debugnewvm/debugnewvm.go b/cmd/debugnewvm/debugnewvm.go
index b04d7e7..1496c9e 100644
--- a/cmd/debugnewvm/debugnewvm.go
+++ b/cmd/debugnewvm/debugnewvm.go
@@ -114,7 +114,7 @@
 	name := fmt.Sprintf("debug-temp-%d", time.Now().Unix())
 
 	log.Printf("Creating %s (with VM image %s)", name, vmImageSummary)
-	var bc *buildlet.Client
+	var bc buildlet.Client
 	if hconf.IsEC2() {
 		region := env.AWSRegion
 		if *awsRegion != "" {
@@ -270,7 +270,7 @@
 	return keyID, accessKey, nil
 }
 
-func gceBuildlet(creds *google.Credentials, env *buildenv.Environment, name, hostType, zone string) (*buildlet.Client, error) {
+func gceBuildlet(creds *google.Credentials, env *buildenv.Environment, name, hostType, zone string) (buildlet.Client, error) {
 	var zoneSelected string
 	return buildlet.StartNewVM(creds, env, name, hostType, buildlet.VMOpts{
 		Zone:                zone,
@@ -299,7 +299,7 @@
 	})
 }
 
-func ec2Buildlet(ctx context.Context, ec2Client *buildlet.EC2Client, hconf *dashboard.HostConfig, env *buildenv.Environment, name, hostType, zone string) (*buildlet.Client, error) {
+func ec2Buildlet(ctx context.Context, ec2Client *buildlet.EC2Client, hconf *dashboard.HostConfig, env *buildenv.Environment, name, hostType, zone string) (buildlet.Client, error) {
 	kp, err := buildlet.NewKeyPair()
 	if err != nil {
 		log.Fatalf("key pair failed: %v", err)
diff --git a/cmd/gomote/list.go b/cmd/gomote/list.go
index 62a5bf2..ab99b11 100644
--- a/cmd/gomote/list.go
+++ b/cmd/gomote/list.go
@@ -49,7 +49,7 @@
 // As a special case, if name contains '@', the name is expected to be
 // of the form <build-config-name>@ip[:port]. For example,
 // "windows-amd64-race@10.0.0.1".
-func remoteClient(name string) (*buildlet.Client, error) {
+func remoteClient(name string) (buildlet.Client, error) {
 	bc, _, err := clientAndCondConf(name, false)
 	return bc, err
 }
@@ -61,11 +61,11 @@
 // As a special case, if name contains '@', the name is expected to be
 // of the form <build-config-name>@ip[:port]. For example,
 // "windows-amd64-race@10.0.0.1".
-func clientAndConf(name string) (bc *buildlet.Client, conf *dashboard.BuildConfig, err error) {
+func clientAndConf(name string) (bc buildlet.Client, conf *dashboard.BuildConfig, err error) {
 	return clientAndCondConf(name, true)
 }
 
-func clientAndCondConf(name string, withConf bool) (bc *buildlet.Client, conf *dashboard.BuildConfig, err error) {
+func clientAndCondConf(name string, withConf bool) (bc buildlet.Client, conf *dashboard.BuildConfig, err error) {
 	if strings.Contains(name, "@") {
 		f := strings.SplitN(name, "@", 2)
 		if len(f) != 2 {
diff --git a/cmd/gomote/rdp.go b/cmd/gomote/rdp.go
index 4ecae62..cdfa14c 100644
--- a/cmd/gomote/rdp.go
+++ b/cmd/gomote/rdp.go
@@ -53,7 +53,7 @@
 	}
 }
 
-func handleRDPConn(bc *buildlet.Client, c net.Conn) {
+func handleRDPConn(bc buildlet.Client, c net.Conn) {
 	const Lmsgprefix = 64 // new in Go 1.14, harmless before
 	log := log.New(os.Stderr, c.RemoteAddr().String()+": ", log.LstdFlags|Lmsgprefix)
 	log.Printf("accepted connection, dialing buildlet via coordinator proxy...")
diff --git a/cmd/perfrun/perfrun.go b/cmd/perfrun/perfrun.go
index afe019c..efeb35e 100644
--- a/cmd/perfrun/perfrun.go
+++ b/cmd/perfrun/perfrun.go
@@ -112,7 +112,7 @@
 	return nil
 }
 
-func namedClient(name string) (*buildlet.Client, error) {
+func namedClient(name string) (buildlet.Client, error) {
 	if strings.Contains(name, ":") {
 		return buildlet.NewClient(name, buildlet.NoKeyPair), nil
 	}
diff --git a/cmd/release/release.go b/cmd/release/release.go
index 72f4e7c..004aecb 100644
--- a/cmd/release/release.go
+++ b/cmd/release/release.go
@@ -312,7 +312,7 @@
 	"misc/makerelease",
 }
 
-func (b *Build) buildlet() (*buildlet.Client, error) {
+func (b *Build) buildlet() (buildlet.Client, error) {
 	b.logf("Creating buildlet.")
 	bc, err := coordClient.CreateBuildlet(b.Builder)
 	if err != nil {
@@ -698,7 +698,7 @@
 
 // checkTopLevelDirs checks that all files under client's "."
 // ($WORKDIR) are are under "go/".
-func (b *Build) checkTopLevelDirs(ctx context.Context, client *buildlet.Client) error {
+func (b *Build) checkTopLevelDirs(ctx context.Context, client buildlet.Client) error {
 	var badFileErr error // non-nil once an unexpected file/dir is found
 	if err := client.ListDir(ctx, ".", buildlet.ListDirOpts{Recursive: true}, func(ent buildlet.DirEntry) {
 		name := ent.Name()
@@ -716,7 +716,7 @@
 
 // checkPerm checks that files in client's $WORKDIR/go directory
 // have expected permissions.
-func (b *Build) checkPerm(ctx context.Context, client *buildlet.Client) error {
+func (b *Build) checkPerm(ctx context.Context, client buildlet.Client) error {
 	var badPermErr error // non-nil once an unexpected perm is found
 	checkPerm := func(ent buildlet.DirEntry, allowed ...string) {
 		for _, p := range allowed {
@@ -754,7 +754,7 @@
 	return badPermErr
 }
 
-func (b *Build) fetchTarball(ctx context.Context, client *buildlet.Client, dest string) error {
+func (b *Build) fetchTarball(ctx context.Context, client buildlet.Client, dest string) error {
 	b.logf("Downloading tarball.")
 	tgz, err := client.GetTar(ctx, ".")
 	if err != nil {
@@ -763,7 +763,7 @@
 	return b.writeFile(dest, tgz)
 }
 
-func (b *Build) fetchZip(client *buildlet.Client, dest string) error {
+func (b *Build) fetchZip(client buildlet.Client, dest string) error {
 	b.logf("Downloading tarball and re-compressing as zip.")
 
 	tgz, err := client.GetTar(context.Background(), ".")
@@ -835,7 +835,7 @@
 
 // fetchFile fetches the specified directory from the given buildlet, and
 // writes the first file it finds in that directory to dest.
-func (b *Build) fetchFile(client *buildlet.Client, dest, dir string) error {
+func (b *Build) fetchFile(client buildlet.Client, dest, dir string) error {
 	b.logf("Downloading file from %q.", dir)
 	tgz, err := client.GetTar(context.Background(), dir)
 	if err != nil {
@@ -886,7 +886,7 @@
 // checkRelocations runs readelf on pkg/linux_amd64/runtime/cgo.a and makes sure
 // we don't see R_X86_64_REX_GOTPCRELX in new Go 1.15 minor releases.
 // See golang.org/issue/31293 and golang.org/issue/40561#issuecomment-731482962.
-func (b *Build) checkRelocations(client *buildlet.Client) error {
+func (b *Build) checkRelocations(client buildlet.Client) error {
 	if b.OS != "linux" || b.Arch != "amd64" || b.TestOnly {
 		// This check is only applicable to linux/amd64 builds.
 		// However, skip it on test-only builds because they
diff --git a/internal/buildgo/buildgo.go b/internal/buildgo/buildgo.go
index 2e875de..9f20e5f 100644
--- a/internal/buildgo/buildgo.go
+++ b/internal/buildgo/buildgo.go
@@ -100,7 +100,7 @@
 // goroot is relative to the workdir with forward slashes.
 // w is the Writer to send build output to.
 // remoteErr and err are as described at the top of this file.
-func (gb GoBuilder) RunMake(ctx context.Context, bc *buildlet.Client, w io.Writer) (remoteErr, err error) {
+func (gb GoBuilder) RunMake(ctx context.Context, bc buildlet.Client, w io.Writer) (remoteErr, err error) {
 	// Build the source code.
 	makeSpan := gb.CreateSpan("make", gb.Conf.MakeScript())
 	remoteErr, err = bc.Exec(ctx, path.Join(gb.Goroot, gb.Conf.MakeScript()), buildlet.ExecOpts{
@@ -152,7 +152,7 @@
 // with -gcflags=-c=8 using a race-enabled cmd/compile and cmd/link
 // (built by caller, RunMake, per builder config).
 // The idea is that this might find data races in cmd/compile and cmd/link.
-func (gb GoBuilder) runConcurrentGoBuildStdCmd(ctx context.Context, bc *buildlet.Client, w io.Writer) (remoteErr, err error) {
+func (gb GoBuilder) runConcurrentGoBuildStdCmd(ctx context.Context, bc buildlet.Client, w io.Writer) (remoteErr, err error) {
 	span := gb.CreateSpan("go_build_c128_std_cmd")
 	remoteErr, err = bc.Exec(ctx, path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
 		Output:   w,
diff --git a/internal/coordinator/pool/ec2.go b/internal/coordinator/pool/ec2.go
index 84f6d3b..a134103 100644
--- a/internal/coordinator/pool/ec2.go
+++ b/internal/coordinator/pool/ec2.go
@@ -100,7 +100,7 @@
 
 // ec2BuildletClient represents an EC2 buildlet client in the buildlet package.
 type ec2BuildletClient interface {
-	StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *buildlet.VMOpts) (*buildlet.Client, error)
+	StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *buildlet.VMOpts) (buildlet.Client, error)
 }
 
 // NewEC2Buildlet creates a new EC2 buildlet pool used to create and manage the lifecycle of
@@ -162,7 +162,7 @@
 }
 
 // GetBuildlet retrieves a buildlet client for a newly created buildlet.
-func (eb *EC2Buildlet) GetBuildlet(ctx context.Context, hostType string, lg Logger) (*buildlet.Client, error) {
+func (eb *EC2Buildlet) GetBuildlet(ctx context.Context, hostType string, lg Logger) (buildlet.Client, error) {
 	hconf, ok := eb.hosts[hostType]
 	if !ok {
 		return nil, fmt.Errorf("ec2 pool: unknown host type %q", hostType)
diff --git a/internal/coordinator/pool/ec2_test.go b/internal/coordinator/pool/ec2_test.go
index a3d4fcc..cb40d53 100644
--- a/internal/coordinator/pool/ec2_test.go
+++ b/internal/coordinator/pool/ec2_test.go
@@ -563,7 +563,7 @@
 
 // StartNewVM boots a new VM on EC2, waits until the client is accepting connections
 // on the configured port and returns a buildlet client configured communicate with it.
-func (f *fakeEC2BuildletClient) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *buildlet.VMOpts) (*buildlet.Client, error) {
+func (f *fakeEC2BuildletClient) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *buildlet.VMOpts) (buildlet.Client, error) {
 	// check required params
 	if opts == nil || opts.TLS.IsZero() {
 		return nil, errors.New("TLS keypair is not set")
@@ -619,7 +619,7 @@
 			Zone: "zone-a",
 		})
 	}
-	return &buildlet.Client{}, nil
+	return &buildlet.FakeClient{}, nil
 }
 
 type testLogger struct {
diff --git a/internal/coordinator/pool/gce.go b/internal/coordinator/pool/gce.go
index a903e4c..8f064f0 100644
--- a/internal/coordinator/pool/gce.go
+++ b/internal/coordinator/pool/gce.go
@@ -387,7 +387,7 @@
 }
 
 // GetBuildlet retrieves a buildlet client for an available buildlet.
-func (p *GCEBuildlet) GetBuildlet(ctx context.Context, hostType string, lg Logger) (bc *buildlet.Client, err error) {
+func (p *GCEBuildlet) GetBuildlet(ctx context.Context, hostType string, lg Logger) (bc buildlet.Client, err error) {
 	hconf, ok := dashboard.Hosts[hostType]
 	if !ok {
 		return nil, fmt.Errorf("gcepool: unknown host type %q", hostType)
@@ -454,7 +454,7 @@
 	return bc, nil
 }
 
-func (p *GCEBuildlet) putBuildlet(bc *buildlet.Client, hostType, zone, instName string) error {
+func (p *GCEBuildlet) putBuildlet(bc buildlet.Client, hostType, zone, instName string) error {
 	// TODO(bradfitz): add the buildlet to a freelist (of max N
 	// items) for up to 10 minutes since when it got started if
 	// it's never seen a command execution failure, and we can
diff --git a/internal/coordinator/pool/kube.go b/internal/coordinator/pool/kube.go
index 6bc0cb6..11f713c 100644
--- a/internal/coordinator/pool/kube.go
+++ b/internal/coordinator/pool/kube.go
@@ -245,7 +245,7 @@
 
 }
 
-func (p *kubeBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg Logger) (*buildlet.Client, error) {
+func (p *kubeBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg Logger) (buildlet.Client, error) {
 	hconf, ok := dashboard.Hosts[hostType]
 	if !ok || !hconf.IsContainer() {
 		return nil, fmt.Errorf("kubepool: invalid host type %q", hostType)
diff --git a/internal/coordinator/pool/pool.go b/internal/coordinator/pool/pool.go
index 6fe7380..a084a16 100644
--- a/internal/coordinator/pool/pool.go
+++ b/internal/coordinator/pool/pool.go
@@ -35,7 +35,7 @@
 	//
 	// The ctx may have context values of type buildletTimeoutOpt
 	// and highPriorityOpt.
-	GetBuildlet(ctx context.Context, hostType string, lg Logger) (*buildlet.Client, error)
+	GetBuildlet(ctx context.Context, hostType string, lg Logger) (buildlet.Client, error)
 
 	String() string // TODO(bradfitz): more status stuff
 }
diff --git a/internal/coordinator/pool/reverse.go b/internal/coordinator/pool/reverse.go
index dfef9dc..2a96d92 100644
--- a/internal/coordinator/pool/reverse.go
+++ b/internal/coordinator/pool/reverse.go
@@ -57,7 +57,7 @@
 
 var (
 	reversePool = &ReverseBuildletPool{
-		oldInUse:     make(map[*buildlet.Client]bool),
+		oldInUse:     make(map[buildlet.Client]bool),
 		hostLastGood: make(map[string]time.Time),
 	}
 
@@ -94,7 +94,7 @@
 	// oldInUse tracks which buildlets with the old revdial code are currently in use.
 	// These are a liability due to runaway memory issues (Issue 31639) so
 	// we bound how many can be running at once. Fortunately there aren't many left.
-	oldInUse map[*buildlet.Client]bool
+	oldInUse map[buildlet.Client]bool
 
 	// hostLastGood tracks when buildlets were last seen to be
 	// healthy. It's only used by the health reporting code (in
@@ -179,7 +179,7 @@
 //
 // Otherwise it returns how many were busy, which might be 0 if none
 // were (yet?) registered. The busy valid is only valid if bc == nil.
-func (p *ReverseBuildletPool) tryToGrab(hostType string) (bc *buildlet.Client, busy int) {
+func (p *ReverseBuildletPool) tryToGrab(hostType string) (bc buildlet.Client, busy int) {
 	p.mu.Lock()
 	defer p.mu.Unlock()
 	for _, b := range p.buildlets {
@@ -229,7 +229,7 @@
 // nukeBuildlet wipes out victim as a buildlet we'll ever return again,
 // and closes its TCP connection in hopes that it will fix itself
 // later.
-func (p *ReverseBuildletPool) nukeBuildlet(victim *buildlet.Client) {
+func (p *ReverseBuildletPool) nukeBuildlet(victim buildlet.Client) {
 	p.mu.Lock()
 	defer p.mu.Unlock()
 	delete(p.oldInUse, victim)
@@ -326,7 +326,7 @@
 }
 
 // GetBuildlet builds a buildlet client for the passed in host.
-func (p *ReverseBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg Logger) (*buildlet.Client, error) {
+func (p *ReverseBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg Logger) (buildlet.Client, error) {
 	p.updateWaiterCounter(hostType, 1)
 	defer p.updateWaiterCounter(hostType, -1)
 	seenErrInUse := false
@@ -355,7 +355,7 @@
 	}
 }
 
-func (p *ReverseBuildletPool) cleanedBuildlet(b *buildlet.Client, lg Logger) (*buildlet.Client, error) {
+func (p *ReverseBuildletPool) cleanedBuildlet(b buildlet.Client, lg Logger) (buildlet.Client, error) {
 	// Clean up any files from previous builds.
 	sp := lg.CreateSpan("clean_buildlet", b.String())
 	err := b.RemoveAll(context.Background(), ".")
@@ -534,7 +534,7 @@
 	// sessRand is the unique random number for every unique buildlet session.
 	sessRand string
 
-	client  *buildlet.Client
+	client  buildlet.Client
 	conn    net.Conn
 	regTime time.Time // when it was first connected
 
diff --git a/internal/coordinator/remote/remote.go b/internal/coordinator/remote/remote.go
index e3b4572..4435440 100644
--- a/internal/coordinator/remote/remote.go
+++ b/internal/coordinator/remote/remote.go
@@ -13,6 +13,7 @@
 	"sync"
 	"time"
 
+	"golang.org/x/build/buildlet"
 	"golang.org/x/build/internal"
 )
 
@@ -21,21 +22,12 @@
 	remoteBuildletCleanInterval = time.Minute
 )
 
-// BuildletClient is used in order to enable tests. The interface should contain all the buildlet.Client
-// functions used by the callers.
-type BuildletClient interface {
-	Close() error
-	GCEInstanceName() string
-	SetGCEInstanceName(v string)
-	SetName(name string)
-}
-
 // Session stores the metadata for a remote buildlet session.
 type Session struct {
 	mu sync.Mutex
 
 	builderType string // default builder config to use if not overwritten
-	buildlet    BuildletClient
+	buildlet    buildlet.Client
 	created     time.Time
 	expires     time.Time
 	hostType    string
@@ -69,7 +61,7 @@
 }
 
 // Buildlet returns the buildlet client associated with the Session.
-func (s *Session) Buildlet() BuildletClient {
+func (s *Session) Buildlet() buildlet.Client {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
@@ -114,7 +106,7 @@
 }
 
 // AddSession adds the provided session to the session pool.
-func (sp *SessionPool) AddSession(user, builderType, hostType string, bc BuildletClient) (name string) {
+func (sp *SessionPool) AddSession(user, builderType, hostType string, bc buildlet.Client) (name string) {
 	sp.mu.Lock()
 	defer sp.mu.Unlock()
 
diff --git a/internal/coordinator/schedule/fake_schedule.go b/internal/coordinator/schedule/fake_schedule.go
index bf30884..bd6d30d 100644
--- a/internal/coordinator/schedule/fake_schedule.go
+++ b/internal/coordinator/schedule/fake_schedule.go
@@ -29,6 +29,6 @@
 }
 
 // GetBuildlet returns a fake buildlet client for the requested buildlet.
-func (f *Fake) GetBuildlet(ctx context.Context, si *SchedItem) (*buildlet.Client, error) {
-	return &buildlet.Client{}, nil
+func (f *Fake) GetBuildlet(ctx context.Context, si *SchedItem) (buildlet.Client, error) {
+	return &buildlet.FakeClient{}, nil
 }
diff --git a/internal/coordinator/schedule/schedule.go b/internal/coordinator/schedule/schedule.go
index 40a4297..a1dcf18 100644
--- a/internal/coordinator/schedule/schedule.go
+++ b/internal/coordinator/schedule/schedule.go
@@ -48,7 +48,7 @@
 	HostType string
 
 	// One of Client or Err gets set:
-	Client *buildlet.Client
+	Client buildlet.Client
 	Err    error
 }
 
@@ -348,18 +348,18 @@
 	// wantRes is the unbuffered channel that's passed
 	// synchronously from Scheduler.GetBuildlet to
 	// Scheduler.matchBuildlet. Its value is a channel (whose
-	// buffering doesn't matter) to pass over a *buildlet.Client
+	// buffering doesn't matter) to pass over a buildlet.Client
 	// just obtained from a BuildletPool. The contract to use
 	// wantRes is that the sender must have a result already
 	// available to send on the inner channel, and the receiver
 	// still wants it (their context hasn't expired).
-	wantRes chan chan<- *buildlet.Client
+	wantRes chan chan<- buildlet.Client
 }
 
 // GetBuildlet requests a buildlet with the parameters described in si.
 //
 // The provided si must be newly allocated; ownership passes to the scheduler.
-func (s *Scheduler) GetBuildlet(ctx context.Context, si *SchedItem) (*buildlet.Client, error) {
+func (s *Scheduler) GetBuildlet(ctx context.Context, si *SchedItem) (buildlet.Client, error) {
 	hostConf, ok := dashboard.Hosts[si.HostType]
 	if !ok && pool.TestPoolHook == nil {
 		return nil, fmt.Errorf("invalid SchedItem.HostType %q", si.HostType)
@@ -370,11 +370,11 @@
 	si.s = s
 	si.requestTime = time.Now()
 	si.ctxDone = ctx.Done()
-	si.wantRes = make(chan chan<- *buildlet.Client) // unbuffered
+	si.wantRes = make(chan chan<- buildlet.Client) // unbuffered
 
 	s.addWaiter(si)
 
-	ch := make(chan *buildlet.Client)
+	ch := make(chan buildlet.Client)
 	select {
 	case si.wantRes <- ch:
 		// No need to call removeWaiter. If we're here, the
diff --git a/internal/coordinator/schedule/schedule_test.go b/internal/coordinator/schedule/schedule_test.go
index 94eb295..99ded27 100644
--- a/internal/coordinator/schedule/schedule_test.go
+++ b/internal/coordinator/schedule/schedule_test.go
@@ -136,7 +136,7 @@
 	ctxCancel context.CancelFunc
 
 	done      chan struct{} // closed when call done
-	gotClient *buildlet.Client
+	gotClient buildlet.Client
 	gotErr    error
 }
 
@@ -206,16 +206,16 @@
 	}
 }
 
-type poolChan map[string]chan interface{} // hostType -> { *buildlet.Client | error}
+type poolChan map[string]chan interface{} // hostType -> { buildlet.Client | error}
 
-func (m poolChan) GetBuildlet(ctx context.Context, hostType string, lg cpool.Logger) (*buildlet.Client, error) {
+func (m poolChan) GetBuildlet(ctx context.Context, hostType string, lg cpool.Logger) (buildlet.Client, error) {
 	c, ok := m[hostType]
 	if !ok {
 		return nil, fmt.Errorf("pool doesn't support host type %q", hostType)
 	}
 	select {
 	case v := <-c:
-		if c, ok := v.(*buildlet.Client); ok {
+		if c, ok := v.(buildlet.Client); ok {
 			return c, nil
 		}
 		return nil, v.(error)