cdk-factory 0.15.12__py3-none-any.whl → 0.15.14__py3-none-any.whl
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.
Potentially problematic release.
This version of cdk-factory might be problematic. Click here for more details.
- cdk_factory/configurations/resources/rds.py +216 -45
- cdk_factory/version.py +1 -1
- {cdk_factory-0.15.12.dist-info → cdk_factory-0.15.14.dist-info}/METADATA +1 -1
- {cdk_factory-0.15.12.dist-info → cdk_factory-0.15.14.dist-info}/RECORD +7 -7
- {cdk_factory-0.15.12.dist-info → cdk_factory-0.15.14.dist-info}/WHEEL +0 -0
- {cdk_factory-0.15.12.dist-info → cdk_factory-0.15.14.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.15.12.dist-info → cdk_factory-0.15.14.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,12 +5,15 @@ MIT License. See Project Root for license information.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple, Literal
|
|
9
9
|
from aws_lambda_powertools import Logger
|
|
10
10
|
from cdk_factory.configurations.enhanced_base_config import EnhancedBaseConfig
|
|
11
11
|
|
|
12
12
|
logger = Logger(service="RdsConfig")
|
|
13
13
|
|
|
14
|
+
# Supported RDS engines
|
|
15
|
+
Engine = Literal["mysql", "mariadb", "postgres", "aurora-mysql", "aurora-postgres", "sqlserver", "oracle"]
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
class RdsConfig(EnhancedBaseConfig):
|
|
16
19
|
"""
|
|
@@ -27,6 +30,74 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
27
30
|
def name(self) -> str:
|
|
28
31
|
"""RDS instance name"""
|
|
29
32
|
return self.__config.get("name", "database")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def identifier(self) -> str:
|
|
36
|
+
"""RDS DB instance identifier (sanitized)"""
|
|
37
|
+
raw_id = self.__config.get("identifier", self.name)
|
|
38
|
+
return self._sanitize_instance_identifier(raw_id)
|
|
39
|
+
|
|
40
|
+
def _sanitize_instance_identifier(self, identifier: str) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Sanitize DB instance identifier to meet RDS requirements:
|
|
43
|
+
- 1-63 chars, lowercase letters/digits/hyphen
|
|
44
|
+
- Must start with letter, can't end with hyphen, no consecutive hyphens
|
|
45
|
+
"""
|
|
46
|
+
if not identifier:
|
|
47
|
+
raise ValueError("Instance identifier cannot be empty")
|
|
48
|
+
|
|
49
|
+
sanitized, notes = self._sanitize_instance_identifier_impl(identifier)
|
|
50
|
+
|
|
51
|
+
if notes:
|
|
52
|
+
logger.info(f"Sanitized instance identifier from '{identifier}' to '{sanitized}': {', '.join(notes)}")
|
|
53
|
+
|
|
54
|
+
return sanitized
|
|
55
|
+
|
|
56
|
+
def _sanitize_instance_identifier_impl(self, identifier: str) -> Tuple[str, List[str]]:
|
|
57
|
+
"""
|
|
58
|
+
DB instance identifier rules (all engines):
|
|
59
|
+
- 1-63 chars, lowercase letters/digits/hyphen
|
|
60
|
+
- Must start with letter
|
|
61
|
+
- Can't end with hyphen
|
|
62
|
+
- No consecutive hyphens (--)
|
|
63
|
+
"""
|
|
64
|
+
notes: List[str] = []
|
|
65
|
+
s = identifier.lower()
|
|
66
|
+
|
|
67
|
+
# Keep only lowercase letters, digits, hyphen
|
|
68
|
+
s_clean = re.sub(r"[^a-z0-9-]", "", s)
|
|
69
|
+
if s_clean != s:
|
|
70
|
+
notes.append("removed invalid characters (only a-z, 0-9, '-' allowed)")
|
|
71
|
+
s = s_clean
|
|
72
|
+
|
|
73
|
+
if not s:
|
|
74
|
+
raise ValueError(f"Instance identifier '{identifier}' contains no valid characters")
|
|
75
|
+
|
|
76
|
+
# Must start with letter
|
|
77
|
+
if not re.match(r"^[a-z]", s):
|
|
78
|
+
s = f"db{s}"
|
|
79
|
+
notes.append("prefixed with 'db' to start with a letter")
|
|
80
|
+
|
|
81
|
+
# Collapse consecutive hyphens
|
|
82
|
+
s_collapsed = re.sub(r"-{2,}", "-", s)
|
|
83
|
+
if s_collapsed != s:
|
|
84
|
+
s = s_collapsed
|
|
85
|
+
notes.append("collapsed consecutive hyphens")
|
|
86
|
+
|
|
87
|
+
# Can't end with hyphen
|
|
88
|
+
if s.endswith("-"):
|
|
89
|
+
s = s.rstrip("-")
|
|
90
|
+
notes.append("removed trailing hyphen")
|
|
91
|
+
|
|
92
|
+
# Truncate to 63 characters
|
|
93
|
+
if len(s) > 63:
|
|
94
|
+
s = s[:63]
|
|
95
|
+
# Make sure we didn't truncate to a trailing hyphen
|
|
96
|
+
if s.endswith("-"):
|
|
97
|
+
s = s.rstrip("-")
|
|
98
|
+
notes.append("truncated to 63 characters")
|
|
99
|
+
|
|
100
|
+
return s, notes
|
|
30
101
|
|
|
31
102
|
@property
|
|
32
103
|
def engine(self) -> str:
|
|
@@ -109,8 +180,37 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
109
180
|
|
|
110
181
|
@property
|
|
111
182
|
def cloudwatch_logs_exports(self) -> List[str]:
|
|
112
|
-
"""
|
|
113
|
-
|
|
183
|
+
"""
|
|
184
|
+
Log types to export to CloudWatch (engine-specific).
|
|
185
|
+
Returns configured log types or engine-specific defaults.
|
|
186
|
+
"""
|
|
187
|
+
# If explicitly configured, use that
|
|
188
|
+
if "cloudwatch_logs_exports" in self.__config:
|
|
189
|
+
return self.__config["cloudwatch_logs_exports"]
|
|
190
|
+
|
|
191
|
+
# Otherwise, return engine-specific defaults
|
|
192
|
+
engine = self.engine.lower()
|
|
193
|
+
|
|
194
|
+
# MySQL / MariaDB
|
|
195
|
+
if engine in ("mysql", "mariadb", "aurora-mysql"):
|
|
196
|
+
return ["error", "general", "slowquery"]
|
|
197
|
+
|
|
198
|
+
# PostgreSQL
|
|
199
|
+
elif engine in ("postgres", "postgresql", "aurora-postgres", "aurora-postgresql"):
|
|
200
|
+
return ["postgresql"]
|
|
201
|
+
|
|
202
|
+
# SQL Server
|
|
203
|
+
elif engine in ("sqlserver", "sqlserver-ee", "sqlserver-se", "sqlserver-ex", "sqlserver-web"):
|
|
204
|
+
return ["error", "agent"]
|
|
205
|
+
|
|
206
|
+
# Oracle
|
|
207
|
+
elif engine in ("oracle", "oracle-ee", "oracle-se2", "oracle-se1"):
|
|
208
|
+
return ["alert", "audit", "trace"]
|
|
209
|
+
|
|
210
|
+
# Default to empty list for unknown engines (safer than guessing)
|
|
211
|
+
else:
|
|
212
|
+
logger.warning(f"Unknown engine '{engine}', disabling CloudWatch logs exports by default")
|
|
213
|
+
return []
|
|
114
214
|
|
|
115
215
|
@property
|
|
116
216
|
def removal_policy(self) -> str:
|
|
@@ -155,10 +255,8 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
155
255
|
|
|
156
256
|
def _sanitize_database_name(self, name: str) -> str:
|
|
157
257
|
"""
|
|
158
|
-
Sanitize database name to meet RDS requirements
|
|
159
|
-
|
|
160
|
-
- Can contain alphanumeric characters and underscores
|
|
161
|
-
- Max 64 characters
|
|
258
|
+
Sanitize database name to meet RDS requirements (engine-specific).
|
|
259
|
+
Implements rules from RDS documentation for each engine type.
|
|
162
260
|
|
|
163
261
|
Args:
|
|
164
262
|
name: Raw database name from config
|
|
@@ -167,41 +265,89 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
167
265
|
Sanitized database name
|
|
168
266
|
|
|
169
267
|
Raises:
|
|
170
|
-
ValueError: If name
|
|
268
|
+
ValueError: If name cannot be sanitized to meet requirements
|
|
171
269
|
"""
|
|
172
270
|
if not name:
|
|
173
271
|
raise ValueError("Database name cannot be empty")
|
|
174
272
|
|
|
175
|
-
|
|
176
|
-
sanitized =
|
|
177
|
-
sanitized = re.sub(r'[^a-zA-Z0-9_]', '', sanitized)
|
|
273
|
+
engine = self.engine.lower()
|
|
274
|
+
sanitized, notes = self._sanitize_db_name_impl(engine, name)
|
|
178
275
|
|
|
179
|
-
if
|
|
180
|
-
|
|
276
|
+
if notes:
|
|
277
|
+
logger.info(f"Sanitized database name from '{name}' to '{sanitized}': {', '.join(notes)}")
|
|
181
278
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
279
|
+
return sanitized
|
|
280
|
+
|
|
281
|
+
def _sanitize_db_name_impl(self, engine: str, name: str) -> Tuple[str, List[str]]:
|
|
282
|
+
"""
|
|
283
|
+
Engine-specific database name sanitization.
|
|
284
|
+
Based on AWS RDS naming requirements:
|
|
285
|
+
- MySQL/MariaDB: 1-64 chars, start with letter, letters/digits/underscore
|
|
286
|
+
- PostgreSQL: 1-63 chars, start with letter, letters/digits/underscore
|
|
287
|
+
- SQL Server: 1-128 chars, start with letter, letters/digits/underscore
|
|
288
|
+
- Oracle: 1-8 chars (SID), alphanumeric only, start with letter
|
|
289
|
+
"""
|
|
290
|
+
notes: List[str] = []
|
|
188
291
|
|
|
189
|
-
#
|
|
190
|
-
if
|
|
191
|
-
|
|
292
|
+
# Determine engine-specific limits
|
|
293
|
+
if engine in ("mysql", "mariadb", "aurora-mysql"):
|
|
294
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
295
|
+
max_len = 64
|
|
296
|
+
elif engine in ("postgres", "postgresql", "aurora-postgres", "aurora-postgresql"):
|
|
297
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
298
|
+
max_len = 63
|
|
299
|
+
elif engine in ("sqlserver", "sqlserver-ee", "sqlserver-se", "sqlserver-ex", "sqlserver-web"):
|
|
300
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
301
|
+
max_len = 128
|
|
302
|
+
elif engine in ("oracle", "oracle-ee", "oracle-se2", "oracle-se1"):
|
|
303
|
+
allowed_chars = r"A-Za-z0-9" # No underscore for Oracle SID
|
|
304
|
+
max_len = 8
|
|
305
|
+
else:
|
|
306
|
+
# Default to conservative rules
|
|
307
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
308
|
+
max_len = 64
|
|
309
|
+
notes.append(f"unknown engine '{engine}', using default MySQL rules")
|
|
192
310
|
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
311
|
+
# Replace hyphens with underscores (except Oracle which doesn't allow underscores)
|
|
312
|
+
s = name
|
|
313
|
+
if "oracle" not in engine:
|
|
314
|
+
s = s.replace("-", "_")
|
|
315
|
+
if "_" in name and "-" in name:
|
|
316
|
+
notes.append("replaced hyphens with underscores")
|
|
196
317
|
|
|
197
|
-
|
|
318
|
+
# Strip disallowed characters
|
|
319
|
+
s_clean = re.sub(f"[^{allowed_chars}]", "", s)
|
|
320
|
+
if s_clean != s:
|
|
321
|
+
notes.append("removed invalid characters")
|
|
322
|
+
s = s_clean
|
|
323
|
+
|
|
324
|
+
if not s:
|
|
325
|
+
raise ValueError(f"Database name '{name}' contains no valid characters after sanitization")
|
|
326
|
+
|
|
327
|
+
# Must start with a letter
|
|
328
|
+
if not re.match(r"^[A-Za-z]", s):
|
|
329
|
+
s = f"db{s}"
|
|
330
|
+
notes.append("prefixed with 'db' to start with a letter")
|
|
331
|
+
|
|
332
|
+
# Truncate to max length
|
|
333
|
+
if len(s) > max_len:
|
|
334
|
+
s = s[:max_len]
|
|
335
|
+
notes.append(f"truncated to {max_len} characters")
|
|
336
|
+
|
|
337
|
+
# SQL Server: can't start with 'rdsadmin'
|
|
338
|
+
if "sqlserver" in engine and s.lower().startswith("rdsadmin"):
|
|
339
|
+
s = f"db_{s}"
|
|
340
|
+
notes.append("prefixed to avoid 'rdsadmin' (SQL Server restriction)")
|
|
341
|
+
|
|
342
|
+
return s, notes
|
|
198
343
|
|
|
199
344
|
def _sanitize_username(self, username: str) -> str:
|
|
200
345
|
"""
|
|
201
|
-
Sanitize username to meet RDS requirements:
|
|
346
|
+
Sanitize master username to meet RDS requirements:
|
|
202
347
|
- Must begin with a letter (a-z, A-Z)
|
|
203
348
|
- Can contain alphanumeric characters and underscores
|
|
204
|
-
- Max 16 characters
|
|
349
|
+
- Max 16 characters (AWS RDS master username limit)
|
|
350
|
+
- Cannot be a reserved word
|
|
205
351
|
|
|
206
352
|
Args:
|
|
207
353
|
username: Raw username from config
|
|
@@ -215,26 +361,51 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
215
361
|
if not username:
|
|
216
362
|
raise ValueError("Username cannot be empty")
|
|
217
363
|
|
|
364
|
+
sanitized, notes = self._sanitize_master_username_impl(username)
|
|
365
|
+
|
|
366
|
+
if notes:
|
|
367
|
+
logger.info(f"Sanitized username from '{username}' to '{sanitized}': {', '.join(notes)}")
|
|
368
|
+
|
|
369
|
+
return sanitized
|
|
370
|
+
|
|
371
|
+
def _sanitize_master_username_impl(self, username: str) -> Tuple[str, List[str]]:
|
|
372
|
+
"""
|
|
373
|
+
Sanitize master username according to AWS RDS rules:
|
|
374
|
+
- 1-16 characters
|
|
375
|
+
- Start with a letter
|
|
376
|
+
- Letters, digits, underscore only
|
|
377
|
+
- Not a reserved word
|
|
378
|
+
"""
|
|
379
|
+
notes: List[str] = []
|
|
380
|
+
s = username
|
|
381
|
+
|
|
218
382
|
# Replace hyphens with underscores, remove other invalid chars
|
|
219
|
-
|
|
220
|
-
|
|
383
|
+
s = s.replace("-", "_")
|
|
384
|
+
s_clean = re.sub(r"[^A-Za-z0-9_]", "", s)
|
|
385
|
+
if s_clean != s:
|
|
386
|
+
notes.append("removed invalid characters")
|
|
387
|
+
s = s_clean
|
|
221
388
|
|
|
222
|
-
if not
|
|
223
|
-
raise ValueError(f"Username '{username}' contains no valid characters")
|
|
389
|
+
if not s:
|
|
390
|
+
raise ValueError(f"Username '{username}' contains no valid characters after sanitization")
|
|
224
391
|
|
|
225
|
-
#
|
|
226
|
-
if
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
f"Please ensure the username begins with a letter."
|
|
230
|
-
)
|
|
392
|
+
# Must start with a letter
|
|
393
|
+
if not re.match(r"^[A-Za-z]", s):
|
|
394
|
+
s = f"user{s}"
|
|
395
|
+
notes.append("prefixed with 'user' to start with a letter")
|
|
231
396
|
|
|
232
|
-
# Truncate to 16 characters
|
|
233
|
-
if len(
|
|
234
|
-
|
|
397
|
+
# Truncate to 16 characters
|
|
398
|
+
if len(s) > 16:
|
|
399
|
+
s = s[:16]
|
|
400
|
+
notes.append("truncated to 16 characters")
|
|
235
401
|
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
402
|
+
# Check against common reserved words
|
|
403
|
+
reserved = {"postgres", "mysql", "root", "admin", "rdsadmin", "system", "sa", "user"}
|
|
404
|
+
if s.lower() in reserved:
|
|
405
|
+
s = f"{s}_usr"
|
|
406
|
+
# Re-truncate if needed after adding suffix
|
|
407
|
+
if len(s) > 16:
|
|
408
|
+
s = s[:16]
|
|
409
|
+
notes.append("appended '_usr' to avoid reserved username")
|
|
239
410
|
|
|
240
|
-
return
|
|
411
|
+
return s, notes
|
cdk_factory/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.15.
|
|
1
|
+
__version__ = "0.15.14"
|
|
@@ -2,7 +2,7 @@ cdk_factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
cdk_factory/app.py,sha256=RnX0-pwdTAPAdKJK_j13Zl8anf9zYKBwboR0KA8K8xM,10346
|
|
3
3
|
cdk_factory/cdk.json,sha256=SKZKhJ2PBpFH78j-F8S3VDYW-lf76--Q2I3ON-ZIQfw,3106
|
|
4
4
|
cdk_factory/cli.py,sha256=FGbCTS5dYCNsfp-etshzvFlGDCjC28r6rtzYbe7KoHI,6407
|
|
5
|
-
cdk_factory/version.py,sha256=
|
|
5
|
+
cdk_factory/version.py,sha256=twn0Vrxaz4hLyeNEgJYUkN06H8sXuoxF6BpefwWSUTU,24
|
|
6
6
|
cdk_factory/builds/README.md,sha256=9BBWd7bXpyKdMU_g2UljhQwrC9i5O_Tvkb6oPvndoZk,90
|
|
7
7
|
cdk_factory/commands/command_loader.py,sha256=QbLquuP_AdxtlxlDy-2IWCQ6D-7qa58aphnDPtp_uTs,3744
|
|
8
8
|
cdk_factory/configurations/base_config.py,sha256=JKjhNsy0RCUZy1s8n5D_aXXI-upR9izaLtCTfKYiV9k,9624
|
|
@@ -38,7 +38,7 @@ cdk_factory/configurations/resources/lambda_layers.py,sha256=gVeP_-LC3Eq0lkPaG_J
|
|
|
38
38
|
cdk_factory/configurations/resources/lambda_triggers.py,sha256=MD7cdMNKEulNBhtMLIFnWJuJ5R-yyIqa0LHUgbSQerA,834
|
|
39
39
|
cdk_factory/configurations/resources/load_balancer.py,sha256=idpKdvkkCM7J9J2pNjMBOY1DNaFR1tk1tFjTg76bvrY,5267
|
|
40
40
|
cdk_factory/configurations/resources/monitoring.py,sha256=zsfDMa7yph33Ql8iP7lIqqLAyixh-Mesi0imtZJFdcE,2310
|
|
41
|
-
cdk_factory/configurations/resources/rds.py,sha256=
|
|
41
|
+
cdk_factory/configurations/resources/rds.py,sha256=33I1gIbUcTRoJta3E5oYO5z8L09zyohP-0JMpZ7gP7c,15139
|
|
42
42
|
cdk_factory/configurations/resources/resource_mapping.py,sha256=cwv3n63RJ6E59ErsmSTdkW4i-g8huhHtKI0ExbRhJxA,2182
|
|
43
43
|
cdk_factory/configurations/resources/resource_naming.py,sha256=VE9S2cpzp11qqPL2z1sX79wXH0o1SntO2OG74nEmWC8,5508
|
|
44
44
|
cdk_factory/configurations/resources/resource_types.py,sha256=1WQHyDoErb-M-tETZZzyLDtbq_jdC85-I403dM48pgE,2317
|
|
@@ -129,8 +129,8 @@ cdk_factory/utilities/lambda_function_utilities.py,sha256=S1GvBsY_q2cyUiaud3HORJ
|
|
|
129
129
|
cdk_factory/utilities/os_execute.py,sha256=5Op0LY_8Y-pUm04y1k8MTpNrmQvcLmQHPQITEP7EuSU,1019
|
|
130
130
|
cdk_factory/utils/api_gateway_utilities.py,sha256=If7Xu5s_UxmuV-kL3JkXxPLBdSVUKoLtohm0IUFoiV8,4378
|
|
131
131
|
cdk_factory/workload/workload_factory.py,sha256=yDI3cRhVI5ELNDcJPLpk9UY54Uind1xQoV3spzT4z7E,6068
|
|
132
|
-
cdk_factory-0.15.
|
|
133
|
-
cdk_factory-0.15.
|
|
134
|
-
cdk_factory-0.15.
|
|
135
|
-
cdk_factory-0.15.
|
|
136
|
-
cdk_factory-0.15.
|
|
132
|
+
cdk_factory-0.15.14.dist-info/METADATA,sha256=NNJ-nWleMOzgaod0J5zr4g2icglqJte9Dc2jRWQP5Kw,2452
|
|
133
|
+
cdk_factory-0.15.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
134
|
+
cdk_factory-0.15.14.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
|
|
135
|
+
cdk_factory-0.15.14.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
|
|
136
|
+
cdk_factory-0.15.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|