github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/driver/docker-container/driver.go (about)

     1  package docker
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io"
     7  	"net"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/docker/buildx/driver"
    16  	"github.com/docker/buildx/driver/bkimage"
    17  	"github.com/docker/buildx/util/confutil"
    18  	"github.com/docker/buildx/util/imagetools"
    19  	"github.com/docker/buildx/util/progress"
    20  	"github.com/docker/cli/opts"
    21  	dockertypes "github.com/docker/docker/api/types"
    22  	"github.com/docker/docker/api/types/container"
    23  	imagetypes "github.com/docker/docker/api/types/image"
    24  	"github.com/docker/docker/api/types/mount"
    25  	"github.com/docker/docker/api/types/network"
    26  	"github.com/docker/docker/api/types/system"
    27  	dockerclient "github.com/docker/docker/client"
    28  	"github.com/docker/docker/errdefs"
    29  	dockerarchive "github.com/docker/docker/pkg/archive"
    30  	"github.com/docker/docker/pkg/idtools"
    31  	"github.com/docker/docker/pkg/stdcopy"
    32  	"github.com/moby/buildkit/client"
    33  	"github.com/pkg/errors"
    34  )
    35  
    36  const (
    37  	volumeStateSuffix   = "_state"
    38  	buildkitdConfigFile = "buildkitd.toml"
    39  )
    40  
    41  type Driver struct {
    42  	driver.InitConfig
    43  	factory driver.Factory
    44  
    45  	// if you add fields, remember to update docs:
    46  	// https://github.com/docker/docs/blob/main/content/build/drivers/docker-container.md
    47  	netMode       string
    48  	image         string
    49  	memory        opts.MemBytes
    50  	memorySwap    opts.MemSwapBytes
    51  	cpuQuota      int64
    52  	cpuPeriod     int64
    53  	cpuShares     int64
    54  	cpusetCpus    string
    55  	cpusetMems    string
    56  	cgroupParent  string
    57  	restartPolicy container.RestartPolicy
    58  	env           []string
    59  	defaultLoad   bool
    60  }
    61  
    62  func (d *Driver) IsMobyDriver() bool {
    63  	return false
    64  }
    65  
    66  func (d *Driver) Config() driver.InitConfig {
    67  	return d.InitConfig
    68  }
    69  
    70  func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
    71  	return progress.Wrap("[internal] booting buildkit", l, func(sub progress.SubLogger) error {
    72  		_, err := d.DockerAPI.ContainerInspect(ctx, d.Name)
    73  		if err != nil {
    74  			if dockerclient.IsErrNotFound(err) {
    75  				return d.create(ctx, sub)
    76  			}
    77  			return err
    78  		}
    79  		return sub.Wrap("starting container "+d.Name, func() error {
    80  			if err := d.start(ctx); err != nil {
    81  				return err
    82  			}
    83  			return d.wait(ctx, sub)
    84  		})
    85  	})
    86  }
    87  
    88  func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
    89  	imageName := bkimage.DefaultImage
    90  	if d.image != "" {
    91  		imageName = d.image
    92  	}
    93  
    94  	if err := l.Wrap("pulling image "+imageName, func() error {
    95  		ra, err := imagetools.RegistryAuthForRef(imageName, d.Auth)
    96  		if err != nil {
    97  			return err
    98  		}
    99  		rc, err := d.DockerAPI.ImageCreate(ctx, imageName, imagetypes.CreateOptions{
   100  			RegistryAuth: ra,
   101  		})
   102  		if err != nil {
   103  			return err
   104  		}
   105  		_, err = io.Copy(io.Discard, rc)
   106  		return err
   107  	}); err != nil {
   108  		// image pulling failed, check if it exists in local image store.
   109  		// if not, return pulling error. otherwise log it.
   110  		_, _, errInspect := d.DockerAPI.ImageInspectWithRaw(ctx, imageName)
   111  		if errInspect != nil {
   112  			return err
   113  		}
   114  		l.Wrap("pulling failed, using local image "+imageName, func() error { return nil })
   115  	}
   116  
   117  	cfg := &container.Config{
   118  		Image: imageName,
   119  		Env:   d.env,
   120  	}
   121  	cfg.Cmd = getBuildkitFlags(d.InitConfig)
   122  
   123  	useInit := true // let it cleanup exited processes created by BuildKit's container API
   124  	return l.Wrap("creating container "+d.Name, func() error {
   125  		hc := &container.HostConfig{
   126  			Privileged:    true,
   127  			RestartPolicy: d.restartPolicy,
   128  			Mounts: []mount.Mount{
   129  				{
   130  					Type:   mount.TypeVolume,
   131  					Source: d.Name + volumeStateSuffix,
   132  					Target: confutil.DefaultBuildKitStateDir,
   133  				},
   134  			},
   135  			Init: &useInit,
   136  		}
   137  		if d.netMode != "" {
   138  			hc.NetworkMode = container.NetworkMode(d.netMode)
   139  		}
   140  		if d.memory != 0 {
   141  			hc.Resources.Memory = int64(d.memory)
   142  		}
   143  		if d.memorySwap != 0 {
   144  			hc.Resources.MemorySwap = int64(d.memorySwap)
   145  		}
   146  		if d.cpuQuota != 0 {
   147  			hc.Resources.CPUQuota = d.cpuQuota
   148  		}
   149  		if d.cpuPeriod != 0 {
   150  			hc.Resources.CPUPeriod = d.cpuPeriod
   151  		}
   152  		if d.cpuShares != 0 {
   153  			hc.Resources.CPUShares = d.cpuShares
   154  		}
   155  		if d.cpusetCpus != "" {
   156  			hc.Resources.CpusetCpus = d.cpusetCpus
   157  		}
   158  		if d.cpusetMems != "" {
   159  			hc.Resources.CpusetMems = d.cpusetMems
   160  		}
   161  		if info, err := d.DockerAPI.Info(ctx); err == nil {
   162  			if info.CgroupDriver == "cgroupfs" {
   163  				// Place all buildkit containers inside this cgroup by default so limits can be attached
   164  				// to all build activity on the host.
   165  				hc.CgroupParent = "/docker/buildx"
   166  				if d.cgroupParent != "" {
   167  					hc.CgroupParent = d.cgroupParent
   168  				}
   169  			}
   170  
   171  			secOpts, err := system.DecodeSecurityOptions(info.SecurityOptions)
   172  			if err != nil {
   173  				return err
   174  			}
   175  			for _, f := range secOpts {
   176  				if f.Name == "userns" {
   177  					hc.UsernsMode = "host"
   178  					break
   179  				}
   180  			}
   181  
   182  		}
   183  		_, err := d.DockerAPI.ContainerCreate(ctx, cfg, hc, &network.NetworkingConfig{}, nil, d.Name)
   184  		if err != nil && !errdefs.IsConflict(err) {
   185  			return err
   186  		}
   187  		if err == nil {
   188  			if err := d.copyToContainer(ctx, d.InitConfig.Files); err != nil {
   189  				return err
   190  			}
   191  			if err := d.start(ctx); err != nil {
   192  				return err
   193  			}
   194  		}
   195  		return d.wait(ctx, l)
   196  	})
   197  }
   198  
   199  func (d *Driver) wait(ctx context.Context, l progress.SubLogger) error {
   200  	try := 1
   201  	for {
   202  		bufStdout := &bytes.Buffer{}
   203  		bufStderr := &bytes.Buffer{}
   204  		if err := d.run(ctx, []string{"buildctl", "debug", "workers"}, bufStdout, bufStderr); err != nil {
   205  			if try > 15 {
   206  				d.copyLogs(context.TODO(), l)
   207  				if bufStdout.Len() != 0 {
   208  					l.Log(1, bufStdout.Bytes())
   209  				}
   210  				if bufStderr.Len() != 0 {
   211  					l.Log(2, bufStderr.Bytes())
   212  				}
   213  				return err
   214  			}
   215  			select {
   216  			case <-ctx.Done():
   217  				return ctx.Err()
   218  			case <-time.After(time.Duration(try*120) * time.Millisecond):
   219  				try++
   220  				continue
   221  			}
   222  		}
   223  		return nil
   224  	}
   225  }
   226  
   227  func (d *Driver) copyLogs(ctx context.Context, l progress.SubLogger) error {
   228  	rc, err := d.DockerAPI.ContainerLogs(ctx, d.Name, container.LogsOptions{
   229  		ShowStdout: true, ShowStderr: true,
   230  	})
   231  	if err != nil {
   232  		return err
   233  	}
   234  	stdout := &logWriter{logger: l, stream: 1}
   235  	stderr := &logWriter{logger: l, stream: 2}
   236  	if _, err := stdcopy.StdCopy(stdout, stderr, rc); err != nil {
   237  		return err
   238  	}
   239  	return rc.Close()
   240  }
   241  
   242  func (d *Driver) copyToContainer(ctx context.Context, files map[string][]byte) error {
   243  	srcPath, err := writeConfigFiles(files)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	if srcPath != "" {
   248  		defer os.RemoveAll(srcPath)
   249  	}
   250  	srcArchive, err := dockerarchive.TarWithOptions(srcPath, &dockerarchive.TarOptions{
   251  		ChownOpts: &idtools.Identity{UID: 0, GID: 0},
   252  	})
   253  	if err != nil {
   254  		return err
   255  	}
   256  	defer srcArchive.Close()
   257  
   258  	baseDir := path.Dir(confutil.DefaultBuildKitConfigDir)
   259  	return d.DockerAPI.CopyToContainer(ctx, d.Name, baseDir, srcArchive, dockertypes.CopyToContainerOptions{})
   260  }
   261  
   262  func (d *Driver) exec(ctx context.Context, cmd []string) (string, net.Conn, error) {
   263  	execConfig := dockertypes.ExecConfig{
   264  		Cmd:          cmd,
   265  		AttachStdin:  true,
   266  		AttachStdout: true,
   267  		AttachStderr: true,
   268  	}
   269  	response, err := d.DockerAPI.ContainerExecCreate(ctx, d.Name, execConfig)
   270  	if err != nil {
   271  		return "", nil, err
   272  	}
   273  
   274  	execID := response.ID
   275  	if execID == "" {
   276  		return "", nil, errors.New("exec ID empty")
   277  	}
   278  
   279  	resp, err := d.DockerAPI.ContainerExecAttach(ctx, execID, dockertypes.ExecStartCheck{})
   280  	if err != nil {
   281  		return "", nil, err
   282  	}
   283  	return execID, resp.Conn, nil
   284  }
   285  
   286  func (d *Driver) run(ctx context.Context, cmd []string, stdout, stderr io.Writer) (err error) {
   287  	id, conn, err := d.exec(ctx, cmd)
   288  	if err != nil {
   289  		return err
   290  	}
   291  	if _, err := stdcopy.StdCopy(stdout, stderr, conn); err != nil {
   292  		return err
   293  	}
   294  	conn.Close()
   295  	resp, err := d.DockerAPI.ContainerExecInspect(ctx, id)
   296  	if err != nil {
   297  		return err
   298  	}
   299  	if resp.ExitCode != 0 {
   300  		return errors.Errorf("exit code %d", resp.ExitCode)
   301  	}
   302  	return nil
   303  }
   304  
   305  func (d *Driver) start(ctx context.Context) error {
   306  	return d.DockerAPI.ContainerStart(ctx, d.Name, container.StartOptions{})
   307  }
   308  
   309  func (d *Driver) Info(ctx context.Context) (*driver.Info, error) {
   310  	ctn, err := d.DockerAPI.ContainerInspect(ctx, d.Name)
   311  	if err != nil {
   312  		if dockerclient.IsErrNotFound(err) {
   313  			return &driver.Info{
   314  				Status: driver.Inactive,
   315  			}, nil
   316  		}
   317  		return nil, err
   318  	}
   319  
   320  	if ctn.State.Running {
   321  		return &driver.Info{
   322  			Status: driver.Running,
   323  		}, nil
   324  	}
   325  
   326  	return &driver.Info{
   327  		Status: driver.Stopped,
   328  	}, nil
   329  }
   330  
   331  func (d *Driver) Version(ctx context.Context) (string, error) {
   332  	bufStdout := &bytes.Buffer{}
   333  	bufStderr := &bytes.Buffer{}
   334  	if err := d.run(ctx, []string{"buildkitd", "--version"}, bufStdout, bufStderr); err != nil {
   335  		if bufStderr.Len() > 0 {
   336  			return "", errors.Wrap(err, bufStderr.String())
   337  		}
   338  		return "", err
   339  	}
   340  	version := strings.Fields(bufStdout.String())
   341  	if len(version) != 4 {
   342  		return "", errors.Errorf("unexpected version format: %s", bufStdout.String())
   343  	}
   344  	return version[2], nil
   345  }
   346  
   347  func (d *Driver) Stop(ctx context.Context, force bool) error {
   348  	info, err := d.Info(ctx)
   349  	if err != nil {
   350  		return err
   351  	}
   352  	if info.Status == driver.Running {
   353  		return d.DockerAPI.ContainerStop(ctx, d.Name, container.StopOptions{})
   354  	}
   355  	return nil
   356  }
   357  
   358  func (d *Driver) Rm(ctx context.Context, force, rmVolume, rmDaemon bool) error {
   359  	info, err := d.Info(ctx)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	if info.Status != driver.Inactive {
   364  		ctr, err := d.DockerAPI.ContainerInspect(ctx, d.Name)
   365  		if err != nil {
   366  			return err
   367  		}
   368  		if rmDaemon {
   369  			if err := d.DockerAPI.ContainerRemove(ctx, d.Name, container.RemoveOptions{
   370  				RemoveVolumes: true,
   371  				Force:         force,
   372  			}); err != nil {
   373  				return err
   374  			}
   375  			for _, v := range ctr.Mounts {
   376  				if v.Name != d.Name+volumeStateSuffix {
   377  					continue
   378  				}
   379  				if rmVolume {
   380  					return d.DockerAPI.VolumeRemove(ctx, d.Name+volumeStateSuffix, false)
   381  				}
   382  			}
   383  		}
   384  	}
   385  	return nil
   386  }
   387  
   388  func (d *Driver) Dial(ctx context.Context) (net.Conn, error) {
   389  	_, conn, err := d.exec(ctx, []string{"buildctl", "dial-stdio"})
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	conn = demuxConn(conn)
   394  	return conn, nil
   395  }
   396  
   397  func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client.Client, error) {
   398  	conn, err := d.Dial(ctx)
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  
   403  	var counter int64
   404  	opts = append([]client.ClientOpt{
   405  		client.WithContextDialer(func(context.Context, string) (net.Conn, error) {
   406  			if atomic.AddInt64(&counter, 1) > 1 {
   407  				return nil, net.ErrClosed
   408  			}
   409  			return conn, nil
   410  		}),
   411  	}, opts...)
   412  	return client.New(ctx, "", opts...)
   413  }
   414  
   415  func (d *Driver) Factory() driver.Factory {
   416  	return d.factory
   417  }
   418  
   419  func (d *Driver) Features(ctx context.Context) map[driver.Feature]bool {
   420  	return map[driver.Feature]bool{
   421  		driver.OCIExporter:    true,
   422  		driver.DockerExporter: true,
   423  		driver.CacheExport:    true,
   424  		driver.MultiPlatform:  true,
   425  		driver.DefaultLoad:    d.defaultLoad,
   426  	}
   427  }
   428  
   429  func (d *Driver) HostGatewayIP(ctx context.Context) (net.IP, error) {
   430  	return nil, errors.New("host-gateway is not supported by the docker-container driver")
   431  }
   432  
   433  func demuxConn(c net.Conn) net.Conn {
   434  	pr, pw := io.Pipe()
   435  	// TODO: rewrite parser with Reader() to avoid goroutine switch
   436  	go func() {
   437  		_, err := stdcopy.StdCopy(pw, os.Stderr, c)
   438  		pw.CloseWithError(err)
   439  	}()
   440  	return &demux{
   441  		Conn:   c,
   442  		Reader: pr,
   443  	}
   444  }
   445  
   446  type demux struct {
   447  	net.Conn
   448  	io.Reader
   449  }
   450  
   451  func (d *demux) Read(dt []byte) (int, error) {
   452  	return d.Reader.Read(dt)
   453  }
   454  
   455  type logWriter struct {
   456  	logger progress.SubLogger
   457  	stream int
   458  }
   459  
   460  func (l *logWriter) Write(dt []byte) (int, error) {
   461  	l.logger.Log(l.stream, dt)
   462  	return len(dt), nil
   463  }
   464  
   465  func writeConfigFiles(m map[string][]byte) (_ string, err error) {
   466  	// Temp dir that will be copied to the container
   467  	tmpDir, err := os.MkdirTemp("", "buildkitd-config")
   468  	if err != nil {
   469  		return "", err
   470  	}
   471  	defer func() {
   472  		if err != nil {
   473  			os.RemoveAll(tmpDir)
   474  		}
   475  	}()
   476  	configDir := filepath.Base(confutil.DefaultBuildKitConfigDir)
   477  	for f, dt := range m {
   478  		p := filepath.Join(tmpDir, configDir, f)
   479  		if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
   480  			return "", err
   481  		}
   482  		if err := os.WriteFile(p, dt, 0644); err != nil {
   483  			return "", err
   484  		}
   485  	}
   486  	return tmpDir, nil
   487  }
   488  
   489  func getBuildkitFlags(initConfig driver.InitConfig) []string {
   490  	flags := initConfig.BuildkitdFlags
   491  	if _, ok := initConfig.Files[buildkitdConfigFile]; ok {
   492  		// There's no way for us to determine the appropriate default configuration
   493  		// path and the default path can vary depending on if the image is normal
   494  		// or rootless.
   495  		//
   496  		// In order to ensure that --config works, copy to a specific path and
   497  		// specify the location.
   498  		//
   499  		// This should be appended before the user-specified arguments
   500  		// so that this option could be overwritten by the user.
   501  		newFlags := make([]string, 0, len(flags)+2)
   502  		newFlags = append(newFlags, "--config", path.Join("/etc/buildkit", buildkitdConfigFile))
   503  		flags = append(newFlags, flags...)
   504  	}
   505  	return flags
   506  }