Implement new driver-opt: default-load

This eases build driver migrations, as it allows aligning the default behavior.
See also https://docs.docker.com/build/drivers/

Signed-off-by: Niklas Gehlen <niklas@namespacelabs.com>
This commit is contained in:
Niklas Gehlen 2024-04-05 15:30:33 +02:00
parent 5c29e6e26e
commit ccc314a823
No known key found for this signature in database
13 changed files with 118 additions and 31 deletions

View File

@ -169,6 +169,10 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
if noMobyDriver != nil && !noDefaultLoad() && noPrintFunc(opt) { if noMobyDriver != nil && !noDefaultLoad() && noPrintFunc(opt) {
var noOutputTargets []string var noOutputTargets []string
for name, opt := range opt { for name, opt := range opt {
if noMobyDriver.Features(ctx)[driver.DefaultLoad] {
continue
}
if !opt.Linked && len(opt.Exports) == 0 { if !opt.Linked && len(opt.Exports) == 0 {
noOutputTargets = append(noOutputTargets, name) noOutputTargets = append(noOutputTargets, name)
} }

View File

@ -162,10 +162,14 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt Op
case 1: case 1:
// valid // valid
case 0: case 0:
if nodeDriver.IsMobyDriver() && !noDefaultLoad() { if !noDefaultLoad() {
if nodeDriver.IsMobyDriver() {
// backwards compat for docker driver only: // backwards compat for docker driver only:
// this ensures the build results in a docker image. // this ensures the build results in a docker image.
opt.Exports = []client.ExportEntry{{Type: "image", Attrs: map[string]string{}}} opt.Exports = []client.ExportEntry{{Type: "image", Attrs: map[string]string{}}}
} else if nodeDriver.Features(ctx)[driver.DefaultLoad] {
opt.Exports = []client.ExportEntry{{Type: "docker", Attrs: map[string]string{}}}
}
} }
default: default:
if err := bopts.LLBCaps.Supports(pb.CapMultipleExporters); err != nil { if err := bopts.LLBCaps.Supports(pb.CapMultipleExporters); err != nil {

View File

@ -56,6 +56,7 @@ type Driver struct {
cgroupParent string cgroupParent string
restartPolicy container.RestartPolicy restartPolicy container.RestartPolicy
env []string env []string
defaultLoad bool
} }
func (d *Driver) IsMobyDriver() bool { func (d *Driver) IsMobyDriver() bool {
@ -423,6 +424,7 @@ func (d *Driver) Features(ctx context.Context) map[driver.Feature]bool {
driver.DockerExporter: true, driver.DockerExporter: true,
driver.CacheExport: true, driver.CacheExport: true,
driver.MultiPlatform: true, driver.MultiPlatform: true,
driver.DefaultLoad: d.defaultLoad,
} }
} }

View File

@ -94,6 +94,11 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
if err != nil { if err != nil {
return nil, err return nil, err
} }
case k == "default-load":
d.defaultLoad, err = strconv.ParseBool(v)
if err != nil {
return nil, err
}
case strings.HasPrefix(k, "env."): case strings.HasPrefix(k, "env."):
envName := strings.TrimPrefix(k, "env.") envName := strings.TrimPrefix(k, "env.")
if envName == "" { if envName == "" {

View File

@ -93,6 +93,7 @@ func (d *Driver) Features(ctx context.Context) map[driver.Feature]bool {
driver.DockerExporter: useContainerdSnapshotter, driver.DockerExporter: useContainerdSnapshotter,
driver.CacheExport: useContainerdSnapshotter, driver.CacheExport: useContainerdSnapshotter,
driver.MultiPlatform: useContainerdSnapshotter, driver.MultiPlatform: useContainerdSnapshotter,
driver.DefaultLoad: true,
} }
}) })
return d.features.list return d.features.list

View File

@ -7,3 +7,5 @@ const DockerExporter Feature = "Docker exporter"
const CacheExport Feature = "Cache export" const CacheExport Feature = "Cache export"
const MultiPlatform Feature = "Multi-platform build" const MultiPlatform Feature = "Multi-platform build"
const DefaultLoad Feature = "Automatically load images to the Docker Engine image store"

View File

@ -49,6 +49,7 @@ type Driver struct {
podClient clientcorev1.PodInterface podClient clientcorev1.PodInterface
configMapClient clientcorev1.ConfigMapInterface configMapClient clientcorev1.ConfigMapInterface
podChooser podchooser.PodChooser podChooser podchooser.PodChooser
defaultLoad bool
} }
func (d *Driver) IsMobyDriver() bool { func (d *Driver) IsMobyDriver() bool {
@ -229,6 +230,7 @@ func (d *Driver) Features(_ context.Context) map[driver.Feature]bool {
driver.DockerExporter: d.DockerAPI != nil, driver.DockerExporter: d.DockerAPI != nil,
driver.CacheExport: true, driver.CacheExport: true,
driver.MultiPlatform: true, // Untested (needs multiple Driver instances) driver.MultiPlatform: true, // Untested (needs multiple Driver instances)
driver.DefaultLoad: d.defaultLoad,
} }
} }

View File

@ -68,11 +68,13 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
clientset: clientset, clientset: clientset,
} }
deploymentOpt, loadbalance, namespace, err := f.processDriverOpts(deploymentName, namespace, cfg) deploymentOpt, loadbalance, namespace, defaultLoad, err := f.processDriverOpts(deploymentName, namespace, cfg)
if nil != err { if nil != err {
return nil, err return nil, err
} }
d.defaultLoad = defaultLoad
d.deployment, d.configMaps, err = manifest.NewDeployment(deploymentOpt) d.deployment, d.configMaps, err = manifest.NewDeployment(deploymentOpt)
if err != nil { if err != nil {
return nil, err return nil, err
@ -100,7 +102,7 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
return d, nil return d, nil
} }
func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg driver.InitConfig) (*manifest.DeploymentOpt, string, string, error) { func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg driver.InitConfig) (*manifest.DeploymentOpt, string, string, bool, error) {
deploymentOpt := &manifest.DeploymentOpt{ deploymentOpt := &manifest.DeploymentOpt{
Name: deploymentName, Name: deploymentName,
Image: bkimage.DefaultImage, Image: bkimage.DefaultImage,
@ -111,6 +113,8 @@ func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg
ConfigFiles: cfg.Files, ConfigFiles: cfg.Files,
} }
defaultLoad := false
deploymentOpt.Qemu.Image = bkimage.QemuImage deploymentOpt.Qemu.Image = bkimage.QemuImage
loadbalance := LoadbalanceSticky loadbalance := LoadbalanceSticky
@ -127,7 +131,7 @@ func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg
case "replicas": case "replicas":
deploymentOpt.Replicas, err = strconv.Atoi(v) deploymentOpt.Replicas, err = strconv.Atoi(v)
if err != nil { if err != nil {
return nil, "", "", err return nil, "", "", false, err
} }
case "requests.cpu": case "requests.cpu":
deploymentOpt.RequestsCPU = v deploymentOpt.RequestsCPU = v
@ -140,7 +144,7 @@ func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg
case "rootless": case "rootless":
deploymentOpt.Rootless, err = strconv.ParseBool(v) deploymentOpt.Rootless, err = strconv.ParseBool(v)
if err != nil { if err != nil {
return nil, "", "", err return nil, "", "", false, err
} }
if _, isImage := cfg.DriverOpts["image"]; !isImage { if _, isImage := cfg.DriverOpts["image"]; !isImage {
deploymentOpt.Image = bkimage.DefaultRootlessImage deploymentOpt.Image = bkimage.DefaultRootlessImage
@ -150,17 +154,17 @@ func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg
case "nodeselector": case "nodeselector":
deploymentOpt.NodeSelector, err = splitMultiValues(v, ",", "=") deploymentOpt.NodeSelector, err = splitMultiValues(v, ",", "=")
if err != nil { if err != nil {
return nil, "", "", errors.Wrap(err, "cannot parse node selector") return nil, "", "", false, errors.Wrap(err, "cannot parse node selector")
} }
case "annotations": case "annotations":
deploymentOpt.CustomAnnotations, err = splitMultiValues(v, ",", "=") deploymentOpt.CustomAnnotations, err = splitMultiValues(v, ",", "=")
if err != nil { if err != nil {
return nil, "", "", errors.Wrap(err, "cannot parse annotations") return nil, "", "", false, errors.Wrap(err, "cannot parse annotations")
} }
case "labels": case "labels":
deploymentOpt.CustomLabels, err = splitMultiValues(v, ",", "=") deploymentOpt.CustomLabels, err = splitMultiValues(v, ",", "=")
if err != nil { if err != nil {
return nil, "", "", errors.Wrap(err, "cannot parse labels") return nil, "", "", false, errors.Wrap(err, "cannot parse labels")
} }
case "tolerations": case "tolerations":
ts := strings.Split(v, ";") ts := strings.Split(v, ";")
@ -185,12 +189,12 @@ func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg
case "tolerationSeconds": case "tolerationSeconds":
c, err := strconv.Atoi(kv[1]) c, err := strconv.Atoi(kv[1])
if nil != err { if nil != err {
return nil, "", "", err return nil, "", "", false, err
} }
c64 := int64(c) c64 := int64(c)
t.TolerationSeconds = &c64 t.TolerationSeconds = &c64
default: default:
return nil, "", "", errors.Errorf("invalid tolaration %q", v) return nil, "", "", false, errors.Errorf("invalid tolaration %q", v)
} }
} }
} }
@ -202,24 +206,29 @@ func (f *factory) processDriverOpts(deploymentName string, namespace string, cfg
case LoadbalanceSticky: case LoadbalanceSticky:
case LoadbalanceRandom: case LoadbalanceRandom:
default: default:
return nil, "", "", errors.Errorf("invalid loadbalance %q", v) return nil, "", "", false, errors.Errorf("invalid loadbalance %q", v)
} }
loadbalance = v loadbalance = v
case "qemu.install": case "qemu.install":
deploymentOpt.Qemu.Install, err = strconv.ParseBool(v) deploymentOpt.Qemu.Install, err = strconv.ParseBool(v)
if err != nil { if err != nil {
return nil, "", "", err return nil, "", "", false, err
} }
case "qemu.image": case "qemu.image":
if v != "" { if v != "" {
deploymentOpt.Qemu.Image = v deploymentOpt.Qemu.Image = v
} }
case "default-load":
defaultLoad, err = strconv.ParseBool(v)
if err != nil {
return nil, "", "", false, err
}
default: default:
return nil, "", "", errors.Errorf("invalid driver option %s for driver %s", k, DriverName) return nil, "", "", false, errors.Errorf("invalid driver option %s for driver %s", k, DriverName)
} }
} }
return deploymentOpt, loadbalance, namespace, nil return deploymentOpt, loadbalance, namespace, defaultLoad, nil
} }
func splitMultiValues(in string, itemsep string, kvsep string) (map[string]string, error) { func splitMultiValues(in string, itemsep string, kvsep string) (map[string]string, error) {

View File

@ -52,8 +52,9 @@ func TestFactory_processDriverOpts(t *testing.T) {
"loadbalance": "random", "loadbalance": "random",
"qemu.install": "true", "qemu.install": "true",
"qemu.image": "qemu:latest", "qemu.image": "qemu:latest",
"default-load": "true",
} }
r, loadbalance, ns, err := f.processDriverOpts(cfg.Name, "test", cfg) r, loadbalance, ns, defaultLoad, err := f.processDriverOpts(cfg.Name, "test", cfg)
nodeSelectors := map[string]string{ nodeSelectors := map[string]string{
"selector1": "value1", "selector1": "value1",
@ -102,6 +103,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
require.Equal(t, LoadbalanceRandom, loadbalance) require.Equal(t, LoadbalanceRandom, loadbalance)
require.True(t, r.Qemu.Install) require.True(t, r.Qemu.Install)
require.Equal(t, "qemu:latest", r.Qemu.Image) require.Equal(t, "qemu:latest", r.Qemu.Image)
require.True(t, defaultLoad)
}, },
) )
@ -109,7 +111,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
"NoOptions", func(t *testing.T) { "NoOptions", func(t *testing.T) {
cfg.DriverOpts = map[string]string{} cfg.DriverOpts = map[string]string{}
r, loadbalance, ns, err := f.processDriverOpts(cfg.Name, "test", cfg) r, loadbalance, ns, defaultLoad, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.NoError(t, err) require.NoError(t, err)
@ -128,6 +130,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
require.Equal(t, LoadbalanceSticky, loadbalance) require.Equal(t, LoadbalanceSticky, loadbalance)
require.False(t, r.Qemu.Install) require.False(t, r.Qemu.Install)
require.Equal(t, bkimage.QemuImage, r.Qemu.Image) require.Equal(t, bkimage.QemuImage, r.Qemu.Image)
require.False(t, defaultLoad)
}, },
) )
@ -138,7 +141,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
"loadbalance": "sticky", "loadbalance": "sticky",
} }
r, loadbalance, ns, err := f.processDriverOpts(cfg.Name, "test", cfg) r, loadbalance, ns, defaultLoad, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.NoError(t, err) require.NoError(t, err)
@ -157,6 +160,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
require.Equal(t, LoadbalanceSticky, loadbalance) require.Equal(t, LoadbalanceSticky, loadbalance)
require.False(t, r.Qemu.Install) require.False(t, r.Qemu.Install)
require.Equal(t, bkimage.QemuImage, r.Qemu.Image) require.Equal(t, bkimage.QemuImage, r.Qemu.Image)
require.False(t, defaultLoad)
}, },
) )
@ -165,7 +169,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"replicas": "invalid", "replicas": "invalid",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -175,7 +179,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"rootless": "invalid", "rootless": "invalid",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -185,7 +189,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"tolerations": "key=foo,value=bar,invalid=foo2", "tolerations": "key=foo,value=bar,invalid=foo2",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -195,7 +199,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"tolerations": "key=foo,value=bar,tolerationSeconds=invalid", "tolerations": "key=foo,value=bar,tolerationSeconds=invalid",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -205,7 +209,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"annotations": "key,value", "annotations": "key,value",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -215,7 +219,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"labels": "key=value=foo", "labels": "key=value=foo",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -225,7 +229,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"loadbalance": "invalid", "loadbalance": "invalid",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -235,7 +239,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"qemu.install": "invalid", "qemu.install": "invalid",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )
@ -245,7 +249,7 @@ func TestFactory_processDriverOpts(t *testing.T) {
cfg.DriverOpts = map[string]string{ cfg.DriverOpts = map[string]string{
"invalid": "foo", "invalid": "foo",
} }
_, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg) _, _, _, _, err := f.processDriverOpts(cfg.Name, "test", cfg)
require.Error(t, err) require.Error(t, err)
}, },
) )

View File

@ -24,6 +24,7 @@ type Driver struct {
// if you add fields, remember to update docs: // if you add fields, remember to update docs:
// https://github.com/docker/docs/blob/main/content/build/drivers/remote.md // https://github.com/docker/docs/blob/main/content/build/drivers/remote.md
*tlsOpts *tlsOpts
defaultLoad bool
} }
type tlsOpts struct { type tlsOpts struct {
@ -149,6 +150,7 @@ func (d *Driver) Features(ctx context.Context) map[driver.Feature]bool {
driver.DockerExporter: true, driver.DockerExporter: true,
driver.CacheExport: true, driver.CacheExport: true,
driver.MultiPlatform: true, driver.MultiPlatform: true,
driver.DefaultLoad: d.defaultLoad,
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
// import connhelpers for special url schemes // import connhelpers for special url schemes
@ -80,6 +81,12 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
} }
tls.key = v tls.key = v
tlsEnabled = true tlsEnabled = true
case "default-load":
parsed, err := strconv.ParseBool(v)
if err != nil {
return nil, err
}
d.defaultLoad = parsed
default: default:
return nil, errors.Errorf("invalid driver option %s for remote driver", k) return nil, errors.Errorf("invalid driver option %s for remote driver", k)
} }

View File

@ -59,6 +59,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){
testBuildMultiExporters, testBuildMultiExporters,
testBuildLoadPush, testBuildLoadPush,
testBuildSecret, testBuildSecret,
testBuildDefaultLoad,
} }
func testBuild(t *testing.T, sb integration.Sandbox) { func testBuild(t *testing.T, sb integration.Sandbox) {
@ -750,6 +751,50 @@ COPY --from=build /token /
}) })
} }
func testBuildDefaultLoad(t *testing.T, sb integration.Sandbox) {
if !isDockerWorker(sb) {
t.Skip("only testing with docker workers")
}
tag := "buildx/build:" + identity.NewID()
var builderName string
t.Cleanup(func() {
if builderName == "" {
return
}
cmd := dockerCmd(sb, withArgs("image", "rm", tag))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
out, err := rmCmd(sb, withArgs(builderName))
require.NoError(t, err, out)
})
out, err := createCmd(sb, withArgs(
"--driver", "docker-container",
"--driver-opt", "default-load=true",
))
require.NoError(t, err, out)
builderName = strings.TrimSpace(out)
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs(
"build",
fmt.Sprintf("-t=%s", tag),
dir,
))
cmd.Env = append(cmd.Env, "BUILDX_BUILDER="+builderName)
outb, err := cmd.CombinedOutput()
require.NoError(t, err, string(outb))
cmd = dockerCmd(sb, withArgs("image", "inspect", tag))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
}
func createTestProject(t *testing.T) string { func createTestProject(t *testing.T) string {
dockerfile := []byte(` dockerfile := []byte(`
FROM busybox:latest AS base FROM busybox:latest AS base

View File

@ -64,8 +64,8 @@ func testCreateRestartAlways(t *testing.T, sb integration.Sandbox) {
} }
func testCreateRemoteContainer(t *testing.T, sb integration.Sandbox) { func testCreateRemoteContainer(t *testing.T, sb integration.Sandbox) {
if sb.Name() != "docker" { if !isDockerWorker(sb) {
t.Skip("skipping test for non-docker workers") t.Skip("only testing with docker workers")
} }
ctnBuilderName := "ctn-builder-" + identity.NewID() ctnBuilderName := "ctn-builder-" + identity.NewID()