From 7e5a8633b0dbd63488354dd54b58ac3675625839 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 17 Jun 2025 21:57:25 +0000 Subject: [PATCH 01/20] Add clustered scenario test Signed-off-by: jamshale --- .../examples/clustered/docker-compose.yml | 202 +++++++++++++ scenarios/examples/clustered/example.py | 285 ++++++++++++++++++ scenarios/examples/clustered/nginx.conf | 59 ++++ 3 files changed, 546 insertions(+) create mode 100644 scenarios/examples/clustered/docker-compose.yml create mode 100644 scenarios/examples/clustered/example.py create mode 100644 scenarios/examples/clustered/nginx.conf diff --git a/scenarios/examples/clustered/docker-compose.yml b/scenarios/examples/clustered/docker-compose.yml new file mode 100644 index 0000000000..30f69c1538 --- /dev/null +++ b/scenarios/examples/clustered/docker-compose.yml @@ -0,0 +1,202 @@ + + services: + nginx: + image: nginx:latest + container_name: test_nginx + ports: + - "8080:80" # Host:Container for Admin API + - "5000:5000" # Host:Container for didcomm + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - faber_0 + - faber_1 + - faber_2 + - alice + + faber_0: + image: acapy-test + ports: + - "4000:4000" + command: > + start + --label Faber + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://nginx:5000 + --admin 0.0.0.0 4000 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar-anoncreds + --wallet-name faber + --wallet-key insecure + --wallet-storage-type "postgres_storage" + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-provision + --log-level info + --debug-webhooks + --notify-revocation + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:4000/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + faber_1: + image: acapy-test + ports: + - "4001:4001" + command: > + start + --label Faber + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://nginx:5000 + --admin 0.0.0.0 4001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar-anoncreds + --wallet-name faber + --wallet-key insecure + --wallet-storage-type "postgres_storage" + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-provision + --log-level info + --debug-webhooks + --notify-revocation + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:4001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + # This prevents db access race conditions. + faber_0: + condition: service_healthy + faber_2: + image: acapy-test + ports: + - "4002:4002" + command: > + start + --label Faber + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://nginx:5000 + --admin 0.0.0.0 4002 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar-anoncreds + --wallet-name faber + --wallet-key insecure + --wallet-storage-type "postgres_storage" + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-provision + --log-level info + --debug-webhooks + --notify-revocation + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:4002/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + # This prevents db access race conditions. + faber_1: + condition: service_healthy + + alice: + image: acapy-test + ports: + - "6001:6001" + command: > + start + --label Alice + --inbound-transport http 0.0.0.0 6000 + --outbound-transport http + --endpoint http://alice:6000 + --admin 0.0.0.0 6001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar + --wallet-name alice + --wallet-key insecure + --auto-provision + --log-level info + --debug-webhooks + --monitor-revocation-notification + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:6001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + + example: + container_name: controller + build: + context: ../.. + environment: + - FABER_0=http://faber_0:4000 + - FABER_1=http://faber_1:4001 + - FABER_2=http://faber_2:4002 + - ALICE=http://alice:6001 + volumes: + - ./example.py:/usr/src/app/example.py:ro,z + command: python -m example + depends_on: + faber_0: + condition: service_healthy + faber_1: + condition: service_healthy + faber_2: + condition: service_healthy + alice: + condition: service_healthy + wallet-db: + condition: service_healthy + + tails: + image: ghcr.io/bcgov/tails-server:latest + ports: + - 6543:6543 + environment: + - GENESIS_URL=http://test.bcovrin.vonx.io/genesis + command: > + tails-server + --host 0.0.0.0 + --port 6543 + --storage-path /tmp/tails-files + --log-level info + + wallet-db: + image: postgres:12 + environment: + - POSTGRES_USER=DB_USER + - POSTGRES_PASSWORD=DB_PASSWORD + ports: + - 5433:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U DB_USER"] + interval: 10s + retries: 5 + start_period: 30s + timeout: 10s + + \ No newline at end of file diff --git a/scenarios/examples/clustered/example.py b/scenarios/examples/clustered/example.py new file mode 100644 index 0000000000..a27381f391 --- /dev/null +++ b/scenarios/examples/clustered/example.py @@ -0,0 +1,285 @@ +"""Example of issuing multiple credentials with anoncreds in a clustered environment.""" + +import asyncio +from os import getenv +from secrets import token_hex +from typing import Optional + +import aiohttp +from acapy_controller import Controller +from acapy_controller.logging import logging_to_stdout +from acapy_controller.protocols import ( + ConnRecord, + DIDResult, + InvitationMessage, + OobRecord, + oob_invitation, + params, +) +from aiohttp import ClientSession +from examples.util import ( + CredDefResultAnonCreds, + SchemaResultAnonCreds, + V20CredExRecord, +) + +CREDENTIALS_BASE_PATH = "/issue-credential-2.0" +REVOCATION_BASE_PATH = "/anoncreds" + +FABER = getenv("FABER", "http://nginx") +ALICE = getenv("ALICE", "http://alice:6001") + + +# Custom didexchange function without `completed` listener with to handle multiple +# instances. It may get received by another agent. +async def didexchange( + inviter: Controller, + invitee: Controller, + *, + invite: Optional[InvitationMessage] = None, + use_existing_connection: bool = False, +): + """Custom didexchange without the webhook listener for multiple instances.""" + if not invite: + invite = await oob_invitation(inviter) + + invitee_oob_record = await invitee.post( + "/out-of-band/receive-invitation", + json=invite, + params=params( + use_existing_connection=use_existing_connection, + ), + response=OobRecord, + ) + + if use_existing_connection and invitee_oob_record == "reuse-accepted": + inviter_oob_record = await inviter.event_with_values( + topic="out_of_band", + invi_msg_id=invite.id, + event_type=OobRecord, + ) + inviter_conn = await inviter.get( + f"/connections/{inviter_oob_record.connection_id}", + response=ConnRecord, + ) + invitee_conn = await invitee.get( + f"/connections/{invitee_oob_record.connection_id}", + response=ConnRecord, + ) + return inviter_conn, invitee_conn + + invitee_conn = await invitee.post( + f"/didexchange/{invitee_oob_record.connection_id}/accept-invitation", + response=ConnRecord, + ) + inviter_oob_record = await inviter.event_with_values( + topic="out_of_band", + invi_msg_id=invite.id, + state="done", + event_type=OobRecord, + ) + inviter_conn = await inviter.event_with_values( + topic="connections", + event_type=ConnRecord, + rfc23_state="request-received", + invitation_key=inviter_oob_record.our_recipient_key, + ) + await asyncio.sleep(1) + inviter_conn = await inviter.post( + f"/didexchange/{inviter_conn.connection_id}/accept-request", + response=ConnRecord, + ) + + await invitee.event_with_values( + topic="connections", + connection_id=invitee_conn.connection_id, + rfc23_state="response-received", + ) + + return inviter_conn, invitee_conn + + +async def check_unique_cred_rev_ids( + agent: Controller, credential_exchange_ids: list[str] +) -> None: + """Check that all credential revocation IDs are unique.""" + seen = [] + + for cred_ex_id in credential_exchange_ids: + result = ( + await agent.get( + f"{REVOCATION_BASE_PATH}/revocation/credential-record?cred_ex_id={cred_ex_id}" + ) + )["result"] + + cred_rev_id = int(result["cred_rev_id"]) + if cred_rev_id not in seen: + seen.append(cred_rev_id) + else: + raise AssertionError( + f"Duplicate cred_rev_id found: {cred_rev_id} for credential {cred_ex_id}" + ) + + print(f"Unique cred_rev_ids found: {len(seen)}") + seen.sort() + print(f"Credential revocation IDs: {seen}") + + +async def main(): + """Test Controller protocols.""" + async with Controller(base_url=ALICE) as alice, Controller(base_url=FABER) as faber: + # Connecting + alice_conn, faber_conn = await didexchange(faber, alice) + + # Issuance prep + config = (await alice.get("/status/config"))["config"] + genesis_url = config.get("ledger.genesis_url") + public_did = (await alice.get("/wallet/did/public", response=DIDResult)).result + if not public_did: + public_did = ( + await faber.post( + "/wallet/did/create", + json={"method": "sov", "options": {"key_type": "ed25519"}}, + response=DIDResult, + ) + ).result + assert public_did + + async with ClientSession() as session: + register_url = genesis_url.replace("/genesis", "/register") + async with session.post( + register_url, + json={ + "did": public_did.did, + "verkey": public_did.verkey, + "alias": None, + "role": "ENDORSER", + }, + ) as resp: + assert resp.ok + + await faber.post("/wallet/did/public", params=params(did=public_did.did)) + schema_name = "anoncreds-test-" + token_hex(8) + schema_version = "1.0" + schema = await faber.post( + "/anoncreds/schema", + json={ + "schema": { + "name": schema_name, + "version": schema_version, + "attrNames": ["middlename"], + "issuerId": public_did.did, + } + }, + response=SchemaResultAnonCreds, + ) + cred_def = await faber.post( + "/anoncreds/credential-definition", + json={ + "credential_definition": { + "issuerId": schema.schema_state["schema"]["issuerId"], + "schemaId": schema.schema_state["schema_id"], + "tag": token_hex(8), + }, + "options": {"support_revocation": True, "revocation_registry_size": 100}, + }, + response=CredDefResultAnonCreds, + ) + + num_creds = 10 # Number of credentials to issue concurrently + + # Create and send credential offers concurrently + faber_cred_ex_ids = [] + for i in range(num_creds): + issuer_cred_ex = await faber.post( + "/issue-credential-2.0/send-offer", + json={ + "auto_issue": True, + "auto_remove": False, + "comment": "Credential from minimal example", + "trace": False, + "connection_id": alice_conn.connection_id, + "filter": { + "anoncreds": { + "cred_def_id": cred_def.credential_definition_state[ + "credential_definition_id" + ], + "schema_name": schema_name, + "schema_version": schema_version, + } + }, + "credential_preview": { + "type": "issue-credential-2.0/2.0/credential-preview", # pyright: ignore + "attributes": [ + { + "name": "middlename", + "value": f"MiddleName-{i + 1}", + } + ], + }, + }, + response=V20CredExRecord, + ) + faber_cred_ex_ids.append(issuer_cred_ex.cred_ex_id) + + # Wait for all credentials to be received by Alice + num_tries = 0 + credentials_returned = {"results": []} + while ( + len(credentials_returned["results"]) != num_creds and num_tries < 20 + ): # Increased timeout for many creds + await asyncio.sleep(0.5) + credentials_returned = await alice.get(f"{CREDENTIALS_BASE_PATH}/records") + num_tries += 1 + + print(f"Number of credentials returned: {len(credentials_returned['results'])}") + + assert len(credentials_returned["results"]) == num_creds, ( + f"Expected {num_creds} credentials to be issued; only got {credentials_returned}" + ) + + # Accept all credentials concurrently using asyncio.gather() + request_tasks = [] + for cred in credentials_returned["results"]: + task = alice.post( + f"{CREDENTIALS_BASE_PATH}/records/{cred['cred_ex_record']['cred_ex_id']}/send-request", + json={}, + ) + request_tasks.append(task) + + # Execute all credential requests concurrently + await asyncio.gather(*request_tasks) + + # Wait for all credentials to be completed. + # This could be done more efficiently with a webhook listener, but + # is challenging with the current Controller library and multiple instances. + await asyncio.sleep(8) + async with aiohttp.ClientSession() as session: + seen = [] + + active_rev_reg = ( + await ( + await session.get( + f"http://nginx/anoncreds/revocation/active-registry/{cred_def.credential_definition_state['credential_definition_id']}" + ) + ).json() + )["result"]["revoc_reg_id"] + + results = await session.get( + f"http://nginx/anoncreds/revocation/registry/{active_rev_reg}/issued/details" + ) + + for entry in await results.json(): + if entry["cred_rev_id"] not in seen: + seen.append(entry["cred_rev_id"]) + else: + raise AssertionError( + f"Duplicate cred_rev_id found: {entry['cred_rev_id']}" + ) + + print(f"Credential revocation IDs created: {seen}") + + +if __name__ == "__main__": + logging_to_stdout() + asyncio.run(main()) diff --git a/scenarios/examples/clustered/nginx.conf b/scenarios/examples/clustered/nginx.conf new file mode 100644 index 0000000000..bd294840cc --- /dev/null +++ b/scenarios/examples/clustered/nginx.conf @@ -0,0 +1,59 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + upstream backend_cluster { + server faber_0:4000; + server faber_1:4001; + server faber_2:4002; + # You can add weights or max_fails if needed + # server 127.0.0.1:8001 weight=2; + } + + server { + listen 80; + + location / { + proxy_pass http://backend_cluster; + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Forward real IP and host headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + + upstream didcomm_cluster { + server faber_0:3000; + server faber_1:3000; + server faber_2:3000; + # You can add weights or max_fails if needed + # server 127.0.0.1:8001 weight=2; + } + + server { + listen 5000; + + location / { + proxy_pass http://didcomm_cluster; + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Forward real IP and host headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file From cb4e55f41b1d9e9764434cdc3250cc5f1ab61eb8 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 17 Jun 2025 21:59:53 +0000 Subject: [PATCH 02/20] Fix: transaction when reading and updating rev_list Signed-off-by: jamshale --- acapy_agent/anoncreds/revocation.py | 70 +++++++++++++++-------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/acapy_agent/anoncreds/revocation.py b/acapy_agent/anoncreds/revocation.py index 4a6f9f0d5c..f5f4aff282 100644 --- a/acapy_agent/anoncreds/revocation.py +++ b/acapy_agent/anoncreds/revocation.py @@ -993,46 +993,48 @@ def _has_required_id_and_tails_path(): rev_list = None if _has_required_id_and_tails_path(): - async with self.profile.session() as session: - rev_reg_def = await session.handle.fetch( - CATEGORY_REV_REG_DEF, rev_reg_def_id - ) - rev_list = await session.handle.fetch(CATEGORY_REV_LIST, rev_reg_def_id) - rev_key = await session.handle.fetch( + # We need to make sure the read, index increment, and write + # operations on the revocation list are atomic. + # This is done by using a transaction. + async with self.profile.transaction() as txn: + rev_reg_def = await txn.handle.fetch(CATEGORY_REV_REG_DEF, rev_reg_def_id) + rev_list = await txn.handle.fetch(CATEGORY_REV_LIST, rev_reg_def_id) + rev_key = await txn.handle.fetch( CATEGORY_REV_REG_DEF_PRIVATE, rev_reg_def_id ) - _handle_missing_entries(rev_list, rev_reg_def, rev_key) + _handle_missing_entries(rev_list, rev_reg_def, rev_key) - rev_list_value_json = rev_list.value_json - rev_list_tags = rev_list.tags + rev_list_value_json = rev_list.value_json + rev_list_tags = rev_list.tags - # If the rev_list state is failed then the tails file was never uploaded, - # try to upload it now and finish the revocation list - if rev_list_tags.get("state") == RevListState.STATE_FAILED: - await self.upload_tails_file( - RevRegDef.deserialize(rev_reg_def.value_json) - ) - rev_list_tags["state"] = RevListState.STATE_FINISHED - - rev_reg_index = rev_list_value_json["next_index"] - try: - rev_reg_def = RevocationRegistryDefinition.load(rev_reg_def.raw_value) - rev_list = RevocationStatusList.load(rev_list_value_json["rev_list"]) - except AnoncredsError as err: - raise AnonCredsRevocationError( - "Error loading revocation registry" - ) from err + # If the rev_list state is failed then the tails file was never uploaded, + # try to upload it now and finish the revocation list + if rev_list_tags.get("state") == RevListState.STATE_FAILED: + await self.upload_tails_file( + RevRegDef.deserialize(rev_reg_def.value_json) + ) + rev_list_tags["state"] = RevListState.STATE_FINISHED - # NOTE: we increment the index ahead of time to keep the - # transaction short. The revocation registry itself will NOT - # be updated because we always use ISSUANCE_BY_DEFAULT. - # If something goes wrong later, the index will be skipped. - # FIXME - double check issuance type in case of upgraded wallet? - if rev_reg_index > rev_reg_def.max_cred_num: - raise AnonCredsRevocationRegistryFullError("Revocation registry is full") - rev_list_value_json["next_index"] = rev_reg_index + 1 - async with self.profile.transaction() as txn: + rev_reg_index = rev_list_value_json["next_index"] + try: + rev_reg_def = RevocationRegistryDefinition.load(rev_reg_def.raw_value) + rev_list = RevocationStatusList.load(rev_list_value_json["rev_list"]) + except AnoncredsError as err: + raise AnonCredsRevocationError( + "Error loading revocation registry" + ) from err + + # NOTE: we increment the index ahead of time to keep the + # transaction short. The revocation registry itself will NOT + # be updated because we always use ISSUANCE_BY_DEFAULT. + # If something goes wrong later, the index will be skipped. + # FIXME - double check issuance type in case of upgraded wallet? + if rev_reg_index > rev_reg_def.max_cred_num: + raise AnonCredsRevocationRegistryFullError( + "Revocation registry is full" + ) + rev_list_value_json["next_index"] = rev_reg_index + 1 await txn.handle.replace( CATEGORY_REV_LIST, rev_reg_def_id, From 170f9537b97e1fd10ce92306773eb27b3ab86645 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 17 Jun 2025 23:11:43 +0000 Subject: [PATCH 03/20] Tiny sonarcloud linting fix Signed-off-by: jamshale --- scenarios/examples/clustered/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/examples/clustered/example.py b/scenarios/examples/clustered/example.py index a27381f391..de38eb3366 100644 --- a/scenarios/examples/clustered/example.py +++ b/scenarios/examples/clustered/example.py @@ -129,7 +129,7 @@ async def main(): """Test Controller protocols.""" async with Controller(base_url=ALICE) as alice, Controller(base_url=FABER) as faber: # Connecting - alice_conn, faber_conn = await didexchange(faber, alice) + alice_conn, _ = await didexchange(faber, alice) # Issuance prep config = (await alice.get("/status/config"))["config"] From 6ab47a552a81bebf3855d368c9eb0bfeaa5efce2 Mon Sep 17 00:00:00 2001 From: jamshale Date: Wed, 18 Jun 2025 17:06:40 +0000 Subject: [PATCH 04/20] Change clustered scenario test to use replicas Signed-off-by: jamshale --- .../examples/clustered/docker-compose.yml | 97 ++----------------- scenarios/examples/clustered/nginx.conf | 8 +- 2 files changed, 12 insertions(+), 93 deletions(-) diff --git a/scenarios/examples/clustered/docker-compose.yml b/scenarios/examples/clustered/docker-compose.yml index 30f69c1538..a2653bcc53 100644 --- a/scenarios/examples/clustered/docker-compose.yml +++ b/scenarios/examples/clustered/docker-compose.yml @@ -9,15 +9,16 @@ volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - - faber_0 - - faber_1 - - faber_2 + - faber - alice - faber_0: + faber: image: acapy-test ports: - - "4000:4000" + - "4000" + deploy: + replicas: 3 + restart: unless-stopped # Due to askar store race condition. Can remove if that's fixed. command: > start --label Faber @@ -39,7 +40,7 @@ --debug-webhooks --notify-revocation healthcheck: - test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:4000/status/live" | grep "200" > /dev/null + test: curl -s -o /dev/null -w '%{http_code}' "http://nginx/status/live" | grep "200" > /dev/null start_period: 30s interval: 7s timeout: 5s @@ -47,78 +48,6 @@ depends_on: tails: condition: service_started - faber_1: - image: acapy-test - ports: - - "4001:4001" - command: > - start - --label Faber - --inbound-transport http 0.0.0.0 3000 - --outbound-transport http - --endpoint http://nginx:5000 - --admin 0.0.0.0 4001 - --admin-insecure-mode - --tails-server-base-url http://tails:6543 - --genesis-url http://test.bcovrin.vonx.io/genesis - --wallet-type askar-anoncreds - --wallet-name faber - --wallet-key insecure - --wallet-storage-type "postgres_storage" - --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" - --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" - --auto-provision - --log-level info - --debug-webhooks - --notify-revocation - healthcheck: - test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:4001/status/live" | grep "200" > /dev/null - start_period: 30s - interval: 7s - timeout: 5s - retries: 5 - depends_on: - tails: - condition: service_started - # This prevents db access race conditions. - faber_0: - condition: service_healthy - faber_2: - image: acapy-test - ports: - - "4002:4002" - command: > - start - --label Faber - --inbound-transport http 0.0.0.0 3000 - --outbound-transport http - --endpoint http://nginx:5000 - --admin 0.0.0.0 4002 - --admin-insecure-mode - --tails-server-base-url http://tails:6543 - --genesis-url http://test.bcovrin.vonx.io/genesis - --wallet-type askar-anoncreds - --wallet-name faber - --wallet-key insecure - --wallet-storage-type "postgres_storage" - --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" - --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" - --auto-provision - --log-level info - --debug-webhooks - --notify-revocation - healthcheck: - test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:4002/status/live" | grep "200" > /dev/null - start_period: 30s - interval: 7s - timeout: 5s - retries: 5 - depends_on: - tails: - condition: service_started - # This prevents db access race conditions. - faber_1: - condition: service_healthy alice: image: acapy-test @@ -142,7 +71,7 @@ --debug-webhooks --monitor-revocation-notification healthcheck: - test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:6001/status/live" | grep "200" > /dev/null + test: curl -s -o /dev/null -w '%{http_code}' "http://alice:6001/status/live" | grep "200" > /dev/null start_period: 30s interval: 7s timeout: 5s @@ -153,19 +82,13 @@ build: context: ../.. environment: - - FABER_0=http://faber_0:4000 - - FABER_1=http://faber_1:4001 - - FABER_2=http://faber_2:4002 + - FABER=http://nginx - ALICE=http://alice:6001 volumes: - ./example.py:/usr/src/app/example.py:ro,z command: python -m example depends_on: - faber_0: - condition: service_healthy - faber_1: - condition: service_healthy - faber_2: + faber: condition: service_healthy alice: condition: service_healthy diff --git a/scenarios/examples/clustered/nginx.conf b/scenarios/examples/clustered/nginx.conf index bd294840cc..70b1210deb 100644 --- a/scenarios/examples/clustered/nginx.conf +++ b/scenarios/examples/clustered/nginx.conf @@ -6,9 +6,7 @@ events { http { upstream backend_cluster { - server faber_0:4000; - server faber_1:4001; - server faber_2:4002; + server faber:4000; # You can add weights or max_fails if needed # server 127.0.0.1:8001 weight=2; } @@ -32,9 +30,7 @@ http { } upstream didcomm_cluster { - server faber_0:3000; - server faber_1:3000; - server faber_2:3000; + server faber:3000; # You can add weights or max_fails if needed # server 127.0.0.1:8001 weight=2; } From c527b1a48b233c7103c56735dc750b8597504283 Mon Sep 17 00:00:00 2001 From: jamshale Date: Mon, 23 Jun 2025 21:15:02 +0000 Subject: [PATCH 05/20] Repair clustered scneario test Signed-off-by: jamshale --- .../examples/clustered/docker-compose.yml | 6 +- scenarios/examples/clustered/example.py | 100 +++++------------- 2 files changed, 29 insertions(+), 77 deletions(-) diff --git a/scenarios/examples/clustered/docker-compose.yml b/scenarios/examples/clustered/docker-compose.yml index a2653bcc53..235894b4a0 100644 --- a/scenarios/examples/clustered/docker-compose.yml +++ b/scenarios/examples/clustered/docker-compose.yml @@ -35,12 +35,14 @@ --wallet-storage-type "postgres_storage" --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-accept-invites + --auto-accept-requests --auto-provision --log-level info --debug-webhooks --notify-revocation healthcheck: - test: curl -s -o /dev/null -w '%{http_code}' "http://nginx/status/live" | grep "200" > /dev/null + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:4000/status/live" | grep "200" > /dev/null start_period: 30s interval: 7s timeout: 5s @@ -67,6 +69,8 @@ --wallet-name alice --wallet-key insecure --auto-provision + --auto-accept-invites + --auto-accept-requests --log-level info --debug-webhooks --monitor-revocation-notification diff --git a/scenarios/examples/clustered/example.py b/scenarios/examples/clustered/example.py index de38eb3366..c34e535045 100644 --- a/scenarios/examples/clustered/example.py +++ b/scenarios/examples/clustered/example.py @@ -3,17 +3,14 @@ import asyncio from os import getenv from secrets import token_hex -from typing import Optional import aiohttp from acapy_controller import Controller from acapy_controller.logging import logging_to_stdout from acapy_controller.protocols import ( - ConnRecord, DIDResult, - InvitationMessage, + InvitationRecord, OobRecord, - oob_invitation, params, ) from aiohttp import ClientSession @@ -30,75 +27,6 @@ ALICE = getenv("ALICE", "http://alice:6001") -# Custom didexchange function without `completed` listener with to handle multiple -# instances. It may get received by another agent. -async def didexchange( - inviter: Controller, - invitee: Controller, - *, - invite: Optional[InvitationMessage] = None, - use_existing_connection: bool = False, -): - """Custom didexchange without the webhook listener for multiple instances.""" - if not invite: - invite = await oob_invitation(inviter) - - invitee_oob_record = await invitee.post( - "/out-of-band/receive-invitation", - json=invite, - params=params( - use_existing_connection=use_existing_connection, - ), - response=OobRecord, - ) - - if use_existing_connection and invitee_oob_record == "reuse-accepted": - inviter_oob_record = await inviter.event_with_values( - topic="out_of_band", - invi_msg_id=invite.id, - event_type=OobRecord, - ) - inviter_conn = await inviter.get( - f"/connections/{inviter_oob_record.connection_id}", - response=ConnRecord, - ) - invitee_conn = await invitee.get( - f"/connections/{invitee_oob_record.connection_id}", - response=ConnRecord, - ) - return inviter_conn, invitee_conn - - invitee_conn = await invitee.post( - f"/didexchange/{invitee_oob_record.connection_id}/accept-invitation", - response=ConnRecord, - ) - inviter_oob_record = await inviter.event_with_values( - topic="out_of_band", - invi_msg_id=invite.id, - state="done", - event_type=OobRecord, - ) - inviter_conn = await inviter.event_with_values( - topic="connections", - event_type=ConnRecord, - rfc23_state="request-received", - invitation_key=inviter_oob_record.our_recipient_key, - ) - await asyncio.sleep(1) - inviter_conn = await inviter.post( - f"/didexchange/{inviter_conn.connection_id}/accept-request", - response=ConnRecord, - ) - - await invitee.event_with_values( - topic="connections", - connection_id=invitee_conn.connection_id, - rfc23_state="response-received", - ) - - return inviter_conn, invitee_conn - - async def check_unique_cred_rev_ids( agent: Controller, credential_exchange_ids: list[str] ) -> None: @@ -129,7 +57,27 @@ async def main(): """Test Controller protocols.""" async with Controller(base_url=ALICE) as alice, Controller(base_url=FABER) as faber: # Connecting - alice_conn, _ = await didexchange(faber, alice) + + invite_record = await faber.post( + "/out-of-band/create-invitation", + json={ + "handshake_protocols": ["https://didcomm.org/didexchange/1.1"], + }, + params=params( + auto_accept=True, + ), + response=InvitationRecord, + ) + + await alice.post( + "/out-of-band/receive-invitation", + json=invite_record.invitation, + response=OobRecord, + ) + + await asyncio.sleep(1) # Wait for the invitation to be processed + + alice_conn = (await faber.get("connections"))["results"][0] # Issuance prep config = (await alice.get("/status/config"))["config"] @@ -198,7 +146,7 @@ async def main(): "auto_remove": False, "comment": "Credential from minimal example", "trace": False, - "connection_id": alice_conn.connection_id, + "connection_id": alice_conn["connection_id"], "filter": { "anoncreds": { "cred_def_id": cred_def.credential_definition_state[ @@ -253,7 +201,7 @@ async def main(): # Wait for all credentials to be completed. # This could be done more efficiently with a webhook listener, but # is challenging with the current Controller library and multiple instances. - await asyncio.sleep(8) + await asyncio.sleep(3) async with aiohttp.ClientSession() as session: seen = [] From 2e3bf51ee347d2f76545ff248d54aaced7ae53be Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 16:28:00 +0000 Subject: [PATCH 06/20] Test: Try adding health check and more depends_on Signed-off-by: jamshale --- scenarios/examples/clustered/docker-compose.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scenarios/examples/clustered/docker-compose.yml b/scenarios/examples/clustered/docker-compose.yml index 235894b4a0..4885df674c 100644 --- a/scenarios/examples/clustered/docker-compose.yml +++ b/scenarios/examples/clustered/docker-compose.yml @@ -11,6 +11,12 @@ depends_on: - faber - alice + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s faber: image: acapy-test @@ -50,6 +56,8 @@ depends_on: tails: condition: service_started + wallet-db: + condition: service_healthy alice: image: acapy-test @@ -80,6 +88,11 @@ interval: 7s timeout: 5s retries: 5 + depends_on: + tails: + condition: service_started + wallet-db: + condition: service_healthy example: container_name: controller @@ -98,6 +111,8 @@ condition: service_healthy wallet-db: condition: service_healthy + nginx: + condition: service_started tails: image: ghcr.io/bcgov/tails-server:latest From 6e304834c6bd646b6a768b26c9c2d5107f1e5e1a Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 18:05:03 +0000 Subject: [PATCH 07/20] Switch repo to jamshale Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index 76545be8c1..4002edda0a 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -26,7 +26,7 @@ jobs: test: runs-on: ubuntu-latest # Run on openwallet-foundation and non-draft PRs or on non-PR events - if: (github.repository == 'openwallet-foundation/acapy') && ((github.event_name == 'pull_request' && github.event.pull_request.draft == false) || (github.event_name != 'pull_request')) + if: (github.repository == 'jamshale/acapy') && ((github.event_name == 'pull_request' && github.event.pull_request.draft == false) || (github.event_name != 'pull_request')) steps: - name: checkout-acapy uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From f7c9f115bbf995cf792c250504f6491d1b6aea0c Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 18:19:19 +0000 Subject: [PATCH 08/20] Don't run from pytest Signed-off-by: jamshale --- .../workflows/scenario-integration-tests.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index 4002edda0a..a7cc03ff69 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -66,3 +66,21 @@ jobs: cd scenarios poetry install --no-root poetry run pytest -m examples + run: | + set -euo pipefail + + for examples_file in $(find scenarios -type f -name 'examples'); do + examples_dir=$(dirname "$examples_file") + echo -e "\nšŸ”Ž Running scenario in: $examples_dir" + cd "$examples_dir" + docker compose build + if ! docker compose up --exit-code-from tests; then + echo "āŒ Tests failed for $dir - dumping logs:" + docker compose logs + docker compose down --remove-orphans + exit 1 + fi + echo "āœ… Tests passed for $examples_dir" + docker compose down --remove-orphans + cd - > /dev/null + done From af6187fc160b3262d2be8b07798a877e22d4b847 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 18:20:08 +0000 Subject: [PATCH 09/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index a7cc03ff69..705d1723ed 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -60,12 +60,6 @@ jobs: cache: "poetry" - name: Run Scenario Tests if: steps.check-if-scenarios-or-src-changed.outputs.run_tests != 'false' - run: | - # Build the docker image for testing - docker build -t acapy-test -f docker/Dockerfile.run . - cd scenarios - poetry install --no-root - poetry run pytest -m examples run: | set -euo pipefail From 0d9c5211061c132eb7b5ea336a847945f65dec31 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 18:23:18 +0000 Subject: [PATCH 10/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index 705d1723ed..200b81da73 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -63,12 +63,12 @@ jobs: run: | set -euo pipefail - for examples_file in $(find scenarios -type f -name 'examples'); do + for examples_file in $(find examples -type f -name 'example.py'); do examples_dir=$(dirname "$examples_file") echo -e "\nšŸ”Ž Running scenario in: $examples_dir" cd "$examples_dir" docker compose build - if ! docker compose up --exit-code-from tests; then + if ! docker compose up --exit-code-from example; then echo "āŒ Tests failed for $dir - dumping logs:" docker compose logs docker compose down --remove-orphans From a7ca134ac09b1ff2941e0a00add9e609ad202a25 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 18:27:35 +0000 Subject: [PATCH 11/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index 200b81da73..e5e9660b13 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -63,6 +63,8 @@ jobs: run: | set -euo pipefail + cd scenarios + for examples_file in $(find examples -type f -name 'example.py'); do examples_dir=$(dirname "$examples_file") echo -e "\nšŸ”Ž Running scenario in: $examples_dir" From 179afe32dceb1e63bcc4e7820cf887f727f35f97 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 18:33:50 +0000 Subject: [PATCH 12/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index e5e9660b13..e161b4d737 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -63,6 +63,8 @@ jobs: run: | set -euo pipefail + docker build -t acapy-test -f docker/Dockerfile.run . + cd scenarios for examples_file in $(find examples -type f -name 'example.py'); do From 2517883154ffb1ebbefadda5409828e96a6dfbef Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 19:26:23 +0000 Subject: [PATCH 13/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index e161b4d737..a051be386e 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -72,7 +72,7 @@ jobs: echo -e "\nšŸ”Ž Running scenario in: $examples_dir" cd "$examples_dir" docker compose build - if ! docker compose up --exit-code-from example; then + if ! docker compose up --abort-on-container-exit --exit-code-from example; then echo "āŒ Tests failed for $dir - dumping logs:" docker compose logs docker compose down --remove-orphans From d2faf22cc7c2c9ea72d36d9631fae5fbb9434747 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 19:28:34 +0000 Subject: [PATCH 14/20] Don't run from pytest Signed-off-by: jamshale --- scenarios/examples/connectionless/docker-compose.yml | 4 ++-- scenarios/examples/json_ld/docker-compose.yml | 4 ++-- scenarios/examples/mediation/docker-compose.yml | 6 +++--- scenarios/examples/multitenancy/docker-compose.yml | 2 +- .../examples/multiuse_invitations/docker-compose.yml | 4 ++-- .../presenting_revoked_credential/docker-compose.yml | 4 ++-- .../examples/restart_anoncreds_upgrade/docker-compose.yml | 8 ++++---- scenarios/examples/self_attested/docker-compose.yml | 4 ++-- scenarios/examples/simple/docker-compose.yml | 4 ++-- scenarios/examples/simple_restart/docker-compose.yml | 4 ++-- scenarios/examples/vc_holder/docker-compose.yml | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/scenarios/examples/connectionless/docker-compose.yml b/scenarios/examples/connectionless/docker-compose.yml index 7bc9e0852a..bcf023edd2 100644 --- a/scenarios/examples/connectionless/docker-compose.yml +++ b/scenarios/examples/connectionless/docker-compose.yml @@ -19,7 +19,7 @@ --wallet-name alice --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks healthcheck: test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null @@ -51,7 +51,7 @@ --wallet-name bob --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification healthcheck: diff --git a/scenarios/examples/json_ld/docker-compose.yml b/scenarios/examples/json_ld/docker-compose.yml index b4a14e6b73..accaa190d4 100644 --- a/scenarios/examples/json_ld/docker-compose.yml +++ b/scenarios/examples/json_ld/docker-compose.yml @@ -9,7 +9,7 @@ -ot http -e http://alice:3000 --admin 0.0.0.0 3001 --admin-insecure-mode - --log-level debug + --log-level info --genesis-url http://test.bcovrin.vonx.io/genesis --wallet-type askar --wallet-name alice @@ -34,7 +34,7 @@ -ot http -e http://bob:3000 --admin 0.0.0.0 3001 --admin-insecure-mode - --log-level debug + --log-level info --genesis-url http://test.bcovrin.vonx.io/genesis --wallet-type askar --wallet-name bob diff --git a/scenarios/examples/mediation/docker-compose.yml b/scenarios/examples/mediation/docker-compose.yml index b6e9dbebbb..c5e608523e 100644 --- a/scenarios/examples/mediation/docker-compose.yml +++ b/scenarios/examples/mediation/docker-compose.yml @@ -18,7 +18,7 @@ --wallet-name alice --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks healthcheck: test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null @@ -46,7 +46,7 @@ --wallet-name bob --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification healthcheck: @@ -75,7 +75,7 @@ --wallet-name mediator --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --enable-undelivered-queue healthcheck: diff --git a/scenarios/examples/multitenancy/docker-compose.yml b/scenarios/examples/multitenancy/docker-compose.yml index 040771c60a..494ede8573 100644 --- a/scenarios/examples/multitenancy/docker-compose.yml +++ b/scenarios/examples/multitenancy/docker-compose.yml @@ -21,7 +21,7 @@ --multitenant-admin --jwt-secret insecure --multitenancy-config wallet_type=single-wallet-askar key_derivation_method=RAW - --log-level debug + --log-level info --debug-webhooks healthcheck: test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null diff --git a/scenarios/examples/multiuse_invitations/docker-compose.yml b/scenarios/examples/multiuse_invitations/docker-compose.yml index 7bc9e0852a..bcf023edd2 100644 --- a/scenarios/examples/multiuse_invitations/docker-compose.yml +++ b/scenarios/examples/multiuse_invitations/docker-compose.yml @@ -19,7 +19,7 @@ --wallet-name alice --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks healthcheck: test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null @@ -51,7 +51,7 @@ --wallet-name bob --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification healthcheck: diff --git a/scenarios/examples/presenting_revoked_credential/docker-compose.yml b/scenarios/examples/presenting_revoked_credential/docker-compose.yml index 221ec26300..67e4424893 100644 --- a/scenarios/examples/presenting_revoked_credential/docker-compose.yml +++ b/scenarios/examples/presenting_revoked_credential/docker-compose.yml @@ -17,7 +17,7 @@ --wallet-name alice --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --notify-revocation healthcheck: @@ -48,7 +48,7 @@ --wallet-name bob --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification healthcheck: diff --git a/scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml b/scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml index caddadc8be..6b15329d8d 100644 --- a/scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml +++ b/scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml @@ -36,7 +36,7 @@ services: --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" --auto-provision - --log-level debug + --log-level info --debug-webhooks --notify-revocation --preserve-exchange-records @@ -75,7 +75,7 @@ services: --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification --preserve-exchange-records @@ -114,7 +114,7 @@ services: --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification --preserve-exchange-records @@ -153,7 +153,7 @@ services: --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification --preserve-exchange-records diff --git a/scenarios/examples/self_attested/docker-compose.yml b/scenarios/examples/self_attested/docker-compose.yml index b66e9268e8..6c63a3be2b 100644 --- a/scenarios/examples/self_attested/docker-compose.yml +++ b/scenarios/examples/self_attested/docker-compose.yml @@ -17,7 +17,7 @@ --wallet-name alice --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks healthcheck: test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null @@ -47,7 +47,7 @@ --wallet-name bob --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification healthcheck: diff --git a/scenarios/examples/simple/docker-compose.yml b/scenarios/examples/simple/docker-compose.yml index 199f3d8957..aa62289d26 100644 --- a/scenarios/examples/simple/docker-compose.yml +++ b/scenarios/examples/simple/docker-compose.yml @@ -19,7 +19,7 @@ --wallet-name alice --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --universal-resolver healthcheck: @@ -52,7 +52,7 @@ --wallet-name bob --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification healthcheck: diff --git a/scenarios/examples/simple_restart/docker-compose.yml b/scenarios/examples/simple_restart/docker-compose.yml index 59a7ed2d28..14555fbb72 100644 --- a/scenarios/examples/simple_restart/docker-compose.yml +++ b/scenarios/examples/simple_restart/docker-compose.yml @@ -36,7 +36,7 @@ services: --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" --auto-provision - --log-level debug + --log-level info --debug-webhooks --preserve-exchange-records healthcheck: @@ -74,7 +74,7 @@ services: --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" --auto-provision - --log-level debug + --log-level info --debug-webhooks --monitor-revocation-notification --preserve-exchange-records diff --git a/scenarios/examples/vc_holder/docker-compose.yml b/scenarios/examples/vc_holder/docker-compose.yml index ac9d0ef7a4..f0fd81e542 100644 --- a/scenarios/examples/vc_holder/docker-compose.yml +++ b/scenarios/examples/vc_holder/docker-compose.yml @@ -18,7 +18,7 @@ --wallet-name alice --wallet-key insecure --auto-provision - --log-level debug + --log-level info --debug-webhooks --multitenant --multitenant-admin From 52ca55eead433fff183470295695c35248a91d01 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 19:43:28 +0000 Subject: [PATCH 15/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index a051be386e..bda79a0e75 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -71,14 +71,19 @@ jobs: examples_dir=$(dirname "$examples_file") echo -e "\nšŸ”Ž Running scenario in: $examples_dir" cd "$examples_dir" + docker compose build - if ! docker compose up --abort-on-container-exit --exit-code-from example; then - echo "āŒ Tests failed for $dir - dumping logs:" + docker compose up -d + + echo "ā³ Waiting for example to finish..." + if ! docker compose run --rm example; then + echo "āŒ Example failed. Dumping logs..." docker compose logs - docker compose down --remove-orphans + docker compose down --remove-orphans --timeout 5 exit 1 fi - echo "āœ… Tests passed for $examples_dir" - docker compose down --remove-orphans + + docker compose down --remove-orphans --timeout 5 + echo "āœ… Scenario passed" cd - > /dev/null done From 30d94230e83a57e9340d478440b3ff32756470af Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 19:47:05 +0000 Subject: [PATCH 16/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index bda79a0e75..d3ae43bbb0 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -76,7 +76,7 @@ jobs: docker compose up -d echo "ā³ Waiting for example to finish..." - if ! docker compose run --rm example; then + if ! docker compose run --rm controller; then echo "āŒ Example failed. Dumping logs..." docker compose logs docker compose down --remove-orphans --timeout 5 From 2cefb89922bf2817f41d247cf0f9e14b225c7da4 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 19:50:29 +0000 Subject: [PATCH 17/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index d3ae43bbb0..8368154005 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -71,19 +71,14 @@ jobs: examples_dir=$(dirname "$examples_file") echo -e "\nšŸ”Ž Running scenario in: $examples_dir" cd "$examples_dir" - docker compose build - docker compose up -d - - echo "ā³ Waiting for example to finish..." - if ! docker compose run --rm controller; then - echo "āŒ Example failed. Dumping logs..." + if ! docker compose up --exit-code-from controller; then + echo "āŒ Tests failed for $dir - dumping logs:" docker compose logs - docker compose down --remove-orphans --timeout 5 + docker compose down --remove-orphans exit 1 fi - - docker compose down --remove-orphans --timeout 5 - echo "āœ… Scenario passed" + echo "āœ… Tests passed for $examples_dir" + docker compose down --remove-orphans cd - > /dev/null done From 54e3ca63b0976f0e826ecf65efad7c3e1856515d Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 20:04:52 +0000 Subject: [PATCH 18/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 3 ++- .../anoncreds_issuance_and_revocation/example.py | 8 +++++++- scenarios/examples/clustered/example.py | 8 +++++++- scenarios/examples/connectionless/example.py | 8 +++++++- .../examples/did_indy_issuance_and_revocation/example.py | 8 +++++++- scenarios/examples/json_ld/example.py | 9 ++++++++- scenarios/examples/mediation/example.py | 9 ++++++++- scenarios/examples/multitenancy/example.py | 8 +++++++- scenarios/examples/multiuse_invitations/example.py | 8 +++++++- .../examples/presenting_revoked_credential/example.py | 8 +++++++- scenarios/examples/restart_anoncreds_upgrade/example.py | 8 +++++++- scenarios/examples/simple/example.py | 8 +++++++- scenarios/examples/simple_restart/example.py | 8 +++++++- scenarios/examples/vc_holder/example.py | 8 +++++++- 14 files changed, 95 insertions(+), 14 deletions(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index 8368154005..350852e073 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -72,7 +72,8 @@ jobs: echo -e "\nšŸ”Ž Running scenario in: $examples_dir" cd "$examples_dir" docker compose build - if ! docker compose up --exit-code-from controller; then + docker compose up -d + if ! docker compose run --rm example; then echo "āŒ Tests failed for $dir - dumping logs:" docker compose logs docker compose down --remove-orphans diff --git a/scenarios/examples/anoncreds_issuance_and_revocation/example.py b/scenarios/examples/anoncreds_issuance_and_revocation/example.py index a9f7c6b4e7..f37b4efc75 100644 --- a/scenarios/examples/anoncreds_issuance_and_revocation/example.py +++ b/scenarios/examples/anoncreds_issuance_and_revocation/example.py @@ -4,6 +4,7 @@ """ import asyncio +import sys from datetime import datetime from os import getenv from secrets import token_hex @@ -551,8 +552,13 @@ async def main(): ) await holder_anoncreds.record(topic="revocation-notification") + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/clustered/example.py b/scenarios/examples/clustered/example.py index c34e535045..943c20ba07 100644 --- a/scenarios/examples/clustered/example.py +++ b/scenarios/examples/clustered/example.py @@ -1,6 +1,7 @@ """Example of issuing multiple credentials with anoncreds in a clustered environment.""" import asyncio +import sys from os import getenv from secrets import token_hex @@ -226,8 +227,13 @@ async def main(): ) print(f"Credential revocation IDs created: {seen}") + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/connectionless/example.py b/scenarios/examples/connectionless/example.py index 7c0d867461..ea6dfac3c7 100644 --- a/scenarios/examples/connectionless/example.py +++ b/scenarios/examples/connectionless/example.py @@ -4,6 +4,7 @@ """ import asyncio +import sys from dataclasses import dataclass from os import getenv @@ -284,6 +285,7 @@ async def icv1(): credential_exchange_id=bob_cred_ex_id, state="credential_acked", ) + sys.exit(0) async def main(): @@ -294,4 +296,8 @@ async def main(): if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/did_indy_issuance_and_revocation/example.py b/scenarios/examples/did_indy_issuance_and_revocation/example.py index 407de5e2a5..55c2004f20 100644 --- a/scenarios/examples/did_indy_issuance_and_revocation/example.py +++ b/scenarios/examples/did_indy_issuance_and_revocation/example.py @@ -5,6 +5,7 @@ import asyncio import json +import sys from dataclasses import dataclass from os import getenv @@ -115,8 +116,13 @@ async def main(): ) await indy_anoncreds_publish_revocation(alice, cred_ex=alice_cred_ex) await bob.record(topic="revocation-notification") + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/json_ld/example.py b/scenarios/examples/json_ld/example.py index 1629335fc0..89172e0604 100644 --- a/scenarios/examples/json_ld/example.py +++ b/scenarios/examples/json_ld/example.py @@ -5,6 +5,7 @@ import asyncio import json +import sys from datetime import date from os import getenv from uuid import uuid4 @@ -449,7 +450,13 @@ async def main(): with section("Presentation summary", character="-"): print(presentation_summary(alice_pres_ex.into(V20PresExRecord))) + sys.exit(0) + if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/mediation/example.py b/scenarios/examples/mediation/example.py index 3de69ad703..c005940e05 100644 --- a/scenarios/examples/mediation/example.py +++ b/scenarios/examples/mediation/example.py @@ -4,6 +4,7 @@ """ import asyncio +import sys from os import getenv from acapy_controller import Controller @@ -33,7 +34,13 @@ async def main(): ab, ba = await didexchange(alice, bob) await trustping(alice, ab) + sys.exit(0) + if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/multitenancy/example.py b/scenarios/examples/multitenancy/example.py index 2d8b2529c9..41cc21c07f 100644 --- a/scenarios/examples/multitenancy/example.py +++ b/scenarios/examples/multitenancy/example.py @@ -4,6 +4,7 @@ """ import asyncio +import sys from os import getenv from acapy_controller import Controller @@ -105,8 +106,13 @@ async def main(): alice_conn.connection_id, requested_attributes=[{"name": "firstname"}], ) + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/multiuse_invitations/example.py b/scenarios/examples/multiuse_invitations/example.py index 40a25536ae..a3fe413669 100644 --- a/scenarios/examples/multiuse_invitations/example.py +++ b/scenarios/examples/multiuse_invitations/example.py @@ -4,6 +4,7 @@ """ import asyncio +import sys from os import getenv from acapy_controller import Controller @@ -27,8 +28,13 @@ async def main(): a2 = a2.serialize() assert a2["invitation_msg_id"] assert a1["invitation_msg_id"] == a2["invitation_msg_id"] + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/presenting_revoked_credential/example.py b/scenarios/examples/presenting_revoked_credential/example.py index 2e3cdd80b4..240edfe3e1 100644 --- a/scenarios/examples/presenting_revoked_credential/example.py +++ b/scenarios/examples/presenting_revoked_credential/example.py @@ -5,6 +5,7 @@ import asyncio import json +import sys import time from os import getenv @@ -191,8 +192,13 @@ async def main(): # Presentation summary for i, pres in enumerate(presentations.results): print(summary(pres)) + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/restart_anoncreds_upgrade/example.py b/scenarios/examples/restart_anoncreds_upgrade/example.py index cc272c1d11..98a7dc506f 100644 --- a/scenarios/examples/restart_anoncreds_upgrade/example.py +++ b/scenarios/examples/restart_anoncreds_upgrade/example.py @@ -5,6 +5,7 @@ import asyncio import json +import sys from os import getenv from acapy_controller import Controller @@ -493,8 +494,13 @@ async def main(): if bob_id and new_bob_container: # cleanup - shut down bob agent (not part of docker compose) stop_and_remove_container(client, bob_id) + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/simple/example.py b/scenarios/examples/simple/example.py index a41994e569..226db8468c 100644 --- a/scenarios/examples/simple/example.py +++ b/scenarios/examples/simple/example.py @@ -4,6 +4,7 @@ """ import asyncio +import sys from os import getenv from acapy_controller import Controller @@ -18,8 +19,13 @@ async def main(): """Test Controller protocols.""" async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB) as bob: await didexchange(alice, bob) + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/simple_restart/example.py b/scenarios/examples/simple_restart/example.py index 3727568da9..aee18e6df5 100644 --- a/scenarios/examples/simple_restart/example.py +++ b/scenarios/examples/simple_restart/example.py @@ -4,6 +4,7 @@ """ import asyncio +import sys from os import getenv from acapy_controller import Controller @@ -248,8 +249,13 @@ async def main(): agency_container.stop() wait_until_healthy(client, agency_id, is_healthy=False) agency_container.remove() + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scenarios/examples/vc_holder/example.py b/scenarios/examples/vc_holder/example.py index 1e214419f0..8c0a3c4e0b 100644 --- a/scenarios/examples/vc_holder/example.py +++ b/scenarios/examples/vc_holder/example.py @@ -1,6 +1,7 @@ """Test VC Holder multi-tenancy isolation.""" import asyncio +import sys from os import getenv from acapy_controller import Controller @@ -88,8 +89,13 @@ async def main(): ) result = await bob.get("/vc/credentials") assert len(result["results"]) == 0 + sys.exit(0) if __name__ == "__main__": logging_to_stdout() - asyncio.run(main()) + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) From c84c805cd49aa814dcd7390dbef94f98befdf8a4 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 20:09:04 +0000 Subject: [PATCH 19/20] Don't run from pytest Signed-off-by: jamshale --- .github/workflows/scenario-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-integration-tests.yml b/.github/workflows/scenario-integration-tests.yml index 350852e073..7da0fa8246 100644 --- a/.github/workflows/scenario-integration-tests.yml +++ b/.github/workflows/scenario-integration-tests.yml @@ -74,7 +74,7 @@ jobs: docker compose build docker compose up -d if ! docker compose run --rm example; then - echo "āŒ Tests failed for $dir - dumping logs:" + echo "āŒ Tests failed for $examples_dir - dumping logs:" docker compose logs docker compose down --remove-orphans exit 1 From a9a136a9475e085bf6a2951bb30e47e0e52c74ad Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 24 Jun 2025 20:27:19 +0000 Subject: [PATCH 20/20] Don't run from pytest Signed-off-by: jamshale --- scenarios/examples/anoncreds_issuance_and_revocation/example.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scenarios/examples/anoncreds_issuance_and_revocation/example.py b/scenarios/examples/anoncreds_issuance_and_revocation/example.py index f37b4efc75..af004dc05e 100644 --- a/scenarios/examples/anoncreds_issuance_and_revocation/example.py +++ b/scenarios/examples/anoncreds_issuance_and_revocation/example.py @@ -552,7 +552,6 @@ async def main(): ) await holder_anoncreds.record(topic="revocation-notification") - sys.exit(0) if __name__ == "__main__":