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.

@@ -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
- """Log types to export to CloudWatch"""
113
- return self.__config.get("cloudwatch_logs_exports", ["postgresql"])
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
- - Must begin with a letter (a-z, A-Z)
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 starts with a number or is empty after sanitization
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
- # Replace hyphens with underscores, remove other invalid chars
176
- sanitized = name.replace('-', '_')
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 not sanitized:
180
- raise ValueError(f"Database name '{name}' contains no valid characters")
276
+ if notes:
277
+ logger.info(f"Sanitized database name from '{name}' to '{sanitized}': {', '.join(notes)}")
181
278
 
182
- # Check if it starts with a number
183
- if sanitized[0].isdigit():
184
- raise ValueError(
185
- f"Database name '{name}' (sanitized to '{sanitized}') cannot start with a number. "
186
- f"Please ensure the database name begins with a letter."
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
- # Truncate to 64 characters if needed
190
- if len(sanitized) > 64:
191
- sanitized = sanitized[:64]
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
- # Log if sanitization changed the name
194
- if sanitized != name:
195
- logger.info(f"Sanitized database name from '{name}' to '{sanitized}'")
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
- return sanitized
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 for MySQL
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
- sanitized = username.replace('-', '_')
220
- sanitized = re.sub(r'[^a-zA-Z0-9_]', '', sanitized)
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 sanitized:
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
- # Check if it starts with a number
226
- if sanitized[0].isdigit():
227
- raise ValueError(
228
- f"Username '{username}' (sanitized to '{sanitized}') cannot start with a number. "
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 for MySQL (other engines may vary)
233
- if len(sanitized) > 16:
234
- sanitized = sanitized[:16]
397
+ # Truncate to 16 characters
398
+ if len(s) > 16:
399
+ s = s[:16]
400
+ notes.append("truncated to 16 characters")
235
401
 
236
- # Log if sanitization changed the username
237
- if sanitized != username:
238
- logger.info(f"Sanitized username from '{username}' to '{sanitized}'")
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 sanitized
411
+ return s, notes
cdk_factory/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.15.12"
1
+ __version__ = "0.15.14"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdk_factory
3
- Version: 0.15.12
3
+ Version: 0.15.14
4
4
  Summary: CDK Factory. A QuickStarter and best practices setup for CDK projects
5
5
  Author-email: Eric Wilson <eric.wilson@geekcafe.com>
6
6
  License: MIT License
@@ -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=uhKEhmOyw-8a1XvaAZVr11Ygw-f1noUzBbHqYnFKzHE,24
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=IQyi5UFf3LEWgkp71XHkjdedqW-PWS2UvpjXJpp7df0,8557
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.12.dist-info/METADATA,sha256=krTwolq-UXam3vFCtadV1i4DPAOsprJnhuNlsRWacik,2452
133
- cdk_factory-0.15.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
- cdk_factory-0.15.12.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
135
- cdk_factory-0.15.12.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
136
- cdk_factory-0.15.12.dist-info/RECORD,,
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,,