cdk-factory 0.15.10__py3-none-any.whl → 0.18.9__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/base_config.py +23 -24
- cdk_factory/configurations/cdk_config.py +6 -4
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/pipeline_stage.py +29 -5
- cdk_factory/configurations/resources/acm.py +85 -0
- cdk_factory/configurations/resources/auto_scaling.py +7 -5
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +108 -0
- cdk_factory/configurations/resources/ecs_service.py +17 -2
- cdk_factory/configurations/resources/load_balancer.py +17 -4
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +305 -19
- cdk_factory/configurations/resources/rum.py +7 -2
- cdk_factory/configurations/resources/s3.py +1 -1
- cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
- cdk_factory/configurations/resources/vpc.py +19 -0
- cdk_factory/configurations/workload.py +32 -2
- cdk_factory/constructs/ecr/ecr_construct.py +9 -2
- cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
- cdk_factory/interfaces/istack.py +6 -3
- cdk_factory/interfaces/networked_stack_mixin.py +75 -0
- cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +210 -0
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +222 -27
- cdk_factory/stack/stack_factory.py +34 -0
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/__init__.py +6 -0
- cdk_factory/stack_library/acm/acm_stack.py +169 -0
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +366 -408
- cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
- cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
- cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
- cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
- cdk_factory/stack_library/ecs/__init__.py +12 -0
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +316 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +20 -39
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +151 -118
- cdk_factory/stack_library/rds/rds_stack.py +85 -74
- cdk_factory/stack_library/route53/route53_stack.py +8 -3
- cdk_factory/stack_library/rum/rum_stack.py +108 -91
- cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
- cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
- cdk_factory/stack_library/stack_base.py +5 -0
- cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
- cdk_factory/stack_library/websites/static_website_stack.py +1 -1
- cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
- cdk_factory/utilities/environment_services.py +5 -5
- cdk_factory/utilities/json_loading_utility.py +12 -3
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- cdk_factory/workload/workload_factory.py +1 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +61 -54
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -329
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,9 +4,16 @@ Maintainers: Eric Wilson
|
|
|
4
4
|
MIT License. See Project Root for license information.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple, Literal
|
|
9
|
+
from aws_lambda_powertools import Logger
|
|
8
10
|
from cdk_factory.configurations.enhanced_base_config import EnhancedBaseConfig
|
|
9
11
|
|
|
12
|
+
logger = Logger(service="RdsConfig")
|
|
13
|
+
|
|
14
|
+
# Supported RDS engines
|
|
15
|
+
Engine = Literal["mysql", "mariadb", "postgres", "aurora-mysql", "aurora-postgres", "sqlserver", "oracle"]
|
|
16
|
+
|
|
10
17
|
|
|
11
18
|
class RdsConfig(EnhancedBaseConfig):
|
|
12
19
|
"""
|
|
@@ -23,6 +30,74 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
23
30
|
def name(self) -> str:
|
|
24
31
|
"""RDS instance name"""
|
|
25
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
|
|
26
101
|
|
|
27
102
|
@property
|
|
28
103
|
def engine(self) -> str:
|
|
@@ -44,27 +119,48 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
44
119
|
|
|
45
120
|
@property
|
|
46
121
|
def database_name(self) -> str:
|
|
47
|
-
"""Name of the database to create"""
|
|
48
|
-
|
|
122
|
+
"""Name of the database to create (sanitized for RDS requirements)"""
|
|
123
|
+
raw_name = self.__config.get("database_name", "appdb")
|
|
124
|
+
return self._sanitize_database_name(raw_name)
|
|
49
125
|
|
|
50
126
|
@property
|
|
51
127
|
def username(self) -> str:
|
|
52
|
-
"""Master username for the database"""
|
|
53
|
-
|
|
128
|
+
"""Master username for the database (sanitized for RDS requirements)"""
|
|
129
|
+
raw_username = self.__config.get("username", "appuser")
|
|
130
|
+
return self._sanitize_username(raw_username)
|
|
54
131
|
|
|
55
132
|
@property
|
|
56
133
|
def secret_name(self) -> str:
|
|
57
|
-
"""Name of the secret to store credentials"""
|
|
58
|
-
|
|
134
|
+
"""Name of the secret to store credentials (includes workload to prevent collisions)"""
|
|
135
|
+
if "secret_name" in self.__config:
|
|
136
|
+
return self.__config["secret_name"]
|
|
137
|
+
|
|
138
|
+
# Build a unique secret name using environment and workload
|
|
139
|
+
if not self.__deployment:
|
|
140
|
+
raise ValueError("No deployment context found for RDS secret name")
|
|
141
|
+
|
|
142
|
+
env_name = self.__deployment.environment
|
|
143
|
+
workload_name = self.__deployment.workload_name
|
|
144
|
+
|
|
59
145
|
if not env_name:
|
|
60
|
-
raise ValueError("No environment found for RDS secret name.
|
|
61
|
-
|
|
146
|
+
raise ValueError("No environment found for RDS secret name. Please add an environment to the deployment.")
|
|
147
|
+
if not workload_name:
|
|
148
|
+
raise ValueError("No workload name found for RDS secret name. Please add a workload name to the deployment.")
|
|
149
|
+
|
|
150
|
+
# Default pattern: /{environment}/{workload}/rds/credentials
|
|
151
|
+
return f"/{env_name}/{workload_name}/rds/credentials"
|
|
62
152
|
|
|
63
153
|
@property
|
|
64
154
|
def allocated_storage(self) -> int:
|
|
65
155
|
"""Allocated storage in GB"""
|
|
66
156
|
# Ensure we return an integer
|
|
67
157
|
return int(self.__config.get("allocated_storage", 20))
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def max_allocated_storage(self) -> Optional[int]:
|
|
161
|
+
"""Maximum storage for auto-scaling in GB (enables storage auto-scaling if set)"""
|
|
162
|
+
max_storage = self.__config.get("max_allocated_storage")
|
|
163
|
+
return int(max_storage) if max_storage is not None else None
|
|
68
164
|
|
|
69
165
|
@property
|
|
70
166
|
def storage_encrypted(self) -> bool:
|
|
@@ -90,6 +186,11 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
90
186
|
def enable_performance_insights(self) -> bool:
|
|
91
187
|
"""Whether to enable Performance Insights"""
|
|
92
188
|
return self.__config.get("enable_performance_insights", True)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def allow_major_version_upgrade(self) -> bool:
|
|
192
|
+
"""Whether to allow major version upgrades"""
|
|
193
|
+
return self.__config.get("allow_major_version_upgrade", False)
|
|
93
194
|
|
|
94
195
|
@property
|
|
95
196
|
def subnet_group_name(self) -> str:
|
|
@@ -103,8 +204,37 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
103
204
|
|
|
104
205
|
@property
|
|
105
206
|
def cloudwatch_logs_exports(self) -> List[str]:
|
|
106
|
-
"""
|
|
107
|
-
|
|
207
|
+
"""
|
|
208
|
+
Log types to export to CloudWatch (engine-specific).
|
|
209
|
+
Returns configured log types or engine-specific defaults.
|
|
210
|
+
"""
|
|
211
|
+
# If explicitly configured, use that
|
|
212
|
+
if "cloudwatch_logs_exports" in self.__config:
|
|
213
|
+
return self.__config["cloudwatch_logs_exports"]
|
|
214
|
+
|
|
215
|
+
# Otherwise, return engine-specific defaults
|
|
216
|
+
engine = self.engine.lower()
|
|
217
|
+
|
|
218
|
+
# MySQL / MariaDB
|
|
219
|
+
if engine in ("mysql", "mariadb", "aurora-mysql"):
|
|
220
|
+
return ["error", "general", "slowquery"]
|
|
221
|
+
|
|
222
|
+
# PostgreSQL
|
|
223
|
+
elif engine in ("postgres", "postgresql", "aurora-postgres", "aurora-postgresql"):
|
|
224
|
+
return ["postgresql"]
|
|
225
|
+
|
|
226
|
+
# SQL Server
|
|
227
|
+
elif engine in ("sqlserver", "sqlserver-ee", "sqlserver-se", "sqlserver-ex", "sqlserver-web"):
|
|
228
|
+
return ["error", "agent"]
|
|
229
|
+
|
|
230
|
+
# Oracle
|
|
231
|
+
elif engine in ("oracle", "oracle-ee", "oracle-se2", "oracle-se1"):
|
|
232
|
+
return ["alert", "audit", "trace"]
|
|
233
|
+
|
|
234
|
+
# Default to empty list for unknown engines (safer than guessing)
|
|
235
|
+
else:
|
|
236
|
+
logger.warning(f"Unknown engine '{engine}', disabling CloudWatch logs exports by default")
|
|
237
|
+
return []
|
|
108
238
|
|
|
109
239
|
@property
|
|
110
240
|
def removal_policy(self) -> str:
|
|
@@ -131,18 +261,174 @@ class RdsConfig(EnhancedBaseConfig):
|
|
|
131
261
|
"""Sets the VPC ID for the Security Group"""
|
|
132
262
|
self.__config["vpc_id"] = value
|
|
133
263
|
|
|
264
|
+
@property
|
|
265
|
+
def ssm(self) -> Dict[str, Any]:
|
|
266
|
+
"""SSM configuration"""
|
|
267
|
+
return self.__config.get("ssm", {})
|
|
268
|
+
|
|
134
269
|
@property
|
|
135
270
|
def ssm_imports(self) -> Dict[str, str]:
|
|
136
271
|
"""SSM parameter imports for the RDS instance"""
|
|
137
|
-
|
|
138
|
-
if "ssm" in self.__config and "imports" in self.__config["ssm"]:
|
|
139
|
-
return self.__config["ssm"]["imports"]
|
|
140
|
-
return self.__config.get("ssm_imports", {})
|
|
272
|
+
return self.ssm.get("imports", {})
|
|
141
273
|
|
|
142
274
|
@property
|
|
143
275
|
def ssm_exports(self) -> Dict[str, str]:
|
|
144
276
|
"""SSM parameter exports for the RDS instance"""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
277
|
+
return self.ssm.get("exports", {})
|
|
278
|
+
|
|
279
|
+
def _sanitize_database_name(self, name: str) -> str:
|
|
280
|
+
"""
|
|
281
|
+
Sanitize database name to meet RDS requirements (engine-specific).
|
|
282
|
+
Implements rules from RDS documentation for each engine type.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
name: Raw database name from config
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Sanitized database name
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
ValueError: If name cannot be sanitized to meet requirements
|
|
292
|
+
"""
|
|
293
|
+
if not name:
|
|
294
|
+
raise ValueError("Database name cannot be empty")
|
|
295
|
+
|
|
296
|
+
engine = self.engine.lower()
|
|
297
|
+
sanitized, notes = self._sanitize_db_name_impl(engine, name)
|
|
298
|
+
|
|
299
|
+
if notes:
|
|
300
|
+
logger.info(f"Sanitized database name from '{name}' to '{sanitized}': {', '.join(notes)}")
|
|
301
|
+
|
|
302
|
+
return sanitized
|
|
303
|
+
|
|
304
|
+
def _sanitize_db_name_impl(self, engine: str, name: str) -> Tuple[str, List[str]]:
|
|
305
|
+
"""
|
|
306
|
+
Engine-specific database name sanitization.
|
|
307
|
+
Based on AWS RDS naming requirements:
|
|
308
|
+
- MySQL/MariaDB: 1-64 chars, start with letter, letters/digits/underscore
|
|
309
|
+
- PostgreSQL: 1-63 chars, start with letter, letters/digits/underscore
|
|
310
|
+
- SQL Server: 1-128 chars, start with letter, letters/digits/underscore
|
|
311
|
+
- Oracle: 1-8 chars (SID), alphanumeric only, start with letter
|
|
312
|
+
"""
|
|
313
|
+
notes: List[str] = []
|
|
314
|
+
|
|
315
|
+
# Determine engine-specific limits
|
|
316
|
+
if engine in ("mysql", "mariadb", "aurora-mysql"):
|
|
317
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
318
|
+
max_len = 64
|
|
319
|
+
elif engine in ("postgres", "postgresql", "aurora-postgres", "aurora-postgresql"):
|
|
320
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
321
|
+
max_len = 63
|
|
322
|
+
elif engine in ("sqlserver", "sqlserver-ee", "sqlserver-se", "sqlserver-ex", "sqlserver-web"):
|
|
323
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
324
|
+
max_len = 128
|
|
325
|
+
elif engine in ("oracle", "oracle-ee", "oracle-se2", "oracle-se1"):
|
|
326
|
+
allowed_chars = r"A-Za-z0-9" # No underscore for Oracle SID
|
|
327
|
+
max_len = 8
|
|
328
|
+
else:
|
|
329
|
+
# Default to conservative rules
|
|
330
|
+
allowed_chars = r"A-Za-z0-9_"
|
|
331
|
+
max_len = 64
|
|
332
|
+
notes.append(f"unknown engine '{engine}', using default MySQL rules")
|
|
333
|
+
|
|
334
|
+
# Replace hyphens with underscores (except Oracle which doesn't allow underscores)
|
|
335
|
+
s = name
|
|
336
|
+
if "oracle" not in engine:
|
|
337
|
+
s = s.replace("-", "_")
|
|
338
|
+
if "_" in name and "-" in name:
|
|
339
|
+
notes.append("replaced hyphens with underscores")
|
|
340
|
+
|
|
341
|
+
# Strip disallowed characters
|
|
342
|
+
s_clean = re.sub(f"[^{allowed_chars}]", "", s)
|
|
343
|
+
if s_clean != s:
|
|
344
|
+
notes.append("removed invalid characters")
|
|
345
|
+
s = s_clean
|
|
346
|
+
|
|
347
|
+
if not s:
|
|
348
|
+
raise ValueError(f"Database name '{name}' contains no valid characters after sanitization")
|
|
349
|
+
|
|
350
|
+
# Must start with a letter
|
|
351
|
+
if not re.match(r"^[A-Za-z]", s):
|
|
352
|
+
s = f"db{s}"
|
|
353
|
+
notes.append("prefixed with 'db' to start with a letter")
|
|
354
|
+
|
|
355
|
+
# Truncate to max length
|
|
356
|
+
if len(s) > max_len:
|
|
357
|
+
s = s[:max_len]
|
|
358
|
+
notes.append(f"truncated to {max_len} characters")
|
|
359
|
+
|
|
360
|
+
# SQL Server: can't start with 'rdsadmin'
|
|
361
|
+
if "sqlserver" in engine and s.lower().startswith("rdsadmin"):
|
|
362
|
+
s = f"db_{s}"
|
|
363
|
+
notes.append("prefixed to avoid 'rdsadmin' (SQL Server restriction)")
|
|
364
|
+
|
|
365
|
+
return s, notes
|
|
366
|
+
|
|
367
|
+
def _sanitize_username(self, username: str) -> str:
|
|
368
|
+
"""
|
|
369
|
+
Sanitize master username to meet RDS requirements:
|
|
370
|
+
- Must begin with a letter (a-z, A-Z)
|
|
371
|
+
- Can contain alphanumeric characters and underscores
|
|
372
|
+
- Max 16 characters (AWS RDS master username limit)
|
|
373
|
+
- Cannot be a reserved word
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
username: Raw username from config
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Sanitized username
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
ValueError: If username is invalid
|
|
383
|
+
"""
|
|
384
|
+
if not username:
|
|
385
|
+
raise ValueError("Username cannot be empty")
|
|
386
|
+
|
|
387
|
+
sanitized, notes = self._sanitize_master_username_impl(username)
|
|
388
|
+
|
|
389
|
+
if notes:
|
|
390
|
+
logger.info(f"Sanitized username from '{username}' to '{sanitized}': {', '.join(notes)}")
|
|
391
|
+
|
|
392
|
+
return sanitized
|
|
393
|
+
|
|
394
|
+
def _sanitize_master_username_impl(self, username: str) -> Tuple[str, List[str]]:
|
|
395
|
+
"""
|
|
396
|
+
Sanitize master username according to AWS RDS rules:
|
|
397
|
+
- 1-16 characters
|
|
398
|
+
- Start with a letter
|
|
399
|
+
- Letters, digits, underscore only
|
|
400
|
+
- Not a reserved word
|
|
401
|
+
"""
|
|
402
|
+
notes: List[str] = []
|
|
403
|
+
s = username
|
|
404
|
+
|
|
405
|
+
# Replace hyphens with underscores, remove other invalid chars
|
|
406
|
+
s = s.replace("-", "_")
|
|
407
|
+
s_clean = re.sub(r"[^A-Za-z0-9_]", "", s)
|
|
408
|
+
if s_clean != s:
|
|
409
|
+
notes.append("removed invalid characters")
|
|
410
|
+
s = s_clean
|
|
411
|
+
|
|
412
|
+
if not s:
|
|
413
|
+
raise ValueError(f"Username '{username}' contains no valid characters after sanitization")
|
|
414
|
+
|
|
415
|
+
# Must start with a letter
|
|
416
|
+
if not re.match(r"^[A-Za-z]", s):
|
|
417
|
+
s = f"user{s}"
|
|
418
|
+
notes.append("prefixed with 'user' to start with a letter")
|
|
419
|
+
|
|
420
|
+
# Truncate to 16 characters
|
|
421
|
+
if len(s) > 16:
|
|
422
|
+
s = s[:16]
|
|
423
|
+
notes.append("truncated to 16 characters")
|
|
424
|
+
|
|
425
|
+
# Check against common reserved words
|
|
426
|
+
reserved = {"postgres", "mysql", "root", "admin", "rdsadmin", "system", "sa", "user"}
|
|
427
|
+
if s.lower() in reserved:
|
|
428
|
+
s = f"{s}_usr"
|
|
429
|
+
# Re-truncate if needed after adding suffix
|
|
430
|
+
if len(s) > 16:
|
|
431
|
+
s = s[:16]
|
|
432
|
+
notes.append("appended '_usr' to avoid reserved username")
|
|
433
|
+
|
|
434
|
+
return s, notes
|
|
@@ -125,11 +125,16 @@ class RumConfig(EnhancedBaseConfig):
|
|
|
125
125
|
|
|
126
126
|
# SSM Integration
|
|
127
127
|
@property
|
|
128
|
+
def ssm(self) -> Dict[str, Any]:
|
|
129
|
+
"""SSM configuration for importing/exporting resources"""
|
|
130
|
+
return self.__config.get("ssm", {})
|
|
131
|
+
|
|
132
|
+
@property
|
|
128
133
|
def ssm_exports(self) -> Dict[str, str]:
|
|
129
134
|
"""SSM parameter paths for exporting RUM resources"""
|
|
130
|
-
return self.
|
|
135
|
+
return self.ssm.get("exports", {})
|
|
131
136
|
|
|
132
137
|
@property
|
|
133
138
|
def ssm_imports(self) -> Dict[str, str]:
|
|
134
139
|
"""SSM parameter paths for importing external resources"""
|
|
135
|
-
return self.
|
|
140
|
+
return self.ssm.get("imports", {})
|
|
@@ -30,7 +30,7 @@ class S3BucketConfig(EnhancedBaseConfig):
|
|
|
30
30
|
"S3 Bucket Configuration must be a dictionary. Found: "
|
|
31
31
|
f"{type(self.__config)}"
|
|
32
32
|
)
|
|
33
|
-
if
|
|
33
|
+
if not self.__config.keys():
|
|
34
34
|
raise ValueError("S3 Bucket Configuration cannot be empty")
|
|
35
35
|
|
|
36
36
|
@property
|
|
@@ -62,21 +62,20 @@ class SecurityGroupFullStackConfig:
|
|
|
62
62
|
"""Tags to apply to the Security Group"""
|
|
63
63
|
return self.__config.get("tags", {})
|
|
64
64
|
|
|
65
|
+
@property
|
|
66
|
+
def ssm(self) -> Dict[str, Any]:
|
|
67
|
+
"""SSM configuration"""
|
|
68
|
+
return self.__config.get("ssm", {})
|
|
69
|
+
|
|
65
70
|
@property
|
|
66
71
|
def ssm_imports(self) -> Dict[str, str]:
|
|
67
72
|
"""SSM parameter imports for the Security Group"""
|
|
68
|
-
|
|
69
|
-
if "ssm" in self.__config and "imports" in self.__config["ssm"]:
|
|
70
|
-
return self.__config["ssm"]["imports"]
|
|
71
|
-
return self.__config.get("ssm_imports", {})
|
|
73
|
+
return self.ssm.get("imports", {})
|
|
72
74
|
|
|
73
75
|
@property
|
|
74
76
|
def ssm_exports(self) -> Dict[str, str]:
|
|
75
77
|
"""SSM parameter exports for the Security Group"""
|
|
76
|
-
|
|
77
|
-
if "ssm" in self.__config and "exports" in self.__config["ssm"]:
|
|
78
|
-
return self.__config["ssm"]["exports"]
|
|
79
|
-
return self.__config.get("ssm_exports", {})
|
|
78
|
+
return self.ssm.get("exports", {})
|
|
80
79
|
|
|
81
80
|
@property
|
|
82
81
|
def security_groups(self) -> List[Dict[str, Any]]:
|
|
@@ -122,3 +122,22 @@ class VpcConfig(EnhancedBaseConfig):
|
|
|
122
122
|
def isolated_subnet_name(self) -> str:
|
|
123
123
|
"""Custom name for isolated subnets"""
|
|
124
124
|
return self.get("isolated_subnet_name", "isolated")
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def subnets(self) -> Dict[str, Any]:
|
|
128
|
+
"""Subnet configuration for the VPC"""
|
|
129
|
+
return self.get("subnets", {
|
|
130
|
+
"public": {
|
|
131
|
+
"enabled": self.public_subnets,
|
|
132
|
+
"cidr_mask": self.public_subnet_mask,
|
|
133
|
+
"map_public_ip": True
|
|
134
|
+
},
|
|
135
|
+
"private": {
|
|
136
|
+
"enabled": self.private_subnets,
|
|
137
|
+
"cidr_mask": self.private_subnet_mask
|
|
138
|
+
},
|
|
139
|
+
"isolated": {
|
|
140
|
+
"enabled": self.isolated_subnets,
|
|
141
|
+
"cidr_mask": self.isolated_subnet_mask
|
|
142
|
+
}
|
|
143
|
+
})
|
|
@@ -5,6 +5,7 @@ MIT License. See Project Root for the license information.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import copy
|
|
8
9
|
from typing import Any, Dict, List
|
|
9
10
|
|
|
10
11
|
from aws_lambda_powertools import Logger
|
|
@@ -66,13 +67,42 @@ class WorkloadConfig:
|
|
|
66
67
|
if self.__app_config is None:
|
|
67
68
|
raise ValueError("Configuration is not defined.")
|
|
68
69
|
|
|
70
|
+
# Create a deep copy to avoid mutating the original configuration
|
|
69
71
|
if "workload" in self.__app_config:
|
|
70
|
-
workload = self.__app_config["workload"]
|
|
72
|
+
workload = copy.deepcopy(self.__app_config["workload"])
|
|
71
73
|
else:
|
|
72
|
-
workload = self.__app_config
|
|
74
|
+
workload = copy.deepcopy(self.__app_config)
|
|
73
75
|
|
|
74
76
|
self.__workload = workload
|
|
75
77
|
|
|
78
|
+
# Handle missing devops section gracefully
|
|
79
|
+
if "devops" not in workload:
|
|
80
|
+
logger.warning("Devops configuration not found in workload, using defaults")
|
|
81
|
+
|
|
82
|
+
# Get environment variables for defaults
|
|
83
|
+
devops_account = os.environ.get("DEVOPS_AWS_ACCOUNT")
|
|
84
|
+
devops_region = os.environ.get("DEVOPS_REGION")
|
|
85
|
+
|
|
86
|
+
# Validate required environment variables
|
|
87
|
+
if not devops_account or not devops_region:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"DEVOPS_AWS_ACCOUNT and DEVOPS_REGION environment variables must be set when devops config is missing"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Use a separate defaults object instead of mutating the original
|
|
93
|
+
devops_defaults = {
|
|
94
|
+
"account": devops_account,
|
|
95
|
+
"region": devops_region,
|
|
96
|
+
"code_repository": {
|
|
97
|
+
"name": os.environ.get("CODE_REPOSITORY_NAME", "default-repo"),
|
|
98
|
+
"type": "connector_arn",
|
|
99
|
+
"connector_arn": os.environ.get("CODE_REPOSITORY_ARN", f"arn:aws:codeconnections:{os.environ.get('DEVOPS_REGION', 'us-east-1')}:{os.environ.get('DEVOPS_AWS_ACCOUNT')}:connection/default")
|
|
100
|
+
},
|
|
101
|
+
"commands": []
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
workload["devops"] = devops_defaults
|
|
105
|
+
|
|
76
106
|
self.__devops = DevOps(workload["devops"])
|
|
77
107
|
self.__management = Management(workload.get("management", {}))
|
|
78
108
|
self.__cloudfront = CloudFrontConfig(workload.get("cloudfront", {}))
|
|
@@ -14,12 +14,12 @@ from constructs import Construct, IConstruct
|
|
|
14
14
|
from cdk_factory.configurations.resources.resource_types import ResourceTypes
|
|
15
15
|
from cdk_factory.configurations.resources.ecr import ECRConfig as ECR
|
|
16
16
|
from cdk_factory.configurations.deployment import DeploymentConfig as Deployment
|
|
17
|
-
from cdk_factory.interfaces.
|
|
17
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
18
18
|
|
|
19
19
|
logger = Logger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class ECRConstruct(Construct,
|
|
22
|
+
class ECRConstruct(Construct, StandardizedSsmMixin):
|
|
23
23
|
def __init__(
|
|
24
24
|
self,
|
|
25
25
|
scope: Construct,
|
|
@@ -30,6 +30,8 @@ class ECRConstruct(Construct, SsmParameterMixin):
|
|
|
30
30
|
**kwargs,
|
|
31
31
|
) -> None:
|
|
32
32
|
super().__init__(scope, id, **kwargs)
|
|
33
|
+
# Initialize StandardizedSsmMixin explicitly
|
|
34
|
+
StandardizedSsmMixin.__init__(self, **kwargs)
|
|
33
35
|
|
|
34
36
|
self.scope = scope
|
|
35
37
|
self.deployment = deployment
|
|
@@ -77,6 +79,11 @@ class ECRConstruct(Construct, SsmParameterMixin):
|
|
|
77
79
|
|
|
78
80
|
This method uses the new configurable SSM parameter prefix system.
|
|
79
81
|
"""
|
|
82
|
+
# Check if SSM exports are configured
|
|
83
|
+
if not hasattr(self.repo, 'ssm_exports') or not self.repo.ssm_exports:
|
|
84
|
+
logger.debug("No SSM exports configured for ECR repository")
|
|
85
|
+
return
|
|
86
|
+
|
|
80
87
|
# Create a dictionary of resource values to export
|
|
81
88
|
resource_values = {
|
|
82
89
|
"name": self.ecr.repository_name,
|
|
@@ -35,18 +35,18 @@ class ResourceResolver:
|
|
|
35
35
|
"""Get or create enhanced SSM parameter mixin"""
|
|
36
36
|
if self._ssm_mixin is None:
|
|
37
37
|
try:
|
|
38
|
-
from cdk_factory.interfaces.
|
|
39
|
-
|
|
38
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import (
|
|
39
|
+
StandardizedSsmMixin,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
self._ssm_mixin =
|
|
42
|
+
self._ssm_mixin = StandardizedSsmMixin()
|
|
43
43
|
|
|
44
44
|
# Setup enhanced SSM integration if lambda config has SSM settings
|
|
45
45
|
lambda_dict = getattr(self.lambda_config, "dictionary", {})
|
|
46
46
|
ssm_config = lambda_dict.get("ssm", {})
|
|
47
47
|
|
|
48
48
|
if ssm_config.get("enabled", False):
|
|
49
|
-
self._ssm_mixin.
|
|
49
|
+
self._ssm_mixin.setup_ssm_integration(
|
|
50
50
|
scope=self.scope,
|
|
51
51
|
config=lambda_dict,
|
|
52
52
|
resource_type="lambda",
|
cdk_factory/interfaces/istack.py
CHANGED
|
@@ -8,14 +8,14 @@ from abc import ABCMeta, abstractmethod
|
|
|
8
8
|
import jsii
|
|
9
9
|
from constructs import Construct
|
|
10
10
|
from aws_cdk import Stack
|
|
11
|
-
from cdk_factory.interfaces.
|
|
11
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class StackABCMeta(jsii.JSIIMeta, ABCMeta):
|
|
15
15
|
"""StackABCMeta"""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class IStack(Stack,
|
|
18
|
+
class IStack(Stack, StandardizedSsmMixin, metaclass=StackABCMeta):
|
|
19
19
|
"""
|
|
20
20
|
IStack for Dynamically loaded Factory Stacks
|
|
21
21
|
Only imports from constructs and abc to avoid circular dependencies.
|
|
@@ -23,7 +23,10 @@ class IStack(Stack, SsmParameterMixin, metaclass=StackABCMeta):
|
|
|
23
23
|
|
|
24
24
|
@abstractmethod
|
|
25
25
|
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
|
|
26
|
-
|
|
26
|
+
# Initialize Stack first
|
|
27
|
+
Stack.__init__(self, scope, id, **kwargs)
|
|
28
|
+
# Initialize StandardizedSsmMixin (no super() call to avoid MRO issues)
|
|
29
|
+
StandardizedSsmMixin.__init__(self, **kwargs)
|
|
27
30
|
|
|
28
31
|
@abstractmethod
|
|
29
32
|
def build(self, *, stack_config, deployment, workload) -> None:
|