cdk-rds-sql 6.1.1 → 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 +36 -22
- package/lambda/engine.postgresql.test.ts +21 -9
- package/lambda/engine.postgresql.ts +36 -18
- 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
|
@@ -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)
|
package/lambda/types.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import { CloudFormationCustomResourceResourcePropertiesCommon } from "aws-lambda"
|
|
2
2
|
import { RdsSqlResource } from "../src/enum"
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Engine-specific interfaces (business logic properties only)
|
|
5
|
+
export interface EngineDatabaseProperties {
|
|
6
|
+
readonly Owner?: string
|
|
7
|
+
readonly MasterOwner?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EngineRoleProperties {
|
|
11
|
+
readonly PasswordArn?: string
|
|
12
|
+
readonly DatabaseName?: string
|
|
13
|
+
readonly EnableIamAuth?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EngineSchemaProperties {
|
|
17
|
+
readonly DatabaseName?: string
|
|
18
|
+
readonly RoleName?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EngineSqlProperties {
|
|
22
|
+
readonly DatabaseName?: string
|
|
23
|
+
readonly Statement?: string
|
|
24
|
+
readonly Rollback?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Common CloudFormation properties shared by all resources
|
|
5
28
|
export interface CommonProperties
|
|
6
29
|
extends CloudFormationCustomResourceResourcePropertiesCommon {
|
|
7
30
|
readonly Resource: RdsSqlResource
|
|
@@ -9,32 +32,24 @@ export interface CommonProperties
|
|
|
9
32
|
readonly SecretArn: string
|
|
10
33
|
}
|
|
11
34
|
|
|
12
|
-
//
|
|
13
|
-
export interface DatabaseProperties extends CommonProperties {
|
|
35
|
+
// CloudFormation-specific properties (engine properties + CloudFormation metadata)
|
|
36
|
+
export interface DatabaseProperties extends CommonProperties, EngineDatabaseProperties {
|
|
14
37
|
readonly Resource: RdsSqlResource.DATABASE
|
|
15
|
-
readonly Owner?: string
|
|
16
38
|
}
|
|
17
39
|
|
|
18
|
-
// Role specific properties
|
|
19
40
|
export interface RoleProperties extends CommonProperties {
|
|
20
41
|
readonly Resource: RdsSqlResource.ROLE
|
|
21
42
|
readonly PasswordArn?: string
|
|
22
43
|
readonly DatabaseName?: string
|
|
44
|
+
readonly EnableIamAuth?: string // CloudFormation passes boolean as string
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
|
|
26
|
-
export interface SchemaProperties extends CommonProperties {
|
|
47
|
+
export interface SchemaProperties extends CommonProperties, EngineSchemaProperties {
|
|
27
48
|
readonly Resource: RdsSqlResource.SCHEMA
|
|
28
|
-
readonly DatabaseName?: string
|
|
29
|
-
readonly RoleName?: string
|
|
30
49
|
}
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
export interface SqlProperties extends CommonProperties {
|
|
51
|
+
export interface SqlProperties extends CommonProperties, EngineSqlProperties {
|
|
34
52
|
readonly Resource: RdsSqlResource.SQL
|
|
35
|
-
readonly DatabaseName?: string
|
|
36
|
-
readonly Statement?: string
|
|
37
|
-
readonly Rollback?: string
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
// Parameter password specific properties
|
package/lambda/util.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface ResourcePropertiesInput {
|
|
|
25
25
|
export const createRequest = (
|
|
26
26
|
resource: string,
|
|
27
27
|
resourceId: string,
|
|
28
|
-
props?:
|
|
28
|
+
props?: Partial<ResourceProperties>
|
|
29
29
|
): CloudFormationCustomResourceCreateEvent<ResourceProperties> => {
|
|
30
30
|
return {
|
|
31
31
|
ServiceToken: "",
|
|
@@ -41,7 +41,7 @@ export const createRequest = (
|
|
|
41
41
|
SecretArn:
|
|
42
42
|
props?.SecretArn || "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
43
43
|
...props,
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
45
|
RequestType: "Create",
|
|
46
46
|
}
|
|
47
47
|
}
|
package/lib/database.js
CHANGED
|
@@ -40,5 +40,5 @@ class Database extends aws_cdk_lib_1.CustomResource {
|
|
|
40
40
|
}
|
|
41
41
|
exports.Database = Database;
|
|
42
42
|
_a = JSII_RTTI_SYMBOL_1;
|
|
43
|
-
Database[_a] = { fqn: "cdk-rds-sql.Database", version: "6.1.
|
|
43
|
+
Database[_a] = { fqn: "cdk-rds-sql.Database", version: "6.1.2" };
|
|
44
44
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0YWJhc2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZGF0YWJhc2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSw2Q0FBNEM7QUFDNUMsMkNBQXNDO0FBQ3RDLGlDQUF1QztBQTJCdkMsTUFBTSxnQkFBaUIsU0FBUSxzQkFBUztJQUd0QyxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQXlCO1FBQ2pFLEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUE7UUFDaEIsSUFBSSxDQUFDLFlBQVksR0FBRyxLQUFLLENBQUMsWUFBWSxDQUFBO0lBQ3hDLENBQUM7Q0FDRjtBQUVELE1BQWEsUUFBUyxTQUFRLDRCQUFjO0lBQzFDOztPQUVHO0lBQ0gsTUFBTSxDQUFDLGdCQUFnQixDQUFDLEtBQWdCLEVBQUUsRUFBVSxFQUFFLFlBQW9CO1FBQ3hFLE9BQU8sSUFBSSxnQkFBZ0IsQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFO1lBQ3JDLFlBQVksRUFBRSxZQUFZO1NBQzNCLENBQUMsQ0FBQTtJQUNKLENBQUM7SUFJRCxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQW9CO1FBQzVELEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFO1lBQ2YsWUFBWSxFQUFFLEtBQUssQ0FBQyxRQUFRLENBQUMsWUFBWTtZQUN6QyxVQUFVLEVBQUU7Z0JBQ1YsUUFBUSxFQUFFLHFCQUFjLENBQUMsUUFBUTtnQkFDakMsVUFBVSxFQUFFLEtBQUssQ0FBQyxZQUFZO2dCQUM5QixTQUFTLEVBQUUsS0FBSyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsU0FBUztnQkFDMUMsS0FBSyxFQUFFLEtBQUssQ0FBQyxLQUFLLEVBQUUsUUFBUTthQUM3QjtTQUNGLENBQUMsQ0FBQTtRQUNGLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQTtRQUN2QyxJQUFJLENBQUMsWUFBWSxHQUFHLEtBQUssQ0FBQyxZQUFZLENBQUE7UUFDdEMsSUFBSSxLQUFLLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDaEIsSUFBSSxDQUFDLElBQUksQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFBO1FBQ3RDLENBQUM7SUFDSCxDQUFDOztBQTNCSCw0QkE0QkMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBDdXN0b21SZXNvdXJjZSB9IGZyb20gXCJhd3MtY2RrLWxpYlwiXG5pbXBvcnQgeyBDb25zdHJ1Y3QgfSBmcm9tIFwiY29uc3RydWN0c1wiXG5pbXBvcnQgeyBSZHNTcWxSZXNvdXJjZSB9IGZyb20gXCIuL2VudW1cIlxuaW1wb3J0IHsgUHJvdmlkZXIgfSBmcm9tIFwiLi9wcm92aWRlclwiXG5pbXBvcnQgeyBSb2xlIH0gZnJvbSBcIi4vcm9sZVwiXG5cbmludGVyZmFjZSBEYXRhYmFzZUF0dHJpYnV0ZXMge1xuICAvKipcbiAgICogTmFtZSBvZiBkYXRhYmFzZSB0byBjcmVhdGUuXG4gICAqL1xuICByZWFkb25seSBkYXRhYmFzZU5hbWU6IHN0cmluZ1xuXG4gIC8qKlxuICAgKiBPcHRpb25hbCBkYXRhYmFzZSBvd25lci5cbiAgICovXG4gIHJlYWRvbmx5IG93bmVyPzogUm9sZVxufVxuXG5leHBvcnQgaW50ZXJmYWNlIERhdGFiYXNlUHJvcHMgZXh0ZW5kcyBEYXRhYmFzZUF0dHJpYnV0ZXMge1xuICAvKipcbiAgICogUHJvdmlkZXIuXG4gICAqL1xuICByZWFkb25seSBwcm92aWRlcjogUHJvdmlkZXJcbn1cblxuZXhwb3J0IGludGVyZmFjZSBJRGF0YWJhc2Uge1xuICByZWFkb25seSBkYXRhYmFzZU5hbWU6IHN0cmluZ1xufVxuXG5jbGFzcyBJbXBvcnRlZERhdGFiYXNlIGV4dGVuZHMgQ29uc3RydWN0IGltcGxlbWVudHMgSURhdGFiYXNlIHtcbiAgcHVibGljIHJlYWRvbmx5IGRhdGFiYXNlTmFtZTogc3RyaW5nXG5cbiAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM6IERhdGFiYXNlQXR0cmlidXRlcykge1xuICAgIHN1cGVyKHNjb3BlLCBpZClcbiAgICB0aGlzLmRhdGFiYXNlTmFtZSA9IHByb3BzLmRhdGFiYXNlTmFtZVxuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBEYXRhYmFzZSBleHRlbmRzIEN1c3RvbVJlc291cmNlIGltcGxlbWVudHMgSURhdGFiYXNlIHtcbiAgLyoqXG4gICAqIFJldHVybiBhIERhdGFiYXNlIGJhc2VkIHVwb24gbmFtZSBvbmx5LiBVc2UgZm9yIGltcG9ydGluZyBleGlzdGluZyBkYXRhYmFzZXMuXG4gICAqL1xuICBzdGF0aWMgZnJvbURhdGFiYXNlTmFtZShzY29wZTogQ29uc3RydWN0LCBpZDogc3RyaW5nLCBkYXRhYmFzZU5hbWU6IHN0cmluZyk6IElEYXRhYmFzZSB7XG4gICAgcmV0dXJuIG5ldyBJbXBvcnRlZERhdGFiYXNlKHNjb3BlLCBpZCwge1xuICAgICAgZGF0YWJhc2VOYW1lOiBkYXRhYmFzZU5hbWUsXG4gICAgfSlcbiAgfVxuXG4gIHB1YmxpYyByZWFkb25seSBkYXRhYmFzZU5hbWU6IHN0cmluZ1xuXG4gIGNvbnN0cnVjdG9yKHNjb3BlOiBDb25zdHJ1Y3QsIGlkOiBzdHJpbmcsIHByb3BzOiBEYXRhYmFzZVByb3BzKSB7XG4gICAgc3VwZXIoc2NvcGUsIGlkLCB7XG4gICAgICBzZXJ2aWNlVG9rZW46IHByb3BzLnByb3ZpZGVyLnNlcnZpY2VUb2tlbixcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgUmVzb3VyY2U6IFJkc1NxbFJlc291cmNlLkRBVEFCQVNFLFxuICAgICAgICBSZXNvdXJjZUlkOiBwcm9wcy5kYXRhYmFzZU5hbWUsXG4gICAgICAgIFNlY3JldEFybjogcHJvcHMucHJvdmlkZXIuc2VjcmV0LnNlY3JldEFybixcbiAgICAgICAgT3duZXI6IHByb3BzLm93bmVyPy5yb2xlTmFtZSxcbiAgICAgIH0sXG4gICAgfSlcbiAgICB0aGlzLm5vZGUuYWRkRGVwZW5kZW5jeShwcm9wcy5wcm92aWRlcilcbiAgICB0aGlzLmRhdGFiYXNlTmFtZSA9IHByb3BzLmRhdGFiYXNlTmFtZVxuICAgIGlmIChwcm9wcy5vd25lcikge1xuICAgICAgdGhpcy5ub2RlLmFkZERlcGVuZGVuY3kocHJvcHMub3duZXIpXG4gICAgfVxuICB9XG59XG4iXX0=
|