Files
buildx/build/utils.go
T
Jiří Moravčík 80b2293881 build: Add per-step CPU and memory resource limits
Port of moby/buildkit#6569 to buildx. Adds --memory, --memory-swap,
--cpu-shares, --cpu-period, --cpu-quota, --cpuset-cpus, and --cpuset-mems
flags to build, plus the equivalent bake target attributes and compose
x-bake fields.

Signed-off-by: Jiří Moravčík <jiri.moravcik@gmail.com>
2026-06-08 15:39:14 +02:00

210 lines
5.9 KiB
Go

package build
import (
"archive/tar"
"bytes"
"context"
"net"
"os"
"strconv"
"strings"
"github.com/docker/buildx/driver"
"github.com/docker/cli/opts"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
// archiveHeaderSize is the number of bytes in an archive header
archiveHeaderSize = 512
// mobyHostGatewayName defines a special string which users can append to
// --add-host to add an extra entry in /etc/hosts that maps
// host.docker.internal to the host IP
mobyHostGatewayName = "host-gateway"
)
func isArchive(header []byte) bool {
for _, m := range [][]byte{
{0x42, 0x5A, 0x68}, // bzip2
{0x1F, 0x8B, 0x08}, // gzip
{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz
} {
if len(header) < len(m) {
continue
}
if bytes.Equal(m, header[:len(m)]) {
return true
}
}
r := tar.NewReader(bytes.NewBuffer(header))
_, err := r.Next()
return err == nil
}
// toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format
func toBuildkitExtraHosts(ctx context.Context, inp []string, nodeDriver *driver.DriverHandle) (string, error) {
if len(inp) == 0 {
return "", nil
}
hosts := make([]string, 0, len(inp))
for _, h := range inp {
host, ip, ok := strings.Cut(h, "=")
if !ok {
host, ip, ok = strings.Cut(h, ":")
}
if !ok || host == "" || ip == "" {
return "", errors.Errorf("invalid host %s", h)
}
// If the IP Address is a "host-gateway", replace this value with the
// IP address provided by the worker's label.
var ips []string
if ip == mobyHostGatewayName {
hgip, err := nodeDriver.HostGatewayIP(ctx)
if err != nil {
return "", errors.Wrap(err, "unable to derive the IP value for host-gateway")
}
ips = append(ips, hgip.String())
} else {
for v := range strings.SplitSeq(ip, ",") {
// If the address is enclosed in square brackets, extract it
// (for IPv6, but permit it for IPv4 as well; we don't know the
// address family here, but it's unambiguous).
if len(v) > 2 && v[0] == '[' && v[len(v)-1] == ']' {
v = v[1 : len(v)-1]
}
if net.ParseIP(v) == nil {
return "", errors.Errorf("invalid host %s", h)
}
ips = append(ips, v)
}
}
for _, v := range ips {
hosts = append(hosts, host+"="+v)
}
}
return strings.Join(hosts, ","), nil
}
// toBuildkitUlimits converts ulimits from docker type=soft:hard format to buildkit's csv format
func toBuildkitUlimits(inp *opts.UlimitOpt) (string, error) {
if inp == nil || len(inp.GetList()) == 0 {
return "", nil
}
ulimits := make([]string, 0, len(inp.GetList()))
for _, ulimit := range inp.GetList() {
ulimits = append(ulimits, ulimit.String())
}
return strings.Join(ulimits, ","), nil
}
// User-facing resource keys accepted in `--resource key=value` entries, mirroring docker run flag names.
const (
resourceKeyMemory = "memory"
resourceKeyMemorySwap = "memory-swap"
resourceKeyCPUShares = "cpu-shares"
resourceKeyCPUPeriod = "cpu-period"
resourceKeyCPUQuota = "cpu-quota"
resourceKeyCPUSetCPUs = "cpuset-cpus"
resourceKeyCPUSetMems = "cpuset-mems"
)
// Frontend attribute keys, must match those parsed by BuildKit's dockerui frontend.
const (
attrMemory = "memory"
attrMemorySwap = "memswap"
attrCPUShares = "cpushares"
attrCPUPeriod = "cpuperiod"
attrCPUQuota = "cpuquota"
attrCPUSetCPUs = "cpusetcpus"
attrCPUSetMems = "cpusetmems"
)
// ParseResourceLimits parses `key=value` entries from the `--resource` flag into ResourceLimits.
func ParseResourceLimits(entries []string) (ResourceLimits, error) {
var rl ResourceLimits
for _, entry := range entries {
k, v, ok := strings.Cut(entry, "=")
if !ok {
return rl, errors.Errorf("invalid resource %q, expected key=value", entry)
}
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
switch k {
case resourceKeyMemory:
if err := rl.Memory.Set(v); err != nil {
return rl, errors.Wrapf(err, "invalid value %q for resource %s", v, k)
}
case resourceKeyMemorySwap:
if err := rl.MemorySwap.Set(v); err != nil {
return rl, errors.Wrapf(err, "invalid value %q for resource %s", v, k)
}
case resourceKeyCPUShares, resourceKeyCPUPeriod, resourceKeyCPUQuota:
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return rl, errors.Wrapf(err, "invalid value %q for resource %s", v, k)
}
switch k {
case resourceKeyCPUShares:
rl.CPUShares = n
case resourceKeyCPUPeriod:
rl.CPUPeriod = n
case resourceKeyCPUQuota:
rl.CPUQuota = n
}
case resourceKeyCPUSetCPUs:
rl.CPUSetCPUs = v
case resourceKeyCPUSetMems:
rl.CPUSetMems = v
default:
return rl, errors.Errorf("unknown resource %q", k)
}
}
return rl, nil
}
// addResourceLimits sets the frontend attributes for the resource limits.
// Only non-zero values are sent, so builds against a daemon without the feature keep working.
func addResourceLimits(rl ResourceLimits, attrs map[string]string) {
if v := rl.Memory.Value(); v > 0 {
attrs[attrMemory] = strconv.FormatInt(v, 10)
}
if v := rl.MemorySwap.Value(); v != 0 {
attrs[attrMemorySwap] = strconv.FormatInt(v, 10)
}
if rl.CPUShares > 0 {
attrs[attrCPUShares] = strconv.FormatInt(rl.CPUShares, 10)
}
if rl.CPUPeriod > 0 {
attrs[attrCPUPeriod] = strconv.FormatInt(rl.CPUPeriod, 10)
}
if rl.CPUQuota > 0 {
attrs[attrCPUQuota] = strconv.FormatInt(rl.CPUQuota, 10)
}
if rl.CPUSetCPUs != "" {
attrs[attrCPUSetCPUs] = rl.CPUSetCPUs
}
if rl.CPUSetMems != "" {
attrs[attrCPUSetMems] = rl.CPUSetMems
}
}
func notSupported(f driver.Feature, d *driver.DriverHandle, docs string) error {
return errors.Errorf(`%s is not supported for the %s driver.
Switch to a different driver, or turn on the containerd image store, and try again.
Learn more at %s`, f, d.Factory().Name(), docs)
}
func noDefaultLoad() bool {
v, ok := os.LookupEnv("BUILDX_NO_DEFAULT_LOAD")
if !ok {
return false
}
b, err := strconv.ParseBool(v)
if err != nil {
logrus.Warnf("invalid non-bool value for BUILDX_NO_DEFAULT_LOAD: %s", v)
}
return b
}