@hasna/uptime 0.1.5 → 0.1.7
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/.dockerignore +13 -0
- package/CHANGELOG.md +41 -3
- package/Dockerfile +31 -0
- package/Dockerfile.package +22 -0
- package/README.md +10 -0
- package/dist/api.js +38 -8
- package/dist/cli/index.js +110 -51
- package/dist/cloud-plan.d.ts +21 -4
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +59 -38
- package/dist/index.js +97 -46
- package/dist/mcp/index.js +38 -8
- package/dist/service.d.ts +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +38 -8
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +40 -9
- package/docs/aws-deployment-runbook.md +48 -23
- package/infra/aws/.terraform.lock.hcl +25 -0
- package/infra/aws/README.md +43 -0
- package/infra/aws/main.tf +795 -0
- package/infra/aws/outputs.tf +34 -0
- package/infra/aws/terraform.tfvars.example +28 -0
- package/infra/aws/variables.tf +170 -0
- package/package.json +8 -1
package/.dockerignore
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -6,15 +6,53 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.7] - 2026-06-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Explicit hosted EFS-backed SQLite runtime path with
|
|
14
|
+
`HASNA_UPTIME_HOSTED_SQLITE_DB` and `hosted-efs-sqlite` health metadata.
|
|
15
|
+
- AWS Terraform EFS file system, access point, ECS volume mount, and AWS Backup
|
|
16
|
+
plan for the hosted SQLite data store.
|
|
17
|
+
- `Dockerfile.package` plus AWS CodeBuild image-builder Terraform resources to
|
|
18
|
+
build the published npm package into ECR without relying on local Docker.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Hosted AWS deployment artifacts no longer inject `HASNA_UPTIME_DATABASE_URL`;
|
|
23
|
+
the async Postgres adapter remains future work.
|
|
24
|
+
- The EFS-backed SQLite bridge is single-writer only: one web task maximum and
|
|
25
|
+
scheduler/public-probe/reporter services remain disabled until Postgres and
|
|
26
|
+
cloud leases exist.
|
|
27
|
+
|
|
28
|
+
## [0.1.6] - 2026-06-28
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Bun-based hosted runtime `Dockerfile` and `.dockerignore`.
|
|
33
|
+
- Reviewable Terraform/OpenTofu AWS starter plan under `infra/aws` for ECR,
|
|
34
|
+
S3 evidence storage, ECS/Fargate services, ALB/TLS/DNS, task roles,
|
|
35
|
+
CloudWatch logs, security groups, and secret refs.
|
|
36
|
+
- Cloud plan SDK/CLI fields that point to `Dockerfile` and `infra/aws` with
|
|
37
|
+
format/init/validate/plan commands while keeping apply disabled.
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
|
|
41
|
+
- AWS infra templates use secret ARNs/valueFrom references and example
|
|
42
|
+
placeholders only; no plaintext service tokens, database URLs, or private keys
|
|
43
|
+
are stored in the repo.
|
|
44
|
+
- Terraform desired counts default to zero until hosted cloud-store/auth/probe
|
|
45
|
+
blockers are closed.
|
|
46
|
+
|
|
9
47
|
## [0.1.5] - 2026-06-28
|
|
10
48
|
|
|
11
49
|
### Added
|
|
12
50
|
|
|
13
|
-
- Dry-run AWS deployment plan generator for
|
|
51
|
+
- Dry-run AWS deployment plan generator for a reviewed AWS target,
|
|
14
52
|
covering ECS/Fargate services, ECR image commands, ALB/RDS/S3/Secrets/Logs
|
|
15
53
|
resources, rollback steps, and safety assertions.
|
|
16
|
-
- Spark01
|
|
17
|
-
rendering.
|
|
54
|
+
- Spark01 hosted-targeted private probe preflight config generator with JSON and
|
|
55
|
+
env-file rendering.
|
|
18
56
|
- CLI commands `uptime cloud plan` and `uptime cloud spark01-config`.
|
|
19
57
|
- SDK export `@hasna/uptime/cloud-plan`.
|
|
20
58
|
- Machine-readable `blocked`/`canApply:false` and `blocked`/`canStart:false`
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
FROM oven/bun:1.3.13-slim AS build
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
COPY package.json bun.lock tsconfig.json tsconfig.build.json ./
|
|
7
|
+
COPY src ./src
|
|
8
|
+
|
|
9
|
+
RUN bun install --frozen-lockfile
|
|
10
|
+
RUN bun run build
|
|
11
|
+
RUN bun install --production --frozen-lockfile
|
|
12
|
+
|
|
13
|
+
FROM oven/bun:1.3.13-slim AS runtime
|
|
14
|
+
ENV NODE_ENV=production \
|
|
15
|
+
HASNA_UPTIME_MODE=hosted
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
|
|
18
|
+
RUN addgroup --system --gid 10001 uptime \
|
|
19
|
+
&& adduser --system --uid 10001 --ingroup uptime uptime
|
|
20
|
+
|
|
21
|
+
COPY --from=build /app/package.json ./package.json
|
|
22
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
23
|
+
COPY --from=build /app/dist ./dist
|
|
24
|
+
|
|
25
|
+
USER uptime
|
|
26
|
+
EXPOSE 3899
|
|
27
|
+
|
|
28
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
29
|
+
CMD bun -e "const r = await fetch('http://127.0.0.1:3899/health'); process.exit(r.ok ? 0 : 1)"
|
|
30
|
+
|
|
31
|
+
CMD ["bun", "dist/cli/index.js", "serve", "--mode", "hosted", "--host", "0.0.0.0", "--port", "3899"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
FROM oven/bun:1.3.13-slim AS runtime
|
|
4
|
+
ENV NODE_ENV=production \
|
|
5
|
+
HASNA_UPTIME_MODE=hosted
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
RUN addgroup --system --gid 10001 uptime \
|
|
9
|
+
&& adduser --system --uid 10001 --ingroup uptime uptime
|
|
10
|
+
|
|
11
|
+
COPY package.json ./package.json
|
|
12
|
+
COPY dist ./dist
|
|
13
|
+
|
|
14
|
+
RUN bun install --production
|
|
15
|
+
|
|
16
|
+
USER uptime
|
|
17
|
+
EXPOSE 3899
|
|
18
|
+
|
|
19
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
20
|
+
CMD bun -e "const r = await fetch('http://127.0.0.1:3899/health'); process.exit(r.ok ? 0 : 1)"
|
|
21
|
+
|
|
22
|
+
CMD ["bun", "dist/cli/index.js", "serve", "--mode", "hosted", "--host", "0.0.0.0", "--port", "3899"]
|
package/README.md
CHANGED
|
@@ -46,6 +46,16 @@ only. They do not call AWS, write secrets, or produce an approved deploy script;
|
|
|
46
46
|
current output is intentionally blocked until the infra and cloud-store evidence
|
|
47
47
|
in `docs/aws-deployment-runbook.md` is satisfied.
|
|
48
48
|
|
|
49
|
+
Deployment review artifacts live in `Dockerfile` and `infra/aws`. The Terraform
|
|
50
|
+
desired counts default to zero, and `uptime cloud plan --json` exposes the
|
|
51
|
+
format/init/validate/plan commands with `applyAllowed: false`. Hosted AWS
|
|
52
|
+
runtime state currently uses explicit EFS-backed SQLite via
|
|
53
|
+
`HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db` for one protected web
|
|
54
|
+
task maximum; do not set `HASNA_UPTIME_DATABASE_URL` until the async Postgres
|
|
55
|
+
adapter is implemented.
|
|
56
|
+
`Dockerfile.package` is used by the Terraform CodeBuild image builder to build
|
|
57
|
+
the published npm package into ECR from inside AWS.
|
|
58
|
+
|
|
49
59
|
Private/local probes can submit signed results from another machine:
|
|
50
60
|
|
|
51
61
|
```bash
|
package/dist/api.js
CHANGED
|
@@ -820,10 +820,12 @@ function ensureUptimeHome() {
|
|
|
820
820
|
}
|
|
821
821
|
|
|
822
822
|
// src/store.ts
|
|
823
|
-
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statSync } from "fs";
|
|
823
|
+
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statfsSync, statSync } from "fs";
|
|
824
824
|
import { dirname, join as join2 } from "path";
|
|
825
825
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
826
826
|
import { Database } from "bun:sqlite";
|
|
827
|
+
var DEFAULT_HOSTED_SQLITE_DB_PATH = "/data/uptime/uptime.db";
|
|
828
|
+
var NFS_SUPER_MAGIC = 26985;
|
|
827
829
|
var SECRET_URL_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
|
|
828
830
|
var REQUIRED_TABLES = [
|
|
829
831
|
"schema_migrations",
|
|
@@ -860,18 +862,39 @@ class UptimeStore {
|
|
|
860
862
|
this.mode = resolveRuntimeMode(options.mode ?? "local");
|
|
861
863
|
const cloudDatabaseUrl = options.cloudDatabaseUrl ?? process.env.HASNA_UPTIME_DATABASE_URL;
|
|
862
864
|
if (this.mode === "hosted" && cloudDatabaseUrl) {
|
|
863
|
-
throw new Error("hosted
|
|
865
|
+
throw new Error("hosted Postgres adapter is not implemented yet; use HASNA_UPTIME_HOSTED_SQLITE_DB on cloud-mounted storage for the current hosted deployment path");
|
|
864
866
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
+
const hostedSqliteDbPath = options.hostedSqliteDbPath ?? process.env.HASNA_UPTIME_HOSTED_SQLITE_DB;
|
|
868
|
+
if (this.mode === "hosted" && hostedSqliteDbPath) {
|
|
869
|
+
if (hostedSqliteDbPath === ":memory:" || !hostedSqliteDbPath.startsWith("/")) {
|
|
870
|
+
throw new Error("HASNA_UPTIME_HOSTED_SQLITE_DB must be an absolute path on mounted cloud storage");
|
|
871
|
+
}
|
|
872
|
+
const approvedHostedPath = hostedSqliteDbPath === DEFAULT_HOSTED_SQLITE_DB_PATH;
|
|
873
|
+
if (!approvedHostedPath && !allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
874
|
+
throw new Error(`HASNA_UPTIME_HOSTED_SQLITE_DB must be ${DEFAULT_HOSTED_SQLITE_DB_PATH}; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing`);
|
|
875
|
+
}
|
|
876
|
+
const verifiedCloudMount = approvedHostedPath && isNfsMount(dirname(hostedSqliteDbPath));
|
|
877
|
+
if (approvedHostedPath && !verifiedCloudMount && !allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
878
|
+
throw new Error(`${DEFAULT_HOSTED_SQLITE_DB_PATH} must be on a mounted EFS/NFS filesystem; refusing to create hosted task-local SQLite`);
|
|
879
|
+
}
|
|
880
|
+
this.dataMode = verifiedCloudMount ? "hosted-efs-sqlite" : "hosted-local-sqlite";
|
|
881
|
+
this.dbPath = hostedSqliteDbPath;
|
|
882
|
+
} else if (this.mode === "hosted") {
|
|
883
|
+
if (!allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
884
|
+
throw new Error("hosted mode requires HASNA_UPTIME_HOSTED_SQLITE_DB on mounted cloud storage; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
|
|
885
|
+
}
|
|
886
|
+
this.dataMode = "hosted-local-sqlite";
|
|
887
|
+
this.dbPath = options.dbPath ?? uptimeHostedFallbackDbPath();
|
|
888
|
+
} else {
|
|
889
|
+
this.dataMode = "local-sqlite";
|
|
890
|
+
this.dbPath = options.dbPath ?? uptimeDbPath();
|
|
867
891
|
}
|
|
868
|
-
|
|
869
|
-
this.dbPath = options.dbPath ?? (this.mode === "hosted" ? uptimeHostedFallbackDbPath() : uptimeDbPath());
|
|
870
|
-
if (this.dbPath !== ":memory:") {
|
|
892
|
+
if (this.dbPath !== ":memory:" && this.dataMode !== "hosted-efs-sqlite") {
|
|
871
893
|
mkdirSync2(dirname(this.dbPath), { recursive: true });
|
|
872
894
|
}
|
|
873
895
|
this.db = new Database(this.dbPath, { create: true });
|
|
874
|
-
this.db.run("PRAGMA journal_mode = WAL");
|
|
896
|
+
this.db.run(this.dataMode === "hosted-efs-sqlite" ? "PRAGMA journal_mode = DELETE" : "PRAGMA journal_mode = WAL");
|
|
897
|
+
this.db.run("PRAGMA busy_timeout = 5000");
|
|
875
898
|
this.db.run("PRAGMA foreign_keys = ON");
|
|
876
899
|
this.migrate();
|
|
877
900
|
}
|
|
@@ -1751,6 +1774,13 @@ function resolveRuntimeMode(mode) {
|
|
|
1751
1774
|
function allowHostedLocalStore(value) {
|
|
1752
1775
|
return value === true || process.env.HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE === "1";
|
|
1753
1776
|
}
|
|
1777
|
+
function isNfsMount(path) {
|
|
1778
|
+
try {
|
|
1779
|
+
return statfsSync(path).type === NFS_SUPER_MAGIC;
|
|
1780
|
+
} catch {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1754
1784
|
function verifyBackupFile(backupPath) {
|
|
1755
1785
|
const db = new Database(backupPath, { readonly: true });
|
|
1756
1786
|
try {
|
package/dist/cli/index.js
CHANGED
|
@@ -3381,7 +3381,7 @@ function stableJson(value) {
|
|
|
3381
3381
|
}
|
|
3382
3382
|
|
|
3383
3383
|
// src/store.ts
|
|
3384
|
-
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statSync } from "fs";
|
|
3384
|
+
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statfsSync, statSync } from "fs";
|
|
3385
3385
|
import { dirname, join as join2 } from "path";
|
|
3386
3386
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3387
3387
|
import { Database } from "bun:sqlite";
|
|
@@ -3406,6 +3406,8 @@ function ensureUptimeHome() {
|
|
|
3406
3406
|
}
|
|
3407
3407
|
|
|
3408
3408
|
// src/store.ts
|
|
3409
|
+
var DEFAULT_HOSTED_SQLITE_DB_PATH = "/data/uptime/uptime.db";
|
|
3410
|
+
var NFS_SUPER_MAGIC = 26985;
|
|
3409
3411
|
var SECRET_URL_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
|
|
3410
3412
|
var REQUIRED_TABLES = [
|
|
3411
3413
|
"schema_migrations",
|
|
@@ -3442,18 +3444,39 @@ class UptimeStore {
|
|
|
3442
3444
|
this.mode = resolveRuntimeMode(options.mode ?? "local");
|
|
3443
3445
|
const cloudDatabaseUrl = options.cloudDatabaseUrl ?? process.env.HASNA_UPTIME_DATABASE_URL;
|
|
3444
3446
|
if (this.mode === "hosted" && cloudDatabaseUrl) {
|
|
3445
|
-
throw new Error("hosted
|
|
3447
|
+
throw new Error("hosted Postgres adapter is not implemented yet; use HASNA_UPTIME_HOSTED_SQLITE_DB on cloud-mounted storage for the current hosted deployment path");
|
|
3446
3448
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
+
const hostedSqliteDbPath = options.hostedSqliteDbPath ?? process.env.HASNA_UPTIME_HOSTED_SQLITE_DB;
|
|
3450
|
+
if (this.mode === "hosted" && hostedSqliteDbPath) {
|
|
3451
|
+
if (hostedSqliteDbPath === ":memory:" || !hostedSqliteDbPath.startsWith("/")) {
|
|
3452
|
+
throw new Error("HASNA_UPTIME_HOSTED_SQLITE_DB must be an absolute path on mounted cloud storage");
|
|
3453
|
+
}
|
|
3454
|
+
const approvedHostedPath = hostedSqliteDbPath === DEFAULT_HOSTED_SQLITE_DB_PATH;
|
|
3455
|
+
if (!approvedHostedPath && !allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
3456
|
+
throw new Error(`HASNA_UPTIME_HOSTED_SQLITE_DB must be ${DEFAULT_HOSTED_SQLITE_DB_PATH}; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing`);
|
|
3457
|
+
}
|
|
3458
|
+
const verifiedCloudMount = approvedHostedPath && isNfsMount(dirname(hostedSqliteDbPath));
|
|
3459
|
+
if (approvedHostedPath && !verifiedCloudMount && !allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
3460
|
+
throw new Error(`${DEFAULT_HOSTED_SQLITE_DB_PATH} must be on a mounted EFS/NFS filesystem; refusing to create hosted task-local SQLite`);
|
|
3461
|
+
}
|
|
3462
|
+
this.dataMode = verifiedCloudMount ? "hosted-efs-sqlite" : "hosted-local-sqlite";
|
|
3463
|
+
this.dbPath = hostedSqliteDbPath;
|
|
3464
|
+
} else if (this.mode === "hosted") {
|
|
3465
|
+
if (!allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
3466
|
+
throw new Error("hosted mode requires HASNA_UPTIME_HOSTED_SQLITE_DB on mounted cloud storage; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
|
|
3467
|
+
}
|
|
3468
|
+
this.dataMode = "hosted-local-sqlite";
|
|
3469
|
+
this.dbPath = options.dbPath ?? uptimeHostedFallbackDbPath();
|
|
3470
|
+
} else {
|
|
3471
|
+
this.dataMode = "local-sqlite";
|
|
3472
|
+
this.dbPath = options.dbPath ?? uptimeDbPath();
|
|
3449
3473
|
}
|
|
3450
|
-
|
|
3451
|
-
this.dbPath = options.dbPath ?? (this.mode === "hosted" ? uptimeHostedFallbackDbPath() : uptimeDbPath());
|
|
3452
|
-
if (this.dbPath !== ":memory:") {
|
|
3474
|
+
if (this.dbPath !== ":memory:" && this.dataMode !== "hosted-efs-sqlite") {
|
|
3453
3475
|
mkdirSync2(dirname(this.dbPath), { recursive: true });
|
|
3454
3476
|
}
|
|
3455
3477
|
this.db = new Database(this.dbPath, { create: true });
|
|
3456
|
-
this.db.run("PRAGMA journal_mode = WAL");
|
|
3478
|
+
this.db.run(this.dataMode === "hosted-efs-sqlite" ? "PRAGMA journal_mode = DELETE" : "PRAGMA journal_mode = WAL");
|
|
3479
|
+
this.db.run("PRAGMA busy_timeout = 5000");
|
|
3457
3480
|
this.db.run("PRAGMA foreign_keys = ON");
|
|
3458
3481
|
this.migrate();
|
|
3459
3482
|
}
|
|
@@ -4333,6 +4356,13 @@ function resolveRuntimeMode(mode) {
|
|
|
4333
4356
|
function allowHostedLocalStore(value) {
|
|
4334
4357
|
return value === true || process.env.HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE === "1";
|
|
4335
4358
|
}
|
|
4359
|
+
function isNfsMount(path) {
|
|
4360
|
+
try {
|
|
4361
|
+
return statfsSync(path).type === NFS_SUPER_MAGIC;
|
|
4362
|
+
} catch {
|
|
4363
|
+
return false;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4336
4366
|
function verifyBackupFile(backupPath) {
|
|
4337
4367
|
const db = new Database(backupPath, { readonly: true });
|
|
4338
4368
|
try {
|
|
@@ -6388,14 +6418,14 @@ class ApiError extends Error {
|
|
|
6388
6418
|
}
|
|
6389
6419
|
|
|
6390
6420
|
// src/cloud-plan.ts
|
|
6391
|
-
var DEFAULT_ACCOUNT = "
|
|
6421
|
+
var DEFAULT_ACCOUNT = "aws-profile";
|
|
6392
6422
|
var DEFAULT_REGION = "us-east-1";
|
|
6393
6423
|
var DEFAULT_STAGE = "prod";
|
|
6394
6424
|
var DEFAULT_PREFIX = "open-uptime";
|
|
6395
|
-
var DEFAULT_HOSTNAME = "uptime.
|
|
6396
|
-
var DEFAULT_WORKSPACE_ID = "
|
|
6397
|
-
var DEFAULT_VPC_ID = "vpc-
|
|
6398
|
-
var
|
|
6425
|
+
var DEFAULT_HOSTNAME = "uptime.example.com";
|
|
6426
|
+
var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
6427
|
+
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
6428
|
+
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
6399
6429
|
function buildAwsDeploymentPlan(options = {}) {
|
|
6400
6430
|
const region = clean(options.region, DEFAULT_REGION);
|
|
6401
6431
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -6403,36 +6433,39 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6403
6433
|
const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
|
|
6404
6434
|
const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
|
|
6405
6435
|
const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
|
|
6406
|
-
const ecrRepository = clean(options.ecrRepository,
|
|
6407
|
-
const
|
|
6436
|
+
const ecrRepository = clean(options.ecrRepository, prefix);
|
|
6437
|
+
const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
|
|
6438
|
+
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
6408
6439
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
6440
|
+
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
6441
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
|
|
6409
6442
|
const cluster = `${prefix}-${stage}`;
|
|
6410
6443
|
const secrets = {
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
|
|
6444
|
+
appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
|
|
6445
|
+
hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
|
|
6446
|
+
publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
|
|
6447
|
+
privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
|
|
6448
|
+
reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
|
|
6417
6449
|
};
|
|
6418
6450
|
const services = [
|
|
6419
|
-
servicePlan(prefix, stage, "web",
|
|
6451
|
+
servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
|
|
6420
6452
|
HASNA_UPTIME_MODE: "hosted",
|
|
6453
|
+
HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
|
|
6421
6454
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
6422
6455
|
HASNA_UPTIME_HOSTNAME: hostname
|
|
6423
6456
|
}),
|
|
6424
|
-
servicePlan(prefix, stage, "scheduler",
|
|
6457
|
+
servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
|
|
6425
6458
|
HASNA_UPTIME_MODE: "hosted",
|
|
6426
6459
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
6427
6460
|
HASNA_UPTIME_COMPONENT: "scheduler"
|
|
6428
6461
|
}),
|
|
6429
|
-
servicePlan(prefix, stage, "public-probe",
|
|
6462
|
+
servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
|
|
6430
6463
|
HASNA_UPTIME_MODE: "hosted",
|
|
6431
6464
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
6432
6465
|
HASNA_UPTIME_COMPONENT: "public-probe",
|
|
6433
6466
|
HASNA_UPTIME_PROBE_LOCATION: region
|
|
6434
6467
|
}),
|
|
6435
|
-
servicePlan(prefix, stage, "reporter",
|
|
6468
|
+
servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
|
|
6436
6469
|
HASNA_UPTIME_MODE: "hosted",
|
|
6437
6470
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
6438
6471
|
HASNA_UPTIME_COMPONENT: "reporter"
|
|
@@ -6445,7 +6478,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6445
6478
|
];
|
|
6446
6479
|
return {
|
|
6447
6480
|
kind: "open-uptime.aws-deployment-plan",
|
|
6448
|
-
version:
|
|
6481
|
+
version: 2,
|
|
6449
6482
|
generatedAt: new Date().toISOString(),
|
|
6450
6483
|
status: "blocked",
|
|
6451
6484
|
canApply: false,
|
|
@@ -6458,10 +6491,13 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6458
6491
|
mode: "hosted",
|
|
6459
6492
|
resources: {
|
|
6460
6493
|
ecrRepository,
|
|
6494
|
+
imageBuilder: `${prefix}-${stage}-image-builder`,
|
|
6461
6495
|
ecsCluster: cluster,
|
|
6462
6496
|
services,
|
|
6463
6497
|
vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
|
|
6464
|
-
|
|
6498
|
+
efsFileSystem: `${prefix}-${stage}-data`,
|
|
6499
|
+
efsAccessPoint: `${prefix}-${stage}-uptime`,
|
|
6500
|
+
hostedSqliteDbPath,
|
|
6465
6501
|
evidenceBucket,
|
|
6466
6502
|
loadBalancer: `${prefix}-${stage}-alb`,
|
|
6467
6503
|
targetGroups: [`${prefix}-${stage}-web-tg`],
|
|
@@ -6470,42 +6506,54 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6470
6506
|
`${prefix}-${stage}-web-sg`,
|
|
6471
6507
|
`${prefix}-${stage}-scheduler-sg`,
|
|
6472
6508
|
`${prefix}-${stage}-public-probe-sg`,
|
|
6473
|
-
`${prefix}-${stage}-
|
|
6509
|
+
`${prefix}-${stage}-reporter-sg`,
|
|
6510
|
+
`${prefix}-${stage}-migration-sg`,
|
|
6511
|
+
`${prefix}-${stage}-efs-sg`
|
|
6474
6512
|
],
|
|
6475
6513
|
secrets,
|
|
6476
6514
|
logGroups: services.map((service) => service.logGroup),
|
|
6477
6515
|
alarms: [
|
|
6478
6516
|
`${prefix}-${stage}-web-5xx`,
|
|
6479
|
-
`${prefix}-${stage}-
|
|
6480
|
-
`${prefix}-${stage}-probe-stale`,
|
|
6481
|
-
`${prefix}-${stage}-report-delivery-failures`
|
|
6517
|
+
`${prefix}-${stage}-web-unhealthy`
|
|
6482
6518
|
]
|
|
6483
6519
|
},
|
|
6484
6520
|
image: {
|
|
6485
6521
|
repository: ecrRepository,
|
|
6486
6522
|
uri: image,
|
|
6487
|
-
|
|
6523
|
+
dockerfile: "Dockerfile.package",
|
|
6524
|
+
buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
|
|
6488
6525
|
pushCommands: [
|
|
6489
|
-
|
|
6526
|
+
`BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
|
|
6490
6527
|
"BLOCKED: deploy services by immutable image digest, not by mutable tags"
|
|
6491
6528
|
]
|
|
6492
6529
|
},
|
|
6530
|
+
infra: {
|
|
6531
|
+
path: "infra/aws",
|
|
6532
|
+
fmtCommand: "terraform -chdir=infra/aws fmt -check",
|
|
6533
|
+
initCommand: "terraform -chdir=infra/aws init -backend=false",
|
|
6534
|
+
validateCommand: "terraform -chdir=infra/aws validate",
|
|
6535
|
+
planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
|
|
6536
|
+
applyAllowed: false
|
|
6537
|
+
},
|
|
6493
6538
|
runbook: {
|
|
6494
6539
|
preflight: [
|
|
6495
6540
|
`aws sts get-caller-identity --profile ${accountName}`,
|
|
6496
|
-
`aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
|
|
6497
6541
|
`aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
|
|
6542
|
+
`aws efs describe-file-systems --region ${region}`,
|
|
6498
6543
|
"Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
|
|
6499
6544
|
],
|
|
6500
6545
|
provision: [
|
|
6501
6546
|
`Infra PR must declare or update ECR repository ${ecrRepository}.`,
|
|
6547
|
+
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
6502
6548
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
6549
|
+
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
6503
6550
|
`Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
6504
6551
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
6505
6552
|
],
|
|
6506
6553
|
deploy: [
|
|
6507
6554
|
"Build and publish the image only after the Dockerfile/container target is reviewed.",
|
|
6508
|
-
|
|
6555
|
+
`Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
|
|
6556
|
+
"For the EFS SQLite bridge, do not run migration, scheduler, public-probe, or reporter tasks; keep them at desired count 0 until Postgres and cloud leases exist.",
|
|
6509
6557
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
6510
6558
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
6511
6559
|
`Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
@@ -6514,7 +6562,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6514
6562
|
"Keep previous task definition ARNs before each service update.",
|
|
6515
6563
|
"Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
|
|
6516
6564
|
"Disable scheduler/reporter services before data rollback.",
|
|
6517
|
-
"Restore
|
|
6565
|
+
"Restore EFS backup recovery point only after explicit operator approval and audit record."
|
|
6518
6566
|
],
|
|
6519
6567
|
spark01: [
|
|
6520
6568
|
"Create a private probe identity with a caller-managed public key.",
|
|
@@ -6523,19 +6571,19 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6523
6571
|
]
|
|
6524
6572
|
},
|
|
6525
6573
|
blockers: [
|
|
6526
|
-
"The
|
|
6527
|
-
"The
|
|
6528
|
-
"Hosted Postgres storage adapter and migrations are not implemented.",
|
|
6574
|
+
"The infrastructure owner repository was not found in this workspace.",
|
|
6575
|
+
"The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
|
|
6529
6576
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
6530
6577
|
"Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
|
|
6531
6578
|
"Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
|
|
6532
6579
|
],
|
|
6533
6580
|
requiredEvidence: [
|
|
6534
6581
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
6535
|
-
"
|
|
6582
|
+
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
6536
6583
|
"ECS task definitions using secrets.valueFrom only.",
|
|
6537
|
-
"ALB/TLS/DNS/auth denial smokes.",
|
|
6538
|
-
"
|
|
6584
|
+
"ALB/TLS/DNS/auth denial smokes and web alarm checks.",
|
|
6585
|
+
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
6586
|
+
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
6539
6587
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
6540
6588
|
"Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
|
|
6541
6589
|
],
|
|
@@ -6545,7 +6593,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6545
6593
|
hostedLocalSqliteAllowed: false,
|
|
6546
6594
|
notes: [
|
|
6547
6595
|
"This plan generator does not call AWS.",
|
|
6548
|
-
"
|
|
6596
|
+
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
6597
|
+
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
6598
|
+
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
6549
6599
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
|
6550
6600
|
"Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
|
|
6551
6601
|
]
|
|
@@ -6606,7 +6656,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
6606
6656
|
privateKeyInline: false,
|
|
6607
6657
|
tokenInline: false,
|
|
6608
6658
|
notes: [
|
|
6609
|
-
"This config is
|
|
6659
|
+
"This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
|
|
6610
6660
|
"The private key file path is referenced, not embedded.",
|
|
6611
6661
|
"Hosted token or probe auth material must come from the machine secret store, not this generated config."
|
|
6612
6662
|
]
|
|
@@ -6627,7 +6677,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
|
|
|
6627
6677
|
return {
|
|
6628
6678
|
name,
|
|
6629
6679
|
role,
|
|
6630
|
-
desiredCount,
|
|
6680
|
+
desiredCount: 0,
|
|
6681
|
+
targetDesiredCount: desiredCount,
|
|
6631
6682
|
taskRole: `${name}-task-role`,
|
|
6632
6683
|
executionRole: `${prefix}-${stage}-execution-role`,
|
|
6633
6684
|
logGroup: `/ecs/${name}`,
|
|
@@ -6636,7 +6687,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
|
|
|
6636
6687
|
HASNA_UPTIME_IMAGE: image,
|
|
6637
6688
|
...environment
|
|
6638
6689
|
},
|
|
6639
|
-
secrets: role === "
|
|
6690
|
+
secrets: role === "web" ? { APP_ENV: secrets.appEnv, HASNA_UPTIME_HOSTED_TOKEN: secrets.hostedToken } : role === "public-probe" ? { PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { REPORTING_CONFIG: secrets.reporting } : { APP_ENV: secrets.appEnv }
|
|
6640
6691
|
};
|
|
6641
6692
|
}
|
|
6642
6693
|
function clean(value, fallback) {
|
|
@@ -6957,7 +7008,7 @@ program2.command("audit").description("List local audit events").option("--resou
|
|
|
6957
7008
|
}
|
|
6958
7009
|
});
|
|
6959
7010
|
var cloud = program2.command("cloud").description("Generate dry-run cloud deployment and Spark01 configuration artifacts");
|
|
6960
|
-
cloud.command("plan").description("Generate a dry-run AWS deployment plan
|
|
7011
|
+
cloud.command("plan").description("Generate a dry-run AWS deployment plan").option("--account <name>", "AWS account/profile label", "aws-profile").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.example.com").option("--workspace-id <id>", "workspace id", "workspace-id").option("--vpc-id <id>", "target VPC id").option("--hosted-sqlite-db <path>", "hosted SQLite path on the EFS mount").option("--rds-instance-id <id>", "deprecated; ignored until the hosted Postgres adapter exists").option("--database-secret-name <name>", "deprecated; ignored until the hosted Postgres adapter exists").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--runtime-package-version <version>", "published @hasna/uptime version for the AWS image builder").option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
|
|
6961
7012
|
try {
|
|
6962
7013
|
const plan = buildAwsDeploymentPlan({
|
|
6963
7014
|
accountName: opts.account,
|
|
@@ -6966,9 +7017,12 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan for ha
|
|
|
6966
7017
|
hostname: opts.hostname,
|
|
6967
7018
|
workspaceId: opts.workspaceId,
|
|
6968
7019
|
vpcId: opts.vpcId,
|
|
7020
|
+
hostedSqliteDbPath: opts.hostedSqliteDb,
|
|
6969
7021
|
rdsInstanceId: opts.rdsInstanceId,
|
|
7022
|
+
databaseSecretName: opts.databaseSecretName,
|
|
6970
7023
|
ecrRepository: opts.ecrRepository,
|
|
6971
7024
|
image: opts.image,
|
|
7025
|
+
runtimePackageVersion: opts.runtimePackageVersion,
|
|
6972
7026
|
evidenceBucket: opts.evidenceBucket
|
|
6973
7027
|
});
|
|
6974
7028
|
print(plan, renderCloudPlan(plan), opts);
|
|
@@ -6976,7 +7030,7 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan for ha
|
|
|
6976
7030
|
fail(error);
|
|
6977
7031
|
}
|
|
6978
7032
|
});
|
|
6979
|
-
cloud.command("spark01-config").description("Generate Spark01
|
|
7033
|
+
cloud.command("spark01-config").description("Generate Spark01 hosted-targeted private probe preflight configuration").option("--api-url <url>", "hosted Open Uptime API URL", "https://uptime.example.com/api/v1").option("--workspace-id <id>", "workspace id", "workspace-id").option("--probe-id <id>", "cloud registered private probe id").option("--private-key-file <path>", "Spark01 private probe key file", "~/.hasna/uptime/probes/spark01.key.pem").option("--machine-id <id>", "machine id", "spark01").option("--log-level <level>", "probe log level", "info").option("--env", "print shell env file instead of summary text").option("-j, --json", "print JSON").action((opts) => {
|
|
6980
7034
|
try {
|
|
6981
7035
|
const config = buildSpark01CloudConfig({
|
|
6982
7036
|
apiUrl: opts.apiUrl,
|
|
@@ -7176,7 +7230,7 @@ program2.command("restore <backup-path>").description("Restore a verified local
|
|
|
7176
7230
|
fail(error);
|
|
7177
7231
|
}
|
|
7178
7232
|
});
|
|
7179
|
-
program2.command("serve").description("Serve the local API and dashboard").option("--host <host>", "host to bind", "127.0.0.1").option("--port <port>", "port", parseInteger, 3899).option("--check", "run the scheduler while serving").addOption(new Option("--mode <mode>", "runtime mode").choices(["local", "hosted"]).default("local")).option("--api-token <token>", "token required for non-loopback mutation hosts").option("--hosted-token <token>", "scoped hosted-mode token").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("--allow-unsafe-remote-mutations", "allow state-changing requests from non-loopback hosts without a token").option("-j, --json", "print JSON").action((opts) => {
|
|
7233
|
+
program2.command("serve").description("Serve the local API and dashboard").option("--host <host>", "host to bind", "127.0.0.1").option("--port <port>", "port", parseInteger, 3899).option("--check", "run the scheduler while serving").addOption(new Option("--mode <mode>", "runtime mode").choices(["local", "hosted"]).default("local")).option("--api-token <token>", "token required for non-loopback mutation hosts").option("--hosted-token <token>", "scoped hosted-mode token").option("--hosted-sqlite-db <path>", "absolute SQLite database path on hosted cloud-mounted storage").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("--allow-unsafe-remote-mutations", "allow state-changing requests from non-loopback hosts without a token").option("-j, --json", "print JSON").action((opts) => {
|
|
7180
7234
|
try {
|
|
7181
7235
|
const { server } = serveUptime({
|
|
7182
7236
|
host: opts.host,
|
|
@@ -7185,6 +7239,7 @@ program2.command("serve").description("Serve the local API and dashboard").optio
|
|
|
7185
7239
|
mode: opts.mode,
|
|
7186
7240
|
apiToken: opts.apiToken,
|
|
7187
7241
|
hostedToken: opts.hostedToken,
|
|
7242
|
+
hostedSqliteDbPath: opts.hostedSqliteDb,
|
|
7188
7243
|
allowHostedLocalStore: opts.allowHostedLocalStore,
|
|
7189
7244
|
allowUnsafeRemoteMutations: opts.allowUnsafeRemoteMutations
|
|
7190
7245
|
});
|
|
@@ -7355,9 +7410,13 @@ function renderCloudPlan(plan) {
|
|
|
7355
7410
|
`host: ${plan.hostname}`,
|
|
7356
7411
|
`cluster: ${plan.resources.ecsCluster}`,
|
|
7357
7412
|
`image: ${plan.image.uri}`,
|
|
7413
|
+
`image builder: ${plan.resources.imageBuilder}`,
|
|
7414
|
+
`dockerfile: ${plan.image.dockerfile}`,
|
|
7415
|
+
`infra: ${plan.infra.path}`,
|
|
7358
7416
|
`vpc: ${plan.resources.vpcId}`,
|
|
7359
|
-
`
|
|
7360
|
-
`
|
|
7417
|
+
`efs: ${plan.resources.efsFileSystem}`,
|
|
7418
|
+
`hosted sqlite: ${plan.resources.hostedSqliteDbPath}`,
|
|
7419
|
+
`services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}/${service2.targetDesiredCount}`).join(", ")}`,
|
|
7361
7420
|
`evidence bucket: ${plan.resources.evidenceBucket}`,
|
|
7362
7421
|
`blockers: ${plan.blockers.length}`,
|
|
7363
7422
|
"live AWS mutation: false"
|