anbaric-cloud-hosting 1.4.0 → 1.5.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 +5 -5
- package/src/hosting/HostingServer.ts +1 -0
- package/src/hosting/Router.ts +301 -213
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "chris@anbaric.ai",
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"@aws-sdk/client-s3": "^3.1110.0",
|
|
18
18
|
"@aws-sdk/client-secrets-manager": "^3.700.0",
|
|
19
19
|
"@aws-sdk/client-servicediscovery": "^3.1110.0",
|
|
20
|
-
"anbaric-data-store": "^1.
|
|
21
|
-
"anbaric-tsapi": "^1.
|
|
20
|
+
"anbaric-data-store": "^1.5.0",
|
|
21
|
+
"anbaric-tsapi": "^1.5.0",
|
|
22
22
|
"pg": "^8.16.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^26.2.0",
|
|
26
26
|
"@types/pg": "^8.15.0",
|
|
27
|
-
"anbaric-cloud": "^1.
|
|
28
|
-
"anbaric-state-machine": "^1.
|
|
27
|
+
"anbaric-impl-cloud": "^1.5.0",
|
|
28
|
+
"anbaric-state-machine": "^1.5.0",
|
|
29
29
|
"tsx": "^4.20.0",
|
|
30
30
|
"typescript": "^7.0.2"
|
|
31
31
|
},
|
|
@@ -30,6 +30,7 @@ class HostingServer {
|
|
|
30
30
|
private tokenAuthenticator? : TokenAuthenticator,
|
|
31
31
|
private tenant? : string,
|
|
32
32
|
auditRecords? : AuditRecordStore) {
|
|
33
|
+
|
|
33
34
|
this.router = new Router(persistence, queue, registry, buildLayer, documentStoreFor, secretStore, cliAuthorizer, tenant, auditRecords);
|
|
34
35
|
this.server = this.serverFor((request, response) => this.handle(request, response));
|
|
35
36
|
this.internalServer = this.serverFor((request, response) => this.handleInternal(request, response));
|
package/src/hosting/Router.ts
CHANGED
|
@@ -3,14 +3,14 @@ import {IncomingMessage, ServerResponse} from "node:http";
|
|
|
3
3
|
import {AuditChange, JobPersistence, JsonStore, SecretStore, deserializeJob, serializeJob} from "anbaric-tsapi";
|
|
4
4
|
import {BuildLayer} from "../app-management/BuildLayer";
|
|
5
5
|
import {AuditRecordStore} from "../auditing/AuditRecordStore";
|
|
6
|
-
|
|
7
|
-
const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
|
|
8
6
|
import {CliAuthorizer} from "../auth/CliAuthorizer";
|
|
9
7
|
import {Tenant} from "../auth/Tenant";
|
|
10
8
|
import {User} from "../auth/User";
|
|
11
9
|
import {ConfirmableQueue} from "../queuing/ConfirmableQueue";
|
|
12
10
|
import {ConsumerRegistry} from "../queuing/ConsumerRegistry";
|
|
13
11
|
|
|
12
|
+
const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
|
|
13
|
+
|
|
14
14
|
const readRawBody = (request : IncomingMessage) : Promise<Buffer> =>
|
|
15
15
|
new Promise((resolve, reject) => {
|
|
16
16
|
const chunks : Array<Buffer> = [];
|
|
@@ -24,6 +24,10 @@ const readBody = async (request : IncomingMessage) : Promise<any> => {
|
|
|
24
24
|
return raw.length === 0 ? undefined : JSON.parse(raw);
|
|
25
25
|
};
|
|
26
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. */
|
|
27
31
|
class Router {
|
|
28
32
|
|
|
29
33
|
constructor(private persistence : JobPersistence, private queue : ConfirmableQueue,
|
|
@@ -40,36 +44,141 @@ class Router {
|
|
|
40
44
|
const [resource, id, subresource] = url.pathname.split("/").filter(Boolean);
|
|
41
45
|
const method = request.method ?? "GET";
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
|
48
95
|
}
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
102
|
}
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
54
135
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|
|
58
161
|
}
|
|
59
|
-
|
|
60
|
-
|
|
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": {
|
|
61
170
|
const record = await readBody(request);
|
|
62
171
|
if (typeof record?.jobId !== "string" || typeof record?.description !== "string"
|
|
63
172
|
|| typeof record?.actorId !== "string" || typeof record?.actorType !== "string"
|
|
64
173
|
|| !AUDIT_CHANGES.has(record?.change)) {
|
|
65
174
|
return this.reply(response, 400, { error: "Expected a body of { jobId, actorId, actorType, change, description, ... }" });
|
|
66
175
|
}
|
|
67
|
-
await this.auditRecords
|
|
176
|
+
await this.auditRecords!.save(record);
|
|
68
177
|
return this.reply(response, 204);
|
|
69
178
|
}
|
|
70
|
-
|
|
179
|
+
case "GET": {
|
|
71
180
|
const change = url.searchParams.get("change");
|
|
72
|
-
const records = await this.auditRecords
|
|
181
|
+
const records = await this.auditRecords!.list({
|
|
73
182
|
jobId: url.searchParams.get("jobId") ?? undefined,
|
|
74
183
|
actorId: url.searchParams.get("actorId") ?? undefined,
|
|
75
184
|
change: change && AUDIT_CHANGES.has(change) ? change as AuditChange : undefined,
|
|
@@ -80,83 +189,195 @@ class Router {
|
|
|
80
189
|
return this.reply(response, 200, records);
|
|
81
190
|
}
|
|
82
191
|
}
|
|
83
|
-
if (resource === "audit" && method === "GET" && !id) {
|
|
84
|
-
return this.servePage(response);
|
|
85
|
-
}
|
|
86
|
-
if (resource === "jobs") return this.handleJobs(method, id, subresource, url, request, response);
|
|
87
|
-
if (resource === "queue" && method === "POST" && !subresource) return this.handleQueue(id, request, response);
|
|
88
|
-
if (resource === "consumers" && method === "POST" && !id) {
|
|
89
|
-
const { workflowId, url: consumerUrl } = await readBody(request);
|
|
90
|
-
this.registry.register(workflowId, consumerUrl);
|
|
91
|
-
return this.reply(response, 204);
|
|
92
|
-
}
|
|
93
|
-
if (resource === "apps" && this.buildLayer) {
|
|
94
|
-
return this.handleApps(method, id, subresource, url, request, response);
|
|
95
|
-
}
|
|
96
|
-
if (resource === "state-machines" && method === "GET" && !id) {
|
|
97
|
-
return this.reply(response, 200, this.registry.list());
|
|
98
|
-
}
|
|
99
|
-
if (resource === "documents" && id && this.documentStoreFor) {
|
|
100
|
-
return this.handleDocuments(method, this.documentStoreFor(id), subresource, url, request, response);
|
|
101
|
-
}
|
|
102
|
-
if (resource === "secrets" && this.secretStore && !subresource) {
|
|
103
|
-
return this.handleSecrets(method, id, request, response);
|
|
104
|
-
}
|
|
105
|
-
if (resource && this.buildLayer) {
|
|
106
|
-
const app = this.buildLayer.status(resource);
|
|
107
|
-
if (app && app.status === "running") {
|
|
108
|
-
return this.forwardToApp(app.appHost, app.appPort, resource, method, url, request, response);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
192
|
|
|
112
|
-
this.
|
|
193
|
+
this.notFound(response);
|
|
113
194
|
}
|
|
114
195
|
|
|
115
|
-
private async
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
122
233
|
}
|
|
123
234
|
|
|
124
|
-
|
|
125
|
-
|
|
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;
|
|
126
274
|
}
|
|
127
275
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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);
|
|
132
286
|
}
|
|
133
|
-
await this.cliAuthorizer!.approve(requestId, clientName.trim(), user ?? new User("local"),
|
|
134
|
-
sessionTenant?.id ?? this.tenant);
|
|
135
|
-
return this.reply(response, 204);
|
|
136
287
|
}
|
|
137
288
|
|
|
138
|
-
this.
|
|
289
|
+
this.notFound(response);
|
|
139
290
|
}
|
|
140
291
|
|
|
141
|
-
private async
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
}
|
|
144
327
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
328
|
+
this.notFound(response);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async handleDocuments(method : string, store : JsonStore, documentId : string | undefined,
|
|
332
|
+
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
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
|
+
}
|
|
152
352
|
}
|
|
153
353
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
354
|
+
this.notFound(response);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private async handleSecrets(method : string, name : string | undefined,
|
|
358
|
+
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
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
|
+
}
|
|
157
378
|
}
|
|
158
379
|
|
|
159
|
-
this.
|
|
380
|
+
this.notFound(response);
|
|
160
381
|
}
|
|
161
382
|
|
|
162
383
|
private async servePage(response : ServerResponse) : Promise<void> {
|
|
@@ -185,140 +406,7 @@ class Router {
|
|
|
185
406
|
response.end(payload);
|
|
186
407
|
}
|
|
187
408
|
|
|
188
|
-
private
|
|
189
|
-
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
190
|
-
if (!name && method === "GET") {
|
|
191
|
-
return this.reply(response, 200, await this.secretStore!.list());
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (name) {
|
|
195
|
-
if (method === "PUT") {
|
|
196
|
-
const { value } = await readBody(request);
|
|
197
|
-
if (typeof value !== "string") return this.reply(response, 400, { error: "Expected a body of { value : string }" });
|
|
198
|
-
await this.secretStore!.save(name, value);
|
|
199
|
-
return this.reply(response, 204);
|
|
200
|
-
}
|
|
201
|
-
if (method === "GET") {
|
|
202
|
-
return this.reply(response, 200, { value: await this.secretStore!.retrieve(name) });
|
|
203
|
-
}
|
|
204
|
-
if (method === "DELETE") {
|
|
205
|
-
await this.secretStore!.delete(name);
|
|
206
|
-
return this.reply(response, 204);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
this.reply(response, 404, { error: "Not found" });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private async handleDocuments(method : string, store : JsonStore, documentId : string | undefined,
|
|
214
|
-
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
215
|
-
if (!documentId && method === "GET") {
|
|
216
|
-
const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
|
|
217
|
-
const page = Number(url.searchParams.get("page") ?? 0);
|
|
218
|
-
return this.reply(response, 200, await store.list(pageSize, page));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (documentId) {
|
|
222
|
-
if (method === "PUT") {
|
|
223
|
-
await store.save(documentId, await readBody(request));
|
|
224
|
-
return this.reply(response, 204);
|
|
225
|
-
}
|
|
226
|
-
if (method === "GET") {
|
|
227
|
-
return this.reply(response, 200, await store.retrieve(documentId));
|
|
228
|
-
}
|
|
229
|
-
if (method === "DELETE") {
|
|
230
|
-
await store.delete(documentId);
|
|
231
|
-
return this.reply(response, 204);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.reply(response, 404, { error: "Not found" });
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private async handleApps(method : string, appName : string | undefined, subresource : string | undefined,
|
|
239
|
-
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
240
|
-
if (method === "GET" && !appName) {
|
|
241
|
-
return this.reply(response, 200, this.buildLayer!.list());
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (!appName) return this.reply(response, 404, { error: "Not found" });
|
|
245
|
-
|
|
246
|
-
if (method === "POST" && subresource === "deploy") {
|
|
247
|
-
const appPort = Number(url.searchParams.get("port"));
|
|
248
|
-
if (!Number.isInteger(appPort) || appPort <= 0) {
|
|
249
|
-
return this.reply(response, 400, { error: "Expected a numeric port query parameter" });
|
|
250
|
-
}
|
|
251
|
-
const tarball = await readRawBody(request);
|
|
252
|
-
if (tarball.length === 0) return this.reply(response, 400, { error: "Expected a gzipped tarball body" });
|
|
253
|
-
return this.reply(response, 202, this.buildLayer!.deploy(appName, appPort, tarball));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (method === "GET" && !subresource) {
|
|
257
|
-
const status = this.buildLayer!.status(appName);
|
|
258
|
-
if (!status) return this.reply(response, 404, { error: `No app named "${appName}"` });
|
|
259
|
-
return this.reply(response, 200, status);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
this.reply(response, 404, { error: "Not found" });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private async handleJobs(method : string, id : string | undefined, subresource : string | undefined,
|
|
266
|
-
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
267
|
-
if (!id && method === "GET") {
|
|
268
|
-
const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
|
|
269
|
-
const page = Number(url.searchParams.get("page") ?? 0);
|
|
270
|
-
const jobs = await this.persistence.list(pageSize, page);
|
|
271
|
-
return this.reply(response, 200, jobs.map(serializeJob));
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (id && !subresource) {
|
|
275
|
-
if (method === "PUT") {
|
|
276
|
-
await this.persistence.save(deserializeJob(await readBody(request)));
|
|
277
|
-
return this.reply(response, 204);
|
|
278
|
-
}
|
|
279
|
-
if (method === "GET") {
|
|
280
|
-
const job = await this.persistence.retrieve(id);
|
|
281
|
-
return this.reply(response, 200, serializeJob(job));
|
|
282
|
-
}
|
|
283
|
-
if (method === "DELETE") {
|
|
284
|
-
await this.persistence.delete(id);
|
|
285
|
-
return this.reply(response, 204);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (id && subresource === "properties" && method === "PATCH") {
|
|
290
|
-
const properties = new Map<string, any>(Object.entries(await readBody(request)));
|
|
291
|
-
await this.persistence.updateProperties(id, properties);
|
|
292
|
-
return this.reply(response, 204);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
this.reply(response, 404, { error: "Not found" });
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private async handleQueue(operation : string | undefined, request : IncomingMessage,
|
|
299
|
-
response : ServerResponse) : Promise<void> {
|
|
300
|
-
if (operation === "enqueue") {
|
|
301
|
-
const { jobId, workflowId } = await readBody(request);
|
|
302
|
-
await this.queue.enqueue(jobId, workflowId);
|
|
303
|
-
return this.reply(response, 204);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (operation === "schedule") {
|
|
307
|
-
const { jobId, workflowId, due } = await readBody(request);
|
|
308
|
-
await this.queue.schedule(jobId, workflowId, new Date(due));
|
|
309
|
-
return this.reply(response, 204);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (operation === "dequeue") {
|
|
313
|
-
return this.reply(response, 200, { messages: await this.queue.dequeueSome() });
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (operation === "confirm") {
|
|
317
|
-
const { jobId, workflowId } = await readBody(request);
|
|
318
|
-
await this.queue.confirm({ jobId, workflowId });
|
|
319
|
-
return this.reply(response, 204);
|
|
320
|
-
}
|
|
321
|
-
|
|
409
|
+
private notFound(response : ServerResponse) : void {
|
|
322
410
|
this.reply(response, 404, { error: "Not found" });
|
|
323
411
|
}
|
|
324
412
|
|