@cardelli/ambit 0.2.3 → 0.3.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 +38 -22
- package/esm/cli/commands/create/index.js +64 -10
- package/esm/cli/commands/create/machine.d.ts.map +1 -1
- package/esm/cli/commands/create/machine.js +1 -1
- 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/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
15
|
import { FLY_PRIVATE_SUBNET, 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, patchAutoApprover, 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,8 +104,29 @@ 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) {
|
|
111
|
+
const hasApprover = isAutoApproverConfigured(policy, opts.tag);
|
|
112
|
+
if (!hasApprover && policy) {
|
|
113
|
+
const beforeApprover = policy;
|
|
114
|
+
policy = patchAutoApprover(policy, opts.tag, FLY_PRIVATE_SUBNET);
|
|
115
|
+
assertAdditivePatch(beforeApprover, policy);
|
|
116
|
+
const validateApprover = await tailscale.acl.validatePolicy(policy);
|
|
117
|
+
if (!validateApprover.ok) {
|
|
118
|
+
return handleAclSetFailure(out, validateApprover, `Validating autoApprover patch for ${opts.tag}`);
|
|
119
|
+
}
|
|
120
|
+
const approverSpinner = out.spinner(`Adding autoApprover for ${opts.tag}`);
|
|
121
|
+
const result = await tailscale.acl.setPolicy(policy);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
approverSpinner.fail(`Adding autoApprover for ${opts.tag}`);
|
|
124
|
+
return handleAclSetFailure(out, result, `Adding autoApprover for ${opts.tag}`);
|
|
125
|
+
}
|
|
126
|
+
approverSpinner.success(`Added autoApprover for ${opts.tag}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (opts.json) {
|
|
85
130
|
const approverSpinner = out.spinner(`Checking autoApprovers for ${opts.tag}`);
|
|
86
131
|
const hasApprover = isAutoApproverConfigured(policy, opts.tag);
|
|
87
132
|
if (!hasApprover) {
|
|
@@ -194,7 +239,8 @@ const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
|
194
239
|
.blank()
|
|
195
240
|
.dim(" You can do this from the Tailscale dashboard:")
|
|
196
241
|
.link(" https://login.tailscale.com/admin/acls/visual/general-access-rules")
|
|
197
|
-
.dim(` Source: group:YOUR_GROUP
|
|
242
|
+
.dim(` Source: group:YOUR_GROUP Destination: ${opts.tag}:53`)
|
|
243
|
+
.dim(` Source: group:YOUR_GROUP Destination: ${ctx.subnet}:*`)
|
|
198
244
|
.blank()
|
|
199
245
|
.dim(" Or you can do it manually with this JSON config:")
|
|
200
246
|
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${opts.tag}:53"]}`)
|
|
@@ -214,7 +260,7 @@ const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
|
214
260
|
const create = async (argv) => {
|
|
215
261
|
const opts = {
|
|
216
262
|
string: ["org", "region", "api-key", "tag"],
|
|
217
|
-
boolean: ["help", "yes", "json", "no-auto-approve"],
|
|
263
|
+
boolean: ["help", "yes", "json", "no-auto-approve", "manual"],
|
|
218
264
|
alias: { y: "yes" },
|
|
219
265
|
};
|
|
220
266
|
const args = parseArgs(argv, opts);
|
|
@@ -231,7 +277,8 @@ ${bold("OPTIONS")}
|
|
|
231
277
|
--region <region> Fly.io region (default: iad)
|
|
232
278
|
--api-key <key> Tailscale API access token (tskey-api-...)
|
|
233
279
|
--tag <tag> Tailscale ACL tag for the router (default: tag:ambit-<network>)
|
|
234
|
-
--
|
|
280
|
+
--manual Skip automatic Tailscale ACL configuration (tagOwners + autoApprovers)
|
|
281
|
+
--no-auto-approve Skip waiting for router and approving routes
|
|
235
282
|
-y, --yes Skip confirmation prompts
|
|
236
283
|
--json Output as JSON (implies --no-auto-approve)
|
|
237
284
|
|
|
@@ -241,9 +288,14 @@ ${bold("DESCRIPTION")}
|
|
|
241
288
|
|
|
242
289
|
my-app.${args._[0] || "<network>"} resolves to my-app.flycast
|
|
243
290
|
|
|
291
|
+
By default, ambit auto-configures your Tailscale ACL policy (tagOwners
|
|
292
|
+
and autoApprovers). Use --manual if your API token lacks ACL write
|
|
293
|
+
permission or you prefer to manage the policy yourself.
|
|
294
|
+
|
|
244
295
|
${bold("EXAMPLES")}
|
|
245
296
|
ambit create browsers
|
|
246
297
|
ambit create browsers --org my-org --region sea
|
|
298
|
+
ambit create browsers --manual
|
|
247
299
|
`);
|
|
248
300
|
return;
|
|
249
301
|
}
|
|
@@ -257,7 +309,8 @@ ${bold("EXAMPLES")}
|
|
|
257
309
|
return out.die(`"${network}" Is a Public TLD and Cannot Be Used as a Network Name`);
|
|
258
310
|
}
|
|
259
311
|
const tag = args.tag || getRouterTag(network);
|
|
260
|
-
const
|
|
312
|
+
const manual = !!args.manual;
|
|
313
|
+
const shouldApprove = !manual || !(args["no-auto-approve"] || args.json);
|
|
261
314
|
out.blank()
|
|
262
315
|
.header("=".repeat(50))
|
|
263
316
|
.header(` ambit Create: ${network}`)
|
|
@@ -270,6 +323,7 @@ ${bold("EXAMPLES")}
|
|
|
270
323
|
});
|
|
271
324
|
const tailscale = await stageTailscaleConfig(out, {
|
|
272
325
|
json: args.json,
|
|
326
|
+
manual,
|
|
273
327
|
apiKey: args["api-key"],
|
|
274
328
|
tag,
|
|
275
329
|
network,
|
|
@@ -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;AAS7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAOrE,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,eAAe,GACf,cAAc,GACd,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,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,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,CAoH7B,CAAC"}
|
|
@@ -5,7 +5,7 @@ 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
10
|
import { enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, waitForDevice, } from "../../../util/tailscale-local.js";
|
|
11
11
|
import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
|
|
@@ -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
|
};
|
|
@@ -130,7 +130,9 @@ const stageSummary = (out, results) => {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
out.blank();
|
|
133
|
-
out.text(issues === 0
|
|
133
|
+
out.text(issues === 0
|
|
134
|
+
? "All Checks Passed."
|
|
135
|
+
: `${issues} Issue${issues > 1 ? "s" : ""} Found.`);
|
|
134
136
|
out.blank();
|
|
135
137
|
out.print();
|
|
136
138
|
};
|
|
@@ -138,7 +140,10 @@ const stageSummary = (out, results) => {
|
|
|
138
140
|
// Doctor Network
|
|
139
141
|
// =============================================================================
|
|
140
142
|
const doctorNetwork = async (argv) => {
|
|
141
|
-
const opts = {
|
|
143
|
+
const opts = {
|
|
144
|
+
string: ["network", "org"],
|
|
145
|
+
boolean: ["help", "json"],
|
|
146
|
+
};
|
|
142
147
|
const args = parseArgs(argv, opts);
|
|
143
148
|
checkArgs(args, opts, "ambit doctor network");
|
|
144
149
|
if (args.help) {
|
|
@@ -168,7 +173,8 @@ ${bold("CHECKS")}
|
|
|
168
173
|
out.blank().header("ambit Doctor: Network").blank();
|
|
169
174
|
const results = [];
|
|
170
175
|
const report = makeReporter(results, out);
|
|
171
|
-
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
176
|
+
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
177
|
+
args.network;
|
|
172
178
|
const { fly, tailscale, org } = await initSession(out, {
|
|
173
179
|
json: args.json,
|
|
174
180
|
org: args.org,
|
|
@@ -181,7 +187,10 @@ ${bold("CHECKS")}
|
|
|
181
187
|
// Doctor App
|
|
182
188
|
// =============================================================================
|
|
183
189
|
const doctorApp = async (argv) => {
|
|
184
|
-
const opts = {
|
|
190
|
+
const opts = {
|
|
191
|
+
string: ["network", "org"],
|
|
192
|
+
boolean: ["help", "json"],
|
|
193
|
+
};
|
|
185
194
|
const args = parseArgs(argv, opts);
|
|
186
195
|
checkArgs(args, opts, "ambit doctor app");
|
|
187
196
|
if (args.help) {
|
package/esm/cli/commands/list.js
CHANGED
|
@@ -7,7 +7,7 @@ import { bold } from "../../lib/cli.js";
|
|
|
7
7
|
import { checkArgs } from "../../lib/args.js";
|
|
8
8
|
import { createOutput } from "../../lib/output.js";
|
|
9
9
|
import { registerCommand } from "../mod.js";
|
|
10
|
-
import { discoverRouters
|
|
10
|
+
import { discoverRouters } from "../../util/discovery.js";
|
|
11
11
|
import { initSession } from "../../util/session.js";
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// Stage: Render
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/share.ts"],"names":[],"mappings":""}
|