golang.org/x/build@v0.0.0-20240506185731-218518f32b70/buildlet/grpcbuildlet.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package buildlet
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"mime/multipart"
    14  	"net/http"
    15  	"os"
    16  	"strings"
    17  
    18  	"golang.org/x/build/internal/gomote/protos"
    19  	"golang.org/x/build/types"
    20  	"google.golang.org/grpc/codes"
    21  	"google.golang.org/grpc/status"
    22  )
    23  
    24  type GRPCCoordinatorClient struct {
    25  	Client protos.GomoteServiceClient
    26  }
    27  
    28  func (c *GRPCCoordinatorClient) CreateBuildlet(ctx context.Context, builderType string) (RemoteClient, error) {
    29  	return c.CreateBuildletWithStatus(ctx, builderType, func(types.BuildletWaitStatus) {})
    30  }
    31  
    32  func (c *GRPCCoordinatorClient) CreateBuildletWithStatus(ctx context.Context, builderType string, status func(types.BuildletWaitStatus)) (RemoteClient, error) {
    33  	stream, err := c.Client.CreateInstance(ctx, &protos.CreateInstanceRequest{BuilderType: builderType})
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  	var instance *protos.Instance
    38  	for {
    39  		update, err := stream.Recv()
    40  		switch {
    41  		case err == io.EOF:
    42  			return &grpcBuildlet{
    43  				client:  c.Client,
    44  				id:      instance.GetGomoteId(),
    45  				workDir: instance.GetWorkingDir(),
    46  			}, nil
    47  		case err != nil:
    48  			return nil, err
    49  		case update.GetStatus() != protos.CreateInstanceResponse_COMPLETE:
    50  			status(types.BuildletWaitStatus{
    51  				Ahead: int(update.WaitersAhead),
    52  			})
    53  
    54  		case update.GetStatus() == protos.CreateInstanceResponse_COMPLETE:
    55  			instance = update.GetInstance()
    56  
    57  		}
    58  	}
    59  }
    60  
    61  type grpcBuildlet struct {
    62  	client  protos.GomoteServiceClient
    63  	id      string
    64  	workDir string
    65  }
    66  
    67  var _ RemoteClient = (*grpcBuildlet)(nil)
    68  
    69  func (b *grpcBuildlet) Close() error {
    70  	_, err := b.client.DestroyInstance(context.TODO(), &protos.DestroyInstanceRequest{
    71  		GomoteId: b.id,
    72  	})
    73  	return err
    74  }
    75  
    76  func (b *grpcBuildlet) Exec(ctx context.Context, cmd string, opts ExecOpts) (remoteErr error, execErr error) {
    77  	stream, err := b.client.ExecuteCommand(ctx, &protos.ExecuteCommandRequest{
    78  		GomoteId:          b.id,
    79  		Command:           cmd,
    80  		SystemLevel:       opts.SystemLevel,
    81  		Debug:             opts.Debug,
    82  		AppendEnvironment: opts.ExtraEnv,
    83  		Path:              opts.Path,
    84  		Directory:         opts.Dir,
    85  		Args:              opts.Args,
    86  	})
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	if opts.OnStartExec != nil {
    91  		opts.OnStartExec()
    92  	}
    93  	for {
    94  		update, err := stream.Recv()
    95  		if errors.Is(err, io.EOF) {
    96  			return nil, nil
    97  		}
    98  		if err != nil {
    99  			// Execution error.
   100  			if status.Code(err) == codes.Aborted {
   101  				return nil, err
   102  			}
   103  			// Unknown, presumed command error.
   104  			return err, nil
   105  		}
   106  		if opts.Output != nil {
   107  			opts.Output.Write(update.Output)
   108  		}
   109  	}
   110  }
   111  
   112  func (b *grpcBuildlet) GetTar(ctx context.Context, dir string) (io.ReadCloser, error) {
   113  	resp, err := b.client.ReadTGZToURL(ctx, &protos.ReadTGZToURLRequest{
   114  		GomoteId:  b.id,
   115  		Directory: dir,
   116  	})
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, resp.GetUrl(), nil)
   121  	if err != nil {
   122  		return nil, fmt.Errorf("server returned invalid URL %q: %v", resp.GetUrl(), err)
   123  	}
   124  	r, err := http.DefaultClient.Do(req)
   125  	if err != nil {
   126  		return nil, fmt.Errorf("error fetching tgz: %v", err)
   127  	}
   128  	if r.StatusCode != http.StatusOK {
   129  		return nil, fmt.Errorf("unexpected status reading tgz: %v", r.Status)
   130  	}
   131  	return r.Body, nil
   132  }
   133  
   134  func (b *grpcBuildlet) ListDir(ctx context.Context, dir string, opts ListDirOpts, fn func(DirEntry)) error {
   135  	resp, err := b.client.ListDirectory(ctx, &protos.ListDirectoryRequest{
   136  		GomoteId:  b.id,
   137  		Directory: dir,
   138  		Recursive: opts.Recursive,
   139  		SkipFiles: opts.Skip,
   140  		Digest:    opts.Digest,
   141  	})
   142  	if err != nil {
   143  		return err
   144  	}
   145  	for _, ent := range resp.Entries {
   146  		fn(DirEntry{ent})
   147  	}
   148  	return nil
   149  }
   150  
   151  func (b *grpcBuildlet) Put(ctx context.Context, r io.Reader, path string, mode os.FileMode) error {
   152  	url, err := b.upload(ctx, r)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	_, err = b.client.WriteFileFromURL(ctx, &protos.WriteFileFromURLRequest{
   157  		GomoteId: b.id,
   158  		Url:      url,
   159  		Filename: path,
   160  		Mode:     uint32(mode),
   161  	})
   162  	return err
   163  }
   164  
   165  func (b *grpcBuildlet) PutTar(ctx context.Context, r io.Reader, dir string) error {
   166  	url, err := b.upload(ctx, r)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	_, err = b.client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
   171  		GomoteId:  b.id,
   172  		Url:       url,
   173  		Directory: dir,
   174  	})
   175  	return err
   176  }
   177  
   178  func (b *grpcBuildlet) PutTarFromURL(ctx context.Context, url string, dir string) error {
   179  	_, err := b.client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
   180  		GomoteId:  b.id,
   181  		Url:       url,
   182  		Directory: dir,
   183  	})
   184  	return err
   185  }
   186  
   187  func (b *grpcBuildlet) upload(ctx context.Context, r io.Reader) (string, error) {
   188  	resp, err := b.client.UploadFile(ctx, &protos.UploadFileRequest{})
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  
   193  	buf := new(bytes.Buffer)
   194  	mw := multipart.NewWriter(buf)
   195  	for k, v := range resp.Fields {
   196  		if err := mw.WriteField(k, v); err != nil {
   197  			return "", fmt.Errorf("unable to write field: %s", err)
   198  		}
   199  	}
   200  	_, err = mw.CreateFormFile("file", resp.ObjectName)
   201  	if err != nil {
   202  		return "", fmt.Errorf("unable to create form file: %s", err)
   203  	}
   204  	// Write our own boundary to avoid buffering entire file into the multipart Writer
   205  	bound := fmt.Sprintf("\r\n--%s--\r\n", mw.Boundary())
   206  	req, err := http.NewRequestWithContext(ctx, "POST", resp.Url, io.NopCloser(io.MultiReader(buf, r, strings.NewReader(bound))))
   207  	if err != nil {
   208  		return "", fmt.Errorf("unable to create request: %s", err)
   209  	}
   210  	req.Header.Set("Content-Type", mw.FormDataContentType())
   211  	res, err := http.DefaultClient.Do(req)
   212  	if err != nil {
   213  		return "", fmt.Errorf("http request failed: %s", err)
   214  	}
   215  	if res.StatusCode != http.StatusNoContent {
   216  		return "", fmt.Errorf("http post failed: status code=%d", res.StatusCode)
   217  	}
   218  	return resp.Url + resp.ObjectName, nil
   219  }
   220  
   221  func (b *grpcBuildlet) ProxyTCP(port int) (io.ReadWriteCloser, error) {
   222  	return nil, fmt.Errorf("TCP proxying unimplemented in grpc")
   223  }
   224  
   225  func (b *grpcBuildlet) RemoteName() string {
   226  	return b.id
   227  }
   228  
   229  func (b *grpcBuildlet) RemoveAll(ctx context.Context, paths ...string) error {
   230  	_, err := b.client.RemoveFiles(ctx, &protos.RemoveFilesRequest{
   231  		GomoteId: b.id,
   232  		Paths:    paths,
   233  	})
   234  	return err
   235  }
   236  
   237  func (b *grpcBuildlet) WorkDir(ctx context.Context) (string, error) {
   238  	return b.workDir, nil
   239  }