runcloud 0.1.121 → 0.1.122

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.
@@ -1305,13 +1305,15 @@ async function action(fn, opts = {}) {
1305
1305
  }
1306
1306
  function configureMachineReadableParseErrors(program) {
1307
1307
  const output = program.configureOutput();
1308
+ const writeErrorLine = output.outputError ??
1309
+ ((value, destination) => destination(value));
1308
1310
  program.configureOutput({
1309
1311
  ...output,
1310
1312
  outputError: (message, write) => {
1311
1313
  const rawArgs = program.rawArgs ?? [];
1312
1314
  const argumentsSeen = [...process.argv, ...rawArgs, ...program.args];
1313
1315
  if (!argumentsSeen.includes('--json')) {
1314
- (output.outputError ?? ((value, destination) => destination(value)))(message, write);
1316
+ writeErrorLine(message, write);
1315
1317
  return;
1316
1318
  }
1317
1319
  write(`${JSON.stringify({
@@ -8,7 +8,7 @@ import { runSandboxCommand, shellCommand } from '../sandboxExec.js';
8
8
  import { runSandboxShell } from '../shellSession.js';
9
9
  import { printJson, StatusLine, style } from '../terminal.js';
10
10
  import { noteDeprecated, registerAccessCommands } from './boxAccess.js';
11
- import { renderSandbox, renderSandboxList, } from './sandboxOutput.js';
11
+ import { renderHeldSecrets, renderSandbox, renderSandboxList, } from './sandboxOutput.js';
12
12
  const MAX_SANDBOX_DISK_GIB = 2048;
13
13
  const DEFAULT_SANDBOX_TIMEOUT_SECONDS = 300;
14
14
  const DEFAULT_BOX_CPU_CORES = 2;
@@ -260,6 +260,15 @@ function secretSelector(opts) {
260
260
  out.env = env;
261
261
  return out;
262
262
  }
263
+ async function replaceSandboxSecrets(id, opts) {
264
+ const body = secretSelector(opts);
265
+ await runCloudApi().put(`/run-cloud/sandboxes/${encodeURIComponent(id)}/secrets`, body);
266
+ if (opts.json) {
267
+ printJson({ id, revoked: opts.noSecrets === true, ...body });
268
+ return;
269
+ }
270
+ console.log(opts.noSecrets ? `${id}: all secrets revoked` : `${id}: secrets replaced`);
271
+ }
263
272
  function collectRepeatable(value, previous) {
264
273
  return [...previous, value];
265
274
  }
@@ -472,17 +481,38 @@ export function registerSandbox(program) {
472
481
  await new Promise((resolveDelay) => setTimeout(resolveDelay, opts.interval * 1_000));
473
482
  }
474
483
  }));
475
- withOutput(sandbox
484
+ const secrets = withOutput(sandbox
476
485
  .command('secrets')
477
- .description('Replace the secrets a running sandbox holds (a full replacement, not a merge)')
486
+ .description('Show what secrets a sandbox holds (names and origins, never values)')
487
+ .argument('[id]', 'sandbox id'))
488
+ .showHelpAfterError("hint: `runcloud sandbox secrets set <id> …` changes what a sandbox holds; the bare form only reads it");
489
+ const wantsJson = (opts) => Boolean(secrets.opts().json) || Boolean(opts.json);
490
+ secrets.action((id, opts) => run(async () => {
491
+ if (!id)
492
+ throw new Error('which sandbox? usage: runcloud sandbox secrets <id>');
493
+ const { held } = (await runCloudApi().get(`/run-cloud/sandboxes/${encodeURIComponent(id)}/secrets`));
494
+ if (opts.json) {
495
+ printJson({ held });
496
+ return;
497
+ }
498
+ console.log(renderHeldSecrets(id, held));
499
+ }));
500
+ withOutput(secrets
501
+ .command('set')
502
+ .description('Replace what a sandbox holds (a full replacement, not a merge)')
478
503
  .argument('<id>', 'sandbox id')
479
504
  .option('--secret-group <group>', 'attach a secret group; repeatable and ORDER MATTERS (a later group wins on a name collision)', collectRepeatable, [])
480
505
  .option('--secret <group/name>', 'attach one secret out of a group; repeatable, same ordering', collectRepeatable, [])
481
506
  .option('--env <NAME=VALUE>', 'literal value; applied last, so it wins; repeatable', collectRepeatable, [])).action((id, opts) => run(async () => {
482
- const body = secretSelector(opts);
483
- await runCloudApi().put(`/run-cloud/sandboxes/${encodeURIComponent(id)}/secrets`, body);
484
- console.log(`${id}: secrets replaced`);
507
+ if (!opts.secretGroup.length && !opts.secret.length && !opts.env.length) {
508
+ throw new Error('name what the sandbox should hold with --secret-group/--secret/--env, or run `runcloud sandbox secrets revoke <id>` to take everything back');
509
+ }
510
+ await replaceSandboxSecrets(id, { ...opts, json: wantsJson(opts) });
485
511
  }));
512
+ withOutput(secrets
513
+ .command('revoke')
514
+ .description('Take every secret back from a sandbox')
515
+ .argument('<id>', 'sandbox id')).action((id, opts) => run(() => replaceSandboxSecrets(id, { noSecrets: true, json: wantsJson(opts) })));
486
516
  withOutput(sandbox
487
517
  .command('expose')
488
518
  .description('Publish a sandbox guest port at a stable hostname, or change the published port')
@@ -1,4 +1,24 @@
1
1
  import { formatAge, formatCpu, formatMib, formatSeconds, formatState, renderDetails, renderTable, shortDigest, style, } from '../terminal.js';
2
+ export function renderHeldSecrets(id, held) {
3
+ const heading = `Secrets held by ${id}`;
4
+ if (held === null) {
5
+ return `${style.bold(heading)}\n\nUnknown — nothing was recorded for this sandbox, so what it\nholds cannot be answered from here.\n\n${style.dim('Check the environment inside it, or run `runcloud sandbox secrets set`,')}\n${style.dim('which records what it pushes.')}`;
6
+ }
7
+ if (held.length === 0) {
8
+ return `${style.bold(heading)}\n\nNothing. This sandbox holds no secrets.\n\n${style.dim('Attach some:')} runcloud sandbox secrets set ${id} --secret-group <group>`;
9
+ }
10
+ return renderTable(`${heading} (${held.length})`, [
11
+ { key: 'name', label: 'NAME', maxWidth: 32 },
12
+ { key: 'kind', label: 'KIND' },
13
+ { key: 'source', label: 'FROM', maxWidth: 28 },
14
+ { key: 'digest', label: 'DIGEST' },
15
+ ], held.map((secret) => ({
16
+ name: secret.name,
17
+ kind: secret.kind,
18
+ source: secret.source,
19
+ digest: secret.digest,
20
+ })));
21
+ }
2
22
  function resources(record) {
3
23
  const memory = formatMib(record.mem_mb);
4
24
  const disk = formatMib(record.disk_mb);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.121';
1
+ export const CLI_VERSION = '0.1.122';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.121",
3
+ "version": "0.1.122",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
@@ -190,9 +190,20 @@ immediately.
190
190
  - Attach only the required groups or names with repeatable `--secret-group` and
191
191
  `--secret`. Later selectors win on name collisions; `--env` is applied last.
192
192
  - Use `--no-secrets` to state explicitly that a new sandbox needs none.
193
- - Treat `runcloud sandbox secrets <id> ...` as full replacement, not a merge.
194
- - Remember that values cannot be read back and snapshots do not contain
195
- secrets.
193
+ - Three verbs, because two of them change things:
194
+ - `runcloud sandbox secrets <id>` reads. Names, where each one resolved from,
195
+ and a digest of its value.
196
+ - `runcloud sandbox secrets set <id> ...` replaces. It is a
197
+ full replacement, not a merge: the sandbox ends up holding exactly what
198
+ that one command names.
199
+ - `runcloud sandbox secrets revoke <id>` takes everything back.
200
+ - A listing of `Unknown` is not `Nothing`. The inventory is recorded when a
201
+ delivery is accepted, so a sandbox created before inventories existed has no
202
+ record — its secrets still work. Only `Nothing` means the sandbox is empty.
203
+ - Values never come back, from any command. The digest exists so you can tell
204
+ whether the value in a sandbox is still the one its group holds; it is not
205
+ reversible and there is no masked form.
206
+ - Snapshots do not contain secrets, so a restored sandbox starts with none.
196
207
 
197
208
  Inspect `runcloud secret-group --help` and `runcloud secrets --help` for the
198
209
  current non-plaintext input forms.
@@ -350,6 +361,7 @@ warm-parked and resumable is the most expensive mistake available here.
350
361
  | `pause` `resume` `stop` `archive` `restore` | as asked | a caller requested exactly this |
351
362
  | `api` `api-force` | destroyed | a caller destroyed it. `api-force` bypassed a wedged guest |
352
363
  | `health-sweep` | interrupted | the platform judged it unhealthy |
364
+ | `host-state-reconcile` | paused | the host had already parked it and the control plane caught up. The pause happened earlier than this event says |
353
365
  | `host-reconcile` `reap-sweep` | destroyed | the platform reclaimed it |
354
366
  | `boot-failure` `placement-timeout` `request-timeout` `scheduler` | never ran | capacity or image, not the workload |
355
367
  | `image-built` `image-build-failure` | varies | an async image build finished or failed |
Binary file