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.

Files changed (63) hide show
  1. cdk_factory/configurations/base_config.py +23 -24
  2. cdk_factory/configurations/cdk_config.py +6 -4
  3. cdk_factory/configurations/deployment.py +12 -0
  4. cdk_factory/configurations/devops.py +1 -1
  5. cdk_factory/configurations/pipeline_stage.py +29 -5
  6. cdk_factory/configurations/resources/acm.py +85 -0
  7. cdk_factory/configurations/resources/auto_scaling.py +7 -5
  8. cdk_factory/configurations/resources/cloudfront.py +7 -2
  9. cdk_factory/configurations/resources/ecr.py +1 -1
  10. cdk_factory/configurations/resources/ecs_cluster.py +108 -0
  11. cdk_factory/configurations/resources/ecs_service.py +17 -2
  12. cdk_factory/configurations/resources/load_balancer.py +17 -4
  13. cdk_factory/configurations/resources/monitoring.py +8 -3
  14. cdk_factory/configurations/resources/rds.py +305 -19
  15. cdk_factory/configurations/resources/rum.py +7 -2
  16. cdk_factory/configurations/resources/s3.py +1 -1
  17. cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
  18. cdk_factory/configurations/resources/vpc.py +19 -0
  19. cdk_factory/configurations/workload.py +32 -2
  20. cdk_factory/constructs/ecr/ecr_construct.py +9 -2
  21. cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
  22. cdk_factory/interfaces/istack.py +6 -3
  23. cdk_factory/interfaces/networked_stack_mixin.py +75 -0
  24. cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
  25. cdk_factory/interfaces/vpc_provider_mixin.py +210 -0
  26. cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
  27. cdk_factory/pipeline/pipeline_factory.py +222 -27
  28. cdk_factory/stack/stack_factory.py +34 -0
  29. cdk_factory/stack_library/__init__.py +3 -2
  30. cdk_factory/stack_library/acm/__init__.py +6 -0
  31. cdk_factory/stack_library/acm/acm_stack.py +169 -0
  32. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
  33. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +366 -408
  34. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
  35. cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
  36. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
  37. cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
  38. cdk_factory/stack_library/ecs/__init__.py +12 -0
  39. cdk_factory/stack_library/ecs/ecs_cluster_stack.py +316 -0
  40. cdk_factory/stack_library/ecs/ecs_service_stack.py +20 -39
  41. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
  42. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +151 -118
  43. cdk_factory/stack_library/rds/rds_stack.py +85 -74
  44. cdk_factory/stack_library/route53/route53_stack.py +8 -3
  45. cdk_factory/stack_library/rum/rum_stack.py +108 -91
  46. cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
  47. cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
  48. cdk_factory/stack_library/stack_base.py +5 -0
  49. cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
  50. cdk_factory/stack_library/websites/static_website_stack.py +1 -1
  51. cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
  52. cdk_factory/utilities/environment_services.py +5 -5
  53. cdk_factory/utilities/json_loading_utility.py +12 -3
  54. cdk_factory/validation/config_validator.py +483 -0
  55. cdk_factory/version.py +1 -1
  56. cdk_factory/workload/workload_factory.py +1 -0
  57. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
  58. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +61 -54
  59. cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
  60. cdk_factory/interfaces/ssm_parameter_mixin.py +0 -329
  61. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
  62. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
  63. {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
- from typing import Any, Dict, List, Optional
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
- return self.__config.get("database_name", "appdb")
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
- return self.__config.get("username", "appuser")
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
- env_name = self.__deployment.environment if self.__deployment else None
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. Please add an environment to the deployment.")
61
- return self.__config.get("secret_name", f"/{env_name}/db/creds")
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
- """Log types to export to CloudWatch"""
107
- return self.__config.get("cloudwatch_logs_exports", ["postgresql"])
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
- # Check both nested and flat structures for backwards compatibility
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
- # Check both nested and flat structures for backwards compatibility
146
- if "ssm" in self.__config and "exports" in self.__config["ssm"]:
147
- return self.__config["ssm"]["exports"]
148
- return self.__config.get("ssm_exports", {})
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.__config.get("ssm_exports", {})
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.__config.get("ssm_imports", {})
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 len(self.__config.keys()) == 0:
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
- # Check both nested and flat structures for backwards compatibility
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
- # Check both nested and flat structures for backwards compatibility
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.ssm_parameter_mixin import SsmParameterMixin
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, SsmParameterMixin):
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.enhanced_ssm_parameter_mixin import (
39
- EnhancedSsmParameterMixin,
38
+ from cdk_factory.interfaces.standardized_ssm_mixin import (
39
+ StandardizedSsmMixin,
40
40
  )
41
41
 
42
- self._ssm_mixin = EnhancedSsmParameterMixin()
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.setup_enhanced_ssm_integration(
49
+ self._ssm_mixin.setup_ssm_integration(
50
50
  scope=self.scope,
51
51
  config=lambda_dict,
52
52
  resource_type="lambda",
@@ -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.ssm_parameter_mixin import SsmParameterMixin
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, SsmParameterMixin, metaclass=StackABCMeta):
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
- super().__init__(scope, id, **kwargs)
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: