cdk-factory 0.16.15__py3-none-any.whl → 0.20.0__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 (66) hide show
  1. cdk_factory/configurations/base_config.py +23 -24
  2. cdk_factory/configurations/cdk_config.py +1 -1
  3. cdk_factory/configurations/deployment.py +12 -0
  4. cdk_factory/configurations/devops.py +1 -1
  5. cdk_factory/configurations/resources/acm.py +9 -2
  6. cdk_factory/configurations/resources/auto_scaling.py +7 -5
  7. cdk_factory/configurations/resources/cloudfront.py +7 -2
  8. cdk_factory/configurations/resources/ecr.py +1 -1
  9. cdk_factory/configurations/resources/ecs_cluster.py +12 -5
  10. cdk_factory/configurations/resources/ecs_service.py +30 -3
  11. cdk_factory/configurations/resources/lambda_edge.py +18 -4
  12. cdk_factory/configurations/resources/load_balancer.py +8 -9
  13. cdk_factory/configurations/resources/monitoring.py +8 -3
  14. cdk_factory/configurations/resources/rds.py +8 -9
  15. cdk_factory/configurations/resources/route53.py +5 -0
  16. cdk_factory/configurations/resources/rum.py +7 -2
  17. cdk_factory/configurations/resources/s3.py +10 -2
  18. cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
  19. cdk_factory/configurations/resources/vpc.py +19 -0
  20. cdk_factory/configurations/workload.py +32 -2
  21. cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +1 -1
  22. cdk_factory/constructs/ecr/ecr_construct.py +9 -2
  23. cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
  24. cdk_factory/interfaces/istack.py +4 -4
  25. cdk_factory/interfaces/networked_stack_mixin.py +6 -6
  26. cdk_factory/interfaces/standardized_ssm_mixin.py +684 -0
  27. cdk_factory/interfaces/vpc_provider_mixin.py +64 -33
  28. cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
  29. cdk_factory/pipeline/pipeline_factory.py +3 -3
  30. cdk_factory/stack_library/__init__.py +3 -2
  31. cdk_factory/stack_library/acm/acm_stack.py +7 -17
  32. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
  33. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +454 -537
  34. cdk_factory/stack_library/cloudfront/cloudfront_stack.py +76 -22
  35. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +5 -27
  36. cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
  37. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
  38. cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
  39. cdk_factory/stack_library/ecs/__init__.py +1 -3
  40. cdk_factory/stack_library/ecs/ecs_cluster_stack.py +159 -75
  41. cdk_factory/stack_library/ecs/ecs_service_stack.py +59 -52
  42. cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +226 -0
  43. cdk_factory/stack_library/lambda_edge/LAMBDA_EDGE_LOG_RETENTION_BLOG.md +215 -0
  44. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +240 -83
  45. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +139 -212
  46. cdk_factory/stack_library/rds/rds_stack.py +74 -98
  47. cdk_factory/stack_library/route53/route53_stack.py +246 -40
  48. cdk_factory/stack_library/rum/rum_stack.py +108 -91
  49. cdk_factory/stack_library/security_group/security_group_full_stack.py +10 -53
  50. cdk_factory/stack_library/security_group/security_group_stack.py +12 -19
  51. cdk_factory/stack_library/simple_queue_service/sqs_stack.py +1 -34
  52. cdk_factory/stack_library/stack_base.py +5 -0
  53. cdk_factory/stack_library/vpc/vpc_stack.py +171 -130
  54. cdk_factory/stack_library/websites/static_website_stack.py +7 -3
  55. cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
  56. cdk_factory/utilities/environment_services.py +5 -5
  57. cdk_factory/utilities/json_loading_utility.py +1 -1
  58. cdk_factory/validation/config_validator.py +483 -0
  59. cdk_factory/version.py +1 -1
  60. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/METADATA +1 -1
  61. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/RECORD +64 -62
  62. cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
  63. cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
  64. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/WHEEL +0 -0
  65. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/entry_points.txt +0 -0
  66. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,684 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ """
8
+ Standardized SSM Parameter Mixin for CDK Factory
9
+
10
+ This is the single, standardized approach for SSM parameter handling
11
+ across all CDK Factory modules. It replaces the mixed patterns of
12
+ Basic SSM, Enhanced SSM, and Custom SSM handling.
13
+
14
+ Key Features:
15
+ - Single source of truth for SSM integration
16
+ - Consistent configuration structure
17
+ - Template variable resolution
18
+ - Comprehensive validation
19
+ - Clear error handling
20
+ - Backward compatibility support
21
+ """
22
+
23
+ import os
24
+ import re
25
+ from typing import Dict, Any, Optional, List, Union
26
+ from aws_cdk import aws_ssm as ssm
27
+ from aws_cdk import aws_ec2 as ec2
28
+ from aws_cdk import aws_ecs as ecs
29
+ from constructs import Construct
30
+ from aws_lambda_powertools import Logger
31
+ from cdk_factory.configurations.deployment import DeploymentConfig
32
+ from cdk_factory.configurations.workload import WorkloadConfig
33
+
34
+ logger = Logger(service="StandardizedSsmMixin")
35
+
36
+
37
+ class StandardizedSsmMixin:
38
+ """
39
+ Standardized SSM parameter mixin for all CDK Factory modules.
40
+
41
+ This mixin provides a single, consistent approach for SSM parameter
42
+ handling that will be used across all modules to eliminate confusion
43
+ and ensure consistency.
44
+
45
+ Standard Configuration Structure:
46
+ {
47
+ "ssm": {
48
+ "enabled": true,
49
+ "imports": {
50
+ "parameter_name": "/path/to/parameter"
51
+ },
52
+ "exports": {
53
+ "parameter_name": {
54
+ "path": "/path/to/export",
55
+ "value": "parameter_value_or_reference"
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ Key Features:
62
+ - Configuration-driven SSM imports/exports
63
+ - Template variable resolution
64
+ - List parameter support (for security groups, etc.)
65
+ - Cached imported values for easy access
66
+ - Backward compatibility with existing interfaces
67
+ """
68
+
69
+ def __init__(self, *args, **kwargs):
70
+ """Initialize the mixin with cached storage for imported values."""
71
+ # Don't call super() to avoid MRO issues in multiple inheritance
72
+ # Initialize cached storage for imported values
73
+ self._ssm_imported_values: Dict[str, Union[str, List[str]]] = {}
74
+ self._ssm_exported_values: Dict[str, str] = {}
75
+
76
+ # Backward compatibility methods from old SsmParameterMixin
77
+ def get_ssm_imported_value(self, key: str, default: Any = None) -> Any:
78
+ """Get an SSM imported value by key with optional default."""
79
+ return self._ssm_imported_values.get(key, default)
80
+
81
+ def has_ssm_import(self, key: str) -> bool:
82
+ """Check if an SSM import exists by key."""
83
+ return key in self._ssm_imported_values
84
+
85
+ def export_ssm_parameter(
86
+ self,
87
+ scope: Construct,
88
+ id: str,
89
+ value: Any,
90
+ parameter_name: str,
91
+ description: str = None,
92
+ string_value: str = None
93
+ ) -> ssm.StringParameter:
94
+ """Export a value to SSM Parameter Store."""
95
+ if string_value is None:
96
+ string_value = str(value)
97
+
98
+ param = ssm.StringParameter(
99
+ scope,
100
+ id,
101
+ parameter_name=parameter_name,
102
+ string_value=string_value,
103
+ description=description or f"SSM parameter: {parameter_name}"
104
+ )
105
+
106
+ # Store in exported values for tracking
107
+ self._ssm_exported_values[parameter_name] = string_value
108
+ return param
109
+
110
+ def export_resource_to_ssm(
111
+ self,
112
+ scope: Construct,
113
+ resource_values: Dict[str, Any],
114
+ config: Any = None,
115
+ resource_name: str = None,
116
+ resource_type: str = None,
117
+ context: Dict[str, Any] = None
118
+ ) -> Dict[str, ssm.StringParameter]:
119
+ """Export multiple resource values to SSM Parameter Store."""
120
+ params = {}
121
+
122
+ invalid_export_keys = []
123
+ # Only export parameters that are explicitly configured in ssm_exports
124
+ if not hasattr(config, 'ssm_exports') or not config.ssm_exports:
125
+ logger.debug("No SSM exports configured")
126
+ return params
127
+
128
+ for key, export_path in config.ssm_exports.items():
129
+ # Only export if the value exists in resource_values
130
+ if key in resource_values:
131
+ value = resource_values[key]
132
+
133
+ param = self.export_ssm_parameter(
134
+ scope=scope,
135
+ id=f"{resource_name}-{key}-param",
136
+ value=value,
137
+ parameter_name=export_path,
138
+ description=f"{(resource_type or 'Resource').title()} {key} for {resource_name}"
139
+ )
140
+ params[key] = param
141
+ else:
142
+ invalid_export_keys.append(key)
143
+ logger.warning(f"SSM export configured for '{key}' but no value found in resource_values")
144
+
145
+ if invalid_export_keys:
146
+ message = f"Export SSM Error\n🚨 SSM exports configured for '{invalid_export_keys}' but no values found in resource_values"
147
+ available_keys = list(resource_values.keys())
148
+ message = f"{message}\n✅ Available keys: {available_keys}"
149
+ message = f"{message}\n👉 Please update to the correct key or remove from the export list."
150
+ logger.warning(message)
151
+ raise ValueError(message)
152
+
153
+ return params
154
+
155
+ def normalize_resource_name(self, name: str, for_export: bool = False) -> str:
156
+ """Normalize a resource name for SSM parameter naming."""
157
+ # Convert to lowercase and replace special characters with hyphens
158
+ import re
159
+ normalized = re.sub(r'[^a-zA-Z0-9-]', '-', str(name).lower())
160
+ # Remove consecutive hyphens
161
+ normalized = re.sub(r'-+', '-', normalized)
162
+ # Remove leading/trailing hyphens
163
+ normalized = normalized.strip('-')
164
+ return normalized
165
+
166
+ def setup_ssm_integration(
167
+ self,
168
+ scope: Construct,
169
+ config: Any,
170
+ resource_type: str,
171
+ resource_name: str,
172
+ deployment: DeploymentConfig = None,
173
+ workload: WorkloadConfig = None
174
+ ):
175
+ """
176
+ Setup standardized SSM integration - single entry point for all modules.
177
+
178
+ Args:
179
+ scope: The CDK construct scope
180
+ config: Configuration object with SSM settings
181
+ resource_type: Type of resource (e.g., 'vpc', 'auto_scaling', 'ecs')
182
+ resource_name: Name of the resource instance
183
+ deployment: Deployment configuration for template variables
184
+ workload: Workload configuration for template variables
185
+ """
186
+ # Store configuration references
187
+ self.scope = scope
188
+ self.resource_type = resource_type
189
+ self.resource_name = resource_name
190
+ self.deployment = deployment
191
+ self.workload = workload
192
+
193
+ # Extract configuration dictionary
194
+ if hasattr(config, 'dictionary'):
195
+ self.config_dict = config.dictionary
196
+ elif isinstance(config, dict):
197
+ self.config_dict = config
198
+ else:
199
+ self.config_dict = {}
200
+
201
+ # Initialize SSM storage
202
+ self._ssm_imported_values: Dict[str, Union[str, List[str]]] = {}
203
+ self._ssm_exported_values: Dict[str, str] = {}
204
+
205
+ # Extract SSM configuration
206
+ self.ssm_config = self.config_dict.get("ssm", {})
207
+
208
+ # Validate SSM configuration structure
209
+ self._validate_ssm_configuration()
210
+
211
+ logger.info(f"Setup standardized SSM integration for {resource_type}/{resource_name}")
212
+ logger.info(f"SSM imports: {len(self.ssm_config.get('imports', {}))}")
213
+ logger.info(f"SSM exports: {len(self.ssm_config.get('exports', {}))}")
214
+
215
+ def process_ssm_imports(self) -> None:
216
+ """
217
+ Process SSM imports using standardized approach.
218
+
219
+ This method handles:
220
+ - Template variable resolution
221
+ - Path validation
222
+ - CDK token creation
223
+ - Error handling
224
+ """
225
+ imports = self.ssm_config.get("imports", {})
226
+
227
+ if not imports:
228
+ logger.info(f"No SSM imports configured for {self.resource_type}/{self.resource_name}")
229
+ return
230
+
231
+ logger.info(f"Processing {len(imports)} SSM imports for {self.resource_type}/{self.resource_name}")
232
+
233
+ for import_key, import_value in imports.items():
234
+ try:
235
+ resolved_value = self._resolve_ssm_import(import_value, import_key)
236
+ self._ssm_imported_values[import_key] = resolved_value
237
+ logger.info(f"Successfully imported SSM parameter: {import_key}")
238
+ except Exception as e:
239
+ error_msg = f"Failed to import SSM parameter {import_key}: {str(e)}"
240
+ logger.error(error_msg)
241
+ raise ValueError(error_msg)
242
+
243
+ def export_ssm_parameters(self, resource_values: Dict[str, Any]) -> Dict[str, str]:
244
+ """
245
+ Export SSM parameters using standardized approach.
246
+
247
+ Args:
248
+ resource_values: Dictionary of resource values to export
249
+
250
+ Returns:
251
+ Dictionary mapping attribute names to SSM parameter paths
252
+ """
253
+ exports = self.ssm_config.get("exports", {})
254
+
255
+ if not exports:
256
+ logger.info(f"No SSM exports configured for {self.resource_type}/{self.resource_name}")
257
+ return {}
258
+
259
+ logger.info(f"Exporting {len(exports)} SSM parameters for {self.resource_type}/{self.resource_name}")
260
+
261
+ exported_params = {}
262
+ for export_key, export_path in exports.items():
263
+ if export_key not in resource_values:
264
+ logger.warning(f"Export key '{export_key}' not found in resource values")
265
+ continue
266
+
267
+ value = resource_values[export_key]
268
+ if value is None:
269
+ logger.warning(f"Export value for '{export_key}' is None, skipping")
270
+ continue
271
+
272
+ try:
273
+ self._create_ssm_parameter(export_key, export_path, value)
274
+ exported_params[export_key] = export_path
275
+ logger.info(f"Successfully exported SSM parameter: {export_key}")
276
+ except Exception as e:
277
+ logger.error(f"Failed to export SSM parameter {export_key}: {str(e)}")
278
+ raise
279
+
280
+ return exported_params
281
+
282
+ def resolve_ssm_value(self, scope: Construct, value: str, unique_id: str)-> str:
283
+ if isinstance(value, str) and value.startswith("{{ssm:") and value.endswith("}}"):
284
+ # Extract SSM parameter path
285
+ ssm_param_path = value[6:-2] # Remove {{ssm: and }}
286
+
287
+ # Import SSM parameter - this creates a token that resolves at deployment time
288
+ param = ssm.StringParameter.from_string_parameter_name(
289
+ scope=scope,
290
+ id=f"{unique_id}-env-{hash(ssm_param_path) % 10000}",
291
+ string_parameter_name=ssm_param_path
292
+ )
293
+ resolved_value = param.string_value
294
+ logger.info(f"Resolved SSM parameter {ssm_param_path}")
295
+ return resolved_value
296
+ else:
297
+ return value
298
+
299
+ def _resolve_ssm_import(self, import_value: Union[str, List[str]], import_key: str) -> Union[str, List[str]]:
300
+ """
301
+ Resolve SSM import value with proper error handling and validation.
302
+
303
+ Args:
304
+ import_value: SSM path or list of SSM paths
305
+ import_key: Import key for error reporting
306
+
307
+ Returns:
308
+ Resolved CDK token(s) for the SSM parameter(s)
309
+ """
310
+ if isinstance(import_value, list):
311
+ # Handle list imports (like security group IDs)
312
+ resolved_list = []
313
+ for i, value in enumerate(import_value):
314
+ resolved_item = self._resolve_single_ssm_import(value, f"{import_key}[{i}]")
315
+ resolved_list.append(resolved_item)
316
+ return resolved_list
317
+ else:
318
+ # Handle single imports
319
+ return self._resolve_single_ssm_import(import_value, import_key)
320
+
321
+ def _resolve_single_ssm_import(self, ssm_path: str, context: str) -> str:
322
+ """
323
+ Resolve individual SSM parameter import.
324
+
325
+ Args:
326
+ ssm_path: SSM parameter path with template variables
327
+ context: Context for error reporting
328
+
329
+ Returns:
330
+ CDK token for the SSM parameter
331
+ """
332
+ # Resolve template variables in path
333
+ resolved_path = self._resolve_template_variables(ssm_path)
334
+
335
+ # Validate path format
336
+ self._validate_ssm_path(resolved_path, context)
337
+
338
+ # Create CDK SSM parameter reference
339
+ construct_id = f"import-{context.replace('.', '-').replace('[', '-').replace(']', '-')}"
340
+ param = ssm.StringParameter.from_string_parameter_name(
341
+ self.scope, construct_id, resolved_path
342
+ )
343
+
344
+ # Return the CDK token (will resolve at deployment time)
345
+ return param.string_value
346
+
347
+ def _resolve_template_variables(self, template_string: str) -> str:
348
+ """
349
+ Resolve template variables in SSM paths.
350
+
351
+ Supported variables:
352
+ - {{ENVIRONMENT}}: Deployment environment
353
+ - {{WORKLOAD_NAME}}: Workload name
354
+ - {{AWS_REGION}}: AWS region
355
+
356
+ Args:
357
+ template_string: String with template variables
358
+
359
+ Returns:
360
+ Resolved string with variables replaced
361
+ """
362
+ if not template_string:
363
+ return template_string
364
+
365
+ # Prepare template variables
366
+ variables = {}
367
+
368
+ # Always prioritize workload environment for consistency
369
+ if self.workload:
370
+ variables["ENVIRONMENT"] = self.workload.dictionary.get("environment", "test")
371
+ variables["WORKLOAD_NAME"] = self.workload.dictionary.get("name", "test-workload")
372
+ variables["AWS_REGION"] = os.getenv("AWS_REGION", "us-east-1")
373
+ elif self.deployment:
374
+ # Fallback to deployment only if workload not available
375
+ variables["ENVIRONMENT"] = self.deployment.environment
376
+ variables["WORKLOAD_NAME"] = self.deployment.workload_name
377
+ variables["AWS_REGION"] = getattr(self.deployment, 'region', None) or os.getenv("AWS_REGION", "us-east-1")
378
+ else:
379
+ # Final fallback to environment variables
380
+ variables["ENVIRONMENT"] = os.getenv("ENVIRONMENT", "test")
381
+ variables["WORKLOAD_NAME"] = os.getenv("WORKLOAD_NAME", "test-workload")
382
+ variables["AWS_REGION"] = os.getenv("AWS_REGION", "us-east-1")
383
+
384
+ # Replace template variables
385
+ resolved = template_string
386
+ for key, value in variables.items():
387
+ pattern = r"\{\{" + re.escape(key) + r"\}\}"
388
+ resolved = re.sub(pattern, str(value), resolved)
389
+
390
+ # Check for unresolved variables
391
+ unresolved_vars = re.findall(r"\{\{([^}]+)\}\}", resolved)
392
+ if unresolved_vars:
393
+ logger.warning(f"Unresolved template variables: {unresolved_vars}")
394
+
395
+ return resolved
396
+
397
+ def _validate_ssm_path(self, path: str, context: str) -> None:
398
+ """
399
+ Validate SSM parameter path format.
400
+
401
+ Args:
402
+ path: SSM parameter path to validate
403
+ context: Context for error reporting
404
+
405
+ Raises:
406
+ ValueError: If path format is invalid
407
+ """
408
+ if not path:
409
+ raise ValueError(f"{context}: SSM path cannot be empty")
410
+
411
+ if not path.startswith("/"):
412
+ raise ValueError(f"{context}: SSM path must start with '/': {path}")
413
+
414
+ segments = path.split("/")
415
+ if len(segments) < 4:
416
+ raise ValueError(f"{context}: SSM path must have at least 4 segments: {path}")
417
+
418
+ # Validate path structure
419
+ # segments[0] = "" (empty from leading /)
420
+ # segments[1] = environment
421
+ # segments[2] = workload_name
422
+ # segments[3] = resource_type
423
+ # segments[4+] = attribute
424
+
425
+ if len(segments) >= 4:
426
+ environment = segments[1]
427
+ resource_type = segments[3]
428
+
429
+ # Check for valid environment patterns
430
+ if environment not in ["dev", "staging", "prod", "test", "alpha", "beta", "sandbox"]:
431
+ logger.warning(f"{context}: Unusual environment segment: {environment}")
432
+
433
+ # Check for valid resource type patterns
434
+ if not re.match(r'^[a-z][a-z0-9_-]*$', resource_type):
435
+ logger.warning(f"{context}: Unusual resource type segment: {resource_type}")
436
+
437
+ def _validate_ssm_configuration(self) -> None:
438
+ """
439
+ Validate the overall SSM configuration structure.
440
+
441
+ Raises:
442
+ ValueError: If configuration structure is invalid
443
+ """
444
+ if not isinstance(self.ssm_config, dict):
445
+ raise ValueError("SSM configuration must be a dictionary")
446
+
447
+ # Validate imports
448
+ imports = self.ssm_config.get("imports", {})
449
+ if imports is not None and not isinstance(imports, dict):
450
+ raise ValueError("SSM imports must be a dictionary")
451
+
452
+ # Validate exports
453
+ exports = self.ssm_config.get("exports", {})
454
+ if exports is not None and not isinstance(exports, dict):
455
+ raise ValueError("SSM exports must be a dictionary")
456
+
457
+ # Validate import paths
458
+ for key, value in imports.items():
459
+ if isinstance(value, list):
460
+ for i, item in enumerate(value):
461
+ self._validate_ssm_path(item, f"imports.{key}[{i}]")
462
+ else:
463
+ self._validate_ssm_path(value, f"imports.{key}")
464
+
465
+ # Validate export paths
466
+ for key, value in exports.items():
467
+ self._validate_ssm_path(value, f"exports.{key}")
468
+
469
+ def _create_ssm_parameter(self, export_key: str, export_path: str, value: Any) -> ssm.StringParameter:
470
+ """
471
+ Create SSM parameter with standard settings.
472
+
473
+ Args:
474
+ export_key: Export key for construct ID
475
+ export_path: SSM parameter path
476
+ value: Value to store
477
+
478
+ Returns:
479
+ Created SSM parameter
480
+ """
481
+ # Resolve template variables in export path
482
+ resolved_path = self._resolve_template_variables(export_path)
483
+
484
+ # Validate export path
485
+ self._validate_ssm_path(resolved_path, f"exports.{export_key}")
486
+
487
+ # Generate unique construct ID
488
+ construct_id = f"export-{export_key.replace('_', '-')}"
489
+
490
+ # Create SSM parameter with standard settings
491
+ param = ssm.StringParameter(
492
+ self.scope,
493
+ construct_id,
494
+ parameter_name=resolved_path,
495
+ string_value=str(value),
496
+ description=f"Auto-exported {export_key} for {self.resource_type}/{self.resource_name}",
497
+ tier=ssm.ParameterTier.STANDARD
498
+ )
499
+
500
+ # Track exported parameter
501
+ self._ssm_exported_values[export_key] = resolved_path
502
+
503
+ return param
504
+
505
+ # Public interface methods for accessing SSM values
506
+
507
+ def has_ssm_import(self, import_name: str) -> bool:
508
+ """
509
+ Check if SSM import exists.
510
+
511
+ Args:
512
+ import_name: Name of the import to check
513
+
514
+ Returns:
515
+ True if import exists, False otherwise
516
+ """
517
+ return import_name in self._ssm_imported_values
518
+
519
+ def get_ssm_imported_value(self, import_name: str, default: Any = None) -> Any:
520
+ """
521
+ Get SSM imported value with optional default.
522
+
523
+ Args:
524
+ import_name: Name of the import
525
+ default: Default value if import not found
526
+
527
+ Returns:
528
+ Imported value or default
529
+ """
530
+ return self._ssm_imported_values.get(import_name, default)
531
+
532
+ def get_ssm_exported_path(self, export_name: str) -> Optional[str]:
533
+ """
534
+ Get SSM exported parameter path.
535
+
536
+ Args:
537
+ export_name: Name of the export
538
+
539
+ Returns:
540
+ SSM parameter path or None if not found
541
+ """
542
+ return self._ssm_exported_values.get(export_name)
543
+
544
+ def get_all_ssm_imports(self) -> Dict[str, Union[str, List[str]]]:
545
+ """
546
+ Get all SSM imported values.
547
+
548
+ Returns:
549
+ Dictionary of all imported values
550
+ """
551
+ return self._ssm_imported_values.copy()
552
+
553
+ def get_all_ssm_exports(self) -> Dict[str, str]:
554
+ """
555
+ Get all SSM exported parameter paths.
556
+
557
+ Returns:
558
+ Dictionary of all exported parameter paths
559
+ """
560
+ return self._ssm_exported_values.copy()
561
+
562
+
563
+ def get_subnet_ids(self, config) -> List[str]:
564
+ """
565
+ Helper function to parse subnet IDs from SSM imports.
566
+
567
+ This common pattern handles:
568
+ 1. Comma-separated subnet ID strings from SSM
569
+ 2. List of subnet IDs from SSM
570
+ 3. Fallback to config attributes
571
+
572
+ Args:
573
+ config: Configuration object that might have subnet_ids attribute
574
+
575
+ Returns:
576
+ List of subnet IDs (empty list if not found or invalid format)
577
+ """
578
+ # Use the standardized SSM imports
579
+ ssm_imports = self.get_all_ssm_imports()
580
+ if "subnet_ids" in ssm_imports:
581
+ subnet_ids = ssm_imports["subnet_ids"]
582
+
583
+ # Handle comma-separated string or list
584
+ if isinstance(subnet_ids, str):
585
+ # Split comma-separated string
586
+ parsed_ids = [sid.strip() for sid in subnet_ids.split(',') if sid.strip()]
587
+ return parsed_ids
588
+ elif isinstance(subnet_ids, list):
589
+ return subnet_ids
590
+ else:
591
+ logger.warning(f"Unexpected subnet_ids type: {type(subnet_ids)}")
592
+ return []
593
+
594
+ # Fallback: Check config attributes
595
+ elif hasattr(config, 'subnet_ids') and config.subnet_ids:
596
+ return config.subnet_ids
597
+
598
+ else:
599
+ logger.warning("No subnet IDs found, using default behavior")
600
+ return []
601
+
602
+ class ValidationResult:
603
+ """Result of configuration validation."""
604
+
605
+ def __init__(self, valid: bool, errors: List[str] = None):
606
+ self.valid = valid
607
+ self.errors = errors or []
608
+
609
+
610
+ class SsmStandardValidator:
611
+ """Validator for SSM standard compliance."""
612
+
613
+ def validate_configuration(self, config: dict) -> ValidationResult:
614
+ """
615
+ Validate configuration against SSM standards.
616
+
617
+ Args:
618
+ config: Configuration dictionary to validate
619
+
620
+ Returns:
621
+ ValidationResult with validation status and errors
622
+ """
623
+ errors = []
624
+
625
+ # Check SSM configuration structure
626
+ ssm_config = config.get("ssm", {})
627
+ if not isinstance(ssm_config, dict):
628
+ errors.append("ssm configuration must be a dictionary")
629
+ else:
630
+ # Validate imports
631
+ imports = ssm_config.get("imports", {})
632
+ if imports is not None and not isinstance(imports, dict):
633
+ errors.append("ssm.imports must be a dictionary")
634
+ else:
635
+ for key, value in imports.items():
636
+ errors.extend(self._validate_import(key, value))
637
+
638
+ # Validate exports
639
+ exports = ssm_config.get("exports", {})
640
+ if exports is not None and not isinstance(exports, dict):
641
+ errors.append("ssm.exports must be a dictionary")
642
+ else:
643
+ for key, value in exports.items():
644
+ errors.extend(self._validate_export(key, value))
645
+
646
+ return ValidationResult(valid=len(errors) == 0, errors=errors)
647
+
648
+ def _validate_import(self, key: str, value) -> List[str]:
649
+ """Validate individual import configuration."""
650
+ errors = []
651
+
652
+ if isinstance(value, list):
653
+ for i, item in enumerate(value):
654
+ errors.extend(self._validate_ssm_path(item, f"imports.{key}[{i}]"))
655
+ else:
656
+ errors.extend(self._validate_ssm_path(value, f"imports.{key}"))
657
+
658
+ return errors
659
+
660
+ def _validate_export(self, key: str, value: str) -> List[str]:
661
+ """Validate individual export configuration."""
662
+ return self._validate_ssm_path(value, f"exports.{key}")
663
+
664
+ def _validate_ssm_path(self, path: str, context: str) -> List[str]:
665
+ """Validate SSM parameter path format."""
666
+ errors = []
667
+
668
+ if not path:
669
+ errors.append(f"{context}: SSM path cannot be empty")
670
+ elif not path.startswith("/"):
671
+ errors.append(f"{context}: SSM path must start with '/': {path}")
672
+ else:
673
+ segments = path.split("/")
674
+ if len(segments) < 4:
675
+ errors.append(f"{context}: SSM path must have at least 4 segments: {path}")
676
+
677
+ # Check for template variables
678
+ if "{{ENVIRONMENT}}" not in path and "{{WORKLOAD_NAME}}" not in path:
679
+ errors.append(f"{context}: SSM path should use template variables: {path}")
680
+
681
+ return errors
682
+
683
+
684
+