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 }