diff --git a/quick-start/.env.example b/quick-start/.env.example index 3b6d9a10..e1ae6e01 100644 --- a/quick-start/.env.example +++ b/quick-start/.env.example @@ -145,6 +145,9 @@ PROVIDER_SUBSCRIBER_ID=provider.oan.dev # The address PEERS are told to POST to, published in each registry row. Nothing # inside this stack resolves it -- routing here is by compose service name -- so # a wrong value costs nothing locally and everything on a shared network. +# +# In single mode only CONSUMER_BASE_URL is a public host; the other two rows stay +# per tier so a later split to multi needs no new participant ids (ADR-0001). CONSUMER_BASE_URL=https://consumer.oan.dev NETWORK_BASE_URL=https://network.oan.dev PROVIDER_BASE_URL=https://provider.oan.dev @@ -211,3 +214,9 @@ POCRA_CAPABILITY=openagrinet:AgricultureFacility POCRA_BASE_URL=https://middleware-bap-client.mahapocra.gov.in POCRA_PATH=/search POCRA_MAPPING_URL=https://raw.githubusercontent.com/OpenAgriNet/helmcharts/0.0.1/quick-start/config/mappings/pocra/agriculture-facility.select.yaml + +# single-adapter: network-adapter runs all three tiers; multi-adapter: the network tier plus +# provider- and consumer-adapter. Unset means single-adapter; the shell's value overrides this one. +NETWORK_ADAPTER_DEPLOYMENT_MODE=single-adapter +# Plain `docker compose up -d` follows the mode too: multi-adapter is also the profile. +COMPOSE_PROFILES=${NETWORK_ADAPTER_DEPLOYMENT_MODE:-single-adapter} diff --git a/quick-start/.gitignore b/quick-start/.gitignore index ac5fe84d..fd046849 100644 --- a/quick-start/.gitignore +++ b/quick-start/.gitignore @@ -16,6 +16,10 @@ config/adapters/experience.yaml config/adapters/exp.yaml config/adapters/network.yaml config/adapters/provider.yaml +config/adapters/single-adapter/network-adapter.yaml +config/adapters/multi-adapter/consumer-adapter.yaml +config/adapters/multi-adapter/network-adapter.yaml +config/adapters/multi-adapter/provider-adapter.yaml # A local override for the discovery service, if you make one. config/discovery/instance.yaml diff --git a/quick-start/Makefile b/quick-start/Makefile index 1aae6475..7054dd22 100644 --- a/quick-start/Makefile +++ b/quick-start/Makefile @@ -3,7 +3,7 @@ # startup order matters, and a good place to make the right order the shortest # thing to type. # -# make up the whole stack, in the order it has to start +# make up the whole stack (Network Adapter), in order # make up-core the same minus the reverse proxy and hyperdx # make down stop everything, keep the data # make help the rest diff --git a/quick-start/bin/bootstrap-ubuntu.sh b/quick-start/bin/bootstrap-ubuntu.sh index 519c6bf0..87140659 100755 --- a/quick-start/bin/bootstrap-ubuntu.sh +++ b/quick-start/bin/bootstrap-ubuntu.sh @@ -110,7 +110,7 @@ cat <<'NEXT' Then: - git clone -b feat/4-docker-compose https://github.com/OpenAgriNet/helmcharts.git + git clone -b development https://github.com/OpenAgriNet/helmcharts.git cd helmcharts/quick-start cp .env.example .env && nano .env # change every credential make up diff --git a/quick-start/bin/setup.py b/quick-start/bin/setup.py index 541ef06e..e7dbd729 100755 --- a/quick-start/bin/setup.py +++ b/quick-start/bin/setup.py @@ -565,17 +565,24 @@ def render(identities): vistaar_binding = (f"{env('VISTAAR_PARTICIPANT_ID')}" f"|{env('VISTAAR_CAPABILITY')}") pocra_binding = f"{env('POCRA_PARTICIPANT_ID')}|{env('POCRA_CAPABILITY')}" - for role in ROLES: - identity = identities[role] - template = (ADAPTERS / f"{role}.yaml.tmpl").read_text() - prefix = role.upper() + # The three tier configs (multi-adapter/), then single-adapter/network-adapter, + # which carries all three tiers' identities. + for name in (*ROLES, "network-adapter"): + folder = "single-adapter" if name == "network-adapter" else "multi-adapter" + fname = name if name == "network-adapter" else f"{name}-adapter" + template = (ADAPTERS / folder / f"{fname}.yaml.tmpl").read_text() + for role in (ROLES if name == "network-adapter" else (name,)): + identity = identities[role] + prefix = role.upper() + for placeholder, value in ( + (f"__{prefix}_SUBSCRIBER_ID__", identity["participantId"]), + (f"__{prefix}_KEY_ID__", identity["keyOsid"]), + (f"__{prefix}_SIGNING_PRIVATE__", identity["signingPrivate"]), + (f"__{prefix}_SIGNING_PUBLIC__", identity["signingPublic"]), + (f"__{prefix}_ENCR_PRIVATE__", identity["encrPrivate"]), + (f"__{prefix}_ENCR_PUBLIC__", identity["encrPublic"])): + template = template.replace(placeholder, value) for placeholder, value in ( - (f"__{prefix}_SUBSCRIBER_ID__", identity["participantId"]), - (f"__{prefix}_KEY_ID__", identity["keyOsid"]), - (f"__{prefix}_SIGNING_PRIVATE__", identity["signingPrivate"]), - (f"__{prefix}_SIGNING_PUBLIC__", identity["signingPublic"]), - (f"__{prefix}_ENCR_PRIVATE__", identity["encrPrivate"]), - (f"__{prefix}_ENCR_PUBLIC__", identity["encrPublic"]), ("__MAUSAMGRAM_BINDING_KEY__", mausamgram_binding), ("__AGMARKNET_BINDING_KEY__", agmarknet_binding), ("__VISTAAR_BINDING_KEY__", vistaar_binding), @@ -601,8 +608,8 @@ def render(identities): ("__OTEL_ENVIRONMENT__", env("OTEL_ENVIRONMENT", "dev"))): template = template.replace(placeholder, value) if "__" in template: - sys.exit(f"setup: {role}.yaml still has unrendered placeholders") - out = ADAPTERS / f"{role}.yaml" + sys.exit(f"setup: {name}.yaml still has unrendered placeholders") + out = ADAPTERS / folder / f"{fname}.yaml" # A bare `docker compose up -d` before this script runs starts the # adapters too, and Docker creates a DIRECTORY at a bind-mount source # that does not exist. Writing would then fail with a bare @@ -613,11 +620,11 @@ def render(identities): f" Docker created it, which means the adapters were started before this\n" f" script ran. Bring them down, remove the empty directories and retry:\n" f" docker compose down\n" - f" rmdir config/adapters/*.yaml\n" + f" rmdir config/adapters/*/*.yaml\n" f" make up") out.write_text(template) out.chmod(0o600) # holds a private key - print(f" config/adapters/{role}.yaml") + print(f" config/adapters/{folder}/{fname}.yaml") if __name__ == "__main__": @@ -659,9 +666,7 @@ def render(identities): happens from here. Next: `make up` continues to step 3 and starts the adapters. If you ran this -on its own, the adapters need recreating to pick up the rendered configs: - - docker compose up -d --force-recreate provider-adapter network-adapter consumer-adapter +on its own, run `make up` (or `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up`) to pick up the rendered configs. Then import ../api-collection/ -- both files, the collection and the environment -- and run it. Eighteen requests across the four capabilities, diff --git a/quick-start/bin/stack.sh b/quick-start/bin/stack.sh index 1e7ac6e1..df1e829f 100755 --- a/quick-start/bin/stack.sh +++ b/quick-start/bin/stack.sh @@ -30,11 +30,55 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" -# Both optional profiles, named explicitly. This matters for `down`: compose -# only acts on services whose profile is active, so a plain `docker compose -# down` leaves the gateway and hyperdx containers running and then reports -# success. Naming them on teardown is what makes "down" mean down. -PROFILES=(--profile reverse-proxy --profile observability) +# Every optional profile, named so `down` also stops profiled services +# (the gateway, hyperdx, provider- and consumer-adapter). +PROFILES=(--profile reverse-proxy --profile observability --profile multi-adapter) + +# Deployment mode for `up`: NETWORK_ADAPTER_DEPLOYMENT_MODE from the shell, else .env, else +# single-adapter (ADR-0001). +# Other commands keep the running mode so depends_on never swaps network-adapter's config. + +# A key's value in .env (quotes, inline comment and CR stripped); empty if absent. +dotenv_get() { + local v + v=$(grep -E "^$1=" .env 2>/dev/null | tail -1 | cut -d= -f2-) || true + v=${v%%#*} + printf '%s' "$v" | tr -d '"'"'"' \t\r' +} + +# Running mode, from the config network-adapter mounts; empty if not running. +running_mode() { + [ "$(docker inspect -f '{{.State.Running}}' network-adapter 2>/dev/null)" = true ] || return 0 + case "$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/app/config/adapter.yaml"}}{{.Source}}{{end}}{{end}}' network-adapter 2>/dev/null)" in + */multi-adapter/*) echo multi-adapter ;; + *) echo single-adapter ;; + esac +} + +MODE="${NETWORK_ADAPTER_DEPLOYMENT_MODE:-$(dotenv_get NETWORK_ADAPTER_DEPLOYMENT_MODE)}" +MODE="${MODE:-single-adapter}" +case "$MODE" in single|multi) MODE="$MODE-adapter" ;; esac # old values still accepted +RUNNING_MODE="$(running_mode)" + +# compose mounts config/adapters//network-adapter.yaml from this variable. +export NETWORK_ADAPTER_DEPLOYMENT_MODE="${RUNNING_MODE:-$MODE}" +check_mode() { + case "$MODE" in single-adapter|multi-adapter) ;; *) die "NETWORK_ADAPTER_DEPLOYMENT_MODE must be single-adapter or multi-adapter, not '$MODE'" ;; esac +} + +adapters_for() { # $1 mode + case "$1" in + single-adapter) echo network-adapter ;; + multi-adapter) echo provider-adapter network-adapter consumer-adapter ;; + esac +} +# The rendered config files (config/adapters/.yaml) a deployment mode mounts. +configs_for() { # $1 mode + case "$1" in + single-adapter) echo single-adapter/network-adapter ;; + multi-adapter) echo multi-adapter/provider-adapter multi-adapter/network-adapter multi-adapter/consumer-adapter ;; + esac +} # ------------------------------------------------------------------ output @@ -74,6 +118,7 @@ preflight() { # `up-core` below is the same thing minus steps 4 and 5, for when you want # neither a public port nor ClickHouse's memory. up() { + check_mode preflight up_registry_tier 5 up_setup 5 @@ -97,6 +142,7 @@ up() { # 16 GB -- this is the stack you can actually exercise, which is the reason the # profiles exist in the first place. up_core() { + check_mode preflight up_registry_tier 3 up_setup 3 @@ -125,7 +171,7 @@ up_registry_tier() { } # Generates the adapter keypairs, registers the three adapter identities, and -# renders config/adapters/{provider,network,consumer}.yaml from the .tmpl files +# renders config/adapters/{multi-adapter,single-adapter}/*.yaml from the .tmpl files # beside them. Safe to re-run: keys come from keys/keys.json once it exists, # and participants already registered are left alone. up_setup() { @@ -142,8 +188,33 @@ up_adapters() { # debugging a mapping without the real credentials -- but that also needs a # new participant id and base URL in .env plus a setup.py re-run, because # the registry cannot repoint an existing row. - step 3 "$1" "adapters (provider, network, consumer)" - docker compose up -d provider-adapter network-adapter consumer-adapter + check_mode + local f + # `up` starts the requested mode, not the running one. + NETWORK_ADAPTER_DEPLOYMENT_MODE="$MODE" + step 3 "$1" "adapters, mode $MODE ($(adapters_for "$MODE"))" + # A pre-merge stack's network-adapter container (service network-adapter-layer) + # is unknown to compose and would clash by name. + if [ "$(docker inspect -f '{{index .Config.Labels "com.docker.compose.service"}}' network-adapter 2>/dev/null)" = network-adapter-layer ]; then + docker rm -f network-adapter >/dev/null + fi + # Single runs network-adapter alone: remove the multi-only adapters. + if [ "$MODE" = single-adapter ]; then + docker compose --profile multi-adapter rm -sf provider-adapter consumer-adapter >/dev/null 2>&1 || true + fi + # Never let compose create a directory where a bind-mounted config belongs. + for f in $(configs_for "$MODE"); do + [ -f "config/adapters/$f.yaml" ] \ + || die "config/adapters/$f.yaml was not rendered (a directory there? rm -r it, then re-run)" + done + # shellcheck disable=SC2046 + docker compose --profile multi-adapter up -d --force-recreate $(adapters_for "$MODE") + # NPM resolves adapter names when nginx loads and caches the addresses, so + # a recreated or switched adapter 502s through it until it restarts. + if [ "$(docker inspect -f '{{.State.Running}}' nginx-proxy-manager 2>/dev/null)" = true ]; then + info "restarting nginx-proxy-manager so it re-resolves the adapter addresses" + docker compose --profile reverse-proxy restart nginx-proxy-manager >/dev/null + fi } done_banner() { @@ -282,7 +353,6 @@ NEXT # It also excludes nginx-proxy-manager, whose routing table lives in a SQLite # database rather than in anything a restart would re-read. `restart-edge` is # the separate target for the one case that does need it. -APP_SERVICES=(registry discovery provider-adapter network-adapter consumer-adapter) # `restart`, not `up -d --force-recreate`. A restart keeps the container and # therefore its address, so NPM's cached proxy_pass targets stay valid -- a @@ -294,10 +364,13 @@ APP_SERVICES=(registry discovery provider-adapter network-adapter consumer-adapt # up is a changed image or a changed environment, both of which need the # container recreated -- use `make up` for those. restart_app() { - step 1 1 "restarting registry, discovery and the three adapters" + [ -n "$RUNNING_MODE" ] && MODE=$RUNNING_MODE + check_mode + step 1 1 "restarting registry, discovery and the $MODE-mode adapters" info "keycloak, both databases and the edge are left alone" - docker compose restart "${APP_SERVICES[@]}" - docker compose ps --format 'table {{.Service}}\t{{.Status}}' + # shellcheck disable=SC2046 + docker compose --profile multi-adapter restart sunbird-registry-service discovery-service $(adapters_for "$MODE") + docker compose --profile multi-adapter ps --format 'table {{.Service}}\t{{.Status}}' } # ----------------------------------------------------------------- misc @@ -324,9 +397,12 @@ usage() { cat <<'USAGE' bin/stack.sh - up the whole stack: registry+discovery -> setup.py -> adapters + up the whole stack, Network Adapter Layer (default): + registry+discovery -> setup.py -> network-adapter (Network Adapter Layer) -> gateway (public, 80/443) -> hyperdx (wants 16 GB) up-core steps 1-3 only. Nothing public, no ClickHouse. + (without a flag: MODE, else NETWORK_ADAPTER_DEPLOYMENT_MODE in + .env, else single-adapter; restart follows the running mode) down stop everything, keep the data destroy stop everything and DELETE every volume setup re-run bin/setup.py only @@ -341,6 +417,8 @@ bin/stack.sh USAGE } +# Every command takes no arguments except logs; an extra one is a mistake, not a mode. +[ $# -le 1 ] || [ "${1:-}" = logs ] || { usage; die "unexpected argument: $2"; } case "${1:-}" in up) up ;; up-core) up_core ;; diff --git a/quick-start/config/adapters/consumer.yaml.tmpl b/quick-start/config/adapters/multi-adapter/consumer-adapter.yaml.tmpl similarity index 96% rename from quick-start/config/adapters/consumer.yaml.tmpl rename to quick-start/config/adapters/multi-adapter/consumer-adapter.yaml.tmpl index 7440f2fb..593f9950 100644 --- a/quick-start/config/adapters/consumer.yaml.tmpl +++ b/quick-start/config/adapters/multi-adapter/consumer-adapter.yaml.tmpl @@ -1,5 +1,5 @@ -# OAN consumer adapter. The caller: routes by action -- discovery to the -# network layer, transactions straight to the provider adapter. +# Use only in multi-adapter deployment mode: the consumer adapter, +# the caller: routes discovery to the network layer, transactions to the provider. appName: "consumer-adapter" log: diff --git a/quick-start/config/adapters/network.yaml.tmpl b/quick-start/config/adapters/multi-adapter/network-adapter.yaml.tmpl similarity index 96% rename from quick-start/config/adapters/network.yaml.tmpl rename to quick-start/config/adapters/multi-adapter/network-adapter.yaml.tmpl index 9bfcf09a..164f4717 100644 --- a/quick-start/config/adapters/network.yaml.tmpl +++ b/quick-start/config/adapters/multi-adapter/network-adapter.yaml.tmpl @@ -1,5 +1,5 @@ -# OAN network adapter. The network layer: hands discovery to the discovery -# service and re-signs as itself on the way out. +# Use only in multi-adapter deployment mode: the network adapter, +# the network layer: hands discovery to the discovery service, re-signs as itself. appName: "network-adapter" log: diff --git a/quick-start/config/adapters/provider.yaml.tmpl b/quick-start/config/adapters/multi-adapter/provider-adapter.yaml.tmpl similarity index 98% rename from quick-start/config/adapters/provider.yaml.tmpl rename to quick-start/config/adapters/multi-adapter/provider-adapter.yaml.tmpl index 53ab6d53..b6783553 100644 --- a/quick-start/config/adapters/provider.yaml.tmpl +++ b/quick-start/config/adapters/multi-adapter/provider-adapter.yaml.tmpl @@ -1,5 +1,5 @@ -# OAN provider adapter. Answers Beckn actions synchronously: resolve the -# capability's call plan, call the provider, return the mapped result. +# Use only in multi-adapter deployment mode: the provider adapter, +# answers Beckn actions from the provider APIs and publishes the catalogue. appName: "provider-adapter" log: diff --git a/quick-start/config/adapters/routing-consumer.yaml b/quick-start/config/adapters/multi-adapter/routing-consumer.yaml similarity index 100% rename from quick-start/config/adapters/routing-consumer.yaml rename to quick-start/config/adapters/multi-adapter/routing-consumer.yaml diff --git a/quick-start/config/adapters/routing-network.yaml b/quick-start/config/adapters/multi-adapter/routing-network.yaml similarity index 100% rename from quick-start/config/adapters/routing-network.yaml rename to quick-start/config/adapters/multi-adapter/routing-network.yaml diff --git a/quick-start/config/adapters/routing-provider.yaml b/quick-start/config/adapters/multi-adapter/routing-provider.yaml similarity index 100% rename from quick-start/config/adapters/routing-provider.yaml rename to quick-start/config/adapters/multi-adapter/routing-provider.yaml diff --git a/quick-start/config/adapters/single-adapter/network-adapter.yaml.tmpl b/quick-start/config/adapters/single-adapter/network-adapter.yaml.tmpl new file mode 100644 index 00000000..eab0776b --- /dev/null +++ b/quick-start/config/adapters/single-adapter/network-adapter.yaml.tmpl @@ -0,0 +1,307 @@ +# Use only in single-adapter deployment mode: consumer, network and provider tiers in one +# process. Signing is off: keep :9201 on 127.0.0.1. setup.py renders it (ADR-0001). +appName: network-adapter + +log: + level: debug + destinations: + - type: stdout + contextKeys: + - transaction_id + - message_id + - subscriber_id + - module_id + +http: + port: 9201 + timeout: + read: 30 + write: 30 + idle: 30 + +pluginManager: + root: ./plugins + +plugins: + otelsetup: + id: otelsetup + config: + serviceName: network-adapter + environment: "__OTEL_ENVIRONMENT__" + otlpEndpoint: "__OTLP_ENDPOINT__" + enableMetrics: "__OTEL_ENABLED__" + enableTracing: "__OTEL_ENABLED__" + enableLogs: "__OTEL_ENABLED__" + +modules: + # consumer-adapter: the app calls /; discover to /_internal/network, transactions + # to /_internal/provider (routing-network-adapter-consumer.yaml). + - name: consumer-adapter + path: / + handler: + type: std + role: bap + subscriberId: "__CONSUMER_SUBSCRIBER_ID__" + plugins: + registry: + id: sunbirdRegistry + config: + url: http://sunbird-registry-service:8081/api/v1 + entity: Participant + providerEntity: ProviderSchema + cacheTTL: 0 + timeout: 2 + retry_max: 1 + retry_wait_min: 100ms + retry_wait_max: 500ms + maxResponseBytes: 1048576 + keyManager: + id: simplekeymanager + config: + subscriberId: "__CONSUMER_SUBSCRIBER_ID__" + keyId: "__CONSUMER_KEY_ID__" + signingPrivateKey: "__CONSUMER_SIGNING_PRIVATE__" + signingPublicKey: "__CONSUMER_SIGNING_PUBLIC__" + encrPrivateKey: "__CONSUMER_ENCR_PRIVATE__" + encrPublicKey: "__CONSUMER_ENCR_PUBLIC__" + signer: + id: signer + signValidator: + id: signvalidator + middleware: + - id: reqpreprocessor + config: + contextKeys: transaction_id,message_id + role: bap + schemaValidator: + id: schemav2validator + config: + type: url + location: https://raw.githubusercontent.com/beckn/protocol-specifications-v2/refs/tags/core-v2.0.0-lts/api/v2.0.0/beckn.yaml + cacheTTL: '3600' + extendedSchema_enabled: "false" + extendedSchema_cacheTTL: '86400' + extendedSchema_maxCacheSize: '100' + extendedSchema_downloadTimeout: '30' + extendedSchema_allowedDomains: raw.githubusercontent.com,schema.beckn.io,schema.nfh.global,openagrinet.github.io + router: + id: router + config: + routingConfig: /app/config/routing-network-adapter-consumer.yaml + steps: + - validateSchema + - addRoute + + # network-adapter: forwards discover and publish to discovery (routing-network.yaml). + - name: network-adapter + path: /_internal/network/ + handler: + type: std + role: bpp + subscriberId: "__NETWORK_SUBSCRIBER_ID__" + plugins: + registry: + id: sunbirdRegistry + config: + url: http://sunbird-registry-service:8081/api/v1 + entity: Participant + providerEntity: ProviderSchema + cacheTTL: 0 + timeout: 2 + retry_max: 1 + retry_wait_min: 100ms + retry_wait_max: 500ms + maxResponseBytes: 1048576 + keyManager: + id: simplekeymanager + config: + subscriberId: "__NETWORK_SUBSCRIBER_ID__" + keyId: "__NETWORK_KEY_ID__" + signingPrivateKey: "__NETWORK_SIGNING_PRIVATE__" + signingPublicKey: "__NETWORK_SIGNING_PUBLIC__" + encrPrivateKey: "__NETWORK_ENCR_PRIVATE__" + encrPublicKey: "__NETWORK_ENCR_PUBLIC__" + signer: + id: signer + signValidator: + id: signvalidator + middleware: + - id: reqpreprocessor + config: + contextKeys: transaction_id,message_id + role: bpp + # Extended layer off: it fetches each resource's @context per payload. + # The extendedSchema_* settings apply only when it is on. + schemaValidator: + id: schemav2validator + config: + type: url + location: "https://raw.githubusercontent.com/beckn/protocol-specifications-v2/refs/tags/core-v2.0.0-lts/api/v2.0.0/beckn.yaml" + cacheTTL: "3600" + extendedSchema_enabled: "false" + extendedSchema_cacheTTL: "86400" + extendedSchema_maxCacheSize: "100" + extendedSchema_downloadTimeout: "30" + extendedSchema_allowedDomains: "raw.githubusercontent.com,schema.beckn.io,schema.nfh.global,openagrinet.github.io" + router: + id: router + config: + routingConfig: /app/config/routing-network.yaml + steps: + - addRoute + + # oanProvider: answers from the upstream APIs itself (no router). + - name: oanProvider + path: /_internal/provider/ + handler: + type: std + role: bpp + subscriberId: "__PROVIDER_SUBSCRIBER_ID__" + plugins: + registry: + id: sunbirdRegistry + config: + url: http://sunbird-registry-service:8081/api/v1 + entity: Participant + providerEntity: ProviderSchema + cacheTTL: 0 + timeout: 2 + retry_max: 1 + retry_wait_min: 100ms + retry_wait_max: 500ms + maxResponseBytes: 1048576 + keyManager: + id: simplekeymanager + config: + subscriberId: "__PROVIDER_SUBSCRIBER_ID__" + keyId: "__PROVIDER_KEY_ID__" + signingPrivateKey: "__PROVIDER_SIGNING_PRIVATE__" + signingPublicKey: "__PROVIDER_SIGNING_PUBLIC__" + encrPrivateKey: "__PROVIDER_ENCR_PRIVATE__" + encrPublicKey: "__PROVIDER_ENCR_PUBLIC__" + signValidator: + id: signvalidator + signer: + id: signer + middleware: + - id: reqpreprocessor + config: + contextKeys: transaction_id,message_id + role: bpp + schemaValidator: + id: schemav2validator + config: + type: url + location: https://raw.githubusercontent.com/beckn/protocol-specifications-v2/refs/tags/core-v2.0.0-lts/api/v2.0.0/beckn.yaml + cacheTTL: '3600' + extendedSchema_enabled: "true" + extendedSchema_allowedDomains: raw.githubusercontent.com,schema.beckn.io,schema.nfh.global,openagrinet.github.io + extendedSchema_cacheTTL: '86400' + extendedSchema_maxCacheSize: '100' + extendedSchema_downloadTimeout: '30' + mapper: + id: jsonmapper + config: + fetchTimeout: 10s + cacheTTL: 1m + negativeTTL: 1m + maxMappingBytes: 262144 + maxCacheEntries: 200 + providerSteps: + - id: WeatherObservation + config: + bindingKeys: "__MAUSAMGRAM_BINDING_KEY__" + __MAUSAMGRAM_PARTICIPANT_ID__: + authScheme: header + headerName: Authorization + headerValueEnv: MAUSAMGRAM_AUTH + - id: MandiPrice + config: + bindingKeys: "__AGMARKNET_BINDING_KEY__" + __AGMARKNET_PARTICIPANT_ID__: + authScheme: tokenQuery + tokenUrl: "__AGMARKNET_TOKEN_URL__" + tokenUserField: access_name + tokenUserEnv: AGMARKNET_ACCESS_NAME + tokenSecretField: password + tokenSecretEnv: AGMARKNET_PASSWORD + tokenResponseField: token + queryName: token + tokenTtl: 10m + - id: KnowledgeAdvisory + config: + bindingKeys: "__VISTAAR_BINDING_KEY__" + __VISTAAR_PARTICIPANT_ID__: + authScheme: oauth2 + tokenUrl: "__VISTAAR_TOKEN_URL__" + clientIdEnv: VISTAAR_CLIENT_ID + clientSecretEnv: VISTAAR_CLIENT_SECRET + maxResponseBytes: 1048576 + - id: AgricultureFacility + config: + bindingKeys: "__POCRA_BINDING_KEY__" + __POCRA_PARTICIPANT_ID__: + authScheme: none + searchConcurrency: 1 + steps: + - validateSchema + - WeatherObservation + - MandiPrice + - KnowledgeAdvisory + - AgricultureFacility + + # oanProviderPublish: the catalogue publishes at /publish; routed to + # /_internal/network/publish (routing-network-adapter-provider.yaml). + - name: oanProviderPublish + path: /publish + handler: + type: std + role: bap + subscriberId: "__PROVIDER_SUBSCRIBER_ID__" + plugins: + registry: + id: sunbirdRegistry + config: + url: http://sunbird-registry-service:8081/api/v1 + entity: Participant + providerEntity: ProviderSchema + cacheTTL: 0 + timeout: 2 + retry_max: 1 + retry_wait_min: 100ms + retry_wait_max: 500ms + maxResponseBytes: 1048576 + keyManager: + id: simplekeymanager + config: + subscriberId: "__PROVIDER_SUBSCRIBER_ID__" + keyId: "__PROVIDER_KEY_ID__" + signingPrivateKey: "__PROVIDER_SIGNING_PRIVATE__" + signingPublicKey: "__PROVIDER_SIGNING_PUBLIC__" + encrPrivateKey: "__PROVIDER_ENCR_PRIVATE__" + encrPublicKey: "__PROVIDER_ENCR_PUBLIC__" + signer: + id: signer + middleware: + - id: reqpreprocessor + config: + contextKeys: transaction_id,message_id + role: bap + schemaValidator: + id: schemav2validator + config: + type: url + location: https://raw.githubusercontent.com/beckn/protocol-specifications-v2/refs/tags/core-v2.0.0-lts/api/v2.0.0/beckn.yaml + cacheTTL: '3600' + extendedSchema_enabled: "true" + extendedSchema_allowedDomains: raw.githubusercontent.com,schema.beckn.io,schema.nfh.global,openagrinet.github.io + extendedSchema_cacheTTL: '86400' + extendedSchema_maxCacheSize: '100' + extendedSchema_downloadTimeout: '30' + router: + id: router + config: + routingConfig: /app/config/routing-network-adapter-provider.yaml + steps: + - validateSchema + - addRoute diff --git a/quick-start/config/adapters/single-adapter/routing-network-adapter-consumer.yaml b/quick-start/config/adapters/single-adapter/routing-network-adapter-consumer.yaml new file mode 100644 index 00000000..b7764b1b --- /dev/null +++ b/quick-start/config/adapters/single-adapter/routing-network-adapter-consumer.yaml @@ -0,0 +1,21 @@ +# Single-mode consumer routing: tier hops are HTTP calls to this same adapter's +# listener (localhost:9201 must match http.port in network-adapter.yaml.tmpl). +routingRules: + # Discovery, via the network module. + - version: "2.0.0" + targetType: "url" + target: + url: "http://localhost:9201/_internal/network" + endpoints: + - discover + + # Transactions go to the provider module; context.bppUri passes through untouched. + - version: "2.0.0" + targetType: "url" + target: + url: "http://localhost:9201/_internal/provider" + endpoints: + - select + - init + - confirm + - status diff --git a/quick-start/config/adapters/single-adapter/routing-network-adapter-provider.yaml b/quick-start/config/adapters/single-adapter/routing-network-adapter-provider.yaml new file mode 100644 index 00000000..5c62ea5f --- /dev/null +++ b/quick-start/config/adapters/single-adapter/routing-network-adapter-provider.yaml @@ -0,0 +1,10 @@ +# Single-mode publish routing: /publish goes through the network module to discovery. +# The mount strips /publish, so the rule keys on "" with excludeAction. +routingRules: + - version: "2.0.0" + targetType: "url" + target: + url: "http://localhost:9201/_internal/network/publish" + excludeAction: true + endpoints: + - "" diff --git a/quick-start/config/reverse-proxy/npm-custom/server_proxy.conf b/quick-start/config/reverse-proxy/npm-custom/server_proxy.conf index 35f956d6..5e828f83 100644 --- a/quick-start/config/reverse-proxy/npm-custom/server_proxy.conf +++ b/quick-start/config/reverse-proxy/npm-custom/server_proxy.conf @@ -2,29 +2,36 @@ # /data/nginx/custom/server_proxy.conf. No UI step. # # Because it applies to every host, only rules that are correct everywhere -# belong here. There is exactly one, and it matters more than the rest of this +# belong here. There are two, and they matter more than the rest of this # stack's edge configuration combined. # The provider adapter mounts TWO modules. / is its Beckn surface and -# verifies the sender's signature against the registry. But oanProviderPublish -# is mounted at `/` with NO validateSign at all, because its intended caller is +# verifies the sender's signature against the registry. But the publish module +# is mounted at `/publish` with NO validateSign at all, because its intended caller is # the provider's own catalogue system, inside the trust boundary -- the same # reason the consumer adapter takes unsigned calls. # # So a proxy host pointed at provider-adapter:9200 exposes, at /publish, # an unauthenticated "write anything into the catalogue" endpoint. NPM's UI # gives no way to route a host while withholding one path, and this is not a -# thing to leave to remembering an Advanced-tab paste on one host out of three. +# thing to leave to remembering an Advanced-tab paste on one host. # # An exact-match location beats NPM's generated `location /`, so this wins # without conflicting with it. On the consumer and network hosts it denies a path # their adapters do not serve, which costs nothing. +# In single mode network-adapter:9201 serves /publish itself, so the deny matters there. # # To open it for a real catalogue system outside this VM, do NOT edit this to # `allow`. Give that caller a tunnel, or put it in the VPC and reach # provider-adapter directly -- an endpoint with no credential to check does not # belong on the public edge, and an address allowlist in front of it is a # statement about the network, which is where it should be made. -# location = /publish { -# deny all; -# } +location = /publish { + deny all; +} + +# Single mode's network and provider tiers live under /_internal/, called only by +# the adapter itself; ^~ stops regex locations from overriding the deny. +location ^~ /_internal { + deny all; +} diff --git a/quick-start/docker-compose.yml b/quick-start/docker-compose.yml index 9568abc3..3f7f8474 100644 --- a/quick-start/docker-compose.yml +++ b/quick-start/docker-compose.yml @@ -11,7 +11,8 @@ # registry sunbird-registry-postgres, sunbird-registry-keycloak, # sunbird-registry-service # discovery discovery-postgres, discovery-service -# adapters provider, network, consumer +# adapters network-adapter (all three tiers by default) + +# provider, consumer (profile multi-adapter) # observability hyperdx profile: observability # edge nginx-proxy-manager profile: reverse-proxy # @@ -26,7 +27,7 @@ # everything EXCEPT the adapters # 3. python3 bin/setup.py keys, the three adapter entries, and # the adapter configs they mount -# 4. docker compose up -d now the adapters +# 4. docker compose up -d now the adapters (mode and profile from .env) # 5. docker compose --profile reverse-proxy --profile observability up -d # the edge and the telemetry stack # @@ -533,7 +534,8 @@ services: condition: service_healthy # ========================================================================== - # adapters -- consumer, network and provider. Same image, three configs. + # adapters -- single-adapter (default): network-adapter runs all three tiers; multi-adapter: + # plus provider- and consumer-adapter (profile multi-adapter). bin/stack.sh picks the config. # # Their configs are bind-mounted FILES that do not exist until bin/setup.py # renders them from the .tmpl beside them, which is what the step ordering @@ -592,6 +594,7 @@ services: # change here or a restart. provider-adapter: <<: *adapter + profiles: ["multi-adapter"] container_name: provider-adapter # Registry only. The mocks are deliberately NOT dependencies: every # capability points at a real external upstream, and depends_on would start @@ -629,12 +632,13 @@ services: VISTAAR_CLIENT_ID: ${VISTAAR_CLIENT_ID} VISTAAR_CLIENT_SECRET: ${VISTAAR_CLIENT_SECRET} volumes: - - ./config/adapters/provider.yaml:/app/config/adapter.yaml:ro - - ./config/adapters/routing-provider.yaml:/app/config/routing-provider.yaml:ro + - ./config/adapters/multi-adapter/provider-adapter.yaml:/app/config/adapter.yaml:ro + - ./config/adapters/multi-adapter/routing-provider.yaml:/app/config/routing-provider.yaml:ro ports: - "127.0.0.1:${PROVIDER_ADAPTER_PORT}:9200" # Verifies the caller, hands discovery on, re-signs as itself. + # Single mode: all three tiers in one process (/, /publish, /_internal/*); keep :9201 local. network-adapter: <<: *adapter container_name: network-adapter @@ -646,9 +650,19 @@ services: environment: <<: *adapter-env OTEL_SERVICE_NAME: network-adapter + # Upstream credentials for the single-mode provider module, read at call + # time -- the same variables and reasons as provider-adapter. + MAUSAMGRAM_AUTH: ${MAUSAMGRAM_AUTH} + AGMARKNET_ACCESS_NAME: ${AGMARKNET_ACCESS_NAME} + AGMARKNET_PASSWORD: ${AGMARKNET_PASSWORD} + MOCKAGMARKNET_TOKEN: ${MOCKAGMARKNET_TOKEN:-local-mandi-token} + VISTAAR_CLIENT_ID: ${VISTAAR_CLIENT_ID} + VISTAAR_CLIENT_SECRET: ${VISTAAR_CLIENT_SECRET} volumes: - - ./config/adapters/network.yaml:/app/config/adapter.yaml:ro - - ./config/adapters/routing-network.yaml:/app/config/routing-network.yaml:ro + - ./config/adapters/${NETWORK_ADAPTER_DEPLOYMENT_MODE:-single-adapter}/network-adapter.yaml:/app/config/adapter.yaml:ro + - ./config/adapters/multi-adapter/routing-network.yaml:/app/config/routing-network.yaml:ro + - ./config/adapters/single-adapter/routing-network-adapter-consumer.yaml:/app/config/routing-network-adapter-consumer.yaml:ro + - ./config/adapters/single-adapter/routing-network-adapter-provider.yaml:/app/config/routing-network-adapter-provider.yaml:ro ports: - "127.0.0.1:${NETWORK_ADAPTER_PORT}:9201" @@ -658,6 +672,7 @@ services: # is also why the edge rate-limits this host and nothing else. consumer-adapter: <<: *adapter + profiles: ["multi-adapter"] container_name: consumer-adapter depends_on: network-adapter: @@ -668,8 +683,8 @@ services: <<: *adapter-env OTEL_SERVICE_NAME: consumer-adapter volumes: - - ./config/adapters/consumer.yaml:/app/config/adapter.yaml:ro - - ./config/adapters/routing-consumer.yaml:/app/config/routing-consumer.yaml:ro + - ./config/adapters/multi-adapter/consumer-adapter.yaml:/app/config/adapter.yaml:ro + - ./config/adapters/multi-adapter/routing-consumer.yaml:/app/config/routing-consumer.yaml:ro ports: - "127.0.0.1:${CONSUMER_ADAPTER_PORT}:9202" @@ -938,12 +953,18 @@ services: # whenever NPM does. That means the restart above is not optional after # `make observability` -- NPM was started before the name existed, so the # HyperDX host 502s until it re-resolves. + # + # required: false -- consumer- and provider-adapter exist only in multi + # mode (profile "multi-adapter"); NPM starts with whichever is running. consumer-adapter: condition: service_started + required: false network-adapter: condition: service_started + required: false provider-adapter: condition: service_started + required: false healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:81/api/ || exit 1"]