runcloud 0.2.189 → 0.2.191
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/dist/commands/dogfood.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
|
-
import { ApiClient, friendlyApiError } from '../api.js';
|
|
7
|
+
import { ApiClient, ApiError, friendlyApiError } from '../api.js';
|
|
8
8
|
import { requireCredentials } from '../config.js';
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
export const DOGFOOD_SECRET_GROUP = 'newly-dogfood';
|
|
@@ -171,26 +171,57 @@ async function projectOrgId(client, projectId) {
|
|
|
171
171
|
}
|
|
172
172
|
export async function pushDogfoodCreds(client, projectId, entries) {
|
|
173
173
|
const orgId = await projectOrgId(client, projectId);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
const path = `/orgs/${encodeURIComponent(orgId)}/secret-groups/${DOGFOOD_SECRET_GROUP}`;
|
|
175
|
+
if (Object.keys(entries).length === 0) {
|
|
176
|
+
try {
|
|
177
|
+
await client.delete(path);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (!(err instanceof ApiError && err.status === 404))
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await client.put(path, { entries, exposeToSandbox: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
export async function fetchServerCredentials(client) {
|
|
188
|
+
try {
|
|
189
|
+
const status = await client.get('/dogfood');
|
|
190
|
+
const provided = status?.serverCredentials;
|
|
191
|
+
return { github: provided?.github === true, gcp: provided?.gcp === true };
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (err instanceof ApiError && err.status === 404)
|
|
195
|
+
return { github: false, gcp: false };
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export function credentialsToSupply(server, opts) {
|
|
200
|
+
return { github: Boolean(opts.githubTokenFile) || !server.github, gcp: opts.gcp && !server.gcp };
|
|
179
201
|
}
|
|
180
202
|
async function credsOnce(opts) {
|
|
181
203
|
const { client, apiUrl } = api();
|
|
182
204
|
const projectId = projectIdFrom(opts.project, apiUrl);
|
|
205
|
+
const supply = credentialsToSupply(await fetchServerCredentials(client), opts);
|
|
206
|
+
if (!supply.github && !supply.gcp) {
|
|
207
|
+
await pushDogfoodCreds(client, projectId, {});
|
|
208
|
+
console.log(`${apiUrl} mints the sessions' GitHub and GCP tokens itself; cleared any stored ones.`);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
183
211
|
const entries = {};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
if (supply.github) {
|
|
213
|
+
const github = await readGithubToken({ tokenFile: opts.githubTokenFile });
|
|
214
|
+
entries.GH_TOKEN = github.token;
|
|
215
|
+
if (github.source === 'gh-cli') {
|
|
216
|
+
console.log('GitHub: using your gh CLI token (all of its scopes). A fine-grained token is safer: --github-token-file.');
|
|
217
|
+
}
|
|
188
218
|
}
|
|
189
|
-
if (
|
|
219
|
+
if (supply.gcp)
|
|
190
220
|
entries.GOOGLE_OAUTH_ACCESS_TOKEN = await mintDogfoodGcpToken();
|
|
191
221
|
await pushDogfoodCreds(client, projectId, entries);
|
|
192
222
|
console.log(`${new Date().toISOString()} credentials pushed to project ${projectId}: ${Object.keys(entries).join(', ')}` +
|
|
193
|
-
(
|
|
223
|
+
(supply.gcp ? ' (GCP token valid ~1h)' : ''));
|
|
224
|
+
return supply.gcp;
|
|
194
225
|
}
|
|
195
226
|
export function dogfoodSessionBody(opts) {
|
|
196
227
|
const body = {
|
|
@@ -341,25 +372,40 @@ export function registerDogfood(program) {
|
|
|
341
372
|
.action((opts) => run(async () => {
|
|
342
373
|
const { client, apiUrl } = api();
|
|
343
374
|
console.log(`control plane: ${apiUrl}`);
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
console.log(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
375
|
+
const supply = credentialsToSupply(await fetchServerCredentials(client), { githubTokenFile: opts.githubTokenFile, gcp: true });
|
|
376
|
+
const entries = {};
|
|
377
|
+
if (supply.gcp) {
|
|
378
|
+
console.log('1/4 GCP: minting a dogfood-agent token by impersonation...');
|
|
379
|
+
const gcpToken = await mintDogfoodGcpToken();
|
|
380
|
+
entries.GOOGLE_OAUTH_ACCESS_TOKEN = gcpToken;
|
|
381
|
+
console.log(' ok');
|
|
382
|
+
console.log('2/4 SOPS: dev must decrypt, prod must be refused...');
|
|
383
|
+
const root = await repoRoot();
|
|
384
|
+
if (root) {
|
|
385
|
+
const result = await verifySopsBoundary(root, gcpToken);
|
|
386
|
+
console.log(result.detail.replace(/^/gm, ' '));
|
|
387
|
+
if (!result.prodRefused)
|
|
388
|
+
throw new Error('the dogfood token can decrypt prod — stop and report this');
|
|
389
|
+
if (!result.dev)
|
|
390
|
+
throw new Error('the dogfood token cannot decrypt dev; see the error above');
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
console.log(' skipped: run setup from a monorepo checkout to verify (needs secrets/*.yaml and sops >= 3.10)');
|
|
394
|
+
}
|
|
356
395
|
}
|
|
357
396
|
else {
|
|
358
|
-
console.log(
|
|
397
|
+
console.log("1/4 GCP: the control plane mints the dogfood agent's token itself (dev sops key only)");
|
|
398
|
+
console.log("2/4 SOPS: nothing to prove from here; the agent account's IAM is the boundary");
|
|
399
|
+
}
|
|
400
|
+
if (supply.github) {
|
|
401
|
+
console.log('3/4 GitHub: reading a token...');
|
|
402
|
+
const github = await readGithubToken({ tokenFile: opts.githubTokenFile });
|
|
403
|
+
entries.GH_TOKEN = github.token;
|
|
404
|
+
console.log(` ok (${github.source})`);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log('3/4 GitHub: the control plane gives sessions a Newly GitHub App token (the dogfood repo only)');
|
|
359
408
|
}
|
|
360
|
-
console.log('3/4 GitHub: reading a token...');
|
|
361
|
-
const github = await readGithubToken({ tokenFile: opts.githubTokenFile });
|
|
362
|
-
console.log(` ok (${github.source})`);
|
|
363
409
|
console.log('4/4 Project...');
|
|
364
410
|
let projectId = opts.project ?? (readState()?.apiUrl === apiUrl ? readState()?.projectId : undefined);
|
|
365
411
|
if (!projectId) {
|
|
@@ -377,10 +423,17 @@ export function registerDogfood(program) {
|
|
|
377
423
|
console.log(` using ${projectId}`);
|
|
378
424
|
}
|
|
379
425
|
writeState({ projectId, apiUrl });
|
|
380
|
-
await pushDogfoodCreds(client, projectId,
|
|
381
|
-
console.log(
|
|
382
|
-
|
|
383
|
-
|
|
426
|
+
await pushDogfoodCreds(client, projectId, entries);
|
|
427
|
+
console.log(Object.keys(entries).length > 0
|
|
428
|
+
? ' credentials stored as the project\'s "newly-dogfood" secret group (dogfood sessions only)'
|
|
429
|
+
: ' no credentials stored: the control plane supplies them (any earlier ones were removed)');
|
|
430
|
+
if (supply.gcp) {
|
|
431
|
+
console.log('\nNext: `newly dogfood creds --watch` in a spare terminal (keeps the GCP token fresh),');
|
|
432
|
+
console.log('then `newly dogfood start "<what to do>"`.');
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
console.log('\nNext: `newly dogfood start "<what to do>"`.');
|
|
436
|
+
}
|
|
384
437
|
}));
|
|
385
438
|
dogfood
|
|
386
439
|
.command('creds')
|
|
@@ -390,8 +443,8 @@ export function registerDogfood(program) {
|
|
|
390
443
|
.option('--no-gcp', 'do not mint a GCP token (sessions then cannot use sops)')
|
|
391
444
|
.option('--watch', `keep refreshing every ${CREDS_REFRESH_MS / 60_000} minutes`, false)
|
|
392
445
|
.action((opts) => run(async () => {
|
|
393
|
-
await credsOnce(opts);
|
|
394
|
-
if (!opts.watch)
|
|
446
|
+
const mintedGcp = await credsOnce(opts);
|
|
447
|
+
if (!opts.watch || !mintedGcp)
|
|
395
448
|
return;
|
|
396
449
|
for (;;) {
|
|
397
450
|
await new Promise((resolve) => setTimeout(resolve, CREDS_REFRESH_MS));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
const path = '/run-cloud/admin/fleet-release-control';
|
|
3
|
+
export function releaseControlRequest(file) {
|
|
4
|
+
const stat = statSync(file);
|
|
5
|
+
if (!stat.isFile() || stat.size > 512 * 1024)
|
|
6
|
+
throw new Error('Use a JSON request file smaller than 512 KiB');
|
|
7
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.reason !== 'string' || !value.reason.trim()
|
|
9
|
+
|| !Number.isSafeInteger(value.expectedRevision) || Number(value.expectedRevision) < 0
|
|
10
|
+
|| !['bootstrap', 'enroll', 'announce', 'override', 'pause', 'resume', 'rollback', 'defer'].includes(String(value.action))) {
|
|
11
|
+
throw new Error('Request needs action, reason and expectedRevision from the current release-control status');
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function registerFleetReleaseControlCommands(fleet, client, action) {
|
|
16
|
+
const release = fleet.command('release-control').description('Audited fleet targets and coordinated health-gated serve-sim rollouts');
|
|
17
|
+
release.command('status').description('Read targets, rollout stages, gate values and audit history').action(() => action(async () => {
|
|
18
|
+
console.log(JSON.stringify(await client().get(path, { signal: AbortSignal.timeout(30000) }), null, 2));
|
|
19
|
+
}));
|
|
20
|
+
release.command('apply <request.json>').description('Apply an audited target or rollout request; pause stops new grants while existing holders finish safely')
|
|
21
|
+
.action((file) => action(async () => {
|
|
22
|
+
console.log(JSON.stringify(await client().post(path, releaseControlRequest(file), { signal: AbortSignal.timeout(30000) }), null, 2));
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
@@ -5,6 +5,7 @@ import { ApiClient, ApiError } from '../api.js';
|
|
|
5
5
|
import { requireCredentials } from '../config.js';
|
|
6
6
|
import { surveyFleet, surveyText } from './run-cloud-fleet-survey.js';
|
|
7
7
|
import { listReleaseChannels, promoteReleaseChannel, releaseText } from './run-cloud-fleet-releases.js';
|
|
8
|
+
import { registerFleetReleaseControlCommands } from './run-cloud-fleet-release-control.js';
|
|
8
9
|
const ROOT = '/run-cloud/admin/simulators';
|
|
9
10
|
const RANGES = ['1h', '6h', '24h', '7d', '30d', '90d', '1y', '2y'];
|
|
10
11
|
export const ACCESSIBILITY_CODES = ['accessibility_device_unavailable', 'accessibility_capture_failed'];
|
|
@@ -303,6 +304,7 @@ function printReport(report, json) {
|
|
|
303
304
|
}
|
|
304
305
|
export function registerRunCloudFleet(program) {
|
|
305
306
|
const fleet = program.command('fleet').description('Admin fleet telemetry, diagnosis, events, failure evidence and serve-sim release channels');
|
|
307
|
+
registerFleetReleaseControlCommands(fleet, credentialsClient, action);
|
|
306
308
|
fleet.command('survey [hostId]').description('Survey all fleet signals in seconds; authenticated admin reads, no local services')
|
|
307
309
|
.option('--json', 'Structured metrics, diagnosis, observations and coverage')
|
|
308
310
|
.option('--range <range>', 'Alert/input-lag window: 1h, 6h, 24h, 7d', '24h')
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.2.
|
|
1
|
+
export const CLI_VERSION = '0.2.191';
|
package/package.json
CHANGED
|
Binary file
|
package/runcloud-0.2.189.tgz
DELETED
|
Binary file
|