From 2706263f41c0eca115fc4a66222668a7aeb48389 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Wed, 17 Jun 2026 21:18:35 +0000 Subject: [PATCH 1/6] test(memory): paginate fully in test_list_memories test_list_memories called list_memories() with the default max_results=100. In the shared integration-test account the memory count exceeds 100, so the just-created lifecycle memory could fall outside the capped result set, causing a flaky 'assert False'. Pass a high max_results so the SDK paginates through all memories before asserting. --- tests_integ/memory/test_memory_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests_integ/memory/test_memory_client.py b/tests_integ/memory/test_memory_client.py index 9facf82e..8b980a8f 100644 --- a/tests_integ/memory/test_memory_client.py +++ b/tests_integ/memory/test_memory_client.py @@ -437,7 +437,10 @@ def test_create_memory_and_wait(self): def test_list_memories(self): if not getattr(self, "lifecycle_memory_id", None): pytest.skip("create test did not run") - memories = self.client.list_memories() + # Paginate through all memories. The shared integ-test account can hold + # more than the default 100-item cap, so a capped list may not include + # the just-created memory even though it exists. + memories = self.client.list_memories(max_results=10000) assert any( m.get("memoryId") == self.lifecycle_memory_id or m.get("id") == self.lifecycle_memory_id for m in memories ) From 398773cebaee840dcd58248532bc15bb544ac80c Mon Sep 17 00:00:00 2001 From: jariy17 Date: Wed, 17 Jun 2026 21:20:11 +0000 Subject: [PATCH 2/6] chore(memory): add orphaned test-memory cleanup script Interrupted/failed integration runs leave Memory resources behind in the shared test account. Once the count exceeds the ~100 list page cap, listing tests flake. Adds a dry-run-by-default cleanup script that deletes only test-prefixed memories older than a threshold, while protecting the prepopulated/observability memories that CI secrets depend on. --- scripts/cleanup_orphaned_test_memories.py | 140 ++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 scripts/cleanup_orphaned_test_memories.py diff --git a/scripts/cleanup_orphaned_test_memories.py b/scripts/cleanup_orphaned_test_memories.py new file mode 100755 index 00000000..69d7493d --- /dev/null +++ b/scripts/cleanup_orphaned_test_memories.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Clean up orphaned AgentCore Memory resources left behind by integration tests. + +The integration tests run in a shared AWS account. Memories created by tests are +normally torn down, but interrupted or failed runs leave orphans behind. Once the +account accumulates more than ~100 memories, ``test_list_memories`` (and any other +test that lists with the default page cap) can flake. + +This script lists every memory in the account and deletes the ones that look like +orphaned test fixtures, while protecting long-lived memories that tests depend on +(e.g. the pre-populated memory referenced by the ``MEMORY_PREPOPULATED_ID`` secret). + +Safety: + * Dry-run by default. Pass ``--apply`` to actually delete. + * Only deletes memories whose id starts with a known test prefix AND that are + older than ``--min-age-days`` (default 1 day), so it never races a live run. + * Never deletes protected memories (``--protect`` substrings). + +Usage: + python scripts/cleanup_orphaned_test_memories.py # dry run + python scripts/cleanup_orphaned_test_memories.py --apply # delete + python scripts/cleanup_orphaned_test_memories.py --region us-west-2 --min-age-days 2 --apply +""" + +import argparse +import datetime +import sys + +import boto3 + +# Memory id prefixes created by the integration test suites. Only memories whose +# id starts with one of these are eligible for deletion. +TEST_PREFIXES = ( + "test_cp_", + "mc_2026", + "memory_", + "test_memory", + "sdk_integ", + "integ_test", +) + +# Substrings of memory ids that must never be deleted (referenced by CI secrets). +PROTECTED_SUBSTRINGS = ( + "prepopulated", + "observability", +) + + +def is_protected(memory_id: str) -> bool: + return any(s in memory_id for s in PROTECTED_SUBSTRINGS) + + +def is_test_memory(memory_id: str) -> bool: + return memory_id.startswith(TEST_PREFIXES) + + +def list_all_memories(client): + memories = [] + token = None + while True: + kwargs = {"maxResults": 100} + if token: + kwargs["nextToken"] = token + resp = client.list_memories(**kwargs) + memories.extend(resp.get("memories", [])) + token = resp.get("nextToken") + if not token: + break + return memories + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--region", default="us-west-2", help="AWS region (default: us-west-2)") + parser.add_argument( + "--min-age-days", + type=float, + default=1.0, + help="Only delete memories older than this many days (default: 1)", + ) + parser.add_argument("--apply", action="store_true", help="Actually delete (default is dry-run)") + args = parser.parse_args() + + client = boto3.client("bedrock-agentcore-control", region_name=args.region) + + memories = list_all_memories(client) + now = datetime.datetime.now(datetime.timezone.utc) + cutoff = now - datetime.timedelta(days=args.min_age_days) + + to_delete, skipped_protected, skipped_recent, skipped_nonmatch = [], [], [], [] + for m in memories: + mid = m.get("id") or m.get("memoryId") or "" + created = m.get("createdAt") + if is_protected(mid): + skipped_protected.append(mid) + elif not is_test_memory(mid): + skipped_nonmatch.append(mid) + elif created and created > cutoff: + skipped_recent.append(mid) + else: + to_delete.append(mid) + + print(f"Region: {args.region}") + print(f"Total memories: {len(memories)}") + print(f" protected (kept): {len(skipped_protected)}") + print(f" non-test (kept): {len(skipped_nonmatch)}") + print(f" too recent (kept): {len(skipped_recent)} (younger than {args.min_age_days}d)") + print(f" orphaned test (delete): {len(to_delete)}") + print() + + if not to_delete: + print("Nothing to delete.") + return 0 + + mode = "DELETING" if args.apply else "DRY RUN (would delete)" + print(f"=== {mode} {len(to_delete)} memories ===") + failures = 0 + for mid in to_delete: + if args.apply: + try: + client.delete_memory(memoryId=mid) + print(f" deleted: {mid}") + except Exception as e: # noqa: BLE001 - best-effort cleanup + failures += 1 + print(f" FAILED: {mid} ({e})") + else: + print(f" would delete: {mid}") + + if not args.apply: + print("\nDry run only. Re-run with --apply to delete.") + elif failures: + print(f"\nCompleted with {failures} failures.") + return 1 + else: + print(f"\nDeleted {len(to_delete)} orphaned memories.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 3df3c7a57787cf9d818e1d883961cb5ea4a391f1 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Wed, 17 Jun 2026 21:39:43 +0000 Subject: [PATCH 3/6] test(gateway): use MANAGED KB for gateway KB-target tests Gateway knowledge base targets only support MANAGED (fully-managed) knowledge bases. The gateway control plane (AmazonGenesisEndpoint) validates the target's referenced KB and rejects the Retrieve connector for any non-MANAGED type with 'Retrieve is not supported for this knowledge base type', so the test's targets went to FAILED. The test created a VECTOR knowledge base backed by self-provisioned S3 Vectors, which is not a valid KB type for a gateway target. Switch setup_class to create a MANAGED KB (Bedrock owns the vector store, so no storageConfiguration and no S3 Vectors bucket/index are needed) and drop the corresponding teardown. Verified end-to-end against live AWS: all 3 KB-target tests (minimal, with options, with credential config) reach READY and tear down cleanly. --- .../gateway/test_gateway_kb_targets.py | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/tests_integ/gateway/test_gateway_kb_targets.py b/tests_integ/gateway/test_gateway_kb_targets.py index 000cf746..dbf40dcb 100644 --- a/tests_integ/gateway/test_gateway_kb_targets.py +++ b/tests_integ/gateway/test_gateway_kb_targets.py @@ -1,16 +1,22 @@ """Integration tests for GatewayClient KB target helper methods. Requires environment variables: - BEDROCK_TEST_REGION: AWS region (default: us-east-1) + BEDROCK_TEST_REGION: AWS region (default: us-west-2) GATEWAY_ROLE_ARN: IAM role ARN with AgentCore gateway trust policy - KB_ROLE_ARN: IAM role ARN with bedrock:InvokeModel, s3:*, and s3vectors:* permissions + KB_ROLE_ARN: IAM role ARN with bedrock:InvokeModel permissions for the embedding model + +Note: + Gateway knowledge base targets only support MANAGED (fully-managed) knowledge + bases. The gateway control plane rejects the Retrieve connector for any other + knowledge base type with "Retrieve is not supported for this knowledge base + type." A MANAGED KB lets Bedrock own the vector store internally, so no + storageConfiguration (and no self-provisioned S3 Vectors index) is required. """ import os import time import uuid -import boto3 import pytest from bedrock_agentcore.gateway.client import GatewayClient @@ -37,36 +43,18 @@ def setup_class(cls): cls.kb_id = None cls.target_ids = [] - # Create S3 Vectors resources - cls.s3vectors_client = boto3.client("s3vectors", region_name=cls.region) - cls.vector_bucket_name = f"kb-gw-integ-vb-{cls.test_suffix}" - cls.vector_index_name = f"kb-gw-integ-idx-{cls.test_suffix}" - cls.s3vectors_client.create_vector_bucket(vectorBucketName=cls.vector_bucket_name) - index_resp = cls.s3vectors_client.create_index( - vectorBucketName=cls.vector_bucket_name, - indexName=cls.vector_index_name, - dataType="float32", - dimension=1024, - distanceMetric="cosine", - ) - cls.index_arn = index_resp["indexArn"] - - # Create Knowledge Base + # Create a MANAGED knowledge base. Gateway KB targets require the MANAGED + # type; Bedrock owns the vector store internally, so no storageConfiguration + # or self-provisioned S3 Vectors index is needed. cls.kb = cls.kb_client.create_knowledge_base_and_wait( name=f"{cls.test_prefix}-kb", roleArn=cls.kb_role_arn, knowledgeBaseConfiguration={ - "type": "VECTOR", - "vectorKnowledgeBaseConfiguration": { + "type": "MANAGED", + "managedKnowledgeBaseConfiguration": { "embeddingModelArn": f"arn:aws:bedrock:{cls.region}::foundation-model/amazon.titan-embed-text-v2:0", }, }, - storageConfiguration={ - "type": "S3_VECTORS", - "s3VectorsConfiguration": { - "indexArn": cls.index_arn, - }, - }, ) cls.kb_id = cls.kb["knowledgeBaseId"] @@ -105,16 +93,6 @@ def teardown_class(cls): except Exception as e: print(f"Failed to delete KB {cls.kb_id}: {e}") - # Delete S3 Vectors - try: - cls.s3vectors_client.delete_index( - vectorBucketName=cls.vector_bucket_name, - indexName=cls.vector_index_name, - ) - cls.s3vectors_client.delete_vector_bucket(vectorBucketName=cls.vector_bucket_name) - except Exception as e: - print(f"Failed to clean up vector bucket: {e}") - @pytest.mark.order(1) def test_create_knowledge_base_target_minimal(self): target = self.gateway_client.create_knowledge_base_target( From ab023f148b15c5ceea721107f0bf4df89f8cae71 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Wed, 17 Jun 2026 21:42:22 +0000 Subject: [PATCH 4/6] chore(gateway): add orphaned test-gateway cleanup script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interrupted/failed gateway integration runs leave Gateway resources behind in the shared test account — a target that fails to reach READY can leave its parent gateway undeleted. Adds a dry-run-by-default cleanup script mirroring the memory cleanup: deletes only test-prefixed gateways older than a threshold, removing their targets first (and waiting for async target deletion to settle) before deleting the gateway. --- scripts/cleanup_orphaned_test_gateways.py | 158 ++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100755 scripts/cleanup_orphaned_test_gateways.py diff --git a/scripts/cleanup_orphaned_test_gateways.py b/scripts/cleanup_orphaned_test_gateways.py new file mode 100755 index 00000000..90ca9ebc --- /dev/null +++ b/scripts/cleanup_orphaned_test_gateways.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Clean up orphaned AgentCore Gateway resources left behind by integration tests. + +The gateway integration tests run in a shared AWS account. Gateways and their +targets are normally torn down, but interrupted or failed runs leave orphans +behind (a target that fails to reach READY can leave the parent gateway +undeleted). These accumulate over time and clutter the account. + +This script lists every gateway in the account and deletes the ones that look +like orphaned test fixtures, deleting their targets first so the gateway delete +succeeds. + +Safety: + * Dry-run by default. Pass ``--apply`` to actually delete. + * Only deletes gateways whose name starts with a known test prefix AND that are + older than ``--min-age-days`` (default 1 day), so it never races a live run. + +Usage: + python scripts/cleanup_orphaned_test_gateways.py # dry run + python scripts/cleanup_orphaned_test_gateways.py --apply # delete + python scripts/cleanup_orphaned_test_gateways.py --region us-west-2 --min-age-days 2 --apply +""" + +import argparse +import datetime +import sys +import time + +import boto3 + +# Gateway name prefixes created by the integration test suites. Only gateways +# whose name starts with one of these are eligible for deletion. +TEST_PREFIXES = ( + "sdk-integ-kb-tgt-", + "sdk-integ-", + "test-gateway", + "integ-test", +) + + +def is_test_gateway(name: str) -> bool: + return name.startswith(TEST_PREFIXES) + + +def list_all_gateways(client): + gateways = [] + token = None + while True: + kwargs = {"maxResults": 100} + if token: + kwargs["nextToken"] = token + resp = client.list_gateways(**kwargs) + gateways.extend(resp.get("items", [])) + token = resp.get("nextToken") + if not token: + break + return gateways + + +def _list_target_ids(client, gateway_id): + ids = [] + token = None + while True: + kwargs = {"gatewayIdentifier": gateway_id, "maxResults": 100} + if token: + kwargs["nextToken"] = token + resp = client.list_gateway_targets(**kwargs) + ids.extend(t["targetId"] for t in resp.get("items", []) if t.get("targetId")) + token = resp.get("nextToken") + if not token: + break + return ids + + +def delete_gateway_targets(client, gateway_id, timeout_s=120, poll_s=5): + """Delete all targets of a gateway and wait until they are gone. + + Target deletion is asynchronous; the parent gateway cannot be deleted until + its targets are fully removed, so poll until the target list is empty. + """ + for tid in _list_target_ids(client, gateway_id): + client.delete_gateway_target(gatewayIdentifier=gateway_id, targetId=tid) + + deadline = time.monotonic() + timeout_s + while _list_target_ids(client, gateway_id): + if time.monotonic() > deadline: + raise TimeoutError(f"targets for gateway {gateway_id} not deleted within {timeout_s}s") + time.sleep(poll_s) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--region", default="us-west-2", help="AWS region (default: us-west-2)") + parser.add_argument( + "--min-age-days", + type=float, + default=1.0, + help="Only delete gateways older than this many days (default: 1)", + ) + parser.add_argument("--apply", action="store_true", help="Actually delete (default is dry-run)") + args = parser.parse_args() + + client = boto3.client("bedrock-agentcore-control", region_name=args.region) + + gateways = list_all_gateways(client) + now = datetime.datetime.now(datetime.timezone.utc) + cutoff = now - datetime.timedelta(days=args.min_age_days) + + to_delete, skipped_recent, skipped_nonmatch = [], [], [] + for g in gateways: + name = g.get("name", "") + gid = g.get("gatewayId") + created = g.get("createdAt") + if not is_test_gateway(name): + skipped_nonmatch.append(name) + elif created and created > cutoff: + skipped_recent.append(name) + else: + to_delete.append((gid, name)) + + print(f"Region: {args.region}") + print(f"Total gateways: {len(gateways)}") + print(f" non-test (kept): {len(skipped_nonmatch)}") + print(f" too recent (kept): {len(skipped_recent)} (younger than {args.min_age_days}d)") + print(f" orphaned test (delete): {len(to_delete)}") + print() + + if not to_delete: + print("Nothing to delete.") + return 0 + + mode = "DELETING" if args.apply else "DRY RUN (would delete)" + print(f"=== {mode} {len(to_delete)} gateways ===") + failures = 0 + for gid, name in to_delete: + if args.apply: + try: + delete_gateway_targets(client, gid) + client.delete_gateway(gatewayIdentifier=gid) + print(f" deleted: {name} ({gid})") + except Exception as e: # noqa: BLE001 - best-effort cleanup + failures += 1 + print(f" FAILED: {name} ({gid}) ({e})") + else: + print(f" would delete: {name} ({gid})") + + if not args.apply: + print("\nDry run only. Re-run with --apply to delete.") + elif failures: + print(f"\nCompleted with {failures} failures.") + return 1 + else: + print(f"\nDeleted {len(to_delete)} orphaned gateways.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 6e0745329cbedfe81540ab0454ac34cbe558c231 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Wed, 17 Jun 2026 21:48:24 +0000 Subject: [PATCH 5/6] chore: dedup cleanup scripts into shared helper Address review feedback on the two orphaned-resource cleanup scripts: - Extract the shared scaffolding (argparse, nextToken pagination, bucketing, report, dry-run/apply loop) into scripts/_cleanup_utils.py; each script now declares only its resource-specific prefixes and list/delete calls. - Drop speculative/redundant test prefixes that never matched real resources (memories keep test_cp_/mc_2026/memory_; gateways collapse to sdk-integ-). - Fix stale --protect docstring reference (protection is via PROTECTED_SUBSTRINGS). - Remove the gateway test docstring Note that duplicated the inline comment. Net ~-170 lines; both scripts dry-run-verified against the shared account. --- scripts/_cleanup_utils.py | 86 +++++++++++ scripts/cleanup_orphaned_test_gateways.py | 142 ++++-------------- scripts/cleanup_orphaned_test_memories.py | 115 +++----------- .../gateway/test_gateway_kb_targets.py | 7 - 4 files changed, 131 insertions(+), 219 deletions(-) create mode 100644 scripts/_cleanup_utils.py diff --git a/scripts/_cleanup_utils.py b/scripts/_cleanup_utils.py new file mode 100644 index 00000000..a03e7649 --- /dev/null +++ b/scripts/_cleanup_utils.py @@ -0,0 +1,86 @@ +"""Shared helpers for the orphaned integration-test resource cleanup scripts. + +Both cleanup scripts (memories, gateways) share the same shape: list everything +in a shared test account, keep what's protected/non-test/too-recent, and delete +the rest — dry-run by default. This module holds that common scaffolding so each +script only declares its resource-specific bits (prefixes, list/delete calls). +""" + +import argparse +import datetime + + +def build_parser(description, noun): + parser = argparse.ArgumentParser(description=description) + parser.add_argument("--region", default="us-west-2", help="AWS region (default: us-west-2)") + parser.add_argument( + "--min-age-days", + type=float, + default=1.0, + help=f"Only delete {noun} older than this many days (default: 1)", + ) + parser.add_argument("--apply", action="store_true", help="Actually delete (default is dry-run)") + return parser + + +def paginate(client, operation, items_key, **kwargs): + """Yield all items across pages of a list_* operation that uses nextToken.""" + token = None + while True: + page = getattr(client, operation)(**kwargs, **({"nextToken": token} if token else {})) + yield from page.get(items_key, []) + token = page.get("nextToken") + if not token: + return + + +def run_cleanup(noun, items, *, label_of, created_of, is_test, delete_one, args, is_protected=None): + """Bucket items into kept/deleted, print a report, and delete (unless dry-run).""" + cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=args.min_age_days) + + to_delete, protected, recent, nonmatch = [], [], [], [] + for item in items: + if is_protected and is_protected(item): + protected.append(item) + elif not is_test(item): + nonmatch.append(item) + elif created_of(item) and created_of(item) > cutoff: + recent.append(item) + else: + to_delete.append(item) + + print(f"Region: {args.region}") + print(f"Total {noun}: {len(items)}") + if is_protected: + print(f" protected (kept): {len(protected)}") + print(f" non-test (kept): {len(nonmatch)}") + print(f" too recent (kept): {len(recent)} (younger than {args.min_age_days}d)") + print(f" orphaned test (delete): {len(to_delete)}") + print() + + if not to_delete: + print("Nothing to delete.") + return 0 + + print(f"=== {'DELETING' if args.apply else 'DRY RUN (would delete)'} {len(to_delete)} {noun} ===") + failures = 0 + for item in to_delete: + label = label_of(item) + if not args.apply: + print(f" would delete: {label}") + continue + try: + delete_one(item) + print(f" deleted: {label}") + except Exception as e: # noqa: BLE001 - best-effort cleanup + failures += 1 + print(f" FAILED: {label} ({e})") + + if not args.apply: + print("\nDry run only. Re-run with --apply to delete.") + elif failures: + print(f"\nCompleted with {failures} failures.") + return 1 + else: + print(f"\nDeleted {len(to_delete)} orphaned {noun}.") + return 0 diff --git a/scripts/cleanup_orphaned_test_gateways.py b/scripts/cleanup_orphaned_test_gateways.py index 90ca9ebc..464ef7fd 100755 --- a/scripts/cleanup_orphaned_test_gateways.py +++ b/scripts/cleanup_orphaned_test_gateways.py @@ -21,137 +21,51 @@ python scripts/cleanup_orphaned_test_gateways.py --region us-west-2 --min-age-days 2 --apply """ -import argparse -import datetime import sys import time import boto3 +from _cleanup_utils import build_parser, paginate, run_cleanup -# Gateway name prefixes created by the integration test suites. Only gateways -# whose name starts with one of these are eligible for deletion. -TEST_PREFIXES = ( - "sdk-integ-kb-tgt-", - "sdk-integ-", - "test-gateway", - "integ-test", -) - - -def is_test_gateway(name: str) -> bool: - return name.startswith(TEST_PREFIXES) - - -def list_all_gateways(client): - gateways = [] - token = None - while True: - kwargs = {"maxResults": 100} - if token: - kwargs["nextToken"] = token - resp = client.list_gateways(**kwargs) - gateways.extend(resp.get("items", [])) - token = resp.get("nextToken") - if not token: - break - return gateways - - -def _list_target_ids(client, gateway_id): - ids = [] - token = None - while True: - kwargs = {"gatewayIdentifier": gateway_id, "maxResults": 100} - if token: - kwargs["nextToken"] = token - resp = client.list_gateway_targets(**kwargs) - ids.extend(t["targetId"] for t in resp.get("items", []) if t.get("targetId")) - token = resp.get("nextToken") - if not token: - break - return ids - - -def delete_gateway_targets(client, gateway_id, timeout_s=120, poll_s=5): - """Delete all targets of a gateway and wait until they are gone. - - Target deletion is asynchronous; the parent gateway cannot be deleted until - its targets are fully removed, so poll until the target list is empty. +# Gateway name prefix created by the integration test suites. Only gateways whose +# name starts with this are eligible for deletion. +TEST_PREFIX = "sdk-integ-" + + +def delete_gateway(client, gateway_id, timeout_s=120, poll_s=5): + """Delete a gateway's targets, wait for the async deletes to settle, then delete it. + + The parent gateway cannot be deleted until its targets are fully removed. """ - for tid in _list_target_ids(client, gateway_id): + def target_ids(): + return [t["targetId"] for t in paginate(client, "list_gateway_targets", "items", + gatewayIdentifier=gateway_id, maxResults=100)] + + for tid in target_ids(): client.delete_gateway_target(gatewayIdentifier=gateway_id, targetId=tid) deadline = time.monotonic() + timeout_s - while _list_target_ids(client, gateway_id): + while target_ids(): if time.monotonic() > deadline: raise TimeoutError(f"targets for gateway {gateway_id} not deleted within {timeout_s}s") time.sleep(poll_s) + client.delete_gateway(gatewayIdentifier=gateway_id) -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--region", default="us-west-2", help="AWS region (default: us-west-2)") - parser.add_argument( - "--min-age-days", - type=float, - default=1.0, - help="Only delete gateways older than this many days (default: 1)", - ) - parser.add_argument("--apply", action="store_true", help="Actually delete (default is dry-run)") - args = parser.parse_args() +def main() -> int: + args = build_parser(__doc__, "gateways").parse_args() client = boto3.client("bedrock-agentcore-control", region_name=args.region) - gateways = list_all_gateways(client) - now = datetime.datetime.now(datetime.timezone.utc) - cutoff = now - datetime.timedelta(days=args.min_age_days) - - to_delete, skipped_recent, skipped_nonmatch = [], [], [] - for g in gateways: - name = g.get("name", "") - gid = g.get("gatewayId") - created = g.get("createdAt") - if not is_test_gateway(name): - skipped_nonmatch.append(name) - elif created and created > cutoff: - skipped_recent.append(name) - else: - to_delete.append((gid, name)) - - print(f"Region: {args.region}") - print(f"Total gateways: {len(gateways)}") - print(f" non-test (kept): {len(skipped_nonmatch)}") - print(f" too recent (kept): {len(skipped_recent)} (younger than {args.min_age_days}d)") - print(f" orphaned test (delete): {len(to_delete)}") - print() - - if not to_delete: - print("Nothing to delete.") - return 0 - - mode = "DELETING" if args.apply else "DRY RUN (would delete)" - print(f"=== {mode} {len(to_delete)} gateways ===") - failures = 0 - for gid, name in to_delete: - if args.apply: - try: - delete_gateway_targets(client, gid) - client.delete_gateway(gatewayIdentifier=gid) - print(f" deleted: {name} ({gid})") - except Exception as e: # noqa: BLE001 - best-effort cleanup - failures += 1 - print(f" FAILED: {name} ({gid}) ({e})") - else: - print(f" would delete: {name} ({gid})") - - if not args.apply: - print("\nDry run only. Re-run with --apply to delete.") - elif failures: - print(f"\nCompleted with {failures} failures.") - return 1 - else: - print(f"\nDeleted {len(to_delete)} orphaned gateways.") - return 0 + return run_cleanup( + "gateways", + list(paginate(client, "list_gateways", "items", maxResults=100)), + label_of=lambda g: f"{g.get('name', '')} ({g.get('gatewayId')})", + created_of=lambda g: g.get("createdAt"), + is_test=lambda g: g.get("name", "").startswith(TEST_PREFIX), + delete_one=lambda g: delete_gateway(client, g.get("gatewayId")), + args=args, + ) if __name__ == "__main__": diff --git a/scripts/cleanup_orphaned_test_memories.py b/scripts/cleanup_orphaned_test_memories.py index 69d7493d..c9bbd47b 100755 --- a/scripts/cleanup_orphaned_test_memories.py +++ b/scripts/cleanup_orphaned_test_memories.py @@ -14,7 +14,7 @@ * Dry-run by default. Pass ``--apply`` to actually delete. * Only deletes memories whose id starts with a known test prefix AND that are older than ``--min-age-days`` (default 1 day), so it never races a live run. - * Never deletes protected memories (``--protect`` substrings). + * Never deletes protected memories (see PROTECTED_SUBSTRINGS). Usage: python scripts/cleanup_orphaned_test_memories.py # dry run @@ -22,118 +22,37 @@ python scripts/cleanup_orphaned_test_memories.py --region us-west-2 --min-age-days 2 --apply """ -import argparse -import datetime import sys import boto3 +from _cleanup_utils import build_parser, paginate, run_cleanup # Memory id prefixes created by the integration test suites. Only memories whose # id starts with one of these are eligible for deletion. -TEST_PREFIXES = ( - "test_cp_", - "mc_2026", - "memory_", - "test_memory", - "sdk_integ", - "integ_test", -) +TEST_PREFIXES = ("test_cp_", "mc_2026", "memory_") # Substrings of memory ids that must never be deleted (referenced by CI secrets). -PROTECTED_SUBSTRINGS = ( - "prepopulated", - "observability", -) +PROTECTED_SUBSTRINGS = ("prepopulated", "observability") -def is_protected(memory_id: str) -> bool: - return any(s in memory_id for s in PROTECTED_SUBSTRINGS) - - -def is_test_memory(memory_id: str) -> bool: - return memory_id.startswith(TEST_PREFIXES) - - -def list_all_memories(client): - memories = [] - token = None - while True: - kwargs = {"maxResults": 100} - if token: - kwargs["nextToken"] = token - resp = client.list_memories(**kwargs) - memories.extend(resp.get("memories", [])) - token = resp.get("nextToken") - if not token: - break - return memories +def _id(memory): + return memory.get("id") or memory.get("memoryId") or "" def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--region", default="us-west-2", help="AWS region (default: us-west-2)") - parser.add_argument( - "--min-age-days", - type=float, - default=1.0, - help="Only delete memories older than this many days (default: 1)", - ) - parser.add_argument("--apply", action="store_true", help="Actually delete (default is dry-run)") - args = parser.parse_args() - + args = build_parser(__doc__, "memories").parse_args() client = boto3.client("bedrock-agentcore-control", region_name=args.region) - memories = list_all_memories(client) - now = datetime.datetime.now(datetime.timezone.utc) - cutoff = now - datetime.timedelta(days=args.min_age_days) - - to_delete, skipped_protected, skipped_recent, skipped_nonmatch = [], [], [], [] - for m in memories: - mid = m.get("id") or m.get("memoryId") or "" - created = m.get("createdAt") - if is_protected(mid): - skipped_protected.append(mid) - elif not is_test_memory(mid): - skipped_nonmatch.append(mid) - elif created and created > cutoff: - skipped_recent.append(mid) - else: - to_delete.append(mid) - - print(f"Region: {args.region}") - print(f"Total memories: {len(memories)}") - print(f" protected (kept): {len(skipped_protected)}") - print(f" non-test (kept): {len(skipped_nonmatch)}") - print(f" too recent (kept): {len(skipped_recent)} (younger than {args.min_age_days}d)") - print(f" orphaned test (delete): {len(to_delete)}") - print() - - if not to_delete: - print("Nothing to delete.") - return 0 - - mode = "DELETING" if args.apply else "DRY RUN (would delete)" - print(f"=== {mode} {len(to_delete)} memories ===") - failures = 0 - for mid in to_delete: - if args.apply: - try: - client.delete_memory(memoryId=mid) - print(f" deleted: {mid}") - except Exception as e: # noqa: BLE001 - best-effort cleanup - failures += 1 - print(f" FAILED: {mid} ({e})") - else: - print(f" would delete: {mid}") - - if not args.apply: - print("\nDry run only. Re-run with --apply to delete.") - elif failures: - print(f"\nCompleted with {failures} failures.") - return 1 - else: - print(f"\nDeleted {len(to_delete)} orphaned memories.") - return 0 + return run_cleanup( + "memories", + list(paginate(client, "list_memories", "memories", maxResults=100)), + label_of=_id, + created_of=lambda m: m.get("createdAt"), + is_test=lambda m: _id(m).startswith(TEST_PREFIXES), + is_protected=lambda m: any(s in _id(m) for s in PROTECTED_SUBSTRINGS), + delete_one=lambda m: client.delete_memory(memoryId=_id(m)), + args=args, + ) if __name__ == "__main__": diff --git a/tests_integ/gateway/test_gateway_kb_targets.py b/tests_integ/gateway/test_gateway_kb_targets.py index dbf40dcb..ecc44d65 100644 --- a/tests_integ/gateway/test_gateway_kb_targets.py +++ b/tests_integ/gateway/test_gateway_kb_targets.py @@ -4,13 +4,6 @@ BEDROCK_TEST_REGION: AWS region (default: us-west-2) GATEWAY_ROLE_ARN: IAM role ARN with AgentCore gateway trust policy KB_ROLE_ARN: IAM role ARN with bedrock:InvokeModel permissions for the embedding model - -Note: - Gateway knowledge base targets only support MANAGED (fully-managed) knowledge - bases. The gateway control plane rejects the Retrieve connector for any other - knowledge base type with "Retrieve is not supported for this knowledge base - type." A MANAGED KB lets Bedrock own the vector store internally, so no - storageConfiguration (and no self-provisioned S3 Vectors index) is required. """ import os From 40011947559fa47b244ce0203a7c7fa3027a74a2 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Wed, 17 Jun 2026 21:54:04 +0000 Subject: [PATCH 6/6] style: apply ruff format to gateway cleanup script --- scripts/cleanup_orphaned_test_gateways.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/cleanup_orphaned_test_gateways.py b/scripts/cleanup_orphaned_test_gateways.py index 464ef7fd..5a7960b0 100755 --- a/scripts/cleanup_orphaned_test_gateways.py +++ b/scripts/cleanup_orphaned_test_gateways.py @@ -37,9 +37,12 @@ def delete_gateway(client, gateway_id, timeout_s=120, poll_s=5): The parent gateway cannot be deleted until its targets are fully removed. """ + def target_ids(): - return [t["targetId"] for t in paginate(client, "list_gateway_targets", "items", - gatewayIdentifier=gateway_id, maxResults=100)] + return [ + t["targetId"] + for t in paginate(client, "list_gateway_targets", "items", gatewayIdentifier=gateway_id, maxResults=100) + ] for tid in target_ids(): client.delete_gateway_target(gatewayIdentifier=gateway_id, targetId=tid)