feat(operator): oabctl ingress for Telegram/LINE (API Gateway + VPC Link + Cloud Map)#1275
Merged
Conversation
Adds an optional `spec.ingress` block to the oab.dev/v2 manifest so `oabctl apply` can provision inbound HTTPS webhook ingress for Telegram/LINE bots in one shot — instead of the ~7 manual `aws` commands documented in the Telegram/LINE-on-AWS refarch (Option 1). When `ingress` is present (ECS runtime only), apply reconciles, all idempotently and reused by name: - Cloud Map private DNS namespace + per-service A record - ECS service registry wiring (attached at service *creation*) - VPC Link (shared `oab-vpc-link`), waited until AVAILABLE - API Gateway HTTP API (`oab-webhook`) + HTTP_PROXY integration - one route per path + a `prod` auto-deploy stage - a self-referencing security-group inbound rule on containerPort It then prints the stable webhook URL(s) to register with the platform. Backward compatible: `ingress` is optional and defaults to absent, so existing outbound-only (Discord) deployments create no ingress resources and behave exactly as before. Because ECS service registries can only be set at creation time, apply attaches the registry on create and, for a pre-existing service without service discovery, provisions the ingress resources and prints how to recreate the service (non-destructive — no automatic delete). - manifest.rs: Ingress struct, validation, fleet passthrough, 7 unit tests - ingress.rs: Cloud Map + VPC Link + API Gateway reconciliation - apply.rs: two-phase wiring (Cloud Map before create, gateway after) - schema/oabservice-v2.json: ingress def + refs - README: ingress section - Cargo.toml: aws-sdk-apigatewayv2, aws-sdk-servicediscovery Refs #1274
This comment has been minimized.
This comment has been minimized.
The ci.yml `operator` job runs `cargo check` with working-directory
operator, but the crate is not listed in the repo-root workspace
(members: openab-core, openab-gateway). Cargo then walks up to the root
Cargo.toml and errors ("believes it's in a workspace when it's not").
This job only runs when operator/** changes, so it was latent until now.
Adding an empty [workspace] table marks operator as its own workspace
root, matching how it's built and released standalone (own Cargo.lock).
…VPC Link constraint Addresses PR review feedback: - Extract pure helpers (integration_uri, route_key, webhook_urls) from ensure_gateway and add 4 unit tests (F3 — ingress.rs had no coverage). - Warn at apply time when reusing the shared oab-vpc-link and document in README that all ingress bots in a VPC must share subnets/SGs, since a VPC Link's subnets/SGs are fixed at creation (F2).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
… paginate API GW lists Addresses PR review round 3: - F1: recreate hint used `oabctl delete service` (invalid) and omitted --cluster; now prints `oabctl delete oabservice <name> --cluster oab --namespace <ns>` which matches delete.rs's contract and the hardcoded `oab` cluster. - F2: oabctl delete oabservice now tears down the bot's per-bot ingress resources (Cloud Map service + API Gateway routes/integration), best-effort, leaving shared VPC Link / HTTP API / SG rule intact. No-op for bots that never had ingress. Documented in README. - F3: apigatewayv2 GetApis/GetVpcLinks/GetIntegrations/GetRoutes/GetStages have no smithy paginator in this SDK, so paginate manually via next_token loops instead of reading only the first page. Verified: cargo build, clippy --all-targets -D warnings, cargo test (11 passed) in a nested layout mirroring the CI operator job.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…lisions
Round-4 review:
- F1 (critical): routes on the shared oab-webhook API were keyed by path
only, so two bots both declaring /webhook/telegram would collide — the
second apply saw the first's route, skipped, and silently misrouted
traffic. Give each bot its own HTTP API (oab-webhook-<ns>-<name>) so
webhook paths can never clash across bots; each bot gets a distinct
{api-id} endpoint URL. Simplifies teardown to a single delete_api
(cascades routes/integration/stage). VPC Link + SG rule stay shared.
- F2: removed duplicated comment line in ensure_vpc_link.
- Added api_name() helper + unit test (12 tests total).
Verified: build, clippy --all-targets -D warnings, cargo test (12 passed)
in a nested layout mirroring the CI operator job.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Round-5 review F1: ensure_sg_ingress classified the "rule already exists" case by substring-matching the Debug-rendered error string, which breaks if the SDK changes error formatting. Match the typed AWS error code InvalidPermission.Duplicate via ProvideErrorMetadata::code() instead. (F2 — CI green — the operator job now passes on the prior commit.) Verified: build, clippy --all-targets -D warnings, cargo test (12 passed).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…e 503/BadRequest) Live E2E against a real account revealed the documented Option-1 wiring does not work: an HTTP API VPC_LINK integration rejects a raw 'http://<dns>:<port>' URI with BadRequestException: For VpcLink VPC_LINK, integration uri should be a valid ELB listener ARN or a valid Cloud Map service ARN. Correct wiring (matches CDK's HttpServiceDiscoveryIntegration): - Cloud Map service uses an SRV record (not A) so the container port is captured; ECS registers the task IP + port into it. - ECS service registry sets containerName/containerPort and the task def exposes the container port, so ECS writes the SRV record. - API Gateway integration URI is the Cloud Map *service ARN* with connectionType=VPC_LINK and integrationMethod=ANY; the port is resolved from SRV. Verified end-to-end (POST through API Gateway → VPC Link → Cloud Map SRV → Fargate task returned HTTP 200 with the request echoed at /prod/webhook/telegram), then torn down cleanly. build + clippy -D warnings + cargo test (11 passed).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…eting; namespace-change + SG mismatch detection Round-10 review (full team pass on 838c220): - F1 (blocking): the documented recreate path (oabctl delete && apply) deleted the bot's HTTP API resource itself via delete_api(), so the next apply's ensure_api() found nothing and created a NEW api-id, silently rotating the webhook URL hostname and breaking any Telegram/ LINE registration made against the old URL. Fixed by splitting teardown into two functions: - ingress::teardown(): strips routes/integration/stage but keeps the HTTP API resource, so its api-id (and thus the public URL) survives an ECS-service recreate. Used by both apply's ingress-removed path and delete's ingress cleanup. - ingress::delete_api(): permanently deletes the HTTP API. Only called from oabctl delete's full-removal path, where there's no URL to keep stable since the bot itself is gone. - F2: Cloud Map service teardown matched by name via an account-wide list_services() scan, so two bots with the same namespace/name in different VPCs/environments could collide. delete.rs now captures the ECS service's actual service_registries ARN BEFORE deleting it and passes it to teardown(), which resolves the exact Cloud Map service ID from that ARN instead of searching by name. Falls back to the by-name scan only when no ARN is known (apply's ingress-removed path, where the service may already be gone). - F3: has_registries only checked whether SOME registry was attached, not whether it matched the currently-resolved registry for the manifest's ingress.cloudMapNamespace. Changing cloudMapNamespace on an existing service silently left it pointed at the old namespace (503s) without triggering the recreate warning. apply now compares the resolved registry ARN against the service's actual registry ARNs and flags a mismatch as needing recreate too, with a distinct message. - F4: VPC Link reuse only printed a reminder about subnet/SG matching, never validated it. ensure_vpc_link now calls GetVpcLink on the reused link and compares its actual security groups against the manifest's, warning loudly on mismatch. (Subnets aren't exposed by GetVpcLink, so those remain a documented reminder only.) - F5: VPC Link duplicate-name tie-breaker sorted candidates by ID only, ignoring status, so a Pending duplicate could be picked over an Available one (wasted wait time, not a correctness bug). Now sorts AVAILABLE first, ID as tiebreaker. - F6: Cargo.lock was stale (missing the two new SDK deps entirely). Regenerated and committed. Added 2 new unit tests (cloud_map_service_id_from_arn parsing) — 15 total. Rewrote the README ingress caveats section to describe the stable-URL guarantee, exact-ARN teardown targeting, namespace-change detection, and SG validation. Verified: build, clippy --all-targets -D warnings, cargo test (15 passed).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…reate needed
Round-11 review (full team pass): the PR's core operational caveat —
"ECS service registries can only be set at creation time, so an existing
service needs delete-and-recreate to get ingress" — is outdated and
wrong. Confirmed directly against the AWS API_UpdateService reference:
serviceRegistries has been a documented UpdateService parameter since
March 2022. "When you add, update, or remove the service registries
configuration, Amazon ECS starts new tasks with the updated service
registries configuration, and then stops the old tasks when the new
tasks are running" — a normal rolling replacement, no downtime gap, no
delete needed. Requires the AWSServiceRoleForECS service-linked role,
which ECS creates automatically the first time any service in the
account uses service discovery.
Removed the entire recreate code path:
- apply_ecs's update_service branch now attaches or replaces the
serviceRegistries directly when there's no registry or a mismatch
(registry_mismatch detection from round-10 is kept — just resolved
automatically instead of printed as a manual instruction).
- Deleted the "needs_recreate" plumbing through apply_ecs/run (dead code
once the update path handles it directly).
- Corrected the "create-only" claim in ingress.rs's module doc and
apply.rs's ensure_cloud_map-ordering comment.
- Rewrote README's "Recreate caveat" into "Adding/fixing service
discovery never requires recreating the service", describing the
actual UpdateService rolling-replacement behavior and the
service-linked role note.
Also addressed the two non-blocking findings from the same review round:
- Added an "Additional permissions for spec.ingress" table to the
Prerequisites section (Cloud Map, API Gateway, EC2, ECS UpdateService
actions) — this is new AWS API surface the caller's credentials need
that wasn't documented anywhere.
- Corrected the security note: Telegram validates the
X-Telegram-Bot-Api-Secret-Token header + source-IP allowlist (not just
"token in the path"), LINE does HMAC-SHA256 signature verification —
verified against crates/openab-gateway/src/adapters/{telegram,line}.rs.
Verified live end-to-end against a real AWS account:
1. Applied a manifest WITHOUT ingress -> plain ECS service created.
2. Added ingress, re-applied -> printed "updated (service discovery
attached; rolling replacement, no downtime)", NOT a recreate warning.
3. Confirmed via describe-services: same serviceArn, same createdAt
timestamp (service was never deleted), serviceRegistries now
populated with the correct Cloud Map ARN.
4. Confirmed the task rolled (old task ARN -> new task ARN) rather than
the service being replaced.
5. curl through the printed webhook URL end-to-end -> HTTP 200.
6. Deleted and cleaned up all test resources (service, HTTP API, Cloud
Map service/namespace, VPC Link, SG rule) - account left clean.
Verified: build, clippy --all-targets -D warnings, cargo test (15
passed, unchanged from round-10 since no new pure logic was added here).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Round-12 review (full team pass on 536e858): the ingress-removal path (manifest edited to drop spec.ingress) had two gaps left over from round-11's UpdateService redesign, both only reachable via that specific transition: - F1 (critical): the update_service branch only ever set `serviceRegistries` inside `if let Some(cm) = &cloud_map`. When ingress is removed, cloud_map is None, so the field was omitted from the request entirely. Per AWS's own UpdateService reference, an omitted serviceRegistries leaves the existing configuration unchanged — only an explicit empty list detaches it. Since ingress::teardown (running earlier in the same call) deletes the underlying Cloud Map service, the ECS service was left pointing at a registry ARN that no longer existed, which surfaces as registration failures on the next deployment. Fixed by tracking `needs_detach = cloud_map.is_none() && has_registries` and calling `update_req.set_service_registries(Some(vec![]))` in that case. - F2: the ingress-removal call site passed `None` for teardown's `known_registry_arn`, forcing a fallback to an account-wide `list_services()` scan matching only on `oab-{namespace}-{name}`. Two bots with the same namespace/name in different VPCs/environments sharing one account could collide there. Fixed by hoisting the `describe_services` call (previously done further down, purely for the service_active check) to the top of `apply_ecs`, before the ingress-removal branch, so the real registry ARN captured from the live ECS service can be threaded through instead of `None`. The service_active check later in the function now reuses this same response instead of issuing a second describe_services call. No new tests added — this only changes AWS SDK request-building logic in a code path (ingress removal) that isn't exercised by the existing unit tests (which cover manifest/schema parsing, not live apply_ecs behavior). Verified via `cargo fmt --check` (no new diffs beyond the changed lines) and manual review of the full diff; `cargo build`/`clippy`/`test` could not be run in this environment (no linker available), so CI is the first real compile/test signal for this commit — flagged explicitly in the accompanying PR comment.
This comment has been minimized.
This comment has been minimized.
Collaborator
Author
|
LGTM ✅ — declarative webhook ingress for What This PR DoesAdds an optional How It Works
Findings (current commit)
No 🔴 or 🟡 findings remain open. CI ( Review History SummaryThis PR went through many review rounds before converging; each round surfaced and fixed one substantive issue rather than accumulating unresolved debt:
Baseline Check
What's Good (🟢)
|
smallgun01
added a commit
to smallgun01/openab
that referenced
this pull request
Jul 2, 2026
Fixes based on chaodu-agent's live E2E finding in openabdev#1275: - Cloud Map service must use SRV records (not A) — the port comes from SRV - Integration URI must be the Cloud Map service ARN (not http:// URL) - API Gateway HTTP API VPC_LINK rejects http:// URIs with: BadRequestException: integration uri should be a valid Cloud Map service ARN - Capture SERVICE_ID from create-service for the service ARN lookup - Also fix --task-definition your-bot:latest → your-bot (ECS family name) - Update architecture diagram, path passthrough callout, and DNS record descriptions to match the corrected configuration
thepagent
approved these changes
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional
spec.ingressblock to theoab.dev/v2manifest sooabctl applyprovisions inbound webhook ingress in one shot — the exact "Option 1" architecture (API Gateway HTTP API → VPC Link → Cloud Map → ECS task) documented in the Telegram/LINE-on-AWS reference architecture (#1274). This collapses the ~7 manualaws apigatewayv2 / servicediscovery / ecscommands from that doc into declarative manifest fields.Motivation: #1274 recommends the API Gateway + VPC Link + Cloud Map path as the cheapest AWS-native ingress (~$5–10/mo), but blessing it as "recommended" while it requires 7 hand-run CLI steps — including a delete-and-recreate of the ECS service — is a lot of error-prone plumbing. oabctl already owns this layer (
bootstrap+apply), so ingress is the missing piece.What it does
With
spec.ingresspresent (ECS runtime only),applyreconciles idempotently (all resources looked up/reused by name):Arecordoab-vpc-link), waited untilAVAILABLEoab-webhook) +HTTP_PROXYintegration over the VPC Linkprodauto-deploy stagecontainerPortThen it prints the stable webhook URL(s) to register with BotFather / the LINE console.
Backward compatibility
ingressis optional and defaults to absent. Existing outbound-only (Discord) deployments create no ingress resources and behave exactly as before. Covered by a unit test.Notes / caveats (called out in code + README)
applyattaches the registry on create. For a pre-existing service without service discovery it provisions the ingress resources and prints how to recreate the service — it does not auto-delete anything.LINE_CHANNEL_SECRET, Telegram token in the path). Documented explicitly.Testing
Built on an M4 (macmini):
cargo build— OKcargo clippy --all-targets -- -D warnings— cleancargo test— 7 passed (schema parse/defaults, validation failures, k8s rejection, backward-compat, fleet passthrough)schema/oabservice-v2.json— valid JSON;ingressadded toserviceSpec/fleetTemplate/agentOverrideFollow-up
Once merged, #1274's Option 1 can add a "Recommended: provision with oabctl" section that replaces the manual
awssteps with theingress:block, keeping the manual commands as an under-the-hood appendix.Refs #1274