@cardelli/ambit 0.1.4 → 0.1.5
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/esm/deno.js +1 -1
- package/esm/deps/jsr.io/@zod/zod/4.3.6/src/v4/core/json-schema-generator.d.ts +1 -1
- package/esm/lib/cli.d.ts +0 -1
- package/esm/lib/cli.d.ts.map +1 -1
- package/esm/lib/cli.js +41 -26
- package/esm/lib/command.d.ts +23 -48
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +60 -89
- package/esm/lib/output.d.ts +1 -1
- package/esm/lib/output.d.ts.map +1 -1
- package/esm/lib/output.js +5 -9
- package/esm/lib/result.d.ts +19 -7
- package/esm/lib/result.d.ts.map +1 -1
- package/esm/lib/result.js +47 -1
- package/esm/src/cli/commands/create.js +110 -96
- package/esm/src/cli/commands/deploy.js +17 -13
- package/esm/src/cli/commands/destroy.js +3 -3
- package/esm/src/cli/commands/doctor.js +1 -1
- package/esm/src/discovery.d.ts +4 -1
- package/esm/src/discovery.d.ts.map +1 -1
- package/esm/src/discovery.js +15 -1
- package/esm/src/docker/router/start.sh +18 -72
- package/esm/src/providers/fly.d.ts +6 -0
- package/esm/src/providers/fly.d.ts.map +1 -1
- package/esm/src/providers/fly.js +77 -81
- package/esm/src/providers/tailscale.d.ts.map +1 -1
- package/esm/src/providers/tailscale.js +5 -11
- package/esm/src/template.d.ts +3 -4
- package/esm/src/template.d.ts.map +1 -1
- package/esm/src/template.js +15 -17
- package/package.json +7 -1
|
@@ -11,13 +11,14 @@ import { createFlyProvider, FlyDeployError, getRouterAppName, } from "../../prov
|
|
|
11
11
|
import { createTailscaleProvider, enableAcceptRoutes, isAcceptRoutesEnabled, isTailscaleInstalled, waitForDevice, } from "../../providers/tailscale.js";
|
|
12
12
|
import { getCredentialStore } from "../../credentials.js";
|
|
13
13
|
import { resolveOrg } from "../../resolve.js";
|
|
14
|
+
import { findRouterApp } from "../../discovery.js";
|
|
14
15
|
// =============================================================================
|
|
15
16
|
// Create Command
|
|
16
17
|
// =============================================================================
|
|
17
18
|
const create = async (argv) => {
|
|
18
19
|
const args = parseArgs(argv, {
|
|
19
20
|
string: ["org", "region", "api-key", "tag"],
|
|
20
|
-
boolean: ["help", "yes", "json", "
|
|
21
|
+
boolean: ["help", "yes", "json", "no-auto-approve"],
|
|
21
22
|
alias: { y: "yes" },
|
|
22
23
|
});
|
|
23
24
|
if (args.help) {
|
|
@@ -32,9 +33,9 @@ ${bold("OPTIONS")}
|
|
|
32
33
|
--region <region> Fly.io region (default: iad)
|
|
33
34
|
--api-key <key> Tailscale API access token (tskey-api-...)
|
|
34
35
|
--tag <tag> Tailscale ACL tag for the router (default: tag:ambit-<network>)
|
|
35
|
-
--
|
|
36
|
+
--no-auto-approve Skip waiting for router and approving routes
|
|
36
37
|
-y, --yes Skip confirmation prompts
|
|
37
|
-
--json Output as JSON
|
|
38
|
+
--json Output as JSON (implies --no-auto-approve)
|
|
38
39
|
|
|
39
40
|
${bold("DESCRIPTION")}
|
|
40
41
|
Deploys a Tailscale subnet router onto a Fly.io custom private network.
|
|
@@ -45,7 +46,6 @@ ${bold("DESCRIPTION")}
|
|
|
45
46
|
${bold("EXAMPLES")}
|
|
46
47
|
ambit create browsers
|
|
47
48
|
ambit create browsers --org my-org --region sea
|
|
48
|
-
ambit create browsers --self-approve
|
|
49
49
|
`);
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
@@ -59,7 +59,7 @@ ${bold("EXAMPLES")}
|
|
|
59
59
|
return out.die(`"${network}" Is a Public TLD and Cannot Be Used as a Network Name`);
|
|
60
60
|
}
|
|
61
61
|
const tag = args.tag || getRouterTag(network);
|
|
62
|
-
const
|
|
62
|
+
const shouldApprove = !(args["no-auto-approve"] || args.json);
|
|
63
63
|
out.blank()
|
|
64
64
|
.header("=".repeat(50))
|
|
65
65
|
.header(` ambit Create: ${network}`)
|
|
@@ -75,7 +75,13 @@ ${bold("EXAMPLES")}
|
|
|
75
75
|
out.ok(`Authenticated as ${email}`);
|
|
76
76
|
const org = await resolveOrg(fly, args, out);
|
|
77
77
|
const region = args.region || "iad";
|
|
78
|
-
out.ok(`Using Region: ${region}`)
|
|
78
|
+
out.ok(`Using Region: ${region}`);
|
|
79
|
+
const existingRouter = await findRouterApp(fly, org, network);
|
|
80
|
+
if (existingRouter) {
|
|
81
|
+
return out.die(`A Router Already Exists for Network "${network}": ${existingRouter.appName}. ` +
|
|
82
|
+
`Use "ambit destroy ${network}" First, or Choose a Different Network Name.`);
|
|
83
|
+
}
|
|
84
|
+
out.blank();
|
|
79
85
|
// ==========================================================================
|
|
80
86
|
// Step 2: Tailscale Configuration
|
|
81
87
|
// ==========================================================================
|
|
@@ -86,8 +92,8 @@ ${bold("EXAMPLES")}
|
|
|
86
92
|
if (args.json) {
|
|
87
93
|
return out.die("--api-key Is Required in JSON Mode");
|
|
88
94
|
}
|
|
89
|
-
out.dim("
|
|
90
|
-
.dim("Create
|
|
95
|
+
out.dim("Ambit Needs an API Access Token (Not an Auth Key) to Manage Your Tailnet.")
|
|
96
|
+
.dim("Create One at: https://login.tailscale.com/admin/settings/keys")
|
|
91
97
|
.blank();
|
|
92
98
|
apiKey = await readSecret("API access token (tskey-api-...): ");
|
|
93
99
|
if (!apiKey) {
|
|
@@ -114,10 +120,10 @@ ${bold("EXAMPLES")}
|
|
|
114
120
|
if (!hasTagOwner) {
|
|
115
121
|
tagOwnerSpinner.fail(`Tag ${tag} Not Configured in tagOwners`);
|
|
116
122
|
out.blank()
|
|
117
|
-
.text(` The
|
|
118
|
-
.text(" Tailscale
|
|
123
|
+
.text(` The Tag ${tag} Does Not Exist in Your Tailscale ACL tagOwners.`)
|
|
124
|
+
.text(" Tailscale Will Reject Auth Keys for Undefined Tags.")
|
|
119
125
|
.blank()
|
|
120
|
-
.text(" Add
|
|
126
|
+
.text(" Add This Tag in Your Tailscale ACL Settings:")
|
|
121
127
|
.dim(" https://login.tailscale.com/admin/acls/visual/tags")
|
|
122
128
|
.blank()
|
|
123
129
|
.dim(` "tagOwners": { "${tag}": ["autogroup:admin"] }`)
|
|
@@ -125,46 +131,31 @@ ${bold("EXAMPLES")}
|
|
|
125
131
|
return out.die(`Add ${tag} to tagOwners Before Creating Router`);
|
|
126
132
|
}
|
|
127
133
|
tagOwnerSpinner.success(`Tag ${tag} Configured in tagOwners`);
|
|
128
|
-
// ==========================================================================
|
|
129
|
-
// Step 2.6: Check autoApprovers
|
|
130
|
-
// ==========================================================================
|
|
131
|
-
if (!selfApprove) {
|
|
132
|
-
const autoApproverSpinner = out.spinner("Checking autoApprovers Configuration");
|
|
133
|
-
const hasAutoApprover = await tailscale.isAutoApproverConfigured(tag);
|
|
134
|
-
if (!hasAutoApprover) {
|
|
135
|
-
autoApproverSpinner.fail("autoApprovers Not Configured");
|
|
136
|
-
out.blank()
|
|
137
|
-
.text(` The tag ${tag} is not listed in your Tailscale ACL autoApprovers.`)
|
|
138
|
-
.text(" Routes advertised by the router will not be auto-approved.")
|
|
139
|
-
.blank()
|
|
140
|
-
.text(" Either configure autoApprovers in your Tailscale ACL policy:")
|
|
141
|
-
.dim(` "autoApprovers": { "routes": { "fdaa:X:XXXX::/48": ["${tag}"] } }`)
|
|
142
|
-
.blank()
|
|
143
|
-
.text(" Or re-run with --self-approve to approve routes via API:")
|
|
144
|
-
.dim(` ambit create ${network} --self-approve`)
|
|
145
|
-
.blank();
|
|
146
|
-
return out.die("Configure autoApprovers or Use --self-approve");
|
|
147
|
-
}
|
|
148
|
-
autoApproverSpinner.success("autoApprovers Configured");
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
out.info("Self-Approve Mode: Routes Will Be Approved via API");
|
|
152
|
-
}
|
|
153
134
|
out.blank();
|
|
154
135
|
// ==========================================================================
|
|
155
136
|
// Step 3: Deploy Router on Custom 6PN
|
|
156
137
|
// ==========================================================================
|
|
138
|
+
let hasAutoApprover = false;
|
|
157
139
|
out.header("Step 3: Deploy Subnet Router").blank();
|
|
158
|
-
const
|
|
140
|
+
const suffix = randomId(8);
|
|
141
|
+
const routerAppName = getRouterAppName(network, suffix);
|
|
159
142
|
out.info(`Creating Router App: ${routerAppName}`)
|
|
160
143
|
.info(`Custom Network: ${network}`)
|
|
161
144
|
.info(`Router Tag: ${tag}`);
|
|
162
145
|
await fly.createApp(routerAppName, org, { network });
|
|
163
146
|
out.ok(`Created App: ${routerAppName}`);
|
|
147
|
+
const authKeySpinner = out.spinner("Creating Tag-Scoped Auth Key");
|
|
148
|
+
const authKey = await tailscale.createAuthKey({
|
|
149
|
+
reusable: false,
|
|
150
|
+
ephemeral: false,
|
|
151
|
+
preauthorized: true,
|
|
152
|
+
tags: [tag],
|
|
153
|
+
});
|
|
154
|
+
authKeySpinner.success("Auth Key Created (Single-Use, 5min Expiry)");
|
|
164
155
|
await fly.setSecrets(routerAppName, {
|
|
165
|
-
|
|
156
|
+
TAILSCALE_AUTHKEY: authKey,
|
|
166
157
|
NETWORK_NAME: network,
|
|
167
|
-
|
|
158
|
+
ROUTER_ID: suffix,
|
|
168
159
|
}, { stage: true });
|
|
169
160
|
out.ok("Set Router Secrets");
|
|
170
161
|
const dockerDir = new URL("../../docker/router", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url).pathname;
|
|
@@ -180,61 +171,79 @@ ${bold("EXAMPLES")}
|
|
|
180
171
|
throw e;
|
|
181
172
|
}
|
|
182
173
|
out.ok("Router Deployed");
|
|
183
|
-
out.blank();
|
|
184
|
-
const joinSpinner = out.spinner("Waiting for Router to Join Tailnet");
|
|
185
|
-
const device = await waitForDevice(tailscale, routerAppName, 180000);
|
|
186
|
-
joinSpinner.success(`Router Joined Tailnet: ${device.addresses[0]}`);
|
|
187
|
-
const machines = await fly.listMachines(routerAppName);
|
|
188
|
-
const routerMachine = machines.find((m) => m.private_ip);
|
|
189
|
-
const subnet = routerMachine?.private_ip
|
|
190
|
-
? extractSubnet(routerMachine.private_ip)
|
|
191
|
-
: null;
|
|
192
|
-
if (subnet) {
|
|
193
|
-
out.ok(`Subnet: ${subnet}`);
|
|
194
|
-
}
|
|
195
|
-
if (selfApprove && subnet) {
|
|
196
|
-
const approveSpinner = out.spinner("Approving Subnet Routes via API");
|
|
197
|
-
await tailscale.approveSubnetRoutes(device.id, [subnet]);
|
|
198
|
-
approveSpinner.success("Subnet Routes Approved via API");
|
|
199
|
-
}
|
|
200
|
-
if (device.advertisedRoutes && device.advertisedRoutes.length > 0) {
|
|
201
|
-
out.ok(`Routes: ${device.advertisedRoutes.join(", ")}`);
|
|
202
|
-
}
|
|
203
|
-
const dnsSpinner = out.spinner("Configuring Split DNS");
|
|
204
|
-
await tailscale.setSplitDns(network, [device.addresses[0]]);
|
|
205
|
-
dnsSpinner.success(`Split DNS Configured: *.${network} -> Router`);
|
|
206
174
|
// ==========================================================================
|
|
207
|
-
// Step 4:
|
|
175
|
+
// Step 4: Wait for Router, Approve Routes, Configure DNS
|
|
208
176
|
// ==========================================================================
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
177
|
+
let device = null;
|
|
178
|
+
let routerMachine;
|
|
179
|
+
let subnet = null;
|
|
180
|
+
if (shouldApprove) {
|
|
181
|
+
out.blank();
|
|
182
|
+
const joinSpinner = out.spinner("Waiting for Router to Join Tailnet");
|
|
183
|
+
device = await waitForDevice(tailscale, routerAppName, 180000);
|
|
184
|
+
joinSpinner.success(`Router Joined Tailnet: ${device.addresses[0]}`);
|
|
185
|
+
const machines = await fly.listMachines(routerAppName);
|
|
186
|
+
routerMachine = machines.find((m) => m.private_ip);
|
|
187
|
+
subnet = routerMachine?.private_ip
|
|
188
|
+
? extractSubnet(routerMachine.private_ip)
|
|
189
|
+
: null;
|
|
190
|
+
if (subnet) {
|
|
191
|
+
out.ok(`Subnet: ${subnet}`);
|
|
192
|
+
hasAutoApprover = await tailscale.isAutoApproverConfigured(tag);
|
|
193
|
+
if (!hasAutoApprover) {
|
|
194
|
+
const approveSpinner = out.spinner("Approving Subnet Routes");
|
|
195
|
+
await tailscale.approveSubnetRoutes(device.id, [subnet]);
|
|
196
|
+
approveSpinner.success("Subnet Routes Approved");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
out.ok("Routes Auto-Approved via ACL Policy");
|
|
200
|
+
}
|
|
213
201
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
202
|
+
if (device.advertisedRoutes && device.advertisedRoutes.length > 0) {
|
|
203
|
+
out.ok(`Routes: ${device.advertisedRoutes.join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
const dnsSpinner = out.spinner("Configuring Split DNS");
|
|
206
|
+
await tailscale.setSplitDns(network, [device.addresses[0]]);
|
|
207
|
+
dnsSpinner.success(`Split DNS Configured: *.${network} -> Router`);
|
|
208
|
+
// ========================================================================
|
|
209
|
+
// Step 5: Local Client Configuration
|
|
210
|
+
// ========================================================================
|
|
211
|
+
out.blank().header("Step 5: Local Client Configuration").blank();
|
|
212
|
+
if (await isTailscaleInstalled()) {
|
|
213
|
+
if (await isAcceptRoutesEnabled()) {
|
|
214
|
+
out.ok("Accept Routes Already Enabled");
|
|
218
215
|
}
|
|
219
216
|
else {
|
|
220
|
-
routeSpinner.
|
|
221
|
-
|
|
222
|
-
.
|
|
223
|
-
|
|
217
|
+
const routeSpinner = out.spinner("Enabling Accept Routes");
|
|
218
|
+
if (await enableAcceptRoutes()) {
|
|
219
|
+
routeSpinner.success("Accept Routes Enabled");
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
routeSpinner.fail("Could Not Enable Accept Routes");
|
|
223
|
+
out.blank()
|
|
224
|
+
.dim("Run Manually With Elevated Permissions:")
|
|
225
|
+
.dim(" sudo tailscale set --accept-routes");
|
|
226
|
+
}
|
|
224
227
|
}
|
|
225
228
|
}
|
|
229
|
+
else {
|
|
230
|
+
out.warn("Tailscale CLI Not Found")
|
|
231
|
+
.dim(" Ensure Accept-Routes is Enabled on This Device");
|
|
232
|
+
}
|
|
226
233
|
}
|
|
227
234
|
else {
|
|
228
|
-
out.
|
|
229
|
-
.dim(" Ensure Accept-Routes Is Enabled on This Device");
|
|
235
|
+
out.blank().info("Skipping Route Approval and DNS Configuration (Use ambit doctor to Verify Later)");
|
|
230
236
|
}
|
|
231
237
|
// ==========================================================================
|
|
232
238
|
// Done
|
|
233
239
|
// ==========================================================================
|
|
234
240
|
out.done({
|
|
235
241
|
network,
|
|
236
|
-
router: {
|
|
237
|
-
|
|
242
|
+
router: {
|
|
243
|
+
appName: routerAppName,
|
|
244
|
+
tailscaleIp: device?.addresses[0] ?? "pending",
|
|
245
|
+
},
|
|
246
|
+
subnet: subnet || "pending",
|
|
238
247
|
tag,
|
|
239
248
|
});
|
|
240
249
|
out.blank()
|
|
@@ -244,43 +253,48 @@ ${bold("EXAMPLES")}
|
|
|
244
253
|
.blank()
|
|
245
254
|
.text(`Any Flycast app on the "${network}" network is reachable as:`)
|
|
246
255
|
.text(` <app-name>.${network}`)
|
|
247
|
-
.blank()
|
|
248
|
-
|
|
249
|
-
.text(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
.blank();
|
|
257
|
+
if (routerMachine?.private_ip) {
|
|
258
|
+
out.text("SOCKS5 Proxy Available at:")
|
|
259
|
+
.text(` socks5://[${routerMachine.private_ip}]:1080`)
|
|
260
|
+
.dim("Containers on This Network Can Use It to Reach Your Tailnet.")
|
|
261
|
+
.blank();
|
|
262
|
+
}
|
|
263
|
+
out.dim("Deploy an App to This Network:")
|
|
253
264
|
.dim(` ambit deploy my-app --network ${network}`)
|
|
254
265
|
.blank()
|
|
255
|
-
.dim("Invite
|
|
266
|
+
.dim("Invite People to Your Tailnet:")
|
|
256
267
|
.dim(" https://login.tailscale.com/admin/users")
|
|
257
|
-
.dim("Control
|
|
268
|
+
.dim("Control Their Access:")
|
|
258
269
|
.dim(" https://login.tailscale.com/admin/acls/visual/general-access-rules")
|
|
259
270
|
.blank();
|
|
260
|
-
if (subnet &&
|
|
261
|
-
out.header("Recommended
|
|
271
|
+
if (subnet && !hasAutoApprover) {
|
|
272
|
+
out.header("Recommended: Configure autoApprovers")
|
|
262
273
|
.blank()
|
|
263
|
-
.dim(" Add
|
|
274
|
+
.dim(" Add to Your Tailnet Policy File at:")
|
|
264
275
|
.dim(" https://login.tailscale.com/admin/acls/file")
|
|
265
276
|
.blank()
|
|
266
|
-
.text(` "tagOwners": { "${tag}": ["autogroup:admin"] }`)
|
|
267
277
|
.text(` "autoApprovers": { "routes": { "${subnet}": ["${tag}"] } }`)
|
|
268
278
|
.blank()
|
|
269
|
-
.dim("
|
|
270
|
-
.dim(
|
|
271
|
-
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${subnet}:*"]}`)
|
|
279
|
+
.dim(" Routes Were Approved via API for This Session.")
|
|
280
|
+
.dim(" autoApprovers Will Auto-Approve on Future Restarts.")
|
|
272
281
|
.blank();
|
|
273
282
|
}
|
|
274
|
-
|
|
283
|
+
if (subnet) {
|
|
275
284
|
out.header("Recommended ACL Rules:")
|
|
276
285
|
.blank()
|
|
277
|
-
.dim(" To
|
|
286
|
+
.dim(" To Restrict Access, Add ACL Rules to Your Policy File:")
|
|
278
287
|
.dim(" https://login.tailscale.com/admin/acls/file")
|
|
279
288
|
.blank()
|
|
280
289
|
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${tag}:53"]}`)
|
|
281
290
|
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${subnet}:*"]}`)
|
|
282
291
|
.blank();
|
|
283
292
|
}
|
|
293
|
+
if (!shouldApprove) {
|
|
294
|
+
out.dim("Route Approval Was Skipped. To Complete Setup:")
|
|
295
|
+
.dim(` ambit doctor --network ${network}`)
|
|
296
|
+
.blank();
|
|
297
|
+
}
|
|
284
298
|
out.print();
|
|
285
299
|
};
|
|
286
300
|
// =============================================================================
|
|
@@ -7,7 +7,7 @@ import { join } from "../../../deps/jsr.io/@std/path/1.1.4/mod.js";
|
|
|
7
7
|
import { bold, confirm, fileExists } from "../../../lib/cli.js";
|
|
8
8
|
import { createOutput } from "../../../lib/output.js";
|
|
9
9
|
import { registerCommand } from "../mod.js";
|
|
10
|
-
import { createFlyProvider, FlyDeployError } from "../../providers/fly.js";
|
|
10
|
+
import { createFlyProvider, FlyDeployError, getWorkloadAppName, } from "../../providers/fly.js";
|
|
11
11
|
import { findRouterApp } from "../../discovery.js";
|
|
12
12
|
import { resolveOrg } from "../../resolve.js";
|
|
13
13
|
import { assertNotRouter, auditDeploy, scanFlyToml } from "../../guard.js";
|
|
@@ -111,19 +111,20 @@ const resolveTemplateMode = async (templateRaw, out) => {
|
|
|
111
111
|
const result = await fetchTemplate(ref);
|
|
112
112
|
if (!result.ok) {
|
|
113
113
|
fetchSpinner.fail("Template Fetch Failed");
|
|
114
|
-
out.die(result.
|
|
114
|
+
out.die(result.error);
|
|
115
115
|
return null;
|
|
116
116
|
}
|
|
117
|
+
const { tempDir, templateDir } = result.value;
|
|
117
118
|
fetchSpinner.success("Template Fetched");
|
|
118
119
|
// Find and scan the template's fly.toml
|
|
119
|
-
const configPath = join(
|
|
120
|
+
const configPath = join(templateDir, "fly.toml");
|
|
120
121
|
let tomlContent;
|
|
121
122
|
try {
|
|
122
123
|
tomlContent = await dntShim.Deno.readTextFile(configPath);
|
|
123
124
|
}
|
|
124
125
|
catch {
|
|
125
126
|
try {
|
|
126
|
-
dntShim.Deno.removeSync(
|
|
127
|
+
dntShim.Deno.removeSync(tempDir, { recursive: true });
|
|
127
128
|
}
|
|
128
129
|
catch { /* ignore */ }
|
|
129
130
|
out.die(`Template '${ref.path === "." ? ref.repo : ref.path}' Has No fly.toml`);
|
|
@@ -132,7 +133,7 @@ const resolveTemplateMode = async (templateRaw, out) => {
|
|
|
132
133
|
const scan = scanFlyToml(tomlContent);
|
|
133
134
|
if (scan.errors.length > 0) {
|
|
134
135
|
try {
|
|
135
|
-
dntShim.Deno.removeSync(
|
|
136
|
+
dntShim.Deno.removeSync(tempDir, { recursive: true });
|
|
136
137
|
}
|
|
137
138
|
catch { /* ignore */ }
|
|
138
139
|
for (const err of scan.errors) {
|
|
@@ -148,7 +149,7 @@ const resolveTemplateMode = async (templateRaw, out) => {
|
|
|
148
149
|
return {
|
|
149
150
|
configPath,
|
|
150
151
|
preflight: { scanned: scan.scanned, warnings: scan.warnings },
|
|
151
|
-
tempDir
|
|
152
|
+
tempDir,
|
|
152
153
|
};
|
|
153
154
|
};
|
|
154
155
|
// =============================================================================
|
|
@@ -294,27 +295,29 @@ ${bold("EXAMPLES")}
|
|
|
294
295
|
`Run 'ambit create ${network}' first.`);
|
|
295
296
|
}
|
|
296
297
|
routerSpinner.success(`Router Found: ${router.appName}`);
|
|
298
|
+
const routerId = router.routerId;
|
|
299
|
+
const flyAppName = getWorkloadAppName(app, routerId);
|
|
297
300
|
out.blank();
|
|
298
301
|
// ==========================================================================
|
|
299
302
|
// Phase 3: App Creation (if needed)
|
|
300
303
|
// ==========================================================================
|
|
301
304
|
out.header("Step 3: App Setup").blank();
|
|
302
305
|
let created = false;
|
|
303
|
-
const exists = await fly.appExists(
|
|
306
|
+
const exists = await fly.appExists(flyAppName);
|
|
304
307
|
if (exists) {
|
|
305
|
-
out.ok(`App '${
|
|
308
|
+
out.ok(`App '${flyAppName}' Exists`);
|
|
306
309
|
}
|
|
307
310
|
else {
|
|
308
|
-
out.info(`App '${
|
|
311
|
+
out.info(`App '${flyAppName}' Does Not Exist — Will Create on Network '${network}'`);
|
|
309
312
|
if (!args.yes && !args.json) {
|
|
310
|
-
const confirmed = await confirm(`Create app '${
|
|
313
|
+
const confirmed = await confirm(`Create app '${flyAppName}' on network '${network}'?`);
|
|
311
314
|
if (!confirmed) {
|
|
312
315
|
out.text("Cancelled.");
|
|
313
316
|
return;
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
|
-
await fly.createApp(app, org, { network });
|
|
317
|
-
out.ok(`Created App '${
|
|
319
|
+
await fly.createApp(app, org, { network, routerId });
|
|
320
|
+
out.ok(`Created App '${flyAppName}' on Network '${network}'`);
|
|
318
321
|
created = true;
|
|
319
322
|
}
|
|
320
323
|
out.blank();
|
|
@@ -342,6 +345,7 @@ ${bold("EXAMPLES")}
|
|
|
342
345
|
out.dim("Deploying with --no-public-ips --flycast ...");
|
|
343
346
|
try {
|
|
344
347
|
await fly.deploySafe(app, {
|
|
348
|
+
routerId,
|
|
345
349
|
image: deployConfig.image,
|
|
346
350
|
config: deployConfig.configPath,
|
|
347
351
|
region: args.region,
|
|
@@ -369,7 +373,7 @@ ${bold("EXAMPLES")}
|
|
|
369
373
|
// ==========================================================================
|
|
370
374
|
out.header("Step 6: Post-flight Audit").blank();
|
|
371
375
|
const auditSpinner = out.spinner("Auditing Deployment");
|
|
372
|
-
const audit = await auditDeploy(fly,
|
|
376
|
+
const audit = await auditDeploy(fly, flyAppName, network);
|
|
373
377
|
auditSpinner.success("Audit Complete");
|
|
374
378
|
if (audit.public_ips_released > 0) {
|
|
375
379
|
out.warn(`Released ${audit.public_ips_released} Public IP(s)`);
|
|
@@ -112,7 +112,7 @@ ${bold("EXAMPLES")}
|
|
|
112
112
|
.text(` Tag: ${tag ?? "unknown"}`)
|
|
113
113
|
.blank();
|
|
114
114
|
if (workloadApps.length > 0) {
|
|
115
|
-
out.warn(`${workloadApps.length}
|
|
115
|
+
out.warn(`${workloadApps.length} Workload App(s) Still on Network '${network}':`);
|
|
116
116
|
for (const wa of workloadApps) {
|
|
117
117
|
out.text(` - ${wa.appName}`);
|
|
118
118
|
}
|
|
@@ -294,7 +294,7 @@ ${bold("EXAMPLES")}
|
|
|
294
294
|
// ===========================================================================
|
|
295
295
|
const appSpinner = out.spinner("Destroying Fly App");
|
|
296
296
|
try {
|
|
297
|
-
await fly.deleteApp(
|
|
297
|
+
await fly.deleteApp(workloadApp.appName);
|
|
298
298
|
appSpinner.success("Fly App Destroyed");
|
|
299
299
|
}
|
|
300
300
|
catch {
|
|
@@ -303,7 +303,7 @@ ${bold("EXAMPLES")}
|
|
|
303
303
|
// ===========================================================================
|
|
304
304
|
// Done
|
|
305
305
|
// ===========================================================================
|
|
306
|
-
out.done({ destroyed: true, appName:
|
|
306
|
+
out.done({ destroyed: true, appName: workloadApp.appName, network });
|
|
307
307
|
out.ok("App Destroyed");
|
|
308
308
|
out.blank();
|
|
309
309
|
out.print();
|
|
@@ -67,7 +67,7 @@ ${bold("CHECKS")}
|
|
|
67
67
|
report("Tailscale Installed", await isTailscaleInstalled(), "Install from https://tailscale.com/download");
|
|
68
68
|
const tsStatus = await runCommand(["tailscale", "status", "--json"]);
|
|
69
69
|
let tsConnected = false;
|
|
70
|
-
if (tsStatus.
|
|
70
|
+
if (tsStatus.ok) {
|
|
71
71
|
try {
|
|
72
72
|
const parsed = JSON.parse(tsStatus.stdout);
|
|
73
73
|
tsConnected = parsed.BackendState === "Running";
|
package/esm/src/discovery.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface RouterApp {
|
|
|
6
6
|
appName: string;
|
|
7
7
|
network: string;
|
|
8
8
|
org: string;
|
|
9
|
+
routerId: string;
|
|
9
10
|
}
|
|
10
11
|
/** Machine state for a router, from the Fly Machines API. */
|
|
11
12
|
export interface RouterMachineInfo {
|
|
@@ -33,7 +34,9 @@ export interface WorkloadApp {
|
|
|
33
34
|
}
|
|
34
35
|
/** List all non-router apps on a specific custom network in an org. */
|
|
35
36
|
export declare const listWorkloadAppsOnNetwork: (fly: FlyProvider, org: string, network: string) => Promise<WorkloadApp[]>;
|
|
36
|
-
/** Find a specific workload app by name, optionally
|
|
37
|
+
/** Find a specific workload app by logical name, optionally scoped to a network.
|
|
38
|
+
* When a network is provided, resolves the router-suffixed Fly app name
|
|
39
|
+
* (e.g. "thing" on network "lab" with routerId "abc123" → "thing-abc123"). */
|
|
37
40
|
export declare const findWorkloadApp: (fly: FlyProvider, org: string, appName: string, network?: string) => Promise<WorkloadApp | null>;
|
|
38
41
|
/** Get machine info for a router app. Returns null if no machines exist. */
|
|
39
42
|
export declare const getRouterMachineInfo: (fly: FlyProvider, appName: string) => Promise<RouterMachineInfo | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/src/discovery.ts"],"names":[],"mappings":"AAcA,OAAO,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/src/discovery.ts"],"names":[],"mappings":"AAcA,OAAO,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAOlE,qDAAqD;AACrD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAaD,wDAAwD;AACxD,eAAO,MAAM,cAAc,GACzB,KAAK,WAAW,EAChB,KAAK,MAAM,KACV,OAAO,CAAC,SAAS,EAAE,CAerB,CAAC;AAEF,kDAAkD;AAClD,eAAO,MAAM,aAAa,GACxB,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,KACd,OAAO,CAAC,SAAS,GAAG,IAAI,CAG1B,CAAC;AAMF,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,GACpC,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,KACd,OAAO,CAAC,WAAW,EAAE,CAcvB,CAAC;AAEF;;+EAE+E;AAC/E,eAAO,MAAM,eAAe,GAC1B,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,EACf,UAAU,MAAM,KACf,OAAO,CAAC,WAAW,GAAG,IAAI,CA4B5B,CAAC;AAMF,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,GAC/B,KAAK,WAAW,EAChB,SAAS,MAAM,KACd,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAclC,CAAC;AAMF,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,GACjC,WAAW,iBAAiB,EAC5B,SAAS,MAAM,KACd,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAcpC,CAAC"}
|
package/esm/src/discovery.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// =============================================================================
|
|
15
15
|
import "../_dnt.polyfills.js";
|
|
16
|
+
import { getRouterSuffix, getWorkloadAppName } from "./providers/fly.js";
|
|
16
17
|
import { extractSubnet } from "./schemas/config.js";
|
|
17
18
|
// =============================================================================
|
|
18
19
|
// Constants
|
|
@@ -32,6 +33,7 @@ export const listRouterApps = async (fly, org) => {
|
|
|
32
33
|
appName: app.name,
|
|
33
34
|
network: app.network,
|
|
34
35
|
org: app.organization?.slug ?? org,
|
|
36
|
+
routerId: getRouterSuffix(app.name, app.network),
|
|
35
37
|
}));
|
|
36
38
|
};
|
|
37
39
|
/** Find the router app for a specific network. */
|
|
@@ -51,7 +53,9 @@ export const listWorkloadAppsOnNetwork = async (fly, org, network) => {
|
|
|
51
53
|
org: app.organization?.slug ?? org,
|
|
52
54
|
}));
|
|
53
55
|
};
|
|
54
|
-
/** Find a specific workload app by name, optionally
|
|
56
|
+
/** Find a specific workload app by logical name, optionally scoped to a network.
|
|
57
|
+
* When a network is provided, resolves the router-suffixed Fly app name
|
|
58
|
+
* (e.g. "thing" on network "lab" with routerId "abc123" → "thing-abc123"). */
|
|
55
59
|
export const findWorkloadApp = async (fly, org, appName, network) => {
|
|
56
60
|
const apps = await fly.listAppsWithNetwork(org);
|
|
57
61
|
const workloads = apps
|
|
@@ -63,8 +67,18 @@ export const findWorkloadApp = async (fly, org, appName, network) => {
|
|
|
63
67
|
org: app.organization?.slug ?? org,
|
|
64
68
|
}));
|
|
65
69
|
if (network) {
|
|
70
|
+
// Resolve the router's suffix so we can match the suffixed Fly app name
|
|
71
|
+
const router = await findRouterApp(fly, org, network);
|
|
72
|
+
if (router) {
|
|
73
|
+
const suffixedName = getWorkloadAppName(appName, router.routerId);
|
|
74
|
+
const found = workloads.find((a) => a.appName === suffixedName && a.network === network);
|
|
75
|
+
if (found)
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
// Fallback: try exact match (for pre-suffix apps or direct Fly name)
|
|
66
79
|
return workloads.find((a) => a.appName === appName && a.network === network) ?? null;
|
|
67
80
|
}
|
|
81
|
+
// Without network, try exact match only
|
|
68
82
|
return workloads.find((a) => a.appName === appName) ?? null;
|
|
69
83
|
};
|
|
70
84
|
// =============================================================================
|