anbaric-cloud-hosting 1.5.0 → 1.7.0
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/package.json +7 -5
- package/src/hosting/HostingServer.ts +94 -98
- package/src/hosting/Middleware.ts +11 -0
- package/src/hosting/Request.ts +108 -0
- package/src/hosting/RequestHandler.ts +9 -0
- package/src/hosting/Router.ts +22 -407
- package/src/hosting/Server.ts +54 -0
- package/src/hosting/handlers/AppProxyHandler.ts +34 -0
- package/src/hosting/handlers/AppsHandler.ts +57 -0
- package/src/hosting/handlers/AuditsHandler.ts +50 -0
- package/src/hosting/handlers/ConsumersHandler.ts +24 -0
- package/src/hosting/handlers/DocumentsHandler.ts +44 -0
- package/src/hosting/handlers/JobsHandler.ts +60 -0
- package/src/hosting/handlers/PagesHandler.ts +22 -0
- package/src/hosting/handlers/PingHandler.ts +18 -0
- package/src/hosting/handlers/PluginsHandler.ts +52 -0
- package/src/hosting/handlers/QueueHandler.ts +36 -0
- package/src/hosting/handlers/SecretsHandler.ts +42 -0
- package/src/hosting/handlers/StateMachinesHandler.ts +21 -0
- package/src/hosting/handlers/auth/AuthorizeCliHandler.ts +50 -0
- package/src/hosting/handlers/auth/KeysHandler.ts +36 -0
- package/src/hosting/handlers/auth/WhoamiHandler.ts +18 -0
- package/src/hosting/middleware/AuthenticationMiddleware.ts +52 -0
- package/src/hosting/middleware/SessionMiddleware.ts +21 -0
- package/src/hosting/pages/platform-ui.html +4 -4
- package/src/index.ts +25 -0
- package/src/main.ts +4 -1
- package/src/plugins/PageDirectory.ts +35 -0
- package/src/plugins/Plugin.ts +39 -0
- package/src/plugins/PluginBundler.ts +75 -0
- package/src/plugins/PluginLoader.ts +72 -0
package/src/hosting/Router.ts
CHANGED
|
@@ -1,423 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {AuditChange, JobPersistence, JsonStore, SecretStore, deserializeJob, serializeJob} from "anbaric-tsapi";
|
|
4
|
-
import {BuildLayer} from "../app-management/BuildLayer";
|
|
5
|
-
import {AuditRecordStore} from "../auditing/AuditRecordStore";
|
|
6
|
-
import {CliAuthorizer} from "../auth/CliAuthorizer";
|
|
7
|
-
import {Tenant} from "../auth/Tenant";
|
|
8
|
-
import {User} from "../auth/User";
|
|
9
|
-
import {ConfirmableQueue} from "../queuing/ConfirmableQueue";
|
|
10
|
-
import {ConsumerRegistry} from "../queuing/ConsumerRegistry";
|
|
1
|
+
import {Request} from "./Request";
|
|
2
|
+
import {RequestHandler} from "./RequestHandler";
|
|
11
3
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
new Promise((resolve, reject) => {
|
|
16
|
-
const chunks : Array<Buffer> = [];
|
|
17
|
-
request.on("data", chunk => chunks.push(chunk));
|
|
18
|
-
request.on("error", reject);
|
|
19
|
-
request.on("end", () => resolve(Buffer.concat(chunks)));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const readBody = async (request : IncomingMessage) : Promise<any> => {
|
|
23
|
-
const raw = (await readRawBody(request)).toString();
|
|
24
|
-
return raw.length === 0 ? undefined : JSON.parse(raw);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/* Routing is a tree: route() dispatches on the resource family (or the few
|
|
28
|
-
specific resources that fit no family), each family handler switches on
|
|
29
|
-
the specific resource shape, and the method switch sits underneath.
|
|
30
|
-
Unmatched requests fall through to the app proxy, then 404. */
|
|
4
|
+
/* Deliberately hollow: other systems register a handler per top-level
|
|
5
|
+
resource, one for the root, and one fallback for anything unregistered
|
|
6
|
+
(the app proxy). All business logic lives in the handlers. */
|
|
31
7
|
class Router {
|
|
32
8
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
private documentStoreFor? : (collection : string) => JsonStore,
|
|
37
|
-
private secretStore? : SecretStore,
|
|
38
|
-
private cliAuthorizer? : CliAuthorizer,
|
|
39
|
-
private tenant? : string,
|
|
40
|
-
private auditRecords? : AuditRecordStore) {}
|
|
41
|
-
|
|
42
|
-
async route(request : IncomingMessage, response : ServerResponse, user? : User, sessionTenant? : Tenant) : Promise<void> {
|
|
43
|
-
const url = new URL(request.url ?? "/", "http://localhost");
|
|
44
|
-
const [resource, id, subresource] = url.pathname.split("/").filter(Boolean);
|
|
45
|
-
const method = request.method ?? "GET";
|
|
46
|
-
|
|
47
|
-
switch (resource) {
|
|
48
|
-
case undefined:
|
|
49
|
-
if (method === "GET") return this.servePage(response);
|
|
50
|
-
break;
|
|
51
|
-
case "audit":
|
|
52
|
-
if (method === "GET" && !id) return this.servePage(response);
|
|
53
|
-
break;
|
|
54
|
-
case "manage-keys":
|
|
55
|
-
if (method === "GET" && !id && this.cliAuthorizer) return this.servePage(response);
|
|
56
|
-
break;
|
|
57
|
-
case "whoami":
|
|
58
|
-
if (method === "GET" && !id && user) {
|
|
59
|
-
return this.reply(response, 200, { id: user.id, roles: user.roles.map(role => role.id) });
|
|
60
|
-
}
|
|
61
|
-
break;
|
|
62
|
-
case "state-machines":
|
|
63
|
-
if (method === "GET" && !id) return this.reply(response, 200, this.registry.list());
|
|
64
|
-
break;
|
|
65
|
-
case "authorize-cli":
|
|
66
|
-
if (this.cliAuthorizer && id) {
|
|
67
|
-
return this.handleAuthorizeCli(method, id, subresource, request, response, user, sessionTenant);
|
|
68
|
-
}
|
|
69
|
-
break;
|
|
70
|
-
case "keys":
|
|
71
|
-
if (this.cliAuthorizer) return this.handleKeys(method, id, response, user);
|
|
72
|
-
break;
|
|
73
|
-
case "audits":
|
|
74
|
-
if (this.auditRecords && !id) return this.handleAudits(method, url, request, response);
|
|
75
|
-
break;
|
|
76
|
-
case "jobs":
|
|
77
|
-
return this.handleJobs(method, id, subresource, url, request, response);
|
|
78
|
-
case "queue":
|
|
79
|
-
if (id && !subresource) return this.handleQueue(method, id, request, response);
|
|
80
|
-
break;
|
|
81
|
-
case "consumers":
|
|
82
|
-
if (!id) return this.handleConsumers(method, request, response);
|
|
83
|
-
break;
|
|
84
|
-
case "apps":
|
|
85
|
-
if (this.buildLayer) return this.handleApps(method, id, subresource, url, request, response);
|
|
86
|
-
break;
|
|
87
|
-
case "documents":
|
|
88
|
-
if (id && this.documentStoreFor) {
|
|
89
|
-
return this.handleDocuments(method, this.documentStoreFor(id), subresource, url, request, response);
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
92
|
-
case "secrets":
|
|
93
|
-
if (!subresource && this.secretStore) return this.handleSecrets(method, id, request, response);
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (resource && this.buildLayer) {
|
|
98
|
-
const app = this.buildLayer.status(resource);
|
|
99
|
-
if (app && app.status === "running") {
|
|
100
|
-
return this.forwardToApp(app.appHost, app.appPort, resource, method, url, request, response);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
this.notFound(response);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private async handleAuthorizeCli(method : string, requestId : string, subresource : string | undefined,
|
|
108
|
-
request : IncomingMessage, response : ServerResponse, user? : User,
|
|
109
|
-
sessionTenant? : Tenant) : Promise<void> {
|
|
110
|
-
switch (subresource) {
|
|
111
|
-
case "poll":
|
|
112
|
-
switch (method) {
|
|
113
|
-
case "GET": {
|
|
114
|
-
const keyPair = this.cliAuthorizer!.collect(requestId);
|
|
115
|
-
if (!keyPair) return this.reply(response, 202, { status: "pending" });
|
|
116
|
-
return this.reply(response, 200, keyPair);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
break;
|
|
120
|
-
case undefined:
|
|
121
|
-
switch (method) {
|
|
122
|
-
case "GET":
|
|
123
|
-
return this.servePage(response);
|
|
124
|
-
case "POST": {
|
|
125
|
-
const { clientName } = await readBody(request);
|
|
126
|
-
if (typeof clientName !== "string" || clientName.trim().length === 0) {
|
|
127
|
-
return this.reply(response, 400, { error: "Expected a body of { clientName : string }" });
|
|
128
|
-
}
|
|
129
|
-
await this.cliAuthorizer!.approve(requestId, clientName.trim(), user ?? new User("local"),
|
|
130
|
-
sessionTenant?.id ?? this.tenant);
|
|
131
|
-
return this.reply(response, 204);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
this.notFound(response);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private async handleKeys(method : string, id : string | undefined,
|
|
141
|
-
response : ServerResponse, user? : User) : Promise<void> {
|
|
142
|
-
const owner = user ?? new User("local");
|
|
143
|
-
|
|
144
|
-
if (id) {
|
|
145
|
-
switch (method) {
|
|
146
|
-
case "DELETE":
|
|
147
|
-
await this.cliAuthorizer!.revoke(id, owner.id);
|
|
148
|
-
return this.reply(response, 204);
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
switch (method) {
|
|
152
|
-
case "GET": {
|
|
153
|
-
const keys = await this.cliAuthorizer!.keysFor(owner.id);
|
|
154
|
-
return this.reply(response, 200, keys.map(key => ({
|
|
155
|
-
id: key.id,
|
|
156
|
-
clientName: key.clientName,
|
|
157
|
-
createdAt: key.createdAt.toISOString(),
|
|
158
|
-
})));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
this.notFound(response);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private async handleAudits(method : string, url : URL,
|
|
167
|
-
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
168
|
-
switch (method) {
|
|
169
|
-
case "POST": {
|
|
170
|
-
const record = await readBody(request);
|
|
171
|
-
if (typeof record?.jobId !== "string" || typeof record?.description !== "string"
|
|
172
|
-
|| typeof record?.actorId !== "string" || typeof record?.actorType !== "string"
|
|
173
|
-
|| !AUDIT_CHANGES.has(record?.change)) {
|
|
174
|
-
return this.reply(response, 400, { error: "Expected a body of { jobId, actorId, actorType, change, description, ... }" });
|
|
175
|
-
}
|
|
176
|
-
await this.auditRecords!.save(record);
|
|
177
|
-
return this.reply(response, 204);
|
|
178
|
-
}
|
|
179
|
-
case "GET": {
|
|
180
|
-
const change = url.searchParams.get("change");
|
|
181
|
-
const records = await this.auditRecords!.list({
|
|
182
|
-
jobId: url.searchParams.get("jobId") ?? undefined,
|
|
183
|
-
actorId: url.searchParams.get("actorId") ?? undefined,
|
|
184
|
-
change: change && AUDIT_CHANGES.has(change) ? change as AuditChange : undefined,
|
|
185
|
-
search: url.searchParams.get("search") ?? undefined,
|
|
186
|
-
pageSize: url.searchParams.has("pageSize") ? Number(url.searchParams.get("pageSize")) : undefined,
|
|
187
|
-
page: url.searchParams.has("page") ? Number(url.searchParams.get("page")) : undefined,
|
|
188
|
-
});
|
|
189
|
-
return this.reply(response, 200, records);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
this.notFound(response);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private async handleJobs(method : string, id : string | undefined, subresource : string | undefined,
|
|
197
|
-
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
198
|
-
switch (subresource) {
|
|
199
|
-
case "properties":
|
|
200
|
-
if (id) {
|
|
201
|
-
switch (method) {
|
|
202
|
-
case "PATCH": {
|
|
203
|
-
const properties = new Map<string, any>(Object.entries(await readBody(request)));
|
|
204
|
-
await this.persistence.updateProperties(id, properties);
|
|
205
|
-
return this.reply(response, 204);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
break;
|
|
210
|
-
case undefined:
|
|
211
|
-
if (id) {
|
|
212
|
-
switch (method) {
|
|
213
|
-
case "PUT":
|
|
214
|
-
await this.persistence.save(deserializeJob(await readBody(request)));
|
|
215
|
-
return this.reply(response, 204);
|
|
216
|
-
case "GET":
|
|
217
|
-
return this.reply(response, 200, serializeJob(await this.persistence.retrieve(id)));
|
|
218
|
-
case "DELETE":
|
|
219
|
-
await this.persistence.delete(id);
|
|
220
|
-
return this.reply(response, 204);
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
switch (method) {
|
|
224
|
-
case "GET": {
|
|
225
|
-
const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
|
|
226
|
-
const page = Number(url.searchParams.get("page") ?? 0);
|
|
227
|
-
const jobs = await this.persistence.list(pageSize, page);
|
|
228
|
-
return this.reply(response, 200, jobs.map(serializeJob));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.notFound(response);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private async handleQueue(method : string, operation : string,
|
|
239
|
-
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
240
|
-
switch (operation) {
|
|
241
|
-
case "enqueue":
|
|
242
|
-
switch (method) {
|
|
243
|
-
case "POST": {
|
|
244
|
-
const { jobId, workflowId } = await readBody(request);
|
|
245
|
-
await this.queue.enqueue(jobId, workflowId);
|
|
246
|
-
return this.reply(response, 204);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
break;
|
|
250
|
-
case "schedule":
|
|
251
|
-
switch (method) {
|
|
252
|
-
case "POST": {
|
|
253
|
-
const { jobId, workflowId, due } = await readBody(request);
|
|
254
|
-
await this.queue.schedule(jobId, workflowId, new Date(due));
|
|
255
|
-
return this.reply(response, 204);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
break;
|
|
259
|
-
case "dequeue":
|
|
260
|
-
switch (method) {
|
|
261
|
-
case "POST":
|
|
262
|
-
return this.reply(response, 200, { messages: await this.queue.dequeueSome() });
|
|
263
|
-
}
|
|
264
|
-
break;
|
|
265
|
-
case "confirm":
|
|
266
|
-
switch (method) {
|
|
267
|
-
case "POST": {
|
|
268
|
-
const { jobId, workflowId } = await readBody(request);
|
|
269
|
-
await this.queue.confirm({ jobId, workflowId });
|
|
270
|
-
return this.reply(response, 204);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
this.notFound(response);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private async handleConsumers(method : string, request : IncomingMessage,
|
|
280
|
-
response : ServerResponse) : Promise<void> {
|
|
281
|
-
switch (method) {
|
|
282
|
-
case "POST": {
|
|
283
|
-
const { workflowId, url: consumerUrl } = await readBody(request);
|
|
284
|
-
this.registry.register(workflowId, consumerUrl);
|
|
285
|
-
return this.reply(response, 204);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
this.notFound(response);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private async handleApps(method : string, appName : string | undefined, subresource : string | undefined,
|
|
293
|
-
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
294
|
-
switch (subresource) {
|
|
295
|
-
case "deploy":
|
|
296
|
-
if (appName) {
|
|
297
|
-
switch (method) {
|
|
298
|
-
case "POST": {
|
|
299
|
-
const appPort = Number(url.searchParams.get("port"));
|
|
300
|
-
if (!Number.isInteger(appPort) || appPort <= 0) {
|
|
301
|
-
return this.reply(response, 400, { error: "Expected a numeric port query parameter" });
|
|
302
|
-
}
|
|
303
|
-
const tarball = await readRawBody(request);
|
|
304
|
-
if (tarball.length === 0) return this.reply(response, 400, { error: "Expected a gzipped tarball body" });
|
|
305
|
-
return this.reply(response, 202, this.buildLayer!.deploy(appName, appPort, tarball));
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
break;
|
|
310
|
-
case undefined:
|
|
311
|
-
if (appName) {
|
|
312
|
-
switch (method) {
|
|
313
|
-
case "GET": {
|
|
314
|
-
const status = this.buildLayer!.status(appName);
|
|
315
|
-
if (!status) return this.reply(response, 404, { error: `No app named "${appName}"` });
|
|
316
|
-
return this.reply(response, 200, status);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
switch (method) {
|
|
321
|
-
case "GET":
|
|
322
|
-
return this.reply(response, 200, this.buildLayer!.list());
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
9
|
+
private handlers = new Map<string, RequestHandler>();
|
|
10
|
+
private rootHandler? : RequestHandler;
|
|
11
|
+
private fallbackHandler? : RequestHandler;
|
|
327
12
|
|
|
328
|
-
|
|
13
|
+
register(resource : string, handler : RequestHandler) : void {
|
|
14
|
+
this.handlers.set(resource, handler);
|
|
329
15
|
}
|
|
330
16
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (documentId) {
|
|
334
|
-
switch (method) {
|
|
335
|
-
case "PUT":
|
|
336
|
-
await store.save(documentId, await readBody(request));
|
|
337
|
-
return this.reply(response, 204);
|
|
338
|
-
case "GET":
|
|
339
|
-
return this.reply(response, 200, await store.retrieve(documentId));
|
|
340
|
-
case "DELETE":
|
|
341
|
-
await store.delete(documentId);
|
|
342
|
-
return this.reply(response, 204);
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
switch (method) {
|
|
346
|
-
case "GET": {
|
|
347
|
-
const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
|
|
348
|
-
const page = Number(url.searchParams.get("page") ?? 0);
|
|
349
|
-
return this.reply(response, 200, await store.list(pageSize, page));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
this.notFound(response);
|
|
17
|
+
registerRoot(handler : RequestHandler) : void {
|
|
18
|
+
this.rootHandler = handler;
|
|
355
19
|
}
|
|
356
20
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (name) {
|
|
360
|
-
switch (method) {
|
|
361
|
-
case "PUT": {
|
|
362
|
-
const { value } = await readBody(request);
|
|
363
|
-
if (typeof value !== "string") return this.reply(response, 400, { error: "Expected a body of { value : string }" });
|
|
364
|
-
await this.secretStore!.save(name, value);
|
|
365
|
-
return this.reply(response, 204);
|
|
366
|
-
}
|
|
367
|
-
case "GET":
|
|
368
|
-
return this.reply(response, 200, { value: await this.secretStore!.retrieve(name) });
|
|
369
|
-
case "DELETE":
|
|
370
|
-
await this.secretStore!.delete(name);
|
|
371
|
-
return this.reply(response, 204);
|
|
372
|
-
}
|
|
373
|
-
} else {
|
|
374
|
-
switch (method) {
|
|
375
|
-
case "GET":
|
|
376
|
-
return this.reply(response, 200, await this.secretStore!.list());
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
this.notFound(response);
|
|
21
|
+
registerFallback(handler : RequestHandler) : void {
|
|
22
|
+
this.fallbackHandler = handler;
|
|
381
23
|
}
|
|
382
24
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
response.end(page);
|
|
388
|
-
} catch {
|
|
389
|
-
this.reply(response, 501, { error: "The platform UI has not been built - run npm run build in anbaric-cloud-hosting/ui" });
|
|
25
|
+
async route(request : Request) : Promise<void> {
|
|
26
|
+
if (request.resource === undefined) {
|
|
27
|
+
if (this.rootHandler) return this.rootHandler.handle(request);
|
|
28
|
+
return request.notFound();
|
|
390
29
|
}
|
|
391
|
-
}
|
|
392
30
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const body = method === "GET" || method === "HEAD" ? undefined : await readRawBody(request);
|
|
31
|
+
const handler = this.handlers.get(request.resource);
|
|
32
|
+
if (handler) return handler.handle(request);
|
|
33
|
+
if (this.fallbackHandler) return this.fallbackHandler.handle(request);
|
|
397
34
|
|
|
398
|
-
|
|
399
|
-
method,
|
|
400
|
-
headers: { "content-type": String(request.headers["content-type"] ?? "application/json") },
|
|
401
|
-
body: body && body.length > 0 ? new Uint8Array(body) : undefined,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
const payload = Buffer.from(await upstream.arrayBuffer());
|
|
405
|
-
response.writeHead(upstream.status, { "content-type": upstream.headers.get("content-type") ?? "application/octet-stream" });
|
|
406
|
-
response.end(payload);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private notFound(response : ServerResponse) : void {
|
|
410
|
-
this.reply(response, 404, { error: "Not found" });
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
private reply(response : ServerResponse, status : number, body? : unknown) : void {
|
|
414
|
-
if (body === undefined) {
|
|
415
|
-
response.statusCode = status;
|
|
416
|
-
response.end();
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
response.writeHead(status, { "content-type": "application/json" });
|
|
420
|
-
response.end(JSON.stringify(body));
|
|
35
|
+
request.notFound();
|
|
421
36
|
}
|
|
422
37
|
|
|
423
38
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {createServer, IncomingMessage, Server as HttpServer, ServerResponse} from "node:http";
|
|
2
|
+
import {AddressInfo} from "node:net";
|
|
3
|
+
import {Middleware} from "./Middleware";
|
|
4
|
+
import {Request} from "./Request";
|
|
5
|
+
import {Router} from "./Router";
|
|
6
|
+
|
|
7
|
+
/* One listener: builds the Request, stamps the ray trace id, runs the
|
|
8
|
+
middleware chain to decorate (or answer) it, then hands it to the
|
|
9
|
+
router. */
|
|
10
|
+
class Server {
|
|
11
|
+
|
|
12
|
+
private server : HttpServer;
|
|
13
|
+
|
|
14
|
+
constructor(private router : Router, private middlewares : Array<Middleware> = []) {
|
|
15
|
+
this.server = createServer((incoming, response) => {
|
|
16
|
+
this.handle(incoming, response).catch(error => {
|
|
17
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
18
|
+
const status = /^No .+ found/.test(message) ? 404 : 500;
|
|
19
|
+
if (!response.writableEnded) {
|
|
20
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
21
|
+
response.end(JSON.stringify({ error: message }));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get listening() : boolean {
|
|
28
|
+
return this.server.listening;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
listen(port : number) : Promise<number> {
|
|
32
|
+
return new Promise(resolve =>
|
|
33
|
+
this.server.listen(port, () => resolve((this.server.address() as AddressInfo).port)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
close() : Promise<void> {
|
|
37
|
+
return new Promise((resolve, reject) =>
|
|
38
|
+
this.server.close(error => error ? reject(error) : resolve()));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async handle(incoming : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
42
|
+
const request = new Request(incoming, response);
|
|
43
|
+
response.setHeader("x-anbaric-ray", request.rayId);
|
|
44
|
+
|
|
45
|
+
for (const middleware of this.middlewares) {
|
|
46
|
+
if (!await middleware.apply(request)) return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await this.router.route(request);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { Server }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {BuildLayer} from "../../app-management/BuildLayer";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
/* The router's fallback: any unregistered top-level path naming a running
|
|
6
|
+
app is forwarded to it. */
|
|
7
|
+
class AppProxyHandler implements RequestHandler {
|
|
8
|
+
|
|
9
|
+
constructor(private buildLayer : BuildLayer) {}
|
|
10
|
+
|
|
11
|
+
async handle(request : Request) : Promise<void> {
|
|
12
|
+
const appName = request.resource!;
|
|
13
|
+
const app = this.buildLayer.status(appName);
|
|
14
|
+
if (!app || app.status !== "running") return request.notFound();
|
|
15
|
+
|
|
16
|
+
const appPath = request.url.pathname.slice(`/${appName}`.length) || "/";
|
|
17
|
+
const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.rawBody();
|
|
18
|
+
|
|
19
|
+
const upstream = await fetch(`http://${app.appHost}:${app.appPort}${appPath}${request.url.search}`, {
|
|
20
|
+
method: request.method,
|
|
21
|
+
headers: { "content-type": request.header("content-type") ?? "application/json" },
|
|
22
|
+
body: body && body.length > 0 ? new Uint8Array(body) : undefined,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const payload = Buffer.from(await upstream.arrayBuffer());
|
|
26
|
+
request.rawResponse.writeHead(upstream.status, {
|
|
27
|
+
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
|
|
28
|
+
});
|
|
29
|
+
request.rawResponse.end(payload);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { AppProxyHandler }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {BuildLayer} from "../../app-management/BuildLayer";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
class AppsHandler implements RequestHandler {
|
|
6
|
+
|
|
7
|
+
constructor(private buildLayer : BuildLayer) {}
|
|
8
|
+
|
|
9
|
+
async handle(request : Request) : Promise<void> {
|
|
10
|
+
switch (request.subresource) {
|
|
11
|
+
case "deploy":
|
|
12
|
+
if (request.id) return this.handleDeploy(request, request.id);
|
|
13
|
+
break;
|
|
14
|
+
case undefined:
|
|
15
|
+
if (request.id) return this.handleApp(request, request.id);
|
|
16
|
+
return this.handleCollection(request);
|
|
17
|
+
}
|
|
18
|
+
request.notFound();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async handleDeploy(request : Request, appName : string) : Promise<void> {
|
|
22
|
+
switch (request.method) {
|
|
23
|
+
case "POST": {
|
|
24
|
+
const appPort = Number(request.query("port"));
|
|
25
|
+
if (!Number.isInteger(appPort) || appPort <= 0) {
|
|
26
|
+
return request.reply(400, { error: "Expected a numeric port query parameter" });
|
|
27
|
+
}
|
|
28
|
+
const tarball = await request.rawBody();
|
|
29
|
+
if (tarball.length === 0) return request.reply(400, { error: "Expected a gzipped tarball body" });
|
|
30
|
+
return request.reply(202, this.buildLayer.deploy(appName, appPort, tarball));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
request.notFound();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async handleApp(request : Request, appName : string) : Promise<void> {
|
|
37
|
+
switch (request.method) {
|
|
38
|
+
case "GET": {
|
|
39
|
+
const status = this.buildLayer.status(appName);
|
|
40
|
+
if (!status) return request.reply(404, { error: `No app named "${appName}"` });
|
|
41
|
+
return request.reply(200, status);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
request.notFound();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async handleCollection(request : Request) : Promise<void> {
|
|
48
|
+
switch (request.method) {
|
|
49
|
+
case "GET":
|
|
50
|
+
return request.reply(200, this.buildLayer.list());
|
|
51
|
+
}
|
|
52
|
+
request.notFound();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { AppsHandler }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {AuditChange} from "anbaric-tsapi";
|
|
2
|
+
import {AuditRecordStore} from "../../auditing/AuditRecordStore";
|
|
3
|
+
import {Request} from "../Request";
|
|
4
|
+
import {RequestHandler} from "../RequestHandler";
|
|
5
|
+
|
|
6
|
+
const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
|
|
7
|
+
|
|
8
|
+
class AuditsHandler implements RequestHandler {
|
|
9
|
+
|
|
10
|
+
constructor(private auditRecords : AuditRecordStore) {}
|
|
11
|
+
|
|
12
|
+
async handle(request : Request) : Promise<void> {
|
|
13
|
+
if (request.id) return request.notFound();
|
|
14
|
+
|
|
15
|
+
switch (request.method) {
|
|
16
|
+
case "POST":
|
|
17
|
+
return this.handleRecord(request);
|
|
18
|
+
case "GET":
|
|
19
|
+
return this.handleQuery(request);
|
|
20
|
+
}
|
|
21
|
+
request.notFound();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async handleRecord(request : Request) : Promise<void> {
|
|
25
|
+
const record = await request.body();
|
|
26
|
+
if (typeof record?.jobId !== "string" || typeof record?.description !== "string"
|
|
27
|
+
|| typeof record?.actorId !== "string" || typeof record?.actorType !== "string"
|
|
28
|
+
|| !AUDIT_CHANGES.has(record?.change)) {
|
|
29
|
+
return request.reply(400, { error: "Expected a body of { jobId, actorId, actorType, change, description, ... }" });
|
|
30
|
+
}
|
|
31
|
+
await this.auditRecords.save(record);
|
|
32
|
+
request.reply(204);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async handleQuery(request : Request) : Promise<void> {
|
|
36
|
+
const change = request.query("change");
|
|
37
|
+
const records = await this.auditRecords.list({
|
|
38
|
+
jobId: request.query("jobId"),
|
|
39
|
+
actorId: request.query("actorId"),
|
|
40
|
+
change: change && AUDIT_CHANGES.has(change) ? change as AuditChange : undefined,
|
|
41
|
+
search: request.query("search"),
|
|
42
|
+
pageSize: request.query("pageSize") === undefined ? undefined : Number(request.query("pageSize")),
|
|
43
|
+
page: request.query("page") === undefined ? undefined : Number(request.query("page")),
|
|
44
|
+
});
|
|
45
|
+
request.reply(200, records);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { AuditsHandler }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {ConsumerRegistry} from "../../queuing/ConsumerRegistry";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
class ConsumersHandler implements RequestHandler {
|
|
6
|
+
|
|
7
|
+
constructor(private registry : ConsumerRegistry) {}
|
|
8
|
+
|
|
9
|
+
async handle(request : Request) : Promise<void> {
|
|
10
|
+
if (request.id) return request.notFound();
|
|
11
|
+
|
|
12
|
+
switch (request.method) {
|
|
13
|
+
case "POST": {
|
|
14
|
+
const { workflowId, url } = await request.body();
|
|
15
|
+
this.registry.register(workflowId, url);
|
|
16
|
+
return request.reply(204);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
request.notFound();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { ConsumersHandler }
|