anbaric-cloud-hosting 1.16.0 → 1.16.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.1",
|
|
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",
|
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
"@aws-sdk/client-s3": "^3.1110.0",
|
|
19
19
|
"@aws-sdk/client-secrets-manager": "^3.700.0",
|
|
20
20
|
"@aws-sdk/client-servicediscovery": "^3.1110.0",
|
|
21
|
-
"anbaric-data-store": "^1.16.
|
|
22
|
-
"anbaric-plugins": "^1.16.
|
|
23
|
-
"anbaric-tsapi": "^1.16.
|
|
21
|
+
"anbaric-data-store": "^1.16.1",
|
|
22
|
+
"anbaric-plugins": "^1.16.1",
|
|
23
|
+
"anbaric-tsapi": "^1.16.1",
|
|
24
24
|
"esbuild": "^0.28.2",
|
|
25
25
|
"pg": "^8.16.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^26.2.0",
|
|
29
29
|
"@types/pg": "^8.15.0",
|
|
30
|
-
"anbaric-impl-cloud": "^1.16.
|
|
31
|
-
"anbaric-state-machine": "^1.16.
|
|
30
|
+
"anbaric-impl-cloud": "^1.16.1",
|
|
31
|
+
"anbaric-state-machine": "^1.16.1",
|
|
32
32
|
"tsx": "^4.20.0",
|
|
33
33
|
"typescript": "^7.0.2"
|
|
34
34
|
},
|
|
@@ -31,11 +31,24 @@ abstract class BaseBuildLayer implements BuildLayer {
|
|
|
31
31
|
|
|
32
32
|
protected deployments = new Map<string, Deployment>();
|
|
33
33
|
private nextAppIndex = 0;
|
|
34
|
+
private hydration? : Promise<void>;
|
|
34
35
|
|
|
35
36
|
constructor(protected appsDir : string, private consumerPortBase : number = 8800,
|
|
36
37
|
private probe : Probe = adminProbe,
|
|
37
38
|
private livenessTimeoutMs : number = LIVENESS_TIMEOUT_MS) {}
|
|
38
39
|
|
|
40
|
+
/* The deployments map is rebuilt from the durable backend the first time the
|
|
41
|
+
app registry is read, so apps survive a platform restart. A failed
|
|
42
|
+
rehydration is not cached, so a later request retries. */
|
|
43
|
+
ensureHydrated() : Promise<void> {
|
|
44
|
+
return this.hydration ??= this.rehydrate().catch(error => {
|
|
45
|
+
this.hydration = undefined;
|
|
46
|
+
console.warn(`Could not rehydrate the app registry: ${error instanceof Error ? error.message : error}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected async rehydrate() : Promise<void> {}
|
|
51
|
+
|
|
39
52
|
deploy(appName : string, appPort : number, tarball : Buffer) : DeploymentSummary {
|
|
40
53
|
const existing = this.deployments.get(appName);
|
|
41
54
|
|
|
@@ -9,6 +9,7 @@ type DeploymentSummary = {
|
|
|
9
9
|
|
|
10
10
|
interface BuildLayer {
|
|
11
11
|
|
|
12
|
+
ensureHydrated() : Promise<void>;
|
|
12
13
|
deploy(appName : string, appPort : number, tarball : Buffer) : DeploymentSummary;
|
|
13
14
|
status(appName : string) : (DeploymentSummary & { log : Array<string> }) | undefined;
|
|
14
15
|
list() : Array<DeploymentSummary>;
|
|
@@ -2,6 +2,7 @@ import {readFile, writeFile} from "node:fs/promises";
|
|
|
2
2
|
import {join} from "node:path";
|
|
3
3
|
import {CodeBuildClient, BatchGetBuildsCommand, StartBuildCommand} from "@aws-sdk/client-codebuild";
|
|
4
4
|
import {ECSClient, CreateServiceCommand, DeleteServiceCommand, DescribeServicesCommand,
|
|
5
|
+
DescribeTaskDefinitionCommand, ListServicesCommand as ListEcsServicesCommand,
|
|
5
6
|
RegisterTaskDefinitionCommand, UpdateServiceCommand} from "@aws-sdk/client-ecs";
|
|
6
7
|
import {S3Client, PutObjectCommand} from "@aws-sdk/client-s3";
|
|
7
8
|
import {ServiceDiscoveryClient, CreateServiceCommand as CreateDiscoveryServiceCommand,
|
|
@@ -255,6 +256,67 @@ class FargateBuildLayer extends BaseBuildLayer {
|
|
|
255
256
|
}));
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
/* ECS is the durable record of what is deployed, so on the first read after
|
|
260
|
+
a restart the registry is rebuilt from the running anbaric-app-* services
|
|
261
|
+
and their task definitions (ports come from the container environment the
|
|
262
|
+
platform set at deploy time). */
|
|
263
|
+
protected async rehydrate() : Promise<void> {
|
|
264
|
+
const serviceArns = await this.appServiceArns();
|
|
265
|
+
|
|
266
|
+
for (let batch = 0; batch < serviceArns.length; batch += 10) {
|
|
267
|
+
const {services} = await this.aws.ecs.send(new DescribeServicesCommand({
|
|
268
|
+
cluster: this.options.cluster,
|
|
269
|
+
services: serviceArns.slice(batch, batch + 10),
|
|
270
|
+
}));
|
|
271
|
+
for (const service of services ?? []) {
|
|
272
|
+
if (service.status !== "ACTIVE" || !service.serviceName?.startsWith("anbaric-app-")) continue;
|
|
273
|
+
const appName = service.serviceName.slice("anbaric-app-".length);
|
|
274
|
+
if (this.deployments.has(appName)) continue;
|
|
275
|
+
const deployment = await this.deploymentFromService(appName, service);
|
|
276
|
+
if (deployment) this.deployments.set(appName, deployment);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async appServiceArns() : Promise<Array<string>> {
|
|
282
|
+
const arns : Array<string> = [];
|
|
283
|
+
let nextToken : string | undefined;
|
|
284
|
+
do {
|
|
285
|
+
const page = await this.aws.ecs.send(new ListEcsServicesCommand({ cluster: this.options.cluster, nextToken }));
|
|
286
|
+
for (const arn of page.serviceArns ?? []) {
|
|
287
|
+
if (arn.includes("/anbaric-app-")) arns.push(arn);
|
|
288
|
+
}
|
|
289
|
+
nextToken = page.nextToken;
|
|
290
|
+
} while (nextToken);
|
|
291
|
+
return arns;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private async deploymentFromService(appName : string, service : { taskDefinition : string, runningCount? : number }) : Promise<Deployment | undefined> {
|
|
295
|
+
const {taskDefinition} = await this.aws.ecs.send(new DescribeTaskDefinitionCommand({ taskDefinition: service.taskDefinition }));
|
|
296
|
+
const container = taskDefinition?.containerDefinitions?.[0];
|
|
297
|
+
if (!container) return undefined;
|
|
298
|
+
|
|
299
|
+
const environment = new Map<string, string>(
|
|
300
|
+
(container.environment ?? []).map((entry : { name : string, value : string }) => [entry.name, entry.value]),
|
|
301
|
+
);
|
|
302
|
+
const port = (name : string, fallback : number) => {
|
|
303
|
+
const value = Number(environment.get(name));
|
|
304
|
+
return Number.isInteger(value) ? value : fallback;
|
|
305
|
+
};
|
|
306
|
+
const appPort = Number(environment.get("PORT"));
|
|
307
|
+
if (!Number.isInteger(appPort)) return undefined;
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
appName,
|
|
311
|
+
status: (service.runningCount ?? 0) > 0 ? "running" : "stopped",
|
|
312
|
+
appPort,
|
|
313
|
+
appHost: this.appHostFor(appName),
|
|
314
|
+
adminPort: port("ANBARIC_ADMIN_PORT", 8791),
|
|
315
|
+
consumerPort: port("ANBARIC_CONSUMER_PORT", 0),
|
|
316
|
+
log: [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
258
320
|
private serviceNameFor(appName : string) : string {
|
|
259
321
|
return `anbaric-app-${appName}`;
|
|
260
322
|
}
|
|
@@ -7,6 +7,7 @@ class AppsHandler implements RequestHandler {
|
|
|
7
7
|
constructor(private buildLayer : BuildLayer) {}
|
|
8
8
|
|
|
9
9
|
async handle(request : Request) : Promise<void> {
|
|
10
|
+
await this.buildLayer.ensureHydrated();
|
|
10
11
|
switch (request.subresource) {
|
|
11
12
|
case "deploy":
|
|
12
13
|
if (request.id) return this.handleDeploy(request, request.id);
|