cdk-factory 0.15.10__py3-none-any.whl → 0.18.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cdk-factory might be problematic. Click here for more details.
- cdk_factory/configurations/base_config.py +23 -24
- cdk_factory/configurations/cdk_config.py +6 -4
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/pipeline_stage.py +29 -5
- cdk_factory/configurations/resources/acm.py +85 -0
- 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 +108 -0
- cdk_factory/configurations/resources/ecs_service.py +17 -2
- cdk_factory/configurations/resources/load_balancer.py +17 -4
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +305 -19
- cdk_factory/configurations/resources/rum.py +7 -2
- cdk_factory/configurations/resources/s3.py +1 -1
- 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/ecr/ecr_construct.py +9 -2
- cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
- cdk_factory/interfaces/istack.py +6 -3
- cdk_factory/interfaces/networked_stack_mixin.py +75 -0
- cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +210 -0
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +222 -27
- cdk_factory/stack/stack_factory.py +34 -0
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/__init__.py +6 -0
- cdk_factory/stack_library/acm/acm_stack.py +169 -0
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +366 -408
- cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
- 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 +12 -0
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +316 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +20 -39
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +151 -118
- cdk_factory/stack_library/rds/rds_stack.py +85 -74
- cdk_factory/stack_library/route53/route53_stack.py +8 -3
- cdk_factory/stack_library/rum/rum_stack.py +108 -91
- cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
- cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
- cdk_factory/stack_library/stack_base.py +5 -0
- cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
- cdk_factory/stack_library/websites/static_website_stack.py +1 -1
- 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 +12 -3
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- cdk_factory/workload/workload_factory.py +1 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +61 -54
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -329
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,6 +5,7 @@ Geek Cafe Pipeline
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import List, Dict, Any
|
|
8
|
+
import yaml
|
|
8
9
|
|
|
9
10
|
import aws_cdk as cdk
|
|
10
11
|
from aws_cdk import aws_codebuild as codebuild
|
|
@@ -84,6 +85,9 @@ class PipelineFactoryStack(IStack):
|
|
|
84
85
|
|
|
85
86
|
self.deployment_waves: Dict[str, pipelines.Wave] = {}
|
|
86
87
|
|
|
88
|
+
# Cache created sources keyed by repo+branch to avoid duplicate node IDs
|
|
89
|
+
self._source_cache: Dict[str, pipelines.CodePipelineSource] = {}
|
|
90
|
+
|
|
87
91
|
@property
|
|
88
92
|
def aws_code_pipeline(self) -> pipelines.CodePipeline:
|
|
89
93
|
"""AWS Code Pipeline"""
|
|
@@ -114,7 +118,7 @@ class PipelineFactoryStack(IStack):
|
|
|
114
118
|
f"\t🚨 Deployment for Environment: {deployment.environment} "
|
|
115
119
|
f"is disabled."
|
|
116
120
|
)
|
|
117
|
-
if
|
|
121
|
+
if not pipeline_deployments:
|
|
118
122
|
print(f"\t⛔️ No Pipeline Deployments configured for {self.workload.name}.")
|
|
119
123
|
|
|
120
124
|
return len(pipeline_deployments)
|
|
@@ -207,12 +211,12 @@ class PipelineFactoryStack(IStack):
|
|
|
207
211
|
stage_config=stage, pipeline_stage=pipeline_stage, deployment=deployment
|
|
208
212
|
)
|
|
209
213
|
# add the stacks to a wave or a regular
|
|
210
|
-
pre_steps = self._get_pre_steps(stage)
|
|
211
|
-
post_steps = self._get_post_steps(stage)
|
|
214
|
+
pre_steps = self._get_pre_steps(stage, deployment)
|
|
215
|
+
post_steps = self._get_post_steps(stage, deployment)
|
|
212
216
|
wave_name = stage.wave_name
|
|
213
217
|
|
|
214
218
|
# if we don't have any stacks we'll need to use the wave
|
|
215
|
-
if
|
|
219
|
+
if not stage.stacks:
|
|
216
220
|
wave_name = stage.name
|
|
217
221
|
|
|
218
222
|
if wave_name:
|
|
@@ -237,16 +241,16 @@ class PipelineFactoryStack(IStack):
|
|
|
237
241
|
)
|
|
238
242
|
|
|
239
243
|
def _get_pre_steps(
|
|
240
|
-
self, stage_config: PipelineStageConfig
|
|
244
|
+
self, stage_config: PipelineStageConfig, deployment: DeploymentConfig
|
|
241
245
|
) -> List[pipelines.ShellStep]:
|
|
242
|
-
return self._get_steps("pre_steps", stage_config)
|
|
246
|
+
return self._get_steps("pre_steps", stage_config, deployment)
|
|
243
247
|
|
|
244
248
|
def _get_post_steps(
|
|
245
|
-
self, stage_config: PipelineStageConfig
|
|
249
|
+
self, stage_config: PipelineStageConfig, deployment: DeploymentConfig
|
|
246
250
|
) -> List[pipelines.ShellStep]:
|
|
247
|
-
return self._get_steps("post_steps", stage_config)
|
|
251
|
+
return self._get_steps("post_steps", stage_config, deployment)
|
|
248
252
|
|
|
249
|
-
def _get_steps(self, key: str, stage_config: PipelineStageConfig):
|
|
253
|
+
def _get_steps(self, key: str, stage_config: PipelineStageConfig, deployment: DeploymentConfig):
|
|
250
254
|
"""
|
|
251
255
|
Gets the build steps from the config.json.
|
|
252
256
|
|
|
@@ -255,29 +259,219 @@ class PipelineFactoryStack(IStack):
|
|
|
255
259
|
- A single multi-line string (treated as a single script block)
|
|
256
260
|
|
|
257
261
|
This allows support for complex shell constructs like if blocks, loops, etc.
|
|
262
|
+
|
|
263
|
+
For builds with source/buildspec/environment, creates CodeBuildStep instead of ShellStep.
|
|
258
264
|
"""
|
|
259
|
-
shell_steps: List[pipelines.
|
|
265
|
+
shell_steps: List[pipelines.Step] = []
|
|
266
|
+
|
|
267
|
+
# Only process builds if this stage explicitly defines them
|
|
268
|
+
if not stage_config.dictionary.get("builds"):
|
|
269
|
+
return shell_steps
|
|
260
270
|
|
|
261
271
|
for build in stage_config.builds:
|
|
262
272
|
if str(build.get("enabled", "true")).lower() == "true":
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
273
|
+
# Check if this is a CodeBuild step (has source, buildspec, or environment)
|
|
274
|
+
if build.get("source") or build.get("buildspec") or build.get("environment"):
|
|
275
|
+
# Create CodeBuildStep for external builds
|
|
276
|
+
codebuild_step = self._create_codebuild_step(build, key, deployment, stage_config.name)
|
|
277
|
+
if codebuild_step:
|
|
278
|
+
shell_steps.append(codebuild_step)
|
|
279
|
+
else:
|
|
280
|
+
# Create traditional ShellStep for inline commands
|
|
281
|
+
steps = build.get(key, [])
|
|
282
|
+
step: Dict[str, Any]
|
|
283
|
+
for step in steps:
|
|
284
|
+
step_id = step.get("id") or step.get("name")
|
|
285
|
+
commands = step.get("commands", [])
|
|
286
|
+
|
|
287
|
+
# Normalize commands to a list
|
|
288
|
+
# If commands is a single string, wrap it in a list
|
|
289
|
+
if isinstance(commands, str):
|
|
290
|
+
commands = [commands]
|
|
291
|
+
|
|
292
|
+
shell_step = pipelines.ShellStep(
|
|
293
|
+
id=step_id,
|
|
294
|
+
commands=commands,
|
|
295
|
+
)
|
|
296
|
+
shell_steps.append(shell_step)
|
|
279
297
|
|
|
280
298
|
return shell_steps
|
|
299
|
+
|
|
300
|
+
def _create_codebuild_step(self, build: Dict[str, Any], key: str, deployment: DeploymentConfig, stage_name: str) -> pipelines.CodeBuildStep:
|
|
301
|
+
"""
|
|
302
|
+
Creates a CodeBuildStep for builds that specify source, buildspec, or environment.
|
|
303
|
+
|
|
304
|
+
Supports:
|
|
305
|
+
- External GitHub repositories (public and private)
|
|
306
|
+
- Custom buildspec files
|
|
307
|
+
- Environment configuration (compute type, image, privileged mode)
|
|
308
|
+
- Environment variables
|
|
309
|
+
- GitHub authentication via CodeConnections
|
|
310
|
+
"""
|
|
311
|
+
build_name = build.get("name", "custom-build")
|
|
312
|
+
|
|
313
|
+
# Parse source configuration
|
|
314
|
+
source_config = build.get("source", {})
|
|
315
|
+
source_type = source_config.get("type", "GITHUB").upper()
|
|
316
|
+
source_location = source_config.get("location")
|
|
317
|
+
source_branch = source_config.get("branch", "main")
|
|
318
|
+
|
|
319
|
+
# Determine if this is the right step type (pre or post)
|
|
320
|
+
# Only create the step if it's supposed to run at this point
|
|
321
|
+
# For now, assume CodeBuild steps run as pre_steps by default
|
|
322
|
+
if key != "pre_steps":
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
if not source_location:
|
|
326
|
+
logger.warning(f"Build '{build_name}' has no source location specified, skipping")
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
# Parse buildspec
|
|
330
|
+
# If a buildspec path is specified, try to load it locally and convert to from_object()
|
|
331
|
+
buildspec_path = build.get("buildspec")
|
|
332
|
+
buildspec = None
|
|
333
|
+
|
|
334
|
+
if buildspec_path:
|
|
335
|
+
try:
|
|
336
|
+
candidate = Path(buildspec_path)
|
|
337
|
+
if not candidate.is_file():
|
|
338
|
+
# Try relative to cwd
|
|
339
|
+
candidate = Path(os.getcwd()) / buildspec_path
|
|
340
|
+
if candidate.is_file():
|
|
341
|
+
with candidate.open("r", encoding="utf-8") as f:
|
|
342
|
+
yml = yaml.safe_load(f)
|
|
343
|
+
if isinstance(yml, dict):
|
|
344
|
+
buildspec = codebuild.BuildSpec.from_object(yml)
|
|
345
|
+
else:
|
|
346
|
+
raise ValueError("Parsed buildspec YAML is not a dictionary")
|
|
347
|
+
else:
|
|
348
|
+
raise FileNotFoundError(f"Buildspec file not found: {buildspec_path}")
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
raise RuntimeError(f"Failed to load buildspec from '{buildspec_path}': {exc}")
|
|
351
|
+
else:
|
|
352
|
+
# No buildspec specified - check for inline commands
|
|
353
|
+
inline_commands = build.get("commands", [])
|
|
354
|
+
if isinstance(inline_commands, str):
|
|
355
|
+
inline_commands = [inline_commands]
|
|
356
|
+
if inline_commands:
|
|
357
|
+
# Create inline buildspec from commands
|
|
358
|
+
buildspec = codebuild.BuildSpec.from_object({
|
|
359
|
+
"version": "0.2",
|
|
360
|
+
"phases": {
|
|
361
|
+
"build": {
|
|
362
|
+
"commands": inline_commands
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
# Parse environment configuration
|
|
368
|
+
env_config = build.get("environment", {})
|
|
369
|
+
compute_type_str = env_config.get("compute_type", "BUILD_GENERAL1_SMALL")
|
|
370
|
+
|
|
371
|
+
# Map string to CDK enum
|
|
372
|
+
compute_type_map = {
|
|
373
|
+
"BUILD_GENERAL1_SMALL": codebuild.ComputeType.SMALL,
|
|
374
|
+
"BUILD_GENERAL1_MEDIUM": codebuild.ComputeType.MEDIUM,
|
|
375
|
+
"BUILD_GENERAL1_LARGE": codebuild.ComputeType.LARGE,
|
|
376
|
+
"BUILD_GENERAL1_2XLARGE": codebuild.ComputeType.X2_LARGE,
|
|
377
|
+
}
|
|
378
|
+
compute_type = compute_type_map.get(compute_type_str, codebuild.ComputeType.SMALL)
|
|
379
|
+
|
|
380
|
+
build_image_str = env_config.get("image", "aws/codebuild/standard:7.0")
|
|
381
|
+
privileged_mode = env_config.get("privileged_mode", False)
|
|
382
|
+
|
|
383
|
+
# Parse build image
|
|
384
|
+
if build_image_str.startswith("aws/codebuild/standard:"):
|
|
385
|
+
version = build_image_str.split(":")[-1]
|
|
386
|
+
build_image = codebuild.LinuxBuildImage.from_code_build_image_id(f"aws/codebuild/standard:{version}")
|
|
387
|
+
else:
|
|
388
|
+
build_image = codebuild.LinuxBuildImage.from_code_build_image_id(build_image_str)
|
|
389
|
+
|
|
390
|
+
# Parse environment variables
|
|
391
|
+
env_vars_list = build.get("environment_variables", [])
|
|
392
|
+
env_vars = {}
|
|
393
|
+
for env_var in env_vars_list:
|
|
394
|
+
var_name = env_var.get("name")
|
|
395
|
+
var_value = env_var.get("value")
|
|
396
|
+
var_type = env_var.get("type", "PLAINTEXT")
|
|
397
|
+
|
|
398
|
+
if var_name and var_value is not None:
|
|
399
|
+
if var_type == "PLAINTEXT":
|
|
400
|
+
env_vars[var_name] = codebuild.BuildEnvironmentVariable(value=str(var_value))
|
|
401
|
+
elif var_type == "PARAMETER_STORE":
|
|
402
|
+
env_vars[var_name] = codebuild.BuildEnvironmentVariable(
|
|
403
|
+
value=str(var_value),
|
|
404
|
+
type=codebuild.BuildEnvironmentVariableType.PARAMETER_STORE
|
|
405
|
+
)
|
|
406
|
+
elif var_type == "SECRETS_MANAGER":
|
|
407
|
+
env_vars[var_name] = codebuild.BuildEnvironmentVariable(
|
|
408
|
+
value=str(var_value),
|
|
409
|
+
type=codebuild.BuildEnvironmentVariableType.SECRETS_MANAGER
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Create build environment
|
|
413
|
+
build_environment = codebuild.BuildEnvironment(
|
|
414
|
+
build_image=build_image,
|
|
415
|
+
compute_type=compute_type,
|
|
416
|
+
privileged=privileged_mode,
|
|
417
|
+
environment_variables=env_vars
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Determine input source
|
|
421
|
+
if source_type == "GITHUB":
|
|
422
|
+
# GitHub source - supports both public and private repos
|
|
423
|
+
# For private repos, use the workload's code repository connection
|
|
424
|
+
repo_string = self._parse_github_repo_string(source_location)
|
|
425
|
+
cache_key = f"{repo_string}:{source_branch}"
|
|
426
|
+
input_source = self._source_cache.get(cache_key)
|
|
427
|
+
if not input_source:
|
|
428
|
+
input_source = pipelines.CodePipelineSource.connection(
|
|
429
|
+
repo_string=repo_string,
|
|
430
|
+
branch=source_branch,
|
|
431
|
+
connection_arn=self.workload.devops.code_repository.connector_arn,
|
|
432
|
+
action_name=f"{build_name}",
|
|
433
|
+
)
|
|
434
|
+
self._source_cache[cache_key] = input_source
|
|
435
|
+
else:
|
|
436
|
+
logger.warning(f"Unsupported source type '{source_type}' for build '{build_name}'")
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
# Create CodeBuildStep
|
|
440
|
+
logger.info(f"Creating CodeBuildStep '{build_name}' with source from {source_location}")
|
|
441
|
+
|
|
442
|
+
# CodeBuildStep requires 'commands' param; when using a buildspec (repo or inline)
|
|
443
|
+
# we pass an empty list so only the buildspec runs
|
|
444
|
+
commands: List[str] = []
|
|
445
|
+
|
|
446
|
+
codebuild_step = pipelines.CodeBuildStep(
|
|
447
|
+
id=f"{build_name}-{stage_name}",
|
|
448
|
+
input=input_source,
|
|
449
|
+
commands=commands,
|
|
450
|
+
build_environment=build_environment,
|
|
451
|
+
partial_build_spec=buildspec,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return codebuild_step
|
|
455
|
+
|
|
456
|
+
def _parse_github_repo_string(self, location: str) -> str:
|
|
457
|
+
"""
|
|
458
|
+
Converts GitHub URL to org/repo format.
|
|
459
|
+
|
|
460
|
+
Examples:
|
|
461
|
+
- https://github.com/geekcafe/myrepo.git -> geekcafe/myrepo
|
|
462
|
+
- https://github.com/geekcafe/myrepo -> geekcafe/myrepo
|
|
463
|
+
- geekcafe/myrepo -> geekcafe/myrepo
|
|
464
|
+
"""
|
|
465
|
+
if location.startswith("https://github.com/"):
|
|
466
|
+
# Remove https://github.com/ prefix
|
|
467
|
+
repo_string = location.replace("https://github.com/", "")
|
|
468
|
+
# Remove .git suffix if present
|
|
469
|
+
if repo_string.endswith(".git"):
|
|
470
|
+
repo_string = repo_string[:-4]
|
|
471
|
+
return repo_string
|
|
472
|
+
else:
|
|
473
|
+
# Assume it's already in org/repo format
|
|
474
|
+
return location
|
|
281
475
|
|
|
282
476
|
def __setup_stacks(
|
|
283
477
|
self,
|
|
@@ -305,6 +499,7 @@ class PipelineFactoryStack(IStack):
|
|
|
305
499
|
scope=pipeline_stage,
|
|
306
500
|
id=stack_config.name,
|
|
307
501
|
deployment=deployment,
|
|
502
|
+
stack_config=stack_config,
|
|
308
503
|
add_env_context=self.add_env_context,
|
|
309
504
|
**kwargs,
|
|
310
505
|
)
|
|
@@ -324,7 +519,7 @@ class PipelineFactoryStack(IStack):
|
|
|
324
519
|
f"\t\t ⚠️ Stack {stack_config.name} is disabled in stage: {stage_config.name}"
|
|
325
520
|
)
|
|
326
521
|
|
|
327
|
-
if
|
|
522
|
+
if not cf_stacks:
|
|
328
523
|
print(f"\t\t ⚠️ No stacks added to stage: {stage_config.name}")
|
|
329
524
|
print(f"\t\t ⚠️ Internal Stack Count: {len(stage_config.stacks)}")
|
|
330
525
|
|
|
@@ -11,10 +11,28 @@ from cdk_factory.interfaces.istack import IStack
|
|
|
11
11
|
from cdk_factory.stack.stack_module_loader import ModuleLoader
|
|
12
12
|
from cdk_factory.stack.stack_module_registry import modules
|
|
13
13
|
from cdk_factory.configurations.deployment import DeploymentConfig
|
|
14
|
+
from cdk_factory.configurations.stack import StackConfig
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class StackFactory:
|
|
17
18
|
"""Stack Factory"""
|
|
19
|
+
|
|
20
|
+
# Default descriptions by module type
|
|
21
|
+
DEFAULT_DESCRIPTIONS = {
|
|
22
|
+
"vpc_stack": "VPC infrastructure with public and private subnets across multiple availability zones",
|
|
23
|
+
"security_group_stack": "Security groups for network access control",
|
|
24
|
+
"security_group_full_stack": "Security groups for ALB, ECS, RDS, and monitoring",
|
|
25
|
+
"rds_stack": "Managed relational database instance with automated backups",
|
|
26
|
+
"s3_bucket_stack": "S3 bucket for object storage",
|
|
27
|
+
"media_bucket_stack": "S3 bucket for media asset storage with CDN integration",
|
|
28
|
+
"static_website_stack": "Static website hosted on S3 with CloudFront distribution",
|
|
29
|
+
"ecs_service_stack": "ECS service with auto-scaling and load balancing",
|
|
30
|
+
"lambda_stack": "Lambda function for serverless compute",
|
|
31
|
+
"api_gateway_stack": "API Gateway for REST API endpoints",
|
|
32
|
+
"cloudfront_stack": "CloudFront CDN distribution",
|
|
33
|
+
"monitoring_stack": "CloudWatch monitoring, alarms, and dashboards",
|
|
34
|
+
"ecr_stack": "Elastic Container Registry for Docker images",
|
|
35
|
+
}
|
|
18
36
|
|
|
19
37
|
def __init__(self):
|
|
20
38
|
ml: ModuleLoader = ModuleLoader()
|
|
@@ -27,6 +45,7 @@ class StackFactory:
|
|
|
27
45
|
scope,
|
|
28
46
|
id: str, # pylint: disable=redefined-builtin
|
|
29
47
|
deployment: Optional[DeploymentConfig] = None,
|
|
48
|
+
stack_config: Optional[StackConfig] = None,
|
|
30
49
|
add_env_context: bool = True,
|
|
31
50
|
**kwargs,
|
|
32
51
|
) -> IStack:
|
|
@@ -40,6 +59,12 @@ class StackFactory:
|
|
|
40
59
|
if deployment and add_env_context:
|
|
41
60
|
env_kwargs = self._get_environment_kwargs(deployment)
|
|
42
61
|
kwargs.update(env_kwargs)
|
|
62
|
+
|
|
63
|
+
# Add description if not already provided in kwargs
|
|
64
|
+
if "description" not in kwargs:
|
|
65
|
+
description = self._get_stack_description(module_name, stack_config)
|
|
66
|
+
if description:
|
|
67
|
+
kwargs["description"] = description
|
|
43
68
|
|
|
44
69
|
module = stack_class(scope=scope, id=id, **kwargs)
|
|
45
70
|
|
|
@@ -52,3 +77,12 @@ class StackFactory:
|
|
|
52
77
|
region=deployment.region
|
|
53
78
|
)
|
|
54
79
|
return {"env": env}
|
|
80
|
+
|
|
81
|
+
def _get_stack_description(self, module_name: str, stack_config: Optional[StackConfig] = None) -> Optional[str]:
|
|
82
|
+
"""Get stack description from config or default"""
|
|
83
|
+
# First check if stack_config has a description
|
|
84
|
+
if stack_config and stack_config.description:
|
|
85
|
+
return stack_config.description
|
|
86
|
+
|
|
87
|
+
# Otherwise use default description based on module type
|
|
88
|
+
return self.DEFAULT_DESCRIPTIONS.get(module_name)
|
|
@@ -12,9 +12,10 @@ paths = []
|
|
|
12
12
|
|
|
13
13
|
try:
|
|
14
14
|
paths = __path__
|
|
15
|
-
except
|
|
15
|
+
except (AttributeError, ImportError) as e:
|
|
16
|
+
# Fallback to using os.path if __path__ is not available
|
|
16
17
|
import os
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
paths.append(os.path.dirname(__file__))
|
|
19
20
|
|
|
20
21
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACM (AWS Certificate Manager) Stack Pattern for CDK-Factory
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any, List, Optional
|
|
8
|
+
|
|
9
|
+
import aws_cdk as cdk
|
|
10
|
+
from aws_cdk import aws_certificatemanager as acm
|
|
11
|
+
from aws_cdk import aws_route53 as route53
|
|
12
|
+
from aws_lambda_powertools import Logger
|
|
13
|
+
from constructs import Construct
|
|
14
|
+
|
|
15
|
+
from cdk_factory.configurations.deployment import DeploymentConfig
|
|
16
|
+
from cdk_factory.configurations.stack import StackConfig
|
|
17
|
+
from cdk_factory.configurations.resources.acm import AcmConfig
|
|
18
|
+
from cdk_factory.interfaces.istack import IStack
|
|
19
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
20
|
+
from cdk_factory.stack.stack_module_registry import register_stack
|
|
21
|
+
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
22
|
+
|
|
23
|
+
logger = Logger(service="AcmStack")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@register_stack("acm_stack")
|
|
27
|
+
@register_stack("certificate_stack")
|
|
28
|
+
class AcmStack(IStack, StandardizedSsmMixin):
|
|
29
|
+
"""
|
|
30
|
+
Reusable stack for AWS Certificate Manager.
|
|
31
|
+
Supports creating ACM certificates with DNS validation via Route53.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
|
|
35
|
+
super().__init__(scope, id, **kwargs)
|
|
36
|
+
self.acm_config = None
|
|
37
|
+
self.stack_config = None
|
|
38
|
+
self.deployment = None
|
|
39
|
+
self.workload = None
|
|
40
|
+
self.certificate = None
|
|
41
|
+
self.hosted_zone = None
|
|
42
|
+
|
|
43
|
+
def build(
|
|
44
|
+
self,
|
|
45
|
+
stack_config: StackConfig,
|
|
46
|
+
deployment: DeploymentConfig,
|
|
47
|
+
workload: WorkloadConfig,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Build the ACM Certificate stack"""
|
|
50
|
+
self._build(stack_config, deployment, workload)
|
|
51
|
+
|
|
52
|
+
def _build(
|
|
53
|
+
self,
|
|
54
|
+
stack_config: StackConfig,
|
|
55
|
+
deployment: DeploymentConfig,
|
|
56
|
+
workload: WorkloadConfig,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Internal build method for the ACM Certificate stack"""
|
|
59
|
+
self.stack_config = stack_config
|
|
60
|
+
self.deployment = deployment
|
|
61
|
+
self.workload = workload
|
|
62
|
+
self.acm_config = AcmConfig(
|
|
63
|
+
stack_config.dictionary.get("certificate", {}), deployment
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
cert_name = deployment.build_resource_name(self.acm_config.name)
|
|
67
|
+
|
|
68
|
+
# Get or import hosted zone for DNS validation
|
|
69
|
+
if self.acm_config.hosted_zone_id:
|
|
70
|
+
self.hosted_zone = self._get_hosted_zone()
|
|
71
|
+
|
|
72
|
+
# Create the certificate
|
|
73
|
+
self.certificate = self._create_certificate(cert_name)
|
|
74
|
+
|
|
75
|
+
# Export certificate ARN to SSM
|
|
76
|
+
self._export_certificate_arn(cert_name)
|
|
77
|
+
|
|
78
|
+
# Add outputs
|
|
79
|
+
self._add_outputs(cert_name)
|
|
80
|
+
|
|
81
|
+
def _get_hosted_zone(self) -> route53.IHostedZone:
|
|
82
|
+
"""Get the Route53 hosted zone for DNS validation"""
|
|
83
|
+
if self.acm_config.hosted_zone_id:
|
|
84
|
+
return route53.HostedZone.from_hosted_zone_attributes(
|
|
85
|
+
self,
|
|
86
|
+
"HostedZone",
|
|
87
|
+
hosted_zone_id=self.acm_config.hosted_zone_id,
|
|
88
|
+
zone_name=self.acm_config.domain_name,
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"hosted_zone_id is required for DNS validation. "
|
|
93
|
+
"Provide it in the certificate configuration."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _create_certificate(self, cert_name: str) -> acm.Certificate:
|
|
97
|
+
"""Create an ACM certificate with DNS validation"""
|
|
98
|
+
|
|
99
|
+
# Prepare certificate properties
|
|
100
|
+
cert_props = {
|
|
101
|
+
"domain_name": self.acm_config.domain_name,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Add DNS validation if hosted zone is available
|
|
105
|
+
if self.hosted_zone:
|
|
106
|
+
cert_props["validation"] = acm.CertificateValidation.from_dns(
|
|
107
|
+
self.hosted_zone
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Add subject alternative names if provided
|
|
111
|
+
if self.acm_config.subject_alternative_names:
|
|
112
|
+
cert_props["subject_alternative_names"] = (
|
|
113
|
+
self.acm_config.subject_alternative_names
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
certificate = acm.Certificate(
|
|
117
|
+
self,
|
|
118
|
+
cert_name,
|
|
119
|
+
**cert_props
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Add tags
|
|
123
|
+
for key, value in self.acm_config.tags.items():
|
|
124
|
+
cdk.Tags.of(certificate).add(key, value)
|
|
125
|
+
|
|
126
|
+
logger.info(f"Created certificate for domain: {self.acm_config.domain_name}")
|
|
127
|
+
|
|
128
|
+
return certificate
|
|
129
|
+
|
|
130
|
+
def _export_certificate_arn(self, cert_name: str) -> None:
|
|
131
|
+
"""Export certificate ARN to SSM Parameter Store"""
|
|
132
|
+
ssm_exports = self.acm_config.ssm_exports
|
|
133
|
+
|
|
134
|
+
if not ssm_exports:
|
|
135
|
+
logger.debug("No SSM exports configured for certificate")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Export certificate ARN
|
|
139
|
+
if "certificate_arn" in ssm_exports:
|
|
140
|
+
param_name = ssm_exports["certificate_arn"]
|
|
141
|
+
if not param_name.startswith("/"):
|
|
142
|
+
param_name = f"/{param_name}"
|
|
143
|
+
|
|
144
|
+
self.export_ssm_parameter(
|
|
145
|
+
scope=self,
|
|
146
|
+
id=f"{cert_name}-cert-arn-param",
|
|
147
|
+
value=self.certificate.certificate_arn,
|
|
148
|
+
parameter_name=param_name,
|
|
149
|
+
description=f"Certificate ARN for {self.acm_config.domain_name}",
|
|
150
|
+
)
|
|
151
|
+
logger.info(f"Exported certificate ARN to SSM: {param_name}")
|
|
152
|
+
|
|
153
|
+
def _add_outputs(self, cert_name: str) -> None:
|
|
154
|
+
"""Add CloudFormation outputs"""
|
|
155
|
+
cdk.CfnOutput(
|
|
156
|
+
self,
|
|
157
|
+
"CertificateArn",
|
|
158
|
+
value=self.certificate.certificate_arn,
|
|
159
|
+
description=f"Certificate ARN for {self.acm_config.domain_name}",
|
|
160
|
+
export_name=f"{cert_name}-arn",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
cdk.CfnOutput(
|
|
164
|
+
self,
|
|
165
|
+
"DomainName",
|
|
166
|
+
value=self.acm_config.domain_name,
|
|
167
|
+
description="Primary domain name for the certificate",
|
|
168
|
+
export_name=f"{cert_name}-domain",
|
|
169
|
+
)
|