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.
- cdk_factory/configurations/base_config.py +23 -24
- cdk_factory/configurations/cdk_config.py +1 -1
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/resources/acm.py +9 -2
- cdk_factory/configurations/resources/auto_scaling.py +7 -5
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +12 -5
- cdk_factory/configurations/resources/ecs_service.py +30 -3
- cdk_factory/configurations/resources/lambda_edge.py +18 -4
- cdk_factory/configurations/resources/load_balancer.py +8 -9
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +8 -9
- cdk_factory/configurations/resources/route53.py +5 -0
- cdk_factory/configurations/resources/rum.py +7 -2
- cdk_factory/configurations/resources/s3.py +10 -2
- cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
- cdk_factory/configurations/resources/vpc.py +19 -0
- cdk_factory/configurations/workload.py +32 -2
- cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +1 -1
- cdk_factory/constructs/ecr/ecr_construct.py +9 -2
- cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
- cdk_factory/interfaces/istack.py +4 -4
- cdk_factory/interfaces/networked_stack_mixin.py +6 -6
- cdk_factory/interfaces/standardized_ssm_mixin.py +684 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +64 -33
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +3 -3
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/acm_stack.py +7 -17
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +454 -537
- cdk_factory/stack_library/cloudfront/cloudfront_stack.py +76 -22
- cdk_factory/stack_library/code_artifact/code_artifact_stack.py +5 -27
- cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
- cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
- cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
- cdk_factory/stack_library/ecs/__init__.py +1 -3
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +159 -75
- cdk_factory/stack_library/ecs/ecs_service_stack.py +59 -52
- cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +226 -0
- cdk_factory/stack_library/lambda_edge/LAMBDA_EDGE_LOG_RETENTION_BLOG.md +215 -0
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +240 -83
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +139 -212
- cdk_factory/stack_library/rds/rds_stack.py +74 -98
- cdk_factory/stack_library/route53/route53_stack.py +246 -40
- cdk_factory/stack_library/rum/rum_stack.py +108 -91
- cdk_factory/stack_library/security_group/security_group_full_stack.py +10 -53
- cdk_factory/stack_library/security_group/security_group_stack.py +12 -19
- cdk_factory/stack_library/simple_queue_service/sqs_stack.py +1 -34
- cdk_factory/stack_library/stack_base.py +5 -0
- cdk_factory/stack_library/vpc/vpc_stack.py +171 -130
- cdk_factory/stack_library/websites/static_website_stack.py +7 -3
- cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
- cdk_factory/utilities/environment_services.py +5 -5
- cdk_factory/utilities/json_loading_utility.py +1 -1
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/METADATA +1 -1
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/RECORD +64 -62
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/WHEEL +0 -0
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
|