@magland/mochi 0.3.1 → 0.4.0
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 +7 -6
- package/dist/agefile.js +47 -0
- package/dist/agescript.js +343 -0
- package/dist/api/admin.js +66 -28
- package/dist/api/auth.js +3 -2
- package/dist/api/backup.js +227 -192
- package/dist/api/contents.js +46 -16
- package/dist/api/repos.js +60 -4
- package/dist/api/write.js +138 -8
- package/dist/api.js +34 -1
- package/dist/browse.js +171 -33
- package/dist/build-info.json +2 -2
- package/dist/ci/api.js +138 -4
- package/dist/ci/artifacts.js +82 -2
- package/dist/ci/engine.js +30 -17
- package/dist/ci/expr.js +14 -0
- package/dist/ci/runners.js +60 -1
- package/dist/ci/runs.js +21 -3
- package/dist/ci/trigger.js +6 -4
- package/dist/ci/views.js +24 -3
- package/dist/ci/web.js +48 -0
- package/dist/cli/admin-cmd.js +40 -3
- package/dist/cli/backup-cmd.js +138 -120
- package/dist/cli/gitrun.js +50 -0
- package/dist/cli/pr-cmd.js +155 -0
- package/dist/cli/repo-cmd.js +134 -6
- package/dist/cli/sync-cmd.js +128 -0
- package/dist/cli/target.js +7 -3
- package/dist/config.js +10 -0
- package/dist/credentials.js +9 -8
- package/dist/deploy-cli.js +214 -109
- package/dist/deploy-runner-cli.js +125 -49
- package/dist/domains.js +259 -0
- package/dist/egress.js +23 -6
- package/dist/forms.js +703 -32
- package/dist/git.js +124 -13
- package/dist/githttp.js +80 -23
- package/dist/githubauth.js +185 -0
- package/dist/icons.js +14 -0
- package/dist/import-cli.js +40 -25
- package/dist/index.js +110 -4
- package/dist/issueweb.js +10 -6
- package/dist/lfs.js +9 -2
- package/dist/limit.js +2 -2
- package/dist/logincodes.js +31 -0
- package/dist/maintenance.js +297 -0
- package/dist/markdown.js +192 -3
- package/dist/mdedit.js +29 -0
- package/dist/migrate.js +48 -0
- package/dist/naming.js +25 -0
- package/dist/onetime.js +112 -0
- package/dist/ops.js +293 -16
- package/dist/pagescript.js +355 -24
- package/dist/perms.js +1 -0
- package/dist/profile.js +2 -0
- package/dist/pulls.js +21 -1
- package/dist/pullweb.js +25 -6
- package/dist/releases.js +10 -1
- package/dist/runner/actions.js +12 -1
- package/dist/runner/client.js +40 -2
- package/dist/runner/docker.js +40 -28
- package/dist/runner/download.js +77 -0
- package/dist/runner/externals.js +12 -1
- package/dist/runner/job.js +26 -11
- package/dist/runner/steps.js +42 -7
- package/dist/runner-cli.js +89 -0
- package/dist/scan.js +58 -2
- package/dist/server.js +107 -26
- package/dist/session.js +82 -40
- package/dist/site.js +110 -27
- package/dist/sitepublish.js +177 -0
- package/dist/sitesettings.js +420 -0
- package/dist/siteshost.js +116 -28
- package/dist/source.js +20 -0
- package/dist/style.js +162 -3
- package/dist/topics.js +165 -0
- package/dist/vault.js +399 -6
- package/dist/vendor-age.js +15 -0
- package/dist/views.js +242 -57
- package/dist/web.js +11 -1
- package/dist/webauthn.js +347 -0
- package/dist/webops.js +1263 -33
- package/package.json +4 -4
package/dist/deploy-cli.js
CHANGED
|
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.IMAGE_REPO = void 0;
|
|
36
|
+
exports.MOCHI_DEPLOY = exports.IMAGE_REPO = void 0;
|
|
37
37
|
exports.fly = fly;
|
|
38
38
|
exports.flyStream = flyStream;
|
|
39
39
|
exports.die = die;
|
|
40
40
|
exports.parseVmSize = parseVmSize;
|
|
41
41
|
exports.normalizeMemory = normalizeMemory;
|
|
42
|
+
exports.imagePublished = imagePublished;
|
|
43
|
+
exports.requirePublishedImage = requirePublishedImage;
|
|
42
44
|
exports.ownVersion = ownVersion;
|
|
43
45
|
exports.sourceRoot = sourceRoot;
|
|
44
46
|
exports.requireFly = requireFly;
|
|
@@ -57,6 +59,7 @@ const os = __importStar(require("os"));
|
|
|
57
59
|
const path = __importStar(require("path"));
|
|
58
60
|
const credentials_1 = require("./credentials");
|
|
59
61
|
const backup_cmd_1 = require("./cli/backup-cmd");
|
|
62
|
+
const naming_1 = require("./naming");
|
|
60
63
|
const vault_1 = require("./vault");
|
|
61
64
|
// `mochi deploy fly`: put a vault on Fly.io from one command, and deploy
|
|
62
65
|
// updates to it with the same one. This is a thin driver of the fly command
|
|
@@ -70,8 +73,6 @@ const vault_1 = require("./vault");
|
|
|
70
73
|
// fly.toml goes to a temporary directory for the length of the deploy, which is
|
|
71
74
|
// why there is no fly.toml in this repository to keep in sync or to explain.
|
|
72
75
|
exports.IMAGE_REPO = 'ghcr.io/magland/mochi';
|
|
73
|
-
const VOLUME_NAME = 'vault';
|
|
74
|
-
const OWNER_TOKEN_SECRET = 'MOCHI_OWNER_TOKEN';
|
|
75
76
|
const INTERNAL_PORT = 3000;
|
|
76
77
|
const DEFAULTS = { region: 'ewr', volumeGb: 10, cpuKind: 'shared', cpus: 1, memory: '512mb' };
|
|
77
78
|
const FLY_NOT_FOUND = 'Neither fly nor flyctl is on PATH. Install it from https://fly.io/docs/flyctl/install/';
|
|
@@ -259,6 +260,65 @@ function normalizeMemory(memory) {
|
|
|
259
260
|
die(`Memory of ${memory} is below Fly's 256mb minimum.`);
|
|
260
261
|
return `${mb}mb`;
|
|
261
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Whether a tag exists in the registry, or null when the registry could not
|
|
265
|
+
* be asked. Anonymous, over the registry protocol every pull uses: a token
|
|
266
|
+
* for the repository, then the manifest by tag.
|
|
267
|
+
*
|
|
268
|
+
* The question arises because of the order a release happens in. The npm
|
|
269
|
+
* package is published first and the image is built from the tag that
|
|
270
|
+
* follows it, so for some minutes after a release the newest CLI names an
|
|
271
|
+
* image that is not there yet. Fly would report that as a failed pull after
|
|
272
|
+
* creating the app; asking first turns it into one sentence before anything
|
|
273
|
+
* is touched. Not being able to ask is not a refusal: the deploy goes ahead
|
|
274
|
+
* and Fly says whatever it finds.
|
|
275
|
+
*/
|
|
276
|
+
async function imagePublished(ref) {
|
|
277
|
+
const m = /^ghcr\.io\/([a-z0-9._/-]+):([A-Za-z0-9._-]+)$/.exec(ref);
|
|
278
|
+
if (!m)
|
|
279
|
+
return null;
|
|
280
|
+
try {
|
|
281
|
+
const tokenRes = await fetch(`https://ghcr.io/token?scope=repository:${m[1]}:pull`);
|
|
282
|
+
if (!tokenRes.ok)
|
|
283
|
+
return null;
|
|
284
|
+
const { token } = (await tokenRes.json());
|
|
285
|
+
if (!token)
|
|
286
|
+
return null;
|
|
287
|
+
const res = await fetch(`https://ghcr.io/v2/${m[1]}/manifests/${m[2]}`, {
|
|
288
|
+
method: 'HEAD',
|
|
289
|
+
headers: {
|
|
290
|
+
authorization: `Bearer ${token}`,
|
|
291
|
+
accept: [
|
|
292
|
+
'application/vnd.oci.image.index.v1+json',
|
|
293
|
+
'application/vnd.docker.distribution.manifest.list.v2+json',
|
|
294
|
+
'application/vnd.oci.image.manifest.v1+json',
|
|
295
|
+
'application/vnd.docker.distribution.manifest.v2+json',
|
|
296
|
+
].join(', '),
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
if (res.status === 404)
|
|
300
|
+
return false;
|
|
301
|
+
return res.ok ? true : null;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Refuse, with the reason, when the image this CLI's version names has not
|
|
309
|
+
* been published yet. Only for the image chosen by default: a ref the
|
|
310
|
+
* operator passed by hand is theirs to have got right, and Fly says so.
|
|
311
|
+
*/
|
|
312
|
+
async function requirePublishedImage(image, runner) {
|
|
313
|
+
if ((await imagePublished(image)) !== false)
|
|
314
|
+
return;
|
|
315
|
+
die(`No published image for this CLI's version: ${image} is not in the registry.\n` +
|
|
316
|
+
"A version's image is built after its npm release and is usually there within\n" +
|
|
317
|
+
'ten minutes of it. Try again shortly, or pass --image <ref> to deploy another\n' +
|
|
318
|
+
`tag: ${image.replace(/:[^:]+$/, ':main')} is the newest build of main that passed\n` +
|
|
319
|
+
'its tests' +
|
|
320
|
+
(runner ? '.' : ', and --from-source builds this checkout instead.'));
|
|
321
|
+
}
|
|
262
322
|
/** The version of this CLI, which is the image tag deployed unless --image says otherwise. */
|
|
263
323
|
function ownVersion() {
|
|
264
324
|
try {
|
|
@@ -287,7 +347,7 @@ function sourceRoot() {
|
|
|
287
347
|
` ${root}\n\n` +
|
|
288
348
|
'The published package contains only the compiled output, so there is nothing to\n' +
|
|
289
349
|
'build. Clone the repository and run the deploy from there:\n\n' +
|
|
290
|
-
' git clone https://github.com/magland/
|
|
350
|
+
' git clone https://github.com/magland/mochiforge && cd mochiforge && npm install\n' +
|
|
291
351
|
' npm run build && node dist/index.js deploy fly <app> --from-source\n');
|
|
292
352
|
}
|
|
293
353
|
return root;
|
|
@@ -306,9 +366,6 @@ async function namedVolume(app, name) {
|
|
|
306
366
|
const vols = (await flyJson(['volumes', 'list', '-a', app])) ?? [];
|
|
307
367
|
return vols.find((v) => v.name === name) ?? null;
|
|
308
368
|
}
|
|
309
|
-
async function vaultVolume(app) {
|
|
310
|
-
return namedVolume(app, VOLUME_NAME);
|
|
311
|
-
}
|
|
312
369
|
async function machines(app) {
|
|
313
370
|
return (await flyJson(['machines', 'list', '-a', app])) ?? [];
|
|
314
371
|
}
|
|
@@ -327,10 +384,45 @@ async function secretNames(app) {
|
|
|
327
384
|
const secrets = (await flyJson(['secrets', 'list', '-a', app])) ?? [];
|
|
328
385
|
return secrets.map((s) => s.Name ?? s.name ?? '').filter(Boolean);
|
|
329
386
|
}
|
|
387
|
+
exports.MOCHI_DEPLOY = {
|
|
388
|
+
imageRepo: exports.IMAGE_REPO,
|
|
389
|
+
version: ownVersion,
|
|
390
|
+
buildContext: () => ({ dir: sourceRoot(), cleanup: () => undefined }),
|
|
391
|
+
volumeName: 'vault',
|
|
392
|
+
mountPath: '/vault',
|
|
393
|
+
lfsBucket: true,
|
|
394
|
+
concurrency: { type: 'requests', soft: 200, hard: 250 },
|
|
395
|
+
contents: 'repositories, issues, pull requests, users',
|
|
396
|
+
firstSteps: (url, username) => [
|
|
397
|
+
`To administer the vault in a browser, open its sign-in page and give that token as`,
|
|
398
|
+
`'${username}':`,
|
|
399
|
+
'',
|
|
400
|
+
` ${url}/login`,
|
|
401
|
+
'',
|
|
402
|
+
'The form asks for a username and a token, since a vault has no passwords. From',
|
|
403
|
+
'there the Admin page creates the users and the repositories, which is the usual',
|
|
404
|
+
'way to bootstrap a fresh vault.',
|
|
405
|
+
'',
|
|
406
|
+
"To use the CLI and git instead, hand the same token to git's credential store,",
|
|
407
|
+
'which is what login is for:',
|
|
408
|
+
'',
|
|
409
|
+
` mochi login ${url}`,
|
|
410
|
+
'',
|
|
411
|
+
'It asks for the token without echoing it, checks it, and remembers this vault, so',
|
|
412
|
+
'these need no arguments afterwards and git stops asking on a push:',
|
|
413
|
+
'',
|
|
414
|
+
' mochi whoami',
|
|
415
|
+
' mochi user add alice',
|
|
416
|
+
` mochi import https://github.com/someone/something.git mine`,
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
function volumeOf(app, profile) {
|
|
420
|
+
return namedVolume(app, profile.volumeName);
|
|
421
|
+
}
|
|
330
422
|
/** What Fly currently has, so that a flag-less redeploy changes nothing and one flag changes one thing. */
|
|
331
|
-
async function liveSettings(app) {
|
|
423
|
+
async function liveSettings(app, profile) {
|
|
332
424
|
const out = {};
|
|
333
|
-
const vol = await
|
|
425
|
+
const vol = await volumeOf(app, profile);
|
|
334
426
|
if (vol) {
|
|
335
427
|
out.region = vol.region;
|
|
336
428
|
out.volumeGb = vol.size_gb;
|
|
@@ -362,21 +454,22 @@ function resolveSettings(a, live) {
|
|
|
362
454
|
// auto-start here. A second machine would mean a second volume and a second
|
|
363
455
|
// vault, diverging silently from the first. For the same reason, a busier vault
|
|
364
456
|
// wants a bigger machine rather than more of them.
|
|
365
|
-
function flyToml(app, s) {
|
|
366
|
-
|
|
457
|
+
function flyToml(app, s, profile) {
|
|
458
|
+
const c = profile.concurrency;
|
|
459
|
+
return `# Generated by ${naming_1.naming.product} deploy for '${app}'. Written to a temporary
|
|
367
460
|
# directory for the length of one deploy; edit the deploy command, not this.
|
|
368
461
|
app = "${app}"
|
|
369
462
|
primary_region = "${s.region}"
|
|
370
463
|
|
|
371
464
|
[mounts]
|
|
372
|
-
source = "${
|
|
373
|
-
destination = "
|
|
465
|
+
source = "${profile.volumeName}"
|
|
466
|
+
destination = "${profile.mountPath}"
|
|
374
467
|
|
|
375
468
|
# Fly always terminates TLS in front, so the forwarded headers are the only place
|
|
376
|
-
# the real scheme and address appear. The server records this in the
|
|
469
|
+
# the real scheme and address appear. The server records this in the ${naming_1.naming.rootNoun}'s
|
|
377
470
|
# config.json on the next start, where it can be changed by hand afterwards.
|
|
378
471
|
[env]
|
|
379
|
-
|
|
472
|
+
${naming_1.naming.envPrefix}_TRUST_PROXY = "1"
|
|
380
473
|
|
|
381
474
|
[http_service]
|
|
382
475
|
internal_port = ${INTERNAL_PORT}
|
|
@@ -385,9 +478,9 @@ primary_region = "${s.region}"
|
|
|
385
478
|
auto_start_machines = true
|
|
386
479
|
min_machines_running = 0
|
|
387
480
|
[http_service.concurrency]
|
|
388
|
-
type = "
|
|
389
|
-
hard_limit =
|
|
390
|
-
soft_limit =
|
|
481
|
+
type = "${c.type}"
|
|
482
|
+
hard_limit = ${c.hard}
|
|
483
|
+
soft_limit = ${c.soft}
|
|
391
484
|
|
|
392
485
|
[[vm]]
|
|
393
486
|
cpu_kind = "${s.cpuKind}"
|
|
@@ -395,10 +488,10 @@ primary_region = "${s.region}"
|
|
|
395
488
|
memory = "${s.memory}"
|
|
396
489
|
`;
|
|
397
490
|
}
|
|
398
|
-
function writeTempConfig(app, s) {
|
|
399
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
491
|
+
function writeTempConfig(app, s, profile) {
|
|
492
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${naming_1.naming.product}-deploy-`));
|
|
400
493
|
const file = path.join(dir, 'fly.toml');
|
|
401
|
-
fs.writeFileSync(file, flyToml(app, s));
|
|
494
|
+
fs.writeFileSync(file, flyToml(app, s, profile));
|
|
402
495
|
return file;
|
|
403
496
|
}
|
|
404
497
|
function appUrl(app) {
|
|
@@ -411,6 +504,7 @@ function appUrl(app) {
|
|
|
411
504
|
* that was already initialized answers 401.
|
|
412
505
|
*/
|
|
413
506
|
async function waitForVault(url, token, seconds = 120) {
|
|
507
|
+
const noun = naming_1.naming.rootNoun;
|
|
414
508
|
const deadline = Date.now() + seconds * 1000;
|
|
415
509
|
let last = 'no answer yet';
|
|
416
510
|
while (Date.now() < deadline) {
|
|
@@ -420,12 +514,12 @@ async function waitForVault(url, token, seconds = 120) {
|
|
|
420
514
|
const data = (await resp.json());
|
|
421
515
|
if (typeof data.username === 'string' && data.username)
|
|
422
516
|
return { ok: true, username: data.username };
|
|
423
|
-
last =
|
|
517
|
+
last = `the ${noun} answered without saying who the token belongs to`;
|
|
424
518
|
}
|
|
425
519
|
else if (resp.status === 401 || resp.status === 403) {
|
|
426
520
|
// Conclusive rather than worth retrying: the server is up and has
|
|
427
521
|
// rejected this token, which means the vault was initialized before.
|
|
428
|
-
return { ok: false, reason: `the
|
|
522
|
+
return { ok: false, reason: `the ${noun} did not accept the new owner token (HTTP ${resp.status})` };
|
|
429
523
|
}
|
|
430
524
|
else {
|
|
431
525
|
last = `HTTP ${resp.status} from ${url}`;
|
|
@@ -438,16 +532,22 @@ async function waitForVault(url, token, seconds = 120) {
|
|
|
438
532
|
}
|
|
439
533
|
return { ok: false, reason: `timed out after ${seconds}s: ${last}` };
|
|
440
534
|
}
|
|
441
|
-
async function deployFlyCmd(args, usage) {
|
|
535
|
+
async function deployFlyCmd(args, usage, profile = exports.MOCHI_DEPLOY) {
|
|
536
|
+
const product = naming_1.naming.product;
|
|
537
|
+
const noun = naming_1.naming.rootNoun;
|
|
538
|
+
const ownerTokenSecret = `${naming_1.naming.envPrefix}_OWNER_TOKEN`;
|
|
442
539
|
const a = parseDeployArgs(args, usage);
|
|
443
540
|
if (!a.app) {
|
|
444
541
|
die('Which app? Fly app names are globally unique, and the name becomes the URL:\n\n' +
|
|
445
|
-
|
|
542
|
+
` ${product} deploy fly my-${noun}-name -> https://my-${noun}-name.fly.dev\n`);
|
|
446
543
|
}
|
|
447
544
|
const app = a.app;
|
|
448
545
|
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(app)) {
|
|
449
546
|
die(`Not a valid Fly app name: ${app}\nUse lowercase letters, digits, and dashes.`);
|
|
450
547
|
}
|
|
548
|
+
if (a.lfsBucket && !profile.lfsBucket) {
|
|
549
|
+
die(`--lfs-bucket provisions storage for Git LFS objects, and a ${noun} keeps none.`);
|
|
550
|
+
}
|
|
451
551
|
// What gets deployed: a published image to pull, or this checkout to build.
|
|
452
552
|
// Settled from the flags before anything reaches the network, so a
|
|
453
553
|
// contradiction between them costs no round trip and creates no app.
|
|
@@ -457,18 +557,28 @@ async function deployFlyCmd(args, usage) {
|
|
|
457
557
|
if (a.localBuild && !a.fromSource) {
|
|
458
558
|
die('--local-build says how to build, and without --from-source there is nothing to build.');
|
|
459
559
|
}
|
|
460
|
-
|
|
461
|
-
|
|
560
|
+
// The build context is settled here, before anything reaches the network,
|
|
561
|
+
// so a machine that cannot build (an installed package rather than a
|
|
562
|
+
// checkout) is told so before an app exists. An application that assembles
|
|
563
|
+
// a context in a temporary directory gets it removed however this command
|
|
564
|
+
// ends, since die() exits without unwinding.
|
|
565
|
+
const build = a.fromSource ? profile.buildContext() : null;
|
|
566
|
+
if (build)
|
|
567
|
+
process.on('exit', () => build.cleanup());
|
|
568
|
+
const buildRoot = build?.dir ?? null;
|
|
569
|
+
const image = buildRoot === null ? a.image ?? `${profile.imageRepo}:${profile.version()}` : null;
|
|
570
|
+
if (image !== null && a.image === null)
|
|
571
|
+
await requirePublishedImage(image, false);
|
|
462
572
|
await requireFly();
|
|
463
573
|
const existed = await appExists(app);
|
|
464
|
-
const live = existed ? await liveSettings(app) : {};
|
|
574
|
+
const live = existed ? await liveSettings(app, profile) : {};
|
|
465
575
|
const settings = resolveSettings(a, live);
|
|
466
576
|
// A volume cannot move, so a region flag that disagrees with the volume that
|
|
467
577
|
// exists is a request this cannot carry out. Saying so beats deploying a
|
|
468
578
|
// machine in one region that can never attach the disk in another.
|
|
469
579
|
if (existed && live.region && a.region && a.region !== live.region) {
|
|
470
|
-
die(`This
|
|
471
|
-
|
|
580
|
+
die(`This ${noun}'s volume is in ${live.region}, and a volume cannot be moved to ${a.region}.\n` +
|
|
581
|
+
`Deploying to another region means a new ${noun} and copying the data across.`);
|
|
472
582
|
}
|
|
473
583
|
if (existed) {
|
|
474
584
|
console.log(`==> Updating '${app}' (${appUrl(app)})`);
|
|
@@ -481,13 +591,13 @@ async function deployFlyCmd(args, usage) {
|
|
|
481
591
|
'App names are globally unique, so a name in use by anyone stops this. Try another.');
|
|
482
592
|
}
|
|
483
593
|
}
|
|
484
|
-
const vol = await
|
|
594
|
+
const vol = await volumeOf(app, profile);
|
|
485
595
|
if (!vol) {
|
|
486
|
-
console.log(`==> Creating a ${settings.volumeGb}GB volume '${
|
|
596
|
+
console.log(`==> Creating a ${settings.volumeGb}GB volume '${profile.volumeName}' in ${settings.region}`);
|
|
487
597
|
const code = await flyStream([
|
|
488
598
|
'volumes',
|
|
489
599
|
'create',
|
|
490
|
-
|
|
600
|
+
profile.volumeName,
|
|
491
601
|
'-a',
|
|
492
602
|
app,
|
|
493
603
|
'--region',
|
|
@@ -497,10 +607,10 @@ async function deployFlyCmd(args, usage) {
|
|
|
497
607
|
'--yes',
|
|
498
608
|
]);
|
|
499
609
|
if (code !== 0)
|
|
500
|
-
die(
|
|
610
|
+
die(`\nCould not create the volume, so there is nowhere to keep the ${noun}.`);
|
|
501
611
|
}
|
|
502
612
|
else if (settings.volumeGb > vol.size_gb) {
|
|
503
|
-
console.log(`==> Extending volume '${
|
|
613
|
+
console.log(`==> Extending volume '${profile.volumeName}' from ${vol.size_gb}GB to ${settings.volumeGb}GB`);
|
|
504
614
|
const code = await flyStream(['volumes', 'extend', vol.id, '-a', app, '--size', String(settings.volumeGb)]);
|
|
505
615
|
if (code !== 0)
|
|
506
616
|
die('\nCould not extend the volume.');
|
|
@@ -558,7 +668,7 @@ async function deployFlyCmd(args, usage) {
|
|
|
558
668
|
console.log('==> Setting the one-time owner token as a Fly secret');
|
|
559
669
|
// `secrets import` reads KEY=VALUE lines from stdin, which keeps the token
|
|
560
670
|
// off the child's argv and so out of `ps`.
|
|
561
|
-
const r = await fly(['secrets', 'import', '-a', app, '--stage'], `${
|
|
671
|
+
const r = await fly(['secrets', 'import', '-a', app, '--stage'], `${ownerTokenSecret}=${ownerToken}\n`);
|
|
562
672
|
if (r.code !== 0)
|
|
563
673
|
die(`Could not set the owner token secret:\n${r.stderr.trim() || r.stdout.trim()}`);
|
|
564
674
|
// From here the token exists in two places: this process, and a Fly secret
|
|
@@ -576,16 +686,18 @@ async function deployFlyCmd(args, usage) {
|
|
|
576
686
|
// deploy. The one message that must not be lost cannot go out that way.
|
|
577
687
|
fs.writeSync(2, '\nThe owner token this run staged as a Fly secret, shown here because nothing\n' +
|
|
578
688
|
'else has a copy of it. A Fly secret can be written but not read back, and this\n' +
|
|
579
|
-
|
|
689
|
+
`token is the owner of the ${noun} if this app initialized one:\n` +
|
|
580
690
|
`\n ${ownerToken}\n` +
|
|
581
|
-
`\nKeep it, then:
|
|
691
|
+
`\nKeep it, then sign the CLI in with it: ${product} login ${appUrl(app)}\n` +
|
|
692
|
+
'(paste the token when asked; --token-stdin takes it from a pipe, and --token\n' +
|
|
693
|
+
'on the command line would leave it in shell history).\n');
|
|
582
694
|
});
|
|
583
695
|
}
|
|
584
|
-
const config = writeTempConfig(app, settings);
|
|
585
|
-
// A source build runs fly in the
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
696
|
+
const config = writeTempConfig(app, settings, profile);
|
|
697
|
+
// A source build runs fly in the build context, so fly finds its Dockerfile
|
|
698
|
+
// without being told where it is. --config still points at the generated
|
|
699
|
+
// fly.toml in a temporary directory, which is why there is no fly.toml in the
|
|
700
|
+
// repository for a build to pick up by accident.
|
|
589
701
|
//
|
|
590
702
|
// Without --local-only, flyctl builds on a Fly builder machine, which needs no
|
|
591
703
|
// Docker here and provisions a builder app on first use. --local-build asks for
|
|
@@ -597,17 +709,22 @@ async function deployFlyCmd(args, usage) {
|
|
|
597
709
|
else {
|
|
598
710
|
console.log(`==> Deploying ${image}`);
|
|
599
711
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
712
|
+
let code;
|
|
713
|
+
try {
|
|
714
|
+
code = await flyStream([
|
|
715
|
+
'deploy',
|
|
716
|
+
'--app',
|
|
717
|
+
app,
|
|
718
|
+
'--config',
|
|
719
|
+
config,
|
|
720
|
+
...(source ? (a.localBuild ? ['--local-only'] : []) : ['--image', image]),
|
|
721
|
+
'--ha=false',
|
|
722
|
+
'--yes',
|
|
723
|
+
], buildRoot ?? undefined);
|
|
724
|
+
}
|
|
725
|
+
finally {
|
|
726
|
+
fs.rmSync(path.dirname(config), { recursive: true, force: true });
|
|
727
|
+
}
|
|
611
728
|
if (code !== 0) {
|
|
612
729
|
console.error('');
|
|
613
730
|
console.error('The deploy failed. The app and the volume survive, so fix the cause and run the');
|
|
@@ -619,7 +736,7 @@ async function deployFlyCmd(args, usage) {
|
|
|
619
736
|
// the exit hook is about to print it.
|
|
620
737
|
console.error('');
|
|
621
738
|
console.error('The retry may not mint a token of its own, because the one shown at the end of');
|
|
622
|
-
console.error(
|
|
739
|
+
console.error(`this output is already the token the ${noun} will be initialized with. Keep it,`);
|
|
623
740
|
console.error('and log in with it once a deploy succeeds.');
|
|
624
741
|
}
|
|
625
742
|
if (source) {
|
|
@@ -641,7 +758,7 @@ async function deployFlyCmd(args, usage) {
|
|
|
641
758
|
if (!ownerToken) {
|
|
642
759
|
console.log(`==> Deployed: ${url}`);
|
|
643
760
|
console.log('');
|
|
644
|
-
console.log(
|
|
761
|
+
console.log(`The ${noun} it serves is whichever ${noun} was already on the volume, users and all.`);
|
|
645
762
|
// No token was minted because a machine had run before, which usually means
|
|
646
763
|
// a vault that has been in use and an operator who is already logged in.
|
|
647
764
|
// With nothing stored here, the other reading is possible: an earlier
|
|
@@ -650,18 +767,18 @@ async function deployFlyCmd(args, usage) {
|
|
|
650
767
|
// case where it might be the answer.
|
|
651
768
|
if (!(await (0, credentials_1.readCredential)((0, credentials_1.credentialTarget)(url)))) {
|
|
652
769
|
console.log('');
|
|
653
|
-
console.log(
|
|
770
|
+
console.log(`No token for it is stored on this machine, so log in with one the ${noun} knows:`);
|
|
654
771
|
console.log('');
|
|
655
|
-
console.log(`
|
|
772
|
+
console.log(` ${product} login ${url}`);
|
|
656
773
|
console.log('');
|
|
657
|
-
console.log(
|
|
774
|
+
console.log(`If an earlier deploy of this app failed, the ${noun} was initialized just now with`);
|
|
658
775
|
console.log('the owner token that run printed, and that is the token to use.');
|
|
659
776
|
console.log('');
|
|
660
777
|
}
|
|
661
778
|
console.log(` fly logs -a ${app}`);
|
|
662
779
|
return;
|
|
663
780
|
}
|
|
664
|
-
console.log(
|
|
781
|
+
console.log(`==> Waiting for the ${noun} to answer`);
|
|
665
782
|
const ready = await waitForVault(url, ownerToken);
|
|
666
783
|
if (!ready.ok) {
|
|
667
784
|
console.error('');
|
|
@@ -672,10 +789,10 @@ async function deployFlyCmd(args, usage) {
|
|
|
672
789
|
// a vault that was initialized by an earlier machine. Its own tokens are
|
|
673
790
|
// still the way in, and no token minted here will ever work on it.
|
|
674
791
|
console.error('');
|
|
675
|
-
console.error(
|
|
792
|
+
console.error(`If this app has served a ${noun} before, that ${noun} keeps the users and tokens it`);
|
|
676
793
|
console.error('already had, and a token minted now is not one of them. Log in with one of those:');
|
|
677
794
|
console.error('');
|
|
678
|
-
console.error(`
|
|
795
|
+
console.error(` ${product} login ${url}`);
|
|
679
796
|
}
|
|
680
797
|
// The token this run minted is printed on the way out by the exit hook,
|
|
681
798
|
// since a vault that has not answered yet may still adopt it.
|
|
@@ -685,12 +802,12 @@ async function deployFlyCmd(args, usage) {
|
|
|
685
802
|
// left the operator holding a vault whose token they had never seen, which is
|
|
686
803
|
// no way to sign in to the web UI and nothing to keep anywhere; and the token
|
|
687
804
|
// cannot be recovered later, since the server keeps only its hash and a Fly
|
|
688
|
-
// secret cannot be read back. So it is printed once, with the
|
|
689
|
-
// it, and `
|
|
805
|
+
// secret cannot be read back. So it is printed once, with the ways to use
|
|
806
|
+
// it, and `login` stays the one thing that stores a credential.
|
|
690
807
|
console.log('');
|
|
691
808
|
console.log(`==> Ready: ${url}`);
|
|
692
809
|
console.log('');
|
|
693
|
-
console.log(`The
|
|
810
|
+
console.log(`The ${noun} is initialized, and '${ready.username}' owns it. This is its token, shown`);
|
|
694
811
|
console.log('here once and nowhere else: the server keeps only its hash, and the Fly secret it');
|
|
695
812
|
console.log('was staged in cannot be read back. Keep it somewhere safe now.');
|
|
696
813
|
console.log('');
|
|
@@ -699,56 +816,40 @@ async function deployFlyCmd(args, usage) {
|
|
|
699
816
|
// operator, and stdout can fail (a closed pipe) between here and there.
|
|
700
817
|
tokenDelivered = true;
|
|
701
818
|
console.log('');
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
console.log('');
|
|
705
|
-
console.log(` ${url}/login`);
|
|
706
|
-
console.log('');
|
|
707
|
-
console.log('The form asks for a username and a token, since a vault has no passwords. From');
|
|
708
|
-
console.log('there the Admin page creates the users and the repositories, which is the usual');
|
|
709
|
-
console.log('way to bootstrap a fresh vault.');
|
|
710
|
-
console.log('');
|
|
711
|
-
console.log('To use the CLI and git instead, hand the same token to git\'s credential store,');
|
|
712
|
-
console.log('which is what login is for:');
|
|
713
|
-
console.log('');
|
|
714
|
-
console.log(` mochi login ${url}`);
|
|
715
|
-
console.log('');
|
|
716
|
-
console.log('It asks for the token without echoing it, checks it, and remembers this vault, so');
|
|
717
|
-
console.log('these need no arguments afterwards and git stops asking on a push:');
|
|
718
|
-
console.log('');
|
|
719
|
-
console.log(' mochi whoami');
|
|
720
|
-
console.log(" mochi user add alice --scope 'alice/*'");
|
|
721
|
-
console.log(` mochi import https://github.com/someone/something.git mine`);
|
|
819
|
+
for (const line of profile.firstSteps(url, ready.username))
|
|
820
|
+
console.log(line);
|
|
722
821
|
console.log('');
|
|
723
822
|
console.log('Deploy an update, or change a setting, with the same command:');
|
|
724
823
|
console.log('');
|
|
725
|
-
console.log(`
|
|
726
|
-
console.log(`
|
|
824
|
+
console.log(` ${product} deploy fly ${app}`);
|
|
825
|
+
console.log(` ${product} deploy fly ${app} --volume 50 --vm-memory 1gb`);
|
|
727
826
|
}
|
|
728
|
-
async function deployShowCmd(args, usage) {
|
|
827
|
+
async function deployShowCmd(args, usage, profile = exports.MOCHI_DEPLOY) {
|
|
828
|
+
const product = naming_1.naming.product;
|
|
829
|
+
const noun = naming_1.naming.rootNoun;
|
|
729
830
|
const a = parseDeployArgs(args, usage);
|
|
730
831
|
if (!a.app)
|
|
731
|
-
die(
|
|
732
|
-
rejectFlyFlags(a,
|
|
832
|
+
die(`Which app? Usage: ${product} deploy fly show <app>`);
|
|
833
|
+
rejectFlyFlags(a, `${product} deploy fly show <app>`, false);
|
|
733
834
|
const app = a.app;
|
|
734
835
|
await requireFly();
|
|
735
836
|
if (!(await appExists(app))) {
|
|
736
837
|
die(`No Fly app named '${app}' that you can see. Check the name, or: fly apps list`);
|
|
737
838
|
}
|
|
738
839
|
const url = appUrl(app);
|
|
739
|
-
const vol = await
|
|
840
|
+
const vol = await volumeOf(app, profile);
|
|
740
841
|
const ms = await machines(app);
|
|
741
842
|
const secrets = await secretNames(app);
|
|
742
843
|
console.log(`${app} ${url}`);
|
|
743
844
|
console.log('');
|
|
744
845
|
if (ms.length === 0) {
|
|
745
|
-
console.log(
|
|
846
|
+
console.log(` machines none, so nothing is serving this ${noun}`);
|
|
746
847
|
}
|
|
747
848
|
else {
|
|
748
849
|
// More than one machine is worth naming rather than summarizing: it means
|
|
749
850
|
// two volumes and two vaults, which is the failure --ha=false prevents.
|
|
750
851
|
if (ms.length > 1)
|
|
751
|
-
console.log(` machines ${ms.length}, which is one too many for a single-volume
|
|
852
|
+
console.log(` machines ${ms.length}, which is one too many for a single-volume ${noun}`);
|
|
752
853
|
for (const m of ms) {
|
|
753
854
|
const g = m.config?.guest;
|
|
754
855
|
const shape = g ? `${g.cpu_kind}-cpu-${g.cpus}x, ${g.memory_mb}mb` : 'unknown shape';
|
|
@@ -757,34 +858,37 @@ async function deployShowCmd(args, usage) {
|
|
|
757
858
|
}
|
|
758
859
|
}
|
|
759
860
|
console.log(vol ? ` volume ${vol.size_gb}GB in ${vol.region} (${vol.state ?? 'created'})` : ' volume none');
|
|
760
|
-
|
|
861
|
+
if (profile.lfsBucket) {
|
|
862
|
+
console.log(` lfs ${secrets.includes('BUCKET_NAME') ? 'objects in a bucket (BUCKET_NAME is set)' : 'objects on the volume'}`);
|
|
863
|
+
}
|
|
761
864
|
// Whether it works, which is the question `fly status` cannot answer. A
|
|
762
865
|
// stored credential turns this into a report of who you are on it.
|
|
763
866
|
const target = (0, credentials_1.credentialTarget)(url);
|
|
764
867
|
const stored = await (0, credentials_1.readCredential)(target);
|
|
765
|
-
let
|
|
868
|
+
let status = 'not reachable';
|
|
766
869
|
try {
|
|
767
870
|
const resp = await fetch(`${url}/api/whoami`, {
|
|
768
871
|
headers: stored ? { authorization: `Bearer ${stored.password}` } : {},
|
|
769
872
|
});
|
|
770
873
|
if (resp.ok) {
|
|
771
874
|
const data = (await resp.json());
|
|
772
|
-
|
|
875
|
+
status = `answering, and you are '${String(data.username)}' on it`;
|
|
773
876
|
}
|
|
774
877
|
else if (resp.status === 401) {
|
|
775
|
-
|
|
878
|
+
status = stored ? 'answering, but your stored token is not valid on it' : 'answering (no token stored here)';
|
|
776
879
|
}
|
|
777
880
|
else {
|
|
778
|
-
|
|
881
|
+
status = `answering with HTTP ${resp.status}`;
|
|
779
882
|
}
|
|
780
883
|
}
|
|
781
884
|
catch (e) {
|
|
782
|
-
|
|
885
|
+
status = `not reachable: ${e instanceof Error ? e.message : e}`;
|
|
783
886
|
}
|
|
784
|
-
console.log(`
|
|
887
|
+
console.log(` ${noun.padEnd(9)} ${status}`);
|
|
785
888
|
const saved = (0, credentials_1.loadLogin)();
|
|
786
|
-
if (saved && saved.host.replace(/\/+$/, '') === url)
|
|
787
|
-
console.log(
|
|
889
|
+
if (saved && saved.host.replace(/\/+$/, '') === url) {
|
|
890
|
+
console.log(` login this is the ${noun} ${product} commands use`);
|
|
891
|
+
}
|
|
788
892
|
// Fly's own volume snapshots live at the same provider as the volume, so they
|
|
789
893
|
// are a complement to a backup on a disk of your own rather than a substitute
|
|
790
894
|
// for one. Whether this machine keeps such a copy is worth one line.
|
|
@@ -793,7 +897,7 @@ async function deployShowCmd(args, usage) {
|
|
|
793
897
|
// certainly backed up by that name rather than by <app>.fly.dev, and a report
|
|
794
898
|
// that said "none" in that case would be worse than no report at all.
|
|
795
899
|
const backup = (0, backup_cmd_1.backupLineFor)([url, ...(await certHostnames(app)).map((h) => `https://${h}`)]);
|
|
796
|
-
console.log(backup ? ` backup ${backup}` :
|
|
900
|
+
console.log(backup ? ` backup ${backup}` : ` backup none on this machine (${product} backup <dir>)`);
|
|
797
901
|
console.log('');
|
|
798
902
|
console.log(` fly logs -a ${app}`);
|
|
799
903
|
}
|
|
@@ -814,21 +918,22 @@ function promptLine(prompt) {
|
|
|
814
918
|
process.stdin.on('data', onData);
|
|
815
919
|
});
|
|
816
920
|
}
|
|
817
|
-
async function deployDestroyCmd(args, usage) {
|
|
921
|
+
async function deployDestroyCmd(args, usage, profile = exports.MOCHI_DEPLOY) {
|
|
922
|
+
const product = naming_1.naming.product;
|
|
818
923
|
const a = parseDeployArgs(args, usage);
|
|
819
924
|
if (!a.app)
|
|
820
|
-
die(
|
|
821
|
-
rejectFlyFlags(a,
|
|
925
|
+
die(`Which app? Usage: ${product} deploy fly destroy <app> [--yes]`);
|
|
926
|
+
rejectFlyFlags(a, `${product} deploy fly destroy <app> [--yes]`, true);
|
|
822
927
|
const app = a.app;
|
|
823
928
|
await requireFly();
|
|
824
929
|
if (!(await appExists(app))) {
|
|
825
930
|
die(`No Fly app named '${app}' that you can see. Check the name, or: fly apps list`);
|
|
826
931
|
}
|
|
827
|
-
const vol = await
|
|
828
|
-
const hadBucket = (await secretNames(app)).includes('BUCKET_NAME');
|
|
932
|
+
const vol = await volumeOf(app, profile);
|
|
933
|
+
const hadBucket = profile.lfsBucket && (await secretNames(app)).includes('BUCKET_NAME');
|
|
829
934
|
if (!a.yes) {
|
|
830
935
|
console.log(`This destroys the Fly app '${app}' and its ${vol ? `${vol.size_gb}GB ` : ''}volume.`);
|
|
831
|
-
console.log(
|
|
936
|
+
console.log(`Everything in the ${naming_1.naming.rootNoun} goes with it: ${profile.contents}.`);
|
|
832
937
|
console.log('There is no undo, and Fly keeps no backup of a destroyed volume.');
|
|
833
938
|
console.log('');
|
|
834
939
|
const answer = await promptLine(`Type the app name to confirm: `);
|