@cardelli/ambit 0.2.3 → 0.3.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 +38 -22
- package/esm/cli/commands/create/index.js +53 -17
- package/esm/cli/commands/create/machine.d.ts +2 -1
- package/esm/cli/commands/create/machine.d.ts.map +1 -1
- package/esm/cli/commands/create/machine.js +70 -29
- package/esm/cli/commands/deploy/index.js +2 -4
- package/esm/cli/commands/deploy/machine.d.ts.map +1 -1
- package/esm/cli/commands/destroy/app.d.ts.map +1 -1
- package/esm/cli/commands/destroy/index.js +2 -0
- package/esm/cli/commands/destroy/network.d.ts.map +1 -1
- package/esm/cli/commands/destroy/network.js +66 -5
- package/esm/cli/commands/doctor.js +13 -4
- package/esm/cli/commands/list.js +1 -1
- package/esm/cli/commands/share.d.ts +2 -0
- package/esm/cli/commands/share.d.ts.map +1 -0
- package/esm/cli/commands/share.js +250 -0
- package/esm/cli/commands/status.js +4 -1
- package/esm/cli/mod.d.ts.map +1 -1
- package/esm/cli/mod.js +2 -0
- package/esm/deno.js +1 -1
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +5 -7
- package/esm/main.d.ts +1 -0
- package/esm/main.d.ts.map +1 -1
- package/esm/main.js +2 -0
- package/esm/providers/fly.d.ts.map +1 -1
- package/esm/providers/fly.js +14 -3
- package/esm/providers/tailscale.d.ts +7 -0
- package/esm/providers/tailscale.d.ts.map +1 -1
- package/esm/providers/tailscale.js +23 -1
- package/esm/router/start.sh +7 -4
- package/esm/util/constants.d.ts +0 -1
- package/esm/util/constants.d.ts.map +1 -1
- package/esm/util/constants.js +0 -1
- package/esm/util/credentials.d.ts.map +1 -1
- package/esm/util/credentials.js +1 -1
- package/esm/util/discovery.d.ts.map +1 -1
- package/esm/util/discovery.js +1 -1
- package/esm/util/tailscale-local.d.ts +41 -0
- package/esm/util/tailscale-local.d.ts.map +1 -1
- package/esm/util/tailscale-local.js +146 -0
- package/esm/util/template.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,17 +67,38 @@ Open `http://my-crazy-site.lab`. It works for you and nobody else.
|
|
|
67
67
|
|
|
68
68
|
### `ambit create <network>`
|
|
69
69
|
|
|
70
|
-
This is the first command you run, it sets up your private network: a named slice of the cloud that only your devices can reach. Under the hood it handles Fly.io and Tailscale authentication, deploys the router, sets up DNS,
|
|
71
|
-
|
|
72
|
-
| Flag
|
|
73
|
-
|
|
|
74
|
-
| `--org <org>`
|
|
75
|
-
| `--region <region>`
|
|
76
|
-
| `--api-key <key>`
|
|
77
|
-
| `--tag <tag>`
|
|
78
|
-
| `--
|
|
79
|
-
|
|
|
80
|
-
| `--
|
|
70
|
+
This is the first command you run, it sets up your private network: a named slice of the cloud that only your devices can reach. Under the hood it handles Fly.io and Tailscale authentication, deploys the router, sets up DNS, configures your local machine to accept the new routes, and automatically adds the router's tag to your Tailscale ACL policy.
|
|
71
|
+
|
|
72
|
+
| Flag | Description |
|
|
73
|
+
| ------------------- | --------------------------------------------------------------------------- |
|
|
74
|
+
| `--org <org>` | Fly.io organization slug |
|
|
75
|
+
| `--region <region>` | Fly.io region (default: `iad`) |
|
|
76
|
+
| `--api-key <key>` | Tailscale API access token (tskey-api-...) |
|
|
77
|
+
| `--tag <tag>` | Tailscale ACL tag for the router (default: `tag:ambit-<network>`) |
|
|
78
|
+
| `--manual` | Skip automatic Tailscale ACL configuration (tagOwners + autoApprovers) |
|
|
79
|
+
| `--no-auto-approve` | Skip waiting for router and approving routes |
|
|
80
|
+
| `-y`, `--yes` | Skip confirmation prompts |
|
|
81
|
+
| `--json` | Machine-readable JSON output (implies `--no-auto-approve`) |
|
|
82
|
+
|
|
83
|
+
### `ambit share <network> <member> [<member>...]`
|
|
84
|
+
|
|
85
|
+
Grants one or more members access to a network by adding two ACL rules per member: one for DNS (so they can resolve `*.<network>` names) and one for the subnet (so they can reach apps). All members are validated before any changes are made. The command is idempotent — safe to re-run.
|
|
86
|
+
|
|
87
|
+
Each member must be one of:
|
|
88
|
+
- `group:<name>` — a Tailscale group
|
|
89
|
+
- `tag:<name>` — a device tag
|
|
90
|
+
- `autogroup:<name>` — a built-in Tailscale group (e.g. `autogroup:member`)
|
|
91
|
+
- A valid email address — a specific Tailscale user
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx @cardelli/ambit share browsers group:team
|
|
95
|
+
npx @cardelli/ambit share browsers group:team alice@example.com group:contractors
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Flag | Description |
|
|
99
|
+
| ------------- | ------------------------ |
|
|
100
|
+
| `--org <org>` | Fly.io organization slug |
|
|
101
|
+
| `--json` | Machine-readable JSON |
|
|
81
102
|
|
|
82
103
|
### `ambit deploy <app>.<network>`
|
|
83
104
|
|
|
@@ -173,11 +194,12 @@ Lists all discovered routers across networks in a table showing the network name
|
|
|
173
194
|
|
|
174
195
|
### `ambit destroy network <name>`
|
|
175
196
|
|
|
176
|
-
Tears down a network: destroys the router, cleans up DNS, and removes the Tailscale
|
|
197
|
+
Tears down a network: destroys the router, cleans up DNS, removes the Tailscale device, and automatically removes the router's tag from your Tailscale ACL policy (tagOwners and autoApprovers). If there are workload apps still on the network, ambit warns you before proceeding.
|
|
177
198
|
|
|
178
199
|
| Flag | Description |
|
|
179
200
|
| ----------------- | ------------------------------ |
|
|
180
201
|
| `--org <org>` | Fly.io organization slug |
|
|
202
|
+
| `--manual` | Skip automatic Tailscale ACL cleanup (tagOwners + autoApprovers) |
|
|
181
203
|
| `-y`, `--yes` | Skip confirmation prompts |
|
|
182
204
|
| `--json` | Machine-readable JSON output |
|
|
183
205
|
|
|
@@ -218,20 +240,12 @@ Checks app health: verifies the app is deployed and running, then checks the rou
|
|
|
218
240
|
|
|
219
241
|
## Access Control
|
|
220
242
|
|
|
221
|
-
|
|
243
|
+
By default, `ambit create` automatically adds the router's tag to your Tailscale ACL policy (`tagOwners` and `autoApprovers`). `ambit destroy network` automatically removes them. Pass `--manual` to either command to skip this and manage the policy yourself — useful if your API token lacks ACL write permission (`policy_file` scope).
|
|
222
244
|
|
|
223
|
-
If you want to lock
|
|
245
|
+
If you want to lock down which users can reach which networks, two rules do the job — one for DNS queries and one for data traffic:
|
|
224
246
|
|
|
225
247
|
```jsonc
|
|
226
248
|
{
|
|
227
|
-
"tagOwners": {
|
|
228
|
-
"tag:ambit-infra": ["autogroup:admin"]
|
|
229
|
-
},
|
|
230
|
-
"autoApprovers": {
|
|
231
|
-
"routes": {
|
|
232
|
-
"fdaa:X:XXXX::/48": ["tag:ambit-infra"]
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
249
|
"acls": [
|
|
236
250
|
{
|
|
237
251
|
"action": "accept",
|
|
@@ -243,6 +257,8 @@ If you want to lock it down, two rules do the job — one for DNS queries and on
|
|
|
243
257
|
}
|
|
244
258
|
```
|
|
245
259
|
|
|
260
|
+
These `acls` entries are never touched automatically — ambit only manages `tagOwners` and `autoApprovers`.
|
|
261
|
+
|
|
246
262
|
## Multiple Networks
|
|
247
263
|
|
|
248
264
|
You can create as many networks as you want, and each one gets its own TLD on your tailnet. The SOCKS proxy on each router means containers on one network can reach services on another by going through the tailnet, so a browser on your `browsers` network can connect to a database on your `infra` network.
|
|
@@ -9,12 +9,12 @@ import { runMachine } from "../../../lib/machine.js";
|
|
|
9
9
|
import { registerCommand } from "../../mod.js";
|
|
10
10
|
import { getRouterTag } from "../../../util/naming.js";
|
|
11
11
|
import { isPublicTld } from "../../../util/guard.js";
|
|
12
|
-
import { createFlyProvider
|
|
12
|
+
import { createFlyProvider } from "../../../providers/fly.js";
|
|
13
13
|
import { createTailscaleProvider, } from "../../../providers/tailscale.js";
|
|
14
14
|
import { getCredentialStore } from "../../../util/credentials.js";
|
|
15
|
-
import {
|
|
15
|
+
import { TAILSCALE_API_KEY_PREFIX } from "../../../util/constants.js";
|
|
16
16
|
import { resolveOrg } from "../../../util/resolve.js";
|
|
17
|
-
import { isAutoApproverConfigured, isTagOwnerConfigured } from "../../../util/tailscale-local.js";
|
|
17
|
+
import { assertAdditivePatch, isAutoApproverConfigured, isTagOwnerConfigured, patchTagOwner, } from "../../../util/tailscale-local.js";
|
|
18
18
|
import { createTransition, hydrateCreate, reportSkipped, } from "./machine.js";
|
|
19
19
|
// =============================================================================
|
|
20
20
|
// Stage 1: Fly.io Configuration
|
|
@@ -34,6 +34,13 @@ const stageFlyConfig = async (out, opts) => {
|
|
|
34
34
|
// =============================================================================
|
|
35
35
|
// Stage 2: Tailscale Configuration
|
|
36
36
|
// =============================================================================
|
|
37
|
+
const handleAclSetFailure = (out, result, action) => {
|
|
38
|
+
if (result.status === 403) {
|
|
39
|
+
out.err(`${action}: Permission Denied (HTTP 403)`);
|
|
40
|
+
return out.die("Your API Token Lacks ACL Write Permission. Re-run with --manual to Skip ACL Changes");
|
|
41
|
+
}
|
|
42
|
+
return out.die(`${action}: ${result.error ?? `HTTP ${result.status}`}`);
|
|
43
|
+
};
|
|
37
44
|
const stageTailscaleConfig = async (out, opts) => {
|
|
38
45
|
out.header("Step 2: Tailscale Configuration").blank();
|
|
39
46
|
const credentials = getCredentialStore();
|
|
@@ -63,9 +70,26 @@ const stageTailscaleConfig = async (out, opts) => {
|
|
|
63
70
|
validateSpinner.success("API Access Token Validated");
|
|
64
71
|
await credentials.setTailscaleApiKey(apiKey);
|
|
65
72
|
const tagOwnerSpinner = out.spinner(`Checking tagOwners for ${opts.tag}`);
|
|
66
|
-
|
|
73
|
+
let policy = await tailscale.acl.getPolicy();
|
|
67
74
|
const hasTagOwner = isTagOwnerConfigured(policy, opts.tag);
|
|
68
|
-
if (!hasTagOwner) {
|
|
75
|
+
if (!hasTagOwner && !opts.manual && policy) {
|
|
76
|
+
tagOwnerSpinner.stop();
|
|
77
|
+
const beforeTagOwner = policy;
|
|
78
|
+
policy = patchTagOwner(policy, opts.tag);
|
|
79
|
+
assertAdditivePatch(beforeTagOwner, policy);
|
|
80
|
+
const validateTagOwner = await tailscale.acl.validatePolicy(policy);
|
|
81
|
+
if (!validateTagOwner.ok) {
|
|
82
|
+
return handleAclSetFailure(out, validateTagOwner, `Validating tagOwners patch for ${opts.tag}`);
|
|
83
|
+
}
|
|
84
|
+
const patchSpinner = out.spinner(`Adding ${opts.tag} to tagOwners`);
|
|
85
|
+
const result = await tailscale.acl.setPolicy(policy);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
patchSpinner.fail(`Adding ${opts.tag} to tagOwners`);
|
|
88
|
+
return handleAclSetFailure(out, result, `Adding ${opts.tag} to tagOwners`);
|
|
89
|
+
}
|
|
90
|
+
patchSpinner.success(`Added ${opts.tag} to tagOwners`);
|
|
91
|
+
}
|
|
92
|
+
else if (!hasTagOwner) {
|
|
69
93
|
tagOwnerSpinner.fail(`${opts.tag} Not Set Up Yet`);
|
|
70
94
|
out.blank()
|
|
71
95
|
.text(` You need to grant yourself permission to create the "${opts.network}"`)
|
|
@@ -80,22 +104,24 @@ const stageTailscaleConfig = async (out, opts) => {
|
|
|
80
104
|
.blank();
|
|
81
105
|
return out.die(`Set Up ${opts.tag} in Tailscale, Then Try Again`);
|
|
82
106
|
}
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
else {
|
|
108
|
+
tagOwnerSpinner.success(`${opts.tag} Found in Tailscale ACL`);
|
|
109
|
+
}
|
|
110
|
+
if (opts.manual && opts.json) {
|
|
85
111
|
const approverSpinner = out.spinner(`Checking autoApprovers for ${opts.tag}`);
|
|
86
112
|
const hasApprover = isAutoApproverConfigured(policy, opts.tag);
|
|
87
113
|
if (!hasApprover) {
|
|
88
114
|
approverSpinner.fail(`Auto-approve Not Configured for ${opts.tag}`);
|
|
89
115
|
out.blank()
|
|
90
|
-
.text(" In
|
|
91
|
-
.text(
|
|
116
|
+
.text(" In --manual --json mode, ambit can't interactively approve the")
|
|
117
|
+
.text(" router's subnet routes. Set up autoApprovers first:")
|
|
92
118
|
.link(" https://login.tailscale.com/admin/acls/visual/auto-approvers")
|
|
93
|
-
.dim(` Route:
|
|
119
|
+
.dim(` Route: <subnet>/48 Owner: ${opts.tag}`)
|
|
94
120
|
.blank()
|
|
95
|
-
.dim(" Or
|
|
96
|
-
.dim(` "autoApprovers": { "routes": { "
|
|
121
|
+
.dim(" Or in the ACL file:")
|
|
122
|
+
.dim(` "autoApprovers": { "routes": { "<subnet>/48": ["${opts.tag}"] } }`)
|
|
97
123
|
.blank();
|
|
98
|
-
return out.die(`Set Up Auto-approve for ${opts.tag} to Use --json`);
|
|
124
|
+
return out.die(`Set Up Auto-approve for ${opts.tag} to Use --manual --json`);
|
|
99
125
|
}
|
|
100
126
|
approverSpinner.success(`Auto-approve Configured for ${opts.tag}`);
|
|
101
127
|
}
|
|
@@ -194,7 +220,8 @@ const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
|
194
220
|
.blank()
|
|
195
221
|
.dim(" You can do this from the Tailscale dashboard:")
|
|
196
222
|
.link(" https://login.tailscale.com/admin/acls/visual/general-access-rules")
|
|
197
|
-
.dim(` Source: group:YOUR_GROUP
|
|
223
|
+
.dim(` Source: group:YOUR_GROUP Destination: ${opts.tag}:53`)
|
|
224
|
+
.dim(` Source: group:YOUR_GROUP Destination: ${ctx.subnet}:*`)
|
|
198
225
|
.blank()
|
|
199
226
|
.dim(" Or you can do it manually with this JSON config:")
|
|
200
227
|
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${opts.tag}:53"]}`)
|
|
@@ -214,7 +241,7 @@ const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
|
214
241
|
const create = async (argv) => {
|
|
215
242
|
const opts = {
|
|
216
243
|
string: ["org", "region", "api-key", "tag"],
|
|
217
|
-
boolean: ["help", "yes", "json", "no-auto-approve"],
|
|
244
|
+
boolean: ["help", "yes", "json", "no-auto-approve", "manual"],
|
|
218
245
|
alias: { y: "yes" },
|
|
219
246
|
};
|
|
220
247
|
const args = parseArgs(argv, opts);
|
|
@@ -231,7 +258,8 @@ ${bold("OPTIONS")}
|
|
|
231
258
|
--region <region> Fly.io region (default: iad)
|
|
232
259
|
--api-key <key> Tailscale API access token (tskey-api-...)
|
|
233
260
|
--tag <tag> Tailscale ACL tag for the router (default: tag:ambit-<network>)
|
|
234
|
-
--
|
|
261
|
+
--manual Skip automatic Tailscale ACL configuration (tagOwners + autoApprovers)
|
|
262
|
+
--no-auto-approve Skip waiting for router and approving routes
|
|
235
263
|
-y, --yes Skip confirmation prompts
|
|
236
264
|
--json Output as JSON (implies --no-auto-approve)
|
|
237
265
|
|
|
@@ -241,9 +269,14 @@ ${bold("DESCRIPTION")}
|
|
|
241
269
|
|
|
242
270
|
my-app.${args._[0] || "<network>"} resolves to my-app.flycast
|
|
243
271
|
|
|
272
|
+
By default, ambit auto-configures your Tailscale ACL policy (tagOwners
|
|
273
|
+
and autoApprovers). Use --manual if your API token lacks ACL write
|
|
274
|
+
permission or you prefer to manage the policy yourself.
|
|
275
|
+
|
|
244
276
|
${bold("EXAMPLES")}
|
|
245
277
|
ambit create browsers
|
|
246
278
|
ambit create browsers --org my-org --region sea
|
|
279
|
+
ambit create browsers --manual
|
|
247
280
|
`);
|
|
248
281
|
return;
|
|
249
282
|
}
|
|
@@ -257,7 +290,8 @@ ${bold("EXAMPLES")}
|
|
|
257
290
|
return out.die(`"${network}" Is a Public TLD and Cannot Be Used as a Network Name`);
|
|
258
291
|
}
|
|
259
292
|
const tag = args.tag || getRouterTag(network);
|
|
260
|
-
const
|
|
293
|
+
const manual = !!args.manual;
|
|
294
|
+
const shouldApprove = !manual || !(args["no-auto-approve"] || args.json);
|
|
261
295
|
out.blank()
|
|
262
296
|
.header("=".repeat(50))
|
|
263
297
|
.header(` ambit Create: ${network}`)
|
|
@@ -270,6 +304,7 @@ ${bold("EXAMPLES")}
|
|
|
270
304
|
});
|
|
271
305
|
const tailscale = await stageTailscaleConfig(out, {
|
|
272
306
|
json: args.json,
|
|
307
|
+
manual,
|
|
273
308
|
apiKey: args["api-key"],
|
|
274
309
|
tag,
|
|
275
310
|
network,
|
|
@@ -280,6 +315,7 @@ ${bold("EXAMPLES")}
|
|
|
280
315
|
region,
|
|
281
316
|
tag,
|
|
282
317
|
shouldApprove,
|
|
318
|
+
manual,
|
|
283
319
|
});
|
|
284
320
|
};
|
|
285
321
|
// =============================================================================
|
|
@@ -3,7 +3,7 @@ import { Result } from "../../../lib/result.js";
|
|
|
3
3
|
import { type FlyProvider } from "../../../providers/fly.js";
|
|
4
4
|
import type { TailscaleProvider } from "../../../providers/tailscale.js";
|
|
5
5
|
import type { TailscaleDevice } from "../../../schemas/tailscale.js";
|
|
6
|
-
export type CreatePhase = "create_app" | "deploy_router" | "
|
|
6
|
+
export type CreatePhase = "create_app" | "deploy_router" | "approve_routes" | "configure_dns" | "accept_routes" | "complete";
|
|
7
7
|
export type CreateResult = {
|
|
8
8
|
network: string;
|
|
9
9
|
router: {
|
|
@@ -22,6 +22,7 @@ export interface CreateCtx {
|
|
|
22
22
|
region: string;
|
|
23
23
|
tag: string;
|
|
24
24
|
shouldApprove: boolean;
|
|
25
|
+
manual: boolean;
|
|
25
26
|
appName: string;
|
|
26
27
|
routerId: string;
|
|
27
28
|
device?: TailscaleDevice;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/create/machine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQhD,OAAO,
|
|
1
|
+
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/create/machine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQhD,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAW7E,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAOrE,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAcD,eAAO,MAAM,aAAa,GACxB,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CA4BrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CA6K7B,CAAC"}
|
|
@@ -5,9 +5,9 @@ import { randomId } from "../../../lib/cli.js";
|
|
|
5
5
|
import { Result } from "../../../lib/result.js";
|
|
6
6
|
import { extractSubnet } from "../../../util/fly-transforms.js";
|
|
7
7
|
import { ROUTER_DOCKER_DIR, SECRET_NETWORK_NAME, SECRET_ROUTER_ID, SECRET_TAILSCALE_AUTHKEY, } from "../../../util/constants.js";
|
|
8
|
-
import { FlyDeployError
|
|
8
|
+
import { FlyDeployError } from "../../../providers/fly.js";
|
|
9
9
|
import { getRouterAppName } from "../../../util/naming.js";
|
|
10
|
-
import { enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, waitForDevice, } from "../../../util/tailscale-local.js";
|
|
10
|
+
import { assertAdditivePatch, enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, patchAutoApprover, waitForDevice, } from "../../../util/tailscale-local.js";
|
|
11
11
|
import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// Phase Labels
|
|
@@ -15,7 +15,6 @@ import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js"
|
|
|
15
15
|
const CREATE_PHASES = [
|
|
16
16
|
{ phase: "create_app", label: "Fly App Created" },
|
|
17
17
|
{ phase: "deploy_router", label: "Router Deployed" },
|
|
18
|
-
{ phase: "await_device", label: "Router in Tailnet" },
|
|
19
18
|
{ phase: "approve_routes", label: "Routes Approved" },
|
|
20
19
|
{ phase: "configure_dns", label: "Split DNS Configured" },
|
|
21
20
|
{ phase: "accept_routes", label: "Accept Routes Enabled" },
|
|
@@ -44,7 +43,7 @@ export const hydrateCreate = async (ctx) => {
|
|
|
44
43
|
return "complete";
|
|
45
44
|
const device = await ctx.tailscale.devices.getByHostname(router.appName);
|
|
46
45
|
if (!device)
|
|
47
|
-
return "
|
|
46
|
+
return "approve_routes";
|
|
48
47
|
ctx.device = device;
|
|
49
48
|
const routes = await ctx.tailscale.routes.get(device.id);
|
|
50
49
|
if (!routes || routes.unapproved.length > 0)
|
|
@@ -70,15 +69,7 @@ export const createTransition = async (phase, ctx) => {
|
|
|
70
69
|
return Result.ok("deploy_router");
|
|
71
70
|
}
|
|
72
71
|
case "deploy_router": {
|
|
73
|
-
|
|
74
|
-
reusable: false,
|
|
75
|
-
ephemeral: false,
|
|
76
|
-
preauthorized: true,
|
|
77
|
-
tags: [ctx.tag],
|
|
78
|
-
});
|
|
79
|
-
ctx.out.ok("Auth Key Created");
|
|
80
|
-
await ctx.out.spin("Setting Secrets", () => ctx.fly.secrets.set(ctx.appName, {
|
|
81
|
-
[SECRET_TAILSCALE_AUTHKEY]: authKey,
|
|
72
|
+
await ctx.out.spin("Staging Secrets", () => ctx.fly.secrets.set(ctx.appName, {
|
|
82
73
|
[SECRET_NETWORK_NAME]: ctx.network,
|
|
83
74
|
[SECRET_ROUTER_ID]: ctx.routerId,
|
|
84
75
|
}, { stage: true }));
|
|
@@ -102,34 +93,84 @@ export const createTransition = async (phase, ctx) => {
|
|
|
102
93
|
const m = machines.find((m) => m.private_ip);
|
|
103
94
|
if (m?.private_ip)
|
|
104
95
|
ctx.subnet = extractSubnet(m.private_ip);
|
|
105
|
-
|
|
106
|
-
return Result.ok("complete");
|
|
107
|
-
return Result.ok("await_device");
|
|
96
|
+
return Result.ok("approve_routes");
|
|
108
97
|
}
|
|
109
|
-
case "
|
|
110
|
-
ctx.device = await waitForDevice(ctx.tailscale, ctx.appName, 180000);
|
|
111
|
-
ctx.out.ok(`Router Joined Tailnet: ${ctx.device.addresses[0]}`);
|
|
98
|
+
case "approve_routes": {
|
|
112
99
|
if (!ctx.subnet) {
|
|
113
100
|
const machines = await ctx.fly.machines.list(ctx.appName);
|
|
114
101
|
const m = machines.find((m) => m.private_ip);
|
|
115
102
|
if (m?.private_ip)
|
|
116
103
|
ctx.subnet = extractSubnet(m.private_ip);
|
|
117
104
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!ctx.device || !ctx.subnet) {
|
|
122
|
-
return Result.err("Missing Device or Subnet");
|
|
123
|
-
}
|
|
124
|
-
const policy = await ctx.tailscale.acl.getPolicy();
|
|
105
|
+
if (!ctx.subnet)
|
|
106
|
+
return Result.err("Missing Subnet");
|
|
107
|
+
let policy = await ctx.tailscale.acl.getPolicy();
|
|
125
108
|
const hasAutoApprover = isAutoApproverConfigured(policy, ctx.tag);
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
let approverReady = hasAutoApprover;
|
|
110
|
+
if (!hasAutoApprover && !ctx.manual && policy) {
|
|
111
|
+
const before = policy;
|
|
112
|
+
policy = patchAutoApprover(policy, ctx.tag, ctx.subnet);
|
|
113
|
+
assertAdditivePatch(before, policy);
|
|
114
|
+
const vr = await ctx.tailscale.acl.validatePolicy(policy);
|
|
115
|
+
if (!vr.ok) {
|
|
116
|
+
ctx.out.warn(`Could Not Validate autoApprover Patch: ${vr.error ?? `HTTP ${vr.status}`}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const sr = await ctx.tailscale.acl.setPolicy(policy);
|
|
120
|
+
if (sr.ok) {
|
|
121
|
+
ctx.out.ok(`Added autoApprover for ${ctx.tag} → ${ctx.subnet}`);
|
|
122
|
+
approverReady = true;
|
|
123
|
+
}
|
|
124
|
+
else if (sr.status === 403) {
|
|
125
|
+
ctx.out.warn("API Token Lacks ACL Write Permission — Will Approve Routes Manually");
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
ctx.out.warn(`Could Not Set autoApprover: ${sr.error ?? `HTTP ${sr.status}`}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
128
131
|
}
|
|
129
|
-
|
|
132
|
+
// If the device isn't in the tailnet yet, the router hasn't
|
|
133
|
+
// authenticated. Mint an auth key and deliver it — the non-staged
|
|
134
|
+
// secrets set triggers a Fly restart. The router boots with the key,
|
|
135
|
+
// authenticates, and advertises routes. With autoApprover in place,
|
|
136
|
+
// routes are auto-approved immediately.
|
|
137
|
+
if (!ctx.device) {
|
|
138
|
+
const existing = await ctx.tailscale.devices.getByHostname(ctx.appName);
|
|
139
|
+
if (existing) {
|
|
140
|
+
ctx.device = existing;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const authKey = await ctx.tailscale.auth.createKey({
|
|
144
|
+
reusable: false,
|
|
145
|
+
ephemeral: false,
|
|
146
|
+
preauthorized: true,
|
|
147
|
+
tags: [ctx.tag],
|
|
148
|
+
});
|
|
149
|
+
ctx.out.ok("Auth Key Created");
|
|
150
|
+
const keySpinner = ctx.out.spinner("Delivering Auth Key (restarting router)");
|
|
151
|
+
await ctx.fly.secrets.set(ctx.appName, {
|
|
152
|
+
[SECRET_TAILSCALE_AUTHKEY]: authKey,
|
|
153
|
+
});
|
|
154
|
+
keySpinner.success("Auth Key Delivered");
|
|
155
|
+
if (!ctx.shouldApprove) {
|
|
156
|
+
ctx.out.dim(" Skipping Route Approval (--no-auto-approve)");
|
|
157
|
+
return Result.ok("complete");
|
|
158
|
+
}
|
|
159
|
+
ctx.device = await waitForDevice(ctx.tailscale, ctx.appName, 180000);
|
|
160
|
+
ctx.out.ok(`Router Joined Tailnet: ${ctx.device.addresses[0]}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const routes = await ctx.tailscale.routes.get(ctx.device.id);
|
|
164
|
+
if (routes && routes.unapproved.length > 0) {
|
|
165
|
+
if (approverReady) {
|
|
166
|
+
ctx.out.warn("Routes Not Auto-Approved Despite autoApprover — Approving Manually");
|
|
167
|
+
}
|
|
130
168
|
await ctx.tailscale.routes.approve(ctx.device.id, [ctx.subnet]);
|
|
131
169
|
ctx.out.ok("Subnet Routes Approved");
|
|
132
170
|
}
|
|
171
|
+
else {
|
|
172
|
+
ctx.out.ok("Routes Auto-Approved via ACL Policy");
|
|
173
|
+
}
|
|
133
174
|
return Result.ok("configure_dns");
|
|
134
175
|
}
|
|
135
176
|
case "configure_dns": {
|
|
@@ -7,7 +7,7 @@ import { checkArgs } from "../../../lib/args.js";
|
|
|
7
7
|
import { createOutput } from "../../../lib/output.js";
|
|
8
8
|
import { runMachine } from "../../../lib/machine.js";
|
|
9
9
|
import { registerCommand } from "../../mod.js";
|
|
10
|
-
import { createFlyProvider
|
|
10
|
+
import { createFlyProvider } from "../../../providers/fly.js";
|
|
11
11
|
import { getWorkloadAppName } from "../../../util/naming.js";
|
|
12
12
|
import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
|
|
13
13
|
import { resolveOrg } from "../../../util/resolve.js";
|
|
@@ -126,9 +126,7 @@ const stageSummary = (out, ctx, deployConfig) => {
|
|
|
126
126
|
}
|
|
127
127
|
out.blank()
|
|
128
128
|
.header("=".repeat(50))
|
|
129
|
-
.header(hasIssues
|
|
130
|
-
? " Deploy Completed (with Warnings)"
|
|
131
|
-
: " Deploy Completed!")
|
|
129
|
+
.header(hasIssues ? " Deploy Completed (with Warnings)" : " Deploy Completed!")
|
|
132
130
|
.header("=".repeat(50))
|
|
133
131
|
.blank()
|
|
134
132
|
.text(`App '${ctx.app}' Is Reachable from Your Tailnet as:`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/machine.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,OAAO,GACP,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE;QACL,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,SAAS,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,iBAAiB,CAAC;IACjC,KAAK,CAAC,EAAE;QACN,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AAaD,eAAO,MAAM,mBAAmB,GAC9B,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CAMrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/machine.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,OAAO,GACP,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE;QACL,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,SAAS,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,iBAAiB,CAAC;IACjC,KAAK,CAAC,EAAE;QACN,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AAaD,eAAO,MAAM,mBAAmB,GAC9B,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CAMrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CA6G7B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/app.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/app.ts"],"names":[],"mappings":"AAkLA,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAoF7D,CAAC"}
|
|
@@ -23,10 +23,12 @@ ${bold("SUBCOMMANDS")}
|
|
|
23
23
|
app Destroy a workload app on a network
|
|
24
24
|
|
|
25
25
|
${bold("OPTIONS")}
|
|
26
|
+
--manual Skip automatic Tailscale ACL cleanup (network subcommand)
|
|
26
27
|
--help Show help for a subcommand
|
|
27
28
|
|
|
28
29
|
${bold("EXAMPLES")}
|
|
29
30
|
ambit destroy network browsers
|
|
31
|
+
ambit destroy network browsers --yes
|
|
30
32
|
ambit destroy app my-app.browsers
|
|
31
33
|
ambit destroy app my-app --network browsers
|
|
32
34
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/network.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/network.ts"],"names":[],"mappings":"AAuVA,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CA0DjE,CAAC"}
|
|
@@ -7,7 +7,9 @@ import { checkArgs } from "../../../lib/args.js";
|
|
|
7
7
|
import { createOutput } from "../../../lib/output.js";
|
|
8
8
|
import { Result } from "../../../lib/result.js";
|
|
9
9
|
import { runMachine } from "../../../lib/machine.js";
|
|
10
|
-
import { findRouterApp, listWorkloadAppsOnNetwork
|
|
10
|
+
import { findRouterApp, listWorkloadAppsOnNetwork } from "../../../util/discovery.js";
|
|
11
|
+
import { getRouterTag } from "../../../util/naming.js";
|
|
12
|
+
import { isAutoApproverConfigured, isTagOwnerConfigured, unpatchAutoApprover, unpatchTagOwner, } from "../../../util/tailscale-local.js";
|
|
11
13
|
import { initSession } from "../../../util/session.js";
|
|
12
14
|
// =============================================================================
|
|
13
15
|
// Phase Labels
|
|
@@ -17,6 +19,7 @@ const DESTROY_NETWORK_PHASES = [
|
|
|
17
19
|
{ phase: "clear_dns", label: "Split DNS Cleared" },
|
|
18
20
|
{ phase: "remove_device", label: "Tailscale Device Removed" },
|
|
19
21
|
{ phase: "delete_app", label: "Fly App Destroyed" },
|
|
22
|
+
{ phase: "clean_acl", label: "ACL Policy Cleaned" },
|
|
20
23
|
];
|
|
21
24
|
const reportSkipped = (out, startPhase) => {
|
|
22
25
|
for (const { phase, label } of DESTROY_NETWORK_PHASES) {
|
|
@@ -107,6 +110,48 @@ const destroyNetworkTransition = async (phase, ctx) => {
|
|
|
107
110
|
else {
|
|
108
111
|
ctx.out.skip("Fly App Already Destroyed");
|
|
109
112
|
}
|
|
113
|
+
return Result.ok(ctx.manual ? "complete" : "clean_acl");
|
|
114
|
+
}
|
|
115
|
+
case "clean_acl": {
|
|
116
|
+
const tag = ctx.tag || getRouterTag(ctx.network);
|
|
117
|
+
let policy = await ctx.tailscale.acl.getPolicy();
|
|
118
|
+
if (!policy) {
|
|
119
|
+
ctx.out.skip("Could Not Read ACL Policy");
|
|
120
|
+
return Result.ok("complete");
|
|
121
|
+
}
|
|
122
|
+
let changed = false;
|
|
123
|
+
if (isTagOwnerConfigured(policy, tag)) {
|
|
124
|
+
policy = unpatchTagOwner(policy, tag);
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
if (isAutoApproverConfigured(policy, tag)) {
|
|
128
|
+
policy = unpatchAutoApprover(policy, tag);
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
if (changed) {
|
|
132
|
+
const validateResult = await ctx.tailscale.acl
|
|
133
|
+
.validatePolicy(policy);
|
|
134
|
+
if (!validateResult.ok) {
|
|
135
|
+
ctx.out.warn(`ACL Policy Validation Failed — Skipping Cleanup: ${validateResult.error ?? `HTTP ${validateResult.status}`}`);
|
|
136
|
+
return Result.ok("complete");
|
|
137
|
+
}
|
|
138
|
+
const spinner = ctx.out.spinner(`Removing ${tag} from ACL policy`);
|
|
139
|
+
const result = await ctx.tailscale.acl.setPolicy(policy);
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
spinner.fail(`Removing ${tag} from ACL policy`);
|
|
142
|
+
if (result.status === 403) {
|
|
143
|
+
ctx.out.warn("Your API Token Lacks ACL Write Permission. Re-run with --manual to Skip ACL Changes");
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
ctx.out.warn(`Failed to Update ACL Policy: ${result.error ?? `HTTP ${result.status}`}`);
|
|
147
|
+
}
|
|
148
|
+
return Result.ok("complete");
|
|
149
|
+
}
|
|
150
|
+
spinner.success(`Removed ${tag} from ACL policy`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
ctx.out.skip(`No ACL Entries Found for ${tag}`);
|
|
154
|
+
}
|
|
110
155
|
return Result.ok("complete");
|
|
111
156
|
}
|
|
112
157
|
default:
|
|
@@ -147,7 +192,14 @@ const stageSummary = (out, ctx) => {
|
|
|
147
192
|
workloadAppsWarned: 0,
|
|
148
193
|
});
|
|
149
194
|
out.ok("Router Destroyed");
|
|
150
|
-
|
|
195
|
+
const tag = ctx.tag || getRouterTag(ctx.network);
|
|
196
|
+
if (!ctx.manual) {
|
|
197
|
+
out.blank()
|
|
198
|
+
.dim("If You Added ACL Rules Referencing This Router, Remember to Remove:")
|
|
199
|
+
.dim(` acls: rules referencing ${tag}`)
|
|
200
|
+
.blank();
|
|
201
|
+
}
|
|
202
|
+
else if (ctx.tag) {
|
|
151
203
|
out.blank()
|
|
152
204
|
.dim("If You Added ACL Policy Entries for This Router, Remember to Remove:")
|
|
153
205
|
.dim(` tagOwners: ${ctx.tag}`)
|
|
@@ -169,7 +221,7 @@ const stageSummary = (out, ctx) => {
|
|
|
169
221
|
export const destroyNetwork = async (argv) => {
|
|
170
222
|
const opts = {
|
|
171
223
|
string: ["network", "org"],
|
|
172
|
-
boolean: ["help", "yes", "json"],
|
|
224
|
+
boolean: ["help", "yes", "json", "manual"],
|
|
173
225
|
alias: { y: "yes" },
|
|
174
226
|
};
|
|
175
227
|
const args = parseArgs(argv, opts);
|
|
@@ -183,17 +235,25 @@ ${bold("USAGE")}
|
|
|
183
235
|
|
|
184
236
|
${bold("OPTIONS")}
|
|
185
237
|
--org <org> Fly.io organization slug
|
|
238
|
+
--manual Skip automatic Tailscale ACL cleanup (tagOwners + autoApprovers)
|
|
186
239
|
-y, --yes Skip confirmation prompts
|
|
187
240
|
--json Output as JSON
|
|
188
241
|
|
|
242
|
+
${bold("DESCRIPTION")}
|
|
243
|
+
By default, ambit removes the router's tag from your Tailscale ACL
|
|
244
|
+
policy (tagOwners and autoApprovers). Use --manual if your API token
|
|
245
|
+
lacks ACL write permission or you prefer to manage the policy yourself.
|
|
246
|
+
|
|
189
247
|
${bold("EXAMPLES")}
|
|
190
248
|
ambit destroy network browsers
|
|
191
|
-
ambit destroy network browsers --
|
|
249
|
+
ambit destroy network browsers --yes
|
|
250
|
+
ambit destroy network browsers --manual --org my-org
|
|
192
251
|
`);
|
|
193
252
|
return;
|
|
194
253
|
}
|
|
195
254
|
const out = createOutput(args.json);
|
|
196
|
-
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
255
|
+
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
256
|
+
args.network;
|
|
197
257
|
if (!network) {
|
|
198
258
|
return out.die("Network Name Required. Usage: ambit destroy network <name>");
|
|
199
259
|
}
|
|
@@ -206,5 +266,6 @@ ${bold("EXAMPLES")}
|
|
|
206
266
|
org,
|
|
207
267
|
yes: args.yes,
|
|
208
268
|
json: args.json,
|
|
269
|
+
manual: !!args.manual,
|
|
209
270
|
});
|
|
210
271
|
};
|