fix: isolate container builds with shared resource limits [CI SKIP]

This commit is contained in:
2026-11-02 17:31:03 +09:00
parent 9f67ee5719
commit db6d8ad8b4
8 changed files with 183 additions and 33 deletions
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
set -eu
# All clients use the same worker container on the Docker host. Limits are
# aggregate, including builds submitted simultaneously by API and CI clients.
docker_bin=${DOCKER_BIN:-docker}
builder=studio-limited
node=${builder}0
container=buildx_buildkit_${node}
directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8
"$docker_bin" buildx version >/dev/null
if ! "$docker_bin" buildx inspect "$builder" >/dev/null 2>&1; then
config_flag=--config
if "$docker_bin" buildx create --help | grep -q -- --buildkitd-config; then
config_flag=--buildkitd-config
fi
# Another caller may have registered the builder in this client config first.
"$docker_bin" buildx create --name "$builder" --node "$node" \
--driver docker-container \
--driver-opt "image=$image,memory=8g,memory-swap=8g,cpu-period=100000,cpu-quota=200000,network=host,restart-policy=unless-stopped" \
"$config_flag" "$directory/buildkitd.toml" >/dev/null \
|| "$docker_bin" buildx inspect "$builder" >/dev/null
fi
info=$("$docker_bin" buildx inspect "$builder")
driver=$(printf '%s\n' "$info" | awk '$1 == "Driver:" {print $2}')
[ "$driver" = docker-container ] || { echo 'Refusing unrestricted build driver' >&2; exit 1; }
names=$(printf '%s\n' "$info" | awk '$1 == "Name:" {print $2}')
[ "$names" = "$(printf '%s\n%s' "$builder" "$node")" ] || {
echo 'Refusing unexpected builder nodes' >&2; exit 1
}
"$docker_bin" buildx inspect "$builder" --bootstrap >/dev/null
limits=$("$docker_bin" inspect "$container" --format '{{.HostConfig.CpuPeriod}} {{.HostConfig.CpuQuota}} {{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}')
[ "$limits" = '100000 200000 8589934592 8589934592' ] || {
echo "Refusing build: unexpected worker limits: $limits" >&2
exit 1
}
echo '[limits] shared image builder: 2 CPUs, 8 GiB RAM, no swap'
if [ "${1:-}" = --prepare-only ]; then exit 0; fi
exec "$docker_bin" buildx build --builder "$builder" --load --progress plain "$@"
+64
View File
@@ -0,0 +1,64 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
function run(overrides = {}, args = ['-t', 'test:limited', '.']) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'build-limits-test-'));
try {
const fake = path.join(dir, 'docker');
fs.writeFileSync(fake, `#!${process.execPath}
const fs = require('node:fs');
const a = process.argv.slice(2);
fs.appendFileSync(process.env.CALLS, JSON.stringify(a) + '\\n');
if (a[0] === 'buildx' && a[1] === 'inspect') {
if (process.env.ABSENT && !fs.existsSync(process.env.CREATED)) process.exit(1);
console.log('Name: studio-limited\\nDriver: ' + (process.env.DRIVER || 'docker-container') + '\\nNodes:\\nName: ' + (process.env.NODE || 'studio-limited0'));
}
if (a[0] === 'buildx' && a[1] === 'create') {
if (a.includes('--help')) console.log('--buildkitd-config');
else fs.writeFileSync(process.env.CREATED, 'yes');
}
if (a[0] === 'inspect') console.log(process.env.LIMITS || '100000 200000 8589934592 8589934592');
`, { mode: 0o755 });
const result = spawnSync('/bin/sh', [path.join(__dirname, 'build-image.sh'), ...args], {
encoding: 'utf8',
env: { ...process.env, DOCKER_BIN: fake, CALLS: path.join(dir, 'calls'), CREATED: path.join(dir, 'created'), ...overrides },
});
const calls = fs.readFileSync(path.join(dir, 'calls'), 'utf8').trim().split('\n').map(JSON.parse);
return { ...result, calls };
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
test('uses the shared worker and loads the result for existing docker push', () => {
const result = run();
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(result.calls.at(-1), ['buildx', 'build', '--builder', 'studio-limited', '--load', '--progress', 'plain', '-t', 'test:limited', '.']);
});
test('creates a bounded worker without changing the default builder', () => {
const result = run({ ABSENT: '1' });
assert.equal(result.status, 0, result.stderr);
const create = result.calls.find((a) => a[1] === 'create' && !a.includes('--help'));
const options = create[create.indexOf('--driver-opt') + 1];
for (const value of ['memory=8g', 'memory-swap=8g', 'cpu-period=100000', 'cpu-quota=200000']) assert.ok(options.includes(value));
assert.ok(!create.includes('--use'));
});
for (const overrides of [{ LIMITS: '0 0 0 0' }, { DRIVER: 'docker' }, { NODE: 'other-node' }]) {
test(`refuses an unbounded or unexpected worker: ${JSON.stringify(overrides)}`, () => {
const result = run(overrides);
assert.notEqual(result.status, 0);
assert.ok(!result.calls.some((a) => a[0] === 'buildx' && a[1] === 'build'));
});
}
test('prepare-only never builds an application', () => {
const result = run({}, ['--prepare-only']);
assert.equal(result.status, 0, result.stderr);
assert.ok(!result.calls.some((a) => a[0] === 'buildx' && a[1] === 'build'));
});
+5
View File
@@ -0,0 +1,5 @@
[worker.oci]
max-parallelism = 2
[registry."localhost:5000"]
http = true