bedrock-agentcore-starter-toolkit 0.1.0__py3-none-any.whl → 0.1.1__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 -10
- bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +52 -4
- bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +20 -11
- bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +53 -10
- bedrock_agentcore_starter_toolkit/operations/gateway/README.md +6 -6
- bedrock_agentcore_starter_toolkit/operations/gateway/create_role.py +11 -10
- bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +21 -7
- bedrock_agentcore_starter_toolkit/operations/runtime/create_role.py +404 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +329 -53
- bedrock_agentcore_starter_toolkit/operations/runtime/models.py +4 -1
- bedrock_agentcore_starter_toolkit/services/codebuild.py +332 -0
- bedrock_agentcore_starter_toolkit/services/ecr.py +29 -0
- bedrock_agentcore_starter_toolkit/services/runtime.py +91 -1
- bedrock_agentcore_starter_toolkit/utils/logging_config.py +72 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/entrypoint.py +3 -3
- bedrock_agentcore_starter_toolkit/utils/runtime/policy_template.py +74 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +12 -2
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +10 -25
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/dockerignore.template +0 -1
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_policy.json.j2 +98 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_trust_policy.json.j2 +21 -0
- {bedrock_agentcore_starter_toolkit-0.1.0.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/METADATA +7 -7
- {bedrock_agentcore_starter_toolkit-0.1.0.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/RECORD +27 -21
- {bedrock_agentcore_starter_toolkit-0.1.0.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/WHEEL +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.0.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/entry_points.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.0.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/licenses/LICENSE.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.0.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/licenses/NOTICE.txt +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""CodeBuild service for ARM64 container builds."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
import zipfile
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
import boto3
|
|
14
|
+
from botocore.exceptions import ClientError
|
|
15
|
+
|
|
16
|
+
from ..operations.runtime.create_role import get_or_create_codebuild_execution_role
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CodeBuildService:
|
|
20
|
+
"""Service for managing CodeBuild projects and builds for ARM64."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, session: boto3.Session):
|
|
23
|
+
"""Initialize CodeBuild service with AWS session."""
|
|
24
|
+
self.session = session
|
|
25
|
+
self.client = session.client("codebuild")
|
|
26
|
+
self.s3_client = session.client("s3")
|
|
27
|
+
self.iam_client = session.client("iam")
|
|
28
|
+
self.logger = logging.getLogger(__name__)
|
|
29
|
+
self.source_bucket = None
|
|
30
|
+
|
|
31
|
+
def get_source_bucket_name(self, account_id: str) -> str:
|
|
32
|
+
"""Get S3 bucket name for CodeBuild sources."""
|
|
33
|
+
region = self.session.region_name
|
|
34
|
+
return f"bedrock-agentcore-codebuild-sources-{account_id}-{region}"
|
|
35
|
+
|
|
36
|
+
def ensure_source_bucket(self, account_id: str) -> str:
|
|
37
|
+
"""Ensure S3 bucket exists for CodeBuild sources."""
|
|
38
|
+
bucket_name = self.get_source_bucket_name(account_id)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
self.s3_client.head_bucket(Bucket=bucket_name)
|
|
42
|
+
self.logger.debug("Using existing S3 bucket: %s", bucket_name)
|
|
43
|
+
except ClientError:
|
|
44
|
+
# Create bucket
|
|
45
|
+
region = self.session.region_name
|
|
46
|
+
self.s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": region})
|
|
47
|
+
|
|
48
|
+
# Set lifecycle to cleanup old builds
|
|
49
|
+
self.s3_client.put_bucket_lifecycle_configuration(
|
|
50
|
+
Bucket=bucket_name,
|
|
51
|
+
LifecycleConfiguration={
|
|
52
|
+
"Rules": [{"ID": "DeleteOldBuilds", "Status": "Enabled", "Filter": {}, "Expiration": {"Days": 7}}]
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.logger.info("Created S3 bucket: %s", bucket_name)
|
|
57
|
+
|
|
58
|
+
return bucket_name
|
|
59
|
+
|
|
60
|
+
def upload_source(self, agent_name: str) -> str:
|
|
61
|
+
"""Upload current directory to S3, respecting .dockerignore patterns."""
|
|
62
|
+
account_id = self.session.client("sts").get_caller_identity()["Account"]
|
|
63
|
+
bucket_name = self.ensure_source_bucket(account_id)
|
|
64
|
+
self.source_bucket = bucket_name
|
|
65
|
+
|
|
66
|
+
# Parse .dockerignore patterns
|
|
67
|
+
ignore_patterns = self._parse_dockerignore()
|
|
68
|
+
|
|
69
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
|
|
70
|
+
try:
|
|
71
|
+
with zipfile.ZipFile(temp_zip.name, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
72
|
+
for root, dirs, files in os.walk("."):
|
|
73
|
+
# Convert to relative path
|
|
74
|
+
rel_root = os.path.relpath(root, ".")
|
|
75
|
+
if rel_root == ".":
|
|
76
|
+
rel_root = ""
|
|
77
|
+
|
|
78
|
+
# Filter directories
|
|
79
|
+
dirs[:] = [
|
|
80
|
+
d
|
|
81
|
+
for d in dirs
|
|
82
|
+
if not self._should_ignore(
|
|
83
|
+
os.path.join(rel_root, d) if rel_root else d, ignore_patterns, is_dir=True
|
|
84
|
+
)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
for file in files:
|
|
88
|
+
file_rel_path = os.path.join(rel_root, file) if rel_root else file
|
|
89
|
+
|
|
90
|
+
# Skip if matches ignore pattern
|
|
91
|
+
if self._should_ignore(file_rel_path, ignore_patterns, is_dir=False):
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
file_path = Path(root) / file
|
|
95
|
+
zipf.write(file_path, file_rel_path)
|
|
96
|
+
|
|
97
|
+
# Create agent-organized S3 key: agentname/timestamp.zip
|
|
98
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
99
|
+
s3_key = f"{agent_name}/{timestamp}.zip"
|
|
100
|
+
|
|
101
|
+
self.s3_client.upload_file(temp_zip.name, bucket_name, s3_key)
|
|
102
|
+
|
|
103
|
+
self.logger.info("Uploaded source to S3: %s", s3_key)
|
|
104
|
+
return f"s3://{bucket_name}/{s3_key}"
|
|
105
|
+
|
|
106
|
+
finally:
|
|
107
|
+
os.unlink(temp_zip.name)
|
|
108
|
+
|
|
109
|
+
def _normalize_s3_location(self, source_location: str) -> str:
|
|
110
|
+
"""Convert s3:// URL to bucket/key format for CodeBuild."""
|
|
111
|
+
return source_location.replace("s3://", "") if source_location.startswith("s3://") else source_location
|
|
112
|
+
|
|
113
|
+
def create_codebuild_execution_role(self, account_id: str, ecr_repository_arn: str, agent_name: str) -> str:
|
|
114
|
+
"""Get or create CodeBuild execution role using shared role creation logic."""
|
|
115
|
+
return get_or_create_codebuild_execution_role(
|
|
116
|
+
session=self.session,
|
|
117
|
+
logger=self.logger,
|
|
118
|
+
region=self.session.region_name,
|
|
119
|
+
account_id=account_id,
|
|
120
|
+
agent_name=agent_name,
|
|
121
|
+
ecr_repository_arn=ecr_repository_arn,
|
|
122
|
+
source_bucket_name=self.get_source_bucket_name(account_id),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def create_or_update_project(
|
|
126
|
+
self, agent_name: str, ecr_repository_uri: str, execution_role: str, source_location: str
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Create or update CodeBuild project for ARM64 builds."""
|
|
129
|
+
project_name = f"bedrock-agentcore-{agent_name}-builder"
|
|
130
|
+
|
|
131
|
+
buildspec = self._get_arm64_buildspec(ecr_repository_uri)
|
|
132
|
+
|
|
133
|
+
# CodeBuild expects S3 location without s3:// prefix (bucket/key format)
|
|
134
|
+
codebuild_source_location = self._normalize_s3_location(source_location)
|
|
135
|
+
|
|
136
|
+
project_config = {
|
|
137
|
+
"name": project_name,
|
|
138
|
+
"source": {
|
|
139
|
+
"type": "S3",
|
|
140
|
+
"location": codebuild_source_location,
|
|
141
|
+
"buildspec": buildspec,
|
|
142
|
+
},
|
|
143
|
+
"artifacts": {
|
|
144
|
+
"type": "NO_ARTIFACTS",
|
|
145
|
+
},
|
|
146
|
+
"environment": {
|
|
147
|
+
"type": "ARM_CONTAINER", # ARM64 images require ARM_CONTAINER environment type
|
|
148
|
+
"image": "aws/codebuild/amazonlinux2-aarch64-standard:3.0",
|
|
149
|
+
"computeType": "BUILD_GENERAL1_LARGE", # 4 vCPUs, 7GB RAM for faster builds
|
|
150
|
+
"privilegedMode": True, # Required for Docker
|
|
151
|
+
},
|
|
152
|
+
"serviceRole": execution_role,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
self.client.create_project(**project_config)
|
|
157
|
+
self.logger.info("Created CodeBuild project: %s", project_name)
|
|
158
|
+
except ClientError as e:
|
|
159
|
+
if e.response["Error"]["Code"] == "ResourceAlreadyExistsException":
|
|
160
|
+
self.client.update_project(**project_config)
|
|
161
|
+
self.logger.info("Updated CodeBuild project: %s", project_name)
|
|
162
|
+
else:
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
return project_name
|
|
166
|
+
|
|
167
|
+
def start_build(self, project_name: str, source_location: str) -> str:
|
|
168
|
+
"""Start a CodeBuild build."""
|
|
169
|
+
# CodeBuild expects S3 location without s3:// prefix (bucket/key format)
|
|
170
|
+
codebuild_source_location = self._normalize_s3_location(source_location)
|
|
171
|
+
|
|
172
|
+
response = self.client.start_build(
|
|
173
|
+
projectName=project_name,
|
|
174
|
+
sourceLocationOverride=codebuild_source_location,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return response["build"]["id"]
|
|
178
|
+
|
|
179
|
+
def wait_for_completion(self, build_id: str, timeout: int = 900):
|
|
180
|
+
"""Wait for CodeBuild to complete with detailed phase tracking."""
|
|
181
|
+
self.logger.info("Starting CodeBuild monitoring...")
|
|
182
|
+
|
|
183
|
+
# Phase tracking variables
|
|
184
|
+
current_phase = None
|
|
185
|
+
phase_start_time = None
|
|
186
|
+
build_start_time = time.time()
|
|
187
|
+
|
|
188
|
+
while time.time() - build_start_time < timeout:
|
|
189
|
+
response = self.client.batch_get_builds(ids=[build_id])
|
|
190
|
+
build = response["builds"][0]
|
|
191
|
+
status = build["buildStatus"]
|
|
192
|
+
build_phase = build.get("currentPhase", "UNKNOWN")
|
|
193
|
+
|
|
194
|
+
# Track phase changes
|
|
195
|
+
if build_phase != current_phase:
|
|
196
|
+
# Log previous phase completion (if any)
|
|
197
|
+
if current_phase and phase_start_time:
|
|
198
|
+
phase_duration = time.time() - phase_start_time
|
|
199
|
+
self.logger.info("✅ %s completed in %.1fs", current_phase, phase_duration)
|
|
200
|
+
|
|
201
|
+
# Log new phase start
|
|
202
|
+
current_phase = build_phase
|
|
203
|
+
phase_start_time = time.time()
|
|
204
|
+
total_duration = phase_start_time - build_start_time
|
|
205
|
+
self.logger.info("🔄 %s started (total: %.0fs)", current_phase, total_duration)
|
|
206
|
+
|
|
207
|
+
# Check for completion
|
|
208
|
+
if status == "SUCCEEDED":
|
|
209
|
+
# Log final phase completion
|
|
210
|
+
if current_phase and phase_start_time:
|
|
211
|
+
phase_duration = time.time() - phase_start_time
|
|
212
|
+
self.logger.info("✅ %s completed in %.1fs", current_phase, phase_duration)
|
|
213
|
+
|
|
214
|
+
total_duration = time.time() - build_start_time
|
|
215
|
+
minutes, seconds = divmod(int(total_duration), 60)
|
|
216
|
+
self.logger.info("🎉 CodeBuild completed successfully in %dm %ds", minutes, seconds)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
elif status in ["FAILED", "FAULT", "STOPPED", "TIMED_OUT"]:
|
|
220
|
+
# Log failure with phase info
|
|
221
|
+
if current_phase:
|
|
222
|
+
self.logger.error("❌ Build failed during %s phase", current_phase)
|
|
223
|
+
raise RuntimeError(f"CodeBuild failed with status: {status}")
|
|
224
|
+
|
|
225
|
+
time.sleep(5)
|
|
226
|
+
|
|
227
|
+
total_duration = time.time() - build_start_time
|
|
228
|
+
minutes, seconds = divmod(int(total_duration), 60)
|
|
229
|
+
raise TimeoutError(f"CodeBuild timed out after {minutes}m {seconds}s (current phase: {current_phase})")
|
|
230
|
+
|
|
231
|
+
def _get_arm64_buildspec(self, ecr_repository_uri: str) -> str:
|
|
232
|
+
"""Get optimized buildspec for ARM64 Docker."""
|
|
233
|
+
return f"""
|
|
234
|
+
version: 0.2
|
|
235
|
+
phases:
|
|
236
|
+
pre_build:
|
|
237
|
+
commands:
|
|
238
|
+
- echo Logging in to Amazon ECR...
|
|
239
|
+
- aws ecr get-login-password --region $AWS_DEFAULT_REGION |
|
|
240
|
+
docker login --username AWS --password-stdin {ecr_repository_uri}
|
|
241
|
+
- export DOCKER_BUILDKIT=1
|
|
242
|
+
- export BUILDKIT_PROGRESS=plain
|
|
243
|
+
build:
|
|
244
|
+
commands:
|
|
245
|
+
- echo Build started on `date`
|
|
246
|
+
- echo Building ARM64 Docker image with BuildKit processing...
|
|
247
|
+
- export DOCKER_BUILDKIT=1
|
|
248
|
+
- docker buildx create --name arm64builder --use || true
|
|
249
|
+
- docker buildx build --platform linux/arm64 --load -t bedrock-agentcore-arm64 .
|
|
250
|
+
- docker tag bedrock-agentcore-arm64:latest {ecr_repository_uri}:latest
|
|
251
|
+
post_build:
|
|
252
|
+
commands:
|
|
253
|
+
- echo Build completed on `date`
|
|
254
|
+
- echo Pushing ARM64 image to ECR...
|
|
255
|
+
- docker push {ecr_repository_uri}:latest
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def _parse_dockerignore(self) -> List[str]:
|
|
259
|
+
"""Parse .dockerignore file and return list of patterns."""
|
|
260
|
+
dockerignore_path = Path(".dockerignore")
|
|
261
|
+
patterns = []
|
|
262
|
+
|
|
263
|
+
if dockerignore_path.exists():
|
|
264
|
+
with open(dockerignore_path, "r") as f:
|
|
265
|
+
for line in f:
|
|
266
|
+
line = line.strip()
|
|
267
|
+
if line and not line.startswith("#"):
|
|
268
|
+
patterns.append(line)
|
|
269
|
+
|
|
270
|
+
self.logger.info("Using .dockerignore with %d patterns", len(patterns))
|
|
271
|
+
else:
|
|
272
|
+
# Default patterns if no .dockerignore
|
|
273
|
+
patterns = [
|
|
274
|
+
".git",
|
|
275
|
+
"__pycache__",
|
|
276
|
+
"*.pyc",
|
|
277
|
+
".DS_Store",
|
|
278
|
+
"node_modules",
|
|
279
|
+
".venv",
|
|
280
|
+
"venv",
|
|
281
|
+
"*.egg-info",
|
|
282
|
+
".bedrock_agentcore.yaml", # Always exclude config
|
|
283
|
+
]
|
|
284
|
+
self.logger.info("No .dockerignore found, using default exclude patterns")
|
|
285
|
+
|
|
286
|
+
return patterns
|
|
287
|
+
|
|
288
|
+
def _should_ignore(self, path: str, patterns: List[str], is_dir: bool = False) -> bool:
|
|
289
|
+
"""Check if path should be ignored based on dockerignore patterns."""
|
|
290
|
+
# Normalize path
|
|
291
|
+
if path.startswith("./"):
|
|
292
|
+
path = path[2:]
|
|
293
|
+
|
|
294
|
+
should_ignore = False # Default state: don't ignore
|
|
295
|
+
|
|
296
|
+
for pattern in patterns:
|
|
297
|
+
# Handle negation patterns
|
|
298
|
+
if pattern.startswith("!"):
|
|
299
|
+
if self._matches_pattern(path, pattern[1:], is_dir):
|
|
300
|
+
should_ignore = False # Negation pattern: don't ignore
|
|
301
|
+
else:
|
|
302
|
+
# Regular ignore patterns
|
|
303
|
+
if self._matches_pattern(path, pattern, is_dir):
|
|
304
|
+
should_ignore = True # Regular pattern: ignore
|
|
305
|
+
|
|
306
|
+
return should_ignore
|
|
307
|
+
|
|
308
|
+
def _matches_pattern(self, path: str, pattern: str, is_dir: bool) -> bool:
|
|
309
|
+
"""Check if path matches a dockerignore pattern."""
|
|
310
|
+
# Directory-specific patterns
|
|
311
|
+
if pattern.endswith("/"):
|
|
312
|
+
if not is_dir:
|
|
313
|
+
return False
|
|
314
|
+
pattern = pattern[:-1]
|
|
315
|
+
|
|
316
|
+
# Exact match
|
|
317
|
+
if path == pattern:
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
# Glob pattern match
|
|
321
|
+
if fnmatch.fnmatch(path, pattern):
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
# Directory prefix match
|
|
325
|
+
if is_dir and pattern in path.split("/"):
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
# File in ignored directory
|
|
329
|
+
if not is_dir and any(fnmatch.fnmatch(part, pattern) for part in path.split("/")):
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
return False
|
|
@@ -28,6 +28,35 @@ def create_ecr_repository(repo_name: str, region: str) -> str:
|
|
|
28
28
|
return response["repositories"][0]["repositoryUri"]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def get_or_create_ecr_repository(agent_name: str, region: str) -> str:
|
|
32
|
+
"""Get existing ECR repository or create a new one (idempotent).
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
agent_name: Name of the agent
|
|
36
|
+
region: AWS region
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
ECR repository URI
|
|
40
|
+
"""
|
|
41
|
+
# Generate deterministic repository name based on agent name
|
|
42
|
+
repo_name = f"bedrock-agentcore-{agent_name}"
|
|
43
|
+
|
|
44
|
+
ecr = boto3.client("ecr", region_name=region)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Step 1: Check if repository already exists
|
|
48
|
+
response = ecr.describe_repositories(repositoryNames=[repo_name])
|
|
49
|
+
existing_repo_uri = response["repositories"][0]["repositoryUri"]
|
|
50
|
+
|
|
51
|
+
print(f"✅ Reusing existing ECR repository: {existing_repo_uri}")
|
|
52
|
+
return existing_repo_uri
|
|
53
|
+
|
|
54
|
+
except ecr.exceptions.RepositoryNotFoundException:
|
|
55
|
+
# Step 2: Repository doesn't exist, create it
|
|
56
|
+
print(f"Repository doesn't exist, creating new ECR repository: {repo_name}")
|
|
57
|
+
return create_ecr_repository(repo_name, region)
|
|
58
|
+
|
|
59
|
+
|
|
31
60
|
def deploy_to_ecr(local_tag: str, repo_name: str, region: str, container_runtime: ContainerRuntime) -> str:
|
|
32
61
|
"""Build and push image to ECR."""
|
|
33
62
|
ecr = boto3.client("ecr", region_name=region)
|
|
@@ -9,6 +9,7 @@ from typing import Any, Dict, Optional
|
|
|
9
9
|
|
|
10
10
|
import boto3
|
|
11
11
|
import requests
|
|
12
|
+
from botocore.exceptions import ClientError
|
|
12
13
|
|
|
13
14
|
from ..utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
|
|
14
15
|
|
|
@@ -93,6 +94,7 @@ class BedrockAgentCoreClient:
|
|
|
93
94
|
authorizer_config: Optional[Dict] = None,
|
|
94
95
|
protocol_config: Optional[Dict] = None,
|
|
95
96
|
env_vars: Optional[Dict] = None,
|
|
97
|
+
auto_update_on_conflict: bool = False,
|
|
96
98
|
) -> Dict[str, str]:
|
|
97
99
|
"""Create new agent."""
|
|
98
100
|
self.logger.info("Creating agent '%s' with image URI: %s", agent_name, image_uri)
|
|
@@ -121,6 +123,46 @@ class BedrockAgentCoreClient:
|
|
|
121
123
|
agent_arn = resp["agentRuntimeArn"]
|
|
122
124
|
self.logger.info("Successfully created agent '%s' with ID: %s, ARN: %s", agent_name, agent_id, agent_arn)
|
|
123
125
|
return {"id": agent_id, "arn": agent_arn}
|
|
126
|
+
except ClientError as e:
|
|
127
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
128
|
+
if error_code == "ConflictException":
|
|
129
|
+
if not auto_update_on_conflict:
|
|
130
|
+
self.logger.error("Agent '%s' already exists and auto_update_on_conflict is disabled", agent_name)
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
self.logger.info("Agent '%s' already exists, searching for existing agent...", agent_name)
|
|
134
|
+
|
|
135
|
+
# Find existing agent by name
|
|
136
|
+
existing_agent = self.find_agent_by_name(agent_name)
|
|
137
|
+
|
|
138
|
+
if not existing_agent:
|
|
139
|
+
raise RuntimeError(
|
|
140
|
+
f"ConflictException occurred but couldn't find existing agent '{agent_name}'. "
|
|
141
|
+
f"This might be a permissions issue or the agent name might be different."
|
|
142
|
+
) from e
|
|
143
|
+
|
|
144
|
+
# Extract existing agent details
|
|
145
|
+
existing_agent_id = existing_agent["agentRuntimeId"]
|
|
146
|
+
existing_agent_arn = existing_agent["agentRuntimeArn"]
|
|
147
|
+
|
|
148
|
+
self.logger.info("Found existing agent ID: %s, updating instead...", existing_agent_id)
|
|
149
|
+
|
|
150
|
+
# Update the existing agent
|
|
151
|
+
self.update_agent(
|
|
152
|
+
existing_agent_id,
|
|
153
|
+
image_uri,
|
|
154
|
+
execution_role_arn,
|
|
155
|
+
network_config,
|
|
156
|
+
authorizer_config,
|
|
157
|
+
protocol_config,
|
|
158
|
+
env_vars,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Return the existing agent info (keeping the original ID and ARN)
|
|
162
|
+
return {"id": existing_agent_id, "arn": existing_agent_arn}
|
|
163
|
+
else:
|
|
164
|
+
# Re-raise other ClientErrors
|
|
165
|
+
raise
|
|
124
166
|
except Exception as e:
|
|
125
167
|
self.logger.error("Failed to create agent '%s': %s", agent_name, str(e))
|
|
126
168
|
raise
|
|
@@ -165,6 +207,46 @@ class BedrockAgentCoreClient:
|
|
|
165
207
|
self.logger.error("Failed to update agent ID '%s': %s", agent_id, str(e))
|
|
166
208
|
raise
|
|
167
209
|
|
|
210
|
+
def list_agents(self, max_results: int = 100) -> list:
|
|
211
|
+
"""List all agent runtimes, handling pagination."""
|
|
212
|
+
all_agents = []
|
|
213
|
+
next_token = None
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
while True:
|
|
217
|
+
params = {"maxResults": max_results}
|
|
218
|
+
if next_token:
|
|
219
|
+
params["nextToken"] = next_token
|
|
220
|
+
|
|
221
|
+
response = self.client.list_agent_runtimes(**params)
|
|
222
|
+
agents = response.get("agentRuntimes", [])
|
|
223
|
+
all_agents.extend(agents)
|
|
224
|
+
|
|
225
|
+
next_token = response.get("nextToken")
|
|
226
|
+
if not next_token:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
return all_agents
|
|
230
|
+
except Exception as e:
|
|
231
|
+
self.logger.error("Failed to list agents: %s", str(e))
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
def find_agent_by_name(self, agent_name: str) -> Optional[Dict]:
|
|
235
|
+
"""Find an agent by name, reusing list_agents method."""
|
|
236
|
+
try:
|
|
237
|
+
# Get all agents using the existing method
|
|
238
|
+
all_agents = self.list_agents()
|
|
239
|
+
|
|
240
|
+
# Search for the specific agent by name
|
|
241
|
+
for agent in all_agents:
|
|
242
|
+
if agent.get("agentRuntimeName") == agent_name:
|
|
243
|
+
return agent
|
|
244
|
+
|
|
245
|
+
return None # Agent not found
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self.logger.error("Failed to search for agent '%s': %s", agent_name, str(e))
|
|
248
|
+
raise
|
|
249
|
+
|
|
168
250
|
def create_or_update_agent(
|
|
169
251
|
self,
|
|
170
252
|
agent_id: Optional[str],
|
|
@@ -175,6 +257,7 @@ class BedrockAgentCoreClient:
|
|
|
175
257
|
authorizer_config: Optional[Dict] = None,
|
|
176
258
|
protocol_config: Optional[Dict] = None,
|
|
177
259
|
env_vars: Optional[Dict] = None,
|
|
260
|
+
auto_update_on_conflict: bool = False,
|
|
178
261
|
) -> Dict[str, str]:
|
|
179
262
|
"""Create or update agent."""
|
|
180
263
|
if agent_id:
|
|
@@ -182,7 +265,14 @@ class BedrockAgentCoreClient:
|
|
|
182
265
|
agent_id, image_uri, execution_role_arn, network_config, authorizer_config, protocol_config, env_vars
|
|
183
266
|
)
|
|
184
267
|
return self.create_agent(
|
|
185
|
-
agent_name,
|
|
268
|
+
agent_name,
|
|
269
|
+
image_uri,
|
|
270
|
+
execution_role_arn,
|
|
271
|
+
network_config,
|
|
272
|
+
authorizer_config,
|
|
273
|
+
protocol_config,
|
|
274
|
+
env_vars,
|
|
275
|
+
auto_update_on_conflict,
|
|
186
276
|
)
|
|
187
277
|
|
|
188
278
|
def wait_for_agent_endpoint_ready(self, agent_id: str, endpoint_name: str = "DEFAULT", max_wait: int = 120) -> str:
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Centralized logging configuration for bedrock-agentcore-starter-toolkit."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
_LOGGING_CONFIGURED = False
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_toolkit_logging(mode: str = "sdk") -> None:
|
|
9
|
+
"""Setup logging for bedrock-agentcore-starter-toolkit.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
mode: "cli" or "sdk" (defaults to "sdk")
|
|
13
|
+
"""
|
|
14
|
+
global _LOGGING_CONFIGURED
|
|
15
|
+
if _LOGGING_CONFIGURED:
|
|
16
|
+
return # Already configured, prevent duplicates
|
|
17
|
+
|
|
18
|
+
if mode == "cli":
|
|
19
|
+
_setup_cli_logging()
|
|
20
|
+
elif mode == "sdk":
|
|
21
|
+
_setup_sdk_logging()
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError(f"Invalid logging mode: {mode}. Must be 'cli' or 'sdk'")
|
|
24
|
+
|
|
25
|
+
_LOGGING_CONFIGURED = True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _setup_cli_logging() -> None:
|
|
29
|
+
"""Setup logging for CLI usage with RichHandler."""
|
|
30
|
+
try:
|
|
31
|
+
from rich.logging import RichHandler
|
|
32
|
+
|
|
33
|
+
from ..cli.common import console
|
|
34
|
+
|
|
35
|
+
FORMAT = "%(message)s"
|
|
36
|
+
logging.basicConfig(
|
|
37
|
+
level="INFO",
|
|
38
|
+
format=FORMAT,
|
|
39
|
+
handlers=[RichHandler(show_time=False, show_path=False, show_level=False, console=console)],
|
|
40
|
+
force=True, # Override any existing configuration
|
|
41
|
+
)
|
|
42
|
+
except ImportError:
|
|
43
|
+
# Fallback if rich is not available
|
|
44
|
+
_setup_basic_logging()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _setup_sdk_logging() -> None:
|
|
48
|
+
"""Setup logging for SDK usage (notebooks, scripts, imports) with StreamHandler."""
|
|
49
|
+
# Configure logger for ALL toolkit modules (ensures all operation logs appear)
|
|
50
|
+
toolkit_logger = logging.getLogger("bedrock_agentcore_starter_toolkit")
|
|
51
|
+
|
|
52
|
+
if not toolkit_logger.handlers:
|
|
53
|
+
handler = logging.StreamHandler()
|
|
54
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
55
|
+
toolkit_logger.addHandler(handler)
|
|
56
|
+
toolkit_logger.setLevel(logging.INFO)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _setup_basic_logging() -> None:
|
|
60
|
+
"""Setup basic logging as fallback."""
|
|
61
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s", force=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_logging_configured() -> bool:
|
|
65
|
+
"""Check if toolkit logging has been configured."""
|
|
66
|
+
return _LOGGING_CONFIGURED
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def reset_logging_config() -> None:
|
|
70
|
+
"""Reset logging configuration state (for testing)."""
|
|
71
|
+
global _LOGGING_CONFIGURED
|
|
72
|
+
_LOGGING_CONFIGURED = False
|
|
@@ -181,11 +181,11 @@ def validate_requirements_file(build_dir: Path, requirements_file: str) -> Depen
|
|
|
181
181
|
f"Please specify a requirements file (requirements.txt, pyproject.toml, etc.)"
|
|
182
182
|
)
|
|
183
183
|
|
|
184
|
-
# Validate that it's a recognized dependency file type (
|
|
185
|
-
if file_path.
|
|
184
|
+
# Validate that it's a recognized dependency file type (flexible validation)
|
|
185
|
+
if not (file_path.suffix in [".txt", ".in"] or file_path.name == "pyproject.toml"):
|
|
186
186
|
raise ValueError(
|
|
187
187
|
f"'{file_path.name}' is not a supported dependency file. "
|
|
188
|
-
f"
|
|
188
|
+
f"Supported formats: *.txt, *.in (pip requirements), or pyproject.toml"
|
|
189
189
|
)
|
|
190
190
|
|
|
191
191
|
# Use the existing detect_dependencies function to process the file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Policy template utilities for runtime execution roles."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from jinja2 import Environment, FileSystemLoader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_template_dir() -> Path:
|
|
11
|
+
"""Get the templates directory path."""
|
|
12
|
+
return Path(__file__).parent / "templates"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _render_template(template_name: str, variables: Dict[str, str]) -> str:
|
|
16
|
+
"""Render a Jinja2 template with the provided variables."""
|
|
17
|
+
template_dir = _get_template_dir()
|
|
18
|
+
env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
|
|
19
|
+
template = env.get_template(template_name)
|
|
20
|
+
return template.render(**variables)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_trust_policy_template(region: str, account_id: str) -> str:
|
|
24
|
+
"""Render the trust policy template with provided values.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
region: AWS region
|
|
28
|
+
account_id: AWS account ID
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Rendered trust policy as JSON string
|
|
32
|
+
"""
|
|
33
|
+
variables = {
|
|
34
|
+
"region": region,
|
|
35
|
+
"account_id": account_id,
|
|
36
|
+
}
|
|
37
|
+
return _render_template("execution_role_trust_policy.json.j2", variables)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def render_execution_policy_template(region: str, account_id: str, agent_name: str) -> str:
|
|
41
|
+
"""Render the execution policy template with provided values.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
region: AWS region
|
|
45
|
+
account_id: AWS account ID
|
|
46
|
+
agent_name: Agent name for resource scoping
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Rendered execution policy as JSON string
|
|
50
|
+
"""
|
|
51
|
+
variables = {
|
|
52
|
+
"region": region,
|
|
53
|
+
"account_id": account_id,
|
|
54
|
+
"agent_name": agent_name,
|
|
55
|
+
}
|
|
56
|
+
return _render_template("execution_role_policy.json.j2", variables)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_rendered_policy(policy_json: str) -> Dict:
|
|
60
|
+
"""Validate that the rendered policy is valid JSON.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
policy_json: JSON policy string
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Parsed policy dictionary
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If policy JSON is invalid
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return json.loads(policy_json)
|
|
73
|
+
except json.JSONDecodeError as e:
|
|
74
|
+
raise ValueError(f"Invalid policy JSON: {e}") from e
|