@openparachute/hub 0.6.2 → 0.6.3-rc.2
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 +87 -35
- package/package.json +1 -1
- package/src/__tests__/api-hub-upgrade.test.ts +690 -0
- package/src/__tests__/api-modules-ops.test.ts +359 -3
- package/src/__tests__/api-modules.test.ts +54 -0
- package/src/__tests__/expose-cloudflare.test.ts +163 -72
- package/src/__tests__/expose-off-auto.test.ts +26 -1
- package/src/__tests__/expose.test.ts +260 -240
- package/src/__tests__/hub-control.test.ts +1 -242
- package/src/__tests__/hub-server.test.ts +64 -0
- package/src/__tests__/hub-unit.test.ts +574 -0
- package/src/__tests__/init.test.ts +219 -2
- package/src/__tests__/lifecycle.test.ts +416 -1448
- package/src/__tests__/managed-unit.test.ts +575 -0
- package/src/__tests__/migrate-cutover.test.ts +840 -0
- package/src/__tests__/migrate-offer.test.ts +240 -0
- package/src/__tests__/migrate.test.ts +132 -0
- package/src/__tests__/module-ops-client.test.ts +556 -0
- package/src/__tests__/port-probe.test.ts +23 -0
- package/src/__tests__/setup-wizard.test.ts +130 -0
- package/src/__tests__/status-supervisor.test.ts +504 -0
- package/src/__tests__/status.test.ts +157 -708
- package/src/__tests__/supervisor.test.ts +471 -6
- package/src/__tests__/upgrade.test.ts +351 -5
- package/src/api-hub-upgrade.ts +384 -0
- package/src/api-hub.ts +2 -1
- package/src/api-modules-ops.ts +221 -0
- package/src/api-modules.ts +18 -2
- package/src/cli.ts +97 -12
- package/src/cloudflare/connector-service.ts +117 -322
- package/src/commands/expose-cloudflare.ts +63 -71
- package/src/commands/expose-supervisor.ts +247 -0
- package/src/commands/expose.ts +59 -48
- package/src/commands/init.ts +225 -12
- package/src/commands/lifecycle.ts +455 -816
- package/src/commands/migrate-cutover.ts +837 -0
- package/src/commands/migrate.ts +71 -2
- package/src/commands/serve-boot.ts +71 -25
- package/src/commands/status.ts +535 -235
- package/src/commands/upgrade.ts +100 -2
- package/src/help.ts +128 -68
- package/src/hub-control.ts +23 -162
- package/src/hub-server.ts +39 -0
- package/src/hub-unit.ts +735 -0
- package/src/hub-upgrade-helper.ts +306 -0
- package/src/hub-upgrade-mode.ts +209 -0
- package/src/hub-upgrade-status.ts +150 -0
- package/src/managed-unit.ts +692 -0
- package/src/migrate-offer.ts +186 -0
- package/src/module-ops-client.ts +457 -0
- package/src/port-probe.ts +50 -0
- package/src/process-state.ts +19 -3
- package/src/setup-wizard.ts +80 -1
- package/src/supervisor.ts +389 -38
- package/web/ui/dist/assets/index-D_6AFvZy.js +61 -0
- package/web/ui/dist/assets/{index-BiBlvEaj.css → index-mz8XcVPP.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-CIN3mnmf.js +0 -61
|
@@ -103,8 +103,11 @@ describe("init", () => {
|
|
|
103
103
|
expect(code).toBe(0);
|
|
104
104
|
expect(calls).toEqual(["ensureHub"]);
|
|
105
105
|
const joined = logs.join("\n");
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
// A genuinely-started unit reports the port only — no `pid 0` sentinel,
|
|
107
|
+
// no misleading "starting it now" preamble.
|
|
108
|
+
expect(joined).toContain("Hub unit started (port 1939)");
|
|
109
|
+
expect(joined).not.toContain("pid 0");
|
|
110
|
+
expect(joined).not.toContain("starting it now");
|
|
108
111
|
expect(joined).toContain("http://127.0.0.1:1939/admin/");
|
|
109
112
|
expect(joined).toContain("finish setup in the admin wizard");
|
|
110
113
|
} finally {
|
|
@@ -112,6 +115,46 @@ describe("init", () => {
|
|
|
112
115
|
}
|
|
113
116
|
});
|
|
114
117
|
|
|
118
|
+
test("unit-managed re-run against a live hub logs 'already running', not 'pid 0'", async () => {
|
|
119
|
+
// A unit-managed hub writes no pidfile, so `processState(HUB_SVC)` reports
|
|
120
|
+
// not-running on every re-run — but `ensureHub` probes /health and returns
|
|
121
|
+
// started:false when the hub is already up. The log must be honest: no
|
|
122
|
+
// "starting it now", no "Hub unit started", no `pid 0` sentinel.
|
|
123
|
+
const h = makeHarness();
|
|
124
|
+
try {
|
|
125
|
+
writeHubPort(1939, h.configDir);
|
|
126
|
+
const calls: string[] = [];
|
|
127
|
+
const logs: string[] = [];
|
|
128
|
+
const code = await init({
|
|
129
|
+
configDir: h.configDir,
|
|
130
|
+
manifestPath: h.manifestPath,
|
|
131
|
+
log: (l) => logs.push(l),
|
|
132
|
+
// No pidfile (unit-managed) → processState reports not-running.
|
|
133
|
+
alive: () => false,
|
|
134
|
+
ensureHub: async () => {
|
|
135
|
+
calls.push("ensureHub");
|
|
136
|
+
// Hub already answered /health — ensureHubUnit's already-up arm.
|
|
137
|
+
return { pid: 0, port: 1939, started: false };
|
|
138
|
+
},
|
|
139
|
+
readExposeStateFn: () => undefined,
|
|
140
|
+
isTty: false,
|
|
141
|
+
platform: "darwin",
|
|
142
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
143
|
+
});
|
|
144
|
+
expect(code).toBe(0);
|
|
145
|
+
// ensureHub IS called (processState can't see a unit-managed hub) but
|
|
146
|
+
// reports started:false.
|
|
147
|
+
expect(calls).toEqual(["ensureHub"]);
|
|
148
|
+
const joined = logs.join("\n");
|
|
149
|
+
expect(joined).toContain("Hub already running (port 1939)");
|
|
150
|
+
expect(joined).not.toContain("pid 0");
|
|
151
|
+
expect(joined).not.toContain("starting it now");
|
|
152
|
+
expect(joined).not.toContain("Hub unit started");
|
|
153
|
+
} finally {
|
|
154
|
+
h.cleanup();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
115
158
|
test("idempotent: skips ensureHub and confirms 'looks good' when hub up + vault configured", async () => {
|
|
116
159
|
const h = makeHarness();
|
|
117
160
|
try {
|
|
@@ -879,6 +922,180 @@ describe("init exposure chain", () => {
|
|
|
879
922
|
}
|
|
880
923
|
});
|
|
881
924
|
|
|
925
|
+
// -------------------------------------------------------------------------
|
|
926
|
+
// Phase 3a cutover (design §3.3 init row, §3.1, §4.1/§4.2): init installs +
|
|
927
|
+
// starts the hub UNIT (not a detached spawn) and guarantees an operator
|
|
928
|
+
// token. The `ensureHub` + `guaranteeOperatorToken` seams stay injectable;
|
|
929
|
+
// these tests drive them as stubs (and exercise the REAL operator-token
|
|
930
|
+
// guarantee against a seeded hub DB).
|
|
931
|
+
// -------------------------------------------------------------------------
|
|
932
|
+
|
|
933
|
+
test("calls guaranteeOperatorToken after the hub is up, then falls through to the wizard", async () => {
|
|
934
|
+
const h = makeHarness();
|
|
935
|
+
try {
|
|
936
|
+
const order: string[] = [];
|
|
937
|
+
const code = await init({
|
|
938
|
+
configDir: h.configDir,
|
|
939
|
+
manifestPath: h.manifestPath,
|
|
940
|
+
log: () => {},
|
|
941
|
+
alive: () => false,
|
|
942
|
+
ensureHub: async () => {
|
|
943
|
+
order.push("ensureHub");
|
|
944
|
+
writeHubPort(1939, h.configDir);
|
|
945
|
+
return { pid: 0, port: 1939, started: true };
|
|
946
|
+
},
|
|
947
|
+
guaranteeOperatorToken: async (ctx) => {
|
|
948
|
+
order.push("guaranteeOperatorToken");
|
|
949
|
+
// The hub is up before the token guarantee runs (§3.2 step 4 — read
|
|
950
|
+
// the token AFTER readiness so we don't race the start-hub iss
|
|
951
|
+
// self-heal).
|
|
952
|
+
expect(ctx.hubPort).toBe(1939);
|
|
953
|
+
return "present";
|
|
954
|
+
},
|
|
955
|
+
readExposeStateFn: () => undefined,
|
|
956
|
+
isTty: false,
|
|
957
|
+
platform: "linux",
|
|
958
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
959
|
+
});
|
|
960
|
+
expect(code).toBe(0);
|
|
961
|
+
// hub-up first, then token guarantee — in that order.
|
|
962
|
+
expect(order).toEqual(["ensureHub", "guaranteeOperatorToken"]);
|
|
963
|
+
} finally {
|
|
964
|
+
h.cleanup();
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test("real guarantee: MINTS the operator token when absent + a hub user exists", async () => {
|
|
969
|
+
const h = makeHarness();
|
|
970
|
+
try {
|
|
971
|
+
const { openHubDb, hubDbPath } = await import("../hub-db.ts");
|
|
972
|
+
const { createUser } = await import("../users.ts");
|
|
973
|
+
const { readOperatorTokenFile } = await import("../operator-token.ts");
|
|
974
|
+
// Seed a first-admin so the guarantee has someone to mint for.
|
|
975
|
+
const db = openHubDb(hubDbPath(h.configDir));
|
|
976
|
+
await createUser(db, "owner", "owner-password-123");
|
|
977
|
+
db.close();
|
|
978
|
+
|
|
979
|
+
// No operator.token yet.
|
|
980
|
+
expect(await readOperatorTokenFile(h.configDir)).toBeNull();
|
|
981
|
+
|
|
982
|
+
const code = await init({
|
|
983
|
+
configDir: h.configDir,
|
|
984
|
+
manifestPath: h.manifestPath,
|
|
985
|
+
log: () => {},
|
|
986
|
+
alive: () => false,
|
|
987
|
+
ensureHub: async () => {
|
|
988
|
+
writeHubPort(1939, h.configDir);
|
|
989
|
+
return { pid: 0, port: 1939, started: true };
|
|
990
|
+
},
|
|
991
|
+
// No guaranteeOperatorToken seam → exercises the REAL default.
|
|
992
|
+
readExposeStateFn: () => undefined,
|
|
993
|
+
isTty: false,
|
|
994
|
+
platform: "linux",
|
|
995
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
996
|
+
});
|
|
997
|
+
expect(code).toBe(0);
|
|
998
|
+
// The default minted + wrote the token.
|
|
999
|
+
expect(await readOperatorTokenFile(h.configDir)).not.toBeNull();
|
|
1000
|
+
} finally {
|
|
1001
|
+
h.cleanup();
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
test("real guarantee: does NOT double-mint when a token already exists", async () => {
|
|
1006
|
+
const h = makeHarness();
|
|
1007
|
+
try {
|
|
1008
|
+
const { openHubDb, hubDbPath } = await import("../hub-db.ts");
|
|
1009
|
+
const { createUser } = await import("../users.ts");
|
|
1010
|
+
const { writeOperatorTokenFile, readOperatorTokenFile } = await import(
|
|
1011
|
+
"../operator-token.ts"
|
|
1012
|
+
);
|
|
1013
|
+
const db = openHubDb(hubDbPath(h.configDir));
|
|
1014
|
+
await createUser(db, "owner", "owner-password-123");
|
|
1015
|
+
db.close();
|
|
1016
|
+
// Plant a sentinel token on disk.
|
|
1017
|
+
await writeOperatorTokenFile("SENTINEL.PRE-EXISTING.TOKEN", h.configDir);
|
|
1018
|
+
|
|
1019
|
+
const code = await init({
|
|
1020
|
+
configDir: h.configDir,
|
|
1021
|
+
manifestPath: h.manifestPath,
|
|
1022
|
+
log: () => {},
|
|
1023
|
+
alive: () => false,
|
|
1024
|
+
ensureHub: async () => {
|
|
1025
|
+
writeHubPort(1939, h.configDir);
|
|
1026
|
+
return { pid: 0, port: 1939, started: true };
|
|
1027
|
+
},
|
|
1028
|
+
readExposeStateFn: () => undefined,
|
|
1029
|
+
isTty: false,
|
|
1030
|
+
platform: "linux",
|
|
1031
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
1032
|
+
});
|
|
1033
|
+
expect(code).toBe(0);
|
|
1034
|
+
// Untouched — the guarantee left the pre-existing token in place.
|
|
1035
|
+
expect(await readOperatorTokenFile(h.configDir)).toBe("SENTINEL.PRE-EXISTING.TOKEN");
|
|
1036
|
+
} finally {
|
|
1037
|
+
h.cleanup();
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test("real guarantee: no hub user yet → no token minted, init still succeeds (no-user, not an error)", async () => {
|
|
1042
|
+
const h = makeHarness();
|
|
1043
|
+
try {
|
|
1044
|
+
const { readOperatorTokenFile } = await import("../operator-token.ts");
|
|
1045
|
+
// No user seeded — the common fresh-box case where init runs before the
|
|
1046
|
+
// wizard creates first-admin.
|
|
1047
|
+
const code = await init({
|
|
1048
|
+
configDir: h.configDir,
|
|
1049
|
+
manifestPath: h.manifestPath,
|
|
1050
|
+
log: () => {},
|
|
1051
|
+
alive: () => false,
|
|
1052
|
+
ensureHub: async () => {
|
|
1053
|
+
writeHubPort(1939, h.configDir);
|
|
1054
|
+
return { pid: 0, port: 1939, started: true };
|
|
1055
|
+
},
|
|
1056
|
+
readExposeStateFn: () => undefined,
|
|
1057
|
+
isTty: false,
|
|
1058
|
+
platform: "linux",
|
|
1059
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
1060
|
+
});
|
|
1061
|
+
expect(code).toBe(0);
|
|
1062
|
+
// No token (the wizard mints it when the admin is created).
|
|
1063
|
+
expect(await readOperatorTokenFile(h.configDir)).toBeNull();
|
|
1064
|
+
} finally {
|
|
1065
|
+
h.cleanup();
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
test("hub-unit bringup failure (e.g. no service manager) → init exits 1 with the actionable message", async () => {
|
|
1070
|
+
const h = makeHarness();
|
|
1071
|
+
try {
|
|
1072
|
+
const logs: string[] = [];
|
|
1073
|
+
const code = await init({
|
|
1074
|
+
configDir: h.configDir,
|
|
1075
|
+
manifestPath: h.manifestPath,
|
|
1076
|
+
log: (l) => logs.push(l),
|
|
1077
|
+
alive: () => false,
|
|
1078
|
+
// Mirror what the production default throws when there's no manager.
|
|
1079
|
+
ensureHub: async () => {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
"no service manager (systemd/launchd) found — run `parachute serve` in the foreground, or use a platform that provides one",
|
|
1082
|
+
);
|
|
1083
|
+
},
|
|
1084
|
+
guaranteeOperatorToken: async () => "no-user",
|
|
1085
|
+
readExposeStateFn: () => undefined,
|
|
1086
|
+
isTty: false,
|
|
1087
|
+
platform: "linux",
|
|
1088
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
1089
|
+
});
|
|
1090
|
+
expect(code).toBe(1);
|
|
1091
|
+
const joined = logs.join("\n");
|
|
1092
|
+
expect(joined).toContain("no service manager (systemd/launchd) found");
|
|
1093
|
+
expect(joined).toContain("parachute logs hub");
|
|
1094
|
+
} finally {
|
|
1095
|
+
h.cleanup();
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
882
1099
|
test("after exposure runs, the admin URL re-reads expose state for the FQDN", async () => {
|
|
883
1100
|
const h = makeHarness();
|
|
884
1101
|
try {
|