insta 0.1.24 → 0.1.26
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 +1 -1
- package/dist/commands/cron.js +3 -3
- package/dist/commands/deploy.js +18 -10
- package/dist/commands/domain.js +11 -15
- package/dist/commands/private-network.js +101 -0
- package/dist/commands/services.js +25 -9
- package/dist/commands/template.js +10 -9
- package/dist/deploy-archive.js +7 -3
- package/dist/index.js +20 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,7 +229,7 @@ build never reaches a production installer.
|
|
|
229
229
|
| `insta branch` | `create` · `list` · `switch` · `delete` · `merge` |
|
|
230
230
|
| `insta service` (`services`, `svc`) | `add` · `list` · `remove` · `rename` |
|
|
231
231
|
| `insta secrets` | Write `.env`, plus `list` · `set` · `unset` · `bind` · `unbind` · `bindings` · `sources` · `tree` |
|
|
232
|
-
| `insta domain` | Bought or bring-your-own: `attach` · `check` · `detach
|
|
232
|
+
| `insta domain` | Bought or bring-your-own: `attach` · `check` · `detach`. Domains already bought through InstaCloud: `list` · `status` · `records …` |
|
|
233
233
|
| `insta compute` | `start` · `stop` · `suspend` · `restart` · `status` · `scale` · `limits` · `volume` · `always-on` · `exec` · `ssh` · `repo` · `connect-repo` · `watch-paths` · `disconnect-repo` · `logs` · `metrics` |
|
|
234
234
|
| `insta postgres` | `url` (print the DSN) · `connect` (psql) · `stats` · `limits` · `restart` · `volume` · `always-on` · `private-access` · `public-access` · `logs` · `metrics` — every verb takes `[service]` |
|
|
235
235
|
| `insta redis` · `mysql` · `mongodb` | `query` · `status` · `limits` · `volume` · `always-on` · `logs` · `metrics` |
|
package/dist/commands/cron.js
CHANGED
|
@@ -165,11 +165,11 @@ export function jobShowLines(j, serviceName) {
|
|
|
165
165
|
// decrypts it. Saying so beats an empty "value" column that reads as "this header is set to
|
|
166
166
|
// nothing". Secret-backed headers get a row each, because the secret NAME is readable and is the
|
|
167
167
|
// thing an operator needs when a rotation or a missing secret is the question.
|
|
168
|
-
const refs =
|
|
169
|
-
const literal = j.request.headerNames.filter((n) => findHeader(
|
|
168
|
+
const refs = refsOf(j.request);
|
|
169
|
+
const literal = j.request.headerNames.filter((n) => findHeader(refs, n) === undefined);
|
|
170
170
|
const rows = [
|
|
171
171
|
...(literal.length ? [`${literal.join(', ')} (names only — values are encrypted at rest and never returned)`] : []),
|
|
172
|
-
...refs.map(([h, s]) => `${h} ← secret ${s} (resolved at send time)`),
|
|
172
|
+
...Object.entries(refs).map(([h, s]) => `${h} ← secret ${s} (resolved at send time)`),
|
|
173
173
|
];
|
|
174
174
|
if (!rows.length)
|
|
175
175
|
lines.push(' headers (none)');
|
package/dist/commands/deploy.js
CHANGED
|
@@ -6,7 +6,7 @@ import { info, die, printJson, handleApproval, renderNextActions, CliExit } from
|
|
|
6
6
|
import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner, stderrBuildRunner } from '../flyctl-build.js';
|
|
7
7
|
import { packDirectory, windowsModeCaveat } from '../pack.js';
|
|
8
8
|
import { deployArchive, uploadArchive } from '../deploy-archive.js';
|
|
9
|
-
import { parseEndpoint, parsePort } from './services.js';
|
|
9
|
+
import { isV6Endpoint, parseEndpoint, parsePort } from './services.js';
|
|
10
10
|
// With --json, stdout must carry exactly one JSON document (the deploy result), so every progress
|
|
11
11
|
// line moves to stderr.
|
|
12
12
|
const note = (opts) => (opts.json ? (m) => void process.stderr.write(m + '\n') : info);
|
|
@@ -23,11 +23,19 @@ export function deployRequestBody(source, branch, opts) {
|
|
|
23
23
|
replaceSource: opts.replaceSource ? true : undefined,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
// Where the deploy answers: the HTTPS URL, or for
|
|
27
|
-
// platform reports no URL for one).
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// Where the deploy answers: the HTTPS URL, or for a v6-lane service (tcp, udp, tcp+udp) its
|
|
27
|
+
// address, one scheme per protocol (the platform reports no URL for one). The scheme comes from the
|
|
28
|
+
// RESULT's `endpoint` (instacloud-platform#681 reports it beside endpointHost, including on a
|
|
29
|
+
// redeploy that passed none -- the endpoint the service kept); --endpoint is only the fallback for
|
|
30
|
+
// an older platform that names the host but not the endpoint. With neither, the address prints
|
|
31
|
+
// without a scheme rather than guessing tcp for what may be a udp service. Pure, so it's unit-tested.
|
|
32
|
+
export function deployedTarget(r, port, flag) {
|
|
33
|
+
if (r.endpointHost) {
|
|
34
|
+
const endpoint = isV6Endpoint(r.endpoint) ? r.endpoint : isV6Endpoint(flag) ? flag : undefined;
|
|
35
|
+
if (endpoint === undefined)
|
|
36
|
+
return `${r.endpointHost}:${port ?? '?'} (endpoint unchanged; \`insta service list\` shows its protocol)`;
|
|
37
|
+
return (endpoint === 'tcp+udp' ? ['tcp', 'udp'] : [endpoint]).map((pr) => `${pr}://${r.endpointHost}:${port ?? '?'}`).join(', ');
|
|
38
|
+
}
|
|
31
39
|
return r.url || '(no URL: a worker)';
|
|
32
40
|
}
|
|
33
41
|
// Ask the platform which lane serves this target, so the CLI stops knowing which provider backs
|
|
@@ -152,7 +160,7 @@ export async function deploy(dir, opts) {
|
|
|
152
160
|
if (!dir && !opts.image)
|
|
153
161
|
die('usage: insta deploy <dir> | --image <url> [--branch <b>] [--group <g>] [--port <n>]');
|
|
154
162
|
// Normalized here, once, so both lanes get the same validated value: the archive lane's body
|
|
155
|
-
// (deployArchive) otherwise silently drops anything that isn't exactly
|
|
163
|
+
// (deployArchive) otherwise silently drops anything that isn't exactly one of the endpoints to
|
|
156
164
|
// undefined, which let a typo like --endpoint tpc build and deploy using the service's existing
|
|
157
165
|
// endpoint instead of failing locally. Checked before ApiClient.load()/requireProject(): those
|
|
158
166
|
// can resolve a project and persist an automatic link for an unlinked directory, so a typo must
|
|
@@ -192,8 +200,8 @@ export async function deploy(dir, opts) {
|
|
|
192
200
|
// nextActions, which the operation read does not carry.
|
|
193
201
|
const d = source.deployed;
|
|
194
202
|
if (opts.json)
|
|
195
|
-
return printJson({ image: d.image, url: d.url, ...(d.endpointHost ? { endpointHost: d.endpointHost } : {}), branch: d.branch, group: d.group, machineId: d.machineId });
|
|
196
|
-
info(`deployed ${d.image} -> ${deployedTarget(d, effOpts.port)} (branch ${d.branch}, group ${d.group})`);
|
|
203
|
+
return printJson({ image: d.image, url: d.url, ...(d.endpointHost ? { endpointHost: d.endpointHost } : {}), ...(d.endpoint ? { endpoint: d.endpoint } : {}), branch: d.branch, group: d.group, machineId: d.machineId });
|
|
204
|
+
info(`deployed ${d.image} -> ${deployedTarget(d, effOpts.port, endpoint)} (branch ${d.branch}, group ${d.group})`);
|
|
197
205
|
return;
|
|
198
206
|
}
|
|
199
207
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/deploy`, deployRequestBody(source, branch, effOpts))
|
|
@@ -203,7 +211,7 @@ export async function deploy(dir, opts) {
|
|
|
203
211
|
const what = source.image;
|
|
204
212
|
if (opts.json)
|
|
205
213
|
return printJson({ image: source.image, ...res.body });
|
|
206
|
-
info(`deployed ${what} -> ${deployedTarget(res.body, effOpts.port)} (branch ${res.body.branch}, group ${res.body.group})`);
|
|
214
|
+
info(`deployed ${what} -> ${deployedTarget(res.body, effOpts.port, endpoint)} (branch ${res.body.branch}, group ${res.body.group})`);
|
|
207
215
|
renderNextActions(res.body.nextActions);
|
|
208
216
|
}
|
|
209
217
|
// The platform refuses an image deploy onto a repo-connected service and names the body field it
|
package/dist/commands/domain.js
CHANGED
|
@@ -30,9 +30,6 @@ export async function domainSearch(keyword, opts, deps) {
|
|
|
30
30
|
return printJson(r);
|
|
31
31
|
for (const line of searchLines(r.results))
|
|
32
32
|
info(line);
|
|
33
|
-
const buyable = r.results.find((x) => x.purchasable);
|
|
34
|
-
if (buyable)
|
|
35
|
-
info(`buy one: insta domain buy ${buyable.domainName}`);
|
|
36
33
|
}
|
|
37
34
|
export async function domainBuy(name, opts, deps) {
|
|
38
35
|
const years = whole('--years', opts.years);
|
|
@@ -117,10 +114,10 @@ function domainLines(d, linked = true) {
|
|
|
117
114
|
out.push(` ${h.hostname.padEnd(w)} ${h.state}${h.service ? ` → ${h.service}` : ''}${h.reason ? ` — ${h.reason}` : ''}`);
|
|
118
115
|
return out;
|
|
119
116
|
}
|
|
120
|
-
function orderStatusLines(o
|
|
117
|
+
function orderStatusLines(o) {
|
|
121
118
|
const out = [`order ${o.id}: ${o.domainName} — ${o.status}${o.failedReason ? ` — ${o.failedReason}` : ''}`];
|
|
122
|
-
if (
|
|
123
|
-
out.push(
|
|
119
|
+
if (o.status === 'canceled')
|
|
120
|
+
out.push(' the checkout closed without payment');
|
|
124
121
|
return out;
|
|
125
122
|
}
|
|
126
123
|
export async function domainList(opts, deps) {
|
|
@@ -129,7 +126,7 @@ export async function domainList(opts, deps) {
|
|
|
129
126
|
if (opts.json)
|
|
130
127
|
return printJson(r);
|
|
131
128
|
if (!r.items.length)
|
|
132
|
-
return info('no domains bought through InstaCloud in this org
|
|
129
|
+
return info('no domains bought through InstaCloud in this org');
|
|
133
130
|
for (const d of r.items)
|
|
134
131
|
for (const line of domainLines(d, !opts.org))
|
|
135
132
|
info(line);
|
|
@@ -147,7 +144,7 @@ export async function domainStatus(name, opts, deps) {
|
|
|
147
144
|
die(`${host} was not bought through this org`);
|
|
148
145
|
if (opts.json)
|
|
149
146
|
return printJson({ domain, order });
|
|
150
|
-
for (const line of domain ? domainLines(domain, !opts.org) : orderStatusLines(order
|
|
147
|
+
for (const line of domain ? domainLines(domain, !opts.org) : orderStatusLines(order))
|
|
151
148
|
info(line);
|
|
152
149
|
}
|
|
153
150
|
// ---- nameservers and transferring out ----
|
|
@@ -369,10 +366,9 @@ export function zoneRecordLines(records) {
|
|
|
369
366
|
* into the zone itself — apexes included — instead of printing them for hand-copying. The zone is
|
|
370
367
|
* SEEDED by the provider's record scan, which is a heuristic: the review-then-switch contract
|
|
371
368
|
* (printed, and enforced by nothing else) is to compare `zone records` against the domain's current
|
|
372
|
-
* DNS
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* either way.
|
|
369
|
+
* DNS before re-pointing. A domain carrying live MX records is refused outright: a DNS move that
|
|
370
|
+
* can drop mail is never done implicitly. 202 approval_required in agent mode (zone.delegate); org
|
|
371
|
+
* admin either way.
|
|
376
372
|
*/
|
|
377
373
|
const orgArgOf = (opts) => (opts.org ? ` --org ${opts.org}` : '');
|
|
378
374
|
export async function zoneDelegate(domainName, opts, deps) {
|
|
@@ -391,7 +387,7 @@ export async function zoneDelegate(domainName, opts, deps) {
|
|
|
391
387
|
const z = res.body;
|
|
392
388
|
for (const line of zoneLines(z, opts.org))
|
|
393
389
|
info(line);
|
|
394
|
-
info(`the zone was seeded by a provider scan
|
|
390
|
+
info(`the zone was seeded by a provider scan, which can miss records. Check \`insta domain zone records ${z.domainName}${orgArgOf(opts)}\` against your current DNS, and only then switch the nameservers.`);
|
|
395
391
|
}
|
|
396
392
|
export async function zoneList(opts, deps) {
|
|
397
393
|
const { api, orgId } = await orgDeps(opts, deps);
|
|
@@ -412,7 +408,7 @@ export async function zoneList(opts, deps) {
|
|
|
412
408
|
if (opts.json)
|
|
413
409
|
return printJson(r);
|
|
414
410
|
if (!r.items.length)
|
|
415
|
-
return info('no delegated zones
|
|
411
|
+
return info('no delegated zones');
|
|
416
412
|
for (const z of r.items)
|
|
417
413
|
for (const line of zoneLines(z, opts.org))
|
|
418
414
|
info(line);
|
|
@@ -425,7 +421,7 @@ export async function zoneRecords(domainName, opts, deps) {
|
|
|
425
421
|
for (const line of zoneRecordLines(r.items))
|
|
426
422
|
info(line);
|
|
427
423
|
if (r.items.length)
|
|
428
|
-
info(
|
|
424
|
+
info('every type shows here. The provider scan can miss records, so compare them against your current DNS before switching nameservers');
|
|
429
425
|
}
|
|
430
426
|
/**
|
|
431
427
|
* Release a delegated zone: the platform prunes the records it published and deletes the managed
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// `insta compute public-access` and `insta <redis|mysql|mongodb> public-access`: show, close or
|
|
2
|
+
// re-open a compute-plane service's public access (the platform's PUT .../services/:id/access).
|
|
3
|
+
//
|
|
4
|
+
// Every insta-compute service has a private address in its project branch: `<name>.insta.internal`
|
|
5
|
+
// for compute, `<name>-<type>.insta.internal` for a managed database (the platform's private_host;
|
|
6
|
+
// this file only prints what the platform reports, never composes one). Closing public access makes
|
|
7
|
+
// the service private-only: its URL answers 403 (a database's public *_URL stops connecting) and
|
|
8
|
+
// only services in the same branch reach it. Like `insta postgres public-access off`, closing asks
|
|
9
|
+
// first, and a non-interactive caller (agent, CI, --json) must pass --yes.
|
|
10
|
+
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
11
|
+
import { handleApproval, info, printJson, refuse } from '../util.js';
|
|
12
|
+
import { q, resolveSoleService } from './services.js';
|
|
13
|
+
import { alwaysOnArgs, confirmOnTerminal } from './postgres.js';
|
|
14
|
+
const isDb = (type) => type !== 'compute';
|
|
15
|
+
const credential = (type) => `${type.toUpperCase()}_URL`;
|
|
16
|
+
const privateCredential = (type) => `${type.toUpperCase()}_PRIVATE_URL`;
|
|
17
|
+
/** `<host>:<port>` for the private address, or undefined when the platform reports none. */
|
|
18
|
+
export function privateAddress(s) {
|
|
19
|
+
return s.private_host ? `${s.private_host}${s.port ? `:${s.port}` : ''}` : undefined;
|
|
20
|
+
}
|
|
21
|
+
// The show block. Pure, exported for tests.
|
|
22
|
+
export function publicAccessLines(s) {
|
|
23
|
+
const pub = s.public;
|
|
24
|
+
const head = `${s.type} ${s.name}: public access ${pub === false ? 'off' : pub === true ? 'on' : 'unknown'}`;
|
|
25
|
+
const why = pub === false
|
|
26
|
+
? (isDb(s.type) ? ` — ${credential(s.type)} does not connect; apps in this branch use ${privateCredential(s.type)}` : ' — its URL answers 403; only services in this branch reach it')
|
|
27
|
+
: pub === true ? (isDb(s.type) ? '' : ' — reachable from the internet at its URL') : '';
|
|
28
|
+
const lines = [head + why];
|
|
29
|
+
const addr = privateAddress(s);
|
|
30
|
+
if (addr) {
|
|
31
|
+
lines.push(` private: ${addr}${s.type === 'compute' ? ' (TCP and UDP, from services in this branch)' : ` (from services in this branch; ${privateCredential(s.type)} holds the connection string)`}`);
|
|
32
|
+
if (s.private_fqdn)
|
|
33
|
+
lines.push(` fqdn: ${s.private_fqdn} (for tools that skip the search domain, e.g. nslookup, dig)`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
lines.push(' private: none yet — the platform reports no private address for this service (not yet back-filled, a worker, or a region without the private network)');
|
|
37
|
+
}
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
// Map a refusal to a CLI-shaped message. Pure, exported for tests.
|
|
41
|
+
export function publicAccessErrorMessage(e, ctx) {
|
|
42
|
+
const code = e.body?.code;
|
|
43
|
+
const b = ctx.branch ? ` --branch ${ctx.branch}` : '';
|
|
44
|
+
if (code === 'private_network_disabled')
|
|
45
|
+
return `the private network is not yet available on this deployment — ${ctx.type} ${ctx.service} stays public`;
|
|
46
|
+
if (code === 'private_network_unavailable_in_region')
|
|
47
|
+
return `the region of ${ctx.type} ${ctx.service} has no private network yet — it stays public`;
|
|
48
|
+
if (code === 'no_private_name')
|
|
49
|
+
return `${ctx.type} ${ctx.service} has no private network name, so closing public access would leave it unreachable — rename it first: \`insta service rename ${ctx.type} ${ctx.service} <new-name>${b}\``;
|
|
50
|
+
if (code === 'private_name_taken')
|
|
51
|
+
return `${e.message} — rename one of the services: \`insta service rename ${ctx.type} ${ctx.service} <new-name>${b}\``;
|
|
52
|
+
return `${ctx.what} failed (${e.status}): ${e.message}`;
|
|
53
|
+
}
|
|
54
|
+
// What closing will break, said before asking. Pure, exported for tests.
|
|
55
|
+
export function closeWarning(s) {
|
|
56
|
+
return isDb(s.type)
|
|
57
|
+
? `closing public access: ${credential(s.type)} (the public address) stops connecting — external clients, CI and local development can no longer reach ${s.type} ${s.name}, and apps in this branch must use ${privateCredential(s.type)} (bind it and redeploy them)`
|
|
58
|
+
: `closing public access: ${s.type} ${s.name}'s URL will answer 403 and stop waking it — browsers, webhooks and every caller outside this branch lose it; services in this branch keep reaching it at its private address`;
|
|
59
|
+
}
|
|
60
|
+
export async function servicePublicAccess(type, first, second, opts, deps = {}) {
|
|
61
|
+
const { mode, service } = alwaysOnArgs(first, second);
|
|
62
|
+
const api = await ApiClient.load();
|
|
63
|
+
const p = await requireProject();
|
|
64
|
+
const branch = opts.branch ?? p.branch;
|
|
65
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
66
|
+
const svc = resolveSoleService(services, type, service);
|
|
67
|
+
if (!mode) {
|
|
68
|
+
if (opts.json)
|
|
69
|
+
return printJson({ name: svc.name, type: svc.type, public: svc.public ?? null, private_host: svc.private_host ?? null, private_fqdn: svc.private_fqdn ?? null, port: svc.port ?? null });
|
|
70
|
+
for (const l of publicAccessLines(svc))
|
|
71
|
+
info(l);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (mode === 'off' && !opts.yes) {
|
|
75
|
+
const tty = deps.tty ?? (!!process.stdin.isTTY && !!process.stdout.isTTY);
|
|
76
|
+
if (opts.json || !tty)
|
|
77
|
+
refuse([closeWarning(svc), `${type} ${svc.name}: re-run with --yes to confirm`]);
|
|
78
|
+
info(closeWarning(svc));
|
|
79
|
+
const ok = await (deps.confirm ?? confirmOnTerminal)(`Close public access to ${type} ${svc.name}?`);
|
|
80
|
+
if (!ok)
|
|
81
|
+
return info(`${type} ${svc.name}: public access unchanged`);
|
|
82
|
+
}
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${svc.id}/access`, { public: mode === 'on' });
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (e instanceof ApiError)
|
|
89
|
+
throw new Error(publicAccessErrorMessage(e, { type, service: svc.name, branch: opts.branch, what: mode === 'off' ? 'closing public access' : 'opening public access' }));
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
if (handleApproval(res, opts.json))
|
|
93
|
+
return;
|
|
94
|
+
if (opts.json)
|
|
95
|
+
return printJson(res.body);
|
|
96
|
+
const notice = typeof res.body?.notice === 'string' && res.body.notice ? res.body.notice : undefined;
|
|
97
|
+
info(`${type} ${svc.name}: public access ${mode === 'off' ? 'CLOSED' : 'open'}`);
|
|
98
|
+
if (notice)
|
|
99
|
+
info(` ${notice}`);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=private-network.js.map
|
|
@@ -105,20 +105,32 @@ export function resolveComputeServiceId(services, name) {
|
|
|
105
105
|
function defaultDatabasePort(type) {
|
|
106
106
|
return type === 'mysql' ? 3306 : type === 'mongodb' ? 27017 : 6379;
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
// its own IPv6 address). Checked locally so a typo fails before any network call; which ports tcp
|
|
110
|
-
// accepts is the platform's answer, not repeated here.
|
|
108
|
+
export const ENDPOINTS = ['http', 'tcp', 'udp', 'tcp+udp'];
|
|
111
109
|
export function parseEndpoint(raw) {
|
|
112
110
|
const v = raw.trim().toLowerCase();
|
|
113
|
-
if (v
|
|
114
|
-
throw new Error(`--endpoint must be http or tcp, got: ${raw}`);
|
|
111
|
+
if (!ENDPOINTS.includes(v))
|
|
112
|
+
throw new Error(`--endpoint must be http, tcp, udp or tcp+udp, got: ${raw}`);
|
|
115
113
|
return v;
|
|
116
114
|
}
|
|
115
|
+
// tcp, udp and tcp+udp are the same v6 lane with a different protocol set: no HTTP domain, and the
|
|
116
|
+
// address is endpoint_host on the service port.
|
|
117
|
+
export const isV6Endpoint = (e) => e === 'tcp' || e === 'udp' || e === 'tcp+udp';
|
|
117
118
|
// Where a tcp service is dialed. The host is set once a deploy has confirmed it; before that the
|
|
118
119
|
// row only knows its port.
|
|
119
120
|
export function tcpAddress(s) {
|
|
120
121
|
return s.endpoint_host ? `tcp://${s.endpoint_host}:${s.port}` : `tcp/${s.port ?? '?'} (address after the first deploy)`;
|
|
121
122
|
}
|
|
123
|
+
// Where a v6-lane service (tcp, udp or tcp+udp) is reached, one scheme per protocol it serves:
|
|
124
|
+
// tcp://host:port, udp://host:port, or both for tcp+udp. tcp is exactly tcpAddress. Before a
|
|
125
|
+
// deploy has confirmed the host, the row only knows the protocol and port.
|
|
126
|
+
export function v6Address(s) {
|
|
127
|
+
if (s.endpoint === 'tcp')
|
|
128
|
+
return tcpAddress(s);
|
|
129
|
+
const protos = s.endpoint === 'tcp+udp' ? ['tcp', 'udp'] : ['udp'];
|
|
130
|
+
if (!s.endpoint_host)
|
|
131
|
+
return `${s.endpoint === 'tcp+udp' ? 'tcp+udp' : 'udp'}/${s.port ?? '?'} (address after the first deploy)`;
|
|
132
|
+
return protos.map((pr) => `${pr}://${s.endpoint_host}:${s.port}`).join(', ');
|
|
133
|
+
}
|
|
122
134
|
// Map service-add options to the platform POST body. Pure, so it's unit-tested without a network
|
|
123
135
|
// mock (mirrors deployRequestBody in deploy.ts). Validation (which options are valid for which
|
|
124
136
|
// type) stays in servicesAdd, ahead of any network/config access.
|
|
@@ -199,7 +211,7 @@ export function serviceAddedLine(type, name, branch, svc) {
|
|
|
199
211
|
const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at ${svc.volume_mount_path ?? "/data"}` : '';
|
|
200
212
|
// The major belongs next to the connect hint: it decides which psql/pg_dump to reach for.
|
|
201
213
|
const pg = svc.type === 'postgres' ? pgBadge(svc.pg_version) : '';
|
|
202
|
-
const where = svc.endpoint
|
|
214
|
+
const where = isV6Endpoint(svc.endpoint) ? ` — ${v6Address(svc)}` : svc.domain ? ` — ${svc.domain}` : '';
|
|
203
215
|
return `added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${pg}${where}`;
|
|
204
216
|
}
|
|
205
217
|
// ` pg <major>` for a postgres row, or '' when the platform sent nothing usable. The field arrives
|
|
@@ -218,9 +230,13 @@ export function serviceListLine(s) {
|
|
|
218
230
|
// Postgres major, so the reader picks matching pg_dump/psql BEFORE connecting (a newer client
|
|
219
231
|
// dumps statements an older server cannot restore). Older platforms send no pg_version.
|
|
220
232
|
: s.type === 'postgres' ? pgBadge(s.pg_version) : '';
|
|
221
|
-
// A tcp compute service has no HTTP domain; its address is the
|
|
222
|
-
const where = s.type === 'compute' && s.endpoint
|
|
223
|
-
|
|
233
|
+
// A tcp/udp/tcp+udp compute service has no HTTP domain; its address is the v6-lane one.
|
|
234
|
+
const where = s.type === 'compute' && isV6Endpoint(s.endpoint) ? ` ${v6Address(s)}` : s.domain ? ` ${s.domain}` : '';
|
|
235
|
+
// The private address the platform reports (never composed here), and a closed compute service
|
|
236
|
+
// says so beside the URL it no longer serves.
|
|
237
|
+
const closed = s.type === 'compute' && s.public === false ? ' public off' : '';
|
|
238
|
+
const priv = s.private_host ? ` private ${s.private_host}${s.port ? `:${s.port}` : ''}` : '';
|
|
239
|
+
return `${s.type}/${s.name} [${s.status}]${extra}${where}${closed}${priv} ${s.id}`;
|
|
224
240
|
}
|
|
225
241
|
export async function servicesList(opts) {
|
|
226
242
|
const api = await ApiClient.load();
|
|
@@ -344,6 +344,9 @@ async function promptVariable(v) {
|
|
|
344
344
|
throw new CliCancel();
|
|
345
345
|
return answer.trim();
|
|
346
346
|
}
|
|
347
|
+
// The poll route is keyed by deployment id, not project: name the project so agent mode signs
|
|
348
|
+
// with the project-bound session (a bootstrap session is rejected as "for a different project").
|
|
349
|
+
const readDeployment = (api, projectId) => (id) => api.request('GET', `/template-deployments/${id}`, undefined, { projectId });
|
|
347
350
|
export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
348
351
|
const given = parseSetFlags(opts.set ?? []); // a typo'd --set fails before any network access
|
|
349
352
|
// --json asked for parseable output: every human progress line is suppressed so stdout carries
|
|
@@ -424,9 +427,7 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
424
427
|
for (const line of publicBucketLines(services))
|
|
425
428
|
info(line);
|
|
426
429
|
}
|
|
427
|
-
|
|
428
|
-
// with the project-bound session (a bootstrap session is rejected as "for a different project").
|
|
429
|
-
const dep = await watchDeployment((id) => api.request('GET', `/template-deployments/${id}`, undefined, { projectId: p.projectId }), deploymentId, quiet ? () => { } : info, deps.wait);
|
|
430
|
+
const dep = await watchDeployment(readDeployment(api, p.projectId), deploymentId, quiet ? () => { } : info, deps.wait);
|
|
430
431
|
if (opts.json)
|
|
431
432
|
return printJson(source ? { source, ...dep } : dep);
|
|
432
433
|
info(`template ${codeLabel} deployed to branch ${branchName}`);
|
|
@@ -526,10 +527,10 @@ export async function templateUpgrade(service, opts = {}, deps = {}) {
|
|
|
526
527
|
const branchName = opts.branch ?? p.branch;
|
|
527
528
|
const ask = deps.ask ?? promptVariable;
|
|
528
529
|
const target = await deploymentOfService(api, p.projectId, branchName, service, 'upgrade');
|
|
529
|
-
// The
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
530
|
+
// The preview route is keyed by deployment id, not project, so it names the project as
|
|
531
|
+
// readDeployment does. It is a GET and raises coded 409s of its own (template_superseded among
|
|
532
|
+
// them), so it gets the same reading as the POST rather than the raw "<sentence> (HTTP 409)" the
|
|
533
|
+
// guard prints.
|
|
533
534
|
let plan;
|
|
534
535
|
try {
|
|
535
536
|
;
|
|
@@ -576,7 +577,7 @@ export async function templateUpgrade(service, opts = {}, deps = {}) {
|
|
|
576
577
|
const deploymentId = res.body.deploymentId ?? (res.body.deployment ?? res.body).id;
|
|
577
578
|
if (!quiet)
|
|
578
579
|
info(`upgrading ${service} to ${plan.to_version} (${deploymentId})`);
|
|
579
|
-
const dep = await watchDeployment((
|
|
580
|
+
const dep = await watchDeployment(readDeployment(api, p.projectId), deploymentId, quiet ? () => { } : info, deps.wait);
|
|
580
581
|
if (quiet)
|
|
581
582
|
return printJson(dep);
|
|
582
583
|
info(`${service} is on ${plan.to_version}`);
|
|
@@ -635,7 +636,7 @@ export async function templateRollback(service, opts = {}, deps = {}) {
|
|
|
635
636
|
return;
|
|
636
637
|
const deploymentId = res.body.deploymentId ?? (res.body.deployment ?? res.body).id;
|
|
637
638
|
out(`rolling ${service} back (${deploymentId})`);
|
|
638
|
-
const dep = await watchDeployment((
|
|
639
|
+
const dep = await watchDeployment(readDeployment(api, p.projectId), deploymentId, out, deps.wait);
|
|
639
640
|
if (quiet)
|
|
640
641
|
return printJson(dep);
|
|
641
642
|
info(`${service} is back on ${(dep.deployment ?? dep).template_version ?? 'its previous version'}`);
|
package/dist/deploy-archive.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
2
|
import { handleApproval } from './util.js';
|
|
3
|
+
import { ENDPOINTS, isV6Endpoint } from './commands/services.js';
|
|
3
4
|
export function archiveBuildSpec(hasDockerfile) {
|
|
4
5
|
return hasDockerfile ? { type: 'dockerfile' } : { type: 'nixpacks' };
|
|
5
6
|
}
|
|
@@ -72,7 +73,7 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
|
|
|
72
73
|
archive: ref,
|
|
73
74
|
port: opts.port ? Number(opts.port) : undefined,
|
|
74
75
|
websocket: typeof opts.websocket === 'boolean' ? opts.websocket : undefined,
|
|
75
|
-
endpoint:
|
|
76
|
+
endpoint: ENDPOINTS.includes(opts.endpoint ?? '') ? opts.endpoint : undefined,
|
|
76
77
|
replaceSource: opts.replaceSource === true ? true : undefined,
|
|
77
78
|
});
|
|
78
79
|
// An approval is a 202 too, told apart by its status word; anything else here is our operation.
|
|
@@ -122,8 +123,11 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
|
|
|
122
123
|
if (state === 'live') {
|
|
123
124
|
const image = res.body?.imageRef;
|
|
124
125
|
const url = res.body?.url;
|
|
125
|
-
//
|
|
126
|
+
// A tcp/udp/tcp+udp (v6-lane) service has no URL: its answer is the v6 host instead. A worker has neither.
|
|
126
127
|
const endpointHost = typeof res.body?.endpointHost === 'string' && res.body.endpointHost ? res.body.endpointHost : undefined;
|
|
128
|
+
// The endpoint the service is on (instacloud-platform#681), so the address gets its scheme;
|
|
129
|
+
// an older platform omits it, and a value this CLI does not know is treated as absent.
|
|
130
|
+
const endpoint = endpointHost && isV6Endpoint(res.body?.endpoint) ? res.body.endpoint : undefined;
|
|
127
131
|
if (typeof image !== 'string' || !image || typeof url !== 'string' || (!url && opts.port !== '0' && !endpointHost)) {
|
|
128
132
|
throw new Error('the deploy finished but the platform returned no image or URL for it — check `insta status`');
|
|
129
133
|
}
|
|
@@ -135,7 +139,7 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
|
|
|
135
139
|
return v;
|
|
136
140
|
};
|
|
137
141
|
return {
|
|
138
|
-
image, url, ...(endpointHost ? { endpointHost } : {}),
|
|
142
|
+
image, url, ...(endpointHost ? { endpointHost } : {}), ...(endpoint ? { endpoint } : {}),
|
|
139
143
|
branch: optionalString('branch', res.body.branch) ?? branch,
|
|
140
144
|
group: optionalString('group', res.body.group) ?? opts.group ?? '',
|
|
141
145
|
machineId: optionalString('machineId', res.body.machineId),
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { buildLogs } from './commands/build-logs.js';
|
|
|
28
28
|
import * as computeCmd from './commands/compute.js';
|
|
29
29
|
import * as githubCmd from './commands/github.js';
|
|
30
30
|
import * as pgCmd from './commands/postgres.js';
|
|
31
|
+
import * as privateNet from './commands/private-network.js';
|
|
31
32
|
import * as dbQueryCmd from './commands/db-query.js';
|
|
32
33
|
import * as managedDb from './commands/managed-db.js';
|
|
33
34
|
import * as storageCmd from './commands/storage.js';
|
|
@@ -155,7 +156,7 @@ svc.command('add [type] [name]').description('Provision a service on demand (com
|
|
|
155
156
|
.option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
|
|
156
157
|
.option('--image <url>', 'compute only: run this container image at creation')
|
|
157
158
|
.option('--port <n>', 'compute only: port the image listens on (default 8080; 0 for a worker without a routed port)')
|
|
158
|
-
.option('--endpoint <http|tcp>', 'compute only: how the service is reached — http (default: HTTPS at its domain) or tcp (raw TCP on --port at its own public IPv6 address; IPv6 clients only, no platform TLS, no custom domain). tcp needs --port and refuses commonly scanned, web (80/443) and platform-reserved ports')
|
|
159
|
+
.option('--endpoint <http|tcp|udp|tcp+udp>', 'compute only: how the service is reached — http (default: HTTPS at its domain), or tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only, no platform TLS, no custom domain). tcp needs --port and refuses commonly scanned, web (80/443) and platform-reserved ports; udp refuses amplification-prone (DNS, NTP, SNMP, memcached, …) and platform-reserved ports; tcp+udp refuses both lists. A udp datagram wakes a suspended service and what the wake held is re-sent until the service first replies (an app may see it twice); a receive-only udp service (never replies) needs --always-on to stay awake')
|
|
159
160
|
.option('--always-on', 'compute only: create as always-on — never scales to zero (all plans; billing is actual usage either way). A worker (--port 0) is always-on regardless')
|
|
160
161
|
.option('--no-always-on', 'compute only: create as scale-to-zero — idle machines suspend and wake on the next request (the default for new compute services; refused with --port 0, a worker cannot scale to zero)')
|
|
161
162
|
.option('--mount-path <path>', 'compute only: container mount path for a new volume (requires --volume; default /data)')
|
|
@@ -272,11 +273,12 @@ cron.command('runs <name>').description('Run history, most recent first: status,
|
|
|
272
273
|
cron.command('preview <expression>').description('Validate an expression and print its next five UTC fire times — answered by the same parser the scheduler uses. Exits 1 when the expression is invalid')
|
|
273
274
|
.option('--json').action(guard((expression, o) => cronCmd.cronPreview(expression, o)));
|
|
274
275
|
// ---- domain (bought here, or bring your own; hostnames on compute services; DNS of bought zones) ----
|
|
275
|
-
const dom = program.command('domain').description('Domains:
|
|
276
|
-
|
|
276
|
+
const dom = program.command('domain').description('Domains: attach, check and detach hostnames on compute services, and manage domains already bought through InstaCloud');
|
|
277
|
+
// search, buy and zone delegate are hidden while the platform pauses new orders and BYO delegation.
|
|
278
|
+
dom.command('search <keyword>', { hidden: true }).description('Search purchasable names with prices (a label like "myapp" or a full name like "myapp.com")')
|
|
277
279
|
.option('--tlds <list>', 'comma-separated TLDs to include').option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
278
280
|
.action(guard((keyword, o) => domainCmd.domainSearch(keyword, o)));
|
|
279
|
-
dom.command('buy <name>').description('Buy a domain — pay at the printed Stripe Checkout link. It serves nothing until you attach it (gated: domain.purchase)')
|
|
281
|
+
dom.command('buy <name>', { hidden: true }).description('Buy a domain — pay at the printed Stripe Checkout link. It serves nothing until you attach it (gated: domain.purchase)')
|
|
280
282
|
.option('--years <n>', 'registration term in years (default 1)')
|
|
281
283
|
.option('--no-open', 'print the checkout URL instead of opening a browser').option('--json')
|
|
282
284
|
.action(guard((name, o) => domainCmd.domainBuy(name, o)));
|
|
@@ -308,14 +310,14 @@ ns.command('set <domain> <nameservers...>').description("Delegate the zone to na
|
|
|
308
310
|
ns.command('reset <domain>').description("Put the zone back on the registrar's own nameservers — from your own delegation or from an InstaCloud-managed zone alike; a hostname `set` took down is re-attached with `insta domain attach`, one a managed zone was serving re-verifies on its own")
|
|
309
311
|
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
310
312
|
.action(guard((domain, o) => domainCmd.domainNameserversReset(domain, o)));
|
|
311
|
-
const zone = dom.command('zone').description("Bring-your-own domains
|
|
312
|
-
zone.command('delegate <domain>').description("Delegate a domain you own elsewhere: builds its managed zone (seeded by a provider record scan — a HEURISTIC, so review `zone records`
|
|
313
|
+
const zone = dom.command('zone').description("Bring-your-own domains already delegated to an InstaCloud-managed zone: list them, review their records, or release them. For a hostname under a domain you own elsewhere, `insta domain attach` prints the records to add in your own DNS");
|
|
314
|
+
zone.command('delegate <domain>', { hidden: true }).description("Delegate a domain you own elsewhere: builds its managed zone (seeded by a provider record scan — a HEURISTIC, so review `zone records` against your current DNS BEFORE switching nameservers) and answers the two nameservers to set at your registrar. A domain carrying live MX records is refused — move mail first or keep the records path (org admin; gated: zone.delegate — agent mode gates from a linked project, an unlinked --org call falls under org administration instead)")
|
|
313
315
|
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
314
316
|
.action(guard((domain, o) => domainCmd.zoneDelegate(domain, o)));
|
|
315
317
|
zone.command('list').description("The org's delegated zones: which are still waiting for the nameserver switch and which are serving")
|
|
316
318
|
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
317
319
|
.action(guard((o) => domainCmd.zoneList(o)));
|
|
318
|
-
zone.command('records <domain>').description("Every record in the delegated zone
|
|
320
|
+
zone.command('records <domain>').description("Every record in the delegated zone, for review before you switch nameservers. The provider scan seeds common records but is not exhaustive, so compare it against your current DNS first")
|
|
319
321
|
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
320
322
|
.action(guard((domain, o) => domainCmd.zoneRecords(domain, o)));
|
|
321
323
|
zone.command('release <domain>').description("Release a delegated zone: the platform's records are pruned and the zone deleted. Point the nameservers back at your own provider; hostnames re-verify on the records path (org admin; gated: zone.delegate — agent mode gates from a linked project, an unlinked --org call falls under org administration instead)")
|
|
@@ -365,7 +367,7 @@ function addObservability(group, component, noun) {
|
|
|
365
367
|
// optional makes commander unable to hold that boundary itself).
|
|
366
368
|
const { argv: computeArgv, command: execCommand, windowsFallback: execWindowsFallback, } = computeCmd.splitExecArgs(process.argv);
|
|
367
369
|
// ---- compute ----
|
|
368
|
-
const compute = program.command('compute').description('Compute services: lifecycle (start/stop/suspend/restart/status), scale, limits, volume, always-on, exec, ssh, GitHub source, logs, metrics');
|
|
370
|
+
const compute = program.command('compute').description('Compute services: lifecycle (start/stop/suspend/restart/status), scale, limits, volume, public access, always-on, exec, ssh, GitHub source, logs, metrics');
|
|
369
371
|
compute.command('start [service]').description('Bring a compute service online (persistent — re-enables auto-wake)')
|
|
370
372
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStart(service, o)));
|
|
371
373
|
compute.command('stop [service]').description('Take a compute service offline; traffic will NOT wake it until `start`')
|
|
@@ -384,6 +386,10 @@ compute.command('scale [count] [service]').description('Set a compute service sa
|
|
|
384
386
|
compute.command('limits [service]').description("Show or set a compute service's resource ceiling (any plan within the free cap; raising above it needs a paid plan). --memory is the dial; cpu derives from it unless --cpu is given. Billing is actual usage — the ceiling caps what the app may burn, it is not a price")
|
|
385
387
|
.option('--memory <size>', 'memory ceiling, e.g. 512mb or 1gb').option('--cpu <n>', 'vCPU ceiling override (provider sizes: 1, 2, 4, 6, 8)')
|
|
386
388
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
|
|
389
|
+
compute.command('public-access [mode] [service]').description("Show, close or re-open a compute service's public access (mode: on|off; no mode = print it and the service's private address). Every compute service is reachable by the other services in its project branch at <name>.insta.internal:<port> (TCP and UDP); off = private-only: its URL answers 403 and no longer wakes it, so only services in the same branch reach it. Immediate, no redeploy. Asks first; --yes when there is no terminal or with --json. insta-compute only (gated: service.setAccess)")
|
|
390
|
+
.option('-y, --yes', 'close without asking (required when there is no terminal, or with --json)')
|
|
391
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
392
|
+
.action(guard((mode, service, o) => privateNet.servicePublicAccess('compute', mode, service, o)));
|
|
387
393
|
compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero; off = scale-to-zero (the default for new compute services; refused on a worker, port 0). All plans; billing is actual usage either way')
|
|
388
394
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
|
|
389
395
|
compute.command('ssh [service]')
|
|
@@ -463,7 +469,7 @@ pg.command('public-access [mode] [service]').description("Show, close or re-open
|
|
|
463
469
|
addObservability(pg, 'db', 'postgres');
|
|
464
470
|
// ---- redis / mysql / mongodb (managed databases) ----
|
|
465
471
|
for (const type of ['redis', 'mysql', 'mongodb']) {
|
|
466
|
-
const g = program.command(type).description(`Managed ${type} services: query, status, resource ceiling, volume, always-on, logs, metrics`);
|
|
472
|
+
const g = program.command(type).description(`Managed ${type} services: query, status, resource ceiling, volume, public access, always-on, logs, metrics`);
|
|
467
473
|
const query = g.command('query <service> [args...]').description(type === 'redis'
|
|
468
474
|
? 'Run a redis command against the service via the console exec API — a pre-tokenized argv, e.g. `GET mykey`'
|
|
469
475
|
: `Run one quoted ${type} statement against the service via the console exec API`)
|
|
@@ -482,6 +488,10 @@ for (const type of ['redis', 'mysql', 'mongodb']) {
|
|
|
482
488
|
.option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
|
|
483
489
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
484
490
|
.action(guard((service, o) => computeCmd.serviceVolume(type, service, o)));
|
|
491
|
+
g.command('public-access [mode] [service]').description(`Show, close or re-open a ${type} service's public access (mode: on|off; no mode = print it and the private address). On insta-compute every ${type} is reachable from services in its project branch at <name>-${type}.insta.internal (${type.toUpperCase()}_PRIVATE_URL); off = ${type.toUpperCase()}_URL stops connecting and only apps in the branch reach it, through ${type.toUpperCase()}_PRIVATE_URL. Asks first; --yes when there is no terminal or with --json (gated: service.setAccess)`)
|
|
492
|
+
.option('-y, --yes', 'close without asking (required when there is no terminal, or with --json)')
|
|
493
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
494
|
+
.action(guard((mode, service, o) => privateNet.servicePublicAccess(type, mode, service, o)));
|
|
485
495
|
g.command('always-on <mode> [service]').description(`Set a ${type} service always-on (mode: on|off). on = machines never scale to zero; off = scale-to-zero. Billing is actual usage either way`)
|
|
486
496
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
487
497
|
.action(guard((mode, service, o) => computeCmd.serviceAlwaysOn(type, mode, service, o)));
|
|
@@ -526,7 +536,7 @@ buildCmd.command('logs <build-id>').description('Read source-build output for a
|
|
|
526
536
|
program.command('deploy [dir]').description('Deploy a source directory (built remotely; on insta-compute a Dockerfile is optional and nixpacks detects the runtime) or a prebuilt --image to a branch compute group')
|
|
527
537
|
.option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>', 'port the app listens on (0 for a worker without a routed port)')
|
|
528
538
|
.option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
|
|
529
|
-
.option('--endpoint <http|tcp>', 'switch how the service is reached from this deploy on: tcp (raw TCP on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
|
|
539
|
+
.option('--endpoint <http|tcp|udp|tcp+udp>', 'switch how the service is reached from this deploy on: tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
|
|
530
540
|
.option('--replace-source', 'the service deploys from a connected GitHub repo: switch it to this image and remove the repo connection (admin); without it such a deploy is refused')
|
|
531
541
|
.option('--json', 'print the deploy result as JSON (build progress goes to stderr)')
|
|
532
542
|
.action(guard((dir, o) => deploy(dir, o)));
|