insta 0.1.18 → 0.1.20

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.
@@ -68,10 +68,26 @@ export function domainGuidanceLines(r, ctx = {}) {
68
68
  // where the hostname resolves — we cannot confirm routing, and must not call it serving. A Fly
69
69
  // answer carries no `ssl` and has no per-hostname origin concept, so its own verdict stands.
70
70
  const reportsOrigin = (r) => r.ssl !== undefined;
71
+ // A deployment with NO edge custody for custom domains (a pre-Cloudflare plane). It reports
72
+ // `origin: ""` exactly like a region whose operator forgot cf-custom-origin, so the pair alone reads
73
+ // it backwards and names a setting this deployment does not have. The plane's own verdict
74
+ // (`originStatus: 'unmanaged'`) decides when relayed; without it, `ssl: 'external'` is the plane's
75
+ // marker for the same deployment class (it manages no edge certificate here) — the same fallback the
76
+ // console uses. Only an unconfirmed origin is reinterpreted: a reported, confirmed origin stands.
77
+ const edgeUnmanaged = (r) => r.originStatus === 'unmanaged' ||
78
+ (r.originStatus === undefined && r.ssl === 'external' && (r.origin === undefined || r.origin === ''));
71
79
  // Where the hostname actually resolves — the region-specific origin the plane requested vs what
72
80
  // Cloudflare holds. Each shape carries its action; absent fields are reported as absent.
73
81
  export function domainResolveLine(r) {
74
82
  const region = r.region ?? 'this region';
83
+ if (edgeUnmanaged(r)) {
84
+ // Not ready (routing cannot be confirmed from here), but no operator knob to name either:
85
+ // setting cf-custom-origin does nothing on a deployment with no edge custody.
86
+ return {
87
+ line: ` ${pad('resolves to', 12)}UNMANAGED — ${region} does not manage edge certificates for custom domains, so where ${r.hostname} lands cannot be confirmed from here; if it does not serve, contact support`,
88
+ ready: false,
89
+ };
90
+ }
75
91
  if (r.origin === undefined) {
76
92
  if (reportsOrigin(r)) {
77
93
  return {
@@ -194,6 +210,7 @@ export function domainStatusLines(r, ctx = {}) {
194
210
  }
195
211
  }
196
212
  const ssl = r.ssl ?? (r.configured ? 'active' : undefined);
213
+ const reasonOnCert = !!r.errorReason && r.status !== 'error' && ssl !== undefined && ssl !== 'active' && ssl !== 'external';
197
214
  if (ssl === 'active')
198
215
  stage('certificate', 'active', '(edge TLS issued)');
199
216
  else if (ssl === 'external')
@@ -203,7 +220,11 @@ export function domainStatusLines(r, ctx = {}) {
203
220
  blockers.push('certificate');
204
221
  }
205
222
  else {
206
- stage('certificate', 'pending', `(${ssl} — issues once ownership is verified)`);
223
+ // A pending certificate with a reason from the plane: the reason IS why it has not issued (a CAA
224
+ // block, the hostname not pointed at the edge), so it replaces the generic text here and is not
225
+ // repeated below. An `error` row keeps its reason on its own error line.
226
+ const detail = reasonOnCert ? `(${ssl}) ${r.errorReason}` : `(${ssl} — issues once ownership is verified)`;
227
+ stage('certificate', 'pending', detail);
207
228
  blockers.push('certificate');
208
229
  }
209
230
  const resolve = domainResolveLine(r);
@@ -214,9 +235,17 @@ export function domainStatusLines(r, ctx = {}) {
214
235
  stage('error', r.status, r.errorReason || '(the plane reported an error state with no reason)');
215
236
  blockers.push('the plane reports an error state');
216
237
  }
217
- else if (r.errorReason) {
218
- stage('error', r.status, r.errorReason);
219
- blockers.push(r.errorReason);
238
+ else if (r.errorReason && !reasonOnCert) {
239
+ // On an ACTIVE row the plane keeps serving and REPORTS what the edge says has regressed (it
240
+ // never demotes a serving row on an edge read). That is a warning beside a serving hostname,
241
+ // not a blocker: turning it into one would print "not yet" for a domain taking traffic.
242
+ // Every other state with a reason is still being set up, and the reason is what blocks it.
243
+ if (r.status === 'active')
244
+ stage('warning', r.status, r.errorReason);
245
+ else {
246
+ stage('error', r.status, r.errorReason);
247
+ blockers.push(r.errorReason);
248
+ }
220
249
  }
221
250
  // An unconfirmed routing target is one blocker among the others, not a headline that hides them:
222
251
  // a user fixing their DNS needs the whole outstanding list, not whichever item sorted first.
@@ -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.type && s.type !== 'compute' ? s.type : undefined, s.port ? `port ${s.port}` : undefined, disk].filter(Boolean);
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
@@ -541,7 +541,7 @@ tpl.command('info <code>').description('Show a template: version, upstream pin,
541
541
  .option('--json').action(guard((code, o) => template.templateInfo(code, o)));
542
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')
543
543
  .option('--branch <b>', 'target branch (default: current)')
544
- .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`)')
545
545
  .option('--set <NAME=value>', 'set a template variable (repeatable)', (v, prev) => [...prev, v], [])
546
546
  .option('-y, --yes', 'non-interactive: missing required variables fail with a --set list instead of prompting')
547
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 exactly — plus two
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 platform provisions these and owns everything about them (platform templateManifest.ts).
18
- const MANAGED_TYPES = ['postgres', 'redis', 'mysql', 'mongodb'];
19
- const MANIFEST_TYPES = ['web', 'worker', ...MANAGED_TYPES];
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
- if (!type || !MANIFEST_TYPES.includes(type)) {
87
- problems.push(`${where}.type must be one of ${MANIFEST_TYPES.join(', ')}`);
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 && MANAGED_TYPES.includes(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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [