cdk-rds-sql 7.0.2 → 7.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/.jsii +309 -75
- package/.pre-commit-config.yaml +4 -0
- package/.prettierignore +6 -0
- package/README.md +107 -16
- package/lambda/engine.abstract.ts +16 -0
- package/lambda/engine.dsql.ts +302 -0
- package/lambda/engine.factory.ts +3 -0
- package/lambda/engine.mysql.ts +24 -0
- package/lambda/engine.postgresql.ts +24 -0
- package/lambda/handler.ts +81 -27
- package/lambda/types.ts +10 -0
- package/lib/database.js +14 -3
- package/lib/enum.d.ts +2 -1
- package/lib/enum.js +2 -1
- package/lib/handler/handler.js +24063 -23774
- package/lib/iam-grant.d.ts +20 -0
- package/lib/iam-grant.js +35 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +2 -1
- package/lib/provider.d.ts +15 -6
- package/lib/provider.js +77 -23
- package/lib/role.custom-resource.js +2 -2
- package/lib/role.d.ts +5 -1
- package/lib/role.js +75 -65
- package/lib/schema.d.ts +3 -1
- package/lib/schema.js +3 -3
- package/lib/sql.js +3 -3
- package/package.json +13 -9
package/.pre-commit-config.yaml
CHANGED
|
@@ -35,6 +35,7 @@ repos:
|
|
|
35
35
|
language: system
|
|
36
36
|
entry: sh -c 'npx tsc --noEmit'
|
|
37
37
|
files: \.ts$
|
|
38
|
+
pass_filenames: false
|
|
38
39
|
- repo: local
|
|
39
40
|
hooks:
|
|
40
41
|
- id: synth
|
|
@@ -42,6 +43,7 @@ repos:
|
|
|
42
43
|
language: system
|
|
43
44
|
entry: sh -c 'npx cdk synth --quiet --context vpc-id=$VPC_ID'
|
|
44
45
|
files: src/.*\.ts$
|
|
46
|
+
pass_filenames: false
|
|
45
47
|
- repo: local
|
|
46
48
|
hooks:
|
|
47
49
|
- id: compile-project
|
|
@@ -50,6 +52,7 @@ repos:
|
|
|
50
52
|
language: system
|
|
51
53
|
entry: sh -c 'npx projen compile'
|
|
52
54
|
files: \.ts$
|
|
55
|
+
pass_filenames: false
|
|
53
56
|
- repo: local
|
|
54
57
|
hooks:
|
|
55
58
|
- id: test
|
|
@@ -58,3 +61,4 @@ repos:
|
|
|
58
61
|
language: system
|
|
59
62
|
entry: sh -c 'npx jest --coverage=false'
|
|
60
63
|
files: \.ts$
|
|
64
|
+
pass_filenames: false
|
package/.prettierignore
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# About
|
|
2
2
|
|
|
3
3
|
This CDK construct library makes it possible to create databases,
|
|
4
|
-
schemas, and roles in an Aurora Serverless v2, RDS Database Cluster
|
|
5
|
-
Database Instance.
|
|
4
|
+
schemas, and roles in an Aurora Serverless v2, RDS Database Cluster,
|
|
5
|
+
Database Instance, or AWS DSQL clusters. PostgreSQL, MySQL, and DSQL
|
|
6
|
+
databases are supported.
|
|
6
7
|
|
|
7
8
|
This construct library is intended to be used in enterprise
|
|
8
9
|
environments, and works in isolated subnets.
|
|
@@ -103,20 +104,6 @@ const provider = new Provider(this, "Provider", {
|
|
|
103
104
|
})
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
### Disabling SSL
|
|
107
|
-
|
|
108
|
-
The default connection to RDS is ssl enabled. You can disable ssl by
|
|
109
|
-
setting the `ssl` option to `false`:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
const provider = new Provider(this, "Provider", {
|
|
113
|
-
vpc: vpc,
|
|
114
|
-
instance: instance,
|
|
115
|
-
secret: cluster.secret!,
|
|
116
|
-
ssl: false, // default is true
|
|
117
|
-
})
|
|
118
|
-
```
|
|
119
|
-
|
|
120
107
|
## Roles
|
|
121
108
|
|
|
122
109
|
Create a postgres role (or mysql user) for a cluster as follows:
|
|
@@ -388,6 +375,92 @@ initialPolicy: [
|
|
|
388
375
|
|
|
389
376
|
Note that your VPC will need an SSM Parameters interface endpoint to support this.
|
|
390
377
|
|
|
378
|
+
# AWS DSQL Support
|
|
379
|
+
|
|
380
|
+
This construct also supports AWS DSQL. DSQL is a great number of
|
|
381
|
+
limitations around it's postgresql support, so there is some learing
|
|
382
|
+
curve. This library supports creating roles, schemas, and executing
|
|
383
|
+
SQL on DSQL clusters. Creating databases is not supported by DSQL.
|
|
384
|
+
|
|
385
|
+
## DSQL Provider Setup
|
|
386
|
+
|
|
387
|
+
DSQL clusters don't require a VPC or secrets since they use IAM authentication:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import * as dsql from "aws-cdk-lib/aws-dsql"
|
|
391
|
+
import { Provider } from "cdk-rds-sql"
|
|
392
|
+
|
|
393
|
+
const dsqlCluster = new dsql.CfnCluster(this, "DsqlCluster", {
|
|
394
|
+
deletionProtectionEnabled: false,
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const provider = new Provider(this, "Provider", {
|
|
398
|
+
cluster: dsqlCluster,
|
|
399
|
+
// No VPC or secret needed for DSQL
|
|
400
|
+
})
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
You can access DSQL from within a VPC, but cdk-rds-sql has not been
|
|
404
|
+
specifically tested with that scenario.
|
|
405
|
+
|
|
406
|
+
## DSQL Roles
|
|
407
|
+
|
|
408
|
+
DSQL always uses IAM authentication, so the `enableIamAuth` property is ignored:
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
import { Role } from "cdk-rds-sql"
|
|
412
|
+
|
|
413
|
+
const role = new Role(this, "Role", {
|
|
414
|
+
provider: provider,
|
|
415
|
+
roleName: "myrole",
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Using DSQL
|
|
420
|
+
|
|
421
|
+
Connecting to DSQL requires a couple of things:
|
|
422
|
+
|
|
423
|
+
1. Your IAM role needs the `dsql:DbConnect` IAM permission to connect to the cluster.
|
|
424
|
+
2. You probably want a normal postgres role (not "admin") to use from your apps.
|
|
425
|
+
3. Your IAM role needs to be connected to your postgres role: use the `IamGrant` construct for that:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
new IamGrant(this, "LambdaIamGrant", {
|
|
429
|
+
provider: provider,
|
|
430
|
+
roleName: role.roleName,
|
|
431
|
+
resourceArn: queryLambda.role!.roleArn,
|
|
432
|
+
})
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
4. From your code use the Dsql signer to create the password:
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { DsqlSigner } from "@aws-sdk/dsql-signer"
|
|
439
|
+
import { Client } from "pg"
|
|
440
|
+
|
|
441
|
+
const region =
|
|
442
|
+
process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"] || "us-west-2"
|
|
443
|
+
|
|
444
|
+
// Generate DSQL auth token using AWS SDK
|
|
445
|
+
const signer = new DsqlSigner({
|
|
446
|
+
hostname: host,
|
|
447
|
+
region,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const authToken = await signer.getDbConnectAuthToken()
|
|
451
|
+
|
|
452
|
+
const client = new Client({
|
|
453
|
+
host,
|
|
454
|
+
port: 5432,
|
|
455
|
+
user: dbUser,
|
|
456
|
+
database: dbName,
|
|
457
|
+
password: authToken,
|
|
458
|
+
ssl: { rejectUnauthorized: false },
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
await client.connect()
|
|
462
|
+
```
|
|
463
|
+
|
|
391
464
|
# IPv6
|
|
392
465
|
|
|
393
466
|
If you use the provider in an IPv6 subnet you probably need these settings:
|
|
@@ -404,6 +477,20 @@ const provider = new Provider(this, "Provider", {
|
|
|
404
477
|
}
|
|
405
478
|
```
|
|
406
479
|
|
|
480
|
+
# Disabling SSL
|
|
481
|
+
|
|
482
|
+
The default connection to RDS is ssl enabled. You can disable ssl by
|
|
483
|
+
setting the `ssl` option to `false`:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
const provider = new Provider(this, "Provider", {
|
|
487
|
+
vpc: vpc,
|
|
488
|
+
instance: instance,
|
|
489
|
+
secret: cluster.secret!,
|
|
490
|
+
ssl: false, // default is true
|
|
491
|
+
})
|
|
492
|
+
```
|
|
493
|
+
|
|
407
494
|
# Working on this code
|
|
408
495
|
|
|
409
496
|
This code is managed by
|
|
@@ -432,6 +519,10 @@ If you want to use an existing vpc:
|
|
|
432
519
|
|
|
433
520
|
npx cdk deploy --context vpc-id=vpc-0123456789 TestServerlessV2Stack
|
|
434
521
|
|
|
522
|
+
You can also test DSQL support with:
|
|
523
|
+
|
|
524
|
+
npx projen integ:deploy:dsql
|
|
525
|
+
|
|
435
526
|
# To do
|
|
436
527
|
|
|
437
528
|
- Update role: will not revoke connect to previous database if database name has changed.
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
EngineRoleProperties,
|
|
9
9
|
EngineSchemaProperties,
|
|
10
10
|
EngineSqlProperties,
|
|
11
|
+
EngineIamGrantProperties,
|
|
11
12
|
} from "./types"
|
|
12
13
|
|
|
13
14
|
export interface EngineConnectionConfig {
|
|
@@ -74,6 +75,21 @@ export abstract class AbstractEngine {
|
|
|
74
75
|
): string | string[]
|
|
75
76
|
abstract deleteSql(resourceId: string, props: EngineSqlProperties): string | string[]
|
|
76
77
|
|
|
78
|
+
abstract createIamGrant(
|
|
79
|
+
resourceId: string,
|
|
80
|
+
props: EngineIamGrantProperties
|
|
81
|
+
): string | string[]
|
|
82
|
+
abstract updateIamGrant(
|
|
83
|
+
resourceId: string,
|
|
84
|
+
oldResourceId: string,
|
|
85
|
+
props: EngineIamGrantProperties,
|
|
86
|
+
oldProps: EngineIamGrantProperties
|
|
87
|
+
): string | string[]
|
|
88
|
+
abstract deleteIamGrant(
|
|
89
|
+
resourceId: string,
|
|
90
|
+
props: EngineIamGrantProperties
|
|
91
|
+
): string | string[]
|
|
92
|
+
|
|
77
93
|
abstract executeSQL(
|
|
78
94
|
sql: string | string[],
|
|
79
95
|
config: EngineConnectionConfig
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { DsqlSigner } from "@aws-sdk/dsql-signer"
|
|
2
|
+
import { format as pgFormat } from "node-pg-format"
|
|
3
|
+
import { Client, ClientConfig } from "pg"
|
|
4
|
+
import { AbstractEngine, EngineConnectionConfig } from "./engine.abstract"
|
|
5
|
+
import {
|
|
6
|
+
EngineDatabaseProperties,
|
|
7
|
+
EngineRoleProperties,
|
|
8
|
+
EngineSchemaProperties,
|
|
9
|
+
EngineSqlProperties,
|
|
10
|
+
EngineIamGrantProperties,
|
|
11
|
+
} from "./types"
|
|
12
|
+
|
|
13
|
+
export class DsqlEngine extends AbstractEngine {
|
|
14
|
+
createDatabase(
|
|
15
|
+
_resourceId: string,
|
|
16
|
+
_props: EngineDatabaseProperties
|
|
17
|
+
): string | string[] {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"DSQL does not support creating databases. Database is always 'postgres'."
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
updateDatabase(
|
|
24
|
+
_resourceId: string,
|
|
25
|
+
_oldResourceId: string,
|
|
26
|
+
_props: EngineDatabaseProperties
|
|
27
|
+
): string[] {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"DSQL does not support database operations. Database is always 'postgres'."
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
deleteDatabase(_resourceId: string, _masterUser: string): string | string[] {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"DSQL does not support deleting databases. Database is always 'postgres'."
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async createRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
|
|
40
|
+
const sql = ["BEGIN"]
|
|
41
|
+
|
|
42
|
+
// DSQL always uses IAM authentication, no passwords
|
|
43
|
+
sql.push(pgFormat("CREATE ROLE %I WITH LOGIN", resourceId))
|
|
44
|
+
|
|
45
|
+
if (props?.DatabaseName) {
|
|
46
|
+
sql.push(
|
|
47
|
+
pgFormat("GRANT CONNECT ON DATABASE %I TO %I", props.DatabaseName, resourceId)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sql.push("COMMIT")
|
|
52
|
+
return sql
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async updateRole(
|
|
56
|
+
resourceId: string,
|
|
57
|
+
oldResourceId: string,
|
|
58
|
+
props: EngineRoleProperties,
|
|
59
|
+
oldProps: EngineRoleProperties
|
|
60
|
+
): Promise<string[]> {
|
|
61
|
+
const sql = ["BEGIN"]
|
|
62
|
+
|
|
63
|
+
if (oldResourceId !== resourceId) {
|
|
64
|
+
sql.push(pgFormat("ALTER ROLE %I RENAME TO %I", oldResourceId, resourceId))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle database permission changes
|
|
68
|
+
if (
|
|
69
|
+
oldProps?.DatabaseName &&
|
|
70
|
+
props?.DatabaseName &&
|
|
71
|
+
oldProps.DatabaseName !== props.DatabaseName
|
|
72
|
+
) {
|
|
73
|
+
// Revoke from old database and grant to new
|
|
74
|
+
sql.push(
|
|
75
|
+
pgFormat(
|
|
76
|
+
"REVOKE CONNECT ON DATABASE %I FROM %I",
|
|
77
|
+
oldProps.DatabaseName,
|
|
78
|
+
resourceId
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
sql.push(
|
|
82
|
+
pgFormat("GRANT CONNECT ON DATABASE %I TO %I", props.DatabaseName, resourceId)
|
|
83
|
+
)
|
|
84
|
+
} else if (props?.DatabaseName && !oldProps?.DatabaseName) {
|
|
85
|
+
// Grant to new database
|
|
86
|
+
sql.push(
|
|
87
|
+
pgFormat("GRANT CONNECT ON DATABASE %I TO %I", props.DatabaseName, resourceId)
|
|
88
|
+
)
|
|
89
|
+
} else if (!props?.DatabaseName && oldProps?.DatabaseName) {
|
|
90
|
+
// Revoke from old database
|
|
91
|
+
sql.push(
|
|
92
|
+
pgFormat(
|
|
93
|
+
"REVOKE CONNECT ON DATABASE %I FROM %I",
|
|
94
|
+
oldProps.DatabaseName,
|
|
95
|
+
resourceId
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
sql.push("COMMIT")
|
|
101
|
+
return sql
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
deleteRole(resourceId: string, props: EngineRoleProperties): string | string[] {
|
|
105
|
+
const sql = ["BEGIN"]
|
|
106
|
+
|
|
107
|
+
if (props?.DatabaseName) {
|
|
108
|
+
sql.push(
|
|
109
|
+
pgFormat(
|
|
110
|
+
"REVOKE ALL PRIVILEGES ON DATABASE %I FROM %I",
|
|
111
|
+
props.DatabaseName,
|
|
112
|
+
resourceId
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sql.push(pgFormat("DROP ROLE IF EXISTS %I", resourceId))
|
|
118
|
+
sql.push("COMMIT")
|
|
119
|
+
return sql
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
createSchema(resourceId: string, props: EngineSchemaProperties): string | string[] {
|
|
123
|
+
const sql: string[] = [pgFormat("CREATE SCHEMA IF NOT EXISTS %I", resourceId)]
|
|
124
|
+
if (props?.RoleName) {
|
|
125
|
+
this.grantRoleForSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
126
|
+
sql.push(stmt)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return sql
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
updateSchema(
|
|
133
|
+
resourceId: string,
|
|
134
|
+
oldResourceId: string,
|
|
135
|
+
props: EngineSchemaProperties
|
|
136
|
+
): string | string[] {
|
|
137
|
+
const statements: string[] = []
|
|
138
|
+
if (props?.RoleName) {
|
|
139
|
+
this.revokeRoleFromSchema(oldResourceId, props.RoleName).forEach((stmt) =>
|
|
140
|
+
statements.push(stmt)
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
if (resourceId !== oldResourceId) {
|
|
144
|
+
statements.push(pgFormat("ALTER SCHEMA %I RENAME TO %I", oldResourceId, resourceId))
|
|
145
|
+
}
|
|
146
|
+
if (props?.RoleName) {
|
|
147
|
+
this.grantRoleForSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
148
|
+
statements.push(stmt)
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
return statements
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
deleteSchema(resourceId: string, props: EngineSchemaProperties): string | string[] {
|
|
155
|
+
const statements: string[] = []
|
|
156
|
+
if (props?.RoleName) {
|
|
157
|
+
this.revokeRoleFromSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
158
|
+
statements.push(stmt)
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
statements.push(pgFormat("DROP SCHEMA IF EXISTS %I CASCADE", resourceId))
|
|
162
|
+
return statements
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
createSql(_resourceId: string, props: EngineSqlProperties): string | string[] {
|
|
166
|
+
if (!props.Statement) {
|
|
167
|
+
throw new Error("Statement is required for SQL resource")
|
|
168
|
+
}
|
|
169
|
+
return props.Statement
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
updateSql(
|
|
173
|
+
_resourceId: string,
|
|
174
|
+
_oldResourceId: string,
|
|
175
|
+
props: EngineSqlProperties
|
|
176
|
+
): string | string[] {
|
|
177
|
+
if (!props.Statement) {
|
|
178
|
+
throw new Error("Statement is required for SQL resource")
|
|
179
|
+
}
|
|
180
|
+
return props.Statement
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
deleteSql(_resourceId: string, props: EngineSqlProperties): string | string[] {
|
|
184
|
+
if (props.Rollback) {
|
|
185
|
+
return props.Rollback
|
|
186
|
+
}
|
|
187
|
+
return []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
createIamGrant(resourceId: string, props: EngineIamGrantProperties): string | string[] {
|
|
191
|
+
if (!props.ResourceArn) {
|
|
192
|
+
throw new Error("ResourceArn is required for IAM grant")
|
|
193
|
+
}
|
|
194
|
+
return pgFormat("AWS IAM GRANT %I TO %L", resourceId, props.ResourceArn)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
updateIamGrant(
|
|
198
|
+
resourceId: string,
|
|
199
|
+
oldResourceId: string,
|
|
200
|
+
props: EngineIamGrantProperties,
|
|
201
|
+
oldProps: EngineIamGrantProperties
|
|
202
|
+
): string | string[] {
|
|
203
|
+
const statements: string[] = []
|
|
204
|
+
|
|
205
|
+
// Revoke old grant if resource ARN or role name changed
|
|
206
|
+
if (oldProps.ResourceArn) {
|
|
207
|
+
statements.push(
|
|
208
|
+
pgFormat("AWS IAM REVOKE %I FROM %L", oldResourceId, oldProps.ResourceArn)
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Grant new permissions
|
|
213
|
+
if (props.ResourceArn) {
|
|
214
|
+
statements.push(pgFormat("AWS IAM GRANT %I TO %L", resourceId, props.ResourceArn))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return statements
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
deleteIamGrant(resourceId: string, props: EngineIamGrantProperties): string | string[] {
|
|
221
|
+
if (!props.ResourceArn) {
|
|
222
|
+
throw new Error("ResourceArn is required for IAM grant deletion")
|
|
223
|
+
}
|
|
224
|
+
return pgFormat("AWS IAM REVOKE %I FROM %L", resourceId, props.ResourceArn)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> {
|
|
228
|
+
this.log("Connecting to DSQL cluster...")
|
|
229
|
+
|
|
230
|
+
// For DSQL, we need to generate an IAM auth token
|
|
231
|
+
const region = process.env.AWS_REGION
|
|
232
|
+
if (!region) {
|
|
233
|
+
throw new Error("AWS_REGION environment variable is required for DSQL")
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const signer = new DsqlSigner({
|
|
237
|
+
hostname: config.host,
|
|
238
|
+
region: region,
|
|
239
|
+
})
|
|
240
|
+
const authToken = await signer.getDbConnectAdminAuthToken()
|
|
241
|
+
|
|
242
|
+
const clientConfig: ClientConfig = {
|
|
243
|
+
host: config.host,
|
|
244
|
+
port: config.port,
|
|
245
|
+
user: "admin", // DSQL always uses 'admin' user
|
|
246
|
+
password: authToken, // Use IAM auth token as password
|
|
247
|
+
database: "postgres", // DSQL always uses 'postgres' database
|
|
248
|
+
ssl: {
|
|
249
|
+
//ca: fs.readFileSync(`${process.env.LAMBDA_TASK_ROOT}/global-bundle.pem`),
|
|
250
|
+
rejectUnauthorized: true,
|
|
251
|
+
},
|
|
252
|
+
connectionTimeoutMillis: 30000,
|
|
253
|
+
query_timeout: 30000,
|
|
254
|
+
//statement_timeout: 30000, // not supported on DSQL
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const client = new Client(clientConfig)
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await client.connect()
|
|
261
|
+
this.log("Connected to DSQL cluster")
|
|
262
|
+
|
|
263
|
+
const statements = Array.isArray(sql) ? sql : [sql]
|
|
264
|
+
const results = []
|
|
265
|
+
|
|
266
|
+
for (const statement of statements) {
|
|
267
|
+
if (statement.trim()) {
|
|
268
|
+
this.log(`Executing SQL: ${statement}`)
|
|
269
|
+
const result = await client.query(statement)
|
|
270
|
+
results.push(result)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.log("SQL execution completed")
|
|
275
|
+
return results.length === 1 ? results[0] : results
|
|
276
|
+
} catch (error) {
|
|
277
|
+
this.log(`Error executing SQL: ${error}`)
|
|
278
|
+
throw error
|
|
279
|
+
} finally {
|
|
280
|
+
try {
|
|
281
|
+
await client.end()
|
|
282
|
+
this.log("Disconnected from DSQL cluster")
|
|
283
|
+
} catch (error) {
|
|
284
|
+
this.log(`Error disconnecting: ${error}`)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private grantRoleForSchema(schema: string, roleName: string): string[] {
|
|
290
|
+
return [
|
|
291
|
+
pgFormat("GRANT USAGE ON SCHEMA %I TO %I", schema, roleName),
|
|
292
|
+
pgFormat("GRANT CREATE ON SCHEMA %I TO %I", schema, roleName),
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private revokeRoleFromSchema(schema: string, roleName: string): string[] {
|
|
297
|
+
return [
|
|
298
|
+
pgFormat("REVOKE CREATE ON SCHEMA %I FROM %I", schema, roleName),
|
|
299
|
+
pgFormat("REVOKE ALL ON SCHEMA %I FROM %I", schema, roleName),
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
}
|
package/lambda/engine.factory.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { AbstractEngine } from "./engine.abstract"
|
|
2
|
+
import { DsqlEngine } from "./engine.dsql"
|
|
2
3
|
import { MysqlEngine } from "./engine.mysql"
|
|
3
4
|
import { PostgresqlEngine } from "./engine.postgresql"
|
|
4
5
|
|
|
5
6
|
export class EngineFactory {
|
|
6
7
|
static createEngine(engine: string): AbstractEngine {
|
|
7
8
|
switch (engine.toLowerCase()) {
|
|
9
|
+
case "dsql":
|
|
10
|
+
return new DsqlEngine()
|
|
8
11
|
case "postgres":
|
|
9
12
|
case "postgresql":
|
|
10
13
|
case "aurora-postgresql":
|
package/lambda/engine.mysql.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
EngineRoleProperties,
|
|
6
6
|
EngineSchemaProperties,
|
|
7
7
|
EngineSqlProperties,
|
|
8
|
+
EngineIamGrantProperties,
|
|
8
9
|
} from "./types"
|
|
9
10
|
|
|
10
11
|
export class MysqlEngine extends AbstractEngine {
|
|
@@ -183,6 +184,29 @@ export class MysqlEngine extends AbstractEngine {
|
|
|
183
184
|
return props?.Rollback || ""
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
createIamGrant(
|
|
188
|
+
_resourceId: string,
|
|
189
|
+
_props: EngineIamGrantProperties
|
|
190
|
+
): string | string[] {
|
|
191
|
+
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
updateIamGrant(
|
|
195
|
+
_resourceId: string,
|
|
196
|
+
_oldResourceId: string,
|
|
197
|
+
_props: EngineIamGrantProperties,
|
|
198
|
+
_oldProps: EngineIamGrantProperties
|
|
199
|
+
): string | string[] {
|
|
200
|
+
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
deleteIamGrant(
|
|
204
|
+
_resourceId: string,
|
|
205
|
+
_props: EngineIamGrantProperties
|
|
206
|
+
): string | string[] {
|
|
207
|
+
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
208
|
+
}
|
|
209
|
+
|
|
186
210
|
async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> {
|
|
187
211
|
// Dynamic import to avoid bundling issues
|
|
188
212
|
const { createConnection } = await import("mysql2/promise")
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
EngineRoleProperties,
|
|
9
9
|
EngineSchemaProperties,
|
|
10
10
|
EngineSqlProperties,
|
|
11
|
+
EngineIamGrantProperties,
|
|
11
12
|
} from "./types"
|
|
12
13
|
|
|
13
14
|
export class PostgresqlEngine extends AbstractEngine {
|
|
@@ -267,6 +268,29 @@ END$$;`,
|
|
|
267
268
|
]
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
createIamGrant(
|
|
272
|
+
_resourceId: string,
|
|
273
|
+
_props: EngineIamGrantProperties
|
|
274
|
+
): string | string[] {
|
|
275
|
+
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
updateIamGrant(
|
|
279
|
+
_resourceId: string,
|
|
280
|
+
_oldResourceId: string,
|
|
281
|
+
_props: EngineIamGrantProperties,
|
|
282
|
+
_oldProps: EngineIamGrantProperties
|
|
283
|
+
): string | string[] {
|
|
284
|
+
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
deleteIamGrant(
|
|
288
|
+
_resourceId: string,
|
|
289
|
+
_props: EngineIamGrantProperties
|
|
290
|
+
): string | string[] {
|
|
291
|
+
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
292
|
+
}
|
|
293
|
+
|
|
270
294
|
async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> {
|
|
271
295
|
const isSslEnabled = process.env.SSL ? JSON.parse(process.env.SSL) : true
|
|
272
296
|
const ssl: ConnectionOptions | false = isSslEnabled
|