anbaric-cloud-hosting 1.0.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/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ export * from "./app-management/BuildLayer";
2
+ export * from "./auth/Authenticator";
3
+ export * from "./auth/CliAuthorizer";
4
+ export * from "./auth/CliKey";
5
+ export * from "./auth/CliKeyStore";
6
+ export * from "./auth/InMemoryCliKeyStore";
7
+ export * from "./auth/AuthenticatorLoader";
8
+ export * from "./auth/KeyPair";
9
+ export * from "./auth/Role";
10
+ export * from "./auth/Tenant";
11
+ export * from "./auth/TokenAuthenticator";
12
+ export * from "./auth/User";
13
+ export * from "./app-management/DockerBuildLayer";
14
+ export * from "./app-management/FargateBuildLayer";
15
+ export * from "./data-store/PostgresCliKeyStore";
16
+ export * from "./data-store/PostgresJobPersistence";
17
+ export * from "./data-store/PostgresJsonStore";
18
+ export * from "./data-store/Schema";
19
+ export * from "./data-store/SecretsManagerSecretStore";
20
+ export * from "./hosting/HostingServer";
21
+ export * from "./hosting/Router";
22
+ export * from "./queuing/ConfirmableQueue";
23
+ export * from "./queuing/ConsumerRegistry";
24
+ export * from "./queuing/Dispatcher";
package/src/main.ts ADDED
@@ -0,0 +1,72 @@
1
+ import {SecretsManagerClient} from "@aws-sdk/client-secrets-manager";
2
+ import {SecretStore} from "anbaric-tsapi";
3
+ import {InMemorySecretStore} from "anbaric-data-store";
4
+ import {Pool} from "pg";
5
+ import {loadAuthenticator} from "./auth/AuthenticatorLoader";
6
+ import {CliAuthorizer} from "./auth/CliAuthorizer";
7
+ import {TokenAuthenticator} from "./auth/TokenAuthenticator";
8
+ import {PostgresCliKeyStore} from "./data-store/PostgresCliKeyStore";
9
+ import {ensureSchema} from "./data-store/Schema";
10
+ import {SecretsManagerSecretStore} from "./data-store/SecretsManagerSecretStore";
11
+ import {PostgresJobPersistence} from "./data-store/PostgresJobPersistence";
12
+ import {PostgresJsonStore} from "./data-store/PostgresJsonStore";
13
+ import {PostgresQueue} from "./queuing/PostgresQueue";
14
+ import {DockerBuildLayer} from "./app-management/DockerBuildLayer";
15
+ import {FargateBuildLayer} from "./app-management/FargateBuildLayer";
16
+ import {ConsumerRegistry} from "./queuing/ConsumerRegistry";
17
+ import {Dispatcher} from "./queuing/Dispatcher";
18
+ import {HostingServer} from "./hosting/HostingServer";
19
+
20
+ const pool = new Pool({ connectionString: process.env.ANBARIC_DATABASE_URL });
21
+ await ensureSchema(pool);
22
+
23
+ const queue = new PostgresQueue(pool);
24
+ const registry = new ConsumerRegistry();
25
+
26
+ const hostingPort = Number(process.env.ANBARIC_HOSTING_PORT ?? 8787);
27
+ const internalPort = Number(process.env.ANBARIC_INTERNAL_PORT ?? 8788);
28
+ const appsDir = process.env.ANBARIC_APPS_DIR ?? "/tmp/anbaric-apps";
29
+
30
+ const buildLayer = process.env.ANBARIC_BUILD_LAYER === "docker"
31
+ ? new DockerBuildLayer(appsDir, {
32
+ baseImage: process.env.ANBARIC_APP_BASE_IMAGE ?? "anbaric-v2-platform:local",
33
+ network: process.env.ANBARIC_DOCKER_NETWORK ?? "anbaric-v2-local",
34
+ platformUrl: process.env.ANBARIC_PLATFORM_INTERNAL_URL ?? `http://localhost:${internalPort}`,
35
+ })
36
+ : process.env.ANBARIC_BUILD_LAYER === "fargate"
37
+ ? new FargateBuildLayer(appsDir, {
38
+ awsRegion: process.env.AWS_REGION!,
39
+ cluster: process.env.ANBARIC_AWS_CLUSTER!,
40
+ subnets: (process.env.ANBARIC_AWS_SUBNETS ?? "").split(","),
41
+ appSecurityGroup: process.env.ANBARIC_AWS_APP_SECURITY_GROUP!,
42
+ namespaceId: process.env.ANBARIC_AWS_NAMESPACE_ID!,
43
+ namespaceName: process.env.ANBARIC_AWS_NAMESPACE_NAME!,
44
+ appsRepositoryUrl: process.env.ANBARIC_AWS_APPS_REPOSITORY!,
45
+ buildBucket: process.env.ANBARIC_AWS_BUILD_BUCKET!,
46
+ buildProject: process.env.ANBARIC_AWS_BUILD_PROJECT!,
47
+ baseImage: process.env.ANBARIC_AWS_BASE_IMAGE!,
48
+ appExecutionRoleArn: process.env.ANBARIC_AWS_APP_EXECUTION_ROLE!,
49
+ appsLogGroup: process.env.ANBARIC_AWS_APPS_LOG_GROUP!,
50
+ platformUrl: process.env.ANBARIC_PLATFORM_INTERNAL_URL!,
51
+ })
52
+ : undefined;
53
+
54
+ const secretStore : SecretStore = process.env.AWS_REGION
55
+ ? new SecretsManagerSecretStore(new SecretsManagerClient({}))
56
+ : new InMemorySecretStore();
57
+
58
+ const authenticator = await loadAuthenticator(process.env.ANBARIC_AUTHENTICATOR);
59
+ const cliKeyStore = new PostgresCliKeyStore(pool);
60
+ const cliAuthorizer = new CliAuthorizer(cliKeyStore);
61
+ const tokenAuthenticator = new TokenAuthenticator(cliKeyStore);
62
+
63
+ const server = new HostingServer(new PostgresJobPersistence(pool), queue, registry, buildLayer,
64
+ (collection) => new PostgresJsonStore(pool, collection), secretStore, authenticator, cliAuthorizer,
65
+ tokenAuthenticator, process.env.ANBARIC_TENANT);
66
+ const port = await server.listen(hostingPort);
67
+ const internal = await server.listenInternal(internalPort);
68
+
69
+ const dispatcher = new Dispatcher(queue, registry, Number(process.env.ANBARIC_DISPATCH_INTERVAL_MS ?? 1000));
70
+ dispatcher.start();
71
+
72
+ console.log(`anbaric-cloud-hosting listening on port ${port}, internal entry point on ${internal}`);
@@ -0,0 +1,9 @@
1
+ import {Dequeue, QueueMessage} from "anbaric-tsapi";
2
+
3
+ interface ConfirmableQueue extends Dequeue {
4
+
5
+ confirm(message : QueueMessage) : Promise<void>;
6
+
7
+ }
8
+
9
+ export { ConfirmableQueue }
@@ -0,0 +1,19 @@
1
+ class ConsumerRegistry {
2
+
3
+ private consumers = new Map<string, string>();
4
+
5
+ register(workflowId : string, url : string) : void {
6
+ this.consumers.set(workflowId, url);
7
+ }
8
+
9
+ lookup(workflowId : string) : string | undefined {
10
+ return this.consumers.get(workflowId);
11
+ }
12
+
13
+ list() : Array<{ workflowId : string, url : string }> {
14
+ return Array.from(this.consumers, ([workflowId, url]) => ({ workflowId, url }));
15
+ }
16
+
17
+ }
18
+
19
+ export { ConsumerRegistry }
@@ -0,0 +1,64 @@
1
+ import {Dequeue, QueueMessage} from "anbaric-tsapi";
2
+ import {ConsumerRegistry} from "./ConsumerRegistry";
3
+
4
+ class Dispatcher {
5
+
6
+ private ticker? : NodeJS.Timeout;
7
+ private draining = false;
8
+
9
+ constructor(private queue : Dequeue, private registry : ConsumerRegistry,
10
+ private dispatchIntervalMs : number = 1000) {}
11
+
12
+ start() : void {
13
+ if (this.ticker) return;
14
+ this.ticker = setInterval(() => void this.drain(), this.dispatchIntervalMs);
15
+ this.ticker.unref();
16
+ }
17
+
18
+ async cleanUp() : Promise<void> {
19
+ if (this.ticker) clearInterval(this.ticker);
20
+ this.ticker = undefined;
21
+ }
22
+
23
+ private async drain() : Promise<void> {
24
+ if (this.draining) return;
25
+ this.draining = true;
26
+ try {
27
+ const messages = await this.queue.dequeueSome();
28
+ const byConsumerUrl = new Map<string, Array<QueueMessage>>();
29
+
30
+ for (const message of messages) {
31
+ const url = this.registry.lookup(message.workflowId);
32
+ if (!url) {
33
+ await this.queue.enqueue(message.jobId, message.workflowId);
34
+ continue;
35
+ }
36
+ byConsumerUrl.set(url, [...(byConsumerUrl.get(url) ?? []), message]);
37
+ }
38
+
39
+ for (const [url, batch] of byConsumerUrl) {
40
+ await this.push(url, batch);
41
+ }
42
+ } finally {
43
+ this.draining = false;
44
+ }
45
+ }
46
+
47
+ private async push(url : string, batch : Array<QueueMessage>) : Promise<void> {
48
+ try {
49
+ const response = await fetch(`${url}/process`, {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json" },
52
+ body: JSON.stringify({ messages: batch }),
53
+ });
54
+ if (!response.ok) throw new Error(`Consumer at ${url} responded with status ${response.status}`);
55
+ } catch {
56
+ for (const message of batch) {
57
+ await this.queue.enqueue(message.jobId, message.workflowId);
58
+ }
59
+ }
60
+ }
61
+
62
+ }
63
+
64
+ export { Dispatcher }
@@ -0,0 +1,54 @@
1
+ import {QueueMessage} from "anbaric-tsapi";
2
+ import {Pool} from "pg";
3
+ import {ConfirmableQueue} from "./ConfirmableQueue";
4
+
5
+ const DEQUEUE_BATCH_SIZE = 100;
6
+ const LEASE_SECONDS = 30;
7
+
8
+ class PostgresQueue implements ConfirmableQueue {
9
+
10
+ constructor(private pool : Pool) {}
11
+
12
+ async enqueue(jobId : string, workflowId : string) : Promise<void> {
13
+ await this.pool.query("INSERT INTO queue (job_id, workflow_id) VALUES ($1, $2)", [jobId, workflowId]);
14
+ }
15
+
16
+ async schedule(jobId : string, workflowId : string, due : Date) : Promise<void> {
17
+ await this.pool.query("INSERT INTO queue (job_id, workflow_id, due) VALUES ($1, $2, $3)", [jobId, workflowId, due]);
18
+ }
19
+
20
+ async dequeueSome() : Promise<Array<QueueMessage>> {
21
+ const result = await this.pool.query(
22
+ `UPDATE queue SET leased_until = now() + interval '${LEASE_SECONDS} seconds'
23
+ WHERE position IN (
24
+ SELECT position FROM queue
25
+ WHERE (due IS NULL OR due <= now())
26
+ AND (leased_until IS NULL OR leased_until < now())
27
+ ORDER BY (due IS NOT NULL), due, position
28
+ LIMIT $1
29
+ FOR UPDATE SKIP LOCKED
30
+ )
31
+ RETURNING job_id, workflow_id, due, position`,
32
+ [DEQUEUE_BATCH_SIZE],
33
+ );
34
+
35
+ return result.rows
36
+ .sort((a, b) => {
37
+ if (!a.due && !b.due) return a.position - b.position;
38
+ if (!a.due) return -1;
39
+ if (!b.due) return 1;
40
+ return a.due.getTime() - b.due.getTime() || a.position - b.position;
41
+ })
42
+ .map(row => ({ jobId: row.job_id, workflowId: row.workflow_id }));
43
+ }
44
+
45
+ async confirm(message : QueueMessage) : Promise<void> {
46
+ await this.pool.query(
47
+ "DELETE FROM queue WHERE job_id = $1 AND workflow_id = $2",
48
+ [message.jobId, message.workflowId],
49
+ );
50
+ }
51
+
52
+ }
53
+
54
+ export { PostgresQueue }