@vzn/vx-reapi 0.0.638 → 0.0.640
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 +8 -6
- package/package.json +2 -2
- package/src/cache.ts +11 -1
- package/src/executor.ts +1 -1
- package/src/index.ts +30 -1
- package/src/wire.ts +19 -7
package/README.md
CHANGED
|
@@ -39,7 +39,9 @@ one) the plugin **declines** and costs nothing, so it is
|
|
|
39
39
|
safe to leave declared. An endpoint that is not `host[:port]`, with an
|
|
40
40
|
optional `grpc(s)://` or `http(s)://` scheme, or a gRPC resolver target
|
|
41
41
|
(`unix:`, `unix-abstract:`, `dns:`, `ipv4:`, `ipv6:`), is refused at startup
|
|
42
|
-
with a line naming the setting.
|
|
42
|
+
with a line naming the setting. So is a TLS endpoint at an IP address:
|
|
43
|
+
Bun's TLS refuses an IP as the server name, so name the server by host
|
|
44
|
+
name, or use `grpc://` for plaintext. `VX_REAPI_ENDPOINT` / `VX_REAPI_INSTANCE` configure it
|
|
43
45
|
from the environment, and `VX_REAPI_EXECUTE=1` turns on remote execution the
|
|
44
46
|
way `execute: true` does (off by default: a plugin must not move where a
|
|
45
47
|
build runs merely by being configured for caching). `execute` is a
|
|
@@ -199,7 +201,7 @@ to `.git/hooks` and a declared directory under it wrote a hook.
|
|
|
199
201
|
|
|
200
202
|
## The existence probe confirms the artifact, not just the entry
|
|
201
203
|
|
|
202
|
-
`has()` — what `vx run --dry`
|
|
204
|
+
`has()` — what `vx run --dry` uses to predict hit vs miss —
|
|
203
205
|
reads the ActionCache entry AND checks the artifact blob is still in the
|
|
204
206
|
CAS. The second call is not redundant, because servers disagree: measured
|
|
205
207
|
against both, bazel-remote validates an ActionResult's referenced blobs
|
|
@@ -439,15 +441,15 @@ the packages a build needs. The answer is the design doc's §7.4 recipe,
|
|
|
439
441
|
```ts
|
|
440
442
|
install: {
|
|
441
443
|
exec: { command: 'pnpm install --frozen-lockfile', remote: 'only' },
|
|
444
|
+
inputs: { files: ['package.json', 'pnpm-lock.yaml'] },
|
|
442
445
|
cache: {
|
|
443
|
-
inputs: { files: ['package.json', 'pnpm-lock.yaml'] },
|
|
444
446
|
outputs: { files: ['node_modules/**'] },
|
|
445
447
|
},
|
|
446
448
|
},
|
|
447
449
|
build: {
|
|
448
450
|
dependsOn: ['install'],
|
|
449
451
|
exec: { command: 'tsc -p .' },
|
|
450
|
-
|
|
452
|
+
inputs: { files: ['src/**'] }, cache: { outputs: { files: ['dist/**'] } },
|
|
451
453
|
},
|
|
452
454
|
```
|
|
453
455
|
|
|
@@ -485,7 +487,7 @@ sorted by name — the proto requires that, so equivalent Commands hash alike:
|
|
|
485
487
|
- **`exec.env.define`** — literal `name: value` pairs from the task config.
|
|
486
488
|
They read the same on every machine, so they are safe to put into the
|
|
487
489
|
action identity, and they are already in the vx cache key.
|
|
488
|
-
- **`
|
|
490
|
+
- **`inputs.env`** — the values this machine resolved for those names,
|
|
489
491
|
for each name the task's local child would get too (it is also in
|
|
490
492
|
`exec.env.passThrough`). They are in the vx cache key by definition, so a
|
|
491
493
|
change to one already produces a different action. A name unset here, or
|
|
@@ -509,6 +511,6 @@ the worker has its own. Two reasons, and both matter:
|
|
|
509
511
|
can name its digest.
|
|
510
512
|
|
|
511
513
|
So a task that needs a value on a worker must `define` it (config literal) or
|
|
512
|
-
list it in both `
|
|
514
|
+
list it in both `inputs.env` and `exec.env.passThrough` (host value,
|
|
513
515
|
keyed, and seen by a local run the same way). A task whose command reads
|
|
514
516
|
a `passThrough` secret is one to keep local with `exec: { remote: false }`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-reapi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.640",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Bazel Remote Execution API plugin for vx — remote cache (ActionCache + CAS) against NativeLink, BuildBuddy, Buildbarn or bazel-remote.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@vzn/vx": "^0.0.
|
|
42
|
+
"@vzn/vx": "^0.0.640"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/cache.ts
CHANGED
|
@@ -165,6 +165,15 @@ export class ReapiRemoteCache {
|
|
|
165
165
|
* stream.
|
|
166
166
|
*/
|
|
167
167
|
async put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void> {
|
|
168
|
+
// An entry is immutable, and UpdateActionResult replaces one: the
|
|
169
|
+
// lookup runs beside the upload (CAS writes are idempotent), so a key
|
|
170
|
+
// stored already costs no round trip of its own and keeps its result.
|
|
171
|
+
// A lookup that fails writes, as before.
|
|
172
|
+
const action = actionDigestFor(hash)
|
|
173
|
+
const stored = this.client.getActionResult(action).then(
|
|
174
|
+
(r) => r?.output_files?.some((f) => f.path === ARTIFACT_PATH) === true,
|
|
175
|
+
() => false,
|
|
176
|
+
)
|
|
168
177
|
// Read once, and those bytes hashed and sent, wherever `writeBlob` would
|
|
169
178
|
// read the Blob whole anyway: a second writer of the key renames its
|
|
170
179
|
// artifact over the file, and two reads sent its bytes under this one's
|
|
@@ -186,7 +195,8 @@ export class ReapiRemoteCache {
|
|
|
186
195
|
const missing = await this.client.findMissingBlobs([digest])
|
|
187
196
|
if (missing.length > 0) await this.client.writeBlob(digest, data ?? body)
|
|
188
197
|
}
|
|
189
|
-
await
|
|
198
|
+
if (await stored) return
|
|
199
|
+
await this.client.updateActionResult(action, {
|
|
190
200
|
exit_code: 0,
|
|
191
201
|
output_files: [{ path: ARTIFACT_PATH, digest, is_executable: false }],
|
|
192
202
|
stdout_raw: encodeDuration(meta.durationMs),
|
package/src/executor.ts
CHANGED
|
@@ -1269,7 +1269,7 @@ export function globToOutputPath(glob: string): string {
|
|
|
1269
1269
|
}
|
|
1270
1270
|
|
|
1271
1271
|
/**
|
|
1272
|
-
* The action's environment: `
|
|
1272
|
+
* The action's environment: `inputs.env` (values read from THIS
|
|
1273
1273
|
* machine's environment, already folded into the cache key) plus
|
|
1274
1274
|
* `exec.env.define` (literals from the task config). A define wins on
|
|
1275
1275
|
* collision — it is the more explicit statement of intent. An `inputs.env`
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// plugin, so nothing here depends on core's internal layout.
|
|
11
11
|
|
|
12
12
|
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { isIP } from 'node:net'
|
|
14
|
+
import * as grpc from '@grpc/grpc-js'
|
|
13
15
|
import {
|
|
14
16
|
definePlugin,
|
|
15
17
|
type RemoteCacheLayer,
|
|
@@ -21,7 +23,7 @@ import {
|
|
|
21
23
|
} from '@vzn/vx'
|
|
22
24
|
import { ReapiRemoteCache } from './cache.js'
|
|
23
25
|
import { reapiExecutor } from './executor.js'
|
|
24
|
-
import { ReapiClient, type ReapiOptions } from './wire.js'
|
|
26
|
+
import { ReapiClient, type ReapiOptions, usesTls } from './wire.js'
|
|
25
27
|
|
|
26
28
|
// `ReapiRemoteCache` is public for a workspace that composes cache
|
|
27
29
|
// layers by hand; the wire, the Merkle encoders and the executor are
|
|
@@ -149,6 +151,11 @@ function connection(options: ReapiPluginOptions): ReapiOptions | undefined {
|
|
|
149
151
|
'vx/reapi: a client certificate and its key go together — set both tlsClientCertificate and tlsClientKey (VX_REAPI_TLS_CLIENT_CERTIFICATE / VX_REAPI_TLS_CLIENT_KEY), or neither',
|
|
150
152
|
)
|
|
151
153
|
}
|
|
154
|
+
if (usesTls({ endpoint, tls: options.tls, tlsCaPem, tlsClientCertPem }) && ipTarget(endpoint)) {
|
|
155
|
+
throw new UserError(
|
|
156
|
+
`vx/reapi: ${from} is ${JSON.stringify(endpoint)}, TLS to an IP address, which Bun cannot open (it refuses an IP as the TLS server name) — name the server by host name, or use grpc:// for plaintext`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
152
159
|
return {
|
|
153
160
|
...options,
|
|
154
161
|
endpoint,
|
|
@@ -179,6 +186,20 @@ function assertEndpoint(endpoint: string, from: string): void {
|
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
/**
|
|
190
|
+
* The endpoint names its server by IP address. grpc-js sends the host as
|
|
191
|
+
* the TLS server name, and Bun's TLS throws on an IP there (Node only
|
|
192
|
+
* warns), so every request failed and the remote was never reached.
|
|
193
|
+
*/
|
|
194
|
+
function ipTarget(endpoint: string): boolean {
|
|
195
|
+
if (/^ipv[46]:/.test(endpoint)) return true
|
|
196
|
+
const host = endpoint
|
|
197
|
+
.replace(/^(https?|grpcs?|dns):\/*/, '')
|
|
198
|
+
.replace(/:\d+$/, '')
|
|
199
|
+
.replace(/^\[(.*)\]$/, '$1')
|
|
200
|
+
return isIP(host) !== 0
|
|
201
|
+
}
|
|
202
|
+
|
|
182
203
|
/** Each option `ReapiPluginOptions` names, with its kind: derived from the type, so the two cannot drift. */
|
|
183
204
|
const REAPI_PLUGIN_KEYS: PluginOptionKinds<ReapiPluginOptions> = {
|
|
184
205
|
executeTimeoutMs: 'number',
|
|
@@ -226,6 +247,14 @@ export function reapi(options: ReapiPluginOptions = {}): VxPlugin {
|
|
|
226
247
|
executorClient.close()
|
|
227
248
|
executorClient = undefined
|
|
228
249
|
const why = err instanceof Error ? err.message : String(err)
|
|
250
|
+
// A server that answered with a refusal was reached: the endpoint
|
|
251
|
+
// advice sent the user to check everything but the token.
|
|
252
|
+
const code = (err as { code?: unknown } | null)?.code
|
|
253
|
+
if (code === grpc.status.UNAUTHENTICATED || code === grpc.status.PERMISSION_DENIED) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`the REAPI server at ${conn.endpoint} refused the credentials for remote execution: ${why} — check the token in \`headers\`, or drop \`execute\` to use it as a cache only`,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
229
258
|
throw new Error(
|
|
230
259
|
`cannot reach the REAPI server at ${conn.endpoint} for remote execution: ${why} — check the endpoint, that the server is running, and that this host can reach it, or drop \`execute\` to use it as a cache only`,
|
|
231
260
|
)
|
package/src/wire.ts
CHANGED
|
@@ -421,6 +421,24 @@ async function* messagesOf(
|
|
|
421
421
|
if (carry.length > 0) yield carry
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
/**
|
|
425
|
+
* TLS unless the endpoint says plaintext, as Bazel reads a schemeless
|
|
426
|
+
* `--remote_cache`; a unix socket stays plaintext.
|
|
427
|
+
*/
|
|
428
|
+
export function usesTls(opts: {
|
|
429
|
+
endpoint: string
|
|
430
|
+
tls?: boolean | undefined
|
|
431
|
+
tlsCaPem?: string | undefined
|
|
432
|
+
tlsClientCertPem?: string | undefined
|
|
433
|
+
}): boolean {
|
|
434
|
+
return (
|
|
435
|
+
opts.tls ??
|
|
436
|
+
(opts.tlsCaPem !== undefined ||
|
|
437
|
+
opts.tlsClientCertPem !== undefined ||
|
|
438
|
+
!/^((http|grpc):\/\/|unix:|unix-abstract:)/.test(opts.endpoint))
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
424
442
|
export class ReapiClient {
|
|
425
443
|
private readonly svc: ServiceClients
|
|
426
444
|
private readonly instance: string
|
|
@@ -453,13 +471,7 @@ export class ReapiClient {
|
|
|
453
471
|
assertBunSupportsChunking()
|
|
454
472
|
const pem = (text: string | undefined): Buffer | null =>
|
|
455
473
|
text === undefined ? null : Buffer.from(text)
|
|
456
|
-
|
|
457
|
-
// `--remote_cache`; a unix socket stays plaintext.
|
|
458
|
-
const tls =
|
|
459
|
-
opts.tls ??
|
|
460
|
-
(opts.tlsCaPem !== undefined ||
|
|
461
|
-
opts.tlsClientCertPem !== undefined ||
|
|
462
|
-
!/^((http|grpc):\/\/|unix:|unix-abstract:)/.test(opts.endpoint))
|
|
474
|
+
const tls = usesTls(opts)
|
|
463
475
|
const target = opts.endpoint.replace(/^(https?|grpcs?):\/\//, '')
|
|
464
476
|
this.svc = loadServices(
|
|
465
477
|
target,
|