cdk-factory 0.8.7__py3-none-any.whl → 0.9.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.
@@ -33,6 +33,7 @@ class ECRConstruct(Construct, SsmParameterMixin):
33
33
 
34
34
  self.scope = scope
35
35
  self.deployment = deployment
36
+ self.repo = repo
36
37
  self.ecr_name = repo.name
37
38
  self.image_scan_on_push = repo.image_scan_on_push
38
39
  self.empty_on_delete = repo.empty_on_delete
@@ -98,28 +99,17 @@ class ECRConstruct(Construct, SsmParameterMixin):
98
99
  )
99
100
 
100
101
  # Add dependencies to ensure SSM parameters are created after the ECR repository
101
- for param in params.values():
102
- if param and param.node.default_child and isinstance(param.node.default_child, CfnResource):
103
- param.node.default_child.add_dependency(
104
- cast(CfnResource, self.ecr.node.default_child)
105
- )
102
+ if params:
103
+ for param in params.values():
104
+ if param and hasattr(param, 'node') and param.node.default_child and isinstance(param.node.default_child, CfnResource):
105
+ param.node.default_child.add_dependency(
106
+ cast(CfnResource, self.ecr.node.default_child)
107
+ )
106
108
 
107
109
  def __set_life_cycle_rules(self) -> None:
108
- # ToDo/FixMe: tag_pattern_list is not recognized in the current version in AWS
109
-
110
- try:
111
- # always keep images tagged as prod
112
- self.ecr.add_lifecycle_rule(
113
- tag_pattern_list=["prod*"], max_image_count=9999
114
- )
115
- except Exception as e: # pylint: disable=w0718
116
- if "unexpected keyword argument" in str(e):
117
- logger.warning(
118
- "tag_pattern_list is not available in this version of the aws cdk"
119
- )
120
- else:
121
- raise
122
-
110
+ # Note: tag_pattern_list is deprecated and causes circular dependencies in CDK synthesis
111
+ # Only add lifecycle rule for untagged images if configured
112
+
123
113
  if not self.auto_delete_untagged_images_in_days:
124
114
  return None
125
115
 
@@ -143,49 +133,131 @@ class ECRConstruct(Construct, SsmParameterMixin):
143
133
  )
144
134
 
145
135
  def __setup_cross_account_access_permissions(self):
146
- # Cross-account access policy
136
+ """
137
+ Setup cross-account access permissions with flexible configuration support.
138
+
139
+ Supports both legacy (default Lambda access) and new configurable approach.
140
+ """
141
+ # Check if cross-account access is disabled
142
+ if not self.repo.cross_account_enabled:
143
+ logger.info(f"Cross-account access disabled for {self.ecr_name}")
144
+ return
147
145
 
148
- if self.deployment.account == self.deployment.workload.get("devops", {}).get(
149
- "account"
150
- ):
151
- # we're in the same account as the "devops" so we don't need cross account
152
- # permisions
146
+ # Check if we're in the same account as devops
147
+ if self.deployment.account == self.deployment.workload.get("devops", {}).get("account"):
148
+ logger.info(f"Same account as devops, skipping cross-account permissions for {self.ecr_name}")
153
149
  return
154
150
 
155
- ecr = self.ecr or self.__get_ecr()
156
- cross_account_policy_statement = iam.PolicyStatement(
151
+ access_config = self.repo.cross_account_access
152
+
153
+ if access_config and access_config.get("services"):
154
+ # New configurable approach
155
+ logger.info(f"Setting up configurable cross-account access for {self.ecr_name}")
156
+ self.__setup_configurable_access(access_config)
157
+ else:
158
+ # Legacy approach - default Lambda access for backward compatibility
159
+ logger.info(f"Setting up legacy cross-account access (Lambda only) for {self.ecr_name}")
160
+ self.__setup_legacy_lambda_access()
161
+
162
+ def __setup_configurable_access(self, access_config: dict):
163
+ """Setup cross-account access using configuration"""
164
+
165
+ # Get list of accounts (default to deployment account)
166
+ accounts = access_config.get("accounts", [self.deployment.account])
167
+
168
+ # Add account principal policies if accounts are specified
169
+ if accounts:
170
+ self.__add_account_principal_policy(accounts)
171
+
172
+ # Add service-specific policies
173
+ services = access_config.get("services", [])
174
+ for service_config in services:
175
+ self.__add_service_principal_policy(service_config)
176
+
177
+ def __add_account_principal_policy(self, accounts: list):
178
+ """Add policy for AWS account principals"""
179
+ principals = [iam.AccountPrincipal(account) for account in accounts]
180
+
181
+ policy_statement = iam.PolicyStatement(
157
182
  actions=[
158
183
  "ecr:GetDownloadUrlForLayer",
159
184
  "ecr:BatchGetImage",
160
185
  "ecr:BatchCheckLayerAvailability",
161
186
  ],
162
- principals=[
163
- iam.AccountPrincipal(self.deployment.account)
164
- ], # Replace with the account ID of the Lambda function
165
- resources=[ecr.repository_arn],
187
+ principals=principals,
166
188
  effect=iam.Effect.ALLOW,
167
189
  )
168
190
 
169
- # Attach the policy to the ECR repository
170
- response = ecr.add_to_resource_policy(cross_account_policy_statement)
171
-
172
- # fails, we're not adding it this way
173
- assert response.statement_added
174
-
175
- response = self.ecr.add_to_resource_policy(
176
- iam.PolicyStatement(
177
- effect=iam.Effect.ALLOW,
178
- actions=["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"],
179
- principals=[iam.ServicePrincipal("lambda.amazonaws.com")],
180
- conditions={
181
- "StringLike": {
182
- "aws:sourceArn": [
183
- f"arn:aws:lambda:{self.deployment.region}:{self.deployment.account}:function:*"
184
- ]
185
- }
186
- },
187
- resources=[ecr.repository_arn],
188
- )
191
+ response = self.ecr.add_to_resource_policy(policy_statement)
192
+ if not response.statement_added:
193
+ logger.warning(f"Failed to add account principal policy for {', '.join(accounts)}")
194
+ else:
195
+ logger.info(f"Added account principal policy for accounts: {', '.join(accounts)}")
196
+
197
+ def __add_service_principal_policy(self, service_config: dict):
198
+ """Add policy for service principal (Lambda, ECS, CodeBuild, etc.)"""
199
+ service_name = service_config.get("name", "unknown")
200
+ service_principal = service_config.get("service_principal")
201
+ actions = service_config.get("actions", ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"])
202
+ conditions = service_config.get("condition")
203
+
204
+ if not service_principal:
205
+ # Infer service principal from common service names
206
+ service_principal_map = {
207
+ "lambda": "lambda.amazonaws.com",
208
+ "ecs": "ecs-tasks.amazonaws.com",
209
+ "ecs-tasks": "ecs-tasks.amazonaws.com",
210
+ "codebuild": "codebuild.amazonaws.com",
211
+ "codepipeline": "codepipeline.amazonaws.com",
212
+ "ec2": "ec2.amazonaws.com",
213
+ }
214
+ service_principal = service_principal_map.get(service_name.lower())
215
+
216
+ if not service_principal:
217
+ logger.warning(f"Unknown service principal for service: {service_name}")
218
+ return
219
+
220
+ policy_statement = iam.PolicyStatement(
221
+ effect=iam.Effect.ALLOW,
222
+ actions=actions,
223
+ principals=[iam.ServicePrincipal(service_principal)],
224
+ )
225
+
226
+ # Add conditions if specified
227
+ if conditions:
228
+ for condition_key, condition_value in conditions.items():
229
+ policy_statement.add_condition(condition_key, condition_value)
230
+
231
+ response = self.ecr.add_to_resource_policy(policy_statement)
232
+ if not response.statement_added:
233
+ logger.warning(f"Failed to add service principal policy for {service_name}")
234
+ else:
235
+ logger.info(f"Added service principal policy for {service_name} ({service_principal})")
236
+
237
+ def __setup_legacy_lambda_access(self):
238
+ """Legacy method: Setup default Lambda-only cross-account access"""
239
+
240
+ # Add account principal policy
241
+ self.__add_account_principal_policy([self.deployment.account])
242
+
243
+ # Add Lambda service principal policy with default condition
244
+ lambda_policy = iam.PolicyStatement(
245
+ effect=iam.Effect.ALLOW,
246
+ actions=["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"],
247
+ principals=[iam.ServicePrincipal("lambda.amazonaws.com")],
248
+ )
249
+
250
+ lambda_policy.add_condition(
251
+ "StringLike",
252
+ {
253
+ "aws:sourceArn": [
254
+ f"arn:aws:lambda:{self.deployment.region}:{self.deployment.account}:function:*"
255
+ ]
256
+ }
189
257
  )
190
258
 
191
- assert response.statement_added
259
+ response = self.ecr.add_to_resource_policy(lambda_policy)
260
+ if not response.statement_added:
261
+ logger.warning("Failed to add Lambda service principal policy")
262
+ else:
263
+ logger.info("Added legacy Lambda service principal policy")
@@ -3,6 +3,7 @@ Geek Cafe Pipeline
3
3
  """
4
4
 
5
5
  import os
6
+ from pathlib import Path
6
7
  from typing import List, Dict, Any
7
8
 
8
9
  import aws_cdk as cdk
@@ -246,7 +247,13 @@ class PipelineFactoryStack(cdk.Stack):
246
247
 
247
248
  def _get_steps(self, key: str, stage_config: PipelineStageConfig):
248
249
  """
249
- Gets the build steps from the config.json
250
+ Gets the build steps from the config.json.
251
+
252
+ Commands can be:
253
+ - A list of strings (each string is a separate command)
254
+ - A single multi-line string (treated as a single script block)
255
+
256
+ This allows support for complex shell constructs like if blocks, loops, etc.
250
257
  """
251
258
  shell_steps: List[pipelines.ShellStep] = []
252
259
 
@@ -257,6 +264,12 @@ class PipelineFactoryStack(cdk.Stack):
257
264
  for step in steps:
258
265
  step_id = step.get("id") or step.get("name")
259
266
  commands = step.get("commands", [])
267
+
268
+ # Normalize commands to a list
269
+ # If commands is a single string, wrap it in a list
270
+ if isinstance(commands, str):
271
+ commands = [commands]
272
+
260
273
  shell_step = pipelines.ShellStep(
261
274
  id=step_id,
262
275
  commands=commands,
@@ -347,9 +360,17 @@ class PipelineFactoryStack(cdk.Stack):
347
360
  build_commands = self._get_build_commands()
348
361
 
349
362
  cdk_out_directory = self.workload.output_directory
363
+
364
+ # CdkAppFactory already provides the correct path:
365
+ # - For pipelines: relative path (e.g., "../../cdk.out" or "cdk.out")
366
+ # - For local: absolute path (but this code only runs for pipelines)
367
+ # If somehow we get an absolute path, convert it to just the basename
368
+ if cdk_out_directory and os.path.isabs(cdk_out_directory):
369
+ # Fallback: just use the last component
370
+ cdk_out_directory = os.path.basename(cdk_out_directory)
350
371
 
351
372
  build_commands.append(f"echo 👉 cdk_directory: {cdk_directory}")
352
- build_commands.append(f"echo 👉 cdk_out_directory: {cdk_out_directory}")
373
+ build_commands.append(f"echo 👉 cdk_out_directory (relative): {cdk_out_directory}")
353
374
  build_commands.append("echo 👉 PWD from synth shell step: ${PWD}")
354
375
 
355
376
  shell = pipelines.ShellStep(
File without changes