puls-dev 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +148 -0
- package/dist/core/checker.d.ts +5 -0
- package/dist/core/checker.js +148 -0
- package/dist/core/config.d.ts +35 -0
- package/dist/core/config.js +15 -0
- package/dist/core/decorators.d.ts +26 -0
- package/dist/core/decorators.js +86 -0
- package/dist/core/output.d.ts +8 -0
- package/dist/core/output.js +19 -0
- package/dist/core/resource.d.ts +20 -0
- package/dist/core/resource.js +77 -0
- package/dist/core/stack.d.ts +20 -0
- package/dist/core/stack.js +120 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/providers/aws/acm.d.ts +22 -0
- package/dist/providers/aws/acm.js +109 -0
- package/dist/providers/aws/api.d.ts +28 -0
- package/dist/providers/aws/api.js +36 -0
- package/dist/providers/aws/apigateway.d.ts +24 -0
- package/dist/providers/aws/apigateway.js +157 -0
- package/dist/providers/aws/cloudfront.d.ts +31 -0
- package/dist/providers/aws/cloudfront.js +205 -0
- package/dist/providers/aws/fargate.d.ts +43 -0
- package/dist/providers/aws/fargate.js +277 -0
- package/dist/providers/aws/index.d.ts +23 -0
- package/dist/providers/aws/index.js +29 -0
- package/dist/providers/aws/lambda.d.ts +30 -0
- package/dist/providers/aws/lambda.js +159 -0
- package/dist/providers/aws/list.d.ts +2 -0
- package/dist/providers/aws/list.js +44 -0
- package/dist/providers/aws/rds.d.ts +46 -0
- package/dist/providers/aws/rds.js +227 -0
- package/dist/providers/aws/route53.d.ts +38 -0
- package/dist/providers/aws/route53.js +218 -0
- package/dist/providers/aws/s3.d.ts +20 -0
- package/dist/providers/aws/s3.js +165 -0
- package/dist/providers/aws/secrets.d.ts +25 -0
- package/dist/providers/aws/secrets.js +151 -0
- package/dist/providers/aws/sqs.d.ts +33 -0
- package/dist/providers/aws/sqs.js +178 -0
- package/dist/providers/do/api.d.ts +11 -0
- package/dist/providers/do/api.js +52 -0
- package/dist/providers/do/certificate.d.ts +7 -0
- package/dist/providers/do/certificate.js +36 -0
- package/dist/providers/do/domain.d.ts +21 -0
- package/dist/providers/do/domain.js +81 -0
- package/dist/providers/do/droplet.d.ts +35 -0
- package/dist/providers/do/droplet.js +180 -0
- package/dist/providers/do/firewall.d.ts +23 -0
- package/dist/providers/do/firewall.js +94 -0
- package/dist/providers/do/index.d.ts +15 -0
- package/dist/providers/do/index.js +21 -0
- package/dist/providers/do/list.d.ts +2 -0
- package/dist/providers/do/list.js +59 -0
- package/dist/providers/do/load_balancer.d.ts +12 -0
- package/dist/providers/do/load_balancer.js +62 -0
- package/dist/providers/firebase/api.d.ts +4 -0
- package/dist/providers/firebase/api.js +62 -0
- package/dist/providers/firebase/auth.d.ts +35 -0
- package/dist/providers/firebase/auth.js +147 -0
- package/dist/providers/firebase/firestore.d.ts +28 -0
- package/dist/providers/firebase/firestore.js +120 -0
- package/dist/providers/firebase/functions.d.ts +50 -0
- package/dist/providers/firebase/functions.js +163 -0
- package/dist/providers/firebase/hosting.d.ts +14 -0
- package/dist/providers/firebase/hosting.js +144 -0
- package/dist/providers/firebase/index.d.ts +15 -0
- package/dist/providers/firebase/index.js +15 -0
- package/dist/providers/firebase/remoteconfig.d.ts +22 -0
- package/dist/providers/firebase/remoteconfig.js +135 -0
- package/dist/providers/firebase/storage.d.ts +34 -0
- package/dist/providers/firebase/storage.js +117 -0
- package/dist/providers/proxmox/api.d.ts +12 -0
- package/dist/providers/proxmox/api.js +50 -0
- package/dist/providers/proxmox/index.d.ts +15 -0
- package/dist/providers/proxmox/index.js +10 -0
- package/dist/providers/proxmox/list.d.ts +2 -0
- package/dist/providers/proxmox/list.js +15 -0
- package/dist/providers/proxmox/vm.d.ts +61 -0
- package/dist/providers/proxmox/vm.js +482 -0
- package/dist/types/aws.d.ts +55 -0
- package/dist/types/aws.js +48 -0
- package/dist/types/do.d.ts +19 -0
- package/dist/types/do.js +19 -0
- package/dist/types/gcp.d.ts +9 -0
- package/dist/types/gcp.js +9 -0
- package/dist/types/inventory.d.ts +87 -0
- package/dist/types/inventory.js +2 -0
- package/dist/types/proxmox.d.ts +11 -0
- package/dist/types/proxmox.js +28 -0
- package/package.json +56 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readFileSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, extname } from 'node:path';
|
|
5
|
+
import { GetFunctionCommand, CreateFunctionCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, DeleteFunctionCommand, } from '@aws-sdk/client-lambda';
|
|
6
|
+
import { GetRoleCommand, CreateRoleCommand, AttachRolePolicyCommand, } from '@aws-sdk/client-iam';
|
|
7
|
+
import { BaseBuilder } from '../../core/resource.js';
|
|
8
|
+
import { getLambdaClient, getIAMClient } from './api.js';
|
|
9
|
+
import { resolveEnvVars } from './secrets.js';
|
|
10
|
+
const ASSUME_ROLE_POLICY = JSON.stringify({
|
|
11
|
+
Version: '2012-10-17',
|
|
12
|
+
Statement: [{ Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, Action: 'sts:AssumeRole' }],
|
|
13
|
+
});
|
|
14
|
+
export class LambdaBuilder extends BaseBuilder {
|
|
15
|
+
_runtime = 'nodejs20.x';
|
|
16
|
+
_handler = 'index.handler';
|
|
17
|
+
_memory = 128;
|
|
18
|
+
_timeout = 30;
|
|
19
|
+
_codePath;
|
|
20
|
+
_env = {};
|
|
21
|
+
_roleArn;
|
|
22
|
+
resolvedArn = null;
|
|
23
|
+
constructor(name) {
|
|
24
|
+
super(name);
|
|
25
|
+
this.discoveryPromise = this.discoverFunction(name);
|
|
26
|
+
}
|
|
27
|
+
async discoverFunction(name) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await getLambdaClient().send(new GetFunctionCommand({ FunctionName: name }));
|
|
30
|
+
this.resolvedArn = result.Configuration?.FunctionArn ?? null;
|
|
31
|
+
return result.Configuration ?? null;
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
if (e.name === 'ResourceNotFoundException')
|
|
35
|
+
return null;
|
|
36
|
+
if (e.name === 'CredentialsProviderError')
|
|
37
|
+
return null;
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
code(pathOrZip) { this._codePath = pathOrZip; return this; }
|
|
42
|
+
runtime(r) { this._runtime = r; return this; }
|
|
43
|
+
handler(h) { this._handler = h; return this; }
|
|
44
|
+
memory(mb) { this._memory = mb; return this; }
|
|
45
|
+
timeout(seconds) { this._timeout = seconds; return this; }
|
|
46
|
+
role(arn) { this._roleArn = arn; return this; }
|
|
47
|
+
env(vars) { this._env = { ...this._env, ...vars }; return this; }
|
|
48
|
+
async ensureRole() {
|
|
49
|
+
if (this._roleArn)
|
|
50
|
+
return this._roleArn;
|
|
51
|
+
const roleName = `puls-lambda-${this.name}-role`;
|
|
52
|
+
const iam = getIAMClient();
|
|
53
|
+
try {
|
|
54
|
+
const existing = await iam.send(new GetRoleCommand({ RoleName: roleName }));
|
|
55
|
+
return existing.Role.Arn;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (e.name !== 'NoSuchEntityException')
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
const created = await iam.send(new CreateRoleCommand({
|
|
62
|
+
RoleName: roleName,
|
|
63
|
+
AssumeRolePolicyDocument: ASSUME_ROLE_POLICY,
|
|
64
|
+
Description: `Execution role for OpsDSL Lambda "${this.name}"`,
|
|
65
|
+
}));
|
|
66
|
+
await iam.send(new AttachRolePolicyCommand({
|
|
67
|
+
RoleName: roleName,
|
|
68
|
+
PolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
|
|
69
|
+
}));
|
|
70
|
+
console.log(` ā
Created execution role: ${roleName}`);
|
|
71
|
+
// IAM propagation ā Lambda rejects a brand-new role for ~10s
|
|
72
|
+
await new Promise(r => setTimeout(r, 10_000));
|
|
73
|
+
return created.Role.Arn;
|
|
74
|
+
}
|
|
75
|
+
buildZip() {
|
|
76
|
+
if (!this._codePath)
|
|
77
|
+
throw new Error(`[Lambda:${this.name}] .code(path) is required`);
|
|
78
|
+
if (extname(this._codePath) === '.zip') {
|
|
79
|
+
return readFileSync(this._codePath);
|
|
80
|
+
}
|
|
81
|
+
const outPath = join(tmpdir(), `puls-lambda-${this.name}-${Date.now()}.zip`);
|
|
82
|
+
execSync(`cd "${this._codePath}" && zip -r "${outPath}" .`, { stdio: 'pipe' });
|
|
83
|
+
const buf = readFileSync(outPath);
|
|
84
|
+
unlinkSync(outPath);
|
|
85
|
+
return buf;
|
|
86
|
+
}
|
|
87
|
+
async deploy() {
|
|
88
|
+
const dryRun = this.isDryRunActive();
|
|
89
|
+
const existing = await this.discoveryPromise;
|
|
90
|
+
const lambda = getLambdaClient();
|
|
91
|
+
console.log(`\nā” Finalizing Lambda Function "${this.name}"...`);
|
|
92
|
+
if (dryRun) {
|
|
93
|
+
console.log(` š [PLAN] ${existing ? 'Update' : 'Create'} function "${this.name}"`);
|
|
94
|
+
console.log(` āā Runtime: ${this._runtime} | Handler: ${this._handler}`);
|
|
95
|
+
console.log(` āā Memory: ${this._memory}MB | Timeout: ${this._timeout}s`);
|
|
96
|
+
if (this._codePath)
|
|
97
|
+
console.log(` āā Code: ${this._codePath}`);
|
|
98
|
+
if (Object.keys(this._env).length)
|
|
99
|
+
console.log(` āā Env vars: ${Object.keys(this._env).join(', ')}`);
|
|
100
|
+
this.resolvedArn = `arn:aws:lambda:DRYRUN:000000000000:function:${this.name}`;
|
|
101
|
+
return { name: this.name, arn: this.resolvedArn };
|
|
102
|
+
}
|
|
103
|
+
const [roleArn, resolvedEnv] = await Promise.all([
|
|
104
|
+
this.ensureRole(),
|
|
105
|
+
resolveEnvVars(this._env),
|
|
106
|
+
]);
|
|
107
|
+
const config = {
|
|
108
|
+
FunctionName: this.name,
|
|
109
|
+
Runtime: this._runtime,
|
|
110
|
+
Handler: this._handler,
|
|
111
|
+
MemorySize: this._memory,
|
|
112
|
+
Timeout: this._timeout,
|
|
113
|
+
Role: roleArn,
|
|
114
|
+
Environment: Object.keys(resolvedEnv).length ? { Variables: resolvedEnv } : undefined,
|
|
115
|
+
};
|
|
116
|
+
if (existing) {
|
|
117
|
+
this.resolvedArn = existing.FunctionArn;
|
|
118
|
+
await lambda.send(new UpdateFunctionConfigurationCommand(config));
|
|
119
|
+
if (this._codePath) {
|
|
120
|
+
await lambda.send(new UpdateFunctionCodeCommand({
|
|
121
|
+
FunctionName: this.name,
|
|
122
|
+
ZipFile: this.buildZip(),
|
|
123
|
+
}));
|
|
124
|
+
console.log(` ā
Updated function "${this.name}" (code + config)`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log(` ā
Updated function "${this.name}" (config only)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (!this._codePath)
|
|
132
|
+
throw new Error(`[Lambda:${this.name}] .code(path) is required to create a function`);
|
|
133
|
+
const result = await lambda.send(new CreateFunctionCommand({
|
|
134
|
+
...config,
|
|
135
|
+
Code: { ZipFile: this.buildZip() },
|
|
136
|
+
}));
|
|
137
|
+
this.resolvedArn = result.FunctionArn;
|
|
138
|
+
console.log(`š Created function "${this.name}" (arn=${this.resolvedArn})`);
|
|
139
|
+
}
|
|
140
|
+
await this.deploySidecars();
|
|
141
|
+
return { name: this.name, arn: this.resolvedArn };
|
|
142
|
+
}
|
|
143
|
+
async destroy() {
|
|
144
|
+
const dryRun = this.isDryRunActive();
|
|
145
|
+
const existing = await this.discoveryPromise;
|
|
146
|
+
console.log(`\nšļø Destroying Lambda Function "${this.name}"...`);
|
|
147
|
+
if (!existing) {
|
|
148
|
+
console.log(` ā
Function "${this.name}" does not exist ā nothing to do`);
|
|
149
|
+
return { destroyed: this.name };
|
|
150
|
+
}
|
|
151
|
+
if (dryRun) {
|
|
152
|
+
console.log(` š [PLAN] Delete function "${this.name}"`);
|
|
153
|
+
return { destroyed: this.name };
|
|
154
|
+
}
|
|
155
|
+
await getLambdaClient().send(new DeleteFunctionCommand({ FunctionName: this.name }));
|
|
156
|
+
console.log(` ā
Deleted function "${this.name}"`);
|
|
157
|
+
return { destroyed: this.name };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ListDistributionsCommand } from '@aws-sdk/client-cloudfront';
|
|
2
|
+
import { ListBucketsCommand } from '@aws-sdk/client-s3';
|
|
3
|
+
import { ListFunctionsCommand } from '@aws-sdk/client-lambda';
|
|
4
|
+
import { DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
|
|
5
|
+
import { ListHostedZonesCommand } from '@aws-sdk/client-route-53';
|
|
6
|
+
import { getCFClient, getS3Client, getLambdaClient, getRDSClient, getR53Client } from './api.js';
|
|
7
|
+
import { Config } from '../../core/config.js';
|
|
8
|
+
export async function listAwsResources() {
|
|
9
|
+
const region = Config.get().providers.aws.region;
|
|
10
|
+
const [cfResult, s3Result, lambdaResult, rdsResult, r53Result] = await Promise.all([
|
|
11
|
+
getCFClient().send(new ListDistributionsCommand({})),
|
|
12
|
+
getS3Client().send(new ListBucketsCommand({})),
|
|
13
|
+
getLambdaClient().send(new ListFunctionsCommand({ MaxItems: 50 })),
|
|
14
|
+
getRDSClient().send(new DescribeDBInstancesCommand({})),
|
|
15
|
+
getR53Client().send(new ListHostedZonesCommand({})),
|
|
16
|
+
]);
|
|
17
|
+
const distributions = (cfResult.DistributionList?.Items ?? []).map((d) => ({
|
|
18
|
+
id: d.Id,
|
|
19
|
+
domain: d.DomainName,
|
|
20
|
+
aliases: d.Aliases?.Items ?? [],
|
|
21
|
+
status: d.Status ?? '',
|
|
22
|
+
}));
|
|
23
|
+
const buckets = (s3Result.Buckets ?? []).map((b) => ({
|
|
24
|
+
name: b.Name,
|
|
25
|
+
}));
|
|
26
|
+
const lambdas = (lambdaResult.Functions ?? []).map((f) => ({
|
|
27
|
+
name: f.FunctionName,
|
|
28
|
+
runtime: f.Runtime ?? 'unknown',
|
|
29
|
+
memorySizeMb: f.MemorySize ?? 0,
|
|
30
|
+
}));
|
|
31
|
+
const rdsInstances = (rdsResult.DBInstances ?? []).map((i) => ({
|
|
32
|
+
identifier: i.DBInstanceIdentifier,
|
|
33
|
+
engine: `${i.Engine} ${i.EngineVersion}`,
|
|
34
|
+
instanceClass: i.DBInstanceClass,
|
|
35
|
+
status: i.DBInstanceStatus ?? '',
|
|
36
|
+
endpoint: i.Endpoint?.Address,
|
|
37
|
+
}));
|
|
38
|
+
const hostedZones = (r53Result.HostedZones ?? []).map((z) => ({
|
|
39
|
+
name: z.Name,
|
|
40
|
+
id: z.Id.replace('/hostedzone/', ''),
|
|
41
|
+
recordCount: z.ResourceRecordSetCount ?? 0,
|
|
42
|
+
}));
|
|
43
|
+
return { region, distributions, buckets, lambdas, rdsInstances, hostedZones };
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BaseBuilder } from '../../core/resource.js';
|
|
2
|
+
export declare class RDSBuilder extends BaseBuilder {
|
|
3
|
+
private _engine;
|
|
4
|
+
private _engineVersion;
|
|
5
|
+
private _instanceClass;
|
|
6
|
+
private _storage;
|
|
7
|
+
private _username?;
|
|
8
|
+
private _password?;
|
|
9
|
+
private _dbName?;
|
|
10
|
+
private _subnetIds?;
|
|
11
|
+
private _securityGroupIds?;
|
|
12
|
+
private _publicAccess;
|
|
13
|
+
resolvedEndpoint: string | null;
|
|
14
|
+
resolvedPort: number | null;
|
|
15
|
+
resolvedArn: string | null;
|
|
16
|
+
constructor(name: string);
|
|
17
|
+
engine(e: {
|
|
18
|
+
engine: string;
|
|
19
|
+
version: string;
|
|
20
|
+
}): this;
|
|
21
|
+
size(instanceClass: string): this;
|
|
22
|
+
storage(gb: number): this;
|
|
23
|
+
subnets(ids: string[]): this;
|
|
24
|
+
securityGroups(ids: string[]): this;
|
|
25
|
+
publicAccess(enabled?: boolean): this;
|
|
26
|
+
database(name: string): this;
|
|
27
|
+
credentials(username: string, password: string): this;
|
|
28
|
+
private discoverInstance;
|
|
29
|
+
private discoverDefaultVpc;
|
|
30
|
+
private ensureSubnetGroup;
|
|
31
|
+
private ensureSecurityGroup;
|
|
32
|
+
deploy(): Promise<{
|
|
33
|
+
name: string;
|
|
34
|
+
endpoint: string;
|
|
35
|
+
port: number;
|
|
36
|
+
arn?: undefined;
|
|
37
|
+
} | {
|
|
38
|
+
name: string;
|
|
39
|
+
endpoint: string | null;
|
|
40
|
+
port: number | null;
|
|
41
|
+
arn: string | null;
|
|
42
|
+
}>;
|
|
43
|
+
destroy(): Promise<{
|
|
44
|
+
destroyed: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
const DB_PORT = {
|
|
7
|
+
postgres: 5432,
|
|
8
|
+
mysql: 3306,
|
|
9
|
+
mariadb: 3306,
|
|
10
|
+
};
|
|
11
|
+
export class RDSBuilder extends BaseBuilder {
|
|
12
|
+
_engine = 'postgres';
|
|
13
|
+
_engineVersion = '16';
|
|
14
|
+
_instanceClass = 'db.t3.micro';
|
|
15
|
+
_storage = 20;
|
|
16
|
+
_username;
|
|
17
|
+
_password;
|
|
18
|
+
_dbName;
|
|
19
|
+
_subnetIds;
|
|
20
|
+
_securityGroupIds;
|
|
21
|
+
_publicAccess = false;
|
|
22
|
+
resolvedEndpoint = null;
|
|
23
|
+
resolvedPort = null;
|
|
24
|
+
resolvedArn = null;
|
|
25
|
+
constructor(name) {
|
|
26
|
+
super(name);
|
|
27
|
+
this.discoveryPromise = this.discoverInstance(name);
|
|
28
|
+
}
|
|
29
|
+
engine(e) {
|
|
30
|
+
this._engine = e.engine;
|
|
31
|
+
this._engineVersion = e.version;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
size(instanceClass) { this._instanceClass = instanceClass; return this; }
|
|
35
|
+
storage(gb) { this._storage = gb; return this; }
|
|
36
|
+
subnets(ids) { this._subnetIds = ids; return this; }
|
|
37
|
+
securityGroups(ids) { this._securityGroupIds = ids; return this; }
|
|
38
|
+
publicAccess(enabled = true) { this._publicAccess = enabled; return this; }
|
|
39
|
+
database(name) { this._dbName = name; return this; }
|
|
40
|
+
credentials(username, password) {
|
|
41
|
+
this._username = username;
|
|
42
|
+
this._password = password;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
async discoverInstance(identifier) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await getRDSClient().send(new DescribeDBInstancesCommand({
|
|
48
|
+
DBInstanceIdentifier: identifier,
|
|
49
|
+
}));
|
|
50
|
+
const instance = result.DBInstances?.[0];
|
|
51
|
+
if (!instance || instance.DBInstanceStatus === 'deleting')
|
|
52
|
+
return null;
|
|
53
|
+
this.resolvedArn = instance.DBInstanceArn ?? null;
|
|
54
|
+
this.resolvedEndpoint = instance.Endpoint?.Address ?? null;
|
|
55
|
+
this.resolvedPort = instance.Endpoint?.Port ?? null;
|
|
56
|
+
return instance;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e.name === 'DBInstanceNotFound')
|
|
60
|
+
return null;
|
|
61
|
+
if (e.name === 'CredentialsProviderError')
|
|
62
|
+
return null;
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async discoverDefaultVpc() {
|
|
67
|
+
const ec2 = getEC2Client();
|
|
68
|
+
const vpcs = await ec2.send(new DescribeVpcsCommand({
|
|
69
|
+
Filters: [{ Name: 'isDefault', Values: ['true'] }],
|
|
70
|
+
}));
|
|
71
|
+
const vpc = vpcs.Vpcs?.[0];
|
|
72
|
+
if (!vpc?.VpcId)
|
|
73
|
+
throw new Error(`[RDS:${this.name}] No default VPC found. Use .subnets(ids[]) to specify subnets.`);
|
|
74
|
+
const subnets = await ec2.send(new DescribeSubnetsCommand({
|
|
75
|
+
Filters: [{ Name: 'vpc-id', Values: [vpc.VpcId] }],
|
|
76
|
+
}));
|
|
77
|
+
const subnetIds = (subnets.Subnets ?? []).map(s => s.SubnetId).filter(Boolean);
|
|
78
|
+
return { vpcId: vpc.VpcId, vpcCidr: vpc.CidrBlock, subnetIds };
|
|
79
|
+
}
|
|
80
|
+
async ensureSubnetGroup(subnetIds) {
|
|
81
|
+
const rds = getRDSClient();
|
|
82
|
+
const groupName = `puls-${this.name}-subnet-group`;
|
|
83
|
+
try {
|
|
84
|
+
await rds.send(new DescribeDBSubnetGroupsCommand({ DBSubnetGroupName: groupName }));
|
|
85
|
+
return groupName;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (e.name !== 'DBSubnetGroupNotFoundFault')
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
await rds.send(new CreateDBSubnetGroupCommand({
|
|
92
|
+
DBSubnetGroupName: groupName,
|
|
93
|
+
DBSubnetGroupDescription: `OpsDSL subnet group for RDS instance "${this.name}"`,
|
|
94
|
+
SubnetIds: subnetIds,
|
|
95
|
+
}));
|
|
96
|
+
console.log(` ā
Created DB subnet group: ${groupName}`);
|
|
97
|
+
return groupName;
|
|
98
|
+
}
|
|
99
|
+
async ensureSecurityGroup(vpcId, vpcCidr) {
|
|
100
|
+
const ec2 = getEC2Client();
|
|
101
|
+
const sgName = `puls-${this.name}-db-sg`;
|
|
102
|
+
const port = DB_PORT[this._engine] ?? 5432;
|
|
103
|
+
const existing = await ec2.send(new DescribeSecurityGroupsCommand({
|
|
104
|
+
Filters: [
|
|
105
|
+
{ Name: 'group-name', Values: [sgName] },
|
|
106
|
+
{ Name: 'vpc-id', Values: [vpcId] },
|
|
107
|
+
],
|
|
108
|
+
}));
|
|
109
|
+
if (existing.SecurityGroups?.length)
|
|
110
|
+
return existing.SecurityGroups[0].GroupId;
|
|
111
|
+
const created = await ec2.send(new CreateSecurityGroupCommand({
|
|
112
|
+
GroupName: sgName,
|
|
113
|
+
Description: `OpsDSL DB access for "${this.name}"`,
|
|
114
|
+
VpcId: vpcId,
|
|
115
|
+
}));
|
|
116
|
+
const sgId = created.GroupId;
|
|
117
|
+
// Allow inbound on DB port from VPC CIDR only (secure default)
|
|
118
|
+
// Use .publicAccess() to open to the internet
|
|
119
|
+
const cidr = this._publicAccess ? '0.0.0.0/0' : vpcCidr;
|
|
120
|
+
await ec2.send(new AuthorizeSecurityGroupIngressCommand({
|
|
121
|
+
GroupId: sgId,
|
|
122
|
+
IpPermissions: [{
|
|
123
|
+
IpProtocol: 'tcp',
|
|
124
|
+
FromPort: port,
|
|
125
|
+
ToPort: port,
|
|
126
|
+
IpRanges: [{ CidrIp: cidr, Description: this._publicAccess ? 'Public access' : 'VPC only' }],
|
|
127
|
+
}],
|
|
128
|
+
}));
|
|
129
|
+
console.log(` ā
Created security group: ${sgName} (${sgId}) ā port ${port} open to ${cidr}`);
|
|
130
|
+
return sgId;
|
|
131
|
+
}
|
|
132
|
+
async deploy() {
|
|
133
|
+
const dryRun = this.isDryRunActive();
|
|
134
|
+
const existing = await this.discoveryPromise;
|
|
135
|
+
const region = Config.get().providers.aws?.region ?? 'us-east-1';
|
|
136
|
+
const port = DB_PORT[this._engine] ?? 5432;
|
|
137
|
+
console.log(`\nā” Finalizing RDS Instance "${this.name}"...`);
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
console.log(` š [PLAN] ${existing ? 'Update' : 'Create'} RDS instance "${this.name}"`);
|
|
140
|
+
console.log(` āā Engine: ${this._engine} ${this._engineVersion}`);
|
|
141
|
+
console.log(` āā Class: ${this._instanceClass} | Storage: ${this._storage}GB`);
|
|
142
|
+
console.log(` āā Port: ${port} | Public: ${this._publicAccess}`);
|
|
143
|
+
this.resolvedEndpoint = `${this.name}.DRYRUN.${region}.rds.amazonaws.com`;
|
|
144
|
+
this.resolvedPort = port;
|
|
145
|
+
return { name: this.name, endpoint: this.resolvedEndpoint, port: this.resolvedPort };
|
|
146
|
+
}
|
|
147
|
+
if (!this._username || !this._password) {
|
|
148
|
+
throw new Error(`[RDS:${this.name}] .credentials(username, password) is required`);
|
|
149
|
+
}
|
|
150
|
+
// Resolve networking ā only hit EC2 if needed
|
|
151
|
+
let subnetGroupName;
|
|
152
|
+
let securityGroupIds;
|
|
153
|
+
if (this._subnetIds && this._securityGroupIds) {
|
|
154
|
+
subnetGroupName = await this.ensureSubnetGroup(this._subnetIds);
|
|
155
|
+
securityGroupIds = this._securityGroupIds;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const { vpcId, vpcCidr, subnetIds } = await this.discoverDefaultVpc();
|
|
159
|
+
subnetGroupName = await this.ensureSubnetGroup(this._subnetIds ?? subnetIds);
|
|
160
|
+
securityGroupIds = this._securityGroupIds ?? [await this.ensureSecurityGroup(vpcId, vpcCidr)];
|
|
161
|
+
}
|
|
162
|
+
const rds = getRDSClient();
|
|
163
|
+
if (existing) {
|
|
164
|
+
await rds.send(new ModifyDBInstanceCommand({
|
|
165
|
+
DBInstanceIdentifier: this.name,
|
|
166
|
+
DBInstanceClass: this._instanceClass,
|
|
167
|
+
AllocatedStorage: this._storage,
|
|
168
|
+
VpcSecurityGroupIds: securityGroupIds,
|
|
169
|
+
ApplyImmediately: true,
|
|
170
|
+
}));
|
|
171
|
+
console.log(` ā
Modification queued for "${this.name}" (ApplyImmediately)`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await rds.send(new CreateDBInstanceCommand({
|
|
175
|
+
DBInstanceIdentifier: this.name,
|
|
176
|
+
DBInstanceClass: this._instanceClass,
|
|
177
|
+
Engine: this._engine,
|
|
178
|
+
EngineVersion: this._engineVersion,
|
|
179
|
+
MasterUsername: this._username,
|
|
180
|
+
MasterUserPassword: this._password,
|
|
181
|
+
AllocatedStorage: this._storage,
|
|
182
|
+
DBName: this._dbName,
|
|
183
|
+
DBSubnetGroupName: subnetGroupName,
|
|
184
|
+
VpcSecurityGroupIds: securityGroupIds,
|
|
185
|
+
PubliclyAccessible: this._publicAccess,
|
|
186
|
+
StorageType: 'gp3',
|
|
187
|
+
BackupRetentionPeriod: 7,
|
|
188
|
+
DeletionProtection: false,
|
|
189
|
+
}));
|
|
190
|
+
console.log(`š Creating RDS instance "${this.name}" ā waiting for available...`);
|
|
191
|
+
await this.waitFor(`"${this.name}" to become available`, async () => {
|
|
192
|
+
const r = await getRDSClient().send(new DescribeDBInstancesCommand({ DBInstanceIdentifier: this.name }));
|
|
193
|
+
const inst = r.DBInstances?.[0];
|
|
194
|
+
if (inst?.DBInstanceStatus === 'available') {
|
|
195
|
+
this.resolvedEndpoint = inst.Endpoint?.Address ?? null;
|
|
196
|
+
this.resolvedPort = inst.Endpoint?.Port ?? null;
|
|
197
|
+
this.resolvedArn = inst.DBInstanceArn ?? null;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}, { intervalMs: 30_000, timeoutMs: 1_200_000 });
|
|
202
|
+
console.log(` š Endpoint: ${this.resolvedEndpoint}:${this.resolvedPort}`);
|
|
203
|
+
}
|
|
204
|
+
await this.deploySidecars();
|
|
205
|
+
return { name: this.name, endpoint: this.resolvedEndpoint, port: this.resolvedPort, arn: this.resolvedArn };
|
|
206
|
+
}
|
|
207
|
+
async destroy() {
|
|
208
|
+
const dryRun = this.isDryRunActive();
|
|
209
|
+
const existing = await this.discoveryPromise;
|
|
210
|
+
console.log(`\nšļø Destroying RDS Instance "${this.name}"...`);
|
|
211
|
+
if (!existing) {
|
|
212
|
+
console.log(` ā
Instance "${this.name}" does not exist ā nothing to do`);
|
|
213
|
+
return { destroyed: this.name };
|
|
214
|
+
}
|
|
215
|
+
if (dryRun) {
|
|
216
|
+
console.log(` š [PLAN] Delete RDS instance "${this.name}" (no final snapshot)`);
|
|
217
|
+
return { destroyed: this.name };
|
|
218
|
+
}
|
|
219
|
+
await getRDSClient().send(new DeleteDBInstanceCommand({
|
|
220
|
+
DBInstanceIdentifier: this.name,
|
|
221
|
+
SkipFinalSnapshot: true,
|
|
222
|
+
DeleteAutomatedBackups: true,
|
|
223
|
+
}));
|
|
224
|
+
console.log(` ā
Deletion initiated for "${this.name}" (takes a few minutes)`);
|
|
225
|
+
return { destroyed: this.name };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
export declare class Route53Builder extends BaseBuilder {
|
|
6
|
+
readonly out: {
|
|
7
|
+
zone: Output<{
|
|
8
|
+
name: string;
|
|
9
|
+
id: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
zoneName: string;
|
|
13
|
+
zoneId?: string;
|
|
14
|
+
private records;
|
|
15
|
+
private _isRegistering;
|
|
16
|
+
private _registrantContact?;
|
|
17
|
+
private _wantsWildcardSSL;
|
|
18
|
+
constructor(zoneName?: string);
|
|
19
|
+
private discoverZone;
|
|
20
|
+
randomDomain(): this;
|
|
21
|
+
cert(): ACMCertificateBuilder | undefined;
|
|
22
|
+
withWildcardSSL(): this;
|
|
23
|
+
register(contact?: RegistrantContact): this;
|
|
24
|
+
record(name: string, type: 'A' | 'CNAME' | 'AAAA', value: string): this;
|
|
25
|
+
pointer(name: string, target: BaseBuilder): this;
|
|
26
|
+
deploy(): Promise<{
|
|
27
|
+
zone: string;
|
|
28
|
+
id: string | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
private registerDomain;
|
|
31
|
+
private mapContact;
|
|
32
|
+
private normalizePhone;
|
|
33
|
+
upsertCnames(records: {
|
|
34
|
+
name: string;
|
|
35
|
+
value: string;
|
|
36
|
+
}[]): Promise<void>;
|
|
37
|
+
private upsertRecords;
|
|
38
|
+
}
|