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