cdk-rds-sql 6.1.0 → 6.1.2
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 +3 -3
- package/.pre-commit-config.yaml +1 -1
- package/CLAUDE.md +117 -0
- package/lambda/engine.abstract.ts +30 -12
- package/lambda/engine.factory.ts +1 -1
- package/lambda/engine.mysql.test.ts +35 -10
- package/lambda/engine.mysql.ts +39 -25
- package/lambda/engine.postgresql.test.ts +21 -9
- package/lambda/engine.postgresql.ts +43 -25
- package/lambda/handler.mysql.test.ts +22 -20
- package/lambda/handler.postgresql.test.ts +6 -6
- package/lambda/handler.test.ts +164 -0
- package/lambda/handler.ts +81 -14
- package/lambda/types.ts +29 -14
- package/lambda/util.ts +2 -2
- package/lib/database.js +1 -1
- package/lib/handler/handler.js +11209 -11157
- package/lib/provider.js +5 -4
- package/lib/role.custom-resource.js +2 -2
- package/lib/role.js +1 -1
- package/lib/schema.js +1 -1
- package/lib/sql.js +1 -1
- package/package.json +3 -3
|
@@ -3,9 +3,15 @@ import { ConnectionOptions } from "tls"
|
|
|
3
3
|
import { format as pgFormat } from "node-pg-format"
|
|
4
4
|
import { Client, ClientConfig } from "pg"
|
|
5
5
|
import { AbstractEngine, EngineConnectionConfig } from "./engine.abstract"
|
|
6
|
+
import {
|
|
7
|
+
EngineDatabaseProperties,
|
|
8
|
+
EngineRoleProperties,
|
|
9
|
+
EngineSchemaProperties,
|
|
10
|
+
EngineSqlProperties,
|
|
11
|
+
} from "./types"
|
|
6
12
|
|
|
7
13
|
export class PostgresqlEngine extends AbstractEngine {
|
|
8
|
-
createDatabase(resourceId: string, props
|
|
14
|
+
createDatabase(resourceId: string, props: EngineDatabaseProperties): string | string[] {
|
|
9
15
|
const owner = props?.Owner
|
|
10
16
|
if (owner) {
|
|
11
17
|
return [
|
|
@@ -17,7 +23,11 @@ export class PostgresqlEngine extends AbstractEngine {
|
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
updateDatabase(
|
|
26
|
+
updateDatabase(
|
|
27
|
+
resourceId: string,
|
|
28
|
+
oldResourceId: string,
|
|
29
|
+
props: EngineDatabaseProperties
|
|
30
|
+
): string[] {
|
|
21
31
|
const statements: string[] = []
|
|
22
32
|
if (resourceId !== oldResourceId) {
|
|
23
33
|
if (props?.MasterOwner) {
|
|
@@ -52,24 +62,24 @@ export class PostgresqlEngine extends AbstractEngine {
|
|
|
52
62
|
]
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
async createRole(resourceId: string, props
|
|
65
|
+
async createRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
|
|
56
66
|
const sql = ["start transaction"]
|
|
57
|
-
|
|
58
|
-
if (props
|
|
67
|
+
|
|
68
|
+
if (props?.EnableIamAuth) {
|
|
59
69
|
// Create role for IAM authentication
|
|
60
70
|
sql.push(pgFormat("create role %I with login", resourceId))
|
|
61
71
|
sql.push(pgFormat("grant rds_iam to %I", resourceId))
|
|
62
72
|
} else {
|
|
63
73
|
// Create role with password authentication
|
|
64
|
-
if (!props
|
|
74
|
+
if (!props?.PasswordArn) throw "No PasswordArn provided"
|
|
65
75
|
const password = await this.getPassword(props.PasswordArn)
|
|
66
76
|
if (!password) {
|
|
67
77
|
throw `Cannot parse password from ${props.PasswordArn}`
|
|
68
78
|
}
|
|
69
79
|
sql.push(pgFormat("create role %I with login password %L", resourceId, password))
|
|
70
80
|
}
|
|
71
|
-
|
|
72
|
-
if (props
|
|
81
|
+
|
|
82
|
+
if (props?.DatabaseName) {
|
|
73
83
|
sql.push(
|
|
74
84
|
pgFormat(
|
|
75
85
|
`DO $$
|
|
@@ -84,7 +94,7 @@ END$$;`,
|
|
|
84
94
|
)
|
|
85
95
|
)
|
|
86
96
|
}
|
|
87
|
-
|
|
97
|
+
|
|
88
98
|
sql.push("commit")
|
|
89
99
|
return sql
|
|
90
100
|
}
|
|
@@ -92,15 +102,15 @@ END$$;`,
|
|
|
92
102
|
async updateRole(
|
|
93
103
|
resourceId: string,
|
|
94
104
|
oldResourceId: string,
|
|
95
|
-
props
|
|
96
|
-
oldProps
|
|
105
|
+
props: EngineRoleProperties,
|
|
106
|
+
oldProps: EngineRoleProperties
|
|
97
107
|
): Promise<string[]> {
|
|
98
108
|
const sql = ["start transaction"]
|
|
99
|
-
|
|
109
|
+
|
|
100
110
|
if (oldResourceId !== resourceId) {
|
|
101
111
|
sql.push(pgFormat("alter role %I rename to %I", oldResourceId, resourceId))
|
|
102
112
|
}
|
|
103
|
-
|
|
113
|
+
|
|
104
114
|
// Handle authentication method changes
|
|
105
115
|
if (props?.EnableIamAuth && !oldProps?.EnableIamAuth) {
|
|
106
116
|
// Switching from password to IAM auth
|
|
@@ -123,7 +133,7 @@ END$$;`,
|
|
|
123
133
|
}
|
|
124
134
|
sql.push(pgFormat("alter role %I with password %L", resourceId, password))
|
|
125
135
|
}
|
|
126
|
-
|
|
136
|
+
|
|
127
137
|
// Check if database name has changed
|
|
128
138
|
if (
|
|
129
139
|
oldProps?.DatabaseName &&
|
|
@@ -161,12 +171,12 @@ END$$;`,
|
|
|
161
171
|
)
|
|
162
172
|
)
|
|
163
173
|
}
|
|
164
|
-
|
|
174
|
+
|
|
165
175
|
sql.push("commit")
|
|
166
176
|
return sql
|
|
167
177
|
}
|
|
168
178
|
|
|
169
|
-
deleteRole(resourceId: string, props
|
|
179
|
+
deleteRole(resourceId: string, props: EngineRoleProperties): string[] {
|
|
170
180
|
return [
|
|
171
181
|
"start transaction",
|
|
172
182
|
pgFormat(
|
|
@@ -186,7 +196,7 @@ END$$;`,
|
|
|
186
196
|
]
|
|
187
197
|
}
|
|
188
198
|
|
|
189
|
-
createSchema(resourceId: string, props
|
|
199
|
+
createSchema(resourceId: string, props: EngineSchemaProperties): string[] {
|
|
190
200
|
const sql: string[] = [pgFormat("create schema if not exists %I", resourceId)]
|
|
191
201
|
if (props?.RoleName) {
|
|
192
202
|
this.grantRoleForSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
@@ -196,7 +206,11 @@ END$$;`,
|
|
|
196
206
|
return sql
|
|
197
207
|
}
|
|
198
208
|
|
|
199
|
-
updateSchema(
|
|
209
|
+
updateSchema(
|
|
210
|
+
resourceId: string,
|
|
211
|
+
oldResourceId: string,
|
|
212
|
+
props: EngineSchemaProperties
|
|
213
|
+
): string[] {
|
|
200
214
|
const sql: string[] = []
|
|
201
215
|
if (props?.RoleName) {
|
|
202
216
|
this.revokeRoleFromSchema(oldResourceId, props.RoleName).forEach((stmt) =>
|
|
@@ -212,7 +226,7 @@ END$$;`,
|
|
|
212
226
|
return sql
|
|
213
227
|
}
|
|
214
228
|
|
|
215
|
-
deleteSchema(resourceId: string, props
|
|
229
|
+
deleteSchema(resourceId: string, props: EngineSchemaProperties): string[] {
|
|
216
230
|
const sql: string[] = []
|
|
217
231
|
if (props?.RoleName) {
|
|
218
232
|
this.revokeRoleFromSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
@@ -223,16 +237,20 @@ END$$;`,
|
|
|
223
237
|
return sql
|
|
224
238
|
}
|
|
225
239
|
|
|
226
|
-
createSql(_resourceId: string, props
|
|
227
|
-
return props
|
|
240
|
+
createSql(_resourceId: string, props: EngineSqlProperties): string {
|
|
241
|
+
return props?.Statement || ""
|
|
228
242
|
}
|
|
229
243
|
|
|
230
|
-
updateSql(
|
|
231
|
-
|
|
244
|
+
updateSql(
|
|
245
|
+
_resourceId: string,
|
|
246
|
+
_oldResourceId: string,
|
|
247
|
+
props: EngineSqlProperties
|
|
248
|
+
): string {
|
|
249
|
+
return props?.Statement || ""
|
|
232
250
|
}
|
|
233
251
|
|
|
234
|
-
deleteSql(_resourceId: string, props
|
|
235
|
-
return props
|
|
252
|
+
deleteSql(_resourceId: string, props: EngineSqlProperties): string {
|
|
253
|
+
return props?.Rollback || ""
|
|
236
254
|
}
|
|
237
255
|
|
|
238
256
|
private grantRoleForSchema(schema: string, roleName: string): string[] {
|
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
|
|
2
2
|
import { createConnection } from "mysql2/promise"
|
|
3
3
|
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
|
4
|
-
import { createRequest, updateRequest, deleteRequest } from "./util"
|
|
5
4
|
import { handler } from "./handler"
|
|
5
|
+
import { createRequest, updateRequest, deleteRequest } from "./util"
|
|
6
6
|
|
|
7
7
|
jest.mock("@aws-sdk/client-secrets-manager")
|
|
8
8
|
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
|
|
9
9
|
typeof SecretsManagerClient
|
|
10
10
|
>
|
|
11
|
-
SecretsManagerClientMock.prototype.send.mockImplementation(() => {
|
|
12
|
-
return {
|
|
13
|
-
SecretString: JSON.stringify({
|
|
14
|
-
host: mysqlHost,
|
|
15
|
-
port: mysqlPort,
|
|
16
|
-
username: DB_MASTER_USERNAME,
|
|
17
|
-
password: DB_MASTER_PASSWORD,
|
|
18
|
-
dbname: DB_DEFAULT_DB,
|
|
19
|
-
engine: "mysql",
|
|
20
|
-
dbClusterIdentifier: "dummy",
|
|
21
|
-
}),
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
11
|
|
|
25
12
|
const DB_PORT = 3306
|
|
26
13
|
const DB_MASTER_USERNAME = "root"
|
|
@@ -45,6 +32,21 @@ beforeEach(async () => {
|
|
|
45
32
|
.start()
|
|
46
33
|
mysqlHost = mysqlContainer.getHost()
|
|
47
34
|
mysqlPort = mysqlContainer.getMappedPort(DB_PORT)
|
|
35
|
+
|
|
36
|
+
// Set up the mock after we have the host and port values
|
|
37
|
+
SecretsManagerClientMock.prototype.send.mockImplementation(() => {
|
|
38
|
+
return {
|
|
39
|
+
SecretString: JSON.stringify({
|
|
40
|
+
host: mysqlHost,
|
|
41
|
+
port: mysqlPort,
|
|
42
|
+
username: DB_MASTER_USERNAME,
|
|
43
|
+
password: DB_MASTER_PASSWORD,
|
|
44
|
+
dbname: DB_DEFAULT_DB,
|
|
45
|
+
engine: "mysql",
|
|
46
|
+
dbClusterIdentifier: "dummy",
|
|
47
|
+
}),
|
|
48
|
+
}
|
|
49
|
+
})
|
|
48
50
|
}, 30000)
|
|
49
51
|
|
|
50
52
|
afterEach(async () => {
|
|
@@ -57,14 +59,14 @@ afterEach(async () => {
|
|
|
57
59
|
// Helper functions for MySQL tests
|
|
58
60
|
async function databaseExists(connection: any, dbName: string): Promise<boolean> {
|
|
59
61
|
const [rows] = await connection.query(
|
|
60
|
-
|
|
62
|
+
"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?",
|
|
61
63
|
[dbName]
|
|
62
64
|
)
|
|
63
65
|
return rows.length > 0
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
async function userExists(connection: any, username: string): Promise<boolean> {
|
|
67
|
-
const [rows] = await connection.query(
|
|
69
|
+
const [rows] = await connection.query("SELECT User FROM mysql.user WHERE User = ?", [
|
|
68
70
|
username,
|
|
69
71
|
])
|
|
70
72
|
return rows.length > 0
|
|
@@ -76,7 +78,7 @@ async function tableExists(
|
|
|
76
78
|
tableName: string
|
|
77
79
|
): Promise<boolean> {
|
|
78
80
|
const [rows] = await connection.query(
|
|
79
|
-
|
|
81
|
+
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
|
|
80
82
|
[dbName, tableName]
|
|
81
83
|
)
|
|
82
84
|
return rows.length > 0
|
|
@@ -103,7 +105,7 @@ test("database", async () => {
|
|
|
103
105
|
const newDatabaseName = "mydb2"
|
|
104
106
|
const create = createRequest("database", oldDatabaseName)
|
|
105
107
|
await handler(create)
|
|
106
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
|
|
108
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
|
|
107
109
|
const connection = await newConnection()
|
|
108
110
|
try {
|
|
109
111
|
expect(await databaseExists(connection, oldDatabaseName)).toEqual(true)
|
|
@@ -188,7 +190,7 @@ describe("User creation", () => {
|
|
|
188
190
|
DatabaseName: DB_DEFAULT_DB,
|
|
189
191
|
})
|
|
190
192
|
await handler(create)
|
|
191
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
193
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
192
194
|
|
|
193
195
|
const connection = await newConnection()
|
|
194
196
|
try {
|
|
@@ -227,7 +229,7 @@ describe("User creation", () => {
|
|
|
227
229
|
DatabaseName: DB_DEFAULT_DB,
|
|
228
230
|
})
|
|
229
231
|
await handler(create)
|
|
230
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
232
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
231
233
|
|
|
232
234
|
const connection = await newConnection()
|
|
233
235
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
|
|
2
2
|
import { Client, ClientConfig } from "pg"
|
|
3
3
|
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
|
4
|
+
import { handler } from "./handler"
|
|
4
5
|
import {
|
|
5
6
|
createRequest,
|
|
6
7
|
updateRequest,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
rowCount,
|
|
13
14
|
roleGrantedForSchema,
|
|
14
15
|
} from "./util"
|
|
15
|
-
import { handler } from "./handler"
|
|
16
16
|
|
|
17
17
|
jest.mock("@aws-sdk/client-secrets-manager")
|
|
18
18
|
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
|
|
@@ -71,7 +71,7 @@ test("schema", async () => {
|
|
|
71
71
|
const newSchemaName = "test2"
|
|
72
72
|
const create = createRequest("schema", oldSchemaName)
|
|
73
73
|
await handler(create)
|
|
74
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
|
|
74
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
|
|
75
75
|
|
|
76
76
|
const client = await newClient()
|
|
77
77
|
try {
|
|
@@ -128,7 +128,7 @@ test("role with existing database", async () => {
|
|
|
128
128
|
DatabaseName: "postgres",
|
|
129
129
|
})
|
|
130
130
|
await handler(create)
|
|
131
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
131
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
132
132
|
const client = await newClient()
|
|
133
133
|
try {
|
|
134
134
|
expect(await roleExists(client, oldRoleName)).toEqual(true)
|
|
@@ -165,7 +165,7 @@ test("role without database", async () => {
|
|
|
165
165
|
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
166
166
|
})
|
|
167
167
|
await handler(create)
|
|
168
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
168
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
169
169
|
const client = await newClient()
|
|
170
170
|
try {
|
|
171
171
|
expect(await roleExists(client, roleName)).toEqual(true)
|
|
@@ -180,7 +180,7 @@ test("change role password", async () => {
|
|
|
180
180
|
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
181
181
|
})
|
|
182
182
|
await handler(create)
|
|
183
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
183
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
184
184
|
const client = await newClient()
|
|
185
185
|
try {
|
|
186
186
|
expect(await roleExists(client, roleName)).toEqual(true)
|
|
@@ -198,7 +198,7 @@ test("database", async () => {
|
|
|
198
198
|
const newDatabaseName = "mydb2"
|
|
199
199
|
const create = createRequest("database", oldDatabaseName)
|
|
200
200
|
await handler(create)
|
|
201
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
|
|
201
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
|
|
202
202
|
const client = await newClient()
|
|
203
203
|
try {
|
|
204
204
|
expect(await databaseExists(client, oldDatabaseName)).toEqual(true)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
|
|
2
|
+
import { MysqlEngine } from "./engine.mysql"
|
|
3
|
+
import { PostgresqlEngine } from "./engine.postgresql"
|
|
4
|
+
import { handler } from "./handler"
|
|
5
|
+
import { createRequest } from "./util"
|
|
6
|
+
|
|
7
|
+
// Mock the secrets manager client
|
|
8
|
+
jest.mock("@aws-sdk/client-secrets-manager")
|
|
9
|
+
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
|
|
10
|
+
typeof SecretsManagerClient
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
// Mock the database engines
|
|
14
|
+
jest.mock("./engine.mysql")
|
|
15
|
+
jest.mock("./engine.postgresql")
|
|
16
|
+
|
|
17
|
+
const MockedMysqlEngine = MysqlEngine as jest.MockedClass<typeof MysqlEngine>
|
|
18
|
+
const MockedPostgresqlEngine = PostgresqlEngine as jest.MockedClass<
|
|
19
|
+
typeof PostgresqlEngine
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
describe("Handler CloudFormation Property Conversion", () => {
|
|
23
|
+
let mockMysqlEngine: jest.Mocked<MysqlEngine>
|
|
24
|
+
let mockPostgresqlEngine: jest.Mocked<PostgresqlEngine>
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks()
|
|
28
|
+
|
|
29
|
+
// Mock the engine instances
|
|
30
|
+
mockMysqlEngine = {
|
|
31
|
+
createRole: jest.fn(),
|
|
32
|
+
executeSQL: jest.fn(),
|
|
33
|
+
} as any
|
|
34
|
+
|
|
35
|
+
mockPostgresqlEngine = {
|
|
36
|
+
createRole: jest.fn(),
|
|
37
|
+
executeSQL: jest.fn(),
|
|
38
|
+
} as any
|
|
39
|
+
|
|
40
|
+
MockedMysqlEngine.mockImplementation(() => mockMysqlEngine)
|
|
41
|
+
MockedPostgresqlEngine.mockImplementation(() => mockPostgresqlEngine)
|
|
42
|
+
|
|
43
|
+
// Mock secrets manager to return MySQL engine
|
|
44
|
+
SecretsManagerClientMock.prototype.send.mockImplementation(() => {
|
|
45
|
+
return Promise.resolve({
|
|
46
|
+
SecretString: JSON.stringify({
|
|
47
|
+
host: "localhost",
|
|
48
|
+
port: 3306,
|
|
49
|
+
username: "root",
|
|
50
|
+
password: "password",
|
|
51
|
+
dbname: "test",
|
|
52
|
+
engine: "mysql",
|
|
53
|
+
}),
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("EnableIamAuth property conversion", () => {
|
|
59
|
+
it("should convert string 'false' to boolean false", async () => {
|
|
60
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
61
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
62
|
+
|
|
63
|
+
// Create a request with EnableIamAuth as string 'false' (simulating CloudFormation)
|
|
64
|
+
const request = createRequest("role", "testuser", {
|
|
65
|
+
EnableIamAuth: "false",
|
|
66
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
await handler(request)
|
|
70
|
+
|
|
71
|
+
// Verify that createRole was called with boolean false, not string 'false'
|
|
72
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
73
|
+
"testuser",
|
|
74
|
+
expect.objectContaining({
|
|
75
|
+
EnableIamAuth: false, // Should be converted to boolean false
|
|
76
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("should convert string 'true' to boolean true", async () => {
|
|
82
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
83
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
84
|
+
|
|
85
|
+
// Create a request with EnableIamAuth as string 'true' (simulating CloudFormation)
|
|
86
|
+
const request = createRequest("role", "testuser", {
|
|
87
|
+
EnableIamAuth: "true", // CloudFormation passes boolean as string
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await handler(request)
|
|
91
|
+
|
|
92
|
+
// Verify that createRole was called with boolean true, not string 'true'
|
|
93
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
94
|
+
"testuser",
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
EnableIamAuth: true, // Should be converted to boolean true
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("should handle number 0 correctly", async () => {
|
|
102
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
103
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
104
|
+
|
|
105
|
+
// Create a request with EnableIamAuth as actual boolean false
|
|
106
|
+
const request = createRequest("role", "testuser", {
|
|
107
|
+
EnableIamAuth: "0",
|
|
108
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await handler(request)
|
|
112
|
+
|
|
113
|
+
// Verify that createRole was called with boolean false
|
|
114
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
115
|
+
"testuser",
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
EnableIamAuth: false,
|
|
118
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should handle number 1 correctly", async () => {
|
|
124
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
125
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
126
|
+
|
|
127
|
+
// Create a request with EnableIamAuth as actual boolean true
|
|
128
|
+
const request = createRequest("role", "testuser", {
|
|
129
|
+
EnableIamAuth: "1",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await handler(request)
|
|
133
|
+
|
|
134
|
+
// Verify that createRole was called with boolean true
|
|
135
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
136
|
+
"testuser",
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
EnableIamAuth: true,
|
|
139
|
+
})
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("should handle undefined EnableIamAuth correctly", async () => {
|
|
144
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
145
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
146
|
+
|
|
147
|
+
// Create a request without EnableIamAuth property
|
|
148
|
+
const request = createRequest("role", "testuser", {
|
|
149
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
await handler(request)
|
|
153
|
+
|
|
154
|
+
// Verify that createRole was called without EnableIamAuth
|
|
155
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
156
|
+
"testuser",
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
159
|
+
EnableIamAuth: false,
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
package/lambda/handler.ts
CHANGED
|
@@ -15,10 +15,17 @@ import {
|
|
|
15
15
|
CloudFormationCustomResourceDeleteEvent,
|
|
16
16
|
} from "aws-lambda"
|
|
17
17
|
import { backOff } from "exponential-backoff"
|
|
18
|
-
import { EngineFactory } from "./engine.factory"
|
|
19
18
|
import { EngineConnectionConfig } from "./engine.abstract"
|
|
19
|
+
import { EngineFactory } from "./engine.factory"
|
|
20
|
+
import {
|
|
21
|
+
ResourceProperties,
|
|
22
|
+
CustomResourceResponse,
|
|
23
|
+
EngineDatabaseProperties,
|
|
24
|
+
EngineRoleProperties,
|
|
25
|
+
EngineSchemaProperties,
|
|
26
|
+
EngineSqlProperties,
|
|
27
|
+
} from "./types"
|
|
20
28
|
import { RdsSqlResource } from "../src/enum"
|
|
21
|
-
import { ResourceProperties, CustomResourceResponse } from "./types"
|
|
22
29
|
|
|
23
30
|
const maxAttempts = 20
|
|
24
31
|
|
|
@@ -29,6 +36,40 @@ const log =
|
|
|
29
36
|
|
|
30
37
|
const secrets_client = new SecretsManagerClient({})
|
|
31
38
|
|
|
39
|
+
// Helper functions to extract engine properties from CloudFormation properties
|
|
40
|
+
// These functions throw if the resource type doesn't match the expected type
|
|
41
|
+
function toDatabaseEngineProps(props: ResourceProperties): EngineDatabaseProperties {
|
|
42
|
+
if (props.Resource !== RdsSqlResource.DATABASE) {
|
|
43
|
+
throw new Error(`Expected DATABASE resource, got ${props.Resource}`)
|
|
44
|
+
}
|
|
45
|
+
return props
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toRoleEngineProps(props: ResourceProperties): EngineRoleProperties {
|
|
49
|
+
if (props.Resource !== RdsSqlResource.ROLE) {
|
|
50
|
+
throw new Error(`Expected ROLE resource, got ${props.Resource}`)
|
|
51
|
+
}
|
|
52
|
+
// Convert string 'true'/'false' to boolean for EnableIamAuth
|
|
53
|
+
return {
|
|
54
|
+
...props,
|
|
55
|
+
EnableIamAuth: props.EnableIamAuth === "true" || props.EnableIamAuth === "1",
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toSchemaEngineProps(props: ResourceProperties): EngineSchemaProperties {
|
|
60
|
+
if (props.Resource !== RdsSqlResource.SCHEMA) {
|
|
61
|
+
throw new Error(`Expected SCHEMA resource, got ${props.Resource}`)
|
|
62
|
+
}
|
|
63
|
+
return props
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toSqlEngineProps(props: ResourceProperties): EngineSqlProperties {
|
|
67
|
+
if (props.Resource !== RdsSqlResource.SQL) {
|
|
68
|
+
throw new Error(`Expected SQL resource, got ${props.Resource}`)
|
|
69
|
+
}
|
|
70
|
+
return props
|
|
71
|
+
}
|
|
72
|
+
|
|
32
73
|
export const handler = async (
|
|
33
74
|
event:
|
|
34
75
|
| CloudFormationCustomResourceCreateEvent<ResourceProperties>
|
|
@@ -82,16 +123,25 @@ export const handler = async (
|
|
|
82
123
|
case "Create": {
|
|
83
124
|
switch (resource) {
|
|
84
125
|
case RdsSqlResource.DATABASE:
|
|
85
|
-
sql = dbEngine.createDatabase(
|
|
126
|
+
sql = dbEngine.createDatabase(
|
|
127
|
+
resourceId,
|
|
128
|
+
toDatabaseEngineProps(event.ResourceProperties)
|
|
129
|
+
)
|
|
86
130
|
break
|
|
87
131
|
case RdsSqlResource.ROLE:
|
|
88
|
-
sql = await dbEngine.createRole(
|
|
132
|
+
sql = await dbEngine.createRole(
|
|
133
|
+
resourceId,
|
|
134
|
+
toRoleEngineProps(event.ResourceProperties)
|
|
135
|
+
)
|
|
89
136
|
break
|
|
90
137
|
case RdsSqlResource.SCHEMA:
|
|
91
|
-
sql = dbEngine.createSchema(
|
|
138
|
+
sql = dbEngine.createSchema(
|
|
139
|
+
resourceId,
|
|
140
|
+
toSchemaEngineProps(event.ResourceProperties)
|
|
141
|
+
)
|
|
92
142
|
break
|
|
93
143
|
case RdsSqlResource.SQL:
|
|
94
|
-
sql = dbEngine.createSql(resourceId, event.ResourceProperties)
|
|
144
|
+
sql = dbEngine.createSql(resourceId, toSqlEngineProps(event.ResourceProperties))
|
|
95
145
|
break
|
|
96
146
|
case RdsSqlResource.PARAMETER_PASSWORD:
|
|
97
147
|
await handleParameterPassword(event.ResourceProperties)
|
|
@@ -104,24 +154,35 @@ export const handler = async (
|
|
|
104
154
|
.PhysicalResourceId
|
|
105
155
|
switch (resource) {
|
|
106
156
|
case RdsSqlResource.DATABASE:
|
|
157
|
+
const dbProps = toDatabaseEngineProps(event.ResourceProperties)
|
|
107
158
|
sql = dbEngine.updateDatabase(resourceId, oldResourceId, {
|
|
108
|
-
...
|
|
159
|
+
...dbProps,
|
|
109
160
|
MasterOwner: secretValues.username,
|
|
110
161
|
})
|
|
111
162
|
break
|
|
112
163
|
case RdsSqlResource.ROLE:
|
|
164
|
+
const updateEvent =
|
|
165
|
+
event as CloudFormationCustomResourceUpdateEvent<ResourceProperties>
|
|
113
166
|
sql = await dbEngine.updateRole(
|
|
114
167
|
resourceId,
|
|
115
168
|
oldResourceId,
|
|
116
|
-
event.ResourceProperties,
|
|
117
|
-
(
|
|
169
|
+
toRoleEngineProps(event.ResourceProperties),
|
|
170
|
+
toRoleEngineProps(updateEvent.OldResourceProperties)
|
|
118
171
|
)
|
|
119
172
|
break
|
|
120
173
|
case RdsSqlResource.SCHEMA:
|
|
121
|
-
sql = dbEngine.updateSchema(
|
|
174
|
+
sql = dbEngine.updateSchema(
|
|
175
|
+
resourceId,
|
|
176
|
+
oldResourceId,
|
|
177
|
+
toSchemaEngineProps(event.ResourceProperties)
|
|
178
|
+
)
|
|
122
179
|
break
|
|
123
180
|
case RdsSqlResource.SQL:
|
|
124
|
-
sql = dbEngine.updateSql(
|
|
181
|
+
sql = dbEngine.updateSql(
|
|
182
|
+
resourceId,
|
|
183
|
+
oldResourceId,
|
|
184
|
+
toSqlEngineProps(event.ResourceProperties)
|
|
185
|
+
)
|
|
125
186
|
break
|
|
126
187
|
case RdsSqlResource.PARAMETER_PASSWORD:
|
|
127
188
|
await handleParameterPassword(event.ResourceProperties)
|
|
@@ -136,13 +197,19 @@ export const handler = async (
|
|
|
136
197
|
sql = dbEngine.deleteDatabase(resourceId, secretValues.username)
|
|
137
198
|
break
|
|
138
199
|
case RdsSqlResource.ROLE:
|
|
139
|
-
sql = dbEngine.deleteRole(
|
|
200
|
+
sql = dbEngine.deleteRole(
|
|
201
|
+
resourceId,
|
|
202
|
+
toRoleEngineProps(event.ResourceProperties)
|
|
203
|
+
)
|
|
140
204
|
break
|
|
141
205
|
case RdsSqlResource.SCHEMA:
|
|
142
|
-
sql = dbEngine.deleteSchema(
|
|
206
|
+
sql = dbEngine.deleteSchema(
|
|
207
|
+
resourceId,
|
|
208
|
+
toSchemaEngineProps(event.ResourceProperties)
|
|
209
|
+
)
|
|
143
210
|
break
|
|
144
211
|
case RdsSqlResource.SQL:
|
|
145
|
-
sql = dbEngine.deleteSql(resourceId, event.ResourceProperties)
|
|
212
|
+
sql = dbEngine.deleteSql(resourceId, toSqlEngineProps(event.ResourceProperties))
|
|
146
213
|
break
|
|
147
214
|
case RdsSqlResource.PARAMETER_PASSWORD:
|
|
148
215
|
await deleteParameterPassword(event.ResourceProperties)
|