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 }