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.
- cdk_factory/app.py +132 -2
- cdk_factory/cli.py +200 -0
- cdk_factory/configurations/resources/ecr.py +47 -2
- cdk_factory/configurations/resources/ecs_service.py +144 -0
- cdk_factory/constructs/ecr/ecr_construct.py +125 -53
- cdk_factory/pipeline/pipeline_factory.py +23 -2
- cdk_factory/stack_library/ecs/__init__.py +0 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +525 -0
- cdk_factory/templates/README.md +99 -0
- cdk_factory/templates/app.py.template +36 -0
- cdk_factory/templates/cdk.json.template +73 -0
- cdk_factory/version.py +1 -1
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.9.0.dist-info}/METADATA +1 -1
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.9.0.dist-info}/RECORD +17 -9
- cdk_factory-0.9.0.dist-info/entry_points.txt +2 -0
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.9.0.dist-info}/WHEEL +0 -0
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
param.node.default_child.
|
|
104
|
-
|
|
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
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|