@xemahq/repo-build-tooling 0.21.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/buildx-builder-host.mjs +61 -0
- package/src/buildx-builder-limits.mjs +4 -10
- package/src/buildx-builder.mjs +32 -10
- package/src/buildx-builder.test.mjs +84 -13
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ pnpm exec xema-scrub-swagger-paths dist
|
|
|
32
32
|
| `xema-composed-image <command>` | Build-time half of a composed-process image: `members`, `filters`, `dirs`, `pins`, `index`, `load`, `stage`, `pnpm-context`, `sync-injected`, `prisma-check`, `migrate`, `coordinate`, `load-image`, `smoke-refusal`, `dockerfile` |
|
|
33
33
|
| `xema-arm64-supplement-needed` | Decides whether an arm64 supplement is still owed at `REGISTRY/IMAGE:CONTENT_TAG` (env: `REGISTRY`, `IMAGE`, `CONTENT_TAG`, `REGISTRY_USER`, `REGISTRY_TOKEN`). `needed=false` only when the coordinate serves an index whose linux/arm64 child is proven by its own image config; any other resolvable state is `needed=true`; a 401/403/5xx or an unresolvable child is an error. Writes `needed=true\|false` to `$GITHUB_OUTPUT`. Replaces the generated `arm64-supplement-needed.mjs` copy. |
|
|
34
34
|
| `xema-promote-image --service <name> --dev-ref <repo> --release-ref <repo> --tag <tag> --commit <sha> --main-ref <ref> --signer-repository <owner/repo> --develop-workflow <file> --release-workflow <file>... --promotion-predicate-out <path> -- <tag command>` | Publishes the develop image's digest under the release tag instead of rebuilding: `reused` (the release digest carries an `image-promotion/v1` attestation signed by a `--release-workflow` identity on main, with exactly one verified statement about the digest and a `ghcr.io/<owner>/dev/<name>` source; no dev-ref probe), `copied` (the proven develop digest, digest-preserving; fails closed if the release digest differs) or `miss` (build it; a reuse whose release digest has no `image-promotion/v1` attestation, a legacy image, is a miss with `reason=reuse-no-promotion` on the stdout line; a signature or identity failure, or a source outside `dev/`, is an error; a copy whose dev digest carries no SLSA v1 provenance at all, an image reused on develop from before provenance, is a miss with `reason=copy-no-develop-provenance`, while provenance that is present but fails a leg is an error). A develop digest is proven only if its SLSA v1 provenance is signed by the develop workflow at `refs/heads/develop`, its revision is an ancestor of the main ref, and the tag command prints the same tag at that revision and at `--commit`; any failed leg is an error, never a miss. On every non-miss result it writes the `https://xema.dev/attestations/image-promotion/v1` predicate for the caller to attest: a copy states the facts just proved; a reuse states this run's `release` and `promotedCommit` and carries `source` and `developProvenance` from the earlier promotion. Writes `result`, `digest`, `source`, `revision`, `evidence`, `provenance_digest`, `promotion_predicate`, `promotion_predicate_type` to `$GITHUB_OUTPUT`. Needs `docker`, `cosign` (major v2, the same as the develop attest step and xema-deploy's verify; it runs `cosign version` first and refuses any other major or an unparsable version), `git` and full history. It does not sign |
|
|
35
|
-
| `xema-buildx-builder ensure` | Ensures the runner's persistent, capped buildkitd container (`xema-buildkit-signed-<runner>`, state volume `-state`) is up and is the current buildx builder. Env: `RUNNER_NAME`, `BUILDKIT_IMAGE`, `BUILDKIT_LIMITS` (JSON `cpus`, `memory`, `maxParallelism`, `gcMaxUsedSpace`, all required and positive), optional `BUILDKIT_READY_DEADLINE_SECONDS` (positive integer, default 180). Pinned to the runner's cgroup cpuset;
|
|
35
|
+
| `xema-buildx-builder ensure` | Ensures the runner's persistent, capped buildkitd container (`xema-buildkit-signed-<runner>`, state volume `-state`) is up and is the current buildx builder. Env: `RUNNER_NAME`, `BUILDKIT_IMAGE`, `BUILDKIT_LIMITS` (JSON `cpus`, `memory`, `maxParallelism`, `gcMaxUsedSpace`, all required and positive), optional `BUILDKIT_READY_DEADLINE_SECONDS` (positive integer, default 180). The slot is `RUNNER_NAME` with a trailing `-<hostname>` stripped only when it equals this container's own hostname, so a recreated runner container reuses its builder and cache. Pinned to the runner's cgroup cpuset; caps are clamped so every `xema-buildkit-signed-*` builder on the host fits (memory at most half of `docker info` MemTotal divided by N, CPUs at most NCPU divided by N). On each run it removes this verb's labelled builders, and their `-state` volumes, whose recorded runner container is no longer running; unlabelled containers are never touched. Recreated once when the image, effective caps, config or runner container change. Prints `buildx-boot path= secs= builder=`. |
|
|
36
36
|
| `xema-check-no-vendored-deps` | Fails on a `workspace:` range for a package the repository does not own |
|
|
37
37
|
| `xema-prepush-sanity` | Lefthook `pre-push`: turbo `lint` and `typecheck` on what the push changes plus dependents (root `lint` only without turbo; typecheck is CI-only there); CI is the proof |
|
|
38
38
|
| `xema-check-workspace-ranges` | Fails on a `workspace:` range the local package does not satisfy |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/repo-build-tooling",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM, depending only on @xemahq/release-policy for registry access, so the published artifact is the reviewed source.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
|
|
@@ -52,6 +52,6 @@
|
|
|
52
52
|
"test": "node --test src/buildx-builder.test.mjs && node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs && node --test src/readiness.test.mjs && node --test src/composed-image.test.mjs && node --test src/composed-image-coordinate-cli.test.mjs && node --test src/declaration-refusal.test.mjs && node --test src/smoke-refusal.test.mjs && node --test src/load-image.test.mjs && node --test src/composed-image-coordinate.test.mjs && node --test src/composed-image-dockerfile.test.mjs && node --test src/service-bootstrap.test.mjs && node --test src/service-bootstrap-protocols.test.mjs && node --test src/composed-image-prebuilt-dockerfile.test.mjs && node --test src/composed-image-in-image-closure.test.mjs && node --test src/composed-image-stage.test.mjs && node --test src/composed-image-pnpm-context.test.mjs && node --test src/composed-image-sync-injected.test.mjs && node --test src/composed-image-prisma-check.test.mjs && node --test src/composed-migrate.test.mjs && node --test src/promote-image.test.mjs && node --test src/arm64-supplement-needed.test.mjs && node --test src/prepush-sanity.test.mjs"
|
|
53
53
|
},
|
|
54
54
|
"xemaRelease": {
|
|
55
|
-
"sourceDigest": "sha256:
|
|
55
|
+
"sourceDigest": "sha256:7c602b30aa27cb65d2c46fbe77f0c93da23bde8525e571943fb8ed5ea48dd924"
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Pure host-level decisions of xema-buildx-builder: the stable slot name, the host clamp on the caps and the
|
|
2
|
+
// orphan selection. No docker and no I/O here; the verb feeds in what `docker` reported.
|
|
3
|
+
|
|
4
|
+
export const BUILDER_PREFIX = 'xema-buildkit-signed-';
|
|
5
|
+
export const OWNER_LABEL = 'xema.buildkit.owner';
|
|
6
|
+
export const OWNER_VALUE = 'xema-buildx-builder';
|
|
7
|
+
export const HOST_LABEL = 'xema.buildkit.runner-host';
|
|
8
|
+
|
|
9
|
+
const UNIT = { '': 1, b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3 };
|
|
10
|
+
const MEBIBYTE = 1024 ** 2;
|
|
11
|
+
const MEMORY_SHARE = 0.5;
|
|
12
|
+
const CONTAINER_ID = /^[0-9a-f]{12}$/;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The runner's slot: RUNNER_NAME with a trailing `-<hostname>` stripped, only when that suffix is exactly this
|
|
16
|
+
* container's own hostname (the runner container id GitHub appends to the name). A recreated runner container
|
|
17
|
+
* then keeps its builder and cache. Anything else keeps RUNNER_NAME unchanged: never a guess.
|
|
18
|
+
*/
|
|
19
|
+
export function runnerSlot(runnerName, hostname) {
|
|
20
|
+
if (runnerName === undefined || runnerName === '') throw new Error('RUNNER_NAME is empty or unset: the builder key is derived from it');
|
|
21
|
+
const suffix = `-${hostname}`;
|
|
22
|
+
const stripped = hostname !== '' && runnerName.endsWith(suffix) && runnerName.length > suffix.length ? runnerName.slice(0, -suffix.length) : runnerName;
|
|
23
|
+
return stripped.toLowerCase().replace(/[^a-z0-9_.-]/g, '-');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function builderName(slot) {
|
|
27
|
+
return `${BUILDER_PREFIX}${slot}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A validated size such as `8g` in bytes. */
|
|
31
|
+
export function memoryBytes(size) {
|
|
32
|
+
const match = /^([0-9]+)([bkmgBKMG]?)$/.exec(size);
|
|
33
|
+
return Number(match[1]) * UNIT[match[2].toLowerCase()];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The caps actually applied: the configured limit, clamped so N builders together stay inside the host
|
|
38
|
+
* (half of its memory, all of its CPUs). N counts every signed builder on the host, this one included.
|
|
39
|
+
* Memory is whole MiB (docker's `m` unit); CPUs are rounded down to 2 decimals.
|
|
40
|
+
*/
|
|
41
|
+
export function effectiveCaps(limits, host, builders) {
|
|
42
|
+
if (!Number.isInteger(builders) || builders < 1) throw new Error(`builder count must be a positive integer, got ${builders}`);
|
|
43
|
+
const memoryMiB = Math.floor(Math.min(memoryBytes(limits.memory), (host.memTotal * MEMORY_SHARE) / builders) / MEBIBYTE);
|
|
44
|
+
const cpus = Math.floor(Math.min(Number(limits.cpus), host.ncpu / builders) * 100) / 100;
|
|
45
|
+
if (memoryMiB < 1 || cpus <= 0) throw new Error(`host (${host.memTotal} bytes, ${host.ncpu} cpus) is too small for ${builders} signed builders`);
|
|
46
|
+
return { cpus: String(cpus), memory: `${memoryMiB}m` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builders that carry this verb's labels and whose recorded runner container is no longer running.
|
|
51
|
+
* `known` rows: { name, owner, host }; `runningIds`: ids of the running containers on the host.
|
|
52
|
+
* A recorded host that is not a container id (a host-network runner records the machine's hostname) cannot
|
|
53
|
+
* be judged and is never an orphan. Unlabelled containers are never touched; `keep` is the caller's own.
|
|
54
|
+
*/
|
|
55
|
+
export function orphanBuilders(known, runningIds, keep) {
|
|
56
|
+
return known.filter((row) =>
|
|
57
|
+
row.name !== keep &&
|
|
58
|
+
row.owner === OWNER_VALUE &&
|
|
59
|
+
CONTAINER_ID.test(row.host) &&
|
|
60
|
+
!runningIds.some((id) => id.startsWith(row.host) || row.host.startsWith(id)));
|
|
61
|
+
}
|
|
@@ -33,14 +33,8 @@ export function buildkitdToml(limits) {
|
|
|
33
33
|
return `[worker.oci]\n gc = true\n maxUsedSpace = "${limits.gcMaxUsedSpace}"\n max-parallelism = ${limits.maxParallelism}\n`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** The label value: a change of image, caps, cpuset or config recreates the container exactly once. */
|
|
37
|
-
export function configHash(image,
|
|
38
|
-
const
|
|
39
|
-
return sha256(`${image}\n${
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The builder key: `xema-buildkit-signed-*` is reserved for signed lanes (T133), so a PR build can never share its cache mounts. The lowercased runner name with anything outside [a-z0-9_.-] mapped to '-'. */
|
|
43
|
-
export function builderName(runnerName) {
|
|
44
|
-
if (runnerName === undefined || runnerName === '') throw new Error('RUNNER_NAME is empty or unset: the builder key is derived from it');
|
|
45
|
-
return `xema-buildkit-signed-${runnerName.toLowerCase().replace(/[^a-z0-9_.-]/g, '-')}`;
|
|
36
|
+
/** The label value: `caps` are the EFFECTIVE caps (after the host clamp); a change of image, caps, cpuset or config recreates the container exactly once. */
|
|
37
|
+
export function configHash(image, caps, cpuset, toml, sha256) {
|
|
38
|
+
const capText = `cpus=${caps.cpus} memory=${caps.memory} memory-swap=${caps.memory} cpuset=${cpuset}`;
|
|
39
|
+
return sha256(`${image}\n${capText}\n${toml}`);
|
|
46
40
|
}
|
package/src/buildx-builder.mjs
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
15
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
-
import { tmpdir } from 'node:os';
|
|
16
|
+
import { hostname as osHostname, tmpdir } from 'node:os';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
|
|
19
19
|
import { invokedDirectly } from './readiness.mjs';
|
|
20
|
-
import {
|
|
20
|
+
import { BUILDER_PREFIX, HOST_LABEL, OWNER_LABEL, OWNER_VALUE, builderName, effectiveCaps, orphanBuilders, runnerSlot } from './buildx-builder-host.mjs';
|
|
21
|
+
import { buildkitdToml, configHash, parseLimits } from './buildx-builder-limits.mjs';
|
|
21
22
|
|
|
22
23
|
const ADMITTED_EVENTS = ['push', 'workflow_dispatch'];
|
|
23
24
|
const ADMITTED_REFS = ['refs/heads/main', 'refs/heads/develop'];
|
|
@@ -57,7 +58,8 @@ export async function ensure(env, log = console.log) {
|
|
|
57
58
|
const image = env.BUILDKIT_IMAGE;
|
|
58
59
|
if (image === undefined || image === '') throw new Error('BUILDKIT_IMAGE is required');
|
|
59
60
|
const limits = parseLimits(env.BUILDKIT_LIMITS);
|
|
60
|
-
const
|
|
61
|
+
const hostname = osHostname();
|
|
62
|
+
const key = builderName(runnerSlot(env.RUNNER_NAME, hostname));
|
|
61
63
|
const cpusetFile = env.BUILDKIT_CPUSET_FILE ?? DEFAULT_CPUSET_FILE;
|
|
62
64
|
let cpuset = '';
|
|
63
65
|
try {
|
|
@@ -68,31 +70,51 @@ export async function ensure(env, log = console.log) {
|
|
|
68
70
|
if (cpuset === '') throw new Error(`${cpusetFile} is missing or empty: cannot bound the builder to the runner's cpuset (cgroup v2 required)`);
|
|
69
71
|
const deadlineSeconds = parseDeadlineSeconds(env.BUILDKIT_READY_DEADLINE_SECONDS);
|
|
70
72
|
const toml = buildkitdToml(limits);
|
|
71
|
-
const cfg = configHash(image, limits, cpuset, toml, sha256);
|
|
72
73
|
|
|
73
74
|
const scratch = mkdtempSync(join(tmpdir(), 'xema-buildkitd-'));
|
|
74
75
|
try {
|
|
75
76
|
const configFile = join(scratch, 'buildkitd.toml');
|
|
76
77
|
writeFileSync(configFile, toml);
|
|
77
78
|
const start = Date.now();
|
|
79
|
+
|
|
80
|
+
// Orphans first: a builder whose runner container is gone keeps its caps on the host for ever otherwise.
|
|
81
|
+
const listed = () => docker(['ps', '-a', '--filter', `name=^/${BUILDER_PREFIX}`, '--format', `{{.Names}}|{{.Label "${OWNER_LABEL}"}}|{{.Label "${HOST_LABEL}"}}`]).stdout
|
|
82
|
+
.split('\n').filter((l) => l !== '').map((l) => { const [name, owner, host] = l.split('|'); return { name, owner, host }; });
|
|
83
|
+
const runningIds = docker(['ps', '--format', '{{.ID}}']).stdout.split('\n').filter((l) => l !== '');
|
|
84
|
+
for (const orphan of orphanBuilders(listed(), runningIds, key)) {
|
|
85
|
+
log(`::notice::removing orphaned builder ${orphan.name} (runner container ${orphan.host} is gone) and its state volume`);
|
|
86
|
+
docker(['rm', '-f', orphan.name], { allowFailure: true });
|
|
87
|
+
docker(['volume', 'rm', `${orphan.name}-state`], { allowFailure: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Clamp to the host: N builders share it, this one included.
|
|
91
|
+
const info = docker(['info', '--format', '{{.MemTotal}} {{.NCPU}}']).stdout.split(' ').map(Number);
|
|
92
|
+
if (info.length !== 2 || !info.every((n) => Number.isInteger(n) && n > 0)) throw new Error(`docker info did not report MemTotal and NCPU: '${info.join(' ')}'`);
|
|
93
|
+
const present = listed();
|
|
94
|
+
const builders = present.some((row) => row.name === key) ? present.length : present.length + 1;
|
|
95
|
+
const caps = effectiveCaps(limits, { memTotal: info[0], ncpu: info[1] }, builders);
|
|
96
|
+
const cfg = configHash(image, caps, cpuset, toml, sha256);
|
|
97
|
+
|
|
78
98
|
const create = () => {
|
|
79
99
|
docker(['create', '--name', key, '--restart', 'unless-stopped', '--privileged',
|
|
80
|
-
'--cpus',
|
|
100
|
+
'--cpus', caps.cpus, '--memory', caps.memory, '--memory-swap', caps.memory, '--cpuset-cpus', cpuset,
|
|
81
101
|
'-v', `${key}-state:/var/lib/buildkit`, '--label', `${LABEL}=${cfg}`,
|
|
102
|
+
'--label', `${OWNER_LABEL}=${OWNER_VALUE}`, '--label', `${HOST_LABEL}=${hostname}`,
|
|
82
103
|
image, '--config', '/etc/buildkitd.toml']);
|
|
83
104
|
docker(['cp', configFile, `${key}:/etc/buildkitd.toml`]);
|
|
84
105
|
docker(['start', key]);
|
|
85
106
|
};
|
|
86
107
|
|
|
87
108
|
let path;
|
|
88
|
-
if (
|
|
109
|
+
if (!present.some((row) => row.name === key)) {
|
|
89
110
|
path = 'created';
|
|
90
111
|
create();
|
|
91
112
|
} else {
|
|
92
113
|
const have = docker(['inspect', '-f', `{{ index .Config.Labels "${LABEL}" }}`, key]).stdout;
|
|
93
114
|
const haveImage = docker(['inspect', '-f', '{{.Config.Image}}', key]).stdout;
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
const haveHost = docker(['inspect', '-f', `{{ index .Config.Labels "${HOST_LABEL}" }}`, key]).stdout;
|
|
116
|
+
if (have !== cfg || haveImage !== image || haveHost !== hostname) {
|
|
117
|
+
log(`::notice::buildkitd ${key} config, image or runner container changed (${have || 'none'} ${haveImage} ${haveHost} -> ${cfg} ${image} ${hostname}): recreating once, state volume kept`);
|
|
96
118
|
docker(['rm', '-f', key]);
|
|
97
119
|
path = 'recreated';
|
|
98
120
|
create();
|
|
@@ -103,7 +125,7 @@ export async function ensure(env, log = console.log) {
|
|
|
103
125
|
docker(['start', key]);
|
|
104
126
|
}
|
|
105
127
|
}
|
|
106
|
-
log(`buildx-boot ${key}: ${path}; caps cpus=${
|
|
128
|
+
log(`buildx-boot ${key}: ${path}; caps cpus=${caps.cpus} memory=${caps.memory} (limit ${limits.cpus}/${limits.memory}, ${builders} builders on the host) cpuset=${cpuset} max-parallelism=${limits.maxParallelism}; waiting for workers (deadline ${deadlineSeconds}s)`);
|
|
107
129
|
|
|
108
130
|
let check = 0;
|
|
109
131
|
for (;;) {
|
|
@@ -131,7 +153,7 @@ export async function ensure(env, log = console.log) {
|
|
|
131
153
|
docker(['buildx', 'inspect', '--bootstrap', key]);
|
|
132
154
|
|
|
133
155
|
const secs = Math.floor((Date.now() - start) / 1000);
|
|
134
|
-
const line = `buildx-boot path=${path} secs=${secs} builder=${key}`;
|
|
156
|
+
const line = `buildx-boot path=${path} secs=${secs} builder=${key} cpus=${caps.cpus} memory=${caps.memory}`;
|
|
135
157
|
log(line);
|
|
136
158
|
return { path, secs, builder: key, line };
|
|
137
159
|
} finally {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
4
|
+
import { hostname, tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import test from 'node:test';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
9
|
const BIN = fileURLToPath(new URL('./buildx-builder.mjs', import.meta.url));
|
|
10
|
+
const HOST = hostname();
|
|
11
|
+
const OWN = 'xema-buildkit-signed-owl-runner-1';
|
|
12
|
+
const ownRow = (host = HOST) => `${OWN}|xema-buildx-builder|${host}`;
|
|
10
13
|
const LIMITS = '{"cpus":"4","memory":"8g","maxParallelism":4,"gcMaxUsedSpace":"5GB"}';
|
|
11
14
|
|
|
12
15
|
// A fake docker on PATH: appends every call to calls.log; `ps` answers $FAKE_PS, `inspect -f ...Labels...` answers $FAKE_LABEL,
|
|
@@ -18,8 +21,9 @@ function sandbox() {
|
|
|
18
21
|
writeFileSync(fake, `#!/bin/sh
|
|
19
22
|
echo "$@" >> "${log}"
|
|
20
23
|
case "$1" in
|
|
21
|
-
ps) printf '%s' "$
|
|
22
|
-
|
|
24
|
+
ps) case "$*" in *'{{.ID}}'*) printf '%s' "$FAKE_RUNNING_IDS" ;; *) printf '%s' "$FAKE_LIST" ;; esac ;;
|
|
25
|
+
info) printf '%s' "\${FAKE_INFO:-68719476736 10}" ;;
|
|
26
|
+
inspect) case "$*" in *runner-host*) printf '%s' "$FAKE_HOST" ;; *Labels*) printf '%s' "$FAKE_LABEL" ;; *Config.Image*) printf '%s' "\${FAKE_IMAGE:-moby/buildkit:v0.1}" ;; *Running*) printf '%s' "$FAKE_RUNNING" ;; esac ;;
|
|
23
27
|
exec) if [ "\${FAKE_EXEC_FAIL:-0}" = 1 ]; then echo "no workers" >&2; exit 1; fi ;;
|
|
24
28
|
buildx) [ "$2" = inspect ] && [ "$3" != --bootstrap ] && exit 1; exit 0 ;;
|
|
25
29
|
esac
|
|
@@ -31,7 +35,7 @@ exit 0
|
|
|
31
35
|
const run = (extra = {}) => {
|
|
32
36
|
const env = {
|
|
33
37
|
PATH: `${dir}:${process.env.PATH}`, RUNNER_NAME: 'Owl Runner/1', BUILDKIT_IMAGE: 'moby/buildkit:v0.1',
|
|
34
|
-
BUILDKIT_LIMITS: LIMITS, BUILDKIT_CPUSET_FILE: cpuset, GITHUB_EVENT_NAME: 'push', GITHUB_REF: 'refs/heads/develop', ...extra,
|
|
38
|
+
BUILDKIT_LIMITS: LIMITS, FAKE_HOST: HOST, BUILDKIT_CPUSET_FILE: cpuset, GITHUB_EVENT_NAME: 'push', GITHUB_REF: 'refs/heads/develop', ...extra,
|
|
35
39
|
};
|
|
36
40
|
for (const [k, v] of Object.entries(env)) if (v === undefined) delete env[k];
|
|
37
41
|
const r = spawnSync(process.execPath, [BIN, 'ensure'], { env, encoding: 'utf8' });
|
|
@@ -97,32 +101,32 @@ test('missing or invalid limits, runner name, image and cpuset fail before any d
|
|
|
97
101
|
test('first run creates the capped container pinned to the runner cpuset, then prints the buildx-boot line', () => {
|
|
98
102
|
const s = sandbox();
|
|
99
103
|
try {
|
|
100
|
-
const r = s.run({
|
|
104
|
+
const r = s.run({});
|
|
101
105
|
assert.equal(r.status, 0, r.stderr);
|
|
102
106
|
const create = r.calls.split('\n').find((l) => l.startsWith('create '));
|
|
103
107
|
assert.match(create, /--name xema-buildkit-signed-owl-runner-1 /);
|
|
104
|
-
assert.match(create, /--cpus 4 --memory
|
|
108
|
+
assert.match(create, /--cpus 4 --memory 8192m --memory-swap 8192m --cpuset-cpus 10-27/);
|
|
105
109
|
assert.match(create, /--privileged/);
|
|
106
110
|
assert.match(create, /-v xema-buildkit-signed-owl-runner-1-state:\/var\/lib\/buildkit/);
|
|
107
111
|
assert.match(r.calls, /cp .*buildkitd\.toml xema-buildkit-signed-owl-runner-1:\/etc\/buildkitd\.toml/);
|
|
108
112
|
assert.match(r.calls, /buildx create --name xema-buildkit-signed-owl-runner-1 --driver remote/);
|
|
109
113
|
assert.match(r.calls, /buildx use xema-buildkit-signed-owl-runner-1/);
|
|
110
|
-
assert.match(r.stdout, /^buildx-boot path=created secs=\d+ builder=xema-buildkit-signed-owl-runner-1$/m);
|
|
114
|
+
assert.match(r.stdout, /^buildx-boot path=created secs=\d+ builder=xema-buildkit-signed-owl-runner-1 cpus=4 memory=8192m$/m);
|
|
111
115
|
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
112
116
|
});
|
|
113
117
|
|
|
114
118
|
test('a changed hash recreates once, keeping the state volume; the same hash reuses', () => {
|
|
115
119
|
const s = sandbox();
|
|
116
120
|
try {
|
|
117
|
-
const first = s.run({
|
|
121
|
+
const first = s.run({});
|
|
118
122
|
const label = /--label xema\.buildkit\.cfg=([0-9a-f]{64})/.exec(first.calls)[1];
|
|
119
123
|
rmSync(join(s.dir, 'calls.log'));
|
|
120
|
-
const recreated = s.run({
|
|
124
|
+
const recreated = s.run({ FAKE_LIST: ownRow(), FAKE_LABEL: 'stale' });
|
|
121
125
|
assert.match(recreated.calls, /rm -f xema-buildkit-signed-owl-runner-1/);
|
|
122
126
|
assert.doesNotMatch(recreated.calls, /volume rm/);
|
|
123
127
|
assert.match(recreated.stdout, /path=recreated/);
|
|
124
128
|
rmSync(join(s.dir, 'calls.log'));
|
|
125
|
-
const reused = s.run({
|
|
129
|
+
const reused = s.run({ FAKE_LIST: ownRow(), FAKE_LABEL: label, FAKE_RUNNING: 'true' });
|
|
126
130
|
assert.doesNotMatch(reused.calls, /^(create|rm) /m);
|
|
127
131
|
assert.match(reused.stdout, /path=running/);
|
|
128
132
|
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
@@ -131,19 +135,86 @@ test('a changed hash recreates once, keeping the state volume; the same hash reu
|
|
|
131
135
|
test('a matching label but a different running image recreates once', () => {
|
|
132
136
|
const s = sandbox();
|
|
133
137
|
try {
|
|
134
|
-
const first = s.run({
|
|
138
|
+
const first = s.run({});
|
|
135
139
|
const label = /--label xema\.buildkit\.cfg=([0-9a-f]{64})/.exec(first.calls)[1];
|
|
136
140
|
rmSync(join(s.dir, 'calls.log'));
|
|
137
|
-
const r = s.run({
|
|
141
|
+
const r = s.run({ FAKE_LIST: ownRow(), FAKE_LABEL: label, FAKE_RUNNING: 'true', FAKE_IMAGE: 'moby/buildkit:old' });
|
|
138
142
|
assert.match(r.calls, /rm -f xema-buildkit-signed-owl-runner-1/);
|
|
139
143
|
assert.match(r.stdout, /path=recreated/);
|
|
140
144
|
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
141
145
|
});
|
|
142
146
|
|
|
147
|
+
test('a recreated runner container (new hostname) recreates its builder once, keeping the state volume', () => {
|
|
148
|
+
const s = sandbox();
|
|
149
|
+
try {
|
|
150
|
+
const first = s.run({});
|
|
151
|
+
const label = /--label xema\.buildkit\.cfg=([0-9a-f]{64})/.exec(first.calls)[1];
|
|
152
|
+
rmSync(join(s.dir, 'calls.log'));
|
|
153
|
+
const r = s.run({ FAKE_LIST: ownRow('0123456789ab'), FAKE_HOST: '0123456789ab', FAKE_LABEL: label, FAKE_RUNNING: 'true' });
|
|
154
|
+
assert.match(r.stdout, /path=recreated/);
|
|
155
|
+
assert.doesNotMatch(r.calls, /volume rm/);
|
|
156
|
+
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('the slot strips a trailing -<hostname> only when it is exactly this container\'s hostname', () => {
|
|
160
|
+
const s = sandbox();
|
|
161
|
+
try {
|
|
162
|
+
const same = s.run({ RUNNER_NAME: `xema-fox1-${HOST}` });
|
|
163
|
+
assert.match(same.stdout, /builder=xema-buildkit-signed-xema-fox1 /);
|
|
164
|
+
assert.match(same.calls, /-v xema-buildkit-signed-xema-fox1-state:/);
|
|
165
|
+
const other = s.run({ RUNNER_NAME: 'xema-fox1-2f7dad65784b' });
|
|
166
|
+
assert.match(other.stdout, /builder=xema-buildkit-signed-xema-fox1-2f7dad65784b /);
|
|
167
|
+
const whole = s.run({ RUNNER_NAME: HOST });
|
|
168
|
+
assert.match(whole.stdout, new RegExp(`builder=xema-buildkit-signed-${HOST.toLowerCase().replace(/[^a-z0-9_.-]/g, '-')} `));
|
|
169
|
+
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('orphan GC removes only labelled builders whose runner container is gone, with their state volume', () => {
|
|
173
|
+
const s = sandbox();
|
|
174
|
+
try {
|
|
175
|
+
const list = [
|
|
176
|
+
ownRow(),
|
|
177
|
+
'xema-buildkit-signed-dead|xema-buildx-builder|aaaaaaaaaaaa',
|
|
178
|
+
'xema-buildkit-signed-alive|xema-buildx-builder|bbbbbbbbbbbb',
|
|
179
|
+
'xema-buildkit-signed-hostnet|xema-buildx-builder|fox-host',
|
|
180
|
+
'xema-buildkit-signed-foreign||cccccccccccc',
|
|
181
|
+
].join('\n');
|
|
182
|
+
const r = s.run({ FAKE_LIST: list, FAKE_RUNNING_IDS: 'bbbbbbbbbbbb\ndddddddddddd', FAKE_HOST: HOST });
|
|
183
|
+
assert.match(r.calls, /^rm -f xema-buildkit-signed-dead$/m);
|
|
184
|
+
assert.match(r.calls, /^volume rm xema-buildkit-signed-dead-state$/m);
|
|
185
|
+
assert.doesNotMatch(r.calls, /rm -f xema-buildkit-signed-(alive|hostnet|foreign)/);
|
|
186
|
+
assert.doesNotMatch(r.stdout, /removing orphaned builder xema-buildkit-signed-owl-runner-1/);
|
|
187
|
+
assert.doesNotMatch(r.calls, /volume rm xema-buildkit-signed-(alive|hostnet|foreign)/);
|
|
188
|
+
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('the caps are clamped so N signed builders fit the host, and the effective caps are printed', () => {
|
|
192
|
+
const s = sandbox();
|
|
193
|
+
try {
|
|
194
|
+
const list = [1, 2, 3].map((n) => `xema-buildkit-signed-other${n}|xema-buildx-builder|bbbbbbbbbbbb`).join('\n');
|
|
195
|
+
// 16 GiB, 8 cpus, 4 builders (3 others + this one): memory 16/2/4 = 2 GiB, cpus 8/4 = 2.
|
|
196
|
+
const r = s.run({ FAKE_LIST: list, FAKE_RUNNING_IDS: 'bbbbbbbbbbbb', FAKE_INFO: '17179869184 8' });
|
|
197
|
+
const create = r.calls.split('\n').find((l) => l.startsWith('create '));
|
|
198
|
+
assert.match(create, /--cpus 2 --memory 2048m --memory-swap 2048m/);
|
|
199
|
+
assert.match(r.stdout, /builder=xema-buildkit-signed-owl-runner-1 cpus=2 memory=2048m$/m);
|
|
200
|
+
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('changed effective caps recreate the builder once', () => {
|
|
204
|
+
const s = sandbox();
|
|
205
|
+
try {
|
|
206
|
+
const first = s.run({});
|
|
207
|
+
const label = /--label xema\.buildkit\.cfg=([0-9a-f]{64})/.exec(first.calls)[1];
|
|
208
|
+
rmSync(join(s.dir, 'calls.log'));
|
|
209
|
+
const r = s.run({ FAKE_LIST: ownRow(), FAKE_LABEL: label, FAKE_RUNNING: 'true', FAKE_INFO: '8589934592 8' });
|
|
210
|
+
assert.match(r.stdout, /path=recreated/);
|
|
211
|
+
} finally { rmSync(s.dir, { recursive: true, force: true }); }
|
|
212
|
+
});
|
|
213
|
+
|
|
143
214
|
test('a builder that never becomes ready fails at the deadline with the container logs', () => {
|
|
144
215
|
const s = sandbox();
|
|
145
216
|
try {
|
|
146
|
-
const r = s.run({
|
|
217
|
+
const r = s.run({ FAKE_EXEC_FAIL: '1', BUILDKIT_READY_DEADLINE_SECONDS: '1' });
|
|
147
218
|
assert.equal(r.status, 1);
|
|
148
219
|
assert.match(r.stderr, /not ready after 1s/);
|
|
149
220
|
assert.match(r.calls, /logs --tail 50/);
|