@codeyam-editor/codeyam-editor 0.1.4 → 0.1.5
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/npm/cloud-branch-switch.js +1 -1
- package/npm/cloud.js +1415 -73
- package/npm/container.js +3 -0
- package/npm/disk-pressure.js +78 -0
- package/npm/editor-dev.js +96 -15
- package/npm/editor.js +85 -37
- package/npm/fleet-auth-health.js +75 -0
- package/npm/fleet-auth.js +6 -1
- package/npm/fleet-build-data.js +51 -2
- package/npm/fleet-cleanup.js +118 -7
- package/npm/fleet-destroy-safety.js +55 -0
- package/npm/fleet-effect-status.js +33 -32
- package/npm/fleet-host.js +74 -0
- package/npm/fleet-job-badge.js +1 -1
- package/npm/fleet-launchd.js +171 -0
- package/npm/fleet-live-status.js +123 -0
- package/npm/fleet-plans.js +135 -0
- package/npm/fleet-prewarm.js +165 -0
- package/npm/fleet-public-url.js +160 -0
- package/npm/fleet-queue.js +38 -1
- package/npm/fleet-reconcile.js +43 -1
- package/npm/fleet-reset.js +298 -13
- package/npm/fleet-roster.js +6 -8
- package/npm/fleet-self-update.js +142 -0
- package/npm/fleet-setup-steps.js +105 -0
- package/npm/fleet-sleep-status.js +33 -0
- package/npm/fleet-strand.js +150 -0
- package/npm/fleet-stuck-status.js +111 -0
- package/npm/fleet-vm-access.js +234 -0
- package/npm/fleet-vm-readiness.js +104 -0
- package/npm/fleet-vm-slugs.js +116 -0
- package/npm/google-ingress.js +211 -0
- package/npm/ingress-invoke.js +292 -0
- package/npm/launch-config.js +204 -3
- package/npm/scenario-check.js +603 -32
- package/npm/scenario-handlers.js +78 -12
- package/npm/scenario-metrics.js +116 -1
- package/npm/scenario-mocks.js +20 -0
- package/npm/scenario-playwright.js +366 -29
- package/npm/script-drift.js +81 -0
- package/npm/utils.js +329 -50
- package/npm/vm-urls.js +69 -8
- package/package.json +5 -5
- package/ui/dist/assets/ContainerInspector-D7VZkkaj.js +2 -0
- package/ui/dist/assets/EditorShell-DHhIaotd.css +1 -0
- package/ui/dist/assets/EditorShell-r-796N_W.js +191 -0
- package/ui/dist/assets/{Markdown-BHBqz-Mj.js → Markdown-BoLHHFXH.js} +1 -1
- package/ui/dist/assets/ProjectLauncherScreen-Ck-XWkQN.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-B9iDSUpA.js +5 -0
- package/ui/dist/assets/ScenarioDataPanelFullPage-CBLTGC4-.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-BhLfT-nX.js +21 -0
- package/ui/dist/assets/StepsSimulator-BFldWyoX.js +1 -0
- package/ui/dist/assets/index-CIXZkwfB.css +1 -0
- package/ui/dist/assets/{index-BuK0lizt.js → index-QsOiOG9x.js} +9 -9
- package/ui/dist/assets/useEvents-DHkLJZR4.js +1 -0
- package/ui/dist/codeyam-mark.svg +4 -0
- package/ui/dist/codeyam-wordmark.svg +5 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +0 -2
- package/ui/dist/assets/EditorShell-tMMdarZD.js +0 -187
- package/ui/dist/assets/ScenarioDataPanel-xWYGZOpo.js +0 -5
- package/ui/dist/assets/ScenarioDataPanelFullPage-5G6KuZgw.js +0 -1
- package/ui/dist/assets/ScenarioRenderer-CSdhX1Ak.js +0 -1
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +0 -1
- package/ui/dist/assets/index-BGqairFV.css +0 -1
- package/ui/dist/assets/useEvents-DggWrtHe.js +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Pure readiness derivation for the Fleet Dashboard's per-VM card
|
|
3
|
+
// (scripts/operator/fleet-dashboard/server.js requires this). Given the merged
|
|
4
|
+
// poll state `v` and the derived `ctx` the card builder already computes,
|
|
5
|
+
// collapse the VM into a single readiness label the card renders. No I/O —
|
|
6
|
+
// server.js owns the polling (including the slow-cadence LB backend-health
|
|
7
|
+
// probe that fills `v.backendHealth`).
|
|
8
|
+
//
|
|
9
|
+
// The bug this fixes: a VM card rendered as idle with a CLICKABLE editor URL the
|
|
10
|
+
// moment the VM was rostered + SSH-tunnel-reachable, even when its public LB URL
|
|
11
|
+
// (vm-<N>.editor.<domain>) would return Envoy "no healthy upstream" (503). The
|
|
12
|
+
// card reflected only SSH-tunnel reachability to :14199, never whether the load
|
|
13
|
+
// balancer actually routes to a HEALTHY backend. Two situations cause the 503,
|
|
14
|
+
// and the card had zero visibility into either:
|
|
15
|
+
//
|
|
16
|
+
// transient half-wire — the editor is serving but the LB health check hasn't
|
|
17
|
+
// marked the backend HEALTHY yet during cold start. It
|
|
18
|
+
// resolves on its own; the card should say 'starting'.
|
|
19
|
+
// permanent half-wire — the per-VM LB backend service was never created (e.g.
|
|
20
|
+
// the Add ran under a service account lacking
|
|
21
|
+
// compute.backendServices.create), so the URL 503s
|
|
22
|
+
// forever until re-wired. The card should say
|
|
23
|
+
// 'half-wired', visibly distinct from 'starting'.
|
|
24
|
+
//
|
|
25
|
+
// SSH reachability (v.reachable from probeVM over the tunnel) is necessary but
|
|
26
|
+
// not sufficient: the tunnel is up in BOTH 503 cases. Only the LB backend health
|
|
27
|
+
// (gcloud compute backend-services get-health, gathered into v.backendHealth)
|
|
28
|
+
// distinguishes ready / starting / half-wired. The editor's own /healthz can't
|
|
29
|
+
// see the half-wire, so it is not enough on its own.
|
|
30
|
+
//
|
|
31
|
+
// 'provisioning' — an Add/recover job is in flight; the VM is still being
|
|
32
|
+
// built, so judging its public-URL readiness is premature.
|
|
33
|
+
// 'unreachable' — the editor did not answer over the tunnel. Nothing
|
|
34
|
+
// downstream (broker, LB health) is meaningful; the link is
|
|
35
|
+
// gated exactly as today.
|
|
36
|
+
// 'half-wired' — LB mode, the editor backend service is ABSENT (never
|
|
37
|
+
// created). The permanent 503 case — the public URL will fail
|
|
38
|
+
// until re-wired. The state that had NO signal before.
|
|
39
|
+
// 'starting' — LB mode, the editor backend exists but is UNHEALTHY (LB has
|
|
40
|
+
// not yet marked it HEALTHY). The transient 503 case; flips to
|
|
41
|
+
// 'ready' on its own once the health check passes.
|
|
42
|
+
// 'broker-down' — reachable + LB-healthy (or legacy mode), but the broker
|
|
43
|
+
// daemon is down, so the VM cannot start an agent session.
|
|
44
|
+
// 'ready' — reachable, broker up, and (LB mode) the editor backend is
|
|
45
|
+
// HEALTHY. The only state whose editor link is live + green.
|
|
46
|
+
//
|
|
47
|
+
// Legacy mode (FLEET_DOMAIN unset ⇒ ctx.lbMode falsey, or no backendHealth
|
|
48
|
+
// supplied): there is no LB to health-check, so the backend-health branch is
|
|
49
|
+
// skipped and readiness degrades to today's reachability/broker behavior — no
|
|
50
|
+
// regression, no gcloud calls.
|
|
51
|
+
const READINESS_STATES = [
|
|
52
|
+
'provisioning',
|
|
53
|
+
'unreachable',
|
|
54
|
+
'half-wired',
|
|
55
|
+
'starting',
|
|
56
|
+
'broker-down',
|
|
57
|
+
'ready',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// A VM mid-provision: an Add (incl. its recovering re-run) that is queued
|
|
61
|
+
// (number reserved, cloud:up not started) or running. Its public URL has no
|
|
62
|
+
// meaningful readiness yet, so this takes precedence over every other signal —
|
|
63
|
+
// a freshly-reserved VM must not flash 'half-wired' before its backend exists.
|
|
64
|
+
function isProvisioning(job) {
|
|
65
|
+
if (!job) return false;
|
|
66
|
+
if (job.action === 'add' && (job.state === 'running' || job.state === 'queued')) return true;
|
|
67
|
+
if (job.recovering && job.state === 'running') return true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `v` is the merged poll state (state.vms[n]); `ctx` carries the derived values
|
|
72
|
+
// the card builder already has on hand:
|
|
73
|
+
// ctx.job — the active job (or null), for the provisioning check.
|
|
74
|
+
// ctx.brokerStatus — the debounced broker tri-state ('down'|'idle'|
|
|
75
|
+
// 'agent-live'|'unknown') the card already derives.
|
|
76
|
+
// ctx.lbMode — true when FLEET_DOMAIN is set (the LB fronts the URLs).
|
|
77
|
+
// When false, the backend-health branch is skipped.
|
|
78
|
+
function deriveVmReadiness(v, ctx) {
|
|
79
|
+
const c = ctx || {};
|
|
80
|
+
// 1. Still being built — don't judge a half-built VM as ready or half-wired.
|
|
81
|
+
if (isProvisioning(c.job)) return 'provisioning';
|
|
82
|
+
// 2. Editor unreachable over the tunnel: we polled nothing, so broker / LB
|
|
83
|
+
// health are unknowable. Link gated, exactly as the pre-readiness card did.
|
|
84
|
+
if (!v || !v.reachable) return 'unreachable';
|
|
85
|
+
// 3. LB mode only: the public URL's fate is the editor backend's LB health.
|
|
86
|
+
// Skipped entirely in legacy mode (no LB) or before the first probe lands
|
|
87
|
+
// (no backendHealth) — both fall through to the reachability/broker path.
|
|
88
|
+
const bh = v.backendHealth;
|
|
89
|
+
if (c.lbMode && bh) {
|
|
90
|
+
// Backend service never created ⇒ permanent 503 until re-wired.
|
|
91
|
+
if (bh.editor === 'absent') return 'half-wired';
|
|
92
|
+
// Backend exists but not yet HEALTHY ⇒ transient 503 during cold start.
|
|
93
|
+
if (bh.editor === 'unhealthy') return 'starting';
|
|
94
|
+
// 'healthy' falls through to ready; 'unknown' (get-health errored) is
|
|
95
|
+
// absence-of-evidence — never rendered as a problem, falls through too.
|
|
96
|
+
}
|
|
97
|
+
// 4. Reachable but the broker daemon is down: the VM can't start a session.
|
|
98
|
+
if (c.brokerStatus === 'down') return 'broker-down';
|
|
99
|
+
// 5. Reachable, broker up, and (LB mode) the editor backend is HEALTHY —
|
|
100
|
+
// genuinely ready: green, clickable, public URL actually routes.
|
|
101
|
+
return 'ready';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { deriveVmReadiness, READINESS_STATES, isProvisioning };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// npm/fleet-vm-slugs.js — the per-VM custom-slug sidecar
|
|
4
|
+
// (`.codeyam/logs/fleet-vm-slugs.json`). A slug is an optional human-readable
|
|
5
|
+
// override for a VM's leftmost hostname label, so its public URLs become
|
|
6
|
+
// `<slug>.editor.<domain>` instead of the number-derived `vm-<N>.editor.<domain>`.
|
|
7
|
+
//
|
|
8
|
+
// Why a sidecar and not the roster: the roster (`npm/fleet-roster.js`) is a flat
|
|
9
|
+
// integer array with a hard "sorted, de-duped set of integers" invariant that
|
|
10
|
+
// bash consumers (`vm-urls.sh`, the watchdog) depend on. Rather than break that
|
|
11
|
+
// shape, slugs live in a SEPARATE map `{ "<N>": "<slug>" }`, fronted by a
|
|
12
|
+
// read+write seam modeled exactly on fleet-roster.js (atomic temp-file + rename,
|
|
13
|
+
// tolerant of a missing/malformed file).
|
|
14
|
+
//
|
|
15
|
+
// The slug is purely a public-hostname override — internal GCP resource names
|
|
16
|
+
// (NEG / backend service / path matcher) stay keyed off the VM number, so a VM
|
|
17
|
+
// added without a slug keeps its `vm-<N>` URLs unchanged.
|
|
18
|
+
//
|
|
19
|
+
// Env override: `VM_SLUGS_FILE` relocates the file (mirrors fleet-roster.js's
|
|
20
|
+
// `VMS_STATE_FILE`); resolved once at module load.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const { rootDir } = require('./utils');
|
|
26
|
+
const { normalizeVmNumber, normalizeVmSlug } = require('./vm-urls');
|
|
27
|
+
|
|
28
|
+
const VM_SLUGS_FILE = process.env.VM_SLUGS_FILE
|
|
29
|
+
|| path.join(rootDir, '.codeyam', 'logs', 'fleet-vm-slugs.json');
|
|
30
|
+
|
|
31
|
+
// Read the slug sidecar, returning a `{ [vmNumber]: slug }` object, or null if
|
|
32
|
+
// the file is missing or malformed (treated as "no slugs"). Keys are kept as the
|
|
33
|
+
// string form JSON emits; callers look them up by `String(n)`.
|
|
34
|
+
function readSlugs() {
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(fs.readFileSync(VM_SLUGS_FILE, 'utf8'));
|
|
37
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) return data;
|
|
38
|
+
} catch { /* missing or malformed */ }
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Persist `map` as the sidecar, atomically: write a temp file then `rename` it
|
|
43
|
+
// over the target so a concurrent reader never observes a half-written file. The
|
|
44
|
+
// temp name carries the pid so two processes writing at once don't clobber each
|
|
45
|
+
// other's temp file before their renames land. Returns the written map.
|
|
46
|
+
function writeSlugs(map) {
|
|
47
|
+
const next = map && typeof map === 'object' && !Array.isArray(map) ? map : {};
|
|
48
|
+
fs.mkdirSync(path.dirname(VM_SLUGS_FILE), { recursive: true });
|
|
49
|
+
const tmp = `${VM_SLUGS_FILE}.${process.pid}.tmp`;
|
|
50
|
+
fs.writeFileSync(tmp, JSON.stringify(next));
|
|
51
|
+
fs.renameSync(tmp, VM_SLUGS_FILE); // atomic replace
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The slug mapped to VM `n`, or null when it has none (or the file is absent).
|
|
56
|
+
* This is the resolver every hostname derivation funnels through:
|
|
57
|
+
* `slugFor(n) || ('vm-' + n)`. */
|
|
58
|
+
function slugFor(n) {
|
|
59
|
+
const num = normalizeVmNumber(n);
|
|
60
|
+
const map = readSlugs();
|
|
61
|
+
const slug = map && map[String(num)];
|
|
62
|
+
return typeof slug === 'string' && slug.length > 0 ? slug : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The VM number currently mapped to `slug`, or null when no VM owns it. Used to
|
|
66
|
+
* reject a duplicate slug BEFORE it is assigned (a slug must be unique across
|
|
67
|
+
* the fleet, else two VMs would claim the same hostname). `slug` is normalized
|
|
68
|
+
* first so the comparison is case-insensitive. */
|
|
69
|
+
function vmForSlug(slug) {
|
|
70
|
+
const want = normalizeVmSlug(slug);
|
|
71
|
+
const map = readSlugs() || {};
|
|
72
|
+
for (const [k, v] of Object.entries(map)) {
|
|
73
|
+
if (typeof v === 'string' && v.toLowerCase() === want) {
|
|
74
|
+
const num = Number(k);
|
|
75
|
+
if (Number.isInteger(num) && num > 0) return num;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Assign `slug` to VM `n`, validating the slug (via `normalizeVmSlug`) and
|
|
82
|
+
* rejecting a slug already mapped to a DIFFERENT VM. Returns the written map.
|
|
83
|
+
* Re-assigning a VM its own existing slug is an idempotent no-op-shaped write. */
|
|
84
|
+
function setSlug(n, slug) {
|
|
85
|
+
const num = normalizeVmNumber(n);
|
|
86
|
+
const norm = normalizeVmSlug(slug);
|
|
87
|
+
const owner = vmForSlug(norm);
|
|
88
|
+
if (owner != null && owner !== num) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`VM slug "${norm}" is already assigned to VM-${owner} — pick another name`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const map = { ...(readSlugs() || {}) };
|
|
94
|
+
map[String(num)] = norm;
|
|
95
|
+
return writeSlugs(map);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Remove VM `n`'s slug mapping, returning the new map. Removing an absent entry
|
|
99
|
+
* is a no-op. Called on Destroy so a number freed and later re-used doesn't
|
|
100
|
+
* inherit a stale slug. */
|
|
101
|
+
function removeSlug(n) {
|
|
102
|
+
const num = normalizeVmNumber(n);
|
|
103
|
+
const map = { ...(readSlugs() || {}) };
|
|
104
|
+
delete map[String(num)];
|
|
105
|
+
return writeSlugs(map);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
VM_SLUGS_FILE,
|
|
110
|
+
readSlugs,
|
|
111
|
+
writeSlugs,
|
|
112
|
+
slugFor,
|
|
113
|
+
vmForSlug,
|
|
114
|
+
setSlug,
|
|
115
|
+
removeSlug,
|
|
116
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Deterministic Google Cloud HTTPS-LB resource-name derivation for the fleet.
|
|
3
|
+
//
|
|
4
|
+
// The fleet's public browser ingress moves from cloudflared named tunnels to a
|
|
5
|
+
// single Google external HTTPS Load Balancer (one URL map, host-based routing).
|
|
6
|
+
// Reaching a specific port on a specific VM needs a zonal NEG + backend service
|
|
7
|
+
// per (VM, surface); the URL map then routes each `vm-<N>.<surface>.<domain>`
|
|
8
|
+
// host to that backend. This module is the single source of truth for those
|
|
9
|
+
// Google resource NAMES so the bash provisioners (scripts/gcp/google-ingress.sh,
|
|
10
|
+
// scripts/gcp/vm-ingress.sh) and any node caller derive identical strings and a
|
|
11
|
+
// unit test guards them against drift — exactly the contract npm/vm-urls.js and
|
|
12
|
+
// npm/launch-config.js follow.
|
|
13
|
+
//
|
|
14
|
+
// Pure logic only — no I/O, no env reads beyond the explicit `env` argument a
|
|
15
|
+
// caller passes in, so it stays unit-testable in isolation
|
|
16
|
+
// (npm/google-ingress.test.js). The HOSTNAMES it routes to are reused verbatim
|
|
17
|
+
// from npm/vm-urls.js (vmHostnames / dashboardHostname): the Google migration is
|
|
18
|
+
// a serving-layer swap, not a URL-scheme change, so hostnames must never drift
|
|
19
|
+
// from the cert/DNS that vm-urls.js already defines.
|
|
20
|
+
//
|
|
21
|
+
// Stack note: this is fleet-operator infrastructure (the cloud GCE fleet that
|
|
22
|
+
// runs codeyam-editor itself), not a per-app-stack assumption. A project that
|
|
23
|
+
// never joins a Google fleet never sets FLEET_DOMAIN / FLEET_INGRESS=google and
|
|
24
|
+
// never derives a name here; the legacy cloudflared path stays the fallback.
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
normalizeVmNumber,
|
|
28
|
+
vmHostnames,
|
|
29
|
+
dashboardHostname,
|
|
30
|
+
} = require('./vm-urls');
|
|
31
|
+
|
|
32
|
+
// The two per-VM ingress surfaces. `editor` fronts the in-VM editor control API
|
|
33
|
+
// (a fixed port); `preview` fronts the VM's dev server (a per-project port).
|
|
34
|
+
// Keep this list as the single definition both name derivation and the host-rule
|
|
35
|
+
// builder iterate, so a third surface is one edit, not three.
|
|
36
|
+
const SURFACES = Object.freeze(['editor', 'preview']);
|
|
37
|
+
|
|
38
|
+
// The in-VM editor control API port. Fixed across the fleet (matches
|
|
39
|
+
// named-tunnel-config.sh's EDITOR_PORT default and cloud.js's `-L …:14199`), so
|
|
40
|
+
// the editor backend NEG always targets this port regardless of project.
|
|
41
|
+
const EDITOR_PORT = 14199;
|
|
42
|
+
|
|
43
|
+
/** The fixed in-VM editor control-API port the editor backend NEG targets. A
|
|
44
|
+
* function (not a bare constant export) so callers read it the same way they
|
|
45
|
+
* read the per-VM appPort — a uniform "resolve the port for this surface" shape. */
|
|
46
|
+
function editorPort() {
|
|
47
|
+
return EDITOR_PORT;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validate an ingress surface against SURFACES. The NEG / backend-service /
|
|
51
|
+
* host-rule derivations all key off it, so a typo'd surface would mint a
|
|
52
|
+
* resource that nothing routes to — reject it loudly at the boundary, mirroring
|
|
53
|
+
* how vm-urls.js rejects a bad VM number or domain. Returns the surface. */
|
|
54
|
+
function normalizeSurface(surface) {
|
|
55
|
+
if (!SURFACES.includes(surface)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`surface must be one of ${SURFACES.join('|')}, got: ${surface}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return surface;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The zonal NEG name for a (VM, surface): `vm-<N>-<surface>-neg`. One network
|
|
64
|
+
* endpoint group per surface points at the VM's internal IP + that surface's
|
|
65
|
+
* port — the Google-native analog of a cloudflared ingress rule's local origin. */
|
|
66
|
+
function negName(n, surface) {
|
|
67
|
+
const num = normalizeVmNumber(n);
|
|
68
|
+
return `vm-${num}-${normalizeSurface(surface)}-neg`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The backend-service name for a (VM, surface): `vm-<N>-<surface>-bes`. One
|
|
72
|
+
* backend service per host rule wraps the matching NEG (with IAP + a health
|
|
73
|
+
* check); the URL map's host rule points at this name. */
|
|
74
|
+
function backendServiceName(n, surface) {
|
|
75
|
+
const num = normalizeVmNumber(n);
|
|
76
|
+
return `vm-${num}-${normalizeSurface(surface)}-bes`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The URL-map host rules for a VM: one `{ host, surface, backendService }` per
|
|
80
|
+
* surface, where `host` is the EXACT hostname vm-urls.js derives (so the LB
|
|
81
|
+
* routes the same hostnames the cert covers and DNS resolves) and
|
|
82
|
+
* `backendService` is this module's name for that surface. Reusing
|
|
83
|
+
* `vmHostnames` rather than re-deriving keeps the hostname scheme single-sourced.
|
|
84
|
+
* Returns the surfaces in SURFACES order so the provisioner's output is stable.
|
|
85
|
+
*
|
|
86
|
+
* An optional `slug` overrides the routed `host` with the VM's custom hostname
|
|
87
|
+
* (`<slug>.<surface>.<domain>`) while the NEG / `backendService` names stay
|
|
88
|
+
* keyed off the VM NUMBER — the slug changes only what the URL map matches, not
|
|
89
|
+
* the internal GCP resource names. */
|
|
90
|
+
function urlMapHostRules(n, domain, slug) {
|
|
91
|
+
const hosts = vmHostnames(n, domain, slug); // also validates n, domain, slug
|
|
92
|
+
return SURFACES.map((surface) => ({
|
|
93
|
+
host: hosts[surface],
|
|
94
|
+
surface,
|
|
95
|
+
backendService: backendServiceName(n, surface),
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Decide which gcloud URL-map operation reconciles a single `(host,
|
|
100
|
+
* pathMatcher)` against the map's CURRENT state, given `hostRules` — the
|
|
101
|
+
* flattened `[{ host, pathMatcher }]` projection of the URL map's host rules
|
|
102
|
+
* (one entry per host, since one host rule can list several hosts). Returns
|
|
103
|
+
* `{ op }`, one of:
|
|
104
|
+
* - `skip` — `host` already routes to `pathMatcher`; nothing to do.
|
|
105
|
+
* - `add-host-rule` — `pathMatcher` already exists (some other host points
|
|
106
|
+
* at it, e.g. the numeric `vmN.<surface>` host) but
|
|
107
|
+
* `host` is not attached: attach it. Fixes the
|
|
108
|
+
* `Duplicate path matcher` failure on a reused slug.
|
|
109
|
+
* - `repoint` — `host` exists but aims at a DIFFERENT matcher (a stale
|
|
110
|
+
* rule left by a destroyed VM): remove it, then attach
|
|
111
|
+
* `host` to the correct matcher. Fixes a reused slug
|
|
112
|
+
* silently skipped as "already configured" while it
|
|
113
|
+
* still routed to a dead backend.
|
|
114
|
+
* - `add-path-matcher`— neither `host` nor `pathMatcher` is present: create
|
|
115
|
+
* the matcher and attach the host (the original path).
|
|
116
|
+
*
|
|
117
|
+
* Pure and side-effect-free — no gcloud, no I/O — so vm-ingress.sh can read the
|
|
118
|
+
* URL-map state in bash, ask this helper for the op, and run the matching
|
|
119
|
+
* gcloud command, keeping the four-way decision unit-testable (the same
|
|
120
|
+
* bash↔node contract the name derivations above follow). Tolerates a
|
|
121
|
+
* missing/empty `hostRules`. */
|
|
122
|
+
function hostRuleOperation({ hostRules, host, pathMatcher } = {}) {
|
|
123
|
+
if (!host) throw new Error('hostRuleOperation requires a host');
|
|
124
|
+
if (!pathMatcher) throw new Error('hostRuleOperation requires a pathMatcher');
|
|
125
|
+
const rules = Array.isArray(hostRules) ? hostRules : [];
|
|
126
|
+
const existing = rules.find((r) => r && r.host === host);
|
|
127
|
+
if (existing) {
|
|
128
|
+
return { op: existing.pathMatcher === pathMatcher ? 'skip' : 'repoint' };
|
|
129
|
+
}
|
|
130
|
+
const matcherExists = rules.some((r) => r && r.pathMatcher === pathMatcher);
|
|
131
|
+
return { op: matcherExists ? 'add-host-rule' : 'add-path-matcher' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Decide whether a NEG must be attached as a backend to its backend service,
|
|
135
|
+
* given `backends` — the BES's CURRENT `backends[].group` self-links (what
|
|
136
|
+
* `gcloud compute backend-services describe --format='value(backends[].group)'`
|
|
137
|
+
* emits) — and `neg`, the bare NEG name this surface expects. Returns `{ op }`:
|
|
138
|
+
* - `skip` — some current backend already resolves to `neg`; nothing to do.
|
|
139
|
+
* - `attach` — `neg` is not yet a backend (an empty BES, or one wired to a
|
|
140
|
+
* different group): `add-backend` it.
|
|
141
|
+
*
|
|
142
|
+
* `backends[].group` is a full self-link
|
|
143
|
+
* (`.../networkEndpointGroups/vm-3-editor-neg`), so the match compares by
|
|
144
|
+
* basename — the bare NEG name — not the whole URL. Pure and side-effect-free
|
|
145
|
+
* (no gcloud, no I/O), so vm-ingress.sh reads the BES's backends in bash, asks
|
|
146
|
+
* this helper for the op, and `add-backend`s only when it returns `attach`: the
|
|
147
|
+
* same bash↔node contract `hostRuleOperation` follows. This is what makes `up`
|
|
148
|
+
* *converge* a BES that pre-exists with zero backends (the permanent-503 empty-
|
|
149
|
+
* backend bug) instead of skipping the attach as "already configured". Throws on
|
|
150
|
+
* a missing `neg` (mirrors `hostRuleOperation`'s guard); tolerates a
|
|
151
|
+
* missing/empty `backends`. */
|
|
152
|
+
function backendNegAttachOperation({ backends, neg } = {}) {
|
|
153
|
+
if (!neg) throw new Error('backendNegAttachOperation requires a neg');
|
|
154
|
+
const groups = Array.isArray(backends) ? backends : [];
|
|
155
|
+
const basename = (g) => String(g).split('/').pop();
|
|
156
|
+
const attached = groups.some((g) => g && basename(g) === neg);
|
|
157
|
+
return { op: attached ? 'skip' : 'attach' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The dashboard's backend-service name: `dashboard-bes`. The dashboard is a
|
|
161
|
+
* single fleet-wide surface (not per-VM), so its name carries no VM number. The
|
|
162
|
+
* backend itself is only fully wired once the dashboard moves onto GCE
|
|
163
|
+
* (vm-dashboard-on-gce), but the name is defined here so the shared provisioner
|
|
164
|
+
* and that dependent plan agree. */
|
|
165
|
+
function dashboardBackendServiceName() {
|
|
166
|
+
return 'dashboard-bes';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The dashboard's zonal NEG name: `dashboard-neg`. One network endpoint group
|
|
170
|
+
* points at the operator host's internal IP + the dashboard port (8787) — the
|
|
171
|
+
* Google-native analog of the dashboard's old ngrok upstream. Single fleet-wide
|
|
172
|
+
* surface, so (like the backend-service name) it carries no VM number. Wired by
|
|
173
|
+
* scripts/gcp/dashboard-ingress.sh once the dashboard moves onto GCE. */
|
|
174
|
+
function dashboardNegName() {
|
|
175
|
+
return 'dashboard-neg';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The dashboard's dedicated health-check name: `dashboard-hc`. The per-VM
|
|
179
|
+
* surfaces share `fleet-ingress-hc` (port 14199), but the dashboard backend
|
|
180
|
+
* health-checks its own port (8787, path /healthz), so it needs its own check
|
|
181
|
+
* rather than reusing the editor-port one. */
|
|
182
|
+
function dashboardHealthCheckName() {
|
|
183
|
+
return 'dashboard-hc';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The dashboard's single URL-map host rule: `{ host, backendService }`, where
|
|
187
|
+
* `host` is `dashboard.<domain>` from vm-urls.js. Separate from
|
|
188
|
+
* `urlMapHostRules` because the dashboard is fleet-wide, not keyed off a VM
|
|
189
|
+
* number. */
|
|
190
|
+
function dashboardHostRule(domain) {
|
|
191
|
+
return {
|
|
192
|
+
host: dashboardHostname(domain), // validates domain
|
|
193
|
+
backendService: dashboardBackendServiceName(),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
SURFACES,
|
|
199
|
+
EDITOR_PORT,
|
|
200
|
+
editorPort,
|
|
201
|
+
normalizeSurface,
|
|
202
|
+
negName,
|
|
203
|
+
backendServiceName,
|
|
204
|
+
urlMapHostRules,
|
|
205
|
+
hostRuleOperation,
|
|
206
|
+
backendNegAttachOperation,
|
|
207
|
+
dashboardBackendServiceName,
|
|
208
|
+
dashboardNegName,
|
|
209
|
+
dashboardHealthCheckName,
|
|
210
|
+
dashboardHostRule,
|
|
211
|
+
};
|