insta 0.1.24 → 0.1.25
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/dist/commands/deploy.js +18 -10
- package/dist/commands/private-network.js +101 -0
- package/dist/commands/services.js +25 -9
- package/dist/deploy-archive.js +7 -3
- package/dist/index.js +13 -4
- package/package.json +1 -1
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
|
|
@@ -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();
|
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)')
|
|
@@ -365,7 +366,7 @@ function addObservability(group, component, noun) {
|
|
|
365
366
|
// optional makes commander unable to hold that boundary itself).
|
|
366
367
|
const { argv: computeArgv, command: execCommand, windowsFallback: execWindowsFallback, } = computeCmd.splitExecArgs(process.argv);
|
|
367
368
|
// ---- 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');
|
|
369
|
+
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
370
|
compute.command('start [service]').description('Bring a compute service online (persistent — re-enables auto-wake)')
|
|
370
371
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStart(service, o)));
|
|
371
372
|
compute.command('stop [service]').description('Take a compute service offline; traffic will NOT wake it until `start`')
|
|
@@ -384,6 +385,10 @@ compute.command('scale [count] [service]').description('Set a compute service sa
|
|
|
384
385
|
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
386
|
.option('--memory <size>', 'memory ceiling, e.g. 512mb or 1gb').option('--cpu <n>', 'vCPU ceiling override (provider sizes: 1, 2, 4, 6, 8)')
|
|
386
387
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
|
|
388
|
+
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)")
|
|
389
|
+
.option('-y, --yes', 'close without asking (required when there is no terminal, or with --json)')
|
|
390
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
391
|
+
.action(guard((mode, service, o) => privateNet.servicePublicAccess('compute', mode, service, o)));
|
|
387
392
|
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
393
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
|
|
389
394
|
compute.command('ssh [service]')
|
|
@@ -463,7 +468,7 @@ pg.command('public-access [mode] [service]').description("Show, close or re-open
|
|
|
463
468
|
addObservability(pg, 'db', 'postgres');
|
|
464
469
|
// ---- redis / mysql / mongodb (managed databases) ----
|
|
465
470
|
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`);
|
|
471
|
+
const g = program.command(type).description(`Managed ${type} services: query, status, resource ceiling, volume, public access, always-on, logs, metrics`);
|
|
467
472
|
const query = g.command('query <service> [args...]').description(type === 'redis'
|
|
468
473
|
? 'Run a redis command against the service via the console exec API — a pre-tokenized argv, e.g. `GET mykey`'
|
|
469
474
|
: `Run one quoted ${type} statement against the service via the console exec API`)
|
|
@@ -482,6 +487,10 @@ for (const type of ['redis', 'mysql', 'mongodb']) {
|
|
|
482
487
|
.option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
|
|
483
488
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
484
489
|
.action(guard((service, o) => computeCmd.serviceVolume(type, service, o)));
|
|
490
|
+
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)`)
|
|
491
|
+
.option('-y, --yes', 'close without asking (required when there is no terminal, or with --json)')
|
|
492
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
493
|
+
.action(guard((mode, service, o) => privateNet.servicePublicAccess(type, mode, service, o)));
|
|
485
494
|
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
495
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
487
496
|
.action(guard((mode, service, o) => computeCmd.serviceAlwaysOn(type, mode, service, o)));
|
|
@@ -526,7 +535,7 @@ buildCmd.command('logs <build-id>').description('Read source-build output for a
|
|
|
526
535
|
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
536
|
.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
537
|
.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')
|
|
538
|
+
.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
539
|
.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
540
|
.option('--json', 'print the deploy result as JSON (build progress goes to stderr)')
|
|
532
541
|
.action(guard((dir, o) => deploy(dir, o)));
|