anbaric-cloud-hosting 1.68.3 → 1.69.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/main.ts CHANGED
@@ -26,6 +26,7 @@ import {FargateBuildLayer} from "./app-management/FargateBuildLayer";
26
26
  import {DocGenerator} from "./docs/DocGenerator";
27
27
  import {GatewayDocModel} from "./docs/GatewayDocModel";
28
28
  import {PostgresAppDocsStore} from "./data-store/PostgresAppDocsStore";
29
+ import {PostgresAppSettingsStore} from "./data-store/PostgresAppSettingsStore";
29
30
  import {PostgresEntitlementStore} from "./data-store/PostgresEntitlementStore";
30
31
  import {PostgresUserDirectory} from "./data-store/PostgresUserDirectory";
31
32
  import {HttpMembershipService} from "./auth/HttpMembershipService";
@@ -41,6 +42,12 @@ import {CentralBilling} from "./billing/CentralBilling";
41
42
 
42
43
  new FatalErrors().install();
43
44
 
45
+ // "key=value,key=value", as the hosting module writes it; nothing means untagged.
46
+ const resourceTags = (spec? : string) : Record<string, string> | undefined => {
47
+ const entries = (spec ?? "").split(",").map(pair => pair.split("=")).filter(pair => pair.length === 2 && pair[0].trim() && pair[1].trim());
48
+ return entries.length === 0 ? undefined : Object.fromEntries(entries.map(([key, value]) => [key.trim(), value.trim()]));
49
+ };
50
+
44
51
  const pool = new Pool({ connectionString: process.env.ANBARIC_DATABASE_URL });
45
52
  await ensureSchema(pool);
46
53
 
@@ -54,6 +61,7 @@ const appsDir = process.env.ANBARIC_APPS_DIR ?? "/tmp/anbaric-apps";
54
61
  // Generates user docs from an app's source on deploy; no-ops without an AI
55
62
  // gateway token. The build layer calls it fire-and-forget off the deploy path.
56
63
  const docGenerator = new DocGenerator(new GatewayDocModel(), new PostgresAppDocsStore(pool));
64
+ const appSettings = new PostgresAppSettingsStore(pool);
57
65
 
58
66
  const buildLayer = process.env.ANBARIC_BUILD_LAYER === "docker"
59
67
  ? new DockerBuildLayer(appsDir, {
@@ -85,6 +93,7 @@ const buildLayer = process.env.ANBARIC_BUILD_LAYER === "docker"
85
93
  sqlDatabaseUrlSecretArn: process.env.ANBARIC_AWS_APP_SQL_URL_SECRET,
86
94
  sqlSchema: process.env.ANBARIC_SQL_SCHEMA,
87
95
  docGenerator,
96
+ tags: resourceTags(process.env.ANBARIC_RESOURCE_TAGS),
88
97
  })
89
98
  : undefined;
90
99
 
@@ -147,7 +156,7 @@ const server = new HostingServer(jobs, queue, registry, buildLayer,
147
156
  tokenAuthenticator, process.env.ANBARIC_TENANT, auditRecords, plugins,
148
157
  new PostgresJobRunSchedulePersistence(pool), await loadNotifier(process.env.ANBARIC_NOTIFIER_MODULE),
149
158
  new PostgresEntitlementStore(pool), new PostgresUserDirectory(pool), memberships,
150
- (appId) => new PostgresPromptManager(pool, appId), fileStorageFor, subdomains, billing);
159
+ (appId) => new PostgresPromptManager(pool, appId), fileStorageFor, subdomains, billing, appSettings);
151
160
  const port = await server.listen(hostingPort);
152
161
  const internal = await server.listenInternal(internalPort);
153
162
 
@@ -163,9 +172,14 @@ if (buildLayer && process.env.ANBARIC_TENANT && process.env.ANBARIC_CLI_KEY_LOOK
163
172
  process.env.ANBARIC_CLI_KEY_LOOKUP_URL,
164
173
  process.env.ANBARIC_CLI_KEY_LOOKUP_SECRET ?? "",
165
174
  process.env.ANBARIC_TENANT,
166
- () => {
175
+ async () => {
167
176
  const live = buildLayer.list().filter(app => app.status === "running" || app.status === "draining");
168
- return { apps: live.length, large: live.filter(app => app.size === "large").length };
177
+ const zeroDowntime = new Set((await appSettings.list()).filter(each => each.zeroDowntime).map(each => each.appName));
178
+ return {
179
+ apps: live.length,
180
+ large: live.filter(app => app.size === "large").length,
181
+ zeroDowntime: live.filter(app => zeroDowntime.has(app.appName)).length,
182
+ };
169
183
  },
170
184
  ).start();
171
185
  }
@@ -1,7 +1,10 @@
1
1
  type FetchFn = (url : string, init : RequestInit) => Promise<Response>;
2
2
 
3
- // How many apps are running, and how many of those are on a large instance.
4
- type CountApps = () => { apps : number, large : number };
3
+ /* How many apps are running, how many of those are on a large instance, and
4
+ how many have zero-downtime deploys turned on. Counted afresh each report,
5
+ and allowed to look something up on the way. */
6
+ type AppCounts = { apps : number, large : number, zeroDowntime? : number };
7
+ type CountApps = () => AppCounts | Promise<AppCounts>;
5
8
 
6
9
  const REPORT_INTERVAL_MS = 60 * 60_000;
7
10
 
@@ -37,7 +40,7 @@ class UsageReporter {
37
40
  const response = await this.fetchFn(`${this.centralUrl}/usage`, {
38
41
  method: "POST",
39
42
  headers: { "content-type": "application/json", "x-anbaric-central-key": this.secret },
40
- body: JSON.stringify({ slug: this.tenantSlug, ...this.counted(), day: this.today() }),
43
+ body: JSON.stringify({ slug: this.tenantSlug, ...(await this.counted()), day: this.today() }),
41
44
  });
42
45
  if (! response.ok) console.warn(`[usage] central answered ${response.status} to the app count`);
43
46
  return response.ok;
@@ -47,9 +50,9 @@ class UsageReporter {
47
50
  }
48
51
  }
49
52
 
50
- private counted() : { apps : number, largeApps : number } {
51
- const { apps, large } = this.countApps();
52
- return { apps, largeApps: large };
53
+ private async counted() : Promise<{ apps : number, largeApps : number, zeroDowntimeApps : number }> {
54
+ const { apps, large, zeroDowntime } = await this.countApps();
55
+ return { apps, largeApps: large, zeroDowntimeApps: zeroDowntime ?? 0 };
53
56
  }
54
57
 
55
58
  // The day in UTC, so every tenant's day starts at the same moment however
@@ -66,4 +69,4 @@ class UsageReporter {
66
69
  }
67
70
 
68
71
  export { UsageReporter };
69
- export type { CountApps };
72
+ export type { CountApps, AppCounts };