Add Solve API for performing a build on a build definition
This commit adds Solve API to the controller. This receives a build definition, performs that build using ResultHandler held by that session. After Solve completes, the client can debug the result using other APIs including Invoke. Note that the ResultHandle provided by Solve overwrites the ResultHandle previously stored in that session (possibly generated by the past Build or Solve API call). Using this API, user can perform build-and-debugging loop on the same session. Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/identity"
|
||||
solverpb "github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/grpcerrors"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -238,3 +239,52 @@ func (c *Client) build(ctx context.Context, ref string, options pb.BuildOptions,
|
||||
func (c *Client) client() pb.ControllerClient {
|
||||
return pb.NewControllerClient(c.conn)
|
||||
}
|
||||
|
||||
func (c *Client) Solve(ctx context.Context, ref string, def *solverpb.Definition, progress progress.Writer) error {
|
||||
statusChan := make(chan *client.SolveStatus)
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
defer close(statusChan)
|
||||
return c.doSolve(egCtx, ref, def, statusChan)
|
||||
})
|
||||
eg.Go(func() error {
|
||||
for s := range statusChan {
|
||||
st := s
|
||||
progress.Write(st)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) doSolve(ctx context.Context, ref string, def *solverpb.Definition, statusChan chan *client.SolveStatus) error {
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
if _, err := c.client().Solve(egCtx, &pb.SolveRequest{
|
||||
Ref: ref,
|
||||
Target: def,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
stream, err := c.client().Status(egCtx, &pb.StatusRequest{
|
||||
Ref: ref,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "failed to receive status")
|
||||
}
|
||||
statusChan <- pb.FromControlStatus(resp)
|
||||
}
|
||||
})
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ type session struct {
|
||||
result *build.ResultHandle
|
||||
|
||||
processes *processes.Manager
|
||||
|
||||
originalResult *build.ResultHandle
|
||||
}
|
||||
|
||||
func (s *session) cancelRunningProcesses() {
|
||||
@@ -208,6 +210,7 @@ func (m *Server) Build(ctx context.Context, req *pb.BuildRequest) (*pb.BuildResp
|
||||
// NOTE: buildFunc can return *build.ResultHandle even on error (e.g. when it's implemented using (github.com/docker/buildx/controller/build).RunBuild).
|
||||
if res != nil {
|
||||
s.result = res
|
||||
s.originalResult = res
|
||||
s.cancelBuild = cancel
|
||||
s.buildOptions = req.Options
|
||||
m.session[ref] = s
|
||||
@@ -439,3 +442,64 @@ func (m *Server) Invoke(srv pb.Controller_InvokeServer) error {
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (m *Server) Solve(ctx context.Context, req *pb.SolveRequest) (*pb.SolveResponse, error) {
|
||||
ref := req.Ref
|
||||
if ref == "" {
|
||||
return nil, errors.New("solve: empty key")
|
||||
}
|
||||
|
||||
m.sessionMu.Lock()
|
||||
if _, ok := m.session[ref]; !ok || m.session[ref].result == nil {
|
||||
m.sessionMu.Unlock()
|
||||
return &pb.SolveResponse{}, errors.Errorf("solve: unknown reference: %q", ref)
|
||||
}
|
||||
s := m.session[ref]
|
||||
s.cancelRunningProcesses()
|
||||
if s.originalResult == nil {
|
||||
return &pb.SolveResponse{}, errors.Errorf("no build has been called")
|
||||
}
|
||||
resultCtx := s.originalResult
|
||||
if s.statusChan != nil {
|
||||
m.sessionMu.Unlock()
|
||||
return &pb.SolveResponse{}, errors.New("solve: build or status ongoing or status didn't call")
|
||||
}
|
||||
statusChan := make(chan *pb.StatusResponse)
|
||||
s.statusChan = statusChan
|
||||
m.session[ref] = s
|
||||
m.sessionMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
close(statusChan)
|
||||
m.sessionMu.Lock()
|
||||
s, ok := m.session[ref]
|
||||
if ok {
|
||||
s.statusChan = nil
|
||||
m.session[ref] = s
|
||||
}
|
||||
m.sessionMu.Unlock()
|
||||
}()
|
||||
|
||||
// Get the target result context
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
pw := pb.NewProgressWriter(statusChan)
|
||||
res, err := build.SolveWithResultHandler(ctx, "buildx", resultCtx, req.Target, pw)
|
||||
if err == nil {
|
||||
m.sessionMu.Lock()
|
||||
if s, ok := m.session[ref]; ok {
|
||||
s.result = res
|
||||
s.cancelBuild = cancel
|
||||
m.session[ref] = s
|
||||
if se := res.SolveError(); se != nil {
|
||||
err = errors.Errorf("failed solve: %v", se)
|
||||
}
|
||||
} else {
|
||||
m.sessionMu.Unlock()
|
||||
return nil, errors.Errorf("build: unknown key %v", ref)
|
||||
}
|
||||
m.sessionMu.Unlock()
|
||||
}
|
||||
|
||||
return &pb.SolveResponse{}, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user