Skip to content

Commit f464fd9

Browse files
committed
test(operator): unit-test ingress URL/route helpers; document shared 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).
1 parent 4a87338 commit f464fd9

2 files changed

Lines changed: 79 additions & 6 deletions

File tree

operator/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ LINE console:
264264
> **Recreate caveat:** ECS service registries can only be set at *creation* time.
265265
> If the service already exists without service discovery, `apply` provisions the
266266
> ingress resources and prints how to recreate the service so traffic can reach it.
267+
>
268+
> **Shared VPC Link:** all ingress-enabled bots in a VPC share one `oab-vpc-link`.
269+
> A VPC Link's subnets/security groups are fixed at creation and cannot be changed,
270+
> so every ingress bot in the VPC must use the same `networking.subnets` /
271+
> `securityGroups` as whichever bot created the link first. `apply` prints a
272+
> reminder when it reuses an existing link.
267273

268274
### OABFleet — batch deploy
269275

operator/src/ingress.rs

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ pub async fn ensure_gateway(
8686
let (api_id, api_endpoint) = ensure_api(&api).await?;
8787

8888
// ── Integration: VPC Link → Cloud Map DNS name on the container port ────
89-
let integration_uri = format!("http://{}:{}", dns_name, ingress.container_port);
89+
let integration_uri = integration_uri(dns_name, ingress.container_port);
9090
let integration_id = ensure_integration(&api, &api_id, &vpc_link_id, &integration_uri).await?;
9191

9292
// ── One route per webhook path, all → the same integration ─────────────
@@ -97,13 +97,27 @@ pub async fn ensure_gateway(
9797
// ── Stage (auto-deploy) ────────────────────────────────────────────────
9898
ensure_stage(&api, &api_id).await?;
9999

100+
Ok(webhook_urls(&api_endpoint, &ingress.paths))
101+
}
102+
103+
/// Integration URI for the VPC Link → Cloud Map target on the container port.
104+
fn integration_uri(dns_name: &str, port: u16) -> String {
105+
format!("http://{dns_name}:{port}")
106+
}
107+
108+
/// API Gateway route key for a webhook path (POST only).
109+
fn route_key(path: &str) -> String {
110+
format!("POST {path}")
111+
}
112+
113+
/// Build the public webhook URL(s) from the API endpoint and paths.
114+
/// Each URL is `<endpoint>/<stage><path>`.
115+
fn webhook_urls(api_endpoint: &str, paths: &[String]) -> Vec<String> {
100116
let base = api_endpoint.trim_end_matches('/');
101-
let urls = ingress
102-
.paths
117+
paths
103118
.iter()
104119
.map(|p| format!("{base}/{STAGE_NAME}{p}"))
105-
.collect();
106-
Ok(urls)
120+
.collect()
107121
}
108122

109123
// ─── VPC resolution ─────────────────────────────────────────────────────────
@@ -328,6 +342,13 @@ async fn ensure_vpc_link(
328342

329343
let link_id = if let Some((id, _status)) = found {
330344
eprintln!(" ✓ VPC Link exists: {VPC_LINK_NAME} ({id})");
345+
// A VPC Link's subnets/SGs are fixed at creation and cannot be updated.
346+
// All ingress-enabled bots in a VPC share this one link, so they must use
347+
// the same subnet/SG set as whichever bot created it first — otherwise the
348+
// link's ENIs won't cover this task's subnets and integrations may 503.
349+
eprintln!(
350+
" ↳ reusing shared link's subnets/SGs (fixed at creation); ensure all\n ingress bots in this VPC use the same subnets/securityGroups"
351+
);
331352
id
332353
} else {
333354
eprintln!(" ⊕ Creating VPC Link: {VPC_LINK_NAME}");
@@ -437,7 +458,7 @@ async fn ensure_route(
437458
path: &str,
438459
integration_id: &str,
439460
) -> Result<()> {
440-
let route_key = format!("POST {path}");
461+
let route_key = route_key(path);
441462
let target = format!("integrations/{integration_id}");
442463
let existing = api
443464
.get_routes()
@@ -485,3 +506,49 @@ async fn ensure_stage(api: &aws_sdk_apigatewayv2::Client, api_id: &str) -> Resul
485506
eprintln!(" ⊕ Created stage: {STAGE_NAME} (auto-deploy)");
486507
Ok(())
487508
}
509+
510+
#[cfg(test)]
511+
mod tests {
512+
use super::*;
513+
514+
#[test]
515+
fn integration_uri_uses_dns_and_port() {
516+
assert_eq!(
517+
integration_uri("oab-prod-mybot.oab", 8080),
518+
"http://oab-prod-mybot.oab:8080"
519+
);
520+
assert_eq!(
521+
integration_uri("svc.ns", 3000),
522+
"http://svc.ns:3000"
523+
);
524+
}
525+
526+
#[test]
527+
fn route_key_is_post_prefixed() {
528+
assert_eq!(route_key("/webhook/telegram"), "POST /webhook/telegram");
529+
assert_eq!(route_key("/webhook/line"), "POST /webhook/line");
530+
}
531+
532+
#[test]
533+
fn webhook_urls_join_endpoint_stage_and_path() {
534+
let paths = vec![
535+
"/webhook/telegram".to_string(),
536+
"/webhook/line".to_string(),
537+
];
538+
let urls = webhook_urls("https://abc123.execute-api.us-east-1.amazonaws.com", &paths);
539+
assert_eq!(
540+
urls,
541+
vec![
542+
"https://abc123.execute-api.us-east-1.amazonaws.com/prod/webhook/telegram",
543+
"https://abc123.execute-api.us-east-1.amazonaws.com/prod/webhook/line",
544+
]
545+
);
546+
}
547+
548+
#[test]
549+
fn webhook_urls_trim_trailing_slash_on_endpoint() {
550+
let paths = vec!["/webhook/telegram".to_string()];
551+
let urls = webhook_urls("https://abc123.example.com/", &paths);
552+
assert_eq!(urls, vec!["https://abc123.example.com/prod/webhook/telegram"]);
553+
}
554+
}

0 commit comments

Comments
 (0)