@cardelli/ambit 0.1.5 → 0.2.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/esm/cli/commands/create/index.d.ts +2 -0
- package/esm/cli/commands/create/index.d.ts.map +1 -0
- package/esm/cli/commands/create/index.js +292 -0
- package/esm/cli/commands/create/machine.d.ts +33 -0
- package/esm/cli/commands/create/machine.d.ts.map +1 -0
- package/esm/cli/commands/create/machine.js +162 -0
- package/esm/cli/commands/deploy/index.d.ts +2 -0
- package/esm/cli/commands/deploy/index.d.ts.map +1 -0
- package/esm/cli/commands/deploy/index.js +290 -0
- package/esm/cli/commands/deploy/machine.d.ts +52 -0
- package/esm/cli/commands/deploy/machine.d.ts.map +1 -0
- package/esm/cli/commands/deploy/machine.js +116 -0
- package/esm/cli/commands/deploy/modes.d.ts +18 -0
- package/esm/cli/commands/deploy/modes.d.ts.map +1 -0
- package/esm/cli/commands/deploy/modes.js +152 -0
- package/esm/cli/commands/destroy/app.d.ts +2 -0
- package/esm/cli/commands/destroy/app.d.ts.map +1 -0
- package/esm/cli/commands/destroy/app.js +173 -0
- package/esm/cli/commands/destroy/index.d.ts +2 -0
- package/esm/cli/commands/destroy/index.d.ts.map +1 -0
- package/esm/cli/commands/destroy/index.js +63 -0
- package/esm/cli/commands/destroy/network.d.ts +2 -0
- package/esm/cli/commands/destroy/network.d.ts.map +1 -0
- package/esm/cli/commands/destroy/network.js +210 -0
- package/esm/cli/commands/doctor.d.ts.map +1 -0
- package/esm/cli/commands/doctor.js +295 -0
- package/esm/{src/cli → cli}/commands/list.d.ts.map +1 -1
- package/esm/{src/cli → cli}/commands/list.js +39 -54
- package/esm/cli/commands/status.d.ts.map +1 -0
- package/esm/cli/commands/status.js +331 -0
- package/esm/cli/mod.d.ts.map +1 -0
- package/esm/{src/cli → cli}/mod.js +4 -4
- package/esm/deno.d.ts +4 -18
- package/esm/deno.js +5 -19
- package/esm/deps/jsr.io/@std/path/1.1.4/constants.d.ts +1 -1
- package/esm/lib/args.d.ts +11 -0
- package/esm/lib/args.d.ts.map +1 -0
- package/esm/lib/args.js +28 -0
- package/esm/lib/cli.d.ts +0 -1
- package/esm/lib/cli.d.ts.map +1 -1
- package/esm/lib/cli.js +0 -1
- package/esm/lib/command.d.ts +0 -3
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +2 -13
- package/esm/lib/machine.d.ts +11 -0
- package/esm/lib/machine.d.ts.map +1 -0
- package/esm/lib/machine.js +15 -0
- package/esm/lib/output.d.ts +2 -1
- package/esm/lib/output.d.ts.map +1 -1
- package/esm/lib/output.js +21 -3
- package/esm/lib/result.d.ts +0 -1
- package/esm/lib/result.d.ts.map +1 -1
- package/esm/lib/result.js +0 -1
- package/esm/main.d.ts +6 -6
- package/esm/main.d.ts.map +1 -1
- package/esm/main.js +7 -9
- package/esm/providers/fly.d.ts +81 -0
- package/esm/providers/fly.d.ts.map +1 -0
- package/esm/providers/fly.js +372 -0
- package/esm/providers/tailscale.d.ts +31 -0
- package/esm/providers/tailscale.d.ts.map +1 -0
- package/esm/providers/tailscale.js +150 -0
- package/esm/{src/schemas → schemas}/fly.d.ts +1 -11
- package/esm/schemas/fly.d.ts.map +1 -0
- package/esm/{src/schemas → schemas}/fly.js +14 -56
- package/esm/{src/schemas → schemas}/tailscale.d.ts +1 -2
- package/esm/schemas/tailscale.d.ts.map +1 -0
- package/esm/{src/schemas → schemas}/tailscale.js +2 -3
- package/esm/src/{docker/router → router}/Dockerfile +0 -11
- package/esm/src/{docker/router → router}/start.sh +18 -9
- package/esm/util/constants.d.ts +13 -0
- package/esm/util/constants.d.ts.map +1 -0
- package/esm/util/constants.js +34 -0
- package/esm/{src → util}/credentials.d.ts +0 -1
- package/esm/util/credentials.d.ts.map +1 -0
- package/esm/{src → util}/credentials.js +3 -5
- package/esm/{src → util}/discovery.d.ts +16 -3
- package/esm/util/discovery.d.ts.map +1 -0
- package/esm/{src → util}/discovery.js +24 -15
- package/esm/util/fly-transforms.d.ts +27 -0
- package/esm/util/fly-transforms.d.ts.map +1 -0
- package/esm/util/fly-transforms.js +87 -0
- package/esm/{src → util}/guard.d.ts +1 -2
- package/esm/util/guard.d.ts.map +1 -0
- package/esm/{src → util}/guard.js +27 -27
- package/esm/util/naming.d.ts +5 -0
- package/esm/util/naming.d.ts.map +1 -0
- package/esm/util/naming.js +12 -0
- package/esm/{src → util}/resolve.d.ts +2 -3
- package/esm/util/resolve.d.ts.map +1 -0
- package/esm/{src → util}/resolve.js +1 -2
- package/esm/util/session.d.ts +16 -0
- package/esm/util/session.d.ts.map +1 -0
- package/esm/util/session.js +19 -0
- package/esm/util/tailscale-local.d.ts +13 -0
- package/esm/util/tailscale-local.d.ts.map +1 -0
- package/esm/util/tailscale-local.js +63 -0
- package/esm/{src → util}/template.d.ts +0 -1
- package/esm/util/template.d.ts.map +1 -0
- package/esm/{src → util}/template.js +0 -1
- package/package.json +1 -49
- package/esm/lib/paths.d.ts +0 -3
- package/esm/lib/paths.d.ts.map +0 -1
- package/esm/lib/paths.js +0 -5
- package/esm/src/cli/commands/create.d.ts +0 -2
- package/esm/src/cli/commands/create.d.ts.map +0 -1
- package/esm/src/cli/commands/create.js +0 -308
- package/esm/src/cli/commands/deploy.d.ts +0 -2
- package/esm/src/cli/commands/deploy.d.ts.map +0 -1
- package/esm/src/cli/commands/deploy.js +0 -430
- package/esm/src/cli/commands/destroy.d.ts +0 -2
- package/esm/src/cli/commands/destroy.d.ts.map +0 -1
- package/esm/src/cli/commands/destroy.js +0 -340
- package/esm/src/cli/commands/doctor.d.ts.map +0 -1
- package/esm/src/cli/commands/doctor.js +0 -141
- package/esm/src/cli/commands/status.d.ts.map +0 -1
- package/esm/src/cli/commands/status.js +0 -152
- package/esm/src/cli/mod.d.ts.map +0 -1
- package/esm/src/credentials.d.ts.map +0 -1
- package/esm/src/discovery.d.ts.map +0 -1
- package/esm/src/guard.d.ts.map +0 -1
- package/esm/src/providers/fly.d.ts +0 -76
- package/esm/src/providers/fly.d.ts.map +0 -1
- package/esm/src/providers/fly.js +0 -407
- package/esm/src/providers/tailscale.d.ts +0 -31
- package/esm/src/providers/tailscale.d.ts.map +0 -1
- package/esm/src/providers/tailscale.js +0 -189
- package/esm/src/resolve.d.ts.map +0 -1
- package/esm/src/schemas/config.d.ts +0 -5
- package/esm/src/schemas/config.d.ts.map +0 -1
- package/esm/src/schemas/config.js +0 -22
- package/esm/src/schemas/fly.d.ts.map +0 -1
- package/esm/src/schemas/tailscale.d.ts.map +0 -1
- package/esm/src/template.d.ts.map +0 -1
- /package/esm/{src/cli → cli}/commands/doctor.d.ts +0 -0
- /package/esm/{src/cli → cli}/commands/list.d.ts +0 -0
- /package/esm/{src/cli → cli}/commands/status.d.ts +0 -0
- /package/esm/{src/cli → cli}/mod.d.ts +0 -0
- /package/esm/src/{docker/router → router}/fly.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/create/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Create Command - Create Tailscale Subnet Router on Fly.io Custom Network
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
5
|
+
import { bold, readSecret } from "../../../lib/cli.js";
|
|
6
|
+
import { checkArgs } from "../../../lib/args.js";
|
|
7
|
+
import { createOutput } from "../../../lib/output.js";
|
|
8
|
+
import { runMachine } from "../../../lib/machine.js";
|
|
9
|
+
import { registerCommand } from "../../mod.js";
|
|
10
|
+
import { getRouterTag } from "../../../util/naming.js";
|
|
11
|
+
import { isPublicTld } from "../../../util/guard.js";
|
|
12
|
+
import { createFlyProvider, } from "../../../providers/fly.js";
|
|
13
|
+
import { createTailscaleProvider, } from "../../../providers/tailscale.js";
|
|
14
|
+
import { getCredentialStore } from "../../../util/credentials.js";
|
|
15
|
+
import { FLY_PRIVATE_SUBNET, SOCKS_PROXY_PORT, TAILSCALE_API_KEY_PREFIX, } from "../../../util/constants.js";
|
|
16
|
+
import { resolveOrg } from "../../../util/resolve.js";
|
|
17
|
+
import { isAutoApproverConfigured, isTagOwnerConfigured } from "../../../util/tailscale-local.js";
|
|
18
|
+
import { createTransition, hydrateCreate, reportSkipped, } from "./machine.js";
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Stage 1: Fly.io Configuration
|
|
21
|
+
// =============================================================================
|
|
22
|
+
const stageFlyConfig = async (out, opts) => {
|
|
23
|
+
out.header("Step 1: Fly.io Configuration").blank();
|
|
24
|
+
const fly = createFlyProvider();
|
|
25
|
+
await fly.auth.ensureInstalled();
|
|
26
|
+
const email = await fly.auth.login({ interactive: !opts.json });
|
|
27
|
+
out.ok(`Authenticated as ${email}`);
|
|
28
|
+
const org = await resolveOrg(fly, opts, out);
|
|
29
|
+
const region = opts.region || "iad";
|
|
30
|
+
out.ok(`Using Region: ${region}`);
|
|
31
|
+
out.blank();
|
|
32
|
+
return { fly, org, region };
|
|
33
|
+
};
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Stage 2: Tailscale Configuration
|
|
36
|
+
// =============================================================================
|
|
37
|
+
const stageTailscaleConfig = async (out, opts) => {
|
|
38
|
+
out.header("Step 2: Tailscale Configuration").blank();
|
|
39
|
+
const credentials = getCredentialStore();
|
|
40
|
+
let apiKey = opts.apiKey || (await credentials.getTailscaleApiKey());
|
|
41
|
+
if (!apiKey) {
|
|
42
|
+
if (opts.json) {
|
|
43
|
+
return out.die("--api-key Is Required in JSON Mode");
|
|
44
|
+
}
|
|
45
|
+
out.dim("Ambit Needs an API Access Token (Not an Auth Key) to Manage Your Tailnet.")
|
|
46
|
+
.dim("Create One at: https://login.tailscale.com/admin/settings/keys")
|
|
47
|
+
.blank();
|
|
48
|
+
apiKey = await readSecret("API access token (tskey-api-...): ");
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
return out.die("Tailscale API Access Token Required");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!apiKey.startsWith(TAILSCALE_API_KEY_PREFIX)) {
|
|
54
|
+
return out.die("Invalid Token Format. Expected 'tskey-api-...' (API Access Token, Not Auth Key)");
|
|
55
|
+
}
|
|
56
|
+
const tailscale = createTailscaleProvider(apiKey);
|
|
57
|
+
const validateSpinner = out.spinner("Validating API Access Token");
|
|
58
|
+
const isValid = await tailscale.auth.validateKey();
|
|
59
|
+
if (!isValid) {
|
|
60
|
+
validateSpinner.fail("Invalid API Access Token");
|
|
61
|
+
return out.die("Failed to Validate Tailscale API Access Token");
|
|
62
|
+
}
|
|
63
|
+
validateSpinner.success("API Access Token Validated");
|
|
64
|
+
await credentials.setTailscaleApiKey(apiKey);
|
|
65
|
+
const tagOwnerSpinner = out.spinner(`Checking tagOwners for ${opts.tag}`);
|
|
66
|
+
const policy = await tailscale.acl.getPolicy();
|
|
67
|
+
const hasTagOwner = isTagOwnerConfigured(policy, opts.tag);
|
|
68
|
+
if (!hasTagOwner) {
|
|
69
|
+
tagOwnerSpinner.fail(`Tag ${opts.tag} Not Configured in tagOwners`);
|
|
70
|
+
out.blank()
|
|
71
|
+
.text(` The Tag ${opts.tag} Does Not Exist in Your Tailscale ACL tagOwners.`)
|
|
72
|
+
.text(" Tailscale Will Reject Auth Keys for Undefined Tags.")
|
|
73
|
+
.blank()
|
|
74
|
+
.text(" Add This Tag in Your Tailscale ACL Settings:")
|
|
75
|
+
.dim(" https://login.tailscale.com/admin/acls/visual/tags")
|
|
76
|
+
.blank()
|
|
77
|
+
.dim(` "tagOwners": { "${opts.tag}": ["autogroup:admin"] }`)
|
|
78
|
+
.blank();
|
|
79
|
+
return out.die(`Add ${opts.tag} to tagOwners Before Creating Router`);
|
|
80
|
+
}
|
|
81
|
+
tagOwnerSpinner.success(`Tag ${opts.tag} Configured in tagOwners`);
|
|
82
|
+
if (opts.json) {
|
|
83
|
+
const approverSpinner = out.spinner(`Checking autoApprovers for ${opts.tag}`);
|
|
84
|
+
const hasApprover = isAutoApproverConfigured(policy, opts.tag);
|
|
85
|
+
if (!hasApprover) {
|
|
86
|
+
approverSpinner.fail(`autoApprovers Not Configured for ${opts.tag}`);
|
|
87
|
+
out.blank()
|
|
88
|
+
.text(" JSON mode skips interactive route approval.")
|
|
89
|
+
.text(` Configure autoApprovers for ${opts.tag} so routes are approved on deploy.`)
|
|
90
|
+
.blank()
|
|
91
|
+
.dim(" Add to your ACL at: https://login.tailscale.com/admin/acls/file")
|
|
92
|
+
.blank()
|
|
93
|
+
.dim(` "autoApprovers": { "routes": { "${FLY_PRIVATE_SUBNET}": ["${opts.tag}"] } }`)
|
|
94
|
+
.blank();
|
|
95
|
+
return out.die(`Configure autoApprovers for ${opts.tag} Before Using --json`);
|
|
96
|
+
}
|
|
97
|
+
approverSpinner.success(`autoApprovers Configured for ${opts.tag}`);
|
|
98
|
+
}
|
|
99
|
+
out.blank();
|
|
100
|
+
return tailscale;
|
|
101
|
+
};
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Stage 3: Deploy Subnet Router
|
|
104
|
+
// =============================================================================
|
|
105
|
+
const stageDeploy = async (out, fly, tailscale, opts) => {
|
|
106
|
+
out.header("Step 3: Deploy Subnet Router").blank();
|
|
107
|
+
const ctx = {
|
|
108
|
+
fly,
|
|
109
|
+
tailscale,
|
|
110
|
+
out,
|
|
111
|
+
...opts,
|
|
112
|
+
appName: "",
|
|
113
|
+
routerId: "",
|
|
114
|
+
};
|
|
115
|
+
const phase = await hydrateCreate(ctx);
|
|
116
|
+
if (phase === "complete") {
|
|
117
|
+
out.ok(`Network "${opts.network}" Already Fully Created`);
|
|
118
|
+
out.done({
|
|
119
|
+
network: opts.network,
|
|
120
|
+
router: {
|
|
121
|
+
appName: ctx.appName,
|
|
122
|
+
tailscaleIp: ctx.device?.addresses[0] ?? null,
|
|
123
|
+
},
|
|
124
|
+
subnet: ctx.subnet ?? null,
|
|
125
|
+
tag: opts.tag,
|
|
126
|
+
});
|
|
127
|
+
out.print();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
reportSkipped(out, phase);
|
|
131
|
+
const machine = {
|
|
132
|
+
terminal: "complete",
|
|
133
|
+
transition: createTransition,
|
|
134
|
+
};
|
|
135
|
+
const result = await runMachine(machine, phase, ctx);
|
|
136
|
+
if (!result.ok)
|
|
137
|
+
return out.die(result.error);
|
|
138
|
+
stageSummary(out, fly, tailscale, ctx, opts);
|
|
139
|
+
};
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Stage 4: Summary
|
|
142
|
+
// =============================================================================
|
|
143
|
+
const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
144
|
+
const policy = await tailscale.acl.getPolicy();
|
|
145
|
+
const hasAutoApprover = isAutoApproverConfigured(policy, opts.tag);
|
|
146
|
+
out.done({
|
|
147
|
+
network: opts.network,
|
|
148
|
+
router: {
|
|
149
|
+
appName: ctx.appName,
|
|
150
|
+
tailscaleIp: ctx.device?.addresses[0] ?? null,
|
|
151
|
+
},
|
|
152
|
+
subnet: ctx.subnet ?? null,
|
|
153
|
+
tag: opts.tag,
|
|
154
|
+
});
|
|
155
|
+
out.blank()
|
|
156
|
+
.header("=".repeat(50))
|
|
157
|
+
.header(" Router Created!")
|
|
158
|
+
.header("=".repeat(50))
|
|
159
|
+
.blank()
|
|
160
|
+
.text(`Any Flycast App on the "${opts.network}" Network Is Reachable as:`)
|
|
161
|
+
.text(` <app-name>.${opts.network}`)
|
|
162
|
+
.blank();
|
|
163
|
+
if (ctx.subnet) {
|
|
164
|
+
const machines = await fly.machines.list(ctx.appName);
|
|
165
|
+
const routerMachine = machines.find((m) => m.private_ip);
|
|
166
|
+
if (routerMachine?.private_ip) {
|
|
167
|
+
out.text("SOCKS5 Proxy Available at:")
|
|
168
|
+
.text(` socks5://[${routerMachine.private_ip}]:${SOCKS_PROXY_PORT}`)
|
|
169
|
+
.dim("Containers on This Network Can Use It to Reach Your Tailnet.")
|
|
170
|
+
.blank();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
out.dim("Deploy an App to This Network:")
|
|
174
|
+
.dim(` ambit deploy my-app --network ${opts.network}`)
|
|
175
|
+
.blank()
|
|
176
|
+
.dim("Invite People to Your Tailnet:")
|
|
177
|
+
.dim(" https://login.tailscale.com/admin/users")
|
|
178
|
+
.dim("Control Their Access:")
|
|
179
|
+
.dim(" https://login.tailscale.com/admin/acls/visual/general-access-rules")
|
|
180
|
+
.blank();
|
|
181
|
+
if (ctx.subnet && !hasAutoApprover) {
|
|
182
|
+
out.header("Recommended: Configure autoApprovers")
|
|
183
|
+
.blank()
|
|
184
|
+
.dim(" Add to Your Tailnet Policy File at:")
|
|
185
|
+
.dim(" https://login.tailscale.com/admin/acls/file")
|
|
186
|
+
.blank()
|
|
187
|
+
.text(` "autoApprovers": { "routes": { "${ctx.subnet}": ["${opts.tag}"] } }`)
|
|
188
|
+
.blank()
|
|
189
|
+
.dim(" Routes Were Approved via API for This Session.")
|
|
190
|
+
.dim(" autoApprovers Will Auto-Approve on Future Restarts.")
|
|
191
|
+
.blank();
|
|
192
|
+
}
|
|
193
|
+
if (ctx.subnet) {
|
|
194
|
+
out.header("Recommended ACL Rules:")
|
|
195
|
+
.blank()
|
|
196
|
+
.dim(" To Restrict Access, Add ACL Rules to Your Policy File:")
|
|
197
|
+
.dim(" https://login.tailscale.com/admin/acls/file")
|
|
198
|
+
.blank()
|
|
199
|
+
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${opts.tag}:53"]}`)
|
|
200
|
+
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${ctx.subnet}:*"]}`)
|
|
201
|
+
.blank();
|
|
202
|
+
}
|
|
203
|
+
if (!opts.shouldApprove) {
|
|
204
|
+
out.dim("Route Approval Was Skipped. To Complete Setup:")
|
|
205
|
+
.dim(` ambit doctor --network ${opts.network}`)
|
|
206
|
+
.blank();
|
|
207
|
+
}
|
|
208
|
+
out.print();
|
|
209
|
+
};
|
|
210
|
+
// =============================================================================
|
|
211
|
+
// Create Command
|
|
212
|
+
// =============================================================================
|
|
213
|
+
const create = async (argv) => {
|
|
214
|
+
const opts = {
|
|
215
|
+
string: ["org", "region", "api-key", "tag"],
|
|
216
|
+
boolean: ["help", "yes", "json", "no-auto-approve"],
|
|
217
|
+
alias: { y: "yes" },
|
|
218
|
+
};
|
|
219
|
+
const args = parseArgs(argv, opts);
|
|
220
|
+
checkArgs(args, opts, "ambit create");
|
|
221
|
+
if (args.help) {
|
|
222
|
+
console.log(`
|
|
223
|
+
${bold("ambit create")} - Create Tailscale Subnet Router
|
|
224
|
+
|
|
225
|
+
${bold("USAGE")}
|
|
226
|
+
ambit create <network> [options]
|
|
227
|
+
|
|
228
|
+
${bold("OPTIONS")}
|
|
229
|
+
--org <org> Fly.io organization slug
|
|
230
|
+
--region <region> Fly.io region (default: iad)
|
|
231
|
+
--api-key <key> Tailscale API access token (tskey-api-...)
|
|
232
|
+
--tag <tag> Tailscale ACL tag for the router (default: tag:ambit-<network>)
|
|
233
|
+
--no-auto-approve Skip waiting for router and approving routes
|
|
234
|
+
-y, --yes Skip confirmation prompts
|
|
235
|
+
--json Output as JSON (implies --no-auto-approve)
|
|
236
|
+
|
|
237
|
+
${bold("DESCRIPTION")}
|
|
238
|
+
Deploys a Tailscale subnet router onto a Fly.io custom private network.
|
|
239
|
+
The network name becomes a TLD on your tailnet:
|
|
240
|
+
|
|
241
|
+
my-app.${args._[0] || "<network>"} resolves to my-app.flycast
|
|
242
|
+
|
|
243
|
+
${bold("EXAMPLES")}
|
|
244
|
+
ambit create browsers
|
|
245
|
+
ambit create browsers --org my-org --region sea
|
|
246
|
+
`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const out = createOutput(args.json);
|
|
250
|
+
const networkArg = args._[0];
|
|
251
|
+
if (!networkArg || typeof networkArg !== "string") {
|
|
252
|
+
return out.die("Network Name Required. Usage: ambit create <network>");
|
|
253
|
+
}
|
|
254
|
+
const network = networkArg;
|
|
255
|
+
if (isPublicTld(network)) {
|
|
256
|
+
return out.die(`"${network}" Is a Public TLD and Cannot Be Used as a Network Name`);
|
|
257
|
+
}
|
|
258
|
+
const tag = args.tag || getRouterTag(network);
|
|
259
|
+
const shouldApprove = !(args["no-auto-approve"] || args.json);
|
|
260
|
+
out.blank()
|
|
261
|
+
.header("=".repeat(50))
|
|
262
|
+
.header(` ambit Create: ${network}`)
|
|
263
|
+
.header("=".repeat(50))
|
|
264
|
+
.blank();
|
|
265
|
+
const { fly, org, region } = await stageFlyConfig(out, {
|
|
266
|
+
json: args.json,
|
|
267
|
+
org: args.org,
|
|
268
|
+
region: args.region,
|
|
269
|
+
});
|
|
270
|
+
const tailscale = await stageTailscaleConfig(out, {
|
|
271
|
+
json: args.json,
|
|
272
|
+
apiKey: args["api-key"],
|
|
273
|
+
tag,
|
|
274
|
+
network,
|
|
275
|
+
});
|
|
276
|
+
await stageDeploy(out, fly, tailscale, {
|
|
277
|
+
network,
|
|
278
|
+
org,
|
|
279
|
+
region,
|
|
280
|
+
tag,
|
|
281
|
+
shouldApprove,
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
// =============================================================================
|
|
285
|
+
// Register Command
|
|
286
|
+
// =============================================================================
|
|
287
|
+
registerCommand({
|
|
288
|
+
name: "create",
|
|
289
|
+
description: "Create a Tailscale subnet router on a Fly.io custom network",
|
|
290
|
+
usage: "ambit create <network> [--org <org>] [--region <region>]",
|
|
291
|
+
run: create,
|
|
292
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Output } from "../../../lib/output.js";
|
|
2
|
+
import { Result } from "../../../lib/result.js";
|
|
3
|
+
import { type FlyProvider } from "../../../providers/fly.js";
|
|
4
|
+
import type { TailscaleProvider } from "../../../providers/tailscale.js";
|
|
5
|
+
import type { TailscaleDevice } from "../../../schemas/tailscale.js";
|
|
6
|
+
export type CreatePhase = "create_app" | "deploy_router" | "await_device" | "approve_routes" | "configure_dns" | "accept_routes" | "complete";
|
|
7
|
+
export type CreateResult = {
|
|
8
|
+
network: string;
|
|
9
|
+
router: {
|
|
10
|
+
appName: string;
|
|
11
|
+
tailscaleIp: string | null;
|
|
12
|
+
};
|
|
13
|
+
subnet: string | null;
|
|
14
|
+
tag: string;
|
|
15
|
+
};
|
|
16
|
+
export interface CreateCtx {
|
|
17
|
+
fly: FlyProvider;
|
|
18
|
+
tailscale: TailscaleProvider;
|
|
19
|
+
out: Output<CreateResult>;
|
|
20
|
+
network: string;
|
|
21
|
+
org: string;
|
|
22
|
+
region: string;
|
|
23
|
+
tag: string;
|
|
24
|
+
shouldApprove: boolean;
|
|
25
|
+
appName: string;
|
|
26
|
+
routerId: string;
|
|
27
|
+
device?: TailscaleDevice;
|
|
28
|
+
subnet?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const reportSkipped: (out: Output<CreateResult>, startPhase: CreatePhase) => void;
|
|
31
|
+
export declare const hydrateCreate: (ctx: CreateCtx) => Promise<CreatePhase>;
|
|
32
|
+
export declare const createTransition: (phase: CreatePhase, ctx: CreateCtx) => Promise<Result<CreatePhase>>;
|
|
33
|
+
//# sourceMappingURL=machine.d.ts.map
|
|
@@ -0,0 +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,EAEL,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AASnC,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,CA8G7B,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Create — Phases, Context, Hydration, Transitions
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { randomId } from "../../../lib/cli.js";
|
|
5
|
+
import { Result } from "../../../lib/result.js";
|
|
6
|
+
import { extractSubnet } from "../../../util/fly-transforms.js";
|
|
7
|
+
import { ROUTER_DOCKER_DIR, SECRET_NETWORK_NAME, SECRET_ROUTER_ID, SECRET_TAILSCALE_AUTHKEY, } from "../../../util/constants.js";
|
|
8
|
+
import { FlyDeployError, } from "../../../providers/fly.js";
|
|
9
|
+
import { getRouterAppName } from "../../../util/naming.js";
|
|
10
|
+
import { enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, waitForDevice, } from "../../../util/tailscale-local.js";
|
|
11
|
+
import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Phase Labels
|
|
14
|
+
// =============================================================================
|
|
15
|
+
const CREATE_PHASES = [
|
|
16
|
+
{ phase: "create_app", label: "Fly App Created" },
|
|
17
|
+
{ phase: "deploy_router", label: "Router Deployed" },
|
|
18
|
+
{ phase: "await_device", label: "Router in Tailnet" },
|
|
19
|
+
{ phase: "approve_routes", label: "Routes Approved" },
|
|
20
|
+
{ phase: "configure_dns", label: "Split DNS Configured" },
|
|
21
|
+
{ phase: "accept_routes", label: "Accept Routes Enabled" },
|
|
22
|
+
];
|
|
23
|
+
export const reportSkipped = (out, startPhase) => {
|
|
24
|
+
for (const { phase, label } of CREATE_PHASES) {
|
|
25
|
+
if (phase === startPhase)
|
|
26
|
+
break;
|
|
27
|
+
out.skip(label);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Hydration — determine starting phase from infrastructure state
|
|
32
|
+
// =============================================================================
|
|
33
|
+
export const hydrateCreate = async (ctx) => {
|
|
34
|
+
const router = await findRouterApp(ctx.fly, ctx.org, ctx.network);
|
|
35
|
+
if (!router)
|
|
36
|
+
return "create_app";
|
|
37
|
+
ctx.appName = router.appName;
|
|
38
|
+
ctx.routerId = router.routerId;
|
|
39
|
+
const machine = await getRouterMachineInfo(ctx.fly, router.appName);
|
|
40
|
+
if (!machine || machine.state !== "started")
|
|
41
|
+
return "deploy_router";
|
|
42
|
+
ctx.subnet = machine.subnet;
|
|
43
|
+
if (!ctx.shouldApprove)
|
|
44
|
+
return "complete";
|
|
45
|
+
const device = await ctx.tailscale.devices.getByHostname(router.appName);
|
|
46
|
+
if (!device)
|
|
47
|
+
return "await_device";
|
|
48
|
+
ctx.device = device;
|
|
49
|
+
const routes = await ctx.tailscale.routes.get(device.id);
|
|
50
|
+
if (!routes || routes.unapproved.length > 0)
|
|
51
|
+
return "approve_routes";
|
|
52
|
+
const dns = await ctx.tailscale.dns.getSplit();
|
|
53
|
+
if (!dns[ctx.network]?.length)
|
|
54
|
+
return "configure_dns";
|
|
55
|
+
if (!(await isAcceptRoutesEnabled()))
|
|
56
|
+
return "accept_routes";
|
|
57
|
+
return "complete";
|
|
58
|
+
};
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Transitions
|
|
61
|
+
// =============================================================================
|
|
62
|
+
export const createTransition = async (phase, ctx) => {
|
|
63
|
+
switch (phase) {
|
|
64
|
+
case "create_app": {
|
|
65
|
+
const suffix = randomId(8);
|
|
66
|
+
ctx.appName = getRouterAppName(ctx.network, suffix);
|
|
67
|
+
ctx.routerId = suffix;
|
|
68
|
+
await ctx.out.spin("Creating App", () => ctx.fly.apps.create(ctx.appName, ctx.org, { network: ctx.network }));
|
|
69
|
+
ctx.out.ok(`Created App: ${ctx.appName}`);
|
|
70
|
+
return Result.ok("deploy_router");
|
|
71
|
+
}
|
|
72
|
+
case "deploy_router": {
|
|
73
|
+
const authKey = await ctx.tailscale.auth.createKey({
|
|
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,
|
|
82
|
+
[SECRET_NETWORK_NAME]: ctx.network,
|
|
83
|
+
[SECRET_ROUTER_ID]: ctx.routerId,
|
|
84
|
+
}, { stage: true }));
|
|
85
|
+
const dockerDir = ROUTER_DOCKER_DIR;
|
|
86
|
+
try {
|
|
87
|
+
await ctx.fly.deploy.router(ctx.appName, dockerDir, {
|
|
88
|
+
region: ctx.region,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
if (e instanceof FlyDeployError) {
|
|
93
|
+
ctx.out.dim(` ${e.detail}`);
|
|
94
|
+
return Result.err(e.message);
|
|
95
|
+
}
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
ctx.out.ok("Router Deployed");
|
|
99
|
+
const machines = await ctx.fly.machines.list(ctx.appName);
|
|
100
|
+
const m = machines.find((m) => m.private_ip);
|
|
101
|
+
if (m?.private_ip)
|
|
102
|
+
ctx.subnet = extractSubnet(m.private_ip);
|
|
103
|
+
if (!ctx.shouldApprove)
|
|
104
|
+
return Result.ok("complete");
|
|
105
|
+
return Result.ok("await_device");
|
|
106
|
+
}
|
|
107
|
+
case "await_device": {
|
|
108
|
+
ctx.device = await waitForDevice(ctx.tailscale, ctx.appName, 180000);
|
|
109
|
+
ctx.out.ok(`Router Joined Tailnet: ${ctx.device.addresses[0]}`);
|
|
110
|
+
if (!ctx.subnet) {
|
|
111
|
+
const machines = await ctx.fly.machines.list(ctx.appName);
|
|
112
|
+
const m = machines.find((m) => m.private_ip);
|
|
113
|
+
if (m?.private_ip)
|
|
114
|
+
ctx.subnet = extractSubnet(m.private_ip);
|
|
115
|
+
}
|
|
116
|
+
return Result.ok("approve_routes");
|
|
117
|
+
}
|
|
118
|
+
case "approve_routes": {
|
|
119
|
+
if (!ctx.device || !ctx.subnet) {
|
|
120
|
+
return Result.err("Missing Device or Subnet");
|
|
121
|
+
}
|
|
122
|
+
const policy = await ctx.tailscale.acl.getPolicy();
|
|
123
|
+
const hasAutoApprover = isAutoApproverConfigured(policy, ctx.tag);
|
|
124
|
+
if (hasAutoApprover) {
|
|
125
|
+
ctx.out.ok("Routes Auto-Approved via ACL Policy");
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
await ctx.tailscale.routes.approve(ctx.device.id, [ctx.subnet]);
|
|
129
|
+
ctx.out.ok("Subnet Routes Approved");
|
|
130
|
+
}
|
|
131
|
+
return Result.ok("configure_dns");
|
|
132
|
+
}
|
|
133
|
+
case "configure_dns": {
|
|
134
|
+
if (!ctx.device)
|
|
135
|
+
return Result.err("Missing Device");
|
|
136
|
+
await ctx.tailscale.dns.setSplit(ctx.network, [ctx.device.addresses[0]]);
|
|
137
|
+
ctx.out.ok(`Split DNS Configured: *.${ctx.network}`);
|
|
138
|
+
return Result.ok("accept_routes");
|
|
139
|
+
}
|
|
140
|
+
case "accept_routes": {
|
|
141
|
+
if (await isTailscaleInstalled()) {
|
|
142
|
+
if (await isAcceptRoutesEnabled()) {
|
|
143
|
+
ctx.out.ok("Accept Routes Already Enabled");
|
|
144
|
+
}
|
|
145
|
+
else if (await enableAcceptRoutes()) {
|
|
146
|
+
ctx.out.ok("Accept Routes Enabled");
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
ctx.out.warn("Could Not Enable Accept Routes");
|
|
150
|
+
ctx.out.dim(" Run: sudo tailscale set --accept-routes");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
ctx.out.warn("Tailscale CLI Not Found");
|
|
155
|
+
ctx.out.dim(" Ensure Accept-Routes is Enabled on This Device");
|
|
156
|
+
}
|
|
157
|
+
return Result.ok("complete");
|
|
158
|
+
}
|
|
159
|
+
default:
|
|
160
|
+
return Result.err(`Unknown Phase: ${phase}`);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/index.ts"],"names":[],"mappings":""}
|