puls-dev 0.1.0 → 0.1.8
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/README.md +10 -8
- package/dist/core/checker.d.ts +1 -1
- package/dist/core/checker.js +88 -56
- package/dist/core/config.test.d.ts +1 -0
- package/dist/core/config.test.js +21 -0
- package/dist/core/decorators.js +8 -2
- package/dist/core/output.test.d.ts +1 -0
- package/dist/core/output.test.js +18 -0
- package/dist/core/resource.js +2 -2
- package/dist/core/stack.d.ts +1 -1
- package/dist/core/stack.js +2 -2
- package/dist/providers/aws/acm.d.ts +1 -1
- package/dist/providers/aws/acm.js +27 -23
- package/dist/providers/aws/api.d.ts +14 -14
- package/dist/providers/aws/api.js +21 -21
- package/dist/providers/aws/apigateway.d.ts +2 -2
- package/dist/providers/aws/apigateway.js +33 -29
- package/dist/providers/aws/cloudfront.d.ts +3 -3
- package/dist/providers/aws/cloudfront.js +49 -34
- package/dist/providers/aws/fargate.d.ts +2 -2
- package/dist/providers/aws/fargate.js +99 -52
- package/dist/providers/aws/lambda.d.ts +2 -2
- package/dist/providers/aws/lambda.js +63 -32
- package/dist/providers/aws/rds.d.ts +1 -1
- package/dist/providers/aws/rds.js +77 -39
- package/dist/providers/aws/route53.d.ts +5 -5
- package/dist/providers/aws/route53.js +42 -35
- package/dist/providers/aws/s3.d.ts +2 -2
- package/dist/providers/aws/s3.js +40 -33
- package/dist/providers/aws/secrets.js +15 -7
- package/dist/providers/aws/sqs.d.ts +1 -1
- package/dist/providers/aws/sqs.js +47 -23
- package/dist/providers/do/domain.d.ts +4 -4
- package/dist/providers/do/domain.js +15 -11
- package/dist/providers/firebase/auth.d.ts +1 -1
- package/dist/providers/firebase/auth.js +65 -33
- package/dist/providers/firebase/firestore.d.ts +2 -2
- package/dist/providers/firebase/firestore.js +45 -28
- package/dist/providers/firebase/functions.d.ts +1 -1
- package/dist/providers/firebase/functions.js +75 -42
- package/dist/providers/firebase/hosting.d.ts +1 -1
- package/dist/providers/firebase/hosting.js +92 -52
- package/dist/providers/firebase/remoteconfig.d.ts +1 -1
- package/dist/providers/firebase/remoteconfig.js +42 -33
- package/dist/providers/firebase/storage.d.ts +1 -1
- package/dist/providers/firebase/storage.js +38 -24
- package/dist/providers/proxmox/vm.d.ts +1 -1
- package/dist/providers/proxmox/vm.js +43 -24
- package/dist/types/aws.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Puls-dev
|
|
2
2
|
|
|
3
|
-
Intent-driven infrastructure-as-code. Describe what you want — Puls figures out create, update, or skip
|
|
3
|
+
**Intent-driven infrastructure-as-code. Describe what you want — Puls figures out create, update, or skip.**
|
|
4
|
+
|
|
5
|
+
[Live Documentation](https://puls-docs.web.app/) | Discord: **pulsdev.io** ([Join](https://discord.gg/CjgRayuH))
|
|
4
6
|
|
|
5
7
|
```typescript
|
|
6
8
|
@Deploy({ proxmox: CONFIG.STAGING })
|
|
@@ -14,7 +16,7 @@ class GameInfra extends Stack {
|
|
|
14
16
|
}
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
No state files. No plan step. Runs against real APIs
|
|
19
|
+
No state files. No plan step. Runs against real APIs - idempotent by default.
|
|
18
20
|
|
|
19
21
|
---
|
|
20
22
|
|
|
@@ -28,7 +30,7 @@ Declare resource → Discovery fires immediately (async)
|
|
|
28
30
|
→ deploy() awaits discovery, diffs, acts
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
Running the same stack twice is always safe
|
|
33
|
+
Running the same stack twice is always safe - existing resources are detected and skipped.
|
|
32
34
|
|
|
33
35
|
---
|
|
34
36
|
|
|
@@ -47,7 +49,7 @@ Running the same stack twice is always safe — existing resources are detected
|
|
|
47
49
|
### DigitalOcean
|
|
48
50
|
|
|
49
51
|
```typescript
|
|
50
|
-
import { DO, DO_TYPES, Stack, Deploy } from "puls";
|
|
52
|
+
import { DO, DO_TYPES, Stack, Deploy } from "puls-dev";
|
|
51
53
|
const { SIZE, REGION } = DO_TYPES;
|
|
52
54
|
|
|
53
55
|
@Deploy({ token: process.env.DO_TOKEN! })
|
|
@@ -60,7 +62,7 @@ class Production extends Stack {
|
|
|
60
62
|
### AWS
|
|
61
63
|
|
|
62
64
|
```typescript
|
|
63
|
-
import { AWS, AWS_TYPES, Stack, Deploy } from "puls";
|
|
65
|
+
import { AWS, AWS_TYPES, Stack, Deploy } from "puls-dev";
|
|
64
66
|
const { DISTRO, BUCKET, DOMAIN_REGISTER, REGION } = AWS_TYPES;
|
|
65
67
|
|
|
66
68
|
@Deploy({ region: REGION.US_EAST_1 })
|
|
@@ -68,7 +70,7 @@ class CDNStack extends Stack {
|
|
|
68
70
|
domain = AWS.Route53().randomDomain().register(DOMAIN_REGISTER).withWildcardSSL();
|
|
69
71
|
|
|
70
72
|
cdn = AWS.CloudFront(`CDN-${this.domain.zoneName.slice(0, 8)}`)
|
|
71
|
-
.copyFrom(DISTRO.
|
|
73
|
+
.copyFrom(DISTRO.CDN)
|
|
72
74
|
.forDomain(this.domain, ["ec", "nc"]);
|
|
73
75
|
|
|
74
76
|
bucket = AWS.S3(BUCKET.NLC_GAMES_UREG)
|
|
@@ -80,7 +82,7 @@ class CDNStack extends Stack {
|
|
|
80
82
|
### Proxmox
|
|
81
83
|
|
|
82
84
|
```typescript
|
|
83
|
-
import { Proxmox, PROXMOX_TYPES, Stack, Deploy, Protected } from "puls";
|
|
85
|
+
import { Proxmox, PROXMOX_TYPES, Stack, Deploy, Protected } from "puls-dev";
|
|
84
86
|
const { CONFIG, OS, KEYS } = PROXMOX_TYPES;
|
|
85
87
|
|
|
86
88
|
@Deploy({ proxmox: CONFIG.STAGING })
|
|
@@ -121,7 +123,7 @@ See [docs/decorators.md](docs/decorators.md) for full reference.
|
|
|
121
123
|
## Running
|
|
122
124
|
|
|
123
125
|
```bash
|
|
124
|
-
npm install
|
|
126
|
+
npm install puls-dev
|
|
125
127
|
npx tsx your-stack.ts
|
|
126
128
|
```
|
|
127
129
|
|
package/dist/core/checker.d.ts
CHANGED
package/dist/core/checker.js
CHANGED
|
@@ -1,108 +1,128 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
|
-
import { Config } from
|
|
3
|
-
import { listProxmoxVMs } from
|
|
4
|
-
import { listDoResources } from
|
|
5
|
-
import { listAwsResources } from
|
|
2
|
+
import { Config } from "./config.js";
|
|
3
|
+
import { listProxmoxVMs } from "../providers/proxmox/list.js";
|
|
4
|
+
import { listDoResources } from "../providers/do/list.js";
|
|
5
|
+
import { listAwsResources } from "../providers/aws/list.js";
|
|
6
6
|
function clip(text, width) {
|
|
7
|
-
return text.length > width
|
|
7
|
+
return text.length > width
|
|
8
|
+
? text.slice(0, width - 1) + "…"
|
|
9
|
+
: text.padEnd(width);
|
|
8
10
|
}
|
|
9
11
|
function printSection(title, rows, cols) {
|
|
10
12
|
const colsWidth = cols.reduce((s, c) => s + c.width, 0) + (cols.length - 1) * 2;
|
|
11
13
|
const innerWidth = Math.max(colsWidth + 4, title.length + 4);
|
|
12
|
-
const bar = (l, r) => ` ${l}${
|
|
14
|
+
const bar = (l, r) => ` ${l}${"─".repeat(innerWidth)}${r}`;
|
|
13
15
|
const row = (content) => ` │ ${content.padEnd(innerWidth - 4)} │`;
|
|
14
|
-
console.log(
|
|
15
|
-
console.log(bar(
|
|
16
|
+
console.log("");
|
|
17
|
+
console.log(bar("┌", "┐"));
|
|
16
18
|
console.log(row(title));
|
|
17
|
-
console.log(bar(
|
|
19
|
+
console.log(bar("├", "┤"));
|
|
18
20
|
if (rows.length === 0) {
|
|
19
|
-
console.log(row(
|
|
21
|
+
console.log(row("(none)"));
|
|
20
22
|
}
|
|
21
23
|
else {
|
|
22
|
-
const headerLine = cols
|
|
24
|
+
const headerLine = cols
|
|
25
|
+
.map((c) => clip(c.header.toUpperCase(), c.width))
|
|
26
|
+
.join(" ");
|
|
23
27
|
console.log(row(headerLine));
|
|
24
|
-
console.log(bar(
|
|
28
|
+
console.log(bar("├", "┤"));
|
|
25
29
|
for (const r of rows) {
|
|
26
|
-
console.log(row(cols.map((c) => clip(c.render(r), c.width)).join(
|
|
30
|
+
console.log(row(cols.map((c) => clip(c.render(r), c.width)).join(" ")));
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
|
-
console.log(bar(
|
|
33
|
+
console.log(bar("└", "┘"));
|
|
30
34
|
}
|
|
31
35
|
// ─── Per-provider renderers ───────────────────────────────────────────────────
|
|
32
36
|
function renderProxmox(inv) {
|
|
33
|
-
const running = inv.vms.filter((v) => v.status ===
|
|
34
|
-
printSection(`Proxmox · ${inv.vms.length} VM${inv.vms.length !== 1 ?
|
|
35
|
-
{ header:
|
|
36
|
-
{ header:
|
|
37
|
-
{ header:
|
|
38
|
-
{ header:
|
|
39
|
-
{
|
|
40
|
-
|
|
37
|
+
const running = inv.vms.filter((v) => v.status === "running").length;
|
|
38
|
+
printSection(`Proxmox · ${inv.vms.length} VM${inv.vms.length !== 1 ? "s" : ""} (${running} running)`, inv.vms, [
|
|
39
|
+
{ header: "Name", width: 26, render: (v) => v.name },
|
|
40
|
+
{ header: "VMID", width: 6, render: (v) => String(v.vmid) },
|
|
41
|
+
{ header: "Node", width: 12, render: (v) => v.node },
|
|
42
|
+
{ header: "Status", width: 8, render: (v) => v.status },
|
|
43
|
+
{
|
|
44
|
+
header: "Mem",
|
|
45
|
+
width: 6,
|
|
46
|
+
render: (v) => `${Math.round(v.maxmem / 1024 ** 3)}GB`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
header: "Disk",
|
|
50
|
+
width: 6,
|
|
51
|
+
render: (v) => `${Math.round(v.maxdisk / 1024 ** 3)}GB`,
|
|
52
|
+
},
|
|
41
53
|
]);
|
|
42
54
|
}
|
|
43
55
|
function renderDo(inv) {
|
|
44
|
-
const costStr = inv.totalMonthlyCost > 0 ? ` · $${inv.totalMonthlyCost}/mo` :
|
|
56
|
+
const costStr = inv.totalMonthlyCost > 0 ? ` · $${inv.totalMonthlyCost}/mo` : "";
|
|
45
57
|
printSection(`DigitalOcean Droplets · ${inv.droplets.length}${costStr}`, inv.droplets, [
|
|
46
|
-
{ header:
|
|
47
|
-
{ header:
|
|
48
|
-
{ header:
|
|
49
|
-
{ header:
|
|
50
|
-
{ header:
|
|
51
|
-
{
|
|
58
|
+
{ header: "Name", width: 24, render: (d) => d.name },
|
|
59
|
+
{ header: "Region", width: 6, render: (d) => d.region },
|
|
60
|
+
{ header: "Size", width: 18, render: (d) => d.size },
|
|
61
|
+
{ header: "Status", width: 8, render: (d) => d.status },
|
|
62
|
+
{ header: "IP", width: 15, render: (d) => d.ip ?? "-" },
|
|
63
|
+
{
|
|
64
|
+
header: "$/mo",
|
|
65
|
+
width: 5,
|
|
66
|
+
render: (d) => (d.monthlyCost > 0 ? `$${d.monthlyCost}` : "?"),
|
|
67
|
+
},
|
|
52
68
|
]);
|
|
53
69
|
if (inv.firewalls.length > 0) {
|
|
54
70
|
printSection(`DigitalOcean Firewalls · ${inv.firewalls.length}`, inv.firewalls, [
|
|
55
|
-
{ header:
|
|
56
|
-
{ header:
|
|
71
|
+
{ header: "Name", width: 32, render: (f) => f.name },
|
|
72
|
+
{ header: "Droplets", width: 8, render: (f) => String(f.dropletCount) },
|
|
57
73
|
]);
|
|
58
74
|
}
|
|
59
75
|
if (inv.loadBalancers.length > 0) {
|
|
60
76
|
printSection(`DigitalOcean Load Balancers · ${inv.loadBalancers.length}`, inv.loadBalancers, [
|
|
61
|
-
{ header:
|
|
62
|
-
{ header:
|
|
63
|
-
{ header:
|
|
64
|
-
{ header:
|
|
77
|
+
{ header: "Name", width: 24, render: (lb) => lb.name },
|
|
78
|
+
{ header: "Region", width: 6, render: (lb) => lb.region },
|
|
79
|
+
{ header: "IP", width: 15, render: (lb) => lb.ip },
|
|
80
|
+
{ header: "Status", width: 8, render: (lb) => lb.status },
|
|
65
81
|
]);
|
|
66
82
|
}
|
|
67
83
|
if (inv.domains.length > 0) {
|
|
68
84
|
printSection(`DigitalOcean Domains · ${inv.domains.length}`, inv.domains, [
|
|
69
|
-
{ header:
|
|
70
|
-
{ header:
|
|
85
|
+
{ header: "Domain", width: 42, render: (d) => d.name },
|
|
86
|
+
{ header: "TTL", width: 6, render: (d) => String(d.ttl) },
|
|
71
87
|
]);
|
|
72
88
|
}
|
|
73
89
|
}
|
|
74
90
|
function renderAws(inv) {
|
|
75
91
|
if (inv.distributions.length > 0) {
|
|
76
92
|
printSection(`AWS CloudFront · ${inv.distributions.length} · ${inv.region}`, inv.distributions, [
|
|
77
|
-
{ header:
|
|
78
|
-
{
|
|
79
|
-
|
|
93
|
+
{ header: "ID", width: 14, render: (d) => d.id },
|
|
94
|
+
{
|
|
95
|
+
header: "Domain",
|
|
96
|
+
width: 34,
|
|
97
|
+
render: (d) => d.aliases[0] ?? d.domain,
|
|
98
|
+
},
|
|
99
|
+
{ header: "Status", width: 10, render: (d) => d.status },
|
|
80
100
|
]);
|
|
81
101
|
}
|
|
82
102
|
if (inv.buckets.length > 0) {
|
|
83
103
|
printSection(`AWS S3 · ${inv.buckets.length} buckets`, inv.buckets, [
|
|
84
|
-
{ header:
|
|
104
|
+
{ header: "Bucket", width: 52, render: (b) => b.name },
|
|
85
105
|
]);
|
|
86
106
|
}
|
|
87
107
|
if (inv.lambdas.length > 0) {
|
|
88
108
|
printSection(`AWS Lambda · ${inv.lambdas.length} functions`, inv.lambdas, [
|
|
89
|
-
{ header:
|
|
90
|
-
{ header:
|
|
91
|
-
{ header:
|
|
109
|
+
{ header: "Function", width: 32, render: (f) => f.name },
|
|
110
|
+
{ header: "Runtime", width: 12, render: (f) => f.runtime },
|
|
111
|
+
{ header: "Memory", width: 8, render: (f) => `${f.memorySizeMb}MB` },
|
|
92
112
|
]);
|
|
93
113
|
}
|
|
94
114
|
if (inv.rdsInstances.length > 0) {
|
|
95
115
|
printSection(`AWS RDS · ${inv.rdsInstances.length} instances`, inv.rdsInstances, [
|
|
96
|
-
{ header:
|
|
97
|
-
{ header:
|
|
98
|
-
{ header:
|
|
99
|
-
{ header:
|
|
116
|
+
{ header: "Identifier", width: 26, render: (i) => i.identifier },
|
|
117
|
+
{ header: "Engine", width: 18, render: (i) => i.engine },
|
|
118
|
+
{ header: "Class", width: 14, render: (i) => i.instanceClass },
|
|
119
|
+
{ header: "Status", width: 10, render: (i) => i.status },
|
|
100
120
|
]);
|
|
101
121
|
}
|
|
102
122
|
if (inv.hostedZones.length > 0) {
|
|
103
123
|
printSection(`AWS Route53 · ${inv.hostedZones.length} zones`, inv.hostedZones, [
|
|
104
|
-
{ header:
|
|
105
|
-
{ header:
|
|
124
|
+
{ header: "Zone", width: 38, render: (z) => z.name },
|
|
125
|
+
{ header: "Records", width: 7, render: (z) => String(z.recordCount) },
|
|
106
126
|
]);
|
|
107
127
|
}
|
|
108
128
|
}
|
|
@@ -116,18 +136,30 @@ export class Checker {
|
|
|
116
136
|
console.log(`\n🔍 Checking infrastructure...`);
|
|
117
137
|
if (cfg.providers.proxmox?.url && cfg.providers.proxmox?.tokenSecret) {
|
|
118
138
|
tasks.push(listProxmoxVMs()
|
|
119
|
-
.then((inv) => {
|
|
120
|
-
.
|
|
139
|
+
.then((inv) => {
|
|
140
|
+
result.proxmox = inv;
|
|
141
|
+
})
|
|
142
|
+
.catch((err) => {
|
|
143
|
+
errors.push({ provider: "proxmox", message: err.message });
|
|
144
|
+
}));
|
|
121
145
|
}
|
|
122
146
|
if (cfg.providers.do?.token) {
|
|
123
147
|
tasks.push(listDoResources()
|
|
124
|
-
.then((inv) => {
|
|
125
|
-
.
|
|
148
|
+
.then((inv) => {
|
|
149
|
+
result.do = inv;
|
|
150
|
+
})
|
|
151
|
+
.catch((err) => {
|
|
152
|
+
errors.push({ provider: "do", message: err.message });
|
|
153
|
+
}));
|
|
126
154
|
}
|
|
127
155
|
if (cfg.providers.aws?.region) {
|
|
128
156
|
tasks.push(listAwsResources()
|
|
129
|
-
.then((inv) => {
|
|
130
|
-
.
|
|
157
|
+
.then((inv) => {
|
|
158
|
+
result.aws = inv;
|
|
159
|
+
})
|
|
160
|
+
.catch((err) => {
|
|
161
|
+
errors.push({ provider: "aws", message: err.message });
|
|
162
|
+
}));
|
|
131
163
|
}
|
|
132
164
|
await Promise.all(tasks);
|
|
133
165
|
if (result.proxmox)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { Config } from "./config.js";
|
|
4
|
+
describe("ConfigManager", () => {
|
|
5
|
+
test("sets and gets config correctly", () => {
|
|
6
|
+
Config.set({ dryRun: true });
|
|
7
|
+
assert.strictEqual(Config.get().dryRun, true);
|
|
8
|
+
assert.strictEqual(Config.isGlobalDryRun(), true);
|
|
9
|
+
Config.set({ dryRun: false });
|
|
10
|
+
assert.strictEqual(Config.get().dryRun, false);
|
|
11
|
+
assert.strictEqual(Config.isGlobalDryRun(), false);
|
|
12
|
+
});
|
|
13
|
+
test("merges provider config correctly", () => {
|
|
14
|
+
Config.set({
|
|
15
|
+
providers: {
|
|
16
|
+
aws: { region: "us-east-1" }
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
assert.strictEqual(Config.get().providers.aws?.region, "us-east-1");
|
|
20
|
+
});
|
|
21
|
+
});
|
package/dist/core/decorators.js
CHANGED
|
@@ -20,7 +20,13 @@ function applyConfig(opts) {
|
|
|
20
20
|
if (opts.firebase) {
|
|
21
21
|
const sa = JSON.parse(readFileSync(opts.firebase, "utf8"));
|
|
22
22
|
Config.set({
|
|
23
|
-
providers: {
|
|
23
|
+
providers: {
|
|
24
|
+
...Config.get().providers,
|
|
25
|
+
firebase: {
|
|
26
|
+
projectId: sa.project_id,
|
|
27
|
+
serviceAccountPath: opts.firebase,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
24
30
|
});
|
|
25
31
|
}
|
|
26
32
|
}
|
|
@@ -75,7 +81,7 @@ export function Check(opts = {}) {
|
|
|
75
81
|
});
|
|
76
82
|
};
|
|
77
83
|
}
|
|
78
|
-
// Shortcut for Dry Run
|
|
84
|
+
// Shortcut for Dry Run - accepts the same options as @Deploy
|
|
79
85
|
export function DryRun(opts = {}) {
|
|
80
86
|
if (typeof opts === "function") {
|
|
81
87
|
Deploy({ dryRun: true })(opts);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { Output } from "./output.js";
|
|
4
|
+
describe("Output", () => {
|
|
5
|
+
test("resolves a value correctly", async () => {
|
|
6
|
+
const out = new Output();
|
|
7
|
+
out.resolve("success");
|
|
8
|
+
const val = await out.get();
|
|
9
|
+
assert.strictEqual(val, "success");
|
|
10
|
+
});
|
|
11
|
+
test("applies transformations via .apply()", async () => {
|
|
12
|
+
const out = new Output();
|
|
13
|
+
const doubled = out.apply(n => n * 2);
|
|
14
|
+
out.resolve(10);
|
|
15
|
+
const result = await doubled.get();
|
|
16
|
+
assert.strictEqual(result, 20);
|
|
17
|
+
});
|
|
18
|
+
});
|
package/dist/core/resource.js
CHANGED
|
@@ -31,7 +31,7 @@ export class BaseBuilder {
|
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
33
|
// Waits for a long-running cloud operation to complete.
|
|
34
|
-
// In dry-run mode: skips entirely
|
|
34
|
+
// In dry-run mode: skips entirely - no waiting.
|
|
35
35
|
// In real mode: polls via the provided condition fn until it returns true.
|
|
36
36
|
// The mock fallback simulates a realistic delay with progress output.
|
|
37
37
|
async waitFor(label, condition, opts = {}) {
|
|
@@ -70,7 +70,7 @@ export class BaseBuilder {
|
|
|
70
70
|
async destroy() {
|
|
71
71
|
const dryRun = this.isDryRunActive();
|
|
72
72
|
console.log(`\n🗑️ Destroying "${this.name}"...`);
|
|
73
|
-
console.log(` ✅ [${dryRun ?
|
|
73
|
+
console.log(` ✅ [${dryRun ? "PLAN" : "OK"}] Resource "${this.name}" marked for destruction.`);
|
|
74
74
|
await this.destroySidecars();
|
|
75
75
|
return { destroyed: this.name };
|
|
76
76
|
}
|
package/dist/core/stack.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
export declare abstract class Stack {
|
|
3
|
-
/** @internal
|
|
3
|
+
/** @internal - called by @Deploy to register the instance for cross-stack references. */
|
|
4
4
|
static _register(cls: Function, instance: Stack): void;
|
|
5
5
|
/**
|
|
6
6
|
* Returns the already-constructed instance of another Stack so you can reference
|
package/dist/core/stack.js
CHANGED
|
@@ -23,7 +23,7 @@ function formatEntry(val) {
|
|
|
23
23
|
const inline = pairs.map(([, v]) => v).join(" · ");
|
|
24
24
|
if (inline.length <= 52)
|
|
25
25
|
return { primary: inline };
|
|
26
|
-
// Too long
|
|
26
|
+
// Too long - first value as primary, rest as sub-lines
|
|
27
27
|
const [[, first], ...rest] = pairs;
|
|
28
28
|
return {
|
|
29
29
|
primary: first,
|
|
@@ -59,7 +59,7 @@ function printOutputs(stackName, outputs) {
|
|
|
59
59
|
console.log(` └${line}┘`);
|
|
60
60
|
}
|
|
61
61
|
export class Stack {
|
|
62
|
-
/** @internal
|
|
62
|
+
/** @internal - called by @Deploy to register the instance for cross-stack references. */
|
|
63
63
|
static _register(cls, instance) {
|
|
64
64
|
_registry.set(cls, instance);
|
|
65
65
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ListCertificatesCommand, RequestCertificateCommand, DescribeCertificateCommand, } from
|
|
2
|
-
import { ChangeResourceRecordSetsCommand } from
|
|
3
|
-
import { BaseBuilder } from
|
|
4
|
-
import { getACMClient, getR53Client } from
|
|
1
|
+
import { ListCertificatesCommand, RequestCertificateCommand, DescribeCertificateCommand, } from "@aws-sdk/client-acm";
|
|
2
|
+
import { ChangeResourceRecordSetsCommand } from "@aws-sdk/client-route-53";
|
|
3
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
4
|
+
import { getACMClient, getR53Client } from "./api.js";
|
|
5
5
|
export class ACMCertificateBuilder extends BaseBuilder {
|
|
6
6
|
domainName;
|
|
7
7
|
wildcard;
|
|
@@ -22,7 +22,9 @@ export class ACMCertificateBuilder extends BaseBuilder {
|
|
|
22
22
|
try {
|
|
23
23
|
const acm = getACMClient();
|
|
24
24
|
const primaryName = wildcard ? `*.${domain}` : domain;
|
|
25
|
-
const list = await acm.send(new ListCertificatesCommand({
|
|
25
|
+
const list = await acm.send(new ListCertificatesCommand({
|
|
26
|
+
CertificateStatuses: ["ISSUED", "PENDING_VALIDATION"],
|
|
27
|
+
}));
|
|
26
28
|
for (const cert of list.CertificateSummaryList ?? []) {
|
|
27
29
|
if (cert.DomainName === primaryName && cert.CertificateArn) {
|
|
28
30
|
this.resolvedArn = cert.CertificateArn;
|
|
@@ -32,7 +34,7 @@ export class ACMCertificateBuilder extends BaseBuilder {
|
|
|
32
34
|
return null;
|
|
33
35
|
}
|
|
34
36
|
catch (e) {
|
|
35
|
-
if (e.name ===
|
|
37
|
+
if (e.name === "CredentialsProviderError")
|
|
36
38
|
return null;
|
|
37
39
|
throw e;
|
|
38
40
|
}
|
|
@@ -42,13 +44,15 @@ export class ACMCertificateBuilder extends BaseBuilder {
|
|
|
42
44
|
const existing = await this.discoveryPromise;
|
|
43
45
|
console.log(`\n🔐 Finalizing ACM Certificate for "${this.domainName}"...`);
|
|
44
46
|
if (existing) {
|
|
45
|
-
console.log(` ✅ Certificate already exists (${existing.Status ??
|
|
47
|
+
console.log(` ✅ Certificate already exists (${existing.Status ?? "ISSUED"}): ${this.resolvedArn}`);
|
|
46
48
|
return { arn: this.resolvedArn };
|
|
47
49
|
}
|
|
48
|
-
const primaryName = this.wildcard
|
|
50
|
+
const primaryName = this.wildcard
|
|
51
|
+
? `*.${this.domainName}`
|
|
52
|
+
: this.domainName;
|
|
49
53
|
const sanNames = this.wildcard ? [this.domainName] : [];
|
|
50
54
|
if (dryRun) {
|
|
51
|
-
console.log(` 📝 [PLAN] Request ${this.wildcard ?
|
|
55
|
+
console.log(` 📝 [PLAN] Request ${this.wildcard ? "wildcard " : ""}certificate: ${primaryName}`);
|
|
52
56
|
console.log(` 📝 [PLAN] DNS validation CNAMEs will be auto-written to Route53`);
|
|
53
57
|
this.resolvedArn = `arn:aws:acm:us-east-1:DRYRUN:certificate/pending`;
|
|
54
58
|
return { arn: this.resolvedArn };
|
|
@@ -57,51 +61,51 @@ export class ACMCertificateBuilder extends BaseBuilder {
|
|
|
57
61
|
const result = await acm.send(new RequestCertificateCommand({
|
|
58
62
|
DomainName: primaryName,
|
|
59
63
|
SubjectAlternativeNames: sanNames.length ? sanNames : undefined,
|
|
60
|
-
ValidationMethod:
|
|
64
|
+
ValidationMethod: "DNS",
|
|
61
65
|
}));
|
|
62
66
|
this.resolvedArn = result.CertificateArn;
|
|
63
67
|
console.log(`🚀 Requested certificate ${primaryName} (arn=${this.resolvedArn})`);
|
|
64
68
|
// Step 1: wait until ALL DomainValidationOptions have their ResourceRecord
|
|
65
69
|
// (wildcard + apex SANs each get an entry; ACM can generate them at different times)
|
|
66
|
-
await this.waitFor(
|
|
70
|
+
await this.waitFor("validation records to be generated", async () => {
|
|
67
71
|
const detail = await acm.send(new DescribeCertificateCommand({ CertificateArn: this.resolvedArn }));
|
|
68
72
|
const options = detail.Certificate?.DomainValidationOptions ?? [];
|
|
69
73
|
if (options.length === 0)
|
|
70
74
|
return false;
|
|
71
|
-
const withRecords = options.filter(o => o.ResourceRecord);
|
|
75
|
+
const withRecords = options.filter((o) => o.ResourceRecord);
|
|
72
76
|
if (withRecords.length < options.length)
|
|
73
77
|
return false;
|
|
74
|
-
this.validationRecords = withRecords.map(o => ({
|
|
78
|
+
this.validationRecords = withRecords.map((o) => ({
|
|
75
79
|
name: o.ResourceRecord.Name,
|
|
76
80
|
value: o.ResourceRecord.Value,
|
|
77
81
|
}));
|
|
78
82
|
return true;
|
|
79
83
|
}, { intervalMs: 5_000, timeoutMs: 60_000 });
|
|
80
|
-
// Step 2: write them into Route53 automatically
|
|
84
|
+
// Step 2: write them into Route53 automatically - no manual DNS work needed
|
|
81
85
|
if (this._zone?.zoneId) {
|
|
82
|
-
// Wildcard certs produce duplicate CNAME names across SANs
|
|
83
|
-
const unique = Array.from(new Map(this.validationRecords.map(r => [r.name, r])).values());
|
|
86
|
+
// Wildcard certs produce duplicate CNAME names across SANs - deduplicate before sending
|
|
87
|
+
const unique = Array.from(new Map(this.validationRecords.map((r) => [r.name, r])).values());
|
|
84
88
|
const r53 = getR53Client();
|
|
85
89
|
await r53.send(new ChangeResourceRecordSetsCommand({
|
|
86
90
|
HostedZoneId: this._zone.zoneId,
|
|
87
91
|
ChangeBatch: {
|
|
88
|
-
Changes: unique.map(r => ({
|
|
89
|
-
Action:
|
|
92
|
+
Changes: unique.map((r) => ({
|
|
93
|
+
Action: "UPSERT",
|
|
90
94
|
ResourceRecordSet: {
|
|
91
|
-
Name: r.name.replace(/\.$/,
|
|
92
|
-
Type:
|
|
95
|
+
Name: r.name.replace(/\.$/, ""),
|
|
96
|
+
Type: "CNAME",
|
|
93
97
|
TTL: 300,
|
|
94
|
-
ResourceRecords: [{ Value: r.value.replace(/\.$/,
|
|
98
|
+
ResourceRecords: [{ Value: r.value.replace(/\.$/, "") }],
|
|
95
99
|
},
|
|
96
100
|
})),
|
|
97
101
|
},
|
|
98
102
|
}));
|
|
99
103
|
console.log(` ✅ Auto-wrote ${unique.length} validation CNAME(s) to Route53`);
|
|
100
104
|
}
|
|
101
|
-
// Step 3: now wait for ISSUED
|
|
105
|
+
// Step 3: now wait for ISSUED - records are in DNS so this will actually resolve
|
|
102
106
|
await this.waitFor(`certificate "${this.domainName}" to be validated`, async () => {
|
|
103
107
|
const detail = await acm.send(new DescribeCertificateCommand({ CertificateArn: this.resolvedArn }));
|
|
104
|
-
return detail.Certificate?.Status ===
|
|
108
|
+
return detail.Certificate?.Status === "ISSUED";
|
|
105
109
|
}, { intervalMs: 15_000, timeoutMs: 600_000 });
|
|
106
110
|
console.log(` ✅ Certificate issued: ${this.resolvedArn}`);
|
|
107
111
|
return { arn: this.resolvedArn };
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { S3Client } from
|
|
2
|
-
import { CloudFrontClient } from
|
|
3
|
-
import { Route53Client } from
|
|
4
|
-
import { Route53DomainsClient } from
|
|
5
|
-
import { ACMClient } from
|
|
6
|
-
import { LambdaClient } from
|
|
7
|
-
import { IAMClient } from
|
|
8
|
-
import { ApiGatewayV2Client } from
|
|
9
|
-
import { ECSClient } from
|
|
10
|
-
import { EC2Client } from
|
|
11
|
-
import { CloudWatchLogsClient } from
|
|
12
|
-
import { RDSClient } from
|
|
13
|
-
import { SQSClient } from
|
|
14
|
-
import { SecretsManagerClient } from
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { CloudFrontClient } from "@aws-sdk/client-cloudfront";
|
|
3
|
+
import { Route53Client } from "@aws-sdk/client-route-53";
|
|
4
|
+
import { Route53DomainsClient } from "@aws-sdk/client-route-53-domains";
|
|
5
|
+
import { ACMClient } from "@aws-sdk/client-acm";
|
|
6
|
+
import { LambdaClient } from "@aws-sdk/client-lambda";
|
|
7
|
+
import { IAMClient } from "@aws-sdk/client-iam";
|
|
8
|
+
import { ApiGatewayV2Client } from "@aws-sdk/client-apigatewayv2";
|
|
9
|
+
import { ECSClient } from "@aws-sdk/client-ecs";
|
|
10
|
+
import { EC2Client } from "@aws-sdk/client-ec2";
|
|
11
|
+
import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
|
|
12
|
+
import { RDSClient } from "@aws-sdk/client-rds";
|
|
13
|
+
import { SQSClient } from "@aws-sdk/client-sqs";
|
|
14
|
+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
15
15
|
export declare const getS3Client: (region?: string) => S3Client;
|
|
16
16
|
export declare const getCFClient: () => CloudFrontClient;
|
|
17
17
|
export declare const getR53Client: () => Route53Client;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { S3Client } from
|
|
2
|
-
import { CloudFrontClient } from
|
|
3
|
-
import { Route53Client } from
|
|
4
|
-
import { Route53DomainsClient } from
|
|
5
|
-
import { ACMClient } from
|
|
6
|
-
import { LambdaClient } from
|
|
7
|
-
import { IAMClient } from
|
|
8
|
-
import { ApiGatewayV2Client } from
|
|
9
|
-
import { ECSClient } from
|
|
10
|
-
import { EC2Client } from
|
|
11
|
-
import { CloudWatchLogsClient } from
|
|
12
|
-
import { RDSClient } from
|
|
13
|
-
import { SQSClient } from
|
|
14
|
-
import { SecretsManagerClient } from
|
|
15
|
-
import { Config } from
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { CloudFrontClient } from "@aws-sdk/client-cloudfront";
|
|
3
|
+
import { Route53Client } from "@aws-sdk/client-route-53";
|
|
4
|
+
import { Route53DomainsClient } from "@aws-sdk/client-route-53-domains";
|
|
5
|
+
import { ACMClient } from "@aws-sdk/client-acm";
|
|
6
|
+
import { LambdaClient } from "@aws-sdk/client-lambda";
|
|
7
|
+
import { IAMClient } from "@aws-sdk/client-iam";
|
|
8
|
+
import { ApiGatewayV2Client } from "@aws-sdk/client-apigatewayv2";
|
|
9
|
+
import { ECSClient } from "@aws-sdk/client-ecs";
|
|
10
|
+
import { EC2Client } from "@aws-sdk/client-ec2";
|
|
11
|
+
import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
|
|
12
|
+
import { RDSClient } from "@aws-sdk/client-rds";
|
|
13
|
+
import { SQSClient } from "@aws-sdk/client-sqs";
|
|
14
|
+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
15
|
+
import { Config } from "../../core/config.js";
|
|
16
16
|
function getRegion() {
|
|
17
17
|
const region = Config.get().providers.aws?.region;
|
|
18
18
|
if (!region)
|
|
@@ -20,12 +20,12 @@ function getRegion() {
|
|
|
20
20
|
return region;
|
|
21
21
|
}
|
|
22
22
|
export const getS3Client = (region) => new S3Client({ region: region ?? getRegion() });
|
|
23
|
-
// CloudFront, Route53, ACM, Route53 Domains, and IAM are all global
|
|
24
|
-
export const getCFClient = () => new CloudFrontClient({ region:
|
|
25
|
-
export const getR53Client = () => new Route53Client({ region:
|
|
26
|
-
export const getR53DomainsClient = () => new Route53DomainsClient({ region:
|
|
27
|
-
export const getACMClient = () => new ACMClient({ region:
|
|
28
|
-
export const getIAMClient = () => new IAMClient({ region:
|
|
23
|
+
// CloudFront, Route53, ACM, Route53 Domains, and IAM are all global - must use us-east-1
|
|
24
|
+
export const getCFClient = () => new CloudFrontClient({ region: "us-east-1" });
|
|
25
|
+
export const getR53Client = () => new Route53Client({ region: "us-east-1" });
|
|
26
|
+
export const getR53DomainsClient = () => new Route53DomainsClient({ region: "us-east-1" });
|
|
27
|
+
export const getACMClient = () => new ACMClient({ region: "us-east-1" });
|
|
28
|
+
export const getIAMClient = () => new IAMClient({ region: "us-east-1" });
|
|
29
29
|
export const getLambdaClient = (region) => new LambdaClient({ region: region ?? getRegion() });
|
|
30
30
|
export const getAPIGWClient = (region) => new ApiGatewayV2Client({ region: region ?? getRegion() });
|
|
31
31
|
export const getECSClient = (region) => new ECSClient({ region: region ?? getRegion() });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseBuilder } from
|
|
2
|
-
import { LambdaBuilder } from
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { LambdaBuilder } from "./lambda.js";
|
|
3
3
|
export declare class APIGatewayBuilder extends BaseBuilder {
|
|
4
4
|
private _routes;
|
|
5
5
|
resolvedId: string | null;
|