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