insta 0.1.17 → 0.1.19
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/compute.js +2 -1
- package/dist/commands/template.js +25 -2
- package/dist/index.js +2 -1
- package/dist/template-manifest.js +8 -28
- package/package.json +1 -1
package/dist/commands/compute.js
CHANGED
|
@@ -1883,7 +1883,8 @@ export async function computeSSH(serviceName, opts, deps = {}) {
|
|
|
1883
1883
|
const mint = deps.mint ?? mintCert;
|
|
1884
1884
|
const emit = deps.emit ?? info;
|
|
1885
1885
|
const api = await (deps.loadApi ?? ApiClient.load)();
|
|
1886
|
-
|
|
1886
|
+
// 'main' is what a link records by default; anything else makes assertAliasFree refuse a setup of the same service from a linked directory.
|
|
1887
|
+
const p = opts.project !== undefined ? { projectId: opts.project, branch: 'main' } : await (deps.loadProject ?? requireProject)();
|
|
1887
1888
|
const branch = opts.branch ?? p.branch;
|
|
1888
1889
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
1889
1890
|
const svc = resolveSoleService(services, 'compute', serviceName);
|
|
@@ -35,6 +35,8 @@ export function normalizeInfoServices(raw) {
|
|
|
35
35
|
volumeGib: s?.volumeGib ?? s?.volume?.size,
|
|
36
36
|
volume: s?.volume === true || s?.volumeGib != null || s?.volume?.size != null,
|
|
37
37
|
mountPath: typeof s?.mountPath === 'string' ? s.mountPath : undefined,
|
|
38
|
+
pgVersion: typeof s?.pgVersion === 'number' ? s.pgVersion : undefined,
|
|
39
|
+
public: typeof s?.public === 'boolean' ? s.public : undefined,
|
|
38
40
|
});
|
|
39
41
|
if (Array.isArray(raw))
|
|
40
42
|
return raw.map((s) => one(s?.name ?? '?', s));
|
|
@@ -56,6 +58,20 @@ export function normalizeInfoVariables(raw) {
|
|
|
56
58
|
}
|
|
57
59
|
return [];
|
|
58
60
|
}
|
|
61
|
+
// The type plus what the manifest fixes about it: a Postgres major, or whether anyone can read a bucket.
|
|
62
|
+
function infoKind(s) {
|
|
63
|
+
if (s.type === 'postgres' && s.pgVersion !== undefined)
|
|
64
|
+
return `postgres ${s.pgVersion}`;
|
|
65
|
+
if (s.type === 'storage')
|
|
66
|
+
return `storage, ${s.public ? 'public' : 'private'}`;
|
|
67
|
+
return s.type && s.type !== 'compute' ? s.type : undefined;
|
|
68
|
+
}
|
|
69
|
+
// One line per bucket the template opens to the world, so a deploy never makes one public unannounced.
|
|
70
|
+
export function publicBucketLines(services) {
|
|
71
|
+
return normalizeInfoServices(services)
|
|
72
|
+
.filter((s) => s.type === 'storage' && s.public === true)
|
|
73
|
+
.map((s) => `${s.name}: public bucket, anyone can read its files (anonymous public-read)`);
|
|
74
|
+
}
|
|
59
75
|
// `bold` is injected so the renderer stays pure (tests pass identity; the command passes ANSI
|
|
60
76
|
// bold on a TTY).
|
|
61
77
|
export function templateInfoLines(t, bold = (s) => s) {
|
|
@@ -75,7 +91,7 @@ export function templateInfoLines(t, bold = (s) => s) {
|
|
|
75
91
|
// A size only when the registry still carries one: a manifest names no size any more, so
|
|
76
92
|
// "persistent /data" is all there is to say until the service exists.
|
|
77
93
|
const disk = s.volumeGib ? `${s.volumeGib}Gi volume` : s.volume ? `persistent ${s.mountPath ?? '/data'}` : undefined;
|
|
78
|
-
const bits = [s
|
|
94
|
+
const bits = [infoKind(s), s.port ? `port ${s.port}` : undefined, disk].filter(Boolean);
|
|
79
95
|
return `${s.name}${bits.length ? ` (${bits.join(', ')})` : ''}`;
|
|
80
96
|
});
|
|
81
97
|
lines.push(`services (${services.length}): ${summary.join(', ')}`);
|
|
@@ -325,10 +341,12 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
325
341
|
let manifest;
|
|
326
342
|
let source;
|
|
327
343
|
let vars;
|
|
344
|
+
let services; // what the template declares, for the one thing the deployer must be told
|
|
328
345
|
if (mode.kind === 'github') {
|
|
329
346
|
const fetched = await (deps.fetchGitHub ?? ((t) => fetchGitHubTemplate(t)))(mode.target);
|
|
330
347
|
manifest = fetched.manifest;
|
|
331
348
|
source = fetched.source;
|
|
349
|
+
services = manifest.services;
|
|
332
350
|
vars = collectManifestVariables(manifest);
|
|
333
351
|
// Exactly ONE line in front of today's output. A second "deploying template …" line
|
|
334
352
|
// would read as a duplicate of the "deploying template <code> to branch <branch>" line below,
|
|
@@ -339,6 +357,7 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
339
357
|
}
|
|
340
358
|
else if (mode.kind === 'local') {
|
|
341
359
|
manifest = loadTemplateManifest(mode.dir); // parse + local validation (pinned images, described vars)
|
|
360
|
+
services = manifest.services;
|
|
342
361
|
vars = collectManifestVariables(manifest);
|
|
343
362
|
if (!quiet)
|
|
344
363
|
info(`deploying local template ${manifest.code}@${manifest.version}`);
|
|
@@ -347,6 +366,7 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
347
366
|
// Learn the variable set up front from the registry so prompting happens before the POST.
|
|
348
367
|
const tpl = await api.request('GET', `/templates/${encodeURIComponent(mode.code)}`);
|
|
349
368
|
vars = normalizeInfoVariables((tpl.template ?? tpl).variables);
|
|
369
|
+
services = (tpl.template ?? tpl).services;
|
|
350
370
|
}
|
|
351
371
|
// --json asked for parseable output, so a caller that happens to own a TTY still gets the error.
|
|
352
372
|
const tty = !opts.json && !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
@@ -381,8 +401,11 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
381
401
|
const deploymentId = res.body.deploymentId ?? (res.body.deployment ?? res.body).id;
|
|
382
402
|
const acceptedRegion = (res.body.deployment ?? res.body).region;
|
|
383
403
|
const codeLabel = manifest?.code ?? target;
|
|
384
|
-
if (!quiet)
|
|
404
|
+
if (!quiet) {
|
|
385
405
|
info(`deploying template ${codeLabel} to branch ${branchName}${acceptedRegion ? ` in ${acceptedRegion}` : ''} (${deploymentId})`);
|
|
406
|
+
for (const line of publicBucketLines(services))
|
|
407
|
+
info(line);
|
|
408
|
+
}
|
|
386
409
|
// The poll route is keyed by deployment id, not project: name the project so agent mode signs
|
|
387
410
|
// with the project-bound session (a bootstrap session is rejected as "for a different project").
|
|
388
411
|
const dep = await watchDeployment((id) => api.request('GET', `/template-deployments/${id}`, undefined, { projectId: p.projectId }), deploymentId, quiet ? () => { } : info, deps.wait);
|
package/dist/index.js
CHANGED
|
@@ -388,6 +388,7 @@ compute.command('ssh [service]')
|
|
|
388
388
|
.description("Issue a short-lived SSH certificate for a compute service and print the command that uses it -- this command does NOT open the session itself, it makes `ssh` work. `--setup` does the one-time work: it generates a dedicated key under ~/.insta/ssh (your existing keys are never touched), has the platform sign a SHORT-LIVED certificate for it, adds one @cert-authority line to ~/.ssh/known_hosts so every region is trusted without per-node fingerprint prompts, and writes an ssh_config block AT THE TOP of ~/.ssh/config giving each compute service the alias `<service>.insta`. After that it is plain `ssh api.insta`, scp and -L: the block renews that alias's certificate for you while OpenSSH parses the config. Needs an interactive login -- API keys are refused; use `insta compute exec` for one-shot commands from CI")
|
|
389
389
|
.option('--setup', 'do the one-time client setup as well as issuing a certificate')
|
|
390
390
|
.option('--ensure-cert <alias>', 'renew the certificate for an alias such as api.insta if it is close to expiry, then exit (used by the ssh_config hook; silent by design)')
|
|
391
|
+
.option('--project <id>', 'project to use instead of the linked one; the directory is not linked (branch defaults to main)')
|
|
391
392
|
.option('-b, --branch <branch>', 'branch (default: linked)')
|
|
392
393
|
.option('--json', 'machine-readable output')
|
|
393
394
|
.action(guard((service, o) => computeCmd.computeSSH(service, o)));
|
|
@@ -540,7 +541,7 @@ tpl.command('info <code>').description('Show a template: version, upstream pin,
|
|
|
540
541
|
.option('--json').action(guard((code, o) => template.templateInfo(code, o)));
|
|
541
542
|
tpl.command('deploy <code-or-dir-or-url>').description('Deploy a template onto a branch — a registry code, a local directory containing insta.template.yaml (a path-looking target is always read as a directory), or a github.com URL (https://github.com/<owner>/<repo>[/tree/<ref>[/<dir>]]) whose manifest is fetched with your own git credentials. Missing required variables are prompted for on a terminal; generator-backed (secret:N) and defaulted ones are resolved by the platform')
|
|
542
543
|
.option('--branch <b>', 'target branch (default: current)')
|
|
543
|
-
.option('--region <region>', 'region for every service the template creates, e.g. us-east (see `insta config regions`)')
|
|
544
|
+
.option('--region <region>', 'region for every service the template creates (a storage bucket has none), e.g. us-east (see `insta config regions`)')
|
|
544
545
|
.option('--set <NAME=value>', 'set a template variable (repeatable)', (v, prev) => [...prev, v], [])
|
|
545
546
|
.option('-y, --yes', 'non-interactive: missing required variables fail with a --set list instead of prompting')
|
|
546
547
|
.option('--json')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Local insta.template.yaml parsing + validation for `insta template deploy ./dir` — the CLI
|
|
2
2
|
// twin of the platform's src/provisioning/templateManifest.ts (THE authority; the executor
|
|
3
3
|
// revalidates every manifest). Local checks exist to fail fast with a file the author can act
|
|
4
|
-
// on, before anything travels, so the rules here mirror the server's
|
|
4
|
+
// on, before anything travels, so the rules here mirror the server's for web and worker services and leave every other type to it — plus two
|
|
5
5
|
// authoring lints the server does not enforce: images must be pinned, and required variables
|
|
6
6
|
// need a description unless a generator answers for the user. Everything here is pure over the
|
|
7
7
|
// parsed document (unit-tested); only loadTemplateManifest touches disk.
|
|
@@ -14,9 +14,10 @@ export const MANIFEST_FILE = 'insta.template.yaml';
|
|
|
14
14
|
const CODE_RE = /^[a-z0-9][a-z0-9-]{0,38}$/;
|
|
15
15
|
export const ENV_NAME_RE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
16
16
|
const GENERATOR_RE = /^secret:([1-9]\d{0,2})$/;
|
|
17
|
-
// The
|
|
18
|
-
const
|
|
19
|
-
|
|
17
|
+
// The compute vocabulary every CLI has known. Any other type is the platform's to accept or refuse.
|
|
18
|
+
const COMPUTE_TYPES = ['web', 'worker'];
|
|
19
|
+
// Datastores that never gain a url or host. Storage is left off because a public bucket may get an address.
|
|
20
|
+
const NO_ADDRESS_TYPES = ['postgres', 'redis', 'mysql', 'mongodb'];
|
|
20
21
|
// What counts as a digest is the PLATFORM's call, not ours: registry.ts's DIGEST regex, verbatim.
|
|
21
22
|
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
22
23
|
// Why an image must carry a pin: a template is a reproducible deploy, and a tagless ref (implicit
|
|
@@ -83,30 +84,9 @@ export function validateManifest(m) {
|
|
|
83
84
|
const where = `services.${name}`;
|
|
84
85
|
// typeof, not String(): a YAML array like [redis] would otherwise stringify to its lone element.
|
|
85
86
|
const type = typeof svc.type === 'string' ? svc.type : undefined;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
// A managed datastore is BARE: the platform owns its image, port, sizing, credentials and env,
|
|
90
|
-
// so every other rule below would be asking about fields it must not carry. Mirrors the
|
|
91
|
-
// platform's own check (provisioning/templateManifest.ts) so an author hears it here.
|
|
92
|
-
if (type && MANAGED_TYPES.includes(type)) {
|
|
93
|
-
const bare = svc;
|
|
94
|
-
for (const field of ['image', 'build', 'port', 'healthcheck', 'volume', 'volumeGib', 'spec', 'alwaysOn', 'command', 'mountPath']) {
|
|
95
|
-
if (bare[field] !== undefined) {
|
|
96
|
-
problems.push(`${where}.${field}: a ${type} service is platform-managed and carries no ${field} — declare it bare ({ type: ${type} })`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const groups = ['fixed', 'generated', 'platform', 'required', 'optional'];
|
|
100
|
-
const envShell = bare.env;
|
|
101
|
-
if (envShell !== undefined) {
|
|
102
|
-
const emptyShell = !!envShell && typeof envShell === 'object' && !Array.isArray(envShell)
|
|
103
|
-
&& Object.entries(envShell).every(([g, v]) => groups.includes(g) && !!v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0);
|
|
104
|
-
if (!emptyShell) {
|
|
105
|
-
problems.push(`${where}.env: a ${type} service is platform-managed and carries no env — declare it bare ({ type: ${type} })`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
87
|
+
// Every other type, and every field and env on it, is the platform's to judge. It answers 400 with its own list.
|
|
88
|
+
if (!type || !COMPUTE_TYPES.includes(type))
|
|
108
89
|
continue;
|
|
109
|
-
}
|
|
110
90
|
if (svc.image && svc.build)
|
|
111
91
|
problems.push(`${where}: image and build are mutually exclusive`);
|
|
112
92
|
if (!svc.image && !svc.build)
|
|
@@ -200,7 +180,7 @@ export function validateManifest(m) {
|
|
|
200
180
|
continue;
|
|
201
181
|
const target = services[svcRef[1]];
|
|
202
182
|
const targetType = target && typeof target.type === 'string' ? target.type : undefined;
|
|
203
|
-
if (targetType &&
|
|
183
|
+
if (targetType && NO_ADDRESS_TYPES.includes(targetType)) {
|
|
204
184
|
problems.push(`services.${name}.env.fixed.${varName}: '${svcRef[1]}' is a managed ${targetType}, so it has no url or host. Use its platform credentials instead: \${{services.${svcRef[1]}.<KEY>}} under env.platform`);
|
|
205
185
|
}
|
|
206
186
|
}
|