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
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { DescribeDBInstancesCommand, CreateDBInstanceCommand, ModifyDBInstanceCommand, DeleteDBInstanceCommand, DescribeDBSubnetGroupsCommand, CreateDBSubnetGroupCommand, } from
|
|
2
|
-
import { DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, CreateSecurityGroupCommand, AuthorizeSecurityGroupIngressCommand, } from
|
|
3
|
-
import { BaseBuilder } from
|
|
4
|
-
import { getRDSClient, getEC2Client } from
|
|
5
|
-
import { Config } from
|
|
1
|
+
import { DescribeDBInstancesCommand, CreateDBInstanceCommand, ModifyDBInstanceCommand, DeleteDBInstanceCommand, DescribeDBSubnetGroupsCommand, CreateDBSubnetGroupCommand, } from "@aws-sdk/client-rds";
|
|
2
|
+
import { DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, CreateSecurityGroupCommand, AuthorizeSecurityGroupIngressCommand, } from "@aws-sdk/client-ec2";
|
|
3
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
4
|
+
import { getRDSClient, getEC2Client } from "./api.js";
|
|
5
|
+
import { Config } from "../../core/config.js";
|
|
6
6
|
const DB_PORT = {
|
|
7
7
|
postgres: 5432,
|
|
8
8
|
mysql: 3306,
|
|
9
9
|
mariadb: 3306,
|
|
10
10
|
};
|
|
11
11
|
export class RDSBuilder extends BaseBuilder {
|
|
12
|
-
_engine =
|
|
13
|
-
_engineVersion =
|
|
14
|
-
_instanceClass =
|
|
12
|
+
_engine = "postgres";
|
|
13
|
+
_engineVersion = "16";
|
|
14
|
+
_instanceClass = "db.t3.micro";
|
|
15
15
|
_storage = 20;
|
|
16
16
|
_username;
|
|
17
17
|
_password;
|
|
@@ -31,12 +31,30 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
31
31
|
this._engineVersion = e.version;
|
|
32
32
|
return this;
|
|
33
33
|
}
|
|
34
|
-
size(instanceClass) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
size(instanceClass) {
|
|
35
|
+
this._instanceClass = instanceClass;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
storage(gb) {
|
|
39
|
+
this._storage = gb;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
subnets(ids) {
|
|
43
|
+
this._subnetIds = ids;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
securityGroups(ids) {
|
|
47
|
+
this._securityGroupIds = ids;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
publicAccess(enabled = true) {
|
|
51
|
+
this._publicAccess = enabled;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
database(name) {
|
|
55
|
+
this._dbName = name;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
40
58
|
credentials(username, password) {
|
|
41
59
|
this._username = username;
|
|
42
60
|
this._password = password;
|
|
@@ -48,7 +66,7 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
48
66
|
DBInstanceIdentifier: identifier,
|
|
49
67
|
}));
|
|
50
68
|
const instance = result.DBInstances?.[0];
|
|
51
|
-
if (!instance || instance.DBInstanceStatus ===
|
|
69
|
+
if (!instance || instance.DBInstanceStatus === "deleting")
|
|
52
70
|
return null;
|
|
53
71
|
this.resolvedArn = instance.DBInstanceArn ?? null;
|
|
54
72
|
this.resolvedEndpoint = instance.Endpoint?.Address ?? null;
|
|
@@ -56,9 +74,9 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
56
74
|
return instance;
|
|
57
75
|
}
|
|
58
76
|
catch (e) {
|
|
59
|
-
if (e.name ===
|
|
77
|
+
if (e.name === "DBInstanceNotFound")
|
|
60
78
|
return null;
|
|
61
|
-
if (e.name ===
|
|
79
|
+
if (e.name === "CredentialsProviderError")
|
|
62
80
|
return null;
|
|
63
81
|
throw e;
|
|
64
82
|
}
|
|
@@ -66,15 +84,17 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
66
84
|
async discoverDefaultVpc() {
|
|
67
85
|
const ec2 = getEC2Client();
|
|
68
86
|
const vpcs = await ec2.send(new DescribeVpcsCommand({
|
|
69
|
-
Filters: [{ Name:
|
|
87
|
+
Filters: [{ Name: "isDefault", Values: ["true"] }],
|
|
70
88
|
}));
|
|
71
89
|
const vpc = vpcs.Vpcs?.[0];
|
|
72
90
|
if (!vpc?.VpcId)
|
|
73
91
|
throw new Error(`[RDS:${this.name}] No default VPC found. Use .subnets(ids[]) to specify subnets.`);
|
|
74
92
|
const subnets = await ec2.send(new DescribeSubnetsCommand({
|
|
75
|
-
Filters: [{ Name:
|
|
93
|
+
Filters: [{ Name: "vpc-id", Values: [vpc.VpcId] }],
|
|
76
94
|
}));
|
|
77
|
-
const subnetIds = (subnets.Subnets ?? [])
|
|
95
|
+
const subnetIds = (subnets.Subnets ?? [])
|
|
96
|
+
.map((s) => s.SubnetId)
|
|
97
|
+
.filter(Boolean);
|
|
78
98
|
return { vpcId: vpc.VpcId, vpcCidr: vpc.CidrBlock, subnetIds };
|
|
79
99
|
}
|
|
80
100
|
async ensureSubnetGroup(subnetIds) {
|
|
@@ -85,7 +105,7 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
85
105
|
return groupName;
|
|
86
106
|
}
|
|
87
107
|
catch (e) {
|
|
88
|
-
if (e.name !==
|
|
108
|
+
if (e.name !== "DBSubnetGroupNotFoundFault")
|
|
89
109
|
throw e;
|
|
90
110
|
}
|
|
91
111
|
await rds.send(new CreateDBSubnetGroupCommand({
|
|
@@ -102,8 +122,8 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
102
122
|
const port = DB_PORT[this._engine] ?? 5432;
|
|
103
123
|
const existing = await ec2.send(new DescribeSecurityGroupsCommand({
|
|
104
124
|
Filters: [
|
|
105
|
-
{ Name:
|
|
106
|
-
{ Name:
|
|
125
|
+
{ Name: "group-name", Values: [sgName] },
|
|
126
|
+
{ Name: "vpc-id", Values: [vpcId] },
|
|
107
127
|
],
|
|
108
128
|
}));
|
|
109
129
|
if (existing.SecurityGroups?.length)
|
|
@@ -116,38 +136,49 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
116
136
|
const sgId = created.GroupId;
|
|
117
137
|
// Allow inbound on DB port from VPC CIDR only (secure default)
|
|
118
138
|
// Use .publicAccess() to open to the internet
|
|
119
|
-
const cidr = this._publicAccess ?
|
|
139
|
+
const cidr = this._publicAccess ? "0.0.0.0/0" : vpcCidr;
|
|
120
140
|
await ec2.send(new AuthorizeSecurityGroupIngressCommand({
|
|
121
141
|
GroupId: sgId,
|
|
122
|
-
IpPermissions: [
|
|
123
|
-
|
|
142
|
+
IpPermissions: [
|
|
143
|
+
{
|
|
144
|
+
IpProtocol: "tcp",
|
|
124
145
|
FromPort: port,
|
|
125
146
|
ToPort: port,
|
|
126
|
-
IpRanges: [
|
|
127
|
-
|
|
147
|
+
IpRanges: [
|
|
148
|
+
{
|
|
149
|
+
CidrIp: cidr,
|
|
150
|
+
Description: this._publicAccess ? "Public access" : "VPC only",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
128
155
|
}));
|
|
129
|
-
console.log(` ā
Created security group: ${sgName} (${sgId})
|
|
156
|
+
console.log(` ā
Created security group: ${sgName} (${sgId}) - port ${port} open to ${cidr}`);
|
|
130
157
|
return sgId;
|
|
131
158
|
}
|
|
132
159
|
async deploy() {
|
|
133
160
|
const dryRun = this.isDryRunActive();
|
|
134
161
|
const existing = await this.discoveryPromise;
|
|
135
|
-
const region = Config.get().providers.aws?.region ??
|
|
162
|
+
const region = Config.get().providers.aws?.region ?? "us-east-1";
|
|
136
163
|
const port = DB_PORT[this._engine] ?? 5432;
|
|
137
164
|
console.log(`\nā” Finalizing RDS Instance "${this.name}"...`);
|
|
138
165
|
if (dryRun) {
|
|
139
|
-
console.log(` š [PLAN] ${existing ?
|
|
166
|
+
console.log(` š [PLAN] ${existing ? "Update" : "Create"} RDS instance "${this.name}"`);
|
|
140
167
|
console.log(` āā Engine: ${this._engine} ${this._engineVersion}`);
|
|
141
168
|
console.log(` āā Class: ${this._instanceClass} | Storage: ${this._storage}GB`);
|
|
142
169
|
console.log(` āā Port: ${port} | Public: ${this._publicAccess}`);
|
|
143
170
|
this.resolvedEndpoint = `${this.name}.DRYRUN.${region}.rds.amazonaws.com`;
|
|
144
171
|
this.resolvedPort = port;
|
|
145
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
name: this.name,
|
|
174
|
+
endpoint: this.resolvedEndpoint,
|
|
175
|
+
port: this.resolvedPort,
|
|
176
|
+
};
|
|
146
177
|
}
|
|
147
178
|
if (!this._username || !this._password) {
|
|
148
179
|
throw new Error(`[RDS:${this.name}] .credentials(username, password) is required`);
|
|
149
180
|
}
|
|
150
|
-
// Resolve networking
|
|
181
|
+
// Resolve networking - only hit EC2 if needed
|
|
151
182
|
let subnetGroupName;
|
|
152
183
|
let securityGroupIds;
|
|
153
184
|
if (this._subnetIds && this._securityGroupIds) {
|
|
@@ -157,7 +188,9 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
157
188
|
else {
|
|
158
189
|
const { vpcId, vpcCidr, subnetIds } = await this.discoverDefaultVpc();
|
|
159
190
|
subnetGroupName = await this.ensureSubnetGroup(this._subnetIds ?? subnetIds);
|
|
160
|
-
securityGroupIds = this._securityGroupIds ?? [
|
|
191
|
+
securityGroupIds = this._securityGroupIds ?? [
|
|
192
|
+
await this.ensureSecurityGroup(vpcId, vpcCidr),
|
|
193
|
+
];
|
|
161
194
|
}
|
|
162
195
|
const rds = getRDSClient();
|
|
163
196
|
if (existing) {
|
|
@@ -183,15 +216,15 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
183
216
|
DBSubnetGroupName: subnetGroupName,
|
|
184
217
|
VpcSecurityGroupIds: securityGroupIds,
|
|
185
218
|
PubliclyAccessible: this._publicAccess,
|
|
186
|
-
StorageType:
|
|
219
|
+
StorageType: "gp3",
|
|
187
220
|
BackupRetentionPeriod: 7,
|
|
188
221
|
DeletionProtection: false,
|
|
189
222
|
}));
|
|
190
|
-
console.log(`š Creating RDS instance "${this.name}"
|
|
223
|
+
console.log(`š Creating RDS instance "${this.name}" - waiting for available...`);
|
|
191
224
|
await this.waitFor(`"${this.name}" to become available`, async () => {
|
|
192
225
|
const r = await getRDSClient().send(new DescribeDBInstancesCommand({ DBInstanceIdentifier: this.name }));
|
|
193
226
|
const inst = r.DBInstances?.[0];
|
|
194
|
-
if (inst?.DBInstanceStatus ===
|
|
227
|
+
if (inst?.DBInstanceStatus === "available") {
|
|
195
228
|
this.resolvedEndpoint = inst.Endpoint?.Address ?? null;
|
|
196
229
|
this.resolvedPort = inst.Endpoint?.Port ?? null;
|
|
197
230
|
this.resolvedArn = inst.DBInstanceArn ?? null;
|
|
@@ -202,14 +235,19 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
202
235
|
console.log(` š Endpoint: ${this.resolvedEndpoint}:${this.resolvedPort}`);
|
|
203
236
|
}
|
|
204
237
|
await this.deploySidecars();
|
|
205
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
name: this.name,
|
|
240
|
+
endpoint: this.resolvedEndpoint,
|
|
241
|
+
port: this.resolvedPort,
|
|
242
|
+
arn: this.resolvedArn,
|
|
243
|
+
};
|
|
206
244
|
}
|
|
207
245
|
async destroy() {
|
|
208
246
|
const dryRun = this.isDryRunActive();
|
|
209
247
|
const existing = await this.discoveryPromise;
|
|
210
248
|
console.log(`\nšļø Destroying RDS Instance "${this.name}"...`);
|
|
211
249
|
if (!existing) {
|
|
212
|
-
console.log(` ā
Instance "${this.name}" does not exist
|
|
250
|
+
console.log(` ā
Instance "${this.name}" does not exist - nothing to do`);
|
|
213
251
|
return { destroyed: this.name };
|
|
214
252
|
}
|
|
215
253
|
if (dryRun) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BaseBuilder } from
|
|
2
|
-
import { Output } from
|
|
3
|
-
import { ACMCertificateBuilder } from
|
|
4
|
-
import type { RegistrantContact } from
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
import { ACMCertificateBuilder } from "./acm.js";
|
|
4
|
+
import type { RegistrantContact } from "../../types/aws.js";
|
|
5
5
|
export declare class Route53Builder extends BaseBuilder {
|
|
6
6
|
readonly out: {
|
|
7
7
|
zone: Output<{
|
|
@@ -21,7 +21,7 @@ export declare class Route53Builder extends BaseBuilder {
|
|
|
21
21
|
cert(): ACMCertificateBuilder | undefined;
|
|
22
22
|
withWildcardSSL(): this;
|
|
23
23
|
register(contact?: RegistrantContact): this;
|
|
24
|
-
record(name: string, type:
|
|
24
|
+
record(name: string, type: "A" | "CNAME" | "AAAA", value: string): this;
|
|
25
25
|
pointer(name: string, target: BaseBuilder): this;
|
|
26
26
|
deploy(): Promise<{
|
|
27
27
|
zone: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ListHostedZonesByNameCommand, CreateHostedZoneCommand, ChangeResourceRecordSetsCommand, } from
|
|
2
|
-
import { RegisterDomainCommand, GetOperationDetailCommand, CheckDomainAvailabilityCommand, } from
|
|
3
|
-
import { BaseBuilder } from
|
|
4
|
-
import { Output } from
|
|
5
|
-
import { ACMCertificateBuilder } from
|
|
6
|
-
import { getR53Client, getR53DomainsClient } from
|
|
1
|
+
import { ListHostedZonesByNameCommand, CreateHostedZoneCommand, ChangeResourceRecordSetsCommand, } from "@aws-sdk/client-route-53";
|
|
2
|
+
import { RegisterDomainCommand, GetOperationDetailCommand, CheckDomainAvailabilityCommand, } from "@aws-sdk/client-route-53-domains";
|
|
3
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
4
|
+
import { Output } from "../../core/output.js";
|
|
5
|
+
import { ACMCertificateBuilder } from "./acm.js";
|
|
6
|
+
import { getR53Client, getR53DomainsClient } from "./api.js";
|
|
7
7
|
export class Route53Builder extends BaseBuilder {
|
|
8
8
|
out = {
|
|
9
9
|
zone: new Output(),
|
|
@@ -14,8 +14,8 @@ export class Route53Builder extends BaseBuilder {
|
|
|
14
14
|
_isRegistering = false;
|
|
15
15
|
_registrantContact;
|
|
16
16
|
_wantsWildcardSSL = false;
|
|
17
|
-
constructor(zoneName =
|
|
18
|
-
super(zoneName ||
|
|
17
|
+
constructor(zoneName = "") {
|
|
18
|
+
super(zoneName || "route53-pending");
|
|
19
19
|
this.zoneName = zoneName;
|
|
20
20
|
this.discoveryPromise = this.discoverZone(zoneName);
|
|
21
21
|
}
|
|
@@ -25,29 +25,29 @@ export class Route53Builder extends BaseBuilder {
|
|
|
25
25
|
try {
|
|
26
26
|
const r53 = getR53Client();
|
|
27
27
|
const result = await r53.send(new ListHostedZonesByNameCommand({ DNSName: name, MaxItems: 5 }));
|
|
28
|
-
const match = (result.HostedZones ?? []).find(z => z.Name === `${name}.`);
|
|
28
|
+
const match = (result.HostedZones ?? []).find((z) => z.Name === `${name}.`);
|
|
29
29
|
if (match) {
|
|
30
|
-
this.zoneId = match.Id.replace(
|
|
30
|
+
this.zoneId = match.Id.replace("/hostedzone/", "");
|
|
31
31
|
return match;
|
|
32
32
|
}
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
catch (e) {
|
|
36
|
-
if (e.name ===
|
|
36
|
+
if (e.name === "CredentialsProviderError")
|
|
37
37
|
return null;
|
|
38
38
|
throw e;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
randomDomain() {
|
|
42
|
-
const chars =
|
|
43
|
-
const id = Array.from({ length: 12 }, () => chars[Math.floor(Math.random() * chars.length)]).join(
|
|
42
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
43
|
+
const id = Array.from({ length: 12 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
|
44
44
|
this.zoneName = `${id}.com`;
|
|
45
45
|
this.name = this.zoneName;
|
|
46
46
|
this.discoveryPromise = this.discoverZone(this.zoneName);
|
|
47
47
|
return this;
|
|
48
48
|
}
|
|
49
49
|
cert() {
|
|
50
|
-
return this.sidecars.find(s => s instanceof ACMCertificateBuilder);
|
|
50
|
+
return this.sidecars.find((s) => s instanceof ACMCertificateBuilder);
|
|
51
51
|
}
|
|
52
52
|
withWildcardSSL() {
|
|
53
53
|
this._wantsWildcardSSL = true;
|
|
@@ -63,7 +63,7 @@ export class Route53Builder extends BaseBuilder {
|
|
|
63
63
|
return this;
|
|
64
64
|
}
|
|
65
65
|
pointer(name, target) {
|
|
66
|
-
this.records.push({ name, type:
|
|
66
|
+
this.records.push({ name, type: "A", value: target, isAlias: true });
|
|
67
67
|
return this;
|
|
68
68
|
}
|
|
69
69
|
async deploy() {
|
|
@@ -78,17 +78,17 @@ export class Route53Builder extends BaseBuilder {
|
|
|
78
78
|
if (!existing) {
|
|
79
79
|
if (dryRun) {
|
|
80
80
|
console.log(` š [PLAN] Create hosted zone ${this.zoneName}`);
|
|
81
|
-
this.out.zone.resolve({ name: this.zoneName, id:
|
|
81
|
+
this.out.zone.resolve({ name: this.zoneName, id: "PENDING" });
|
|
82
82
|
}
|
|
83
83
|
else {
|
|
84
|
-
// Route53 Domains auto-creates the hosted zone on registration
|
|
84
|
+
// Route53 Domains auto-creates the hosted zone on registration - check again before creating
|
|
85
85
|
const recheck = await this.discoverZone(this.zoneName);
|
|
86
86
|
if (!recheck) {
|
|
87
87
|
const result = await r53.send(new CreateHostedZoneCommand({
|
|
88
88
|
Name: this.zoneName,
|
|
89
89
|
CallerReference: `puls-${Date.now()}`,
|
|
90
90
|
}));
|
|
91
|
-
this.zoneId = result.HostedZone.Id.replace(
|
|
91
|
+
this.zoneId = result.HostedZone.Id.replace("/hostedzone/", "");
|
|
92
92
|
console.log(`š Created hosted zone ${this.zoneName} (id=${this.zoneId})`);
|
|
93
93
|
}
|
|
94
94
|
else {
|
|
@@ -106,21 +106,23 @@ export class Route53Builder extends BaseBuilder {
|
|
|
106
106
|
}
|
|
107
107
|
const cert = this.cert();
|
|
108
108
|
if (cert) {
|
|
109
|
-
cert.forZone(this); // zoneId is set by now
|
|
109
|
+
cert.forZone(this); // zoneId is set by now - cert writes its own validation CNAMEs
|
|
110
110
|
await cert.deploy();
|
|
111
111
|
}
|
|
112
112
|
// Regular records
|
|
113
113
|
if (this.records.length > 0 && !dryRun && this.zoneId) {
|
|
114
|
-
const resolved = this.records.map(r => ({
|
|
114
|
+
const resolved = this.records.map((r) => ({
|
|
115
115
|
type: r.type,
|
|
116
116
|
name: r.name,
|
|
117
117
|
value: r.value instanceof BaseBuilder ? `[alias: ${r.value.name}]` : r.value,
|
|
118
118
|
}));
|
|
119
|
-
await this.upsertRecords(r53, resolved.map(r => ({ ...r, ttl: 300 })));
|
|
119
|
+
await this.upsertRecords(r53, resolved.map((r) => ({ ...r, ttl: 300 })));
|
|
120
120
|
}
|
|
121
121
|
for (const rec of this.records) {
|
|
122
|
-
const val = rec.value instanceof BaseBuilder
|
|
123
|
-
|
|
122
|
+
const val = rec.value instanceof BaseBuilder
|
|
123
|
+
? `[Alias to ${rec.value.name}]`
|
|
124
|
+
: rec.value;
|
|
125
|
+
console.log(` ā
[${dryRun ? "PLAN" : "OK"}] ${rec.type}: ${rec.name}.${this.zoneName} ā ${val}`);
|
|
124
126
|
}
|
|
125
127
|
return { zone: this.zoneName, id: this.zoneId };
|
|
126
128
|
}
|
|
@@ -135,13 +137,13 @@ export class Route53Builder extends BaseBuilder {
|
|
|
135
137
|
}
|
|
136
138
|
// Check availability before attempting registration
|
|
137
139
|
const avail = await domains.send(new CheckDomainAvailabilityCommand({ DomainName: this.zoneName }));
|
|
138
|
-
if (avail.Availability !==
|
|
139
|
-
console.log(` ā¹ļø Domain ${this.zoneName} is not available for registration (${avail.Availability})
|
|
140
|
+
if (avail.Availability !== "AVAILABLE") {
|
|
141
|
+
console.log(` ā¹ļø Domain ${this.zoneName} is not available for registration (${avail.Availability}) - skipping`);
|
|
140
142
|
return;
|
|
141
143
|
}
|
|
142
144
|
const contact = c ? this.mapContact(c) : undefined;
|
|
143
145
|
if (!contact)
|
|
144
|
-
throw new Error(`register() called without contact details
|
|
146
|
+
throw new Error(`register() called without contact details - provide a RegistrantContact`);
|
|
145
147
|
console.log(` š Registering domain ${this.zoneName}... (est. ~5 min)`);
|
|
146
148
|
const result = await domains.send(new RegisterDomainCommand({
|
|
147
149
|
DomainName: this.zoneName,
|
|
@@ -157,10 +159,10 @@ export class Route53Builder extends BaseBuilder {
|
|
|
157
159
|
console.log(`š Domain registration submitted (operationId=${result.OperationId})`);
|
|
158
160
|
await this.waitFor(`domain "${this.zoneName}" to become active`, async () => {
|
|
159
161
|
const op = await domains.send(new GetOperationDetailCommand({ OperationId: result.OperationId }));
|
|
160
|
-
if (op.Status ===
|
|
162
|
+
if (op.Status === "ERROR" || op.Status === "FAILED") {
|
|
161
163
|
throw new Error(`Domain registration failed (${op.Status}): ${op.Message}`);
|
|
162
164
|
}
|
|
163
|
-
return op.Status ===
|
|
165
|
+
return op.Status === "SUCCESSFUL";
|
|
164
166
|
}, { intervalMs: 15_000, timeoutMs: 900_000 });
|
|
165
167
|
console.log(` ā
Domain ${this.zoneName} registered`);
|
|
166
168
|
}
|
|
@@ -180,11 +182,11 @@ export class Route53Builder extends BaseBuilder {
|
|
|
180
182
|
}
|
|
181
183
|
// Route53 Domains requires +CC.subscriber format (e.g. +46.708339809)
|
|
182
184
|
normalizePhone(phone) {
|
|
183
|
-
if (phone.includes(
|
|
185
|
+
if (phone.includes("."))
|
|
184
186
|
return phone;
|
|
185
|
-
const digits = phone.replace(/^\+/,
|
|
187
|
+
const digits = phone.replace(/^\+/, "");
|
|
186
188
|
// +1 (US/CA) and +7 (RU/KZ) are single-digit country codes
|
|
187
|
-
if (digits.startsWith(
|
|
189
|
+
if (digits.startsWith("1") || digits.startsWith("7")) {
|
|
188
190
|
return `+${digits[0]}.${digits.slice(1)}`;
|
|
189
191
|
}
|
|
190
192
|
// Default: treat first 2 digits as country code
|
|
@@ -192,9 +194,14 @@ export class Route53Builder extends BaseBuilder {
|
|
|
192
194
|
}
|
|
193
195
|
async upsertCnames(records) {
|
|
194
196
|
if (!this.zoneId)
|
|
195
|
-
throw new Error(`Zone ${this.zoneName} has no ID
|
|
197
|
+
throw new Error(`Zone ${this.zoneName} has no ID - was it deployed?`);
|
|
196
198
|
const r53 = getR53Client();
|
|
197
|
-
await this.upsertRecords(r53, records.map(r => ({
|
|
199
|
+
await this.upsertRecords(r53, records.map((r) => ({
|
|
200
|
+
type: "CNAME",
|
|
201
|
+
name: `${r.name}.${this.zoneName}`,
|
|
202
|
+
value: r.value,
|
|
203
|
+
ttl: 300,
|
|
204
|
+
})));
|
|
198
205
|
for (const r of records) {
|
|
199
206
|
console.log(` ā
CNAME ${r.name}.${this.zoneName} ā ${r.value}`);
|
|
200
207
|
}
|
|
@@ -203,8 +210,8 @@ export class Route53Builder extends BaseBuilder {
|
|
|
203
210
|
await r53.send(new ChangeResourceRecordSetsCommand({
|
|
204
211
|
HostedZoneId: this.zoneId,
|
|
205
212
|
ChangeBatch: {
|
|
206
|
-
Changes: records.map(r => ({
|
|
207
|
-
Action:
|
|
213
|
+
Changes: records.map((r) => ({
|
|
214
|
+
Action: "UPSERT",
|
|
208
215
|
ResourceRecordSet: {
|
|
209
216
|
Name: r.name,
|
|
210
217
|
Type: r.type,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseBuilder } from
|
|
2
|
-
import { CloudFrontBuilder } from
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { CloudFrontBuilder } from "./cloudfront.js";
|
|
3
3
|
export declare class S3BucketBuilder extends BaseBuilder {
|
|
4
4
|
bucketName: string;
|
|
5
5
|
private _versioning;
|
package/dist/providers/aws/s3.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { basename, extname } from
|
|
3
|
-
import { HeadBucketCommand, CreateBucketCommand, GetBucketPolicyCommand, PutBucketPolicyCommand, PutObjectCommand, } from
|
|
4
|
-
import { BaseBuilder } from
|
|
5
|
-
import { getS3Client } from
|
|
6
|
-
import { Config } from
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import { HeadBucketCommand, CreateBucketCommand, GetBucketPolicyCommand, PutBucketPolicyCommand, PutObjectCommand, } from "@aws-sdk/client-s3";
|
|
4
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
5
|
+
import { getS3Client } from "./api.js";
|
|
6
|
+
import { Config } from "../../core/config.js";
|
|
7
7
|
export class S3BucketBuilder extends BaseBuilder {
|
|
8
8
|
bucketName;
|
|
9
9
|
_versioning = false;
|
|
@@ -27,11 +27,11 @@ export class S3BucketBuilder extends BaseBuilder {
|
|
|
27
27
|
}
|
|
28
28
|
catch (e) {
|
|
29
29
|
const status = e.$metadata?.httpStatusCode;
|
|
30
|
-
if (status === 404 || e.name ===
|
|
30
|
+
if (status === 404 || e.name === "NotFound")
|
|
31
31
|
return false;
|
|
32
32
|
if (status === 301 || status === 403)
|
|
33
33
|
return true; // exists in different region or access denied
|
|
34
|
-
if (e.name ===
|
|
34
|
+
if (e.name === "CredentialsProviderError")
|
|
35
35
|
return false;
|
|
36
36
|
throw e;
|
|
37
37
|
}
|
|
@@ -51,7 +51,7 @@ export class S3BucketBuilder extends BaseBuilder {
|
|
|
51
51
|
async deploy() {
|
|
52
52
|
const dryRun = this.isDryRunActive();
|
|
53
53
|
const exists = await this.discoveryPromise;
|
|
54
|
-
const region = this._region ?? Config.get().providers.aws?.region ??
|
|
54
|
+
const region = this._region ?? Config.get().providers.aws?.region ?? "us-east-1";
|
|
55
55
|
const s3 = getS3Client(region);
|
|
56
56
|
console.log(`\nšŖ£ Finalizing S3 Bucket "${this.bucketName}"...`);
|
|
57
57
|
if (!exists) {
|
|
@@ -60,7 +60,7 @@ export class S3BucketBuilder extends BaseBuilder {
|
|
|
60
60
|
}
|
|
61
61
|
else {
|
|
62
62
|
const createCmd = { Bucket: this.bucketName };
|
|
63
|
-
if (region !==
|
|
63
|
+
if (region !== "us-east-1") {
|
|
64
64
|
createCmd.CreateBucketConfiguration = { LocationConstraint: region };
|
|
65
65
|
}
|
|
66
66
|
await s3.send(new CreateBucketCommand(createCmd));
|
|
@@ -71,13 +71,13 @@ export class S3BucketBuilder extends BaseBuilder {
|
|
|
71
71
|
console.log(` ā
Bucket ${this.bucketName} already exists.`);
|
|
72
72
|
}
|
|
73
73
|
if (this._allowedDistributions.length > 0) {
|
|
74
|
-
const unresolved = this._allowedDistributions.filter(d => !d.resolvedArn);
|
|
74
|
+
const unresolved = this._allowedDistributions.filter((d) => !d.resolvedArn);
|
|
75
75
|
if (unresolved.length > 0) {
|
|
76
76
|
throw new Error(`[S3:${this.bucketName}] allowFrom() has unresolved distributions: ` +
|
|
77
|
-
unresolved.map(d => `"${d.name}"`).join(
|
|
78
|
-
|
|
77
|
+
unresolved.map((d) => `"${d.name}"`).join(", ") +
|
|
78
|
+
". Declare the bucket after all CloudFront distributions in your Stack.");
|
|
79
79
|
}
|
|
80
|
-
const newArns = this._allowedDistributions.map(d => d.resolvedArn);
|
|
80
|
+
const newArns = this._allowedDistributions.map((d) => d.resolvedArn);
|
|
81
81
|
if (dryRun) {
|
|
82
82
|
console.log(` š [PLAN] Append ${newArns.length} CloudFront OAC ARN(s) to bucket policy`);
|
|
83
83
|
for (const arn of newArns)
|
|
@@ -102,15 +102,16 @@ export class S3BucketBuilder extends BaseBuilder {
|
|
|
102
102
|
const key = basename(filePath);
|
|
103
103
|
const body = readFileSync(filePath);
|
|
104
104
|
const contentTypeMap = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
".json": "application/json",
|
|
106
|
+
".js": "application/javascript",
|
|
107
|
+
".html": "text/html",
|
|
108
|
+
".css": "text/css",
|
|
109
|
+
".png": "image/png",
|
|
110
|
+
".jpg": "image/jpeg",
|
|
111
|
+
".svg": "image/svg+xml",
|
|
112
112
|
};
|
|
113
|
-
const contentType = contentTypeMap[extname(filePath).toLowerCase()] ??
|
|
113
|
+
const contentType = contentTypeMap[extname(filePath).toLowerCase()] ??
|
|
114
|
+
"application/octet-stream";
|
|
114
115
|
await s3.send(new PutObjectCommand({
|
|
115
116
|
Bucket: this.bucketName,
|
|
116
117
|
Key: key,
|
|
@@ -120,45 +121,51 @@ export class S3BucketBuilder extends BaseBuilder {
|
|
|
120
121
|
console.log(` ā
Uploaded ${key} ā s3://${this.bucketName}/${key}`);
|
|
121
122
|
}
|
|
122
123
|
async updateBucketPolicy(s3, newArns) {
|
|
123
|
-
let policy = { Version:
|
|
124
|
+
let policy = { Version: "2012-10-17", Statement: [] };
|
|
124
125
|
try {
|
|
125
126
|
const existing = await s3.send(new GetBucketPolicyCommand({ Bucket: this.bucketName }));
|
|
126
127
|
if (existing.Policy)
|
|
127
128
|
policy = JSON.parse(existing.Policy);
|
|
128
129
|
}
|
|
129
130
|
catch (e) {
|
|
130
|
-
if (e.name !==
|
|
131
|
+
if (e.name !== "NoSuchBucketPolicy")
|
|
131
132
|
throw e;
|
|
132
133
|
}
|
|
133
134
|
// Find any existing CloudFront-principal statement regardless of Sid
|
|
134
|
-
let stmt = policy.Statement.find((s) => s.Principal?.Service ===
|
|
135
|
+
let stmt = policy.Statement.find((s) => s.Principal?.Service === "cloudfront.amazonaws.com" &&
|
|
136
|
+
s.Effect === "Allow");
|
|
135
137
|
if (!stmt) {
|
|
136
138
|
stmt = {
|
|
137
|
-
Sid:
|
|
138
|
-
Effect:
|
|
139
|
-
Principal: { Service:
|
|
140
|
-
Action:
|
|
139
|
+
Sid: "AllowCloudFrontServicePrincipal",
|
|
140
|
+
Effect: "Allow",
|
|
141
|
+
Principal: { Service: "cloudfront.amazonaws.com" },
|
|
142
|
+
Action: "s3:GetObject",
|
|
141
143
|
Resource: `arn:aws:s3:::${this.bucketName}/*`,
|
|
142
|
-
Condition: { StringEquals: {
|
|
144
|
+
Condition: { StringEquals: { "AWS:SourceArn": [] } },
|
|
143
145
|
};
|
|
144
146
|
policy.Statement.push(stmt);
|
|
145
147
|
}
|
|
146
148
|
// Condition key may be 'aws:SourceArn' or 'AWS:SourceArn' depending on how it was created
|
|
147
149
|
const cond = stmt.Condition?.StringEquals ?? {};
|
|
148
|
-
const sourceArnKey = Object.keys(cond).find(k => k.toLowerCase() ===
|
|
150
|
+
const sourceArnKey = Object.keys(cond).find((k) => k.toLowerCase() === "aws:sourcearn") ??
|
|
151
|
+
"AWS:SourceArn";
|
|
149
152
|
if (!stmt.Condition)
|
|
150
153
|
stmt.Condition = { StringEquals: {} };
|
|
151
154
|
if (!stmt.Condition.StringEquals)
|
|
152
155
|
stmt.Condition.StringEquals = {};
|
|
153
156
|
const existing = stmt.Condition.StringEquals[sourceArnKey];
|
|
154
|
-
const existingArns = Array.isArray(existing)
|
|
157
|
+
const existingArns = Array.isArray(existing)
|
|
158
|
+
? existing
|
|
159
|
+
: existing
|
|
160
|
+
? [existing]
|
|
161
|
+
: [];
|
|
155
162
|
const merged = [...new Set([...existingArns, ...newArns])];
|
|
156
163
|
stmt.Condition.StringEquals[sourceArnKey] = merged;
|
|
157
164
|
await s3.send(new PutBucketPolicyCommand({
|
|
158
165
|
Bucket: this.bucketName,
|
|
159
166
|
Policy: JSON.stringify(policy),
|
|
160
167
|
}));
|
|
161
|
-
console.log(` ā
Updated bucket policy
|
|
168
|
+
console.log(` ā
Updated bucket policy - ${merged.length} distribution ARN(s) allowed`);
|
|
162
169
|
for (const arn of newArns)
|
|
163
170
|
console.log(` āā ${arn}`);
|
|
164
171
|
}
|