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>
This commit is contained in:
+53
-26
@@ -606,6 +606,13 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
|
|||||||
override.Append = appendTo
|
override.Append = appendTo
|
||||||
override.ArrValue = append(override.ArrValue, parts[1])
|
override.ArrValue = append(override.ArrValue, parts[1])
|
||||||
}
|
}
|
||||||
|
case "resources":
|
||||||
|
if len(keys) != 3 {
|
||||||
|
return nil, errors.Errorf("invalid key %s, resources requires name", parts[0])
|
||||||
|
}
|
||||||
|
if len(parts) == 2 {
|
||||||
|
override.Value = parts[1]
|
||||||
|
}
|
||||||
case "args":
|
case "args":
|
||||||
if len(keys) != 3 {
|
if len(keys) != 3 {
|
||||||
return nil, errors.Errorf("invalid key %s, args requires name", parts[0])
|
return nil, errors.Errorf("invalid key %s, args requires name", parts[0])
|
||||||
@@ -745,32 +752,33 @@ type Target struct {
|
|||||||
// Inherits is the only field that cannot be overridden with --set
|
// Inherits is the only field that cannot be overridden with --set
|
||||||
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
|
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
|
||||||
|
|
||||||
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
||||||
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
||||||
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
||||||
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
||||||
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
||||||
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
|
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
|
||||||
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
|
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
|
||||||
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
||||||
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
||||||
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
|
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
|
||||||
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
|
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
|
||||||
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
||||||
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
||||||
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
||||||
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
||||||
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
||||||
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
||||||
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
||||||
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
|
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
|
||||||
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
|
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
|
||||||
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
|
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
|
||||||
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
|
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
|
||||||
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
Resources *buildflags.ResourcesConfig `json:"resources,omitempty" hcl:"resources,optional" cty:"resources"`
|
||||||
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
|
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
||||||
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
|
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
|
||||||
Policy buildflags.PolicyConfigs `json:"policy,omitempty" hcl:"policy,optional" cty:"policy"`
|
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
|
||||||
|
Policy buildflags.PolicyConfigs `json:"policy,omitempty" hcl:"policy,optional" cty:"policy"`
|
||||||
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.
|
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.
|
||||||
|
|
||||||
// linked is a private field to mark a target used as a linked one
|
// linked is a private field to mark a target used as a linked one
|
||||||
@@ -944,6 +952,9 @@ func (t *Target) Merge(t2 *Target) {
|
|||||||
if t2.Ulimits != nil { // merge
|
if t2.Ulimits != nil { // merge
|
||||||
t.Ulimits = append(t.Ulimits, t2.Ulimits...)
|
t.Ulimits = append(t.Ulimits, t2.Ulimits...)
|
||||||
}
|
}
|
||||||
|
if t2.Resources != nil { // merge
|
||||||
|
t.Resources = t.Resources.Merge(t2.Resources)
|
||||||
|
}
|
||||||
if t2.Description != "" {
|
if t2.Description != "" {
|
||||||
t.Description = t2.Description
|
t.Description = t2.Description
|
||||||
}
|
}
|
||||||
@@ -1138,6 +1149,16 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon
|
|||||||
} else {
|
} else {
|
||||||
t.Ulimits = o.ArrValue
|
t.Ulimits = o.ArrValue
|
||||||
}
|
}
|
||||||
|
case "resources":
|
||||||
|
if len(keys) != 2 {
|
||||||
|
return errors.Errorf("invalid format for resources, expecting resources.<name>=<value>")
|
||||||
|
}
|
||||||
|
if t.Resources == nil {
|
||||||
|
t.Resources = &buildflags.ResourcesConfig{}
|
||||||
|
}
|
||||||
|
if err := t.Resources.SetField(keys[1], value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case "network":
|
case "network":
|
||||||
t.NetworkMode = &value
|
t.NetworkMode = &value
|
||||||
case "pull":
|
case "pull":
|
||||||
@@ -1612,6 +1633,12 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
|||||||
}
|
}
|
||||||
bo.Ulimits = ulimits
|
bo.Ulimits = ulimits
|
||||||
|
|
||||||
|
resourceLimits, err := build.ParseResourceLimits(t.Resources.ToEntries())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bo.ResourceLimits = resourceLimits
|
||||||
|
|
||||||
bo.Allow = append(bo.Allow, t.Entitlements...)
|
bo.Allow = append(bo.Allow, t.Entitlements...)
|
||||||
|
|
||||||
return bo, nil
|
return bo, nil
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ target "webDEP" {
|
|||||||
no-cache = true
|
no-cache = true
|
||||||
shm-size = "128m"
|
shm-size = "128m"
|
||||||
ulimits = ["nofile=1024:1024"]
|
ulimits = ["nofile=1024:1024"]
|
||||||
|
resources = {
|
||||||
|
memory = "2g"
|
||||||
|
memory-swap = "4g"
|
||||||
|
cpu-shares = 1024
|
||||||
|
cpu-period = 100000
|
||||||
|
cpu-quota = 50000
|
||||||
|
cpuset-cpus = "0-3"
|
||||||
|
cpuset-mems = "0,1"
|
||||||
|
}
|
||||||
extra-hosts = {
|
extra-hosts = {
|
||||||
my_hostname = "8.8.8.8"
|
my_hostname = "8.8.8.8"
|
||||||
}
|
}
|
||||||
@@ -67,6 +76,14 @@ target "webapp" {
|
|||||||
require.Equal(t, true, *m["webapp"].NoCache)
|
require.Equal(t, true, *m["webapp"].NoCache)
|
||||||
require.Equal(t, "128m", *m["webapp"].ShmSize)
|
require.Equal(t, "128m", *m["webapp"].ShmSize)
|
||||||
require.Equal(t, []string{"nofile=1024:1024"}, m["webapp"].Ulimits)
|
require.Equal(t, []string{"nofile=1024:1024"}, m["webapp"].Ulimits)
|
||||||
|
require.NotNil(t, m["webapp"].Resources)
|
||||||
|
require.Equal(t, ptrstr("2g"), m["webapp"].Resources.Memory)
|
||||||
|
require.Equal(t, ptrstr("4g"), m["webapp"].Resources.MemorySwap)
|
||||||
|
require.Equal(t, int64(1024), *m["webapp"].Resources.CPUShares)
|
||||||
|
require.Equal(t, int64(100000), *m["webapp"].Resources.CPUPeriod)
|
||||||
|
require.Equal(t, int64(50000), *m["webapp"].Resources.CPUQuota)
|
||||||
|
require.Equal(t, ptrstr("0-3"), m["webapp"].Resources.CPUSetCPUs)
|
||||||
|
require.Equal(t, ptrstr("0,1"), m["webapp"].Resources.CPUSetMems)
|
||||||
require.Equal(t, map[string]*string{"my_hostname": ptrstr("8.8.8.8")}, m["webapp"].ExtraHosts)
|
require.Equal(t, map[string]*string{"my_hostname": ptrstr("8.8.8.8")}, m["webapp"].ExtraHosts)
|
||||||
require.Nil(t, m["webapp"].Pull)
|
require.Nil(t, m["webapp"].Pull)
|
||||||
|
|
||||||
@@ -226,6 +243,21 @@ target "webapp" {
|
|||||||
require.Equal(t, "256m", *m["webapp"].ShmSize)
|
require.Equal(t, "256m", *m["webapp"].ShmSize)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ResourceLimitsOverride", func(t *testing.T) {
|
||||||
|
m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{
|
||||||
|
"webapp.resources.memory=512m",
|
||||||
|
"webapp.resources.cpu-quota=25000",
|
||||||
|
}, nil, nil, &EntitlementConf{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ptrstr("512m"), m["webapp"].Resources.Memory)
|
||||||
|
require.Equal(t, int64(25000), *m["webapp"].Resources.CPUQuota)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ResourceLimitsInvalidOverride", func(t *testing.T) {
|
||||||
|
_, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.resources.cpu-quota=notanumber"}, nil, nil, &EntitlementConf{})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("PullOverride", func(t *testing.T) {
|
t.Run("PullOverride", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.pull=false"}, nil, nil, &EntitlementConf{})
|
m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.pull=false"}, nil, nil, &EntitlementConf{})
|
||||||
@@ -1834,6 +1866,100 @@ func TestExtraHostsDeterministicOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResourceLimitsToBuildOpt(t *testing.T) {
|
||||||
|
newInt64 := func(v int64) *int64 { return &v }
|
||||||
|
bo, err := toBuildOpt(&Target{
|
||||||
|
DockerfileInline: ptrstr("FROM scratch"),
|
||||||
|
Resources: &buildflags.ResourcesConfig{
|
||||||
|
Memory: ptrstr("2g"),
|
||||||
|
MemorySwap: ptrstr("4g"),
|
||||||
|
CPUShares: newInt64(1024),
|
||||||
|
CPUPeriod: newInt64(100000),
|
||||||
|
CPUQuota: newInt64(50000),
|
||||||
|
CPUSetCPUs: ptrstr("0-3"),
|
||||||
|
CPUSetMems: ptrstr("0,1"),
|
||||||
|
},
|
||||||
|
}, &Input{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(2*1024*1024*1024), bo.ResourceLimits.Memory.Value())
|
||||||
|
require.Equal(t, int64(4*1024*1024*1024), bo.ResourceLimits.MemorySwap.Value())
|
||||||
|
require.Equal(t, int64(1024), bo.ResourceLimits.CPUShares)
|
||||||
|
require.Equal(t, int64(100000), bo.ResourceLimits.CPUPeriod)
|
||||||
|
require.Equal(t, int64(50000), bo.ResourceLimits.CPUQuota)
|
||||||
|
require.Equal(t, "0-3", bo.ResourceLimits.CPUSetCPUs)
|
||||||
|
require.Equal(t, "0,1", bo.ResourceLimits.CPUSetMems)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceLimitsUnlimitedSwap(t *testing.T) {
|
||||||
|
bo, err := toBuildOpt(&Target{
|
||||||
|
DockerfileInline: ptrstr("FROM scratch"),
|
||||||
|
Resources: &buildflags.ResourcesConfig{MemorySwap: ptrstr("-1")},
|
||||||
|
}, &Input{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(-1), bo.ResourceLimits.MemorySwap.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceLimitsInvalid(t *testing.T) {
|
||||||
|
_, err := toBuildOpt(&Target{
|
||||||
|
DockerfileInline: ptrstr("FROM scratch"),
|
||||||
|
Resources: &buildflags.ResourcesConfig{Memory: ptrstr("notabyte")},
|
||||||
|
}, &Input{})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourcesObject(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
cpuShares, cpuQuota := int64(1024), int64(50000)
|
||||||
|
want := &buildflags.ResourcesConfig{
|
||||||
|
Memory: ptrstr("2g"),
|
||||||
|
MemorySwap: ptrstr("4g"),
|
||||||
|
CPUShares: &cpuShares,
|
||||||
|
CPUQuota: &cpuQuota,
|
||||||
|
CPUSetCPUs: ptrstr("0-3"),
|
||||||
|
}
|
||||||
|
|
||||||
|
data := `target "app" {
|
||||||
|
resources = {
|
||||||
|
memory = "2g"
|
||||||
|
memory-swap = "4g"
|
||||||
|
cpu-shares = 1024
|
||||||
|
cpu-quota = 50000
|
||||||
|
cpuset-cpus = "0-3"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
m, _, err := ReadTargets(ctx, []File{{Name: "docker-bake.hcl", Data: []byte(data)}}, []string{"app"}, nil, nil, nil, &EntitlementConf{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, want, m["app"].Resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResourcesInheritIsolation ensures merging resources from a shared base
|
||||||
|
// target doesn't leak a child's override into the base or a sibling.
|
||||||
|
func TestResourcesInheritIsolation(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
data := `target "base" {
|
||||||
|
resources = {
|
||||||
|
memory = "1g"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target "child1" {
|
||||||
|
inherits = ["base"]
|
||||||
|
resources = {
|
||||||
|
memory = "2g"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target "child2" {
|
||||||
|
inherits = ["base"]
|
||||||
|
}`
|
||||||
|
|
||||||
|
m, _, err := ReadTargets(ctx, []File{{Name: "docker-bake.hcl", Data: []byte(data)}}, []string{"child1", "child2"}, nil, nil, nil, &EntitlementConf{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ptrstr("2g"), m["child1"].Resources.Memory)
|
||||||
|
require.Equal(t, ptrstr("1g"), m["child2"].Resources.Memory)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAnnotations(t *testing.T) {
|
func TestAnnotations(t *testing.T) {
|
||||||
fp := File{
|
fp := File{
|
||||||
Name: "docker-bake.hcl",
|
Name: "docker-bake.hcl",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ type Options struct {
|
|||||||
Tags []string
|
Tags []string
|
||||||
Target string
|
Target string
|
||||||
Ulimits *opts.UlimitOpt
|
Ulimits *opts.UlimitOpt
|
||||||
|
ResourceLimits ResourceLimits
|
||||||
|
|
||||||
Session []session.Attachable
|
Session []session.Attachable
|
||||||
Linked bool // Linked marks this target as exclusively linked (not requested by the user).
|
Linked bool // Linked marks this target as exclusively linked (not requested by the user).
|
||||||
@@ -98,6 +99,18 @@ type Options struct {
|
|||||||
Policy []buildflags.PolicyConfig
|
Policy []buildflags.PolicyConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceLimits holds the cgroup resource constraints applied to individual
|
||||||
|
// build steps (RUN instructions). They don't affect the build cache key.
|
||||||
|
type ResourceLimits struct {
|
||||||
|
Memory opts.MemBytes
|
||||||
|
MemorySwap opts.MemSwapBytes
|
||||||
|
CPUShares int64
|
||||||
|
CPUPeriod int64
|
||||||
|
CPUQuota int64
|
||||||
|
CPUSetCPUs string
|
||||||
|
CPUSetMems string
|
||||||
|
}
|
||||||
|
|
||||||
type CallFunc struct {
|
type CallFunc struct {
|
||||||
Name string
|
Name string
|
||||||
Format string
|
Format string
|
||||||
|
|||||||
@@ -610,6 +610,9 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
|
|||||||
so.FrontendAttrs["ulimit"] = ulimits
|
so.FrontendAttrs["ulimit"] = ulimits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setup per-step resource limits
|
||||||
|
addResourceLimits(opt.ResourceLimits, so.FrontendAttrs)
|
||||||
|
|
||||||
// mark call request as internal
|
// mark call request as internal
|
||||||
if opt.CallFunc != nil {
|
if opt.CallFunc != nil {
|
||||||
so.Internal = true
|
so.Internal = true
|
||||||
|
|||||||
@@ -99,6 +99,97 @@ func toBuildkitUlimits(inp *opts.UlimitOpt) (string, error) {
|
|||||||
return strings.Join(ulimits, ","), nil
|
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 {
|
func notSupported(f driver.Feature, d *driver.DriverHandle, docs string) error {
|
||||||
return errors.Errorf(`%s is not supported for the %s driver.
|
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.
|
Switch to a different driver, or turn on the containerd image store, and try again.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
dockeropts "github.com/docker/cli/opts"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -150,3 +151,109 @@ func TestToBuildkitExtraHosts(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddResourceLimits(t *testing.T) {
|
||||||
|
mustMemSwap := func(v string) dockeropts.MemSwapBytes {
|
||||||
|
var m dockeropts.MemSwapBytes
|
||||||
|
require.NoError(t, m.Set(v))
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
limits ResourceLimits
|
||||||
|
expected map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
limits: ResourceLimits{},
|
||||||
|
expected: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all",
|
||||||
|
limits: ResourceLimits{
|
||||||
|
Memory: dockeropts.MemBytes(2 * 1024 * 1024 * 1024),
|
||||||
|
MemorySwap: mustMemSwap("4g"),
|
||||||
|
CPUShares: 1024,
|
||||||
|
CPUPeriod: 100000,
|
||||||
|
CPUQuota: 50000,
|
||||||
|
CPUSetCPUs: "0-3",
|
||||||
|
CPUSetMems: "0,1",
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"memory": "2147483648",
|
||||||
|
"memswap": "4294967296",
|
||||||
|
"cpushares": "1024",
|
||||||
|
"cpuperiod": "100000",
|
||||||
|
"cpuquota": "50000",
|
||||||
|
"cpusetcpus": "0-3",
|
||||||
|
"cpusetmems": "0,1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unlimited swap",
|
||||||
|
limits: ResourceLimits{MemorySwap: mustMemSwap("-1")},
|
||||||
|
expected: map[string]string{
|
||||||
|
"memswap": "-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partial",
|
||||||
|
limits: ResourceLimits{Memory: dockeropts.MemBytes(512 * 1024 * 1024)},
|
||||||
|
expected: map[string]string{
|
||||||
|
"memory": "536870912",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
attrs := map[string]string{}
|
||||||
|
addResourceLimits(tc.limits, attrs)
|
||||||
|
require.Equal(t, tc.expected, attrs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseResourceLimits(t *testing.T) {
|
||||||
|
t.Run("all", func(t *testing.T) {
|
||||||
|
rl, err := ParseResourceLimits([]string{
|
||||||
|
"memory=2g",
|
||||||
|
"memory-swap=4g",
|
||||||
|
"cpu-shares=1024",
|
||||||
|
"cpu-period=100000",
|
||||||
|
"cpu-quota=50000",
|
||||||
|
"cpuset-cpus=0-3",
|
||||||
|
"cpuset-mems=0,1",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(2*1024*1024*1024), rl.Memory.Value())
|
||||||
|
require.Equal(t, int64(4*1024*1024*1024), rl.MemorySwap.Value())
|
||||||
|
require.Equal(t, int64(1024), rl.CPUShares)
|
||||||
|
require.Equal(t, int64(100000), rl.CPUPeriod)
|
||||||
|
require.Equal(t, int64(50000), rl.CPUQuota)
|
||||||
|
require.Equal(t, "0-3", rl.CPUSetCPUs)
|
||||||
|
require.Equal(t, "0,1", rl.CPUSetMems)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unlimited swap", func(t *testing.T) {
|
||||||
|
rl, err := ParseResourceLimits([]string{"memory-swap=-1"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(-1), rl.MemorySwap.Value())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing value", func(t *testing.T) {
|
||||||
|
_, err := ParseResourceLimits([]string{"memory"})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown key", func(t *testing.T) {
|
||||||
|
_, err := ParseResourceLimits([]string{"bogus=1"})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid int", func(t *testing.T) {
|
||||||
|
_, err := ParseResourceLimits([]string{"cpu-shares=notanumber"})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+51
-22
@@ -89,6 +89,11 @@ type buildOptions struct {
|
|||||||
target string
|
target string
|
||||||
ulimits *dockeropts.UlimitOpt
|
ulimits *dockeropts.UlimitOpt
|
||||||
|
|
||||||
|
resources []string
|
||||||
|
// legacyResources is kept separate from resources because the pflag
|
||||||
|
// StringArray for --resource resets its backing slice on first use.
|
||||||
|
legacyResources []string
|
||||||
|
|
||||||
attests []string
|
attests []string
|
||||||
sbom string
|
sbom string
|
||||||
provenance string
|
provenance string
|
||||||
@@ -133,6 +138,7 @@ func (o *buildOptions) toOptions() (*BuildOptions, error) {
|
|||||||
Tags: o.tags,
|
Tags: o.tags,
|
||||||
Target: o.target,
|
Target: o.target,
|
||||||
Ulimits: o.ulimits,
|
Ulimits: o.ulimits,
|
||||||
|
Resources: append(o.legacyResources, o.resources...),
|
||||||
Builder: o.builder,
|
Builder: o.builder,
|
||||||
NoCache: o.noCache,
|
NoCache: o.noCache,
|
||||||
Pull: o.pull,
|
Pull: o.pull,
|
||||||
@@ -234,6 +240,20 @@ func (o *buildOptions) toDisplayMode() (progressui.DisplayMode, error) {
|
|||||||
return progress, nil
|
return progress, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacyResourceValue is a pflag.Value that appends a "<key>=value" entry to
|
||||||
|
// the --resource list, wiring the hidden legacy flags into the same code path.
|
||||||
|
type legacyResourceValue struct {
|
||||||
|
key string
|
||||||
|
resources *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *legacyResourceValue) String() string { return "" }
|
||||||
|
func (v *legacyResourceValue) Type() string { return "string" }
|
||||||
|
func (v *legacyResourceValue) Set(s string) error {
|
||||||
|
*v.resources = append(*v.resources, v.key+"="+s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
commandNameAttribute = attribute.Key("command.name")
|
commandNameAttribute = attribute.Key("command.name")
|
||||||
commandOptionsHash = attribute.Key("command.options.hash")
|
commandOptionsHash = attribute.Key("command.options.hash")
|
||||||
@@ -583,6 +603,8 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
|
|||||||
options.ulimits = dockeropts.NewUlimitOpt(nil)
|
options.ulimits = dockeropts.NewUlimitOpt(nil)
|
||||||
flags.Var(options.ulimits, "ulimit", "Ulimit options")
|
flags.Var(options.ulimits, "ulimit", "Ulimit options")
|
||||||
|
|
||||||
|
flags.StringArrayVar(&options.resources, "resource", []string{}, `Resource limits for build containers (format: "memory=2g", "cpu-quota=50000")`)
|
||||||
|
|
||||||
flags.StringArrayVar(&options.attests, "attest", []string{}, `Attestation parameters (format: "type=sbom,generator=image")`)
|
flags.StringArrayVar(&options.attests, "attest", []string{}, `Attestation parameters (format: "type=sbom,generator=image")`)
|
||||||
flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--attest=type=sbom"`)
|
flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--attest=type=sbom"`)
|
||||||
flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--attest=type=provenance"`)
|
flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--attest=type=provenance"`)
|
||||||
@@ -595,12 +617,33 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
|
|||||||
var ignore string
|
var ignore string
|
||||||
var ignoreSlice []string
|
var ignoreSlice []string
|
||||||
var ignoreBool bool
|
var ignoreBool bool
|
||||||
var ignoreInt int64
|
|
||||||
|
|
||||||
flags.StringVar(&options.callFunc, "print", "", "Print result of information request (e.g., outline, targets)")
|
flags.StringVar(&options.callFunc, "print", "", "Print result of information request (e.g., outline, targets)")
|
||||||
cobrautil.MarkFlagsExperimental(flags, "print")
|
cobrautil.MarkFlagsExperimental(flags, "print")
|
||||||
flags.MarkHidden("print")
|
flags.MarkHidden("print")
|
||||||
|
|
||||||
|
// Legacy per-resource flags, hidden and superseded by --resource.
|
||||||
|
flags.VarP(&legacyResourceValue{key: "memory", resources: &options.legacyResources}, "memory", "m", "Memory limit")
|
||||||
|
flags.MarkHidden("memory")
|
||||||
|
|
||||||
|
flags.VarP(&legacyResourceValue{key: "memory-swap", resources: &options.legacyResources}, "memory-swap", "", `Swap limit equal to memory plus swap: "-1" to enable unlimited swap`)
|
||||||
|
flags.MarkHidden("memory-swap")
|
||||||
|
|
||||||
|
flags.VarP(&legacyResourceValue{key: "cpu-shares", resources: &options.legacyResources}, "cpu-shares", "c", "CPU shares (relative weight)")
|
||||||
|
flags.MarkHidden("cpu-shares")
|
||||||
|
|
||||||
|
flags.VarP(&legacyResourceValue{key: "cpu-period", resources: &options.legacyResources}, "cpu-period", "", "Limit the CPU CFS (Completely Fair Scheduler) period")
|
||||||
|
flags.MarkHidden("cpu-period")
|
||||||
|
|
||||||
|
flags.VarP(&legacyResourceValue{key: "cpu-quota", resources: &options.legacyResources}, "cpu-quota", "", "Limit the CPU CFS (Completely Fair Scheduler) quota")
|
||||||
|
flags.MarkHidden("cpu-quota")
|
||||||
|
|
||||||
|
flags.VarP(&legacyResourceValue{key: "cpuset-cpus", resources: &options.legacyResources}, "cpuset-cpus", "", `CPUs in which to allow execution ("0-3", "0,1")`)
|
||||||
|
flags.MarkHidden("cpuset-cpus")
|
||||||
|
|
||||||
|
flags.VarP(&legacyResourceValue{key: "cpuset-mems", resources: &options.legacyResources}, "cpuset-mems", "", `MEMs in which to allow execution ("0-3", "0,1")`)
|
||||||
|
flags.MarkHidden("cpuset-mems")
|
||||||
|
|
||||||
flags.BoolVar(&ignoreBool, "compress", false, "Compress the build context using gzip")
|
flags.BoolVar(&ignoreBool, "compress", false, "Compress the build context using gzip")
|
||||||
flags.MarkHidden("compress")
|
flags.MarkHidden("compress")
|
||||||
|
|
||||||
@@ -617,27 +660,6 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
|
|||||||
flags.SetAnnotation("squash", "flag-warn", []string{"experimental flag squash is removed with BuildKit. You should squash inside build using a multi-stage Dockerfile for efficiency."})
|
flags.SetAnnotation("squash", "flag-warn", []string{"experimental flag squash is removed with BuildKit. You should squash inside build using a multi-stage Dockerfile for efficiency."})
|
||||||
cobrautil.MarkFlagsExperimental(flags, "squash")
|
cobrautil.MarkFlagsExperimental(flags, "squash")
|
||||||
|
|
||||||
flags.StringVarP(&ignore, "memory", "m", "", "Memory limit")
|
|
||||||
flags.MarkHidden("memory")
|
|
||||||
|
|
||||||
flags.StringVar(&ignore, "memory-swap", "", `Swap limit equal to memory plus swap: "-1" to enable unlimited swap`)
|
|
||||||
flags.MarkHidden("memory-swap")
|
|
||||||
|
|
||||||
flags.Int64VarP(&ignoreInt, "cpu-shares", "c", 0, "CPU shares (relative weight)")
|
|
||||||
flags.MarkHidden("cpu-shares")
|
|
||||||
|
|
||||||
flags.Int64Var(&ignoreInt, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period")
|
|
||||||
flags.MarkHidden("cpu-period")
|
|
||||||
|
|
||||||
flags.Int64Var(&ignoreInt, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota")
|
|
||||||
flags.MarkHidden("cpu-quota")
|
|
||||||
|
|
||||||
flags.StringVar(&ignore, "cpuset-cpus", "", `CPUs in which to allow execution ("0-3", "0,1")`)
|
|
||||||
flags.MarkHidden("cpuset-cpus")
|
|
||||||
|
|
||||||
flags.StringVar(&ignore, "cpuset-mems", "", `MEMs in which to allow execution ("0-3", "0,1")`)
|
|
||||||
flags.MarkHidden("cpuset-mems")
|
|
||||||
|
|
||||||
flags.BoolVar(&ignoreBool, "rm", true, "Remove intermediate containers after a successful build")
|
flags.BoolVar(&ignoreBool, "rm", true, "Remove intermediate containers after a successful build")
|
||||||
flags.MarkHidden("rm")
|
flags.MarkHidden("rm")
|
||||||
|
|
||||||
@@ -990,6 +1012,7 @@ type BuildOptions struct {
|
|||||||
Tags []string
|
Tags []string
|
||||||
Target string
|
Target string
|
||||||
Ulimits *dockeropts.UlimitOpt
|
Ulimits *dockeropts.UlimitOpt
|
||||||
|
Resources []string
|
||||||
Builder string
|
Builder string
|
||||||
NoCache bool
|
NoCache bool
|
||||||
Pull bool
|
Pull bool
|
||||||
@@ -1014,6 +1037,11 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
|
|||||||
contexts[name] = build.NamedContext{Path: path}
|
contexts[name] = build.NamedContext{Path: path}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resourceLimits, err := build.ParseResourceLimits(in.Resources)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
opts := build.Options{
|
opts := build.Options{
|
||||||
Inputs: build.Inputs{
|
Inputs: build.Inputs{
|
||||||
ContextPath: in.ContextPath,
|
ContextPath: in.ContextPath,
|
||||||
@@ -1034,6 +1062,7 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
|
|||||||
Tags: in.Tags,
|
Tags: in.Tags,
|
||||||
Target: in.Target,
|
Target: in.Target,
|
||||||
Ulimits: in.Ulimits,
|
Ulimits: in.Ulimits,
|
||||||
|
ResourceLimits: resourceLimits,
|
||||||
GroupRef: in.GroupRef,
|
GroupRef: in.GroupRef,
|
||||||
ProvenanceResponseMode: confutil.ParseMetadataProvenance(in.ProvenanceResponseMode),
|
ProvenanceResponseMode: confutil.ParseMetadataProvenance(in.ProvenanceResponseMode),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,13 @@ type configOutput struct {
|
|||||||
|
|
||||||
ShmSize string `json:",omitempty"`
|
ShmSize string `json:",omitempty"`
|
||||||
Ulimit string `json:",omitempty"`
|
Ulimit string `json:",omitempty"`
|
||||||
|
Memory string `json:",omitempty"`
|
||||||
|
MemorySwap string `json:",omitempty"`
|
||||||
|
CPUShares string `json:",omitempty"`
|
||||||
|
CPUPeriod string `json:",omitempty"`
|
||||||
|
CPUQuota string `json:",omitempty"`
|
||||||
|
CPUSetCPUs string `json:",omitempty"`
|
||||||
|
CPUSetMems string `json:",omitempty"`
|
||||||
CacheMountNS string `json:",omitempty"`
|
CacheMountNS string `json:",omitempty"`
|
||||||
DockerfileCheckConfig string `json:",omitempty"`
|
DockerfileCheckConfig string `json:",omitempty"`
|
||||||
SourceDateEpoch string `json:",omitempty"`
|
SourceDateEpoch string `json:",omitempty"`
|
||||||
@@ -391,6 +398,13 @@ workers0:
|
|||||||
|
|
||||||
readAttr(attrs, "shm-size", &out.Config.ShmSize, nil)
|
readAttr(attrs, "shm-size", &out.Config.ShmSize, nil)
|
||||||
readAttr(attrs, "ulimit", &out.Config.Ulimit, nil)
|
readAttr(attrs, "ulimit", &out.Config.Ulimit, nil)
|
||||||
|
readAttr(attrs, "memory", &out.Config.Memory, nil)
|
||||||
|
readAttr(attrs, "memswap", &out.Config.MemorySwap, nil)
|
||||||
|
readAttr(attrs, "cpushares", &out.Config.CPUShares, nil)
|
||||||
|
readAttr(attrs, "cpuperiod", &out.Config.CPUPeriod, nil)
|
||||||
|
readAttr(attrs, "cpuquota", &out.Config.CPUQuota, nil)
|
||||||
|
readAttr(attrs, "cpusetcpus", &out.Config.CPUSetCPUs, nil)
|
||||||
|
readAttr(attrs, "cpusetmems", &out.Config.CPUSetMems, nil)
|
||||||
readAttr(attrs, "build-arg:BUILDKIT_CACHE_MOUNT_NS", &out.Config.CacheMountNS, nil)
|
readAttr(attrs, "build-arg:BUILDKIT_CACHE_MOUNT_NS", &out.Config.CacheMountNS, nil)
|
||||||
readAttr(attrs, "build-arg:BUILDKIT_DOCKERFILE_CHECK", &out.Config.DockerfileCheckConfig, nil)
|
readAttr(attrs, "build-arg:BUILDKIT_DOCKERFILE_CHECK", &out.Config.DockerfileCheckConfig, nil)
|
||||||
readAttr(attrs, "build-arg:"+epoch.SourceDateEpochEnv, &out.Config.SourceDateEpoch, nil)
|
readAttr(attrs, "build-arg:"+epoch.SourceDateEpochEnv, &out.Config.SourceDateEpoch, nil)
|
||||||
@@ -570,6 +584,27 @@ workers0:
|
|||||||
if out.Config.Ulimit != "" {
|
if out.Config.Ulimit != "" {
|
||||||
fmt.Fprintf(tw, "Resource Limits:\t%s\n", out.Config.Ulimit)
|
fmt.Fprintf(tw, "Resource Limits:\t%s\n", out.Config.Ulimit)
|
||||||
}
|
}
|
||||||
|
if out.Config.Memory != "" {
|
||||||
|
fmt.Fprintf(tw, "Memory:\t%s\n", out.Config.Memory)
|
||||||
|
}
|
||||||
|
if out.Config.MemorySwap != "" {
|
||||||
|
fmt.Fprintf(tw, "Memory Swap:\t%s\n", out.Config.MemorySwap)
|
||||||
|
}
|
||||||
|
if out.Config.CPUShares != "" {
|
||||||
|
fmt.Fprintf(tw, "CPU Shares:\t%s\n", out.Config.CPUShares)
|
||||||
|
}
|
||||||
|
if out.Config.CPUPeriod != "" {
|
||||||
|
fmt.Fprintf(tw, "CPU Period:\t%s\n", out.Config.CPUPeriod)
|
||||||
|
}
|
||||||
|
if out.Config.CPUQuota != "" {
|
||||||
|
fmt.Fprintf(tw, "CPU Quota:\t%s\n", out.Config.CPUQuota)
|
||||||
|
}
|
||||||
|
if out.Config.CPUSetCPUs != "" {
|
||||||
|
fmt.Fprintf(tw, "CPUSet CPUs:\t%s\n", out.Config.CPUSetCPUs)
|
||||||
|
}
|
||||||
|
if out.Config.CPUSetMems != "" {
|
||||||
|
fmt.Fprintf(tw, "CPUSet Mems:\t%s\n", out.Config.CPUSetMems)
|
||||||
|
}
|
||||||
if out.Config.CacheMountNS != "" {
|
if out.Config.CacheMountNS != "" {
|
||||||
fmt.Fprintf(tw, "Cache Mount Namespace:\t%s\n", out.Config.CacheMountNS)
|
fmt.Fprintf(tw, "Cache Mount Namespace:\t%s\n", out.Config.CacheMountNS)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ The following table shows the complete list of attributes that you can assign to
|
|||||||
| [`policy`](#targetpolicy) | List | Policies to validate build sources and metadata |
|
| [`policy`](#targetpolicy) | List | Policies to validate build sources and metadata |
|
||||||
| [`platforms`](#targetplatforms) | List | Target platforms |
|
| [`platforms`](#targetplatforms) | List | Target platforms |
|
||||||
| [`pull`](#targetpull) | Boolean | Always pull images |
|
| [`pull`](#targetpull) | Boolean | Always pull images |
|
||||||
|
| [`resources`](#targetresources) | Map | Resource limits for build containers |
|
||||||
| [`secret`](#targetsecret) | List | Secrets to expose to the build |
|
| [`secret`](#targetsecret) | List | Secrets to expose to the build |
|
||||||
| [`shm-size`](#targetshm-size) | List | Size of `/dev/shm` |
|
| [`shm-size`](#targetshm-size) | List | Size of `/dev/shm` |
|
||||||
| [`ssh`](#targetssh) | List | SSH agent sockets or keys to expose to the build |
|
| [`ssh`](#targetssh) | List | SSH agent sockets or keys to expose to the build |
|
||||||
@@ -939,6 +940,29 @@ target "default" {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `target.resources`
|
||||||
|
|
||||||
|
Sets cgroup resource limits for the containers that run `RUN` instructions
|
||||||
|
during the build. The supported keys are `memory`, `memory-swap`, `cpu-shares`,
|
||||||
|
`cpu-period`, `cpu-quota`, `cpuset-cpus`, and `cpuset-mems`. These map to the
|
||||||
|
equivalent `docker build` flags and to the
|
||||||
|
[`--resource`](https://docs.docker.com/reference/cli/docker/buildx/build/#resource)
|
||||||
|
flag for `docker buildx build`.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
target "default" {
|
||||||
|
resources = {
|
||||||
|
memory = "2g"
|
||||||
|
memory-swap = "4g"
|
||||||
|
cpu-quota = 50000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> These limits require a BuildKit daemon that supports per-step resource limits
|
||||||
|
> and only take effect on Linux. They don't affect the build cache key.
|
||||||
|
|
||||||
### `target.secret`
|
### `target.secret`
|
||||||
|
|
||||||
Defines secrets to expose to the build target.
|
Defines secrets to expose to the build target.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ Start a build
|
|||||||
| `--pull` | `bool` | | Always attempt to pull all referenced images |
|
| `--pull` | `bool` | | Always attempt to pull all referenced images |
|
||||||
| [`--push`](#push) | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
|
| [`--push`](#push) | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
|
||||||
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
|
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
|
||||||
|
| [`--resource`](#resource) | `stringArray` | | Resource limits for build containers (format: `memory=2g`, `cpu-quota=50000`) |
|
||||||
| [`--sbom`](#sbom) | `string` | | Shorthand for `--attest=type=sbom` |
|
| [`--sbom`](#sbom) | `string` | | Shorthand for `--attest=type=sbom` |
|
||||||
| [`--secret`](#secret) | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
|
| [`--secret`](#secret) | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
|
||||||
| [`--shm-size`](#shm-size) | `bytes` | `0` | Shared memory size for build containers |
|
| [`--shm-size`](#shm-size) | `bytes` | `0` | Shared memory size for build containers |
|
||||||
@@ -1158,3 +1159,40 @@ $ docker buildx build --ulimit nofile=1024:1024 .
|
|||||||
> In most cases, it is recommended to let the builder automatically determine
|
> In most cases, it is recommended to let the builder automatically determine
|
||||||
> the appropriate configurations. Manual adjustments should only be considered
|
> the appropriate configurations. Manual adjustments should only be considered
|
||||||
> when specific performance tuning is required for complex build scenarios.
|
> when specific performance tuning is required for complex build scenarios.
|
||||||
|
|
||||||
|
### <a name="resource"></a> Set CPU and memory limits for build containers (--resource)
|
||||||
|
|
||||||
|
The `--resource` flag constrains the resources available to the containers that
|
||||||
|
run your `RUN` instructions during the build. It's repeatable and takes
|
||||||
|
`key=value` pairs, where `key` is one of:
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
|:--------------|:-----------------------------------------------------------------------------|
|
||||||
|
| `memory` | Memory limit (format: `<number><unit>`, e.g. `512m`, `2g`). |
|
||||||
|
| `memory-swap` | Total memory plus swap limit. Set to `-1` to allow unlimited swap. |
|
||||||
|
| `cpu-shares` | CPU shares (relative weight). |
|
||||||
|
| `cpu-period` | Length of a CPU CFS (Completely Fair Scheduler) period, in microseconds. |
|
||||||
|
| `cpu-quota` | CPU CFS quota, in microseconds, within each `cpu-period`. |
|
||||||
|
| `cpuset-cpus` | CPUs in which to allow execution (`0-3`, `0,1`). |
|
||||||
|
| `cpuset-mems` | Memory nodes (MEMs) in which to allow execution (`0-3`, `0,1`). |
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ docker buildx build --resource memory=2g --resource cpu-quota=50000 --resource cpu-period=100000 .
|
||||||
|
```
|
||||||
|
|
||||||
|
These map to the cgroup resource limits of the legacy `docker build` API and
|
||||||
|
only apply to individual build steps. They don't affect the build cache key.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> These limits require a BuildKit daemon that supports per-step resource limits
|
||||||
|
> (the `exec.meta.linux.resources` capability) and only take effect on Linux.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Because BuildKit can run build steps in parallel, these limits apply to each
|
||||||
|
> step in isolation rather than to the build as a whole. When the same step is
|
||||||
|
> requested with different limits, the most relaxed limits are used.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> In most cases, it is recommended to let the builder automatically determine
|
||||||
|
> the appropriate configurations. Manual adjustments should only be considered
|
||||||
|
> when specific performance tuning is required for complex build scenarios.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Start a build
|
|||||||
| `--pull` | `bool` | | Always attempt to pull all referenced images |
|
| `--pull` | `bool` | | Always attempt to pull all referenced images |
|
||||||
| `--push` | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
|
| `--push` | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
|
||||||
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
|
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
|
||||||
|
| `--resource` | `stringArray` | | Resource limits for build containers (format: `memory=2g`, `cpu-quota=50000`) |
|
||||||
| `--sbom` | `string` | | Shorthand for `--attest=type=sbom` |
|
| `--sbom` | `string` | | Shorthand for `--attest=type=sbom` |
|
||||||
| `--secret` | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
|
| `--secret` | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
|
||||||
| `--shm-size` | `bytes` | `0` | Shared memory size for build containers |
|
| `--shm-size` | `bytes` | `0` | Shared memory size for build containers |
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Start a build
|
|||||||
| `--pull` | `bool` | | Always attempt to pull all referenced images |
|
| `--pull` | `bool` | | Always attempt to pull all referenced images |
|
||||||
| `--push` | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
|
| `--push` | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
|
||||||
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
|
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
|
||||||
|
| `--resource` | `stringArray` | | Resource limits for build containers (format: `memory=2g`, `cpu-quota=50000`) |
|
||||||
| `--sbom` | `string` | | Shorthand for `--attest=type=sbom` |
|
| `--sbom` | `string` | | Shorthand for `--attest=type=sbom` |
|
||||||
| `--secret` | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
|
| `--secret` | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
|
||||||
| `--shm-size` | `bytes` | `0` | Shared memory size for build containers |
|
| `--shm-size` | `bytes` | `0` | Shared memory size for build containers |
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package buildflags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourcesConfig holds the cgroup resource limits applied to individual build
|
||||||
|
// steps (RUN instructions). It maps to the `--resource` build flags.
|
||||||
|
type ResourcesConfig struct {
|
||||||
|
Memory *string `json:"memory,omitempty"`
|
||||||
|
MemorySwap *string `json:"memory-swap,omitempty"`
|
||||||
|
CPUShares *int64 `json:"cpu-shares,omitempty"`
|
||||||
|
CPUPeriod *int64 `json:"cpu-period,omitempty"`
|
||||||
|
CPUQuota *int64 `json:"cpu-quota,omitempty"`
|
||||||
|
CPUSetCPUs *string `json:"cpuset-cpus,omitempty"`
|
||||||
|
CPUSetMems *string `json:"cpuset-mems,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResourcesConfig) Merge(other *ResourcesConfig) *ResourcesConfig {
|
||||||
|
if r == nil {
|
||||||
|
r = &ResourcesConfig{}
|
||||||
|
}
|
||||||
|
merged := *r
|
||||||
|
if other != nil {
|
||||||
|
if other.Memory != nil {
|
||||||
|
merged.Memory = other.Memory
|
||||||
|
}
|
||||||
|
if other.MemorySwap != nil {
|
||||||
|
merged.MemorySwap = other.MemorySwap
|
||||||
|
}
|
||||||
|
if other.CPUShares != nil {
|
||||||
|
merged.CPUShares = other.CPUShares
|
||||||
|
}
|
||||||
|
if other.CPUPeriod != nil {
|
||||||
|
merged.CPUPeriod = other.CPUPeriod
|
||||||
|
}
|
||||||
|
if other.CPUQuota != nil {
|
||||||
|
merged.CPUQuota = other.CPUQuota
|
||||||
|
}
|
||||||
|
if other.CPUSetCPUs != nil {
|
||||||
|
merged.CPUSetCPUs = other.CPUSetCPUs
|
||||||
|
}
|
||||||
|
if other.CPUSetMems != nil {
|
||||||
|
merged.CPUSetMems = other.CPUSetMems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToEntries renders the config as `key=value` entries for build.ParseResourceLimits.
|
||||||
|
func (r *ResourcesConfig) ToEntries() []string {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var entries []string
|
||||||
|
if r.Memory != nil {
|
||||||
|
entries = append(entries, "memory="+*r.Memory)
|
||||||
|
}
|
||||||
|
if r.MemorySwap != nil {
|
||||||
|
entries = append(entries, "memory-swap="+*r.MemorySwap)
|
||||||
|
}
|
||||||
|
if r.CPUShares != nil {
|
||||||
|
entries = append(entries, "cpu-shares="+strconv.FormatInt(*r.CPUShares, 10))
|
||||||
|
}
|
||||||
|
if r.CPUPeriod != nil {
|
||||||
|
entries = append(entries, "cpu-period="+strconv.FormatInt(*r.CPUPeriod, 10))
|
||||||
|
}
|
||||||
|
if r.CPUQuota != nil {
|
||||||
|
entries = append(entries, "cpu-quota="+strconv.FormatInt(*r.CPUQuota, 10))
|
||||||
|
}
|
||||||
|
if r.CPUSetCPUs != nil {
|
||||||
|
entries = append(entries, "cpuset-cpus="+*r.CPUSetCPUs)
|
||||||
|
}
|
||||||
|
if r.CPUSetMems != nil {
|
||||||
|
entries = append(entries, "cpuset-mems="+*r.CPUSetMems)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetField sets a single resource field by name, used by bake `--set` overrides.
|
||||||
|
func (r *ResourcesConfig) SetField(name, value string) error {
|
||||||
|
switch name {
|
||||||
|
case "memory":
|
||||||
|
r.Memory = &value
|
||||||
|
case "memory-swap":
|
||||||
|
r.MemorySwap = &value
|
||||||
|
case "cpu-shares", "cpu-period", "cpu-quota":
|
||||||
|
n, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("invalid value %s for int64 key resources.%s", value, name)
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "cpu-shares":
|
||||||
|
r.CPUShares = &n
|
||||||
|
case "cpu-period":
|
||||||
|
r.CPUPeriod = &n
|
||||||
|
case "cpu-quota":
|
||||||
|
r.CPUQuota = &n
|
||||||
|
}
|
||||||
|
case "cpuset-cpus":
|
||||||
|
r.CPUSetCPUs = &value
|
||||||
|
case "cpuset-mems":
|
||||||
|
r.CPUSetMems = &value
|
||||||
|
default:
|
||||||
|
return errors.Errorf("unknown resources key %s", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package buildflags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
"github.com/zclconf/go-cty/cty/convert"
|
||||||
|
"github.com/zclconf/go-cty/cty/gocty"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resourcesType = sync.OnceValue(func() cty.Type {
|
||||||
|
return cty.ObjectWithOptionalAttrs(
|
||||||
|
map[string]cty.Type{
|
||||||
|
"memory": cty.String,
|
||||||
|
"memory-swap": cty.String,
|
||||||
|
"cpu-shares": cty.Number,
|
||||||
|
"cpu-period": cty.Number,
|
||||||
|
"cpu-quota": cty.Number,
|
||||||
|
"cpuset-cpus": cty.String,
|
||||||
|
"cpuset-mems": cty.String,
|
||||||
|
},
|
||||||
|
[]string{"memory", "memory-swap", "cpu-shares", "cpu-period", "cpu-quota", "cpuset-cpus", "cpuset-mems"},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
func (r *ResourcesConfig) FromCtyValue(in cty.Value, p cty.Path) error {
|
||||||
|
conv, err := convert.Convert(in, resourcesType())
|
||||||
|
if err != nil {
|
||||||
|
return p.NewError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := conv.GetAttr("memory"); !v.IsNull() && v.IsKnown() {
|
||||||
|
s := v.AsString()
|
||||||
|
r.Memory = &s
|
||||||
|
}
|
||||||
|
if v := conv.GetAttr("memory-swap"); !v.IsNull() && v.IsKnown() {
|
||||||
|
s := v.AsString()
|
||||||
|
r.MemorySwap = &s
|
||||||
|
}
|
||||||
|
if v := conv.GetAttr("cpu-shares"); !v.IsNull() && v.IsKnown() {
|
||||||
|
var n int64
|
||||||
|
if err := gocty.FromCtyValue(v, &n); err != nil {
|
||||||
|
return p.NewError(err)
|
||||||
|
}
|
||||||
|
r.CPUShares = &n
|
||||||
|
}
|
||||||
|
if v := conv.GetAttr("cpu-period"); !v.IsNull() && v.IsKnown() {
|
||||||
|
var n int64
|
||||||
|
if err := gocty.FromCtyValue(v, &n); err != nil {
|
||||||
|
return p.NewError(err)
|
||||||
|
}
|
||||||
|
r.CPUPeriod = &n
|
||||||
|
}
|
||||||
|
if v := conv.GetAttr("cpu-quota"); !v.IsNull() && v.IsKnown() {
|
||||||
|
var n int64
|
||||||
|
if err := gocty.FromCtyValue(v, &n); err != nil {
|
||||||
|
return p.NewError(err)
|
||||||
|
}
|
||||||
|
r.CPUQuota = &n
|
||||||
|
}
|
||||||
|
if v := conv.GetAttr("cpuset-cpus"); !v.IsNull() && v.IsKnown() {
|
||||||
|
s := v.AsString()
|
||||||
|
r.CPUSetCPUs = &s
|
||||||
|
}
|
||||||
|
if v := conv.GetAttr("cpuset-mems"); !v.IsNull() && v.IsKnown() {
|
||||||
|
s := v.AsString()
|
||||||
|
r.CPUSetMems = &s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResourcesConfig) ToCtyValue() cty.Value {
|
||||||
|
if r == nil {
|
||||||
|
return cty.NullVal(resourcesType())
|
||||||
|
}
|
||||||
|
|
||||||
|
vals := map[string]cty.Value{
|
||||||
|
"memory": cty.NullVal(cty.String),
|
||||||
|
"memory-swap": cty.NullVal(cty.String),
|
||||||
|
"cpu-shares": cty.NullVal(cty.Number),
|
||||||
|
"cpu-period": cty.NullVal(cty.Number),
|
||||||
|
"cpu-quota": cty.NullVal(cty.Number),
|
||||||
|
"cpuset-cpus": cty.NullVal(cty.String),
|
||||||
|
"cpuset-mems": cty.NullVal(cty.String),
|
||||||
|
}
|
||||||
|
if r.Memory != nil {
|
||||||
|
vals["memory"] = cty.StringVal(*r.Memory)
|
||||||
|
}
|
||||||
|
if r.MemorySwap != nil {
|
||||||
|
vals["memory-swap"] = cty.StringVal(*r.MemorySwap)
|
||||||
|
}
|
||||||
|
if r.CPUShares != nil {
|
||||||
|
vals["cpu-shares"] = cty.NumberIntVal(*r.CPUShares)
|
||||||
|
}
|
||||||
|
if r.CPUPeriod != nil {
|
||||||
|
vals["cpu-period"] = cty.NumberIntVal(*r.CPUPeriod)
|
||||||
|
}
|
||||||
|
if r.CPUQuota != nil {
|
||||||
|
vals["cpu-quota"] = cty.NumberIntVal(*r.CPUQuota)
|
||||||
|
}
|
||||||
|
if r.CPUSetCPUs != nil {
|
||||||
|
vals["cpuset-cpus"] = cty.StringVal(*r.CPUSetCPUs)
|
||||||
|
}
|
||||||
|
if r.CPUSetMems != nil {
|
||||||
|
vals["cpuset-mems"] = cty.StringVal(*r.CPUSetMems)
|
||||||
|
}
|
||||||
|
return cty.ObjectVal(vals)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user