You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
4.8 KiB
Go
217 lines
4.8 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
_ "crypto/sha256" // ensure digests can be computed
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
|
|
controllerapi "github.com/docker/buildx/controller/pb"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ResultContext is a build result with the client that built it.
|
|
type ResultContext struct {
|
|
Client *client.Client
|
|
Res *gateway.Result
|
|
}
|
|
|
|
type Container struct {
|
|
cancelOnce sync.Once
|
|
containerCancel func()
|
|
|
|
isUnavailable atomic.Bool
|
|
|
|
initStarted atomic.Bool
|
|
|
|
container gateway.Container
|
|
image *specs.Image
|
|
|
|
releaseCh chan struct{}
|
|
}
|
|
|
|
func NewContainer(ctx context.Context, resultCtx *ResultContext) (*Container, error) {
|
|
c, res := resultCtx.Client, resultCtx.Res
|
|
|
|
mainCtx := ctx
|
|
|
|
ctrCh := make(chan *Container)
|
|
errCh := make(chan error)
|
|
go func() {
|
|
_, err := c.Build(context.TODO(), client.SolveOpt{}, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
go func() {
|
|
<-mainCtx.Done()
|
|
cancel()
|
|
}()
|
|
|
|
if res.Ref == nil {
|
|
return nil, errors.Errorf("no reference is registered")
|
|
}
|
|
st, err := res.Ref.ToState()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
def, err := st.Marshal(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imgRef, err := c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
containerCtx, containerCancel := context.WithCancel(ctx)
|
|
defer containerCancel()
|
|
bkContainer, err := c.NewContainer(containerCtx, gateway.NewContainerRequest{
|
|
Mounts: []gateway.Mount{
|
|
{
|
|
Dest: "/",
|
|
MountType: pb.MountType_BIND,
|
|
Ref: imgRef.Ref,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imgData := res.Metadata[exptypes.ExporterImageConfigKey]
|
|
var img *specs.Image
|
|
if len(imgData) > 0 {
|
|
img = &specs.Image{}
|
|
if err := json.Unmarshal(imgData, img); err != nil {
|
|
fmt.Println(err)
|
|
return nil, err
|
|
}
|
|
}
|
|
releaseCh := make(chan struct{})
|
|
container := &Container{
|
|
containerCancel: containerCancel,
|
|
container: bkContainer,
|
|
image: img,
|
|
releaseCh: releaseCh,
|
|
}
|
|
ctrCh <- container
|
|
<-container.releaseCh
|
|
|
|
return nil, bkContainer.Release(ctx)
|
|
}, nil)
|
|
if err != nil {
|
|
errCh <- err
|
|
}
|
|
}()
|
|
select {
|
|
case ctr := <-ctrCh:
|
|
return ctr, nil
|
|
case err := <-errCh:
|
|
return nil, err
|
|
case <-mainCtx.Done():
|
|
return nil, mainCtx.Err()
|
|
}
|
|
}
|
|
|
|
func (c *Container) Cancel() {
|
|
c.markUnavailable()
|
|
c.cancelOnce.Do(func() {
|
|
if c.containerCancel != nil {
|
|
c.containerCancel()
|
|
}
|
|
close(c.releaseCh)
|
|
})
|
|
}
|
|
|
|
func (c *Container) IsUnavailable() bool {
|
|
return c.isUnavailable.Load()
|
|
}
|
|
|
|
func (c *Container) markUnavailable() {
|
|
c.isUnavailable.Store(true)
|
|
}
|
|
|
|
func (c *Container) Exec(ctx context.Context, cfg *controllerapi.InvokeConfig, stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error {
|
|
if isInit := c.initStarted.CompareAndSwap(false, true); isInit {
|
|
defer func() {
|
|
// container can't be used after init exits
|
|
c.markUnavailable()
|
|
}()
|
|
}
|
|
err := exec(ctx, cfg, c.container, c.image, stdin, stdout, stderr)
|
|
if err != nil {
|
|
// Container becomes unavailable if one of the processes fails in it.
|
|
c.markUnavailable()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func exec(ctx context.Context, cfg *controllerapi.InvokeConfig, ctr gateway.Container, img *specs.Image, stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error {
|
|
user := ""
|
|
if !cfg.NoUser {
|
|
user = cfg.User
|
|
} else if img != nil {
|
|
user = img.Config.User
|
|
}
|
|
|
|
cwd := ""
|
|
if !cfg.NoCwd {
|
|
cwd = cfg.Cwd
|
|
} else if img != nil {
|
|
cwd = img.Config.WorkingDir
|
|
}
|
|
|
|
env := []string{}
|
|
if img != nil {
|
|
env = append(env, img.Config.Env...)
|
|
}
|
|
env = append(env, cfg.Env...)
|
|
|
|
args := []string{}
|
|
if cfg.Entrypoint != nil {
|
|
args = append(args, cfg.Entrypoint...)
|
|
}
|
|
if cfg.Cmd != nil {
|
|
args = append(args, cfg.Cmd...)
|
|
}
|
|
// caller should always set args
|
|
if len(args) == 0 {
|
|
return errors.Errorf("specify args to execute")
|
|
}
|
|
proc, err := ctr.Start(ctx, gateway.StartRequest{
|
|
Args: args,
|
|
Env: env,
|
|
User: user,
|
|
Cwd: cwd,
|
|
Tty: cfg.Tty,
|
|
Stdin: stdin,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
})
|
|
if err != nil {
|
|
return errors.Errorf("failed to start container: %v", err)
|
|
}
|
|
|
|
doneCh := make(chan struct{})
|
|
defer close(doneCh)
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
if err := proc.Signal(ctx, syscall.SIGKILL); err != nil {
|
|
logrus.Warnf("failed to kill process: %v", err)
|
|
}
|
|
case <-doneCh:
|
|
}
|
|
}()
|
|
|
|
return proc.Wait()
|
|
}
|