github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/controller/remote/client.go (about) 1 package remote 2 3 import ( 4 "context" 5 "io" 6 "sync" 7 "time" 8 9 "github.com/containerd/containerd/defaults" 10 "github.com/containerd/containerd/pkg/dialer" 11 "github.com/docker/buildx/controller/pb" 12 "github.com/docker/buildx/util/progress" 13 "github.com/moby/buildkit/client" 14 "github.com/moby/buildkit/identity" 15 "github.com/moby/buildkit/util/grpcerrors" 16 "github.com/pkg/errors" 17 "golang.org/x/sync/errgroup" 18 "google.golang.org/grpc" 19 "google.golang.org/grpc/backoff" 20 "google.golang.org/grpc/credentials/insecure" 21 ) 22 23 func NewClient(ctx context.Context, addr string) (*Client, error) { 24 backoffConfig := backoff.DefaultConfig 25 backoffConfig.MaxDelay = 3 * time.Second 26 connParams := grpc.ConnectParams{ 27 Backoff: backoffConfig, 28 } 29 gopts := []grpc.DialOption{ 30 grpc.WithBlock(), 31 grpc.WithTransportCredentials(insecure.NewCredentials()), 32 grpc.WithConnectParams(connParams), 33 grpc.WithContextDialer(dialer.ContextDialer), 34 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)), 35 grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)), 36 grpc.WithUnaryInterceptor(grpcerrors.UnaryClientInterceptor), 37 grpc.WithStreamInterceptor(grpcerrors.StreamClientInterceptor), 38 } 39 conn, err := grpc.DialContext(ctx, dialer.DialAddress(addr), gopts...) 40 if err != nil { 41 return nil, err 42 } 43 return &Client{conn: conn}, nil 44 } 45 46 type Client struct { 47 conn *grpc.ClientConn 48 closeOnce sync.Once 49 } 50 51 func (c *Client) Close() (err error) { 52 c.closeOnce.Do(func() { 53 err = c.conn.Close() 54 }) 55 return 56 } 57 58 func (c *Client) Version(ctx context.Context) (string, string, string, error) { 59 res, err := c.client().Info(ctx, &pb.InfoRequest{}) 60 if err != nil { 61 return "", "", "", err 62 } 63 v := res.BuildxVersion 64 return v.Package, v.Version, v.Revision, nil 65 } 66 67 func (c *Client) List(ctx context.Context) (keys []string, retErr error) { 68 res, err := c.client().List(ctx, &pb.ListRequest{}) 69 if err != nil { 70 return nil, err 71 } 72 return res.Keys, nil 73 } 74 75 func (c *Client) Disconnect(ctx context.Context, key string) error { 76 if key == "" { 77 return nil 78 } 79 _, err := c.client().Disconnect(ctx, &pb.DisconnectRequest{Ref: key}) 80 return err 81 } 82 83 func (c *Client) ListProcesses(ctx context.Context, ref string) (infos []*pb.ProcessInfo, retErr error) { 84 res, err := c.client().ListProcesses(ctx, &pb.ListProcessesRequest{Ref: ref}) 85 if err != nil { 86 return nil, err 87 } 88 return res.Infos, nil 89 } 90 91 func (c *Client) DisconnectProcess(ctx context.Context, ref, pid string) error { 92 _, err := c.client().DisconnectProcess(ctx, &pb.DisconnectProcessRequest{Ref: ref, ProcessID: pid}) 93 return err 94 } 95 96 func (c *Client) Invoke(ctx context.Context, ref string, pid string, invokeConfig pb.InvokeConfig, in io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error { 97 if ref == "" || pid == "" { 98 return errors.New("build reference must be specified") 99 } 100 stream, err := c.client().Invoke(ctx) 101 if err != nil { 102 return err 103 } 104 return attachIO(ctx, stream, &pb.InitMessage{Ref: ref, ProcessID: pid, InvokeConfig: &invokeConfig}, ioAttachConfig{ 105 stdin: in, 106 stdout: stdout, 107 stderr: stderr, 108 // TODO: Signal, Resize 109 }) 110 } 111 112 func (c *Client) Inspect(ctx context.Context, ref string) (*pb.InspectResponse, error) { 113 return c.client().Inspect(ctx, &pb.InspectRequest{Ref: ref}) 114 } 115 116 func (c *Client) Build(ctx context.Context, options pb.BuildOptions, in io.ReadCloser, progress progress.Writer) (string, *client.SolveResponse, error) { 117 ref := identity.NewID() 118 statusChan := make(chan *client.SolveStatus) 119 eg, egCtx := errgroup.WithContext(ctx) 120 var resp *client.SolveResponse 121 eg.Go(func() error { 122 defer close(statusChan) 123 var err error 124 resp, err = c.build(egCtx, ref, options, in, statusChan) 125 return err 126 }) 127 eg.Go(func() error { 128 for s := range statusChan { 129 st := s 130 progress.Write(st) 131 } 132 return nil 133 }) 134 return ref, resp, eg.Wait() 135 } 136 137 func (c *Client) build(ctx context.Context, ref string, options pb.BuildOptions, in io.ReadCloser, statusChan chan *client.SolveStatus) (*client.SolveResponse, error) { 138 eg, egCtx := errgroup.WithContext(ctx) 139 done := make(chan struct{}) 140 141 var resp *client.SolveResponse 142 143 eg.Go(func() error { 144 defer close(done) 145 pbResp, err := c.client().Build(egCtx, &pb.BuildRequest{ 146 Ref: ref, 147 Options: &options, 148 }) 149 if err != nil { 150 return err 151 } 152 resp = &client.SolveResponse{ 153 ExporterResponse: pbResp.ExporterResponse, 154 } 155 return nil 156 }) 157 eg.Go(func() error { 158 stream, err := c.client().Status(egCtx, &pb.StatusRequest{ 159 Ref: ref, 160 }) 161 if err != nil { 162 return err 163 } 164 for { 165 resp, err := stream.Recv() 166 if err != nil { 167 if err == io.EOF { 168 return nil 169 } 170 return errors.Wrap(err, "failed to receive status") 171 } 172 statusChan <- pb.FromControlStatus(resp) 173 } 174 }) 175 if in != nil { 176 eg.Go(func() error { 177 stream, err := c.client().Input(egCtx) 178 if err != nil { 179 return err 180 } 181 if err := stream.Send(&pb.InputMessage{ 182 Input: &pb.InputMessage_Init{ 183 Init: &pb.InputInitMessage{ 184 Ref: ref, 185 }, 186 }, 187 }); err != nil { 188 return errors.Wrap(err, "failed to init input") 189 } 190 191 inReader, inWriter := io.Pipe() 192 eg2, _ := errgroup.WithContext(ctx) 193 eg2.Go(func() error { 194 <-done 195 return inWriter.Close() 196 }) 197 go func() { 198 // do not wait for read completion but return here and let the caller send EOF 199 // this allows us to return on ctx.Done() without being blocked by this reader. 200 io.Copy(inWriter, in) 201 inWriter.Close() 202 }() 203 eg2.Go(func() error { 204 for { 205 buf := make([]byte, 32*1024) 206 n, err := inReader.Read(buf) 207 if err != nil { 208 if err == io.EOF { 209 break // break loop and send EOF 210 } 211 return err 212 } else if n > 0 { 213 if err := stream.Send(&pb.InputMessage{ 214 Input: &pb.InputMessage_Data{ 215 Data: &pb.DataMessage{ 216 Data: buf[:n], 217 }, 218 }, 219 }); err != nil { 220 return err 221 } 222 } 223 } 224 return stream.Send(&pb.InputMessage{ 225 Input: &pb.InputMessage_Data{ 226 Data: &pb.DataMessage{ 227 EOF: true, 228 }, 229 }, 230 }) 231 }) 232 return eg2.Wait() 233 }) 234 } 235 return resp, eg.Wait() 236 } 237 238 func (c *Client) client() pb.ControllerClient { 239 return pb.NewControllerClient(c.conn) 240 }