@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/dist/cloud-plan.d.ts
CHANGED
|
@@ -6,12 +6,16 @@ export interface AwsDeploymentPlanOptions {
|
|
|
6
6
|
hostname?: string;
|
|
7
7
|
workspaceId?: string;
|
|
8
8
|
vpcId?: string;
|
|
9
|
-
rdsInstanceId?: string;
|
|
10
9
|
ecrRepository?: string;
|
|
11
10
|
image?: string;
|
|
12
11
|
evidenceBucket?: string;
|
|
13
|
-
|
|
12
|
+
hostedSqliteDbPath?: string;
|
|
13
|
+
runtimePackageVersion?: string;
|
|
14
|
+
/** @deprecated Postgres is target-state only until the async adapter is implemented. */
|
|
15
|
+
rdsInstanceId?: string;
|
|
16
|
+
/** @deprecated Postgres is target-state only until the async adapter is implemented. */
|
|
14
17
|
databaseSecretName?: string;
|
|
18
|
+
hostedTokenSecretName?: string;
|
|
15
19
|
appEnvSecretName?: string;
|
|
16
20
|
publicProbeSecretName?: string;
|
|
17
21
|
privateProbeSecretName?: string;
|
|
@@ -19,7 +23,7 @@ export interface AwsDeploymentPlanOptions {
|
|
|
19
23
|
}
|
|
20
24
|
export interface AwsDeploymentPlan {
|
|
21
25
|
kind: "open-uptime.aws-deployment-plan";
|
|
22
|
-
version:
|
|
26
|
+
version: 2;
|
|
23
27
|
generatedAt: string;
|
|
24
28
|
status: "blocked";
|
|
25
29
|
canApply: false;
|
|
@@ -32,10 +36,13 @@ export interface AwsDeploymentPlan {
|
|
|
32
36
|
mode: "hosted";
|
|
33
37
|
resources: {
|
|
34
38
|
ecrRepository: string;
|
|
39
|
+
imageBuilder: string;
|
|
35
40
|
ecsCluster: string;
|
|
36
41
|
services: AwsServicePlan[];
|
|
37
42
|
vpcId: string;
|
|
38
|
-
|
|
43
|
+
efsFileSystem: string;
|
|
44
|
+
efsAccessPoint: string;
|
|
45
|
+
hostedSqliteDbPath: string;
|
|
39
46
|
evidenceBucket: string;
|
|
40
47
|
loadBalancer: string;
|
|
41
48
|
targetGroups: string[];
|
|
@@ -47,9 +54,18 @@ export interface AwsDeploymentPlan {
|
|
|
47
54
|
image: {
|
|
48
55
|
repository: string;
|
|
49
56
|
uri: string;
|
|
57
|
+
dockerfile: string;
|
|
50
58
|
buildCommand: string;
|
|
51
59
|
pushCommands: string[];
|
|
52
60
|
};
|
|
61
|
+
infra: {
|
|
62
|
+
path: string;
|
|
63
|
+
fmtCommand: string;
|
|
64
|
+
initCommand: string;
|
|
65
|
+
validateCommand: string;
|
|
66
|
+
planCommand: string;
|
|
67
|
+
applyAllowed: false;
|
|
68
|
+
};
|
|
53
69
|
runbook: {
|
|
54
70
|
preflight: string[];
|
|
55
71
|
provision: string[];
|
|
@@ -70,6 +86,7 @@ export interface AwsServicePlan {
|
|
|
70
86
|
name: string;
|
|
71
87
|
role: "web" | "scheduler" | "public-probe" | "reporter" | "migration";
|
|
72
88
|
desiredCount: number;
|
|
89
|
+
targetDesiredCount: number;
|
|
73
90
|
taskRole: string;
|
|
74
91
|
executionRole: string;
|
|
75
92
|
logGroup: string;
|
package/dist/cloud-plan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,
|
|
1
|
+
{"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE;QACT,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,cAAc,EAAE,CAAC;QAC3B,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;IACF,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,KAAK,CAAC;KACrB,CAAC;IACF,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE;QACN,eAAe,EAAE,KAAK,CAAC;QACvB,gBAAgB,EAAE,KAAK,CAAC;QACxB,wBAAwB,EAAE,KAAK,CAAC;QAChC,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,kCAAkC,CAAC;IACzC,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE;QACN,gBAAgB,EAAE,KAAK,CAAC;QACxB,WAAW,EAAE,KAAK,CAAC;QACnB,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAWD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CAgLhG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,yBAA8B,GAAG,kBAAkB,CA2DnG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CASnE"}
|
package/dist/cloud-plan.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/cloud-plan.ts
|
|
3
|
-
var DEFAULT_ACCOUNT = "
|
|
3
|
+
var DEFAULT_ACCOUNT = "aws-profile";
|
|
4
4
|
var DEFAULT_REGION = "us-east-1";
|
|
5
5
|
var DEFAULT_STAGE = "prod";
|
|
6
6
|
var DEFAULT_PREFIX = "open-uptime";
|
|
7
|
-
var DEFAULT_HOSTNAME = "uptime.
|
|
8
|
-
var DEFAULT_WORKSPACE_ID = "
|
|
9
|
-
var DEFAULT_VPC_ID = "vpc-
|
|
10
|
-
var
|
|
7
|
+
var DEFAULT_HOSTNAME = "uptime.example.com";
|
|
8
|
+
var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
9
|
+
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
10
|
+
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
11
11
|
function buildAwsDeploymentPlan(options = {}) {
|
|
12
12
|
const region = clean(options.region, DEFAULT_REGION);
|
|
13
13
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -15,36 +15,39 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
15
15
|
const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
|
|
16
16
|
const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
|
|
17
17
|
const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
|
|
18
|
-
const ecrRepository = clean(options.ecrRepository,
|
|
19
|
-
const
|
|
18
|
+
const ecrRepository = clean(options.ecrRepository, prefix);
|
|
19
|
+
const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
|
|
20
|
+
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
20
21
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
22
|
+
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
23
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
|
|
21
24
|
const cluster = `${prefix}-${stage}`;
|
|
22
25
|
const secrets = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
|
|
26
|
+
appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
|
|
27
|
+
hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
|
|
28
|
+
publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
|
|
29
|
+
privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
|
|
30
|
+
reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
|
|
29
31
|
};
|
|
30
32
|
const services = [
|
|
31
|
-
servicePlan(prefix, stage, "web",
|
|
33
|
+
servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
|
|
32
34
|
HASNA_UPTIME_MODE: "hosted",
|
|
35
|
+
HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
|
|
33
36
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
34
37
|
HASNA_UPTIME_HOSTNAME: hostname
|
|
35
38
|
}),
|
|
36
|
-
servicePlan(prefix, stage, "scheduler",
|
|
39
|
+
servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
|
|
37
40
|
HASNA_UPTIME_MODE: "hosted",
|
|
38
41
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
39
42
|
HASNA_UPTIME_COMPONENT: "scheduler"
|
|
40
43
|
}),
|
|
41
|
-
servicePlan(prefix, stage, "public-probe",
|
|
44
|
+
servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
|
|
42
45
|
HASNA_UPTIME_MODE: "hosted",
|
|
43
46
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
44
47
|
HASNA_UPTIME_COMPONENT: "public-probe",
|
|
45
48
|
HASNA_UPTIME_PROBE_LOCATION: region
|
|
46
49
|
}),
|
|
47
|
-
servicePlan(prefix, stage, "reporter",
|
|
50
|
+
servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
|
|
48
51
|
HASNA_UPTIME_MODE: "hosted",
|
|
49
52
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
50
53
|
HASNA_UPTIME_COMPONENT: "reporter"
|
|
@@ -57,7 +60,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
57
60
|
];
|
|
58
61
|
return {
|
|
59
62
|
kind: "open-uptime.aws-deployment-plan",
|
|
60
|
-
version:
|
|
63
|
+
version: 2,
|
|
61
64
|
generatedAt: new Date().toISOString(),
|
|
62
65
|
status: "blocked",
|
|
63
66
|
canApply: false,
|
|
@@ -70,10 +73,13 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
70
73
|
mode: "hosted",
|
|
71
74
|
resources: {
|
|
72
75
|
ecrRepository,
|
|
76
|
+
imageBuilder: `${prefix}-${stage}-image-builder`,
|
|
73
77
|
ecsCluster: cluster,
|
|
74
78
|
services,
|
|
75
79
|
vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
|
|
76
|
-
|
|
80
|
+
efsFileSystem: `${prefix}-${stage}-data`,
|
|
81
|
+
efsAccessPoint: `${prefix}-${stage}-uptime`,
|
|
82
|
+
hostedSqliteDbPath,
|
|
77
83
|
evidenceBucket,
|
|
78
84
|
loadBalancer: `${prefix}-${stage}-alb`,
|
|
79
85
|
targetGroups: [`${prefix}-${stage}-web-tg`],
|
|
@@ -82,42 +88,54 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
82
88
|
`${prefix}-${stage}-web-sg`,
|
|
83
89
|
`${prefix}-${stage}-scheduler-sg`,
|
|
84
90
|
`${prefix}-${stage}-public-probe-sg`,
|
|
85
|
-
`${prefix}-${stage}-
|
|
91
|
+
`${prefix}-${stage}-reporter-sg`,
|
|
92
|
+
`${prefix}-${stage}-migration-sg`,
|
|
93
|
+
`${prefix}-${stage}-efs-sg`
|
|
86
94
|
],
|
|
87
95
|
secrets,
|
|
88
96
|
logGroups: services.map((service) => service.logGroup),
|
|
89
97
|
alarms: [
|
|
90
98
|
`${prefix}-${stage}-web-5xx`,
|
|
91
|
-
`${prefix}-${stage}-
|
|
92
|
-
`${prefix}-${stage}-probe-stale`,
|
|
93
|
-
`${prefix}-${stage}-report-delivery-failures`
|
|
99
|
+
`${prefix}-${stage}-web-unhealthy`
|
|
94
100
|
]
|
|
95
101
|
},
|
|
96
102
|
image: {
|
|
97
103
|
repository: ecrRepository,
|
|
98
104
|
uri: image,
|
|
99
|
-
|
|
105
|
+
dockerfile: "Dockerfile.package",
|
|
106
|
+
buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
|
|
100
107
|
pushCommands: [
|
|
101
|
-
|
|
108
|
+
`BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
|
|
102
109
|
"BLOCKED: deploy services by immutable image digest, not by mutable tags"
|
|
103
110
|
]
|
|
104
111
|
},
|
|
112
|
+
infra: {
|
|
113
|
+
path: "infra/aws",
|
|
114
|
+
fmtCommand: "terraform -chdir=infra/aws fmt -check",
|
|
115
|
+
initCommand: "terraform -chdir=infra/aws init -backend=false",
|
|
116
|
+
validateCommand: "terraform -chdir=infra/aws validate",
|
|
117
|
+
planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
|
|
118
|
+
applyAllowed: false
|
|
119
|
+
},
|
|
105
120
|
runbook: {
|
|
106
121
|
preflight: [
|
|
107
122
|
`aws sts get-caller-identity --profile ${accountName}`,
|
|
108
|
-
`aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
|
|
109
123
|
`aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
|
|
124
|
+
`aws efs describe-file-systems --region ${region}`,
|
|
110
125
|
"Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
|
|
111
126
|
],
|
|
112
127
|
provision: [
|
|
113
128
|
`Infra PR must declare or update ECR repository ${ecrRepository}.`,
|
|
129
|
+
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
114
130
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
131
|
+
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
115
132
|
`Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
116
133
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
117
134
|
],
|
|
118
135
|
deploy: [
|
|
119
136
|
"Build and publish the image only after the Dockerfile/container target is reviewed.",
|
|
120
|
-
|
|
137
|
+
`Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
|
|
138
|
+
"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.",
|
|
121
139
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
122
140
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
123
141
|
`Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
@@ -126,7 +144,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
126
144
|
"Keep previous task definition ARNs before each service update.",
|
|
127
145
|
"Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
|
|
128
146
|
"Disable scheduler/reporter services before data rollback.",
|
|
129
|
-
"Restore
|
|
147
|
+
"Restore EFS backup recovery point only after explicit operator approval and audit record."
|
|
130
148
|
],
|
|
131
149
|
spark01: [
|
|
132
150
|
"Create a private probe identity with a caller-managed public key.",
|
|
@@ -135,19 +153,19 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
135
153
|
]
|
|
136
154
|
},
|
|
137
155
|
blockers: [
|
|
138
|
-
"The
|
|
139
|
-
"The
|
|
140
|
-
"Hosted Postgres storage adapter and migrations are not implemented.",
|
|
156
|
+
"The infrastructure owner repository was not found in this workspace.",
|
|
157
|
+
"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.",
|
|
141
158
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
142
159
|
"Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
|
|
143
160
|
"Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
|
|
144
161
|
],
|
|
145
162
|
requiredEvidence: [
|
|
146
163
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
147
|
-
"
|
|
164
|
+
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
148
165
|
"ECS task definitions using secrets.valueFrom only.",
|
|
149
|
-
"ALB/TLS/DNS/auth denial smokes.",
|
|
150
|
-
"
|
|
166
|
+
"ALB/TLS/DNS/auth denial smokes and web alarm checks.",
|
|
167
|
+
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
168
|
+
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
151
169
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
152
170
|
"Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
|
|
153
171
|
],
|
|
@@ -157,7 +175,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
157
175
|
hostedLocalSqliteAllowed: false,
|
|
158
176
|
notes: [
|
|
159
177
|
"This plan generator does not call AWS.",
|
|
160
|
-
"
|
|
178
|
+
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
179
|
+
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
180
|
+
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
161
181
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
|
162
182
|
"Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
|
|
163
183
|
]
|
|
@@ -218,7 +238,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
218
238
|
privateKeyInline: false,
|
|
219
239
|
tokenInline: false,
|
|
220
240
|
notes: [
|
|
221
|
-
"This config is
|
|
241
|
+
"This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
|
|
222
242
|
"The private key file path is referenced, not embedded.",
|
|
223
243
|
"Hosted token or probe auth material must come from the machine secret store, not this generated config."
|
|
224
244
|
]
|
|
@@ -239,7 +259,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
|
|
|
239
259
|
return {
|
|
240
260
|
name,
|
|
241
261
|
role,
|
|
242
|
-
desiredCount,
|
|
262
|
+
desiredCount: 0,
|
|
263
|
+
targetDesiredCount: desiredCount,
|
|
243
264
|
taskRole: `${name}-task-role`,
|
|
244
265
|
executionRole: `${prefix}-${stage}-execution-role`,
|
|
245
266
|
logGroup: `/ecs/${name}`,
|
|
@@ -248,7 +269,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
|
|
|
248
269
|
HASNA_UPTIME_IMAGE: image,
|
|
249
270
|
...environment
|
|
250
271
|
},
|
|
251
|
-
secrets: role === "
|
|
272
|
+
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 }
|
|
252
273
|
};
|
|
253
274
|
}
|
|
254
275
|
function clean(value, fallback) {
|
package/dist/index.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 {
|
|
@@ -3791,14 +3821,14 @@ class ApiError extends Error {
|
|
|
3791
3821
|
}
|
|
3792
3822
|
|
|
3793
3823
|
// src/cloud-plan.ts
|
|
3794
|
-
var DEFAULT_ACCOUNT = "
|
|
3824
|
+
var DEFAULT_ACCOUNT = "aws-profile";
|
|
3795
3825
|
var DEFAULT_REGION = "us-east-1";
|
|
3796
3826
|
var DEFAULT_STAGE = "prod";
|
|
3797
3827
|
var DEFAULT_PREFIX = "open-uptime";
|
|
3798
|
-
var DEFAULT_HOSTNAME = "uptime.
|
|
3799
|
-
var DEFAULT_WORKSPACE_ID = "
|
|
3800
|
-
var DEFAULT_VPC_ID = "vpc-
|
|
3801
|
-
var
|
|
3828
|
+
var DEFAULT_HOSTNAME = "uptime.example.com";
|
|
3829
|
+
var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
3830
|
+
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
3831
|
+
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
3802
3832
|
function buildAwsDeploymentPlan(options = {}) {
|
|
3803
3833
|
const region = clean(options.region, DEFAULT_REGION);
|
|
3804
3834
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -3806,36 +3836,39 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3806
3836
|
const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
|
|
3807
3837
|
const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
|
|
3808
3838
|
const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
|
|
3809
|
-
const ecrRepository = clean(options.ecrRepository,
|
|
3810
|
-
const
|
|
3839
|
+
const ecrRepository = clean(options.ecrRepository, prefix);
|
|
3840
|
+
const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
|
|
3841
|
+
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
3811
3842
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
3843
|
+
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
3844
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
|
|
3812
3845
|
const cluster = `${prefix}-${stage}`;
|
|
3813
3846
|
const secrets = {
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
|
|
3847
|
+
appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
|
|
3848
|
+
hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
|
|
3849
|
+
publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
|
|
3850
|
+
privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
|
|
3851
|
+
reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
|
|
3820
3852
|
};
|
|
3821
3853
|
const services = [
|
|
3822
|
-
servicePlan(prefix, stage, "web",
|
|
3854
|
+
servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
|
|
3823
3855
|
HASNA_UPTIME_MODE: "hosted",
|
|
3856
|
+
HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
|
|
3824
3857
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
3825
3858
|
HASNA_UPTIME_HOSTNAME: hostname
|
|
3826
3859
|
}),
|
|
3827
|
-
servicePlan(prefix, stage, "scheduler",
|
|
3860
|
+
servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
|
|
3828
3861
|
HASNA_UPTIME_MODE: "hosted",
|
|
3829
3862
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
3830
3863
|
HASNA_UPTIME_COMPONENT: "scheduler"
|
|
3831
3864
|
}),
|
|
3832
|
-
servicePlan(prefix, stage, "public-probe",
|
|
3865
|
+
servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
|
|
3833
3866
|
HASNA_UPTIME_MODE: "hosted",
|
|
3834
3867
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
3835
3868
|
HASNA_UPTIME_COMPONENT: "public-probe",
|
|
3836
3869
|
HASNA_UPTIME_PROBE_LOCATION: region
|
|
3837
3870
|
}),
|
|
3838
|
-
servicePlan(prefix, stage, "reporter",
|
|
3871
|
+
servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
|
|
3839
3872
|
HASNA_UPTIME_MODE: "hosted",
|
|
3840
3873
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
3841
3874
|
HASNA_UPTIME_COMPONENT: "reporter"
|
|
@@ -3848,7 +3881,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3848
3881
|
];
|
|
3849
3882
|
return {
|
|
3850
3883
|
kind: "open-uptime.aws-deployment-plan",
|
|
3851
|
-
version:
|
|
3884
|
+
version: 2,
|
|
3852
3885
|
generatedAt: new Date().toISOString(),
|
|
3853
3886
|
status: "blocked",
|
|
3854
3887
|
canApply: false,
|
|
@@ -3861,10 +3894,13 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3861
3894
|
mode: "hosted",
|
|
3862
3895
|
resources: {
|
|
3863
3896
|
ecrRepository,
|
|
3897
|
+
imageBuilder: `${prefix}-${stage}-image-builder`,
|
|
3864
3898
|
ecsCluster: cluster,
|
|
3865
3899
|
services,
|
|
3866
3900
|
vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
|
|
3867
|
-
|
|
3901
|
+
efsFileSystem: `${prefix}-${stage}-data`,
|
|
3902
|
+
efsAccessPoint: `${prefix}-${stage}-uptime`,
|
|
3903
|
+
hostedSqliteDbPath,
|
|
3868
3904
|
evidenceBucket,
|
|
3869
3905
|
loadBalancer: `${prefix}-${stage}-alb`,
|
|
3870
3906
|
targetGroups: [`${prefix}-${stage}-web-tg`],
|
|
@@ -3873,42 +3909,54 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3873
3909
|
`${prefix}-${stage}-web-sg`,
|
|
3874
3910
|
`${prefix}-${stage}-scheduler-sg`,
|
|
3875
3911
|
`${prefix}-${stage}-public-probe-sg`,
|
|
3876
|
-
`${prefix}-${stage}-
|
|
3912
|
+
`${prefix}-${stage}-reporter-sg`,
|
|
3913
|
+
`${prefix}-${stage}-migration-sg`,
|
|
3914
|
+
`${prefix}-${stage}-efs-sg`
|
|
3877
3915
|
],
|
|
3878
3916
|
secrets,
|
|
3879
3917
|
logGroups: services.map((service) => service.logGroup),
|
|
3880
3918
|
alarms: [
|
|
3881
3919
|
`${prefix}-${stage}-web-5xx`,
|
|
3882
|
-
`${prefix}-${stage}-
|
|
3883
|
-
`${prefix}-${stage}-probe-stale`,
|
|
3884
|
-
`${prefix}-${stage}-report-delivery-failures`
|
|
3920
|
+
`${prefix}-${stage}-web-unhealthy`
|
|
3885
3921
|
]
|
|
3886
3922
|
},
|
|
3887
3923
|
image: {
|
|
3888
3924
|
repository: ecrRepository,
|
|
3889
3925
|
uri: image,
|
|
3890
|
-
|
|
3926
|
+
dockerfile: "Dockerfile.package",
|
|
3927
|
+
buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
|
|
3891
3928
|
pushCommands: [
|
|
3892
|
-
|
|
3929
|
+
`BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
|
|
3893
3930
|
"BLOCKED: deploy services by immutable image digest, not by mutable tags"
|
|
3894
3931
|
]
|
|
3895
3932
|
},
|
|
3933
|
+
infra: {
|
|
3934
|
+
path: "infra/aws",
|
|
3935
|
+
fmtCommand: "terraform -chdir=infra/aws fmt -check",
|
|
3936
|
+
initCommand: "terraform -chdir=infra/aws init -backend=false",
|
|
3937
|
+
validateCommand: "terraform -chdir=infra/aws validate",
|
|
3938
|
+
planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
|
|
3939
|
+
applyAllowed: false
|
|
3940
|
+
},
|
|
3896
3941
|
runbook: {
|
|
3897
3942
|
preflight: [
|
|
3898
3943
|
`aws sts get-caller-identity --profile ${accountName}`,
|
|
3899
|
-
`aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
|
|
3900
3944
|
`aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
|
|
3945
|
+
`aws efs describe-file-systems --region ${region}`,
|
|
3901
3946
|
"Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
|
|
3902
3947
|
],
|
|
3903
3948
|
provision: [
|
|
3904
3949
|
`Infra PR must declare or update ECR repository ${ecrRepository}.`,
|
|
3950
|
+
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
3905
3951
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
3952
|
+
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
3906
3953
|
`Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
3907
3954
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
3908
3955
|
],
|
|
3909
3956
|
deploy: [
|
|
3910
3957
|
"Build and publish the image only after the Dockerfile/container target is reviewed.",
|
|
3911
|
-
|
|
3958
|
+
`Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
|
|
3959
|
+
"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.",
|
|
3912
3960
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
3913
3961
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
3914
3962
|
`Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
@@ -3917,7 +3965,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3917
3965
|
"Keep previous task definition ARNs before each service update.",
|
|
3918
3966
|
"Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
|
|
3919
3967
|
"Disable scheduler/reporter services before data rollback.",
|
|
3920
|
-
"Restore
|
|
3968
|
+
"Restore EFS backup recovery point only after explicit operator approval and audit record."
|
|
3921
3969
|
],
|
|
3922
3970
|
spark01: [
|
|
3923
3971
|
"Create a private probe identity with a caller-managed public key.",
|
|
@@ -3926,19 +3974,19 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3926
3974
|
]
|
|
3927
3975
|
},
|
|
3928
3976
|
blockers: [
|
|
3929
|
-
"The
|
|
3930
|
-
"The
|
|
3931
|
-
"Hosted Postgres storage adapter and migrations are not implemented.",
|
|
3977
|
+
"The infrastructure owner repository was not found in this workspace.",
|
|
3978
|
+
"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.",
|
|
3932
3979
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
3933
3980
|
"Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
|
|
3934
3981
|
"Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
|
|
3935
3982
|
],
|
|
3936
3983
|
requiredEvidence: [
|
|
3937
3984
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
3938
|
-
"
|
|
3985
|
+
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
3939
3986
|
"ECS task definitions using secrets.valueFrom only.",
|
|
3940
|
-
"ALB/TLS/DNS/auth denial smokes.",
|
|
3941
|
-
"
|
|
3987
|
+
"ALB/TLS/DNS/auth denial smokes and web alarm checks.",
|
|
3988
|
+
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
3989
|
+
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
3942
3990
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
3943
3991
|
"Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
|
|
3944
3992
|
],
|
|
@@ -3948,7 +3996,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3948
3996
|
hostedLocalSqliteAllowed: false,
|
|
3949
3997
|
notes: [
|
|
3950
3998
|
"This plan generator does not call AWS.",
|
|
3951
|
-
"
|
|
3999
|
+
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
4000
|
+
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
4001
|
+
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
3952
4002
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
|
3953
4003
|
"Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
|
|
3954
4004
|
]
|
|
@@ -4009,7 +4059,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4009
4059
|
privateKeyInline: false,
|
|
4010
4060
|
tokenInline: false,
|
|
4011
4061
|
notes: [
|
|
4012
|
-
"This config is
|
|
4062
|
+
"This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
|
|
4013
4063
|
"The private key file path is referenced, not embedded.",
|
|
4014
4064
|
"Hosted token or probe auth material must come from the machine secret store, not this generated config."
|
|
4015
4065
|
]
|
|
@@ -4030,7 +4080,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
|
|
|
4030
4080
|
return {
|
|
4031
4081
|
name,
|
|
4032
4082
|
role,
|
|
4033
|
-
desiredCount,
|
|
4083
|
+
desiredCount: 0,
|
|
4084
|
+
targetDesiredCount: desiredCount,
|
|
4034
4085
|
taskRole: `${name}-task-role`,
|
|
4035
4086
|
executionRole: `${prefix}-${stage}-execution-role`,
|
|
4036
4087
|
logGroup: `/ecs/${name}`,
|
|
@@ -4039,7 +4090,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
|
|
|
4039
4090
|
HASNA_UPTIME_IMAGE: image,
|
|
4040
4091
|
...environment
|
|
4041
4092
|
},
|
|
4042
|
-
secrets: role === "
|
|
4093
|
+
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 }
|
|
4043
4094
|
};
|
|
4044
4095
|
}
|
|
4045
4096
|
function clean(value, fallback) {
|