cdk-factory 0.16.15__py3-none-any.whl → 0.20.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cdk-factory might be problematic. Click here for more details.

Files changed (66) hide show
  1. cdk_factory/configurations/base_config.py +23 -24
  2. cdk_factory/configurations/cdk_config.py +1 -1
  3. cdk_factory/configurations/deployment.py +12 -0
  4. cdk_factory/configurations/devops.py +1 -1
  5. cdk_factory/configurations/resources/acm.py +9 -2
  6. cdk_factory/configurations/resources/auto_scaling.py +7 -5
  7. cdk_factory/configurations/resources/cloudfront.py +7 -2
  8. cdk_factory/configurations/resources/ecr.py +1 -1
  9. cdk_factory/configurations/resources/ecs_cluster.py +12 -5
  10. cdk_factory/configurations/resources/ecs_service.py +30 -3
  11. cdk_factory/configurations/resources/lambda_edge.py +18 -4
  12. cdk_factory/configurations/resources/load_balancer.py +8 -9
  13. cdk_factory/configurations/resources/monitoring.py +8 -3
  14. cdk_factory/configurations/resources/rds.py +8 -9
  15. cdk_factory/configurations/resources/route53.py +5 -0
  16. cdk_factory/configurations/resources/rum.py +7 -2
  17. cdk_factory/configurations/resources/s3.py +10 -2
  18. cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
  19. cdk_factory/configurations/resources/vpc.py +19 -0
  20. cdk_factory/configurations/workload.py +32 -2
  21. cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +1 -1
  22. cdk_factory/constructs/ecr/ecr_construct.py +9 -2
  23. cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
  24. cdk_factory/interfaces/istack.py +4 -4
  25. cdk_factory/interfaces/networked_stack_mixin.py +6 -6
  26. cdk_factory/interfaces/standardized_ssm_mixin.py +684 -0
  27. cdk_factory/interfaces/vpc_provider_mixin.py +64 -33
  28. cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
  29. cdk_factory/pipeline/pipeline_factory.py +3 -3
  30. cdk_factory/stack_library/__init__.py +3 -2
  31. cdk_factory/stack_library/acm/acm_stack.py +7 -17
  32. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
  33. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +454 -537
  34. cdk_factory/stack_library/cloudfront/cloudfront_stack.py +76 -22
  35. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +5 -27
  36. cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
  37. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
  38. cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
  39. cdk_factory/stack_library/ecs/__init__.py +1 -3
  40. cdk_factory/stack_library/ecs/ecs_cluster_stack.py +159 -75
  41. cdk_factory/stack_library/ecs/ecs_service_stack.py +59 -52
  42. cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +226 -0
  43. cdk_factory/stack_library/lambda_edge/LAMBDA_EDGE_LOG_RETENTION_BLOG.md +215 -0
  44. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +240 -83
  45. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +139 -212
  46. cdk_factory/stack_library/rds/rds_stack.py +74 -98
  47. cdk_factory/stack_library/route53/route53_stack.py +246 -40
  48. cdk_factory/stack_library/rum/rum_stack.py +108 -91
  49. cdk_factory/stack_library/security_group/security_group_full_stack.py +10 -53
  50. cdk_factory/stack_library/security_group/security_group_stack.py +12 -19
  51. cdk_factory/stack_library/simple_queue_service/sqs_stack.py +1 -34
  52. cdk_factory/stack_library/stack_base.py +5 -0
  53. cdk_factory/stack_library/vpc/vpc_stack.py +171 -130
  54. cdk_factory/stack_library/websites/static_website_stack.py +7 -3
  55. cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
  56. cdk_factory/utilities/environment_services.py +5 -5
  57. cdk_factory/utilities/json_loading_utility.py +1 -1
  58. cdk_factory/validation/config_validator.py +483 -0
  59. cdk_factory/version.py +1 -1
  60. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/METADATA +1 -1
  61. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/RECORD +64 -62
  62. cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
  63. cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
  64. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/WHEEL +0 -0
  65. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/entry_points.txt +0 -0
  66. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,14 +17,19 @@ class BaseConfig:
17
17
  SSM parameter paths can be customized with prefixes and templates at different levels:
18
18
  1. Global level: In the workload or deployment config
19
19
  2. Stack level: In the stack config
20
- 3. Resource level: In the resource config (ssm_exports/ssm_imports)
20
+ 3. Resource level: In the resource config (ssm.exports/ssm.imports)
21
21
 
22
22
  Example configurations:
23
23
  ```json
24
24
  {
25
- "ssm_prefix_template": "/{environment}/{resource_type}/{attribute}",
26
- "ssm_exports": {
27
- "vpc_id_path": "my-vpc-id"
25
+ "ssm": {
26
+ "prefix_template": "/{environment}/{resource_type}/{attribute}",
27
+ "exports": {
28
+ "vpc_id": "my-vpc-id"
29
+ },
30
+ "imports": {
31
+ "security_group_id": "/my-app/security-group/id"
32
+ }
28
33
  }
29
34
  }
30
35
  ```
@@ -57,6 +62,16 @@ class BaseConfig:
57
62
  """
58
63
  return self.__config
59
64
 
65
+ @property
66
+ def ssm(self) -> Dict[str, Any]:
67
+ """
68
+ Get the SSM configuration for this resource.
69
+
70
+ Returns:
71
+ Dictionary containing SSM configuration with imports/exports
72
+ """
73
+ return self.__config.get("ssm", {})
74
+
60
75
  @property
61
76
  def ssm_prefix_template(self) -> str:
62
77
  """
@@ -68,7 +83,7 @@ class BaseConfig:
68
83
  Returns:
69
84
  The SSM parameter prefix template string
70
85
  """
71
- return self.__config.get("ssm_prefix_template", "/{deployment_name}/{resource_type}/{attribute}")
86
+ return self.ssm.get("prefix_template", "/{deployment_name}/{resource_type}/{attribute}")
72
87
 
73
88
  @property
74
89
  def ssm_exports(self) -> Dict[str, str]:
@@ -87,7 +102,7 @@ class BaseConfig:
87
102
  Returns:
88
103
  Dictionary mapping attribute names to SSM parameter paths for export
89
104
  """
90
- return self.__config.get("ssm_exports", {})
105
+ return self.ssm.get("exports", {})
91
106
 
92
107
  @property
93
108
  def ssm_imports(self) -> Dict[str, str]:
@@ -106,25 +121,9 @@ class BaseConfig:
106
121
  Returns:
107
122
  Dictionary mapping attribute names to SSM parameter paths for import
108
123
  """
109
- return self.__config.get("ssm_imports", {})
110
-
111
- @property
112
- def ssm_parameters(self) -> Dict[str, str]:
113
- """
114
- Get all SSM parameter path mappings (both exports and imports).
115
-
116
- This is provided for backward compatibility.
117
- New code should use ssm_exports and ssm_imports instead.
118
-
119
- Returns:
120
- Dictionary mapping attribute names to SSM parameter paths
121
- """
122
- # Merge exports and imports, with exports taking precedence
123
- combined = {**self.ssm_imports, **self.ssm_exports}
124
- # Also include any parameters directly under ssm_parameters for backward compatibility
125
- combined.update(self.__config.get("ssm_parameters", {}))
126
- return combined
124
+ return self.ssm.get("imports", {})
127
125
 
126
+
128
127
  def get(self, key: str, default: Any = None) -> Any:
129
128
  """
130
129
  Get a configuration value by key.
@@ -189,7 +189,7 @@ class CdkConfig:
189
189
  if not os.path.exists(Path(path).parent):
190
190
  os.makedirs(Path(path).parent)
191
191
  cdk = config.get("cdk", {})
192
- if replacements and len(replacements) > 0:
192
+ if replacements:
193
193
  config = JsonLoadingUtility.recursive_replace(config, replacements)
194
194
  print(f"📀 Saving config to {path}")
195
195
  # add the original cdk back
@@ -28,6 +28,18 @@ class DeploymentConfig:
28
28
  self.__load()
29
29
 
30
30
  def __load(self):
31
+ # Validate environment consistency
32
+ deployment_env = self.__deployment.get("environment")
33
+ workload_env = self.__workload.get("environment")
34
+
35
+ if deployment_env and workload_env and deployment_env != workload_env:
36
+ from aws_lambda_powertools import Logger
37
+ logger = Logger()
38
+ logger.warning(
39
+ f"Environment mismatch: deployment.environment='{deployment_env}' != workload.environment='{workload_env}'. "
40
+ f"Using workload.environment for consistency."
41
+ )
42
+
31
43
  self.__load_pipeline()
32
44
  self.__load_stacks()
33
45
 
@@ -64,7 +64,7 @@ class DevOps:
64
64
  )
65
65
  if (
66
66
  not self.__code_repository.repository
67
- or len(self.__code_repository.repository) == 0
67
+ or not self.__code_repository.repository
68
68
  ):
69
69
  raise ValueError(
70
70
  "Code Repository is not defined in the configuration "
@@ -59,15 +59,22 @@ class AcmConfig:
59
59
  """Certificate transparency logging preference (ENABLED or DISABLED)"""
60
60
  return self.__config.get("certificate_transparency_logging_preference")
61
61
 
62
+ @property
63
+ def ssm(self) -> Dict[str, Any]:
64
+ """SSM configuration for importing/exporting resources"""
65
+ return self.__config.get("ssm", {})
66
+
62
67
  @property
63
68
  def ssm_exports(self) -> Dict[str, str]:
64
69
  """SSM parameter paths to export certificate details"""
65
- exports = self.__config.get("ssm_exports", {})
70
+ exports = self.ssm.get("exports", {})
66
71
 
67
72
  # Provide default SSM export path if not specified
68
73
  if not exports and self.__deployment:
74
+ workload_env = self.__deployment.workload.get("environment", self.__deployment.environment)
75
+ workload_name = self.__deployment.workload.get("name", self.__deployment.workload_name)
69
76
  exports = {
70
- "certificate_arn": f"/{self.__deployment.environment}/{self.__deployment.workload_name}/certificate/arn"
77
+ "certificate_arn": f"/{workload_env}/{workload_name}/certificate/arn"
71
78
  }
72
79
 
73
80
  return exports
@@ -70,12 +70,14 @@ class AutoScalingConfig(EnhancedBaseConfig):
70
70
  return self.__config.get("termination_policies", ["DEFAULT"])
71
71
 
72
72
  @property
73
- def update_policy(self) -> Dict[str, Any]:
73
+ def update_policy(self) -> Optional[Dict[str, Any]]:
74
74
  """Update policy configuration"""
75
- return self.__config.get(
76
- "update_policy",
77
- {"min_instances_in_service": 1, "max_batch_size": 1, "pause_time": 300},
78
- )
75
+ return self.__config.get("update_policy")
76
+
77
+ @property
78
+ def instance_refresh(self) -> Optional[Dict[str, Any]]:
79
+ """Instance refresh configuration for rolling updates"""
80
+ return self.__config.get("instance_refresh")
79
81
 
80
82
  @property
81
83
  def user_data_commands(self) -> List[str]:
@@ -102,15 +102,20 @@ class CloudFrontConfig(EnhancedBaseConfig):
102
102
  """Resource tags"""
103
103
  return self._config.get("tags", {})
104
104
 
105
+ @property
106
+ def ssm(self) -> Dict[str, Any]:
107
+ """SSM configuration"""
108
+ return self._config.get("ssm", {})
109
+
105
110
  @property
106
111
  def ssm_exports(self) -> Dict[str, str]:
107
112
  """SSM parameter exports"""
108
- return self._config.get("ssm_exports", {})
113
+ return self.ssm.get("exports", {})
109
114
 
110
115
  @property
111
116
  def ssm_imports(self) -> Dict[str, str]:
112
117
  """SSM parameter imports"""
113
- return self._config.get("ssm_imports", {})
118
+ return self.ssm.get("imports", {})
114
119
 
115
120
  @property
116
121
  def hosted_zone_id(self) -> str:
@@ -128,7 +128,7 @@ class ECRConfig(EnhancedBaseConfig):
128
128
  Example:
129
129
  {
130
130
  "enabled": true,
131
- "accounts": ["123456789012", "987654321098"],
131
+ "accounts": [os.environ.get("ECR_ALLOWED_ACCOUNT_1"), os.environ.get("ECR_ALLOWED_ACCOUNT_2")],
132
132
  "services": [
133
133
  {
134
134
  "name": "lambda",
@@ -18,6 +18,11 @@ class EcsClusterConfig:
18
18
  def __init__(self, config: Dict[str, Any]) -> None:
19
19
  self._config = config or {}
20
20
 
21
+ @property
22
+ def dictionary(self) -> Dict[str, Any]:
23
+ """Access to the underlying configuration dictionary (for compatibility with SSM mixin)"""
24
+ return self._config
25
+
21
26
  @property
22
27
  def name(self) -> str:
23
28
  """Name of the ECS cluster. Supports template variables like {{WORKLOAD_NAME}}-{{ENVIRONMENT}}-cluster"""
@@ -87,15 +92,17 @@ class EcsClusterConfig:
87
92
  return self._config.get("export_ssm_parameters", True)
88
93
 
89
94
 
95
+ @property
96
+ def ssm(self) -> Dict[str, Any]:
97
+ """SSM configuration"""
98
+ return self._config.get("ssm", {})
99
+
90
100
  @property
91
101
  def ssm_exports(self) -> Dict[str, str]:
92
102
  """SSM parameter exports"""
93
- return self._config.get("ssm_exports", {})
103
+ return self.ssm.get("exports", {})
94
104
 
95
105
  @property
96
106
  def ssm_imports(self) -> Dict[str, Any]:
97
107
  """SSM parameter imports"""
98
- # Check both nested and flat structures for backwards compatibility
99
- if "ssm" in self._config and "imports" in self._config["ssm"]:
100
- return self._config["ssm"]["imports"]
101
- return self._config.get("ssm_imports", {})
108
+ return self.ssm.get("imports", {})
@@ -83,14 +83,27 @@ class EcsServiceConfig:
83
83
  """Whether to assign public IP addresses"""
84
84
  return self._config.get("assign_public_ip", False)
85
85
 
86
+ @property
87
+ def load_balancer_config(self) -> Dict[str, Any]:
88
+ """Load balancer configuration"""
89
+ return self._config.get("load_balancer", {})
90
+
86
91
  @property
87
92
  def target_group_arns(self) -> List[str]:
88
93
  """Target group ARNs for load balancing"""
94
+ # Check if load_balancer config has target_group_arn
95
+ if self.load_balancer_config and self.load_balancer_config.get("target_group_arn"):
96
+ arn = self.load_balancer_config["target_group_arn"]
97
+ if arn and arn != "arn:aws:elasticloadbalancing:placeholder":
98
+ return [arn]
89
99
  return self._config.get("target_group_arns", [])
90
100
 
91
101
  @property
92
102
  def container_port(self) -> int:
93
103
  """Container port for load balancer"""
104
+ # Check load_balancer config first
105
+ if self.load_balancer_config and self.load_balancer_config.get("container_port"):
106
+ return self.load_balancer_config["container_port"]
94
107
  return self._config.get("container_port", 80)
95
108
 
96
109
  @property
@@ -123,18 +136,32 @@ class EcsServiceConfig:
123
136
  """Resource tags"""
124
137
  return self._config.get("tags", {})
125
138
 
139
+ @property
140
+ def ssm(self) -> Dict[str, Any]:
141
+ """SSM configuration"""
142
+ return self._config.get("ssm", {})
143
+
126
144
  @property
127
145
  def ssm_exports(self) -> Dict[str, str]:
128
146
  """SSM parameter exports"""
129
- return self._config.get("ssm_exports", {})
147
+ return self.ssm.get("exports", {})
130
148
 
131
149
  @property
132
150
  def ssm_imports(self) -> Dict[str, Any]:
133
151
  """SSM parameter imports"""
134
152
  # Check both nested and flat structures for backwards compatibility
135
153
  if "ssm" in self._config and "imports" in self._config["ssm"]:
136
- return self._config["ssm"]["imports"]
137
- return self._config.get("ssm_imports", {})
154
+ imports = self._config["ssm"]["imports"]
155
+ else:
156
+ imports = self.ssm.get("imports", {})
157
+
158
+ # Add load_balancer SSM imports if they exist
159
+ if self.load_balancer_config and "ssm" in self.load_balancer_config:
160
+ lb_ssm = self.load_balancer_config["ssm"]
161
+ if "imports" in lb_ssm:
162
+ imports.update(lb_ssm["imports"])
163
+
164
+ return imports
138
165
 
139
166
  @property
140
167
  def deployment_type(self) -> str:
@@ -49,16 +49,30 @@ class LambdaEdgeConfig(EnhancedBaseConfig):
49
49
 
50
50
  @property
51
51
  def timeout(self) -> int:
52
- """Timeout in seconds (max 5 for origin-request)"""
52
+ """Timeout in seconds
53
+ viewer-request: 5s
54
+ viewer-response: 5s
55
+ ---
56
+ origin-request: 30s
57
+ origin-response: 30s
58
+
59
+
60
+ """
53
61
  timeout = int(self._config.get("timeout", 5))
54
- if timeout > 5:
55
- raise ValueError("Lambda@Edge origin-request timeout cannot exceed 5 seconds")
62
+
63
+ event_type = self.event_type
64
+ if event_type == "viewer-request" or event_type == "viewer-response":
65
+ if timeout > 5:
66
+ raise ValueError("Lambda@Edge viewer timeout cannot exceed 5 seconds. Value was set to {}".format(timeout))
67
+ else:
68
+ if timeout > 30:
69
+ raise ValueError("Lambda@Edge origin timeout cannot exceed 30 seconds. Value was set to {}".format(timeout))
56
70
  return timeout
57
71
 
58
72
  @property
59
73
  def code_path(self) -> str:
60
74
  """Path to Lambda function code directory"""
61
- return self._config.get("code_path", "./lambdas/edge/ip_gate")
75
+ return self._config.get("code_path", "./lambdas/cloudfront/ip_gate")
62
76
 
63
77
  @property
64
78
  def environment(self) -> Dict[str, str]:
@@ -144,21 +144,20 @@ class LoadBalancerConfig(EnhancedBaseConfig):
144
144
  "block_response", default_response
145
145
  )
146
146
 
147
+ @property
148
+ def ssm(self) -> Dict[str, Any]:
149
+ """SSM configuration"""
150
+ return self.__config.get("ssm", {})
151
+
147
152
  @property
148
153
  def ssm_imports(self) -> Dict[str, Any]:
149
154
  """SSM parameter imports for the Load Balancer"""
150
- # Check both nested and flat structures for backwards compatibility
151
- if "ssm" in self.__config and "imports" in self.__config["ssm"]:
152
- return self.__config["ssm"]["imports"]
153
- return self.__config.get("ssm_imports", {})
154
-
155
+ return self.ssm.get("imports", {})
156
+
155
157
  @property
156
158
  def ssm_exports(self) -> Dict[str, Any]:
157
159
  """SSM parameter exports for the Load Balancer"""
158
- # Check both nested and flat structures for backwards compatibility
159
- if "ssm" in self.__config and "exports" in self.__config["ssm"]:
160
- return self.__config["ssm"]["exports"]
161
- return self.__config.get("ssm_exports", {})
160
+ return self.ssm.get("exports", {})
162
161
 
163
162
  @property
164
163
  def ssm_parameters(self) -> Dict[str, Any]:
@@ -18,7 +18,7 @@ class MonitoringConfig(EnhancedBaseConfig):
18
18
  super().__init__(
19
19
  config or {},
20
20
  resource_type="monitoring",
21
- resource_name=config.get("name", "monitoring") if config else "monitoring"
21
+ resource_name=config.get("name", "monitoring") if config else "monitoring",
22
22
  )
23
23
  self._config = config or {}
24
24
  self._deployment = deployment
@@ -63,12 +63,17 @@ class MonitoringConfig(EnhancedBaseConfig):
63
63
  """Resource tags"""
64
64
  return self._config.get("tags", {})
65
65
 
66
+ @property
67
+ def ssm(self) -> Dict[str, Any]:
68
+ """SSM configuration"""
69
+ return self._config.get("ssm", {})
70
+
66
71
  @property
67
72
  def ssm_exports(self) -> Dict[str, str]:
68
73
  """SSM parameter exports"""
69
- return self._config.get("ssm_exports", {})
74
+ return self.ssm.get("exports", {})
70
75
 
71
76
  @property
72
77
  def ssm_imports(self) -> Dict[str, str]:
73
78
  """SSM parameter imports for resource ARNs"""
74
- return self._config.get("ssm_imports", {})
79
+ return self.ssm.get("imports", {})
@@ -29,7 +29,7 @@ class RdsConfig(EnhancedBaseConfig):
29
29
  @property
30
30
  def name(self) -> str:
31
31
  """RDS instance name"""
32
- return self.__config.get("name", "database")
32
+ return self.__config.get("name", self.database_name)
33
33
 
34
34
  @property
35
35
  def identifier(self) -> str:
@@ -261,21 +261,20 @@ class RdsConfig(EnhancedBaseConfig):
261
261
  """Sets the VPC ID for the Security Group"""
262
262
  self.__config["vpc_id"] = value
263
263
 
264
+ @property
265
+ def ssm(self) -> Dict[str, Any]:
266
+ """SSM configuration"""
267
+ return self.__config.get("ssm", {})
268
+
264
269
  @property
265
270
  def ssm_imports(self) -> Dict[str, str]:
266
271
  """SSM parameter imports for the RDS instance"""
267
- # Check both nested and flat structures for backwards compatibility
268
- if "ssm" in self.__config and "imports" in self.__config["ssm"]:
269
- return self.__config["ssm"]["imports"]
270
- return self.__config.get("ssm_imports", {})
272
+ return self.ssm.get("imports", {})
271
273
 
272
274
  @property
273
275
  def ssm_exports(self) -> Dict[str, str]:
274
276
  """SSM parameter exports for the RDS instance"""
275
- # Check both nested and flat structures for backwards compatibility
276
- if "ssm" in self.__config and "exports" in self.__config["ssm"]:
277
- return self.__config["ssm"]["exports"]
278
- return self.__config.get("ssm_exports", {})
277
+ return self.ssm.get("exports", {})
279
278
 
280
279
  def _sanitize_database_name(self, name: str) -> str:
281
280
  """
@@ -106,3 +106,8 @@ class Route53Config:
106
106
  def tags(self) -> Dict[str, str]:
107
107
  """Tags to apply to the Route53 resources"""
108
108
  return self.__config.get("tags", {})
109
+
110
+ @property
111
+ def records(self) -> List[Dict[str, Any]]:
112
+ """Records to create"""
113
+ return self.__config.get("records", [])
@@ -125,11 +125,16 @@ class RumConfig(EnhancedBaseConfig):
125
125
 
126
126
  # SSM Integration
127
127
  @property
128
+ def ssm(self) -> Dict[str, Any]:
129
+ """SSM configuration for importing/exporting resources"""
130
+ return self.__config.get("ssm", {})
131
+
132
+ @property
128
133
  def ssm_exports(self) -> Dict[str, str]:
129
134
  """SSM parameter paths for exporting RUM resources"""
130
- return self.__config.get("ssm_exports", {})
135
+ return self.ssm.get("exports", {})
131
136
 
132
137
  @property
133
138
  def ssm_imports(self) -> Dict[str, str]:
134
139
  """SSM parameter paths for importing external resources"""
135
- return self.__config.get("ssm_imports", {})
140
+ return self.ssm.get("imports", {})
@@ -30,7 +30,7 @@ class S3BucketConfig(EnhancedBaseConfig):
30
30
  "S3 Bucket Configuration must be a dictionary. Found: "
31
31
  f"{type(self.__config)}"
32
32
  )
33
- if len(self.__config.keys()) == 0:
33
+ if not self.__config.keys():
34
34
  raise ValueError("S3 Bucket Configuration cannot be empty")
35
35
 
36
36
  @property
@@ -158,7 +158,15 @@ class S3BucketConfig(EnhancedBaseConfig):
158
158
  value = self.__config.get("block_public_access")
159
159
 
160
160
  if value and isinstance(value, str):
161
- if value.lower() == "block_acls":
161
+ if value.lower() == "disabled":
162
+ # For public website hosting, disable block public access
163
+ return s3.BlockPublicAccess(
164
+ block_public_acls=False,
165
+ block_public_policy=False,
166
+ ignore_public_acls=False,
167
+ restrict_public_buckets=False
168
+ )
169
+ elif value.lower() == "block_acls":
162
170
  return s3.BlockPublicAccess.BLOCK_ACLS
163
171
  # elif value.lower() == "block_public_acls":
164
172
  # return s3.BlockPublicAccess.block_public_acls
@@ -62,21 +62,20 @@ class SecurityGroupFullStackConfig:
62
62
  """Tags to apply to the Security Group"""
63
63
  return self.__config.get("tags", {})
64
64
 
65
+ @property
66
+ def ssm(self) -> Dict[str, Any]:
67
+ """SSM configuration"""
68
+ return self.__config.get("ssm", {})
69
+
65
70
  @property
66
71
  def ssm_imports(self) -> Dict[str, str]:
67
72
  """SSM parameter imports for the Security Group"""
68
- # Check both nested and flat structures for backwards compatibility
69
- if "ssm" in self.__config and "imports" in self.__config["ssm"]:
70
- return self.__config["ssm"]["imports"]
71
- return self.__config.get("ssm_imports", {})
73
+ return self.ssm.get("imports", {})
72
74
 
73
75
  @property
74
76
  def ssm_exports(self) -> Dict[str, str]:
75
77
  """SSM parameter exports for the Security Group"""
76
- # Check both nested and flat structures for backwards compatibility
77
- if "ssm" in self.__config and "exports" in self.__config["ssm"]:
78
- return self.__config["ssm"]["exports"]
79
- return self.__config.get("ssm_exports", {})
78
+ return self.ssm.get("exports", {})
80
79
 
81
80
  @property
82
81
  def security_groups(self) -> List[Dict[str, Any]]:
@@ -122,3 +122,22 @@ class VpcConfig(EnhancedBaseConfig):
122
122
  def isolated_subnet_name(self) -> str:
123
123
  """Custom name for isolated subnets"""
124
124
  return self.get("isolated_subnet_name", "isolated")
125
+
126
+ @property
127
+ def subnets(self) -> Dict[str, Any]:
128
+ """Subnet configuration for the VPC"""
129
+ return self.get("subnets", {
130
+ "public": {
131
+ "enabled": self.public_subnets,
132
+ "cidr_mask": self.public_subnet_mask,
133
+ "map_public_ip": True
134
+ },
135
+ "private": {
136
+ "enabled": self.private_subnets,
137
+ "cidr_mask": self.private_subnet_mask
138
+ },
139
+ "isolated": {
140
+ "enabled": self.isolated_subnets,
141
+ "cidr_mask": self.isolated_subnet_mask
142
+ }
143
+ })
@@ -5,6 +5,7 @@ MIT License. See Project Root for the license information.
5
5
  """
6
6
 
7
7
  import os
8
+ import copy
8
9
  from typing import Any, Dict, List
9
10
 
10
11
  from aws_lambda_powertools import Logger
@@ -66,13 +67,42 @@ class WorkloadConfig:
66
67
  if self.__app_config is None:
67
68
  raise ValueError("Configuration is not defined.")
68
69
 
70
+ # Create a deep copy to avoid mutating the original configuration
69
71
  if "workload" in self.__app_config:
70
- workload = self.__app_config["workload"]
72
+ workload = copy.deepcopy(self.__app_config["workload"])
71
73
  else:
72
- workload = self.__app_config
74
+ workload = copy.deepcopy(self.__app_config)
73
75
 
74
76
  self.__workload = workload
75
77
 
78
+ # Handle missing devops section gracefully
79
+ if "devops" not in workload:
80
+ logger.warning("Devops configuration not found in workload, using defaults")
81
+
82
+ # Get environment variables for defaults
83
+ devops_account = os.environ.get("DEVOPS_AWS_ACCOUNT")
84
+ devops_region = os.environ.get("DEVOPS_REGION")
85
+
86
+ # Validate required environment variables
87
+ if not devops_account or not devops_region:
88
+ raise ValueError(
89
+ "DEVOPS_AWS_ACCOUNT and DEVOPS_REGION environment variables must be set when devops config is missing"
90
+ )
91
+
92
+ # Use a separate defaults object instead of mutating the original
93
+ devops_defaults = {
94
+ "account": devops_account,
95
+ "region": devops_region,
96
+ "code_repository": {
97
+ "name": os.environ.get("CODE_REPOSITORY_NAME", "default-repo"),
98
+ "type": "connector_arn",
99
+ "connector_arn": os.environ.get("CODE_REPOSITORY_ARN", f"arn:aws:codeconnections:{os.environ.get('DEVOPS_REGION', 'us-east-1')}:{os.environ.get('DEVOPS_AWS_ACCOUNT')}:connection/default")
100
+ },
101
+ "commands": []
102
+ }
103
+
104
+ workload["devops"] = devops_defaults
105
+
76
106
  self.__devops = DevOps(workload["devops"])
77
107
  self.__management = Management(workload.get("management", {}))
78
108
  self.__cloudfront = CloudFrontConfig(workload.get("cloudfront", {}))
@@ -558,7 +558,7 @@ class CloudFrontDistributionConstruct(Construct):
558
558
  """
559
559
  bucket_policy = s3.BucketPolicy(
560
560
  self,
561
- id=f"CloudFrontBucketPolicy-{self.source_bucket.bucket_name}",
561
+ id="CloudFrontBucketPolicy",
562
562
  bucket=self.source_bucket,
563
563
  )
564
564
 
@@ -14,12 +14,12 @@ from constructs import Construct, IConstruct
14
14
  from cdk_factory.configurations.resources.resource_types import ResourceTypes
15
15
  from cdk_factory.configurations.resources.ecr import ECRConfig as ECR
16
16
  from cdk_factory.configurations.deployment import DeploymentConfig as Deployment
17
- from cdk_factory.interfaces.ssm_parameter_mixin import SsmParameterMixin
17
+ from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
18
18
 
19
19
  logger = Logger(__name__)
20
20
 
21
21
 
22
- class ECRConstruct(Construct, SsmParameterMixin):
22
+ class ECRConstruct(Construct, StandardizedSsmMixin):
23
23
  def __init__(
24
24
  self,
25
25
  scope: Construct,
@@ -30,6 +30,8 @@ class ECRConstruct(Construct, SsmParameterMixin):
30
30
  **kwargs,
31
31
  ) -> None:
32
32
  super().__init__(scope, id, **kwargs)
33
+ # Initialize StandardizedSsmMixin explicitly
34
+ StandardizedSsmMixin.__init__(self, **kwargs)
33
35
 
34
36
  self.scope = scope
35
37
  self.deployment = deployment
@@ -77,6 +79,11 @@ class ECRConstruct(Construct, SsmParameterMixin):
77
79
 
78
80
  This method uses the new configurable SSM parameter prefix system.
79
81
  """
82
+ # Check if SSM exports are configured
83
+ if not hasattr(self.repo, 'ssm_exports') or not self.repo.ssm_exports:
84
+ logger.debug("No SSM exports configured for ECR repository")
85
+ return
86
+
80
87
  # Create a dictionary of resource values to export
81
88
  resource_values = {
82
89
  "name": self.ecr.repository_name,