bedrock-agentcore-starter-toolkit 0.1.8__py3-none-any.whl → 0.1.10__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 bedrock-agentcore-starter-toolkit might be problematic. Click here for more details.
- bedrock_agentcore_starter_toolkit/cli/cli.py +3 -1
- bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +431 -161
- bedrock_agentcore_starter_toolkit/operations/gateway/create_role.py +48 -12
- bedrock_agentcore_starter_toolkit/operations/runtime/__init__.py +4 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/destroy.py +542 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +1 -1
- bedrock_agentcore_starter_toolkit/operations/runtime/models.py +10 -0
- bedrock_agentcore_starter_toolkit/services/import_agent/scripts/base_bedrock_translate.py +7 -2
- bedrock_agentcore_starter_toolkit/services/runtime.py +34 -4
- bedrock_agentcore_starter_toolkit/utils/runtime/logs.py +6 -2
- {bedrock_agentcore_starter_toolkit-0.1.8.dist-info → bedrock_agentcore_starter_toolkit-0.1.10.dist-info}/METADATA +4 -3
- {bedrock_agentcore_starter_toolkit-0.1.8.dist-info → bedrock_agentcore_starter_toolkit-0.1.10.dist-info}/RECORD +16 -15
- {bedrock_agentcore_starter_toolkit-0.1.8.dist-info → bedrock_agentcore_starter_toolkit-0.1.10.dist-info}/WHEEL +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.8.dist-info → bedrock_agentcore_starter_toolkit-0.1.10.dist-info}/entry_points.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.8.dist-info → bedrock_agentcore_starter_toolkit-0.1.10.dist-info}/licenses/LICENSE.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.8.dist-info → bedrock_agentcore_starter_toolkit-0.1.10.dist-info}/licenses/NOTICE.txt +0 -0
|
@@ -73,18 +73,54 @@ def _attach_policy(
|
|
|
73
73
|
:param policy_name: the policy name (if not using a policy_arn).
|
|
74
74
|
:return:
|
|
75
75
|
"""
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
# Check for invalid combinations of parameters
|
|
77
|
+
if policy_arn:
|
|
78
|
+
if policy_document or policy_name:
|
|
79
|
+
raise Exception("Cannot specify both policy arn and policy document/name")
|
|
80
|
+
elif not (policy_document and policy_name):
|
|
81
|
+
raise Exception("Must specify both policy document and policy name, or just a policy arn")
|
|
82
|
+
|
|
78
83
|
try:
|
|
79
|
-
if
|
|
80
|
-
iam_client
|
|
81
|
-
|
|
82
|
-
policy = iam_client.create_policy(
|
|
83
|
-
PolicyName=policy_name,
|
|
84
|
-
PolicyDocument=policy_document,
|
|
85
|
-
)
|
|
86
|
-
iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy["Policy"]["Arn"])
|
|
87
|
-
else:
|
|
88
|
-
raise Exception("Must specify both policy document and policy name or just a policy arn")
|
|
84
|
+
if policy_document and policy_name:
|
|
85
|
+
policy_arn = _try_create_policy(iam_client, policy_name, policy_document)
|
|
86
|
+
iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
|
|
89
87
|
except ClientError as e:
|
|
90
88
|
raise RuntimeError(f"Failed to attach AgentCore policy: {e}") from e
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _try_create_policy(iam_client: BaseClient, policy_name: str, policy_document: str) -> str:
|
|
92
|
+
"""Try to create a new policy, or return the arn if the policy already exists.
|
|
93
|
+
|
|
94
|
+
:param iam_client: the IAM client to use.
|
|
95
|
+
:param policy_name: the name of the policy to create.
|
|
96
|
+
:param policy_document: the policy document to create.
|
|
97
|
+
:return: the arn of the policy.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
policy = iam_client.create_policy(
|
|
101
|
+
PolicyName=policy_name,
|
|
102
|
+
PolicyDocument=policy_document,
|
|
103
|
+
)
|
|
104
|
+
return policy["Policy"]["Arn"]
|
|
105
|
+
except ClientError as e:
|
|
106
|
+
if e.response["Error"]["Code"] == "EntityAlreadyExists":
|
|
107
|
+
return _get_existing_policy_arn(iam_client, policy_name)
|
|
108
|
+
else:
|
|
109
|
+
raise e
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_existing_policy_arn(iam_client: BaseClient, policy_name: str) -> str:
|
|
113
|
+
"""Get the arn of an existing policy.
|
|
114
|
+
|
|
115
|
+
:param iam_client: the IAM client to use.
|
|
116
|
+
:param policy_name: the name of the policy to get.
|
|
117
|
+
:return: the arn of the policy.
|
|
118
|
+
"""
|
|
119
|
+
paginator = iam_client.get_paginator("list_policies")
|
|
120
|
+
try:
|
|
121
|
+
for page in paginator.paginate(Scope="Local"):
|
|
122
|
+
for policy in page["Policies"]:
|
|
123
|
+
if policy["PolicyName"] == policy_name:
|
|
124
|
+
return policy["Arn"]
|
|
125
|
+
except ClientError as e:
|
|
126
|
+
raise RuntimeError(f"Failed to get existing policy arn: {e}") from e
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Bedrock AgentCore operations - shared business logic for CLI and notebook interfaces."""
|
|
2
2
|
|
|
3
3
|
from .configure import configure_bedrock_agentcore, validate_agent_name
|
|
4
|
+
from .destroy import destroy_bedrock_agentcore
|
|
4
5
|
from .invoke import invoke_bedrock_agentcore
|
|
5
6
|
from .launch import launch_bedrock_agentcore
|
|
6
7
|
from .models import (
|
|
7
8
|
ConfigureResult,
|
|
9
|
+
DestroyResult,
|
|
8
10
|
InvokeResult,
|
|
9
11
|
LaunchResult,
|
|
10
12
|
StatusConfigInfo,
|
|
@@ -14,11 +16,13 @@ from .status import get_status
|
|
|
14
16
|
|
|
15
17
|
__all__ = [
|
|
16
18
|
"configure_bedrock_agentcore",
|
|
19
|
+
"destroy_bedrock_agentcore",
|
|
17
20
|
"validate_agent_name",
|
|
18
21
|
"launch_bedrock_agentcore",
|
|
19
22
|
"invoke_bedrock_agentcore",
|
|
20
23
|
"get_status",
|
|
21
24
|
"ConfigureResult",
|
|
25
|
+
"DestroyResult",
|
|
22
26
|
"InvokeResult",
|
|
23
27
|
"LaunchResult",
|
|
24
28
|
"StatusResult",
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""Destroy operation - removes Bedrock AgentCore resources from AWS."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
|
|
10
|
+
from ...services.runtime import BedrockAgentCoreClient
|
|
11
|
+
from ...utils.runtime.config import load_config, save_config
|
|
12
|
+
from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
|
|
13
|
+
from .models import DestroyResult
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def destroy_bedrock_agentcore(
|
|
19
|
+
config_path: Path,
|
|
20
|
+
agent_name: Optional[str] = None,
|
|
21
|
+
dry_run: bool = False,
|
|
22
|
+
force: bool = False,
|
|
23
|
+
delete_ecr_repo: bool = False,
|
|
24
|
+
) -> DestroyResult:
|
|
25
|
+
"""Destroy Bedrock AgentCore resources.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config_path: Path to the configuration file
|
|
29
|
+
agent_name: Name of the agent to destroy (default: use default agent)
|
|
30
|
+
dry_run: If True, only show what would be destroyed without actually doing it
|
|
31
|
+
force: If True, skip confirmation prompts
|
|
32
|
+
delete_ecr_repo: If True, also delete the ECR repository after removing images
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
DestroyResult: Details of what was destroyed or would be destroyed
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
FileNotFoundError: If configuration file doesn't exist
|
|
39
|
+
ValueError: If agent is not found or not deployed
|
|
40
|
+
RuntimeError: If destruction fails
|
|
41
|
+
"""
|
|
42
|
+
log.info("Starting destroy operation for agent: %s (dry_run=%s, delete_ecr_repo=%s)",
|
|
43
|
+
agent_name or "default", dry_run, delete_ecr_repo)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Load configuration
|
|
47
|
+
project_config = load_config(config_path)
|
|
48
|
+
agent_config = project_config.get_agent_config(agent_name)
|
|
49
|
+
|
|
50
|
+
if not agent_config:
|
|
51
|
+
raise ValueError(f"Agent '{agent_name or 'default'}' not found in configuration")
|
|
52
|
+
|
|
53
|
+
# Initialize result
|
|
54
|
+
result = DestroyResult(agent_name=agent_config.name, dry_run=dry_run)
|
|
55
|
+
|
|
56
|
+
# Check if agent is deployed
|
|
57
|
+
if not agent_config.bedrock_agentcore:
|
|
58
|
+
result.warnings.append("Agent is not deployed, nothing to destroy")
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
# Initialize AWS session and clients
|
|
62
|
+
session = boto3.Session(region_name=agent_config.aws.region)
|
|
63
|
+
|
|
64
|
+
# 1. Destroy Bedrock AgentCore endpoint (if exists)
|
|
65
|
+
_destroy_agentcore_endpoint(session, agent_config, result, dry_run)
|
|
66
|
+
|
|
67
|
+
# 2. Destroy Bedrock AgentCore agent
|
|
68
|
+
_destroy_agentcore_agent(session, agent_config, result, dry_run)
|
|
69
|
+
|
|
70
|
+
# 3. Remove ECR images and optionally the repository
|
|
71
|
+
_destroy_ecr_images(session, agent_config, result, dry_run, delete_ecr_repo)
|
|
72
|
+
|
|
73
|
+
# 4. Remove CodeBuild project
|
|
74
|
+
_destroy_codebuild_project(session, agent_config, result, dry_run)
|
|
75
|
+
|
|
76
|
+
# 5. Remove CodeBuild IAM Role
|
|
77
|
+
_destroy_codebuild_iam_role(session, agent_config, result, dry_run)
|
|
78
|
+
|
|
79
|
+
# 6. Remove IAM execution role (if not used by other agents)
|
|
80
|
+
_destroy_iam_role(session, project_config, agent_config, result, dry_run)
|
|
81
|
+
|
|
82
|
+
# 7. Clean up configuration
|
|
83
|
+
if not dry_run and not result.errors:
|
|
84
|
+
_cleanup_agent_config(config_path, project_config, agent_config.name, result)
|
|
85
|
+
|
|
86
|
+
log.info("Destroy operation completed. Resources removed: %d, Warnings: %d, Errors: %d",
|
|
87
|
+
len(result.resources_removed), len(result.warnings), len(result.errors))
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
log.error("Destroy operation failed: %s", str(e))
|
|
93
|
+
raise RuntimeError(f"Destroy operation failed: {e}") from e
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _destroy_agentcore_endpoint(
|
|
97
|
+
session: boto3.Session,
|
|
98
|
+
agent_config: BedrockAgentCoreAgentSchema,
|
|
99
|
+
result: DestroyResult,
|
|
100
|
+
dry_run: bool,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Destroy Bedrock AgentCore endpoint."""
|
|
103
|
+
if not agent_config.bedrock_agentcore:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
client = BedrockAgentCoreClient(agent_config.aws.region)
|
|
108
|
+
|
|
109
|
+
agent_id = agent_config.bedrock_agentcore.agent_id
|
|
110
|
+
if not agent_id:
|
|
111
|
+
result.warnings.append("No agent ID found, skipping endpoint destruction")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Get actual endpoint details to determine endpoint name
|
|
115
|
+
try:
|
|
116
|
+
endpoint_response = client.get_agent_runtime_endpoint(agent_id)
|
|
117
|
+
endpoint_name = endpoint_response.get("name", "DEFAULT")
|
|
118
|
+
endpoint_arn = endpoint_response.get("agentRuntimeEndpointArn")
|
|
119
|
+
|
|
120
|
+
# Special case: DEFAULT endpoint cannot be explicitly deleted
|
|
121
|
+
if endpoint_name == "DEFAULT":
|
|
122
|
+
result.warnings.append(
|
|
123
|
+
"DEFAULT endpoint cannot be explicitly deleted, skipping"
|
|
124
|
+
)
|
|
125
|
+
log.info("Skipping deletion of DEFAULT endpoint")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if dry_run:
|
|
129
|
+
result.resources_removed.append(f"AgentCore endpoint: {endpoint_name} (DRY RUN)")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Delete the endpoint
|
|
133
|
+
if endpoint_arn:
|
|
134
|
+
try:
|
|
135
|
+
client.delete_agent_runtime_endpoint(agent_id, endpoint_name)
|
|
136
|
+
result.resources_removed.append(f"AgentCore endpoint: {endpoint_arn}")
|
|
137
|
+
log.info("Deleted AgentCore endpoint: %s", endpoint_arn)
|
|
138
|
+
except ClientError as delete_error:
|
|
139
|
+
if delete_error.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
|
|
140
|
+
result.errors.append(f"Failed to delete endpoint {endpoint_arn}: {delete_error}")
|
|
141
|
+
log.error("Failed to delete endpoint: %s", delete_error)
|
|
142
|
+
else:
|
|
143
|
+
result.warnings.append("Endpoint not found or already deleted during deletion")
|
|
144
|
+
else:
|
|
145
|
+
result.warnings.append("No endpoint ARN found for agent")
|
|
146
|
+
|
|
147
|
+
except ClientError as e:
|
|
148
|
+
if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
|
|
149
|
+
result.warnings.append(f"Failed to get endpoint info: {e}")
|
|
150
|
+
log.warning("Failed to get endpoint info: %s", e)
|
|
151
|
+
else:
|
|
152
|
+
result.warnings.append("Endpoint not found or already deleted")
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
result.warnings.append(f"Error during endpoint destruction: {e}")
|
|
156
|
+
log.warning("Error during endpoint destruction: %s", e)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _destroy_agentcore_agent(
|
|
160
|
+
session: boto3.Session,
|
|
161
|
+
agent_config: BedrockAgentCoreAgentSchema,
|
|
162
|
+
result: DestroyResult,
|
|
163
|
+
dry_run: bool,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Destroy Bedrock AgentCore agent."""
|
|
166
|
+
if not agent_config.bedrock_agentcore or not agent_config.bedrock_agentcore.agent_arn:
|
|
167
|
+
result.warnings.append("No agent ARN found, skipping agent destruction")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
client = BedrockAgentCoreClient(agent_config.aws.region)
|
|
172
|
+
agent_arn = agent_config.bedrock_agentcore.agent_arn
|
|
173
|
+
agent_id = agent_config.bedrock_agentcore.agent_id
|
|
174
|
+
|
|
175
|
+
if dry_run:
|
|
176
|
+
result.resources_removed.append(f"AgentCore agent: {agent_arn} (DRY RUN)")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Delete the agent
|
|
180
|
+
try:
|
|
181
|
+
# Use the control plane client directly since there's no delete_agent_runtime method
|
|
182
|
+
# in the BedrockAgentCoreClient class
|
|
183
|
+
control_client = session.client("bedrock-agentcore-control", region_name=agent_config.aws.region)
|
|
184
|
+
control_client.delete_agent_runtime(agentRuntimeId=agent_id)
|
|
185
|
+
result.resources_removed.append(f"AgentCore agent: {agent_arn}")
|
|
186
|
+
log.info("Deleted AgentCore agent: %s", agent_arn)
|
|
187
|
+
except ClientError as e:
|
|
188
|
+
if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
|
|
189
|
+
result.errors.append(f"Failed to delete agent {agent_arn}: {e}")
|
|
190
|
+
log.error("Failed to delete agent: %s", e)
|
|
191
|
+
else:
|
|
192
|
+
result.warnings.append(f"Agent {agent_arn} not found (may have been deleted already)")
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
result.errors.append(f"Error during agent destruction: {e}")
|
|
196
|
+
log.error("Error during agent destruction: %s", e)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _destroy_ecr_images(
|
|
200
|
+
session: boto3.Session,
|
|
201
|
+
agent_config: BedrockAgentCoreAgentSchema,
|
|
202
|
+
result: DestroyResult,
|
|
203
|
+
dry_run: bool,
|
|
204
|
+
delete_ecr_repo: bool = False,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Remove ECR images and optionally the repository for this specific agent."""
|
|
207
|
+
if not agent_config.aws.ecr_repository:
|
|
208
|
+
result.warnings.append("No ECR repository configured, skipping image cleanup")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
# Create ECR client with explicit region specification
|
|
213
|
+
ecr_client = session.client("ecr", region_name=agent_config.aws.region)
|
|
214
|
+
ecr_uri = agent_config.aws.ecr_repository
|
|
215
|
+
|
|
216
|
+
# Extract repository name from URI
|
|
217
|
+
# Format: account.dkr.ecr.region.amazonaws.com/repo-name
|
|
218
|
+
repo_name = ecr_uri.split("/")[-1]
|
|
219
|
+
|
|
220
|
+
log.info("Checking ECR repository: %s in region: %s", repo_name, agent_config.aws.region)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# List all images in the repository (both tagged and untagged)
|
|
224
|
+
response = ecr_client.list_images(repositoryName=repo_name)
|
|
225
|
+
log.debug("ECR list_images response: %s", response)
|
|
226
|
+
|
|
227
|
+
# Fix: use correct response key 'imageIds' instead of 'imageDetails'
|
|
228
|
+
all_images = response.get("imageIds", [])
|
|
229
|
+
if not all_images:
|
|
230
|
+
if delete_ecr_repo:
|
|
231
|
+
# Repository exists but is empty, we can delete it
|
|
232
|
+
if dry_run:
|
|
233
|
+
result.resources_removed.append(f"ECR repository: {repo_name} (empty, DRY RUN)")
|
|
234
|
+
else:
|
|
235
|
+
_delete_ecr_repository(ecr_client, repo_name, result)
|
|
236
|
+
else:
|
|
237
|
+
result.warnings.append(f"No images found in ECR repository: {repo_name}")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
if dry_run:
|
|
241
|
+
# Fix: imageIds structure has imageTag (string) not imageTags (array)
|
|
242
|
+
tagged_count = len([img for img in all_images if img.get("imageTag")])
|
|
243
|
+
untagged_count = len([img for img in all_images if not img.get("imageTag")])
|
|
244
|
+
result.resources_removed.append(
|
|
245
|
+
f"ECR images in repository {repo_name}: {tagged_count} tagged, {untagged_count} untagged (DRY RUN)"
|
|
246
|
+
)
|
|
247
|
+
if delete_ecr_repo:
|
|
248
|
+
result.resources_removed.append(f"ECR repository: {repo_name} (DRY RUN)")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Prepare images for deletion - imageIds are already in the correct format
|
|
252
|
+
images_to_delete = []
|
|
253
|
+
|
|
254
|
+
for image in all_images:
|
|
255
|
+
# imageIds structure already contains the correct identifiers
|
|
256
|
+
image_id = {}
|
|
257
|
+
|
|
258
|
+
# If image has a tag, use it
|
|
259
|
+
if image.get("imageTag"):
|
|
260
|
+
image_id["imageTag"] = image["imageTag"]
|
|
261
|
+
# If no tag, use image digest
|
|
262
|
+
elif image.get("imageDigest"):
|
|
263
|
+
image_id["imageDigest"] = image["imageDigest"]
|
|
264
|
+
|
|
265
|
+
if image_id:
|
|
266
|
+
images_to_delete.append(image_id)
|
|
267
|
+
|
|
268
|
+
if images_to_delete:
|
|
269
|
+
# Delete images in batches (ECR has a limit of 100 images per batch)
|
|
270
|
+
batch_size = 100
|
|
271
|
+
total_deleted = 0
|
|
272
|
+
|
|
273
|
+
for i in range(0, len(images_to_delete), batch_size):
|
|
274
|
+
batch = images_to_delete[i:i + batch_size]
|
|
275
|
+
|
|
276
|
+
delete_response = ecr_client.batch_delete_image(
|
|
277
|
+
repositoryName=repo_name,
|
|
278
|
+
imageIds=batch
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
deleted_images = delete_response.get("imageIds", [])
|
|
282
|
+
total_deleted += len(deleted_images)
|
|
283
|
+
|
|
284
|
+
# Log any failures in this batch
|
|
285
|
+
failures = delete_response.get("failures", [])
|
|
286
|
+
for failure in failures:
|
|
287
|
+
log.warning("Failed to delete image: %s - %s",
|
|
288
|
+
failure.get("imageId"), failure.get("failureReason"))
|
|
289
|
+
|
|
290
|
+
result.resources_removed.append(f"ECR images: {total_deleted} images from {repo_name}")
|
|
291
|
+
log.info("Deleted %d ECR images from %s", total_deleted, repo_name)
|
|
292
|
+
|
|
293
|
+
# Log any partial failures
|
|
294
|
+
if total_deleted < len(images_to_delete):
|
|
295
|
+
failed_count = len(images_to_delete) - total_deleted
|
|
296
|
+
result.warnings.append(
|
|
297
|
+
f"Some ECR images could not be deleted: {failed_count} out of {len(images_to_delete)} failed"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Delete the repository if requested and all images were deleted successfully
|
|
301
|
+
if delete_ecr_repo and total_deleted == len(images_to_delete):
|
|
302
|
+
_delete_ecr_repository(ecr_client, repo_name, result)
|
|
303
|
+
elif delete_ecr_repo and total_deleted < len(images_to_delete):
|
|
304
|
+
result.warnings.append(f"Cannot delete ECR repository {repo_name}: some images failed to delete")
|
|
305
|
+
else:
|
|
306
|
+
result.warnings.append(f"No valid image identifiers found in {repo_name}")
|
|
307
|
+
|
|
308
|
+
except ClientError as e:
|
|
309
|
+
error_code = e.response["Error"]["Code"]
|
|
310
|
+
if error_code == "RepositoryNotFoundException":
|
|
311
|
+
result.warnings.append(f"ECR repository {repo_name} not found")
|
|
312
|
+
elif error_code == "RepositoryNotEmptyException":
|
|
313
|
+
result.warnings.append(f"ECR repository {repo_name} could not be deleted (not empty)")
|
|
314
|
+
else:
|
|
315
|
+
result.warnings.append(f"Failed to delete ECR images: {e}")
|
|
316
|
+
log.warning("Failed to delete ECR images: %s", e)
|
|
317
|
+
|
|
318
|
+
except Exception as e:
|
|
319
|
+
result.warnings.append(f"Error during ECR cleanup: {e}")
|
|
320
|
+
log.warning("Error during ECR cleanup: %s", e)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _delete_ecr_repository(ecr_client, repo_name: str, result: DestroyResult) -> None:
|
|
324
|
+
"""Delete an ECR repository after ensuring it's empty."""
|
|
325
|
+
try:
|
|
326
|
+
# Verify repository is empty before deletion
|
|
327
|
+
response = ecr_client.list_images(repositoryName=repo_name)
|
|
328
|
+
remaining_images = response.get("imageIds", [])
|
|
329
|
+
|
|
330
|
+
if remaining_images:
|
|
331
|
+
result.warnings.append(f"Cannot delete ECR repository {repo_name}: repository is not empty")
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# Delete the empty repository
|
|
335
|
+
ecr_client.delete_repository(repositoryName=repo_name)
|
|
336
|
+
result.resources_removed.append(f"ECR repository: {repo_name}")
|
|
337
|
+
log.info("Deleted ECR repository: %s", repo_name)
|
|
338
|
+
|
|
339
|
+
except ClientError as e:
|
|
340
|
+
error_code = e.response["Error"]["Code"]
|
|
341
|
+
if error_code == "RepositoryNotFoundException":
|
|
342
|
+
result.warnings.append(f"ECR repository {repo_name} not found (may have been deleted already)")
|
|
343
|
+
elif error_code == "RepositoryNotEmptyException":
|
|
344
|
+
result.warnings.append(f"Cannot delete ECR repository {repo_name}: repository is not empty")
|
|
345
|
+
else:
|
|
346
|
+
result.warnings.append(f"Failed to delete ECR repository {repo_name}: {e}")
|
|
347
|
+
log.warning("Failed to delete ECR repository: %s", e)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
result.warnings.append(f"Error deleting ECR repository {repo_name}: {e}")
|
|
350
|
+
log.warning("Error deleting ECR repository: %s", e)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _destroy_codebuild_project(
|
|
354
|
+
session: boto3.Session,
|
|
355
|
+
agent_config: BedrockAgentCoreAgentSchema,
|
|
356
|
+
result: DestroyResult,
|
|
357
|
+
dry_run: bool,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Remove CodeBuild project for this agent."""
|
|
360
|
+
try:
|
|
361
|
+
codebuild_client = session.client("codebuild", region_name=agent_config.aws.region)
|
|
362
|
+
project_name = f"bedrock-agentcore-{agent_config.name}-builder"
|
|
363
|
+
|
|
364
|
+
if dry_run:
|
|
365
|
+
result.resources_removed.append(f"CodeBuild project: {project_name} (DRY RUN)")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
codebuild_client.delete_project(name=project_name)
|
|
370
|
+
result.resources_removed.append(f"CodeBuild project: {project_name}")
|
|
371
|
+
log.info("Deleted CodeBuild project: %s", project_name)
|
|
372
|
+
except ClientError as e:
|
|
373
|
+
if e.response["Error"]["Code"] not in ["ResourceNotFoundException"]:
|
|
374
|
+
result.warnings.append(f"Failed to delete CodeBuild project {project_name}: {e}")
|
|
375
|
+
log.warning("Failed to delete CodeBuild project: %s", e)
|
|
376
|
+
else:
|
|
377
|
+
result.warnings.append(f"CodeBuild project {project_name} not found")
|
|
378
|
+
|
|
379
|
+
except Exception as e:
|
|
380
|
+
result.warnings.append(f"Error during CodeBuild cleanup: {e}")
|
|
381
|
+
log.warning("Error during CodeBuild cleanup: %s", e)
|
|
382
|
+
|
|
383
|
+
def _destroy_codebuild_iam_role(
|
|
384
|
+
session: boto3.Session,
|
|
385
|
+
agent_config: BedrockAgentCoreAgentSchema,
|
|
386
|
+
result: DestroyResult,
|
|
387
|
+
dry_run: bool,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Remove CodeBuild IAM execution role associated with this agent."""
|
|
390
|
+
if not agent_config.codebuild.execution_role:
|
|
391
|
+
result.warnings.append("No CodeBuild execution role configured, skipping IAM cleanup")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
# Note: IAM is a global service, but we specify region for consistency
|
|
396
|
+
iam_client = session.client("iam", region_name=agent_config.aws.region)
|
|
397
|
+
role_arn = agent_config.codebuild.execution_role
|
|
398
|
+
role_name = role_arn.split("/")[-1]
|
|
399
|
+
|
|
400
|
+
if dry_run:
|
|
401
|
+
result.resources_removed.append(f"CodeBuild IAM role: {role_name} (DRY RUN)")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Detach managed policies
|
|
405
|
+
for policy in iam_client.list_attached_role_policies(RoleName=role_name).get("AttachedPolicies", []):
|
|
406
|
+
iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy["PolicyArn"])
|
|
407
|
+
log.info("Detached policy %s from role %s", policy["PolicyArn"], role_name)
|
|
408
|
+
|
|
409
|
+
# Delete inline policies
|
|
410
|
+
for policy_name in iam_client.list_role_policies(RoleName=role_name).get("PolicyNames", []):
|
|
411
|
+
iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
|
|
412
|
+
log.info("Deleted inline policy %s from role %s", policy_name, role_name)
|
|
413
|
+
|
|
414
|
+
# Delete the role itself
|
|
415
|
+
iam_client.delete_role(RoleName=role_name)
|
|
416
|
+
result.resources_removed.append(f"Deleted CodeBuild IAM role: {role_name}")
|
|
417
|
+
log.info("Deleted CodeBuild IAM role: %s", role_name)
|
|
418
|
+
|
|
419
|
+
except ClientError as e:
|
|
420
|
+
result.warnings.append(f"Failed to delete CodeBuild role {role_name}: {e}")
|
|
421
|
+
log.warning("Failed to delete CodeBuild role %s: %s", role_name, e)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
result.warnings.append(f"Error during CodeBuild IAM role cleanup: {e}")
|
|
424
|
+
log.error("Error during CodeBuild IAM role cleanup: %s", e)
|
|
425
|
+
|
|
426
|
+
def _destroy_iam_role(
|
|
427
|
+
session: boto3.Session,
|
|
428
|
+
project_config: BedrockAgentCoreConfigSchema,
|
|
429
|
+
agent_config: BedrockAgentCoreAgentSchema,
|
|
430
|
+
result: DestroyResult,
|
|
431
|
+
dry_run: bool,
|
|
432
|
+
) -> None:
|
|
433
|
+
"""Remove IAM execution role only if not used by other agents."""
|
|
434
|
+
if not agent_config.aws.execution_role:
|
|
435
|
+
result.warnings.append("No execution role configured, skipping IAM cleanup")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
# Note: IAM is a global service, but we specify region for consistency
|
|
440
|
+
iam_client = session.client("iam", region_name=agent_config.aws.region)
|
|
441
|
+
role_arn = agent_config.aws.execution_role
|
|
442
|
+
role_name = role_arn.split("/")[-1]
|
|
443
|
+
|
|
444
|
+
# Check if other agents use the same role
|
|
445
|
+
other_agents_using_role = [
|
|
446
|
+
name for name, agent in project_config.agents.items()
|
|
447
|
+
if name != agent_config.name and agent.aws.execution_role == role_arn
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
if other_agents_using_role:
|
|
451
|
+
result.warnings.append(
|
|
452
|
+
f"IAM role {role_name} is used by other agents: {other_agents_using_role}. Not deleting."
|
|
453
|
+
)
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
if dry_run:
|
|
457
|
+
result.resources_removed.append(f"IAM execution role: {role_name} (DRY RUN)")
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
# Delete attached policies first
|
|
462
|
+
try:
|
|
463
|
+
policies = iam_client.list_attached_role_policies(RoleName=role_name)
|
|
464
|
+
for policy in policies.get("AttachedPolicies", []):
|
|
465
|
+
iam_client.detach_role_policy(
|
|
466
|
+
RoleName=role_name,
|
|
467
|
+
PolicyArn=policy["PolicyArn"]
|
|
468
|
+
)
|
|
469
|
+
except ClientError:
|
|
470
|
+
pass # Continue if policy detachment fails
|
|
471
|
+
|
|
472
|
+
# Delete inline policies
|
|
473
|
+
try:
|
|
474
|
+
inline_policies = iam_client.list_role_policies(RoleName=role_name)
|
|
475
|
+
for policy_name in inline_policies.get("PolicyNames", []):
|
|
476
|
+
iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
|
|
477
|
+
except ClientError:
|
|
478
|
+
pass # Continue if inline policy deletion fails
|
|
479
|
+
|
|
480
|
+
# Delete the role
|
|
481
|
+
iam_client.delete_role(RoleName=role_name)
|
|
482
|
+
result.resources_removed.append(f"IAM execution role: {role_name}")
|
|
483
|
+
log.info("Deleted IAM role: %s", role_name)
|
|
484
|
+
|
|
485
|
+
except ClientError as e:
|
|
486
|
+
if e.response["Error"]["Code"] not in ["NoSuchEntity"]:
|
|
487
|
+
result.warnings.append(f"Failed to delete IAM role {role_name}: {e}")
|
|
488
|
+
log.warning("Failed to delete IAM role: %s", e)
|
|
489
|
+
else:
|
|
490
|
+
result.warnings.append(f"IAM role {role_name} not found")
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
result.warnings.append(f"Error during IAM cleanup: {e}")
|
|
494
|
+
log.warning("Error during IAM cleanup: %s", e)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _cleanup_agent_config(
|
|
498
|
+
config_path: Path,
|
|
499
|
+
project_config: BedrockAgentCoreConfigSchema,
|
|
500
|
+
agent_name: str,
|
|
501
|
+
result: DestroyResult,
|
|
502
|
+
) -> None:
|
|
503
|
+
"""Remove agent configuration from the config file."""
|
|
504
|
+
try:
|
|
505
|
+
if agent_name not in project_config.agents:
|
|
506
|
+
result.warnings.append(f"Agent {agent_name} not found in configuration")
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
# Check if this agent is the default agent
|
|
510
|
+
was_default = project_config.default_agent == agent_name
|
|
511
|
+
|
|
512
|
+
# Remove the agent entry completely
|
|
513
|
+
del project_config.agents[agent_name]
|
|
514
|
+
result.resources_removed.append(f"Agent configuration: {agent_name}")
|
|
515
|
+
log.info("Removed agent configuration: %s", agent_name)
|
|
516
|
+
|
|
517
|
+
# Handle default agent cleanup
|
|
518
|
+
if was_default:
|
|
519
|
+
if project_config.agents:
|
|
520
|
+
# Set default to the first remaining agent
|
|
521
|
+
new_default = list(project_config.agents.keys())[0]
|
|
522
|
+
project_config.default_agent = new_default
|
|
523
|
+
result.resources_removed.append(f"Default agent updated to: {new_default}")
|
|
524
|
+
log.info("Updated default agent from '%s' to '%s'", agent_name, new_default)
|
|
525
|
+
else:
|
|
526
|
+
# No agents left, clear default
|
|
527
|
+
project_config.default_agent = None
|
|
528
|
+
log.info("Cleared default agent (no agents remaining)")
|
|
529
|
+
|
|
530
|
+
# If no agents remain, remove the config file
|
|
531
|
+
if not project_config.agents:
|
|
532
|
+
config_path.unlink()
|
|
533
|
+
result.resources_removed.append("Configuration file (no agents remaining)")
|
|
534
|
+
log.info("Removed configuration file: %s", config_path)
|
|
535
|
+
else:
|
|
536
|
+
# Save updated configuration
|
|
537
|
+
save_config(project_config, config_path)
|
|
538
|
+
log.info("Updated configuration file")
|
|
539
|
+
|
|
540
|
+
except Exception as e:
|
|
541
|
+
result.warnings.append(f"Failed to update configuration: {e}")
|
|
542
|
+
log.warning("Failed to update configuration: %s", e)
|
|
@@ -30,7 +30,7 @@ def invoke_bedrock_agentcore(
|
|
|
30
30
|
agent_config = project_config.get_agent_config(agent_name)
|
|
31
31
|
# Log which agent is being invoked
|
|
32
32
|
mode = "locally" if local_mode else "via cloud endpoint"
|
|
33
|
-
log.
|
|
33
|
+
log.debug("Invoking BedrockAgentCore agent '%s' %s", agent_config.name, mode)
|
|
34
34
|
|
|
35
35
|
region = agent_config.aws.region
|
|
36
36
|
if not region:
|
|
@@ -77,3 +77,13 @@ class StatusResult(BaseModel):
|
|
|
77
77
|
config: StatusConfigInfo = Field(..., description="Configuration information")
|
|
78
78
|
agent: Optional[Dict[str, Any]] = Field(None, description="Agent runtime details or error")
|
|
79
79
|
endpoint: Optional[Dict[str, Any]] = Field(None, description="Endpoint details or error")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DestroyResult(BaseModel):
|
|
83
|
+
"""Result of destroy operation."""
|
|
84
|
+
|
|
85
|
+
agent_name: str = Field(..., description="Name of the destroyed agent")
|
|
86
|
+
resources_removed: List[str] = Field(default_factory=list, description="List of removed AWS resources")
|
|
87
|
+
warnings: List[str] = Field(default_factory=list, description="List of warnings during destruction")
|
|
88
|
+
errors: List[str] = Field(default_factory=list, description="List of errors during destruction")
|
|
89
|
+
dry_run: bool = Field(default=False, description="Whether this was a dry run")
|