@cardelli/ambit 0.1.5 → 0.2.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 +261 -0
- package/esm/cli/commands/create/index.d.ts +2 -0
- package/esm/cli/commands/create/index.d.ts.map +1 -0
- package/esm/cli/commands/create/index.js +292 -0
- package/esm/cli/commands/create/machine.d.ts +33 -0
- package/esm/cli/commands/create/machine.d.ts.map +1 -0
- package/esm/cli/commands/create/machine.js +162 -0
- package/esm/cli/commands/deploy/index.d.ts +2 -0
- package/esm/cli/commands/deploy/index.d.ts.map +1 -0
- package/esm/cli/commands/deploy/index.js +290 -0
- package/esm/cli/commands/deploy/machine.d.ts +52 -0
- package/esm/cli/commands/deploy/machine.d.ts.map +1 -0
- package/esm/cli/commands/deploy/machine.js +116 -0
- package/esm/cli/commands/deploy/modes.d.ts +18 -0
- package/esm/cli/commands/deploy/modes.d.ts.map +1 -0
- package/esm/cli/commands/deploy/modes.js +152 -0
- package/esm/cli/commands/destroy/app.d.ts +2 -0
- package/esm/cli/commands/destroy/app.d.ts.map +1 -0
- package/esm/cli/commands/destroy/app.js +173 -0
- package/esm/cli/commands/destroy/index.d.ts +2 -0
- package/esm/cli/commands/destroy/index.d.ts.map +1 -0
- package/esm/cli/commands/destroy/index.js +63 -0
- package/esm/cli/commands/destroy/network.d.ts +2 -0
- package/esm/cli/commands/destroy/network.d.ts.map +1 -0
- package/esm/cli/commands/destroy/network.js +210 -0
- package/esm/cli/commands/doctor.d.ts.map +1 -0
- package/esm/cli/commands/doctor.js +295 -0
- package/esm/{src/cli → cli}/commands/list.d.ts.map +1 -1
- package/esm/{src/cli → cli}/commands/list.js +39 -54
- package/esm/cli/commands/status.d.ts.map +1 -0
- package/esm/cli/commands/status.js +331 -0
- package/esm/cli/mod.d.ts.map +1 -0
- package/esm/{src/cli → cli}/mod.js +4 -4
- package/esm/deno.d.ts +4 -18
- package/esm/deno.js +5 -19
- package/esm/deps/jsr.io/@std/path/1.1.4/constants.d.ts +1 -1
- package/esm/lib/args.d.ts +11 -0
- package/esm/lib/args.d.ts.map +1 -0
- package/esm/lib/args.js +28 -0
- package/esm/lib/cli.d.ts +0 -1
- package/esm/lib/cli.d.ts.map +1 -1
- package/esm/lib/cli.js +0 -1
- package/esm/lib/command.d.ts +0 -3
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +2 -13
- package/esm/lib/machine.d.ts +11 -0
- package/esm/lib/machine.d.ts.map +1 -0
- package/esm/lib/machine.js +15 -0
- package/esm/lib/output.d.ts +2 -1
- package/esm/lib/output.d.ts.map +1 -1
- package/esm/lib/output.js +21 -3
- package/esm/lib/result.d.ts +0 -1
- package/esm/lib/result.d.ts.map +1 -1
- package/esm/lib/result.js +0 -1
- package/esm/main.d.ts +6 -6
- package/esm/main.d.ts.map +1 -1
- package/esm/main.js +7 -9
- package/esm/providers/fly.d.ts +81 -0
- package/esm/providers/fly.d.ts.map +1 -0
- package/esm/providers/fly.js +372 -0
- package/esm/providers/tailscale.d.ts +31 -0
- package/esm/providers/tailscale.d.ts.map +1 -0
- package/esm/providers/tailscale.js +150 -0
- package/esm/{src/schemas → schemas}/fly.d.ts +1 -11
- package/esm/schemas/fly.d.ts.map +1 -0
- package/esm/{src/schemas → schemas}/fly.js +14 -56
- package/esm/{src/schemas → schemas}/tailscale.d.ts +1 -2
- package/esm/schemas/tailscale.d.ts.map +1 -0
- package/esm/{src/schemas → schemas}/tailscale.js +2 -3
- package/esm/src/{docker/router → router}/Dockerfile +0 -11
- package/esm/src/{docker/router → router}/start.sh +18 -9
- package/esm/util/constants.d.ts +13 -0
- package/esm/util/constants.d.ts.map +1 -0
- package/esm/util/constants.js +34 -0
- package/esm/{src → util}/credentials.d.ts +0 -1
- package/esm/util/credentials.d.ts.map +1 -0
- package/esm/{src → util}/credentials.js +3 -5
- package/esm/{src → util}/discovery.d.ts +16 -3
- package/esm/util/discovery.d.ts.map +1 -0
- package/esm/{src → util}/discovery.js +24 -15
- package/esm/util/fly-transforms.d.ts +27 -0
- package/esm/util/fly-transforms.d.ts.map +1 -0
- package/esm/util/fly-transforms.js +87 -0
- package/esm/{src → util}/guard.d.ts +1 -2
- package/esm/util/guard.d.ts.map +1 -0
- package/esm/{src → util}/guard.js +27 -27
- package/esm/util/naming.d.ts +5 -0
- package/esm/util/naming.d.ts.map +1 -0
- package/esm/util/naming.js +12 -0
- package/esm/{src → util}/resolve.d.ts +2 -3
- package/esm/util/resolve.d.ts.map +1 -0
- package/esm/{src → util}/resolve.js +1 -2
- package/esm/util/session.d.ts +16 -0
- package/esm/util/session.d.ts.map +1 -0
- package/esm/util/session.js +19 -0
- package/esm/util/tailscale-local.d.ts +13 -0
- package/esm/util/tailscale-local.d.ts.map +1 -0
- package/esm/util/tailscale-local.js +63 -0
- package/esm/{src → util}/template.d.ts +0 -1
- package/esm/util/template.d.ts.map +1 -0
- package/esm/{src → util}/template.js +0 -1
- package/package.json +1 -49
- package/esm/lib/paths.d.ts +0 -3
- package/esm/lib/paths.d.ts.map +0 -1
- package/esm/lib/paths.js +0 -5
- package/esm/src/cli/commands/create.d.ts +0 -2
- package/esm/src/cli/commands/create.d.ts.map +0 -1
- package/esm/src/cli/commands/create.js +0 -308
- package/esm/src/cli/commands/deploy.d.ts +0 -2
- package/esm/src/cli/commands/deploy.d.ts.map +0 -1
- package/esm/src/cli/commands/deploy.js +0 -430
- package/esm/src/cli/commands/destroy.d.ts +0 -2
- package/esm/src/cli/commands/destroy.d.ts.map +0 -1
- package/esm/src/cli/commands/destroy.js +0 -340
- package/esm/src/cli/commands/doctor.d.ts.map +0 -1
- package/esm/src/cli/commands/doctor.js +0 -141
- package/esm/src/cli/commands/status.d.ts.map +0 -1
- package/esm/src/cli/commands/status.js +0 -152
- package/esm/src/cli/mod.d.ts.map +0 -1
- package/esm/src/credentials.d.ts.map +0 -1
- package/esm/src/discovery.d.ts.map +0 -1
- package/esm/src/guard.d.ts.map +0 -1
- package/esm/src/providers/fly.d.ts +0 -76
- package/esm/src/providers/fly.d.ts.map +0 -1
- package/esm/src/providers/fly.js +0 -407
- package/esm/src/providers/tailscale.d.ts +0 -31
- package/esm/src/providers/tailscale.d.ts.map +0 -1
- package/esm/src/providers/tailscale.js +0 -189
- package/esm/src/resolve.d.ts.map +0 -1
- package/esm/src/schemas/config.d.ts +0 -5
- package/esm/src/schemas/config.d.ts.map +0 -1
- package/esm/src/schemas/config.js +0 -22
- package/esm/src/schemas/fly.d.ts.map +0 -1
- package/esm/src/schemas/tailscale.d.ts.map +0 -1
- package/esm/src/template.d.ts.map +0 -1
- /package/esm/{src/cli → cli}/commands/doctor.d.ts +0 -0
- /package/esm/{src/cli → cli}/commands/list.d.ts +0 -0
- /package/esm/{src/cli → cli}/commands/status.d.ts +0 -0
- /package/esm/{src/cli → cli}/mod.d.ts +0 -0
- /package/esm/src/{docker/router → router}/fly.toml +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Deploy Modes — Image, Config, and Template Resolution
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import * as dntShim from "../../../_dnt.shims.js";
|
|
5
|
+
import { join } from "../../../deps/jsr.io/@std/path/1.1.4/mod.js";
|
|
6
|
+
import { fileExists } from "../../../lib/cli.js";
|
|
7
|
+
import { scanFlyToml } from "../../../util/guard.js";
|
|
8
|
+
import { fetchTemplate, parseTemplateRef } from "../../../util/template.js";
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Image Mode
|
|
11
|
+
// =============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Generate a minimal fly.toml with http_service config for auto start/stop.
|
|
14
|
+
* Written to a temp directory and cleaned up after deploy.
|
|
15
|
+
*/
|
|
16
|
+
const generateServiceToml = (port) => `[http_service]\n` +
|
|
17
|
+
` internal_port = ${port}\n` +
|
|
18
|
+
` auto_stop_machines = "stop"\n` +
|
|
19
|
+
` auto_start_machines = true\n` +
|
|
20
|
+
` min_machines_running = 0\n`;
|
|
21
|
+
/**
|
|
22
|
+
* Parse --main-port value. Returns the port number, or null if "none".
|
|
23
|
+
* Dies on invalid input.
|
|
24
|
+
*/
|
|
25
|
+
const parseMainPort = (raw, out) => {
|
|
26
|
+
if (raw === "none")
|
|
27
|
+
return null;
|
|
28
|
+
const port = Number(raw);
|
|
29
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
30
|
+
out.die(`Invalid --main-port: "${raw}". Use a Port Number (1-65535) or "none".`);
|
|
31
|
+
return "error";
|
|
32
|
+
}
|
|
33
|
+
return port;
|
|
34
|
+
};
|
|
35
|
+
/** Resolve deploy config for image mode (--image). */
|
|
36
|
+
export const resolveImageMode = (image, mainPortRaw, out) => {
|
|
37
|
+
const mainPort = parseMainPort(mainPortRaw, out);
|
|
38
|
+
if (mainPort === "error")
|
|
39
|
+
return null;
|
|
40
|
+
const preflight = {
|
|
41
|
+
scanned: false,
|
|
42
|
+
warnings: [],
|
|
43
|
+
};
|
|
44
|
+
if (mainPort !== null) {
|
|
45
|
+
const tempDir = dntShim.Deno.makeTempDirSync();
|
|
46
|
+
const configPath = join(tempDir, "fly.toml");
|
|
47
|
+
dntShim.Deno.writeTextFileSync(configPath, generateServiceToml(mainPort));
|
|
48
|
+
out.ok(`HTTP Service on Port ${mainPort} (auto start/stop)`);
|
|
49
|
+
return { image, configPath, preflight, tempDir };
|
|
50
|
+
}
|
|
51
|
+
out.info("Image Mode — No Service Config");
|
|
52
|
+
return { image, preflight };
|
|
53
|
+
};
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Config Mode
|
|
56
|
+
// =============================================================================
|
|
57
|
+
/** Resolve deploy config for config mode (default — uses fly.toml). */
|
|
58
|
+
export const resolveConfigMode = async (explicitConfig, out) => {
|
|
59
|
+
let configPath = explicitConfig;
|
|
60
|
+
if (!configPath && (await fileExists("./fly.toml"))) {
|
|
61
|
+
configPath = "./fly.toml";
|
|
62
|
+
}
|
|
63
|
+
if (!configPath) {
|
|
64
|
+
out.info("No fly.toml Found — Deploying Without Config Scan");
|
|
65
|
+
return { preflight: { scanned: false, warnings: [] } };
|
|
66
|
+
}
|
|
67
|
+
if (!(await fileExists(configPath))) {
|
|
68
|
+
out.die(`Config File Not Found: ${configPath}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const tomlContent = await dntShim.Deno.readTextFile(configPath);
|
|
72
|
+
const scan = scanFlyToml(tomlContent);
|
|
73
|
+
if (scan.errors.length > 0) {
|
|
74
|
+
for (const err of scan.errors) {
|
|
75
|
+
out.err(err);
|
|
76
|
+
}
|
|
77
|
+
out.die("Pre-flight Check Failed. Fix fly.toml Before Deploying.");
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
for (const warn of scan.warnings) {
|
|
81
|
+
out.warn(warn);
|
|
82
|
+
}
|
|
83
|
+
out.ok(`Scanned ${configPath}`);
|
|
84
|
+
return {
|
|
85
|
+
configPath,
|
|
86
|
+
preflight: { scanned: scan.scanned, warnings: scan.warnings },
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// Template Mode
|
|
91
|
+
// =============================================================================
|
|
92
|
+
/** Resolve deploy config for template mode (--template). */
|
|
93
|
+
export const resolveTemplateMode = async (templateRaw, out) => {
|
|
94
|
+
const ref = parseTemplateRef(templateRaw);
|
|
95
|
+
if (!ref) {
|
|
96
|
+
out.die(`Invalid Template Reference: "${templateRaw}". ` +
|
|
97
|
+
`Format: owner/repo[/path][@ref]`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const label = (ref.path === "."
|
|
101
|
+
? `${ref.owner}/${ref.repo}`
|
|
102
|
+
: `${ref.owner}/${ref.repo}/${ref.path}`) +
|
|
103
|
+
(ref.ref ? `@${ref.ref}` : "");
|
|
104
|
+
out.info(`Template: ${label}`);
|
|
105
|
+
const fetchSpinner = out.spinner("Fetching Template from GitHub");
|
|
106
|
+
const result = await fetchTemplate(ref);
|
|
107
|
+
if (!result.ok) {
|
|
108
|
+
fetchSpinner.fail("Template Fetch Failed");
|
|
109
|
+
out.die(result.error);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const { tempDir, templateDir } = result.value;
|
|
113
|
+
fetchSpinner.success("Template Fetched");
|
|
114
|
+
const configPath = join(templateDir, "fly.toml");
|
|
115
|
+
let tomlContent;
|
|
116
|
+
try {
|
|
117
|
+
tomlContent = await dntShim.Deno.readTextFile(configPath);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
try {
|
|
121
|
+
dntShim.Deno.removeSync(tempDir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
out.die(`Template '${ref.path === "." ? ref.repo : ref.path}' Has No fly.toml`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const scan = scanFlyToml(tomlContent);
|
|
130
|
+
if (scan.errors.length > 0) {
|
|
131
|
+
try {
|
|
132
|
+
dntShim.Deno.removeSync(tempDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* ignore */
|
|
136
|
+
}
|
|
137
|
+
for (const err of scan.errors) {
|
|
138
|
+
out.err(err);
|
|
139
|
+
}
|
|
140
|
+
out.die("Pre-flight Check Failed for Template fly.toml");
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
for (const warn of scan.warnings) {
|
|
144
|
+
out.warn(warn);
|
|
145
|
+
}
|
|
146
|
+
out.ok(`Scanned ${ref.path === "." ? "" : ref.path + "/"}fly.toml`);
|
|
147
|
+
return {
|
|
148
|
+
configPath,
|
|
149
|
+
preflight: { scanned: scan.scanned, warnings: scan.warnings },
|
|
150
|
+
tempDir,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/app.ts"],"names":[],"mappings":"AAiLA,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAoF7D,CAAC"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Destroy App — Destroy a Workload App on a Network
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
5
|
+
import { bold, confirm } from "../../../lib/cli.js";
|
|
6
|
+
import { checkArgs } from "../../../lib/args.js";
|
|
7
|
+
import { createOutput } from "../../../lib/output.js";
|
|
8
|
+
import { Result } from "../../../lib/result.js";
|
|
9
|
+
import { runMachine } from "../../../lib/machine.js";
|
|
10
|
+
import { findWorkloadApp } from "../../../util/discovery.js";
|
|
11
|
+
import { assertNotRouter } from "../../../util/guard.js";
|
|
12
|
+
import { initSession } from "../../../util/session.js";
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Hydration
|
|
15
|
+
// =============================================================================
|
|
16
|
+
const hydrateDestroyApp = async (ctx) => {
|
|
17
|
+
const workloadApp = await findWorkloadApp(ctx.fly, ctx.org, ctx.app, ctx.network);
|
|
18
|
+
if (!workloadApp) {
|
|
19
|
+
const anyApp = await findWorkloadApp(ctx.fly, ctx.org, ctx.app);
|
|
20
|
+
if (anyApp) {
|
|
21
|
+
ctx.out.die(`App '${ctx.app}' Exists on Network '${anyApp.network}', Not '${ctx.network}'`);
|
|
22
|
+
}
|
|
23
|
+
return "complete";
|
|
24
|
+
}
|
|
25
|
+
ctx.flyAppName = workloadApp.appName;
|
|
26
|
+
return "confirm";
|
|
27
|
+
};
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Transitions
|
|
30
|
+
// =============================================================================
|
|
31
|
+
const destroyAppTransition = async (phase, ctx) => {
|
|
32
|
+
switch (phase) {
|
|
33
|
+
case "confirm": {
|
|
34
|
+
ctx.out.blank()
|
|
35
|
+
.header("Ambit Destroy App")
|
|
36
|
+
.blank()
|
|
37
|
+
.text(` App: ${ctx.flyAppName}`)
|
|
38
|
+
.text(` Network: ${ctx.network}`)
|
|
39
|
+
.blank();
|
|
40
|
+
if (!ctx.yes && !ctx.json) {
|
|
41
|
+
const confirmed = await confirm(`Destroy App '${ctx.app}' on Network '${ctx.network}'?`);
|
|
42
|
+
if (!confirmed) {
|
|
43
|
+
ctx.out.text("Cancelled.");
|
|
44
|
+
return Result.err("Cancelled");
|
|
45
|
+
}
|
|
46
|
+
ctx.out.blank();
|
|
47
|
+
}
|
|
48
|
+
return Result.ok("delete_app");
|
|
49
|
+
}
|
|
50
|
+
case "delete_app": {
|
|
51
|
+
if (ctx.flyAppName) {
|
|
52
|
+
await ctx.out.spin("Deleting App", () => ctx.fly.apps.delete(ctx.flyAppName));
|
|
53
|
+
ctx.out.ok("Fly App Destroyed");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
ctx.out.skip("Fly App Already Destroyed");
|
|
57
|
+
}
|
|
58
|
+
return Result.ok("complete");
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
return Result.err(`Unknown Phase: ${phase}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Stage 1: Destroy
|
|
66
|
+
// =============================================================================
|
|
67
|
+
const stageDestroy = async (out, fly, opts) => {
|
|
68
|
+
const ctx = { fly, out, ...opts };
|
|
69
|
+
const phase = await hydrateDestroyApp(ctx);
|
|
70
|
+
if (phase === "complete" && !ctx.flyAppName) {
|
|
71
|
+
out.ok(`App '${opts.app}' on Network '${opts.network}' Already Destroyed`);
|
|
72
|
+
out.print();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const machine = {
|
|
76
|
+
terminal: "complete",
|
|
77
|
+
transition: destroyAppTransition,
|
|
78
|
+
};
|
|
79
|
+
const result = await runMachine(machine, phase, ctx);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
if (result.error === "Cancelled")
|
|
82
|
+
return;
|
|
83
|
+
return out.die(result.error);
|
|
84
|
+
}
|
|
85
|
+
stageSummary(out, ctx);
|
|
86
|
+
};
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// Stage 3: Summary
|
|
89
|
+
// =============================================================================
|
|
90
|
+
const stageSummary = (out, ctx) => {
|
|
91
|
+
out.done({
|
|
92
|
+
destroyed: true,
|
|
93
|
+
appName: ctx.flyAppName ?? ctx.app,
|
|
94
|
+
network: ctx.network,
|
|
95
|
+
});
|
|
96
|
+
out.ok("App Destroyed");
|
|
97
|
+
out.blank();
|
|
98
|
+
out.print();
|
|
99
|
+
};
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Destroy App Command
|
|
102
|
+
// =============================================================================
|
|
103
|
+
export const destroyApp = async (argv) => {
|
|
104
|
+
const opts = {
|
|
105
|
+
string: ["network", "org"],
|
|
106
|
+
boolean: ["help", "yes", "json"],
|
|
107
|
+
alias: { y: "yes" },
|
|
108
|
+
};
|
|
109
|
+
const args = parseArgs(argv, opts);
|
|
110
|
+
checkArgs(args, opts, "ambit destroy app");
|
|
111
|
+
if (args.help) {
|
|
112
|
+
console.log(`
|
|
113
|
+
${bold("ambit destroy app")} - Destroy a Workload App
|
|
114
|
+
|
|
115
|
+
${bold("USAGE")}
|
|
116
|
+
ambit destroy app <app>.<network> [--org <org>] [--yes] [--json]
|
|
117
|
+
ambit destroy app <app> --network <name> [--org <org>] [--yes] [--json]
|
|
118
|
+
|
|
119
|
+
${bold("OPTIONS")}
|
|
120
|
+
--network <name> Target network (if not using dot syntax)
|
|
121
|
+
--org <org> Fly.io organization slug
|
|
122
|
+
-y, --yes Skip confirmation prompts
|
|
123
|
+
--json Output as JSON
|
|
124
|
+
|
|
125
|
+
${bold("EXAMPLES")}
|
|
126
|
+
ambit destroy app my-app.browsers
|
|
127
|
+
ambit destroy app my-app --network browsers --yes
|
|
128
|
+
`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const out = createOutput(args.json);
|
|
132
|
+
const appArg = args._[0];
|
|
133
|
+
if (!appArg || typeof appArg !== "string") {
|
|
134
|
+
return out.die("Missing App Name. Usage: ambit destroy app <app>.<network>");
|
|
135
|
+
}
|
|
136
|
+
let app;
|
|
137
|
+
let network;
|
|
138
|
+
if (appArg.includes(".")) {
|
|
139
|
+
const parts = appArg.split(".");
|
|
140
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
141
|
+
return out.die(`'${appArg}' Should Have Exactly One Dot, Like my-app.my-network`);
|
|
142
|
+
}
|
|
143
|
+
if (args.network) {
|
|
144
|
+
return out.die(`Network Is Already Part of the Name ('${appArg}'), --network Is Not Needed`);
|
|
145
|
+
}
|
|
146
|
+
app = parts[0];
|
|
147
|
+
network = parts[1];
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
app = appArg;
|
|
151
|
+
if (!args.network) {
|
|
152
|
+
return out.die(`Missing Network. Use: ambit destroy app ${app}.<network>`);
|
|
153
|
+
}
|
|
154
|
+
network = args.network;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
assertNotRouter(app);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return out.die(e instanceof Error ? e.message : String(e));
|
|
161
|
+
}
|
|
162
|
+
const { fly, org } = await initSession(out, {
|
|
163
|
+
json: args.json,
|
|
164
|
+
org: args.org,
|
|
165
|
+
});
|
|
166
|
+
await stageDestroy(out, fly, {
|
|
167
|
+
app,
|
|
168
|
+
network,
|
|
169
|
+
org,
|
|
170
|
+
yes: args.yes,
|
|
171
|
+
json: args.json,
|
|
172
|
+
});
|
|
173
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Destroy Command - Destroy Networks or Apps
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import * as dntShim from "../../../_dnt.shims.js";
|
|
5
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
6
|
+
import { bold } from "../../../lib/cli.js";
|
|
7
|
+
import { registerCommand } from "../../mod.js";
|
|
8
|
+
import { destroyNetwork } from "./network.js";
|
|
9
|
+
import { destroyApp } from "./app.js";
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Top-Level Help
|
|
12
|
+
// =============================================================================
|
|
13
|
+
const showDestroyHelp = () => {
|
|
14
|
+
console.log(`
|
|
15
|
+
${bold("ambit destroy")} - Destroy Networks or Apps
|
|
16
|
+
|
|
17
|
+
${bold("USAGE")}
|
|
18
|
+
ambit destroy network <name> [options]
|
|
19
|
+
ambit destroy app <app>.<network> [options]
|
|
20
|
+
|
|
21
|
+
${bold("SUBCOMMANDS")}
|
|
22
|
+
network Tear down a router, clean up DNS and tailnet device
|
|
23
|
+
app Destroy a workload app on a network
|
|
24
|
+
|
|
25
|
+
${bold("OPTIONS")}
|
|
26
|
+
--help Show help for a subcommand
|
|
27
|
+
|
|
28
|
+
${bold("EXAMPLES")}
|
|
29
|
+
ambit destroy network browsers
|
|
30
|
+
ambit destroy app my-app.browsers
|
|
31
|
+
ambit destroy app my-app --network browsers
|
|
32
|
+
|
|
33
|
+
Run 'ambit destroy network --help' or 'ambit destroy app --help' for details.
|
|
34
|
+
`);
|
|
35
|
+
};
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Dispatcher
|
|
38
|
+
// =============================================================================
|
|
39
|
+
const destroy = async (argv) => {
|
|
40
|
+
const subcommand = typeof argv[0] === "string" ? argv[0] : undefined;
|
|
41
|
+
if (subcommand === "network") {
|
|
42
|
+
return destroyNetwork(argv.slice(1));
|
|
43
|
+
}
|
|
44
|
+
if (subcommand === "app") {
|
|
45
|
+
return destroyApp(argv.slice(1));
|
|
46
|
+
}
|
|
47
|
+
const args = parseArgs(argv, { boolean: ["help"] });
|
|
48
|
+
if (args.help) {
|
|
49
|
+
showDestroyHelp();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
showDestroyHelp();
|
|
53
|
+
dntShim.Deno.exit(1);
|
|
54
|
+
};
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Register Command
|
|
57
|
+
// =============================================================================
|
|
58
|
+
registerCommand({
|
|
59
|
+
name: "destroy",
|
|
60
|
+
description: "Destroy a network (router) or a workload app",
|
|
61
|
+
usage: "ambit destroy network|app <name> [options]",
|
|
62
|
+
run: destroy,
|
|
63
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/network.ts"],"names":[],"mappings":"AAwQA,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAkDjE,CAAC"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Destroy Network — Tear Down Router and Clean Up Tailnet
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
5
|
+
import { bold, confirm } from "../../../lib/cli.js";
|
|
6
|
+
import { checkArgs } from "../../../lib/args.js";
|
|
7
|
+
import { createOutput } from "../../../lib/output.js";
|
|
8
|
+
import { Result } from "../../../lib/result.js";
|
|
9
|
+
import { runMachine } from "../../../lib/machine.js";
|
|
10
|
+
import { findRouterApp, listWorkloadAppsOnNetwork, } from "../../../util/discovery.js";
|
|
11
|
+
import { initSession } from "../../../util/session.js";
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Phase Labels
|
|
14
|
+
// =============================================================================
|
|
15
|
+
const DESTROY_NETWORK_PHASES = [
|
|
16
|
+
{ phase: "confirm", label: "Confirmed" },
|
|
17
|
+
{ phase: "clear_dns", label: "Split DNS Cleared" },
|
|
18
|
+
{ phase: "remove_device", label: "Tailscale Device Removed" },
|
|
19
|
+
{ phase: "delete_app", label: "Fly App Destroyed" },
|
|
20
|
+
];
|
|
21
|
+
const reportSkipped = (out, startPhase) => {
|
|
22
|
+
for (const { phase, label } of DESTROY_NETWORK_PHASES) {
|
|
23
|
+
if (phase === startPhase)
|
|
24
|
+
break;
|
|
25
|
+
out.skip(label);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Hydration
|
|
30
|
+
// =============================================================================
|
|
31
|
+
const hydrateDestroyNetwork = async (ctx) => {
|
|
32
|
+
const router = await findRouterApp(ctx.fly, ctx.org, ctx.network);
|
|
33
|
+
const dns = await ctx.tailscale.dns.getSplit();
|
|
34
|
+
const hasDns = (dns[ctx.network]?.length ?? 0) > 0;
|
|
35
|
+
if (router) {
|
|
36
|
+
ctx.appName = router.appName;
|
|
37
|
+
const device = await ctx.tailscale.devices.getByHostname(router.appName);
|
|
38
|
+
if (device) {
|
|
39
|
+
ctx.device = device;
|
|
40
|
+
ctx.tag = device.tags?.[0] ?? undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!router && !ctx.device && !hasDns)
|
|
44
|
+
return "complete";
|
|
45
|
+
if (!hasDns && !ctx.device && router)
|
|
46
|
+
return "delete_app";
|
|
47
|
+
if (!hasDns && ctx.device)
|
|
48
|
+
return "remove_device";
|
|
49
|
+
if (!hasDns)
|
|
50
|
+
return router ? "delete_app" : "complete";
|
|
51
|
+
return "confirm";
|
|
52
|
+
};
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Transitions
|
|
55
|
+
// =============================================================================
|
|
56
|
+
const destroyNetworkTransition = async (phase, ctx) => {
|
|
57
|
+
switch (phase) {
|
|
58
|
+
case "confirm": {
|
|
59
|
+
const workloads = await listWorkloadAppsOnNetwork(ctx.fly, ctx.org, ctx.network);
|
|
60
|
+
ctx.out.blank()
|
|
61
|
+
.header("Ambit Destroy Network")
|
|
62
|
+
.blank()
|
|
63
|
+
.text(` Network: ${ctx.network}`)
|
|
64
|
+
.text(` Router App: ${ctx.appName ?? "unknown"}`)
|
|
65
|
+
.text(` Tag: ${ctx.tag ?? "unknown"}`)
|
|
66
|
+
.blank();
|
|
67
|
+
if (workloads.length > 0) {
|
|
68
|
+
ctx.out.warn(`${workloads.length} Workload App(s) Still on Network '${ctx.network}':`);
|
|
69
|
+
for (const wa of workloads) {
|
|
70
|
+
ctx.out.text(` - ${wa.appName}`);
|
|
71
|
+
}
|
|
72
|
+
ctx.out.blank();
|
|
73
|
+
ctx.out.dim("These Apps Will Lose Connectivity when the Router Is Destroyed.");
|
|
74
|
+
ctx.out.dim(`Consider Destroying Them First with: ambit destroy app <name>.${ctx.network}`);
|
|
75
|
+
ctx.out.blank();
|
|
76
|
+
}
|
|
77
|
+
if (!ctx.yes && !ctx.json) {
|
|
78
|
+
const confirmed = await confirm("Destroy This Router?");
|
|
79
|
+
if (!confirmed) {
|
|
80
|
+
ctx.out.text("Cancelled.");
|
|
81
|
+
return Result.err("Cancelled");
|
|
82
|
+
}
|
|
83
|
+
ctx.out.blank();
|
|
84
|
+
}
|
|
85
|
+
return Result.ok("clear_dns");
|
|
86
|
+
}
|
|
87
|
+
case "clear_dns": {
|
|
88
|
+
await ctx.tailscale.dns.clearSplit(ctx.network);
|
|
89
|
+
ctx.out.ok("Split DNS Cleared");
|
|
90
|
+
return Result.ok("remove_device");
|
|
91
|
+
}
|
|
92
|
+
case "remove_device": {
|
|
93
|
+
if (ctx.device) {
|
|
94
|
+
await ctx.tailscale.devices.delete(ctx.device.id);
|
|
95
|
+
ctx.out.ok("Tailscale Device Removed");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
ctx.out.skip("Tailscale Device Already Removed");
|
|
99
|
+
}
|
|
100
|
+
return Result.ok("delete_app");
|
|
101
|
+
}
|
|
102
|
+
case "delete_app": {
|
|
103
|
+
if (ctx.appName) {
|
|
104
|
+
await ctx.out.spin("Deleting App", () => ctx.fly.apps.delete(ctx.appName));
|
|
105
|
+
ctx.out.ok("Fly App Destroyed");
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
ctx.out.skip("Fly App Already Destroyed");
|
|
109
|
+
}
|
|
110
|
+
return Result.ok("complete");
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
return Result.err(`Unknown Phase: ${phase}`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Stage 1: Destroy
|
|
118
|
+
// =============================================================================
|
|
119
|
+
const stageDestroy = async (out, fly, tailscale, opts) => {
|
|
120
|
+
const ctx = { fly, tailscale, out, ...opts };
|
|
121
|
+
const phase = await hydrateDestroyNetwork(ctx);
|
|
122
|
+
if (phase === "complete") {
|
|
123
|
+
out.ok(`Network "${opts.network}" Already Destroyed`);
|
|
124
|
+
out.print();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
reportSkipped(out, phase);
|
|
128
|
+
const machine = {
|
|
129
|
+
terminal: "complete",
|
|
130
|
+
transition: destroyNetworkTransition,
|
|
131
|
+
};
|
|
132
|
+
const result = await runMachine(machine, phase, ctx);
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
if (result.error === "Cancelled")
|
|
135
|
+
return;
|
|
136
|
+
return out.die(result.error);
|
|
137
|
+
}
|
|
138
|
+
stageSummary(out, ctx);
|
|
139
|
+
};
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Stage 3: Summary
|
|
142
|
+
// =============================================================================
|
|
143
|
+
const stageSummary = (out, ctx) => {
|
|
144
|
+
out.done({
|
|
145
|
+
destroyed: true,
|
|
146
|
+
appName: ctx.appName ?? "",
|
|
147
|
+
workloadAppsWarned: 0,
|
|
148
|
+
});
|
|
149
|
+
out.ok("Router Destroyed");
|
|
150
|
+
if (ctx.tag) {
|
|
151
|
+
out.blank()
|
|
152
|
+
.dim("If You Added ACL Policy Entries for This Router, Remember to Remove:")
|
|
153
|
+
.dim(` tagOwners: ${ctx.tag}`)
|
|
154
|
+
.dim(` autoApprovers: routes for ${ctx.tag}`)
|
|
155
|
+
.dim(` acls: rules referencing ${ctx.tag}`)
|
|
156
|
+
.blank();
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
out.blank()
|
|
160
|
+
.dim("If You Added ACL Policy Entries for This Router, Remember to Remove")
|
|
161
|
+
.dim("the Associated Tag from tagOwners, autoApprovers, and acls.")
|
|
162
|
+
.blank();
|
|
163
|
+
}
|
|
164
|
+
out.print();
|
|
165
|
+
};
|
|
166
|
+
// =============================================================================
|
|
167
|
+
// Destroy Network Command
|
|
168
|
+
// =============================================================================
|
|
169
|
+
export const destroyNetwork = async (argv) => {
|
|
170
|
+
const opts = {
|
|
171
|
+
string: ["network", "org"],
|
|
172
|
+
boolean: ["help", "yes", "json"],
|
|
173
|
+
alias: { y: "yes" },
|
|
174
|
+
};
|
|
175
|
+
const args = parseArgs(argv, opts);
|
|
176
|
+
checkArgs(args, opts, "ambit destroy network");
|
|
177
|
+
if (args.help) {
|
|
178
|
+
console.log(`
|
|
179
|
+
${bold("ambit destroy network")} - Tear Down Router
|
|
180
|
+
|
|
181
|
+
${bold("USAGE")}
|
|
182
|
+
ambit destroy network <name> [--org <org>] [--yes] [--json]
|
|
183
|
+
|
|
184
|
+
${bold("OPTIONS")}
|
|
185
|
+
--org <org> Fly.io organization slug
|
|
186
|
+
-y, --yes Skip confirmation prompts
|
|
187
|
+
--json Output as JSON
|
|
188
|
+
|
|
189
|
+
${bold("EXAMPLES")}
|
|
190
|
+
ambit destroy network browsers
|
|
191
|
+
ambit destroy network browsers --org my-org --yes
|
|
192
|
+
`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const out = createOutput(args.json);
|
|
196
|
+
const network = (typeof args._[0] === "string" ? args._[0] : undefined) || args.network;
|
|
197
|
+
if (!network) {
|
|
198
|
+
return out.die("Network Name Required. Usage: ambit destroy network <name>");
|
|
199
|
+
}
|
|
200
|
+
const { fly, tailscale, org } = await initSession(out, {
|
|
201
|
+
json: args.json,
|
|
202
|
+
org: args.org,
|
|
203
|
+
});
|
|
204
|
+
await stageDestroy(out, fly, tailscale, {
|
|
205
|
+
network,
|
|
206
|
+
org,
|
|
207
|
+
yes: args.yes,
|
|
208
|
+
json: args.json,
|
|
209
|
+
});
|
|
210
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/doctor.ts"],"names":[],"mappings":""}
|