Fix router frontdoor readiness and route contracts
This commit is contained in:
@@ -126,20 +126,19 @@ curl -fsS http://127.1.1.3:8080/status
|
|||||||
docker compose -f docker-compose.stella-ops.yml logs -f scanner-web
|
docker compose -f docker-compose.stella-ops.yml logs -f scanner-web
|
||||||
```
|
```
|
||||||
|
|
||||||
### Router Mode Switching
|
### Router Frontdoor Configuration
|
||||||
|
|
||||||
`router-gateway` now supports a compose-driven route table switch via `ROUTER_GATEWAY_CONFIG`.
|
`router-gateway` uses the microservice-first route table in `router-gateway-local.json`.
|
||||||
|
First-party Stella APIs are expected to flow through router transport; reverse proxy remains only for
|
||||||
|
external/bootstrap surfaces that cannot participate in router registration yet (for example OIDC browser
|
||||||
|
flows, Rekor, and static/platform bootstrap assets).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Default mode: microservice routing over Valkey messaging
|
# Default frontdoor route table
|
||||||
ROUTER_GATEWAY_CONFIG=./router-gateway-local.json \
|
ROUTER_GATEWAY_CONFIG=./router-gateway-local.json \
|
||||||
docker compose -f docker-compose.stella-ops.yml up -d
|
docker compose -f docker-compose.stella-ops.yml up -d
|
||||||
|
|
||||||
# Reverse-proxy fallback mode (no route-table edits required)
|
# Optional: scratch redeploy helper with health recovery + header-search smoke checks
|
||||||
ROUTER_GATEWAY_CONFIG=./router-gateway-local.reverseproxy.json \
|
|
||||||
docker compose -f docker-compose.stella-ops.yml up -d
|
|
||||||
|
|
||||||
# Optional: mode switch helper with health recovery + header-search smoke checks
|
|
||||||
pwsh ./scripts/router-mode-redeploy.ps1 -Mode microservice
|
pwsh ./scripts/router-mode-redeploy.ps1 -Mode microservice
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ services:
|
|||||||
- router.stella-ops.local
|
- router.stella-ops.local
|
||||||
- stella-ops.local
|
- stella-ops.local
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/8080'"]
|
test: ["CMD-SHELL", "bash -lc 'exec 3<>/dev/tcp/127.0.0.1/8080 && printf \"GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n\" >&3 && head -n 1 <&3 | grep -q \"200\"'"]
|
||||||
<<: *healthcheck-tcp
|
<<: *healthcheck-tcp
|
||||||
labels: *release-labels
|
labels: *release-labels
|
||||||
|
|
||||||
|
|||||||
7
devops/compose/env/stellaops.env.example
vendored
7
devops/compose/env/stellaops.env.example
vendored
@@ -27,14 +27,13 @@ VALKEY_PORT=6379
|
|||||||
RUSTFS_HTTP_PORT=8333
|
RUSTFS_HTTP_PORT=8333
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ROUTER GATEWAY MODE
|
# ROUTER GATEWAY
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Router route table file mounted to /app/appsettings.local.json
|
# Router route table file mounted to /app/appsettings.local.json
|
||||||
# Microservice + Valkey mode (default):
|
# Microservice-first frontdoor config (default).
|
||||||
|
# Reverse proxy is intentionally limited to external/bootstrap surfaces inside this file.
|
||||||
ROUTER_GATEWAY_CONFIG=./router-gateway-local.json
|
ROUTER_GATEWAY_CONFIG=./router-gateway-local.json
|
||||||
# Reverse-proxy fallback mode:
|
|
||||||
# ROUTER_GATEWAY_CONFIG=./router-gateway-local.reverseproxy.json
|
|
||||||
# Authority claims override endpoint base URL consumed by router-gateway.
|
# Authority claims override endpoint base URL consumed by router-gateway.
|
||||||
ROUTER_AUTHORITY_CLAIMS_OVERRIDES_URL=http://authority.stella-ops.local
|
ROUTER_AUTHORITY_CLAIMS_OVERRIDES_URL=http://authority.stella-ops.local
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"Gateway": {
|
"Gateway": {
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"DpopEnabled": false,
|
"DpopEnabled": false,
|
||||||
"AllowAnonymous": true,
|
"AllowAnonymous": true,
|
||||||
"EnableLegacyHeaders": true,
|
"EnableLegacyHeaders": true,
|
||||||
"AllowScopeHeader": false,
|
"AllowScopeHeader": false,
|
||||||
"ApprovedAuthPassthroughPrefixes": [
|
"ApprovedAuthPassthroughPrefixes": [
|
||||||
"/connect",
|
"/connect",
|
||||||
"/console",
|
"/console",
|
||||||
"/authority",
|
"/authority",
|
||||||
"/doctor",
|
"/doctor",
|
||||||
"/api",
|
"/api",
|
||||||
"/policy/shadow",
|
"/policy/shadow",
|
||||||
"/policy/simulations"
|
"/policy/simulations"
|
||||||
],
|
],
|
||||||
"Authority": {
|
"Authority": {
|
||||||
"Issuer": "https://authority.stella-ops.local/",
|
"Issuer": "https://authority.stella-ops.local/",
|
||||||
"RequireHttpsMetadata": false,
|
"RequireHttpsMetadata": false,
|
||||||
"MetadataAddress": "https://authority.stella-ops.local/.well-known/openid-configuration",
|
"MetadataAddress": "https://authority.stella-ops.local/.well-known/openid-configuration",
|
||||||
"Audiences": [
|
"Audiences": [
|
||||||
|
|
||||||
]
|
]
|
||||||
@@ -30,800 +30,128 @@
|
|||||||
"RequiredMicroservices": [
|
"RequiredMicroservices": [
|
||||||
"platform",
|
"platform",
|
||||||
"policy",
|
"policy",
|
||||||
|
"policy-engine",
|
||||||
"notify",
|
"notify",
|
||||||
|
"notifier",
|
||||||
"scanner",
|
"scanner",
|
||||||
"findings",
|
"findings-ledger",
|
||||||
"integrations",
|
"integrations",
|
||||||
"reachgraph",
|
"reachgraph",
|
||||||
"attestor",
|
"attestor",
|
||||||
"evidence",
|
"evidencelocker",
|
||||||
"sbom",
|
"sbomservice",
|
||||||
"jobengine",
|
"jobengine",
|
||||||
"authority",
|
"authority",
|
||||||
"vex",
|
"vexhub",
|
||||||
"concelier"
|
"concelier"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Routes": [
|
"Routes": [
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v1/vulnerabilities(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities$1" },
|
||||||
"Type": "ReverseProxy",
|
{ "Type": "Microservice", "Path": "^/api/v1/watchlist(.*)", "IsRegex": true, "TranslatesTo": "http://attestor.stella-ops.local/api/v1/watchlist$1" },
|
||||||
"Path": "/api/v1/setup",
|
{ "Type": "Microservice", "Path": "^/api/v1/triage(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage$1" },
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/setup",
|
{ "Type": "Microservice", "Path": "^/api/v1/secrets(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/secrets$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/v1/sources(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/sources$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/v1/witnesses(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/witnesses$1" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v1/trust(.*)", "IsRegex": true, "TranslatesTo": "https://authority.stella-ops.local/api/v1/trust$1" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/v1/evidence(.*)", "IsRegex": true, "TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/evidence$1" },
|
||||||
"Path": "/api/v1/release-orchestrator",
|
{ "Type": "Microservice", "Path": "^/api/v1/proofs(.*)", "IsRegex": true, "TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/proofs$1" },
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/v1/release-orchestrator",
|
{ "Type": "Microservice", "Path": "^/api/v1/verdicts(.*)", "IsRegex": true, "TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/verdicts$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/v1/release-orchestrator(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/release-orchestrator$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/v1/approvals(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/approvals$1" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v1/attestations(.*)", "IsRegex": true, "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestations$1" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/v1/sbom(.*)", "IsRegex": true, "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sbom$1" },
|
||||||
"Path": "/api/v1/approvals",
|
{ "Type": "Microservice", "Path": "^/api/v1/lineage(.*)", "IsRegex": true, "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/lineage$1" },
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/v1/approvals",
|
{ "Type": "Microservice", "Path": "^/api/v1/resolve(.*)", "IsRegex": true, "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/v1/ops/binaryindex(.*)", "IsRegex": true, "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/ops/binaryindex$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/v1/policy(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/policy$1" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v1/governance(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/governance$1" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/v1/determinization(.*)", "IsRegex": true, "TranslatesTo": "http://policy-engine.stella-ops.local/api/v1/determinization$1" },
|
||||||
"Path": "/api/v1/vex",
|
{ "Type": "Microservice", "Path": "^/api/v1/workflows(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/workflows$1" },
|
||||||
"TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex",
|
{ "Type": "Microservice", "Path": "^/api/v1/authority/quotas(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/v1/release-control(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v1/release-control$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/v1/gateway/rate-limits(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v1/gateway/rate-limits$1" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v1/reachability(.*)", "IsRegex": true, "TranslatesTo": "http://reachgraph.stella-ops.local/api/v1/reachability$1" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/v1/timeline(.*)", "IsRegex": true, "TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline$1" },
|
||||||
"Path": "/api/v1/vexlens",
|
{ "Type": "Microservice", "Path": "^/api/v1/audit(.*)", "IsRegex": true, "TranslatesTo": "http://timeline.stella-ops.local/api/v1/audit$1" },
|
||||||
"TranslatesTo": "http://vexlens.stella-ops.local/api/v1/vexlens",
|
{ "Type": "Microservice", "Path": "^/api/v1/export(.*)", "IsRegex": true, "TranslatesTo": "https://exportcenter.stella-ops.local/api/v1/export$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/v1/advisory-sources(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/advisory-sources$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/v1/notifier/delivery(.*)", "IsRegex": true, "TranslatesTo": "http://notifier.stella-ops.local/api/v2/notify/deliveries$1" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v1/search(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/search$1" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/v1/advisory-ai(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai$1" },
|
||||||
"Path": "/api/v1/notify",
|
{ "Type": "Microservice", "Path": "^/api/v1/advisory(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory$1" },
|
||||||
"TranslatesTo": "http://notify.stella-ops.local/api/v1/notify",
|
{ "Type": "Microservice", "Path": "^/api/v1/vex(.*)", "IsRegex": true, "TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/v1/doctor/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://doctor-scheduler.stella-ops.local/api/v1/doctor/scheduler$1" },
|
||||||
},
|
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v2/context(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/context$1" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/v2/releases(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/releases$1" },
|
||||||
"Path": "/api/v1/notifier",
|
{ "Type": "Microservice", "Path": "^/api/v2/security(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/security$1" },
|
||||||
"TranslatesTo": "http://notifier.stella-ops.local/api/v1/notifier",
|
{ "Type": "Microservice", "Path": "^/api/v2/topology(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/topology$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/v2/integrations(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/integrations$1" },
|
||||||
},
|
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/v1/([^/]+)(.*)", "IsRegex": true, "TranslatesTo": "http://$1.stella-ops.local/api/v1/$1$2" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/v2/([^/]+)(.*)", "IsRegex": true, "TranslatesTo": "http://$1.stella-ops.local/api/v2/$1$2" },
|
||||||
"Path": "/api/v1/concelier",
|
|
||||||
"TranslatesTo": "http://concelier.stella-ops.local/api/v1/concelier",
|
{ "Type": "Microservice", "Path": "^/api/(cvss|gate|exceptions|policy)(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/api/$1$2" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/(risk|risk-budget)(.*)", "IsRegex": true, "TranslatesTo": "http://policy-engine.stella-ops.local/api/$1$2" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/(release-orchestrator|releases|approvals)(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/api/$1$2" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/(compare|change-traces|sbomservice)(.*)", "IsRegex": true, "TranslatesTo": "http://sbomservice.stella-ops.local/api/$1$2" },
|
||||||
"Type": "ReverseProxy",
|
{ "Type": "Microservice", "Path": "^/api/fix-verification(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/fix-verification$1" },
|
||||||
"Path": "/api/v1/platform",
|
{ "Type": "Microservice", "Path": "^/api/verdicts(.*)", "IsRegex": true, "TranslatesTo": "https://evidencelocker.stella-ops.local/api/verdicts$1" },
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/platform",
|
{ "Type": "Microservice", "Path": "^/api/vuln-explorer(.*)", "IsRegex": true, "TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/vex(.*)", "IsRegex": true, "TranslatesTo": "https://vexhub.stella-ops.local/api/vex$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/admin/plans(.*)", "IsRegex": true, "TranslatesTo": "http://registry-token.stella-ops.local/api/admin/plans$1" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/api/admin(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/admin$1" },
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/api/analytics(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/analytics$1" },
|
||||||
"Path": "/api/v1/scanner",
|
{ "Type": "Microservice", "Path": "^/api/orchestrator(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator$1" },
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/scanner",
|
{ "Type": "Microservice", "Path": "^/api/jobengine(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/jobengine$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/api/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/api/doctor(.*)", "IsRegex": true, "TranslatesTo": "http://doctor.stella-ops.local/api/doctor$1" },
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
{ "Type": "Microservice", "Path": "^/policy(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/policy$1" },
|
||||||
"Path": "/api/v1/findings",
|
|
||||||
"TranslatesTo": "http://findings-ledger.stella-ops.local/api/v1/findings",
|
{ "Type": "Microservice", "Path": "^/v1/evidence-packs(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs$1" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "Microservice", "Path": "^/v1/runs(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/v1/runs$1" },
|
||||||
},
|
{ "Type": "Microservice", "Path": "^/v1/advisory-ai(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai$1" },
|
||||||
{
|
{ "Type": "Microservice", "Path": "^/v1/audit-bundles(.*)", "IsRegex": true, "TranslatesTo": "https://exportcenter.stella-ops.local/v1/audit-bundles$1" },
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/integrations",
|
{ "Type": "ReverseProxy", "Path": "/connect", "TranslatesTo": "http://authority.stella-ops.local/connect" },
|
||||||
"TranslatesTo": "http://integrations.stella-ops.local/api/v1/integrations",
|
{ "Type": "ReverseProxy", "Path": "/.well-known", "TranslatesTo": "http://authority.stella-ops.local/.well-known" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "ReverseProxy", "Path": "/jwks", "TranslatesTo": "http://authority.stella-ops.local/jwks" },
|
||||||
},
|
{ "Type": "ReverseProxy", "Path": "/authority/console", "TranslatesTo": "https://authority.stella-ops.local/console" },
|
||||||
{
|
{ "Type": "ReverseProxy", "Path": "/authority", "TranslatesTo": "https://authority.stella-ops.local/authority" },
|
||||||
"Type": "Microservice",
|
{ "Type": "ReverseProxy", "Path": "/console", "TranslatesTo": "https://authority.stella-ops.local/console" },
|
||||||
"Path": "/api/v1/policy",
|
{ "Type": "ReverseProxy", "Path": "/rekor", "TranslatesTo": "http://rekor.stella-ops.local:3322", "PreserveAuthHeaders": false },
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/policy",
|
{ "Type": "ReverseProxy", "Path": "/platform/envsettings.json", "TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json" },
|
||||||
"PreserveAuthHeaders": true
|
{ "Type": "ReverseProxy", "Path": "/envsettings.json", "TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json" },
|
||||||
},
|
{ "Type": "ReverseProxy", "Path": "/api/v1/setup", "TranslatesTo": "http://platform.stella-ops.local/api/v1/setup" },
|
||||||
{
|
{ "Type": "ReverseProxy", "Path": "/platform", "TranslatesTo": "http://platform.stella-ops.local/platform" },
|
||||||
"Type": "Microservice",
|
{ "Type": "ReverseProxy", "Path": "/api", "TranslatesTo": "http://platform.stella-ops.local/api" },
|
||||||
"Path": "/api/v1/reachability",
|
|
||||||
"TranslatesTo": "http://reachgraph.stella-ops.local/api/v1/reachability",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/attestor",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestor",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/attestations",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestations",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/sbom",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sbom",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/signals",
|
|
||||||
"TranslatesTo": "http://signals.stella-ops.local/api/v1/signals",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/jobengine",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/v1/jobengine",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/authority/quotas",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/authority",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/api/v1/authority",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/trust",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/api/v1/trust",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/evidence",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/evidence",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/proofs",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/proofs",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/timeline",
|
|
||||||
"TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/audit",
|
|
||||||
"TranslatesTo": "http://timeline.stella-ops.local/api/v1/audit",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/advisory-sources",
|
|
||||||
"TranslatesTo": "http://concelier.stella-ops.local/api/v1/advisory-sources",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/notifier/delivery",
|
|
||||||
"TranslatesTo": "http://notifier.stella-ops.local/api/v2/notify/deliveries",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/release-control",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/release-control",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/context",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/context",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/releases",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/releases",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/security",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/security",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/topology",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/topology",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/integrations",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/integrations",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/authority/console",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/console",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Type": "ReverseProxy",
|
"Type": "StaticFiles",
|
||||||
"Path": "/api/v1/advisory-ai/adapters",
|
"Path": "/",
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai/adapters",
|
"TranslatesTo": "/app/wwwroot",
|
||||||
"PreserveAuthHeaders": true
|
"Headers": {
|
||||||
},
|
"x-spa-fallback": "true"
|
||||||
{
|
}
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/search",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/search",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/advisory-ai",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/v1/evidence-packs",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/advisory",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/vulnerabilities",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/watchlist",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local/api/v1/watchlist",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/resolve",
|
|
||||||
"TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/ops/binaryindex",
|
|
||||||
"TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/ops/binaryindex",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/verdicts",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/verdicts",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/lineage",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/lineage",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/export",
|
|
||||||
"TranslatesTo": "https://exportcenter.stella-ops.local/api/v1/export",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/triage",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/governance",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/governance",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/determinization",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local/api/v1/determinization",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/opsmemory",
|
|
||||||
"TranslatesTo": "http://opsmemory.stella-ops.local/api/v1/opsmemory",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/secrets",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/secrets",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/sources",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/sources",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/workflows",
|
|
||||||
"TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/workflows",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/witnesses",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/witnesses",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/v1/runs",
|
|
||||||
"TranslatesTo": "http://orchestrator.stella-ops.local/v1/runs",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/v1/advisory-ai/adapters",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai/adapters",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/v1/advisory-ai",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/v1/audit-bundles",
|
|
||||||
"TranslatesTo": "https://exportcenter.stella-ops.local/v1/audit-bundles",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/cvss",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/cvss",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/policy",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/risk",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local/api/risk",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/analytics",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/analytics",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/release-orchestrator",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/release-orchestrator",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/releases",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/releases",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/approvals",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/approvals",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/gate",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/gate",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/risk-budget",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local/api/risk-budget",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/fix-verification",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/fix-verification",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/compare",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/compare",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/change-traces",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/change-traces",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/exceptions",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/exceptions",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/verdicts",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/verdicts",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/jobengine",
|
|
||||||
"TranslatesTo": "http://orchestrator.stella-ops.local/api/jobengine",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/v1/gateway/rate-limits",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/gateway/rate-limits",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/sbomservice",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/sbomservice",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/vuln-explorer",
|
|
||||||
"TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/vex",
|
|
||||||
"TranslatesTo": "https://vexhub.stella-ops.local/api/vex",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/admin/plans",
|
|
||||||
"TranslatesTo": "http://registry-token.stella-ops.local/api/admin/plans",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/admin",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/admin",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/api/scheduler",
|
|
||||||
"TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/doctor/scheduler",
|
|
||||||
"TranslatesTo": "http://doctor-scheduler.stella-ops.local/api/v1/doctor/scheduler",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/doctor",
|
|
||||||
"TranslatesTo": "http://doctor.stella-ops.local/api/doctor",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/platform/envsettings.json",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/platform",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/platform",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/connect",
|
|
||||||
"TranslatesTo": "http://authority.stella-ops.local/connect",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/.well-known",
|
|
||||||
"TranslatesTo": "http://authority.stella-ops.local/.well-known",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/jwks",
|
|
||||||
"TranslatesTo": "http://authority.stella-ops.local/jwks",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/authority",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/authority",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/console",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/console",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/rekor",
|
|
||||||
"TranslatesTo": "http://rekor.stella-ops.local:3322"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/envsettings.json",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/scanner",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/policyGateway",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/policyEngine",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/concelier",
|
|
||||||
"TranslatesTo": "http://concelier.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/attestor",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/notify",
|
|
||||||
"TranslatesTo": "http://notify.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/notifier",
|
|
||||||
"TranslatesTo": "http://notifier.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/scheduler",
|
|
||||||
"TranslatesTo": "http://scheduler.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/signals",
|
|
||||||
"TranslatesTo": "http://signals.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/excititor",
|
|
||||||
"TranslatesTo": "http://excititor.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/findingsLedger",
|
|
||||||
"TranslatesTo": "http://findings-ledger.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/vexhub",
|
|
||||||
"TranslatesTo": "https://vexhub.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/vexlens",
|
|
||||||
"TranslatesTo": "http://vexlens.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/jobengine",
|
|
||||||
"TranslatesTo": "http://orchestrator.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/taskrunner",
|
|
||||||
"TranslatesTo": "http://taskrunner.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/cartographer",
|
|
||||||
"TranslatesTo": "http://cartographer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/reachgraph",
|
|
||||||
"TranslatesTo": "http://reachgraph.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/doctor",
|
|
||||||
"TranslatesTo": "http://doctor.stella-ops.local",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/integrations",
|
|
||||||
"TranslatesTo": "http://integrations.stella-ops.local"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": "Microservice",
|
"Type": "NotFoundPage",
|
||||||
"Path": "/policy",
|
"Path": "/_error/404",
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/policy",
|
"TranslatesTo": "/app/wwwroot/index.html"
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": "Microservice",
|
"Type": "ServerErrorPage",
|
||||||
"Path": "/replay",
|
"Path": "/_error/500",
|
||||||
"TranslatesTo": "http://replay.stella-ops.local"
|
"TranslatesTo": "/app/wwwroot/index.html"
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
"Type": "Microservice",
|
},
|
||||||
"Path": "/exportcenter",
|
"Logging": {
|
||||||
"TranslatesTo": "https://exportcenter.stella-ops.local"
|
"LogLevel": {
|
||||||
},
|
"Microsoft.AspNetCore.Authentication": "Information",
|
||||||
{
|
"Microsoft.IdentityModel": "Information",
|
||||||
"Type": "Microservice",
|
"StellaOps": "Information"
|
||||||
"Path": "/evidencelocker",
|
}
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local"
|
}
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/signer",
|
|
||||||
"TranslatesTo": "http://signer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/binaryindex",
|
|
||||||
"TranslatesTo": "http://binaryindex.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/riskengine",
|
|
||||||
"TranslatesTo": "http://riskengine.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/vulnexplorer",
|
|
||||||
"TranslatesTo": "http://vulnexplorer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/sbomservice",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/advisoryai",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/unknowns",
|
|
||||||
"TranslatesTo": "http://unknowns.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/timelineindexer",
|
|
||||||
"TranslatesTo": "http://timelineindexer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/opsmemory",
|
|
||||||
"TranslatesTo": "http://opsmemory.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/issuerdirectory",
|
|
||||||
"TranslatesTo": "http://issuerdirectory.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/symbols",
|
|
||||||
"TranslatesTo": "http://symbols.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/packsregistry",
|
|
||||||
"TranslatesTo": "http://packsregistry.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/registryTokenservice",
|
|
||||||
"TranslatesTo": "http://registry-token.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/airgapController",
|
|
||||||
"TranslatesTo": "http://airgap-controller.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/airgapTime",
|
|
||||||
"TranslatesTo": "http://airgap-time.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "Microservice",
|
|
||||||
"Path": "/smremote",
|
|
||||||
"TranslatesTo": "http://smremote.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "StaticFiles",
|
|
||||||
"Path": "/",
|
|
||||||
"TranslatesTo": "/app/wwwroot",
|
|
||||||
"Headers": {
|
|
||||||
"x-spa-fallback": "true"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "NotFoundPage",
|
|
||||||
"Path": "/_error/404",
|
|
||||||
"TranslatesTo": "/app/wwwroot/index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ServerErrorPage",
|
|
||||||
"Path": "/_error/500",
|
|
||||||
"TranslatesTo": "/app/wwwroot/index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Microsoft.AspNetCore.Authentication": "Information",
|
|
||||||
"Microsoft.IdentityModel": "Information",
|
|
||||||
"StellaOps": "Information"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,812 +0,0 @@
|
|||||||
{
|
|
||||||
"_deprecated": "Legacy fallback config. The canonical default is router-gateway-local.json (Microservice routing via Valkey). Use ROUTER_GATEWAY_CONFIG=./router-gateway-local.reverseproxy.json only when debugging transport issues. Will be removed in a future release.",
|
|
||||||
"Gateway": {
|
|
||||||
"Auth": {
|
|
||||||
"DpopEnabled": false,
|
|
||||||
"AllowAnonymous": true,
|
|
||||||
"EnableLegacyHeaders": true,
|
|
||||||
"AllowScopeHeader": false,
|
|
||||||
"ApprovedAuthPassthroughPrefixes": [
|
|
||||||
"/connect",
|
|
||||||
"/console",
|
|
||||||
"/authority",
|
|
||||||
"/doctor",
|
|
||||||
"/api",
|
|
||||||
"/policy/shadow",
|
|
||||||
"/policy/simulations"
|
|
||||||
],
|
|
||||||
"Authority": {
|
|
||||||
"Issuer": "https://authority.stella-ops.local/",
|
|
||||||
"RequireHttpsMetadata": false,
|
|
||||||
"MetadataAddress": "https://authority.stella-ops.local/.well-known/openid-configuration",
|
|
||||||
"Audiences": [
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Routes": [
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/release-orchestrator",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/v1/release-orchestrator",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/approvals",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/v1/approvals",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/vex",
|
|
||||||
"TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/vexlens",
|
|
||||||
"TranslatesTo": "http://vexlens.stella-ops.local/api/v1/vexlens",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/notify",
|
|
||||||
"TranslatesTo": "http://notify.stella-ops.local/api/v1/notify",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/notifier/delivery",
|
|
||||||
"TranslatesTo": "http://notifier.stella-ops.local/api/v2/notify/deliveries",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/notifier",
|
|
||||||
"TranslatesTo": "http://notifier.stella-ops.local/api/v2/notify",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/concelier",
|
|
||||||
"TranslatesTo": "http://concelier.stella-ops.local/api/v1/concelier",
|
|
||||||
"PreserveAuthHeaders": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/advisory-sources",
|
|
||||||
"TranslatesTo": "http://concelier.stella-ops.local/api/v1/advisory-sources",
|
|
||||||
"PreserveAuthHeaders": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/release-control",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/release-control",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/platform",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/platform",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/scanner",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/scanner",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/findings",
|
|
||||||
"TranslatesTo": "http://findings.stella-ops.local/api/v1/findings",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/integrations",
|
|
||||||
"TranslatesTo": "http://integrations.stella-ops.local/api/v1/integrations",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/policy",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/policy",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/reachability",
|
|
||||||
"TranslatesTo": "http://reachgraph.stella-ops.local/api/v1/reachability",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/attestor",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestor",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/attestations",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestations",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/sbom",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sbom",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/signals",
|
|
||||||
"TranslatesTo": "http://signals.stella-ops.local/signals",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/jobengine",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/v1/jobengine",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/authority/quotas",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/authority",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/api/v1/authority",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/trust",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/api/v1/trust",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/evidence",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/evidence",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/proofs",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/proofs",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/timeline",
|
|
||||||
"TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/audit",
|
|
||||||
"TranslatesTo": "http://timeline.stella-ops.local/api/v1/audit",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/advisory-ai/adapters",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai/adapters",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/search",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/search",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/advisory-ai",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/advisory",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/vulnerabilities",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/watchlist",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local/api/v1/watchlist",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/resolve",
|
|
||||||
"TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/ops/binaryindex",
|
|
||||||
"TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/ops/binaryindex",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/verdicts",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/v1/verdicts",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/lineage",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/lineage",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/export",
|
|
||||||
"TranslatesTo": "https://exportcenter.stella-ops.local/api/v1/export",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/triage",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/governance",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/governance",
|
|
||||||
"PreserveAuthHeaders": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/determinization",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local/api/v1/determinization",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/opsmemory",
|
|
||||||
"TranslatesTo": "http://opsmemory.stella-ops.local/api/v1/opsmemory",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/secrets",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/secrets",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/sources",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/sources",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/workflows",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/v1/workflows",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/witnesses",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/v1/witnesses",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/v1/evidence-packs",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/v1/runs",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/v1/runs",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/v1/advisory-ai/adapters",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai/adapters",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/v1/advisory-ai",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/v1/audit-bundles",
|
|
||||||
"TranslatesTo": "https://exportcenter.stella-ops.local/v1/audit-bundles",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/policy",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/cvss",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/cvss",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/policy/simulations",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/policy/simulations",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/policy/shadow",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/policy/shadow",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/policy",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy",
|
|
||||||
"PreserveAuthHeaders": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/risk",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local/api/risk",
|
|
||||||
"PreserveAuthHeaders": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/analytics",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/analytics",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/release-orchestrator",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/release-orchestrator",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/releases",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/releases",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/approvals",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/approvals",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/gate",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/gate",
|
|
||||||
"PreserveAuthHeaders": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/risk-budget",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local/api/risk-budget",
|
|
||||||
"PreserveAuthHeaders": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/fix-verification",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local/api/fix-verification",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/compare",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/compare",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/change-traces",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/change-traces",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/exceptions",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local/api/exceptions",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/verdicts",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local/api/verdicts",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/jobengine",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local/api/jobengine",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/gateway/rate-limits",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v1/gateway/rate-limits",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/sbomservice",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local/api/sbomservice",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/vuln-explorer",
|
|
||||||
"TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/vex",
|
|
||||||
"TranslatesTo": "https://vexhub.stella-ops.local/api/vex",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/admin/plans",
|
|
||||||
"TranslatesTo": "http://registry-token.stella-ops.local/api/admin/plans",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/admin",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/admin",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/scheduler",
|
|
||||||
"TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v1/doctor/scheduler",
|
|
||||||
"TranslatesTo": "http://doctor-scheduler.stella-ops.local/api/v1/doctor/scheduler",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/doctor",
|
|
||||||
"TranslatesTo": "http://doctor.stella-ops.local/api/doctor",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/context",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/context",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/releases",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/releases",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/security",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/security",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/topology",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/topology",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api/v2/integrations",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api/v2/integrations",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/api",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/api",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "StaticFile",
|
|
||||||
"Path": "/platform/envsettings.json",
|
|
||||||
"TranslatesTo": "/app/envsettings-override.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/platform",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/platform"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/connect",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/connect",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/.well-known",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/well-known",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/jwks",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/jwks",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/authority/console",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/console",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/authority",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/authority",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/console",
|
|
||||||
"TranslatesTo": "https://authority.stella-ops.local/console",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/rekor",
|
|
||||||
"TranslatesTo": "http://rekor.stella-ops.local:3322"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/envsettings.json",
|
|
||||||
"TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/scanner",
|
|
||||||
"TranslatesTo": "http://scanner.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/policyGateway",
|
|
||||||
"TranslatesTo": "http://policy-gateway.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/policyEngine",
|
|
||||||
"TranslatesTo": "http://policy-engine.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/concelier",
|
|
||||||
"TranslatesTo": "http://concelier.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/attestor",
|
|
||||||
"TranslatesTo": "http://attestor.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/notify",
|
|
||||||
"TranslatesTo": "http://notify.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/notifier",
|
|
||||||
"TranslatesTo": "http://notifier.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/scheduler",
|
|
||||||
"TranslatesTo": "http://scheduler.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/signals",
|
|
||||||
"TranslatesTo": "http://signals.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/excititor",
|
|
||||||
"TranslatesTo": "http://excititor.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/findingsLedger",
|
|
||||||
"TranslatesTo": "http://findings.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/vexhub",
|
|
||||||
"TranslatesTo": "https://vexhub.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/vexlens",
|
|
||||||
"TranslatesTo": "http://vexlens.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/jobengine",
|
|
||||||
"TranslatesTo": "http://jobengine.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/taskrunner",
|
|
||||||
"TranslatesTo": "http://taskrunner.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/cartographer",
|
|
||||||
"TranslatesTo": "http://cartographer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/reachgraph",
|
|
||||||
"TranslatesTo": "http://reachgraph.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/doctor",
|
|
||||||
"TranslatesTo": "http://doctor.stella-ops.local",
|
|
||||||
"PreserveAuthHeaders": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/integrations",
|
|
||||||
"TranslatesTo": "http://integrations.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/replay",
|
|
||||||
"TranslatesTo": "http://replay.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/exportcenter",
|
|
||||||
"TranslatesTo": "https://exportcenter.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/evidencelocker",
|
|
||||||
"TranslatesTo": "https://evidencelocker.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/signer",
|
|
||||||
"TranslatesTo": "http://signer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/binaryindex",
|
|
||||||
"TranslatesTo": "http://binaryindex.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/riskengine",
|
|
||||||
"TranslatesTo": "http://riskengine.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/vulnexplorer",
|
|
||||||
"TranslatesTo": "http://vulnexplorer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/sbomservice",
|
|
||||||
"TranslatesTo": "http://sbomservice.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/advisoryai",
|
|
||||||
"TranslatesTo": "http://advisoryai.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/unknowns",
|
|
||||||
"TranslatesTo": "http://unknowns.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/timelineindexer",
|
|
||||||
"TranslatesTo": "http://timelineindexer.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/opsmemory",
|
|
||||||
"TranslatesTo": "http://opsmemory.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/issuerdirectory",
|
|
||||||
"TranslatesTo": "http://issuerdirectory.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/symbols",
|
|
||||||
"TranslatesTo": "http://symbols.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/packsregistry",
|
|
||||||
"TranslatesTo": "http://packsregistry.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/registryTokenservice",
|
|
||||||
"TranslatesTo": "http://registry-token.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/airgapController",
|
|
||||||
"TranslatesTo": "http://airgap-controller.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/airgapTime",
|
|
||||||
"TranslatesTo": "http://airgap-time.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ReverseProxy",
|
|
||||||
"Path": "/smremote",
|
|
||||||
"TranslatesTo": "http://smremote.stella-ops.local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "StaticFiles",
|
|
||||||
"Path": "/",
|
|
||||||
"TranslatesTo": "/app/wwwroot",
|
|
||||||
"Headers": {
|
|
||||||
"x-spa-fallback": "true"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "NotFoundPage",
|
|
||||||
"Path": "/_error/404",
|
|
||||||
"TranslatesTo": "/app/wwwroot/index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Type": "ServerErrorPage",
|
|
||||||
"Path": "/_error/500",
|
|
||||||
"TranslatesTo": "/app/wwwroot/index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Microsoft.AspNetCore.Authentication": "Information",
|
|
||||||
"Microsoft.IdentityModel": "Information",
|
|
||||||
"StellaOps": "Information"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
param(
|
param(
|
||||||
[ValidateSet("microservice", "reverseproxy")]
|
[ValidateSet("microservice")]
|
||||||
[string]$Mode = "microservice",
|
[string]$Mode = "microservice",
|
||||||
[string]$ComposeFile = "docker-compose.stella-ops.yml",
|
[string]$ComposeFile = "docker-compose.stella-ops.yml",
|
||||||
[int]$WaitTimeoutSeconds = 1200,
|
[int]$WaitTimeoutSeconds = 1200,
|
||||||
@@ -25,7 +25,6 @@ if (-not (Test-Path -LiteralPath $resolvedComposeFile)) {
|
|||||||
|
|
||||||
$configFileName = switch ($Mode) {
|
$configFileName = switch ($Mode) {
|
||||||
"microservice" { "router-gateway-local.json" }
|
"microservice" { "router-gateway-local.json" }
|
||||||
"reverseproxy" { "router-gateway-local.reverseproxy.json" }
|
|
||||||
default { throw "Unsupported mode: $Mode" }
|
default { throw "Unsupported mode: $Mode" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Sprint 20260310-001 - Router Frontdoor Required-Service Readiness
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Replace the gateway's shallow "listener started" readiness contract with a required-service registration gate so scratch rebuilds do not expose first-party Stella routes before their router HELLO registrations exist.
|
||||||
|
- Return truthful `503` responses for matched microservice routes whose target service is not yet registered instead of misleading `404` errors that make reverse proxy look safer than router transport.
|
||||||
|
- Keep reverse proxy limited to external/bootstrap surfaces and document the rule explicitly for the local compose frontdoor.
|
||||||
|
- Working directory: `src/Router`.
|
||||||
|
- Allowed coordination edits: `devops/compose/docker-compose.stella-ops.yml`, `devops/compose/router-gateway-local.json`, `devops/compose/README.md`, `devops/compose/env/stellaops.env.example`, `docs/modules/router/architecture.md`, `docs/implplan/SPRINT_20260310_001_Router_frontdoor_required_service_readiness.md`.
|
||||||
|
- Expected evidence: focused router tests, live gateway readiness probes before/after restart, and a rerun of the affected Playwright/live route checks after redeploy.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Follows `SPRINT_20260309_008_Router_live_messaging_heartbeat_contract_repair.md`, which already narrowed the remaining post-redeploy failures to startup/readiness convergence.
|
||||||
|
- Safe parallelism: stay inside the router slice and the listed compose/docs files; do not touch unrelated search, reachability, or general frontend feature work.
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `src/Router/AGENTS.md`
|
||||||
|
- `src/Router/StellaOps.Gateway.WebService/AGENTS.md`
|
||||||
|
- `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/AGENTS.md`
|
||||||
|
- `docs/modules/router/architecture.md`
|
||||||
|
- `docs/modules/router/webservices-valkey-rollout-matrix.md`
|
||||||
|
- `docs/qa/feature-checks/FLOW.md`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
|
||||||
|
### ROUTER-READY-001 - Add required-service readiness evaluation
|
||||||
|
Status: DONE
|
||||||
|
Dependency: none
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- Introduce a source-level readiness evaluator that keeps `/health/ready` false until the configured required first-party microservices have live healthy/degraded router registrations.
|
||||||
|
- Preserve environment ownership of the required-service list so the local scratch compose stack can demand a stricter frontdoor than lighter dev configurations.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Gateway health options support a required microservice list.
|
||||||
|
- [x] `/health/ready` returns `503` with missing-service details until all configured required services are registered.
|
||||||
|
- [x] Focused router tests cover both missing and satisfied readiness states.
|
||||||
|
|
||||||
|
### ROUTER-READY-002 - Return truthful warm-up failures for missing target registrations
|
||||||
|
Status: DONE
|
||||||
|
Dependency: ROUTER-READY-001
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- When a route is already classified as `Microservice` but the target service has not registered, return a service-unavailable contract instead of `404`.
|
||||||
|
- Keep `404` only for genuinely unknown paths or endpoints that do not exist on a registered target service.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Targeted microservice-route misses return `503`.
|
||||||
|
- [x] Registered target service with a missing endpoint still returns `404`.
|
||||||
|
- [x] Focused middleware tests prove the distinction.
|
||||||
|
|
||||||
|
### ROUTER-READY-003 - Make scratch compose wait for the real frontdoor
|
||||||
|
Status: DONE
|
||||||
|
Dependency: ROUTER-READY-002
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- Update the mounted local router config with the required-service list for the client-ready scratch stack and make the router-gateway container healthcheck probe `/health/ready` instead of only testing for an open TCP port.
|
||||||
|
- Document the reverse-proxy exception rule: external/bootstrap only, first-party Stella APIs through router transport.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] `router-gateway-local.json` declares the required first-party services for the local scratch stack.
|
||||||
|
- [x] `docker-compose.stella-ops.yml` checks router readiness instead of raw port openness.
|
||||||
|
- [x] Router architecture docs describe the readiness gate and the reverse-proxy exception rule.
|
||||||
|
|
||||||
|
### ROUTER-READY-004 - Bound microservice HELLO recovery after gateway restart
|
||||||
|
Status: DONE
|
||||||
|
Dependency: ROUTER-READY-003
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- Remove the hidden fixed 30-heartbeat HELLO replay heuristic from the microservice SDK and replace it with an explicit registration refresh interval that repopulates gateway state within seconds after a gateway restart.
|
||||||
|
- Flow the setting through the shared ASP.NET router integration so services can keep the default bounded contract or override it intentionally.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Stella microservice options expose a positive registration refresh interval.
|
||||||
|
- [x] Router connection manager replays HELLO on the configured interval without waiting for dozens of heartbeats.
|
||||||
|
- [x] Focused SDK and integration-helper tests cover the new contract.
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-03-10 | Sprint created after live evidence showed the gateway was returning `404 TargetService=(none)` during post-redeploy convergence even though the mounted route table and aggregated OpenAPI already knew the affected first-party paths. | Developer |
|
||||||
|
| 2026-03-10 | Live restart evidence showed the deeper recovery gap: services only replayed HELLO every 30 heartbeats, leaving the gateway honestly unready for minutes after restart. Added a bounded HELLO refresh task under the same sprint. | Developer |
|
||||||
|
| 2026-03-10 | Audited the frontdoor refactor end to end: focused router tests passed, fresh-stack redeploy converged on `/health/ready`, restart probes now return `503` for missing target registrations before flipping to endpoint-level `404`, and the Playwright canonical route sweep rerun isolated the remaining failures to unrelated frontend routes under `/ops/policy`, `/ops/operations/*`, and trust-signing. | Developer |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Decision: readiness is environment-owned. The gateway source exposes the contract, while the local compose stack opts into a concrete required-service list for scratch QA.
|
||||||
|
- Decision: reverse proxy remains valid for external/bootstrap surfaces such as Rekor, OIDC/browser flows, and SPA/static assets; it is not the preferred path for first-party Stella APIs.
|
||||||
|
- Decision: HELLO recovery is now time-based and explicit rather than a hidden multiple of heartbeat count. The default registration refresh interval is 10 seconds so a gateway restart cannot strand first-party routes behind stale state for minutes.
|
||||||
|
- Decision: the dedicated `router-gateway-local.reverseproxy.json` fallback mode is removed from active compose guidance. The supported scratch stack uses the microservice-first table with narrowly-scoped reverse proxy exceptions inside the same config.
|
||||||
|
- Risk: if the required-service list is too broad for the current compose footprint, `/health/ready` could remain false. Mitigation: use the actual mounted local stack as the authority and verify registrations live after redeploy.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- 2026-03-10: land readiness evaluation and route-level `503` contract.
|
||||||
|
- 2026-03-10: rebuild router-gateway, redeploy, and verify restart behavior with live probes.
|
||||||
|
- 2026-03-10: rerun the targeted Playwright/router checks on the warmed stack.
|
||||||
@@ -85,6 +85,8 @@ Route types:
|
|||||||
| `NotFoundPage` | HTML file served on 404 (after all other middleware) |
|
| `NotFoundPage` | HTML file served on 404 (after all other middleware) |
|
||||||
| `ServerErrorPage` | HTML file served on 5xx (after all other middleware) |
|
| `ServerErrorPage` | HTML file served on 5xx (after all other middleware) |
|
||||||
|
|
||||||
|
Reverse proxy is reserved for external/bootstrap surfaces such as OIDC browser flows, Rekor, and frontdoor static assets. First-party Stella API surfaces are expected to use `Microservice` routing so the gateway remains the single routing authority instead of silently bypassing router registration state.
|
||||||
|
|
||||||
### Pipeline Order
|
### Pipeline Order
|
||||||
|
|
||||||
System paths (`/health`, `/metrics`, `/openapi.*`) bypass the route table entirely. The dispatch middleware runs before the microservice pipeline:
|
System paths (`/health`, `/metrics`, `/openapi.*`) bypass the route table entirely. The dispatch middleware runs before the microservice pipeline:
|
||||||
@@ -540,6 +542,9 @@ Gateway tracks:
|
|||||||
- Uses health in routing decisions
|
- Uses health in routing decisions
|
||||||
- Messaging transports stay push-first even when backed by notifiable queues; the missed-notification safety-net timeout is derived from the configured heartbeat interval and clamped to a short bounded window instead of falling back to a fixed long poll.
|
- Messaging transports stay push-first even when backed by notifiable queues; the missed-notification safety-net timeout is derived from the configured heartbeat interval and clamped to a short bounded window instead of falling back to a fixed long poll.
|
||||||
- Gateway degraded and stale transitions are normalized against the messaging heartbeat contract. A gateway may not mark an instance `Degraded` earlier than `2x` the heartbeat interval or `Unhealthy` earlier than `3x` the heartbeat interval, even when looser defaults were configured.
|
- Gateway degraded and stale transitions are normalized against the messaging heartbeat contract. A gateway may not mark an instance `Degraded` earlier than `2x` the heartbeat interval or `Unhealthy` earlier than `3x` the heartbeat interval, even when looser defaults were configured.
|
||||||
|
- `/health/ready` is stricter than "process started": it remains `503` until the configured required first-party microservices have live healthy or degraded registrations in router state. Local scratch compose uses this to hold the frontdoor unhealthy until the core Stella API surface has replayed HELLO after a rebuild.
|
||||||
|
- The required-service list must use canonical router `serviceName` values, not loose product-family aliases. Gateway readiness normalizes host-style suffixes such as `-gateway`, `-web`, `.stella-ops.local`, and ports, but it does not treat sibling services as interchangeable.
|
||||||
|
- When a request already matched a configured `Microservice` route but the target service has not registered yet, the gateway returns `503 Service Unavailable`, not `404 Not Found`. `404` remains reserved for genuinely unknown paths or missing endpoints on an otherwise registered service.
|
||||||
|
|
||||||
Periodic HELLO re-registration is valid so a microservice can repopulate gateway state after a gateway restart, but it must refresh the existing logical transport connection instead of minting a second one. Gateway routing state also deduplicates by service instance identity (`ServiceName`, `Version`, `InstanceId`, transport) before re-indexing endpoints so repeated HELLO frames cannot accumulate stale route candidates.
|
Periodic HELLO re-registration is valid so a microservice can repopulate gateway state after a gateway restart, but it must refresh the existing logical transport connection instead of minting a second one. Gateway routing state also deduplicates by service instance identity (`ServiceName`, `Version`, `InstanceId`, transport) before re-indexing endpoints so repeated HELLO frames cannot accumulate stale route candidates.
|
||||||
|
|
||||||
|
|||||||
@@ -268,4 +268,6 @@ public sealed class GatewayHealthOptions
|
|||||||
public string DegradedThreshold { get; set; } = "15s";
|
public string DegradedThreshold { get; set; } = "15s";
|
||||||
|
|
||||||
public string CheckInterval { get; set; } = "5s";
|
public string CheckInterval { get; set; } = "5s";
|
||||||
|
|
||||||
|
public List<string> RequiredMicroservices { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ public static class GatewayOptionsValidator
|
|||||||
_ = GatewayValueParser.ParseDuration(options.Health.DegradedThreshold, TimeSpan.FromSeconds(15));
|
_ = GatewayValueParser.ParseDuration(options.Health.DegradedThreshold, TimeSpan.FromSeconds(15));
|
||||||
_ = GatewayValueParser.ParseDuration(options.Health.CheckInterval, TimeSpan.FromSeconds(5));
|
_ = GatewayValueParser.ParseDuration(options.Health.CheckInterval, TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
if (options.Health.RequiredMicroservices.Any(service => string.IsNullOrWhiteSpace(service)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Gateway health required microservices must not contain empty values.");
|
||||||
|
}
|
||||||
|
|
||||||
ValidateRoutes(options.Routes);
|
ValidateRoutes(options.Routes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +138,23 @@ public static class GatewayOptionsValidator
|
|||||||
{
|
{
|
||||||
_ = GatewayValueParser.ParseDuration(route.DefaultTimeout, TimeSpan.FromSeconds(30));
|
_ = GatewayValueParser.ParseDuration(route.DefaultTimeout, TimeSpan.FromSeconds(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route.IsRegex && !string.IsNullOrWhiteSpace(route.TranslatesTo))
|
||||||
|
{
|
||||||
|
var regex = new Regex(route.Path);
|
||||||
|
var groupCount = regex.GetGroupNumbers().Length;
|
||||||
|
var refs = Regex.Matches(route.TranslatesTo, @"\$(\d+)");
|
||||||
|
foreach (Match refMatch in refs)
|
||||||
|
{
|
||||||
|
var groupNum = int.Parse(refMatch.Groups[1].Value);
|
||||||
|
if (groupNum >= groupCount)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{prefix}: TranslatesTo references ${groupNum} but regex only has {groupCount - 1} capture groups.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ public sealed class HealthCheckMiddleware
|
|||||||
_next = next;
|
_next = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context, GatewayServiceStatus status, GatewayMetrics metrics)
|
public async Task InvokeAsync(
|
||||||
|
HttpContext context,
|
||||||
|
GatewayServiceStatus status,
|
||||||
|
GatewayMetrics metrics,
|
||||||
|
GatewayReadinessEvaluator readinessEvaluator)
|
||||||
{
|
{
|
||||||
if (GatewayRoutes.IsMetricsPath(context.Request.Path))
|
if (GatewayRoutes.IsMetricsPath(context.Request.Path))
|
||||||
{
|
{
|
||||||
@@ -37,28 +41,34 @@ public sealed class HealthCheckMiddleware
|
|||||||
var path = context.Request.Path.Value ?? string.Empty;
|
var path = context.Request.Path.Value ?? string.Empty;
|
||||||
if (path.Equals("/health/live", StringComparison.OrdinalIgnoreCase))
|
if (path.Equals("/health/live", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await WriteHealthAsync(context, StatusCodes.Status200OK, "live", status);
|
await WriteHealthAsync(context, StatusCodes.Status200OK, "live", status, readinessEvaluator.Evaluate(status));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.Equals("/health/ready", StringComparison.OrdinalIgnoreCase))
|
if (path.Equals("/health/ready", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var readyStatus = status.IsReady ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
var readiness = readinessEvaluator.Evaluate(status);
|
||||||
await WriteHealthAsync(context, readyStatus, "ready", status);
|
var readyStatus = readiness.IsReady ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
||||||
|
await WriteHealthAsync(context, readyStatus, "ready", status, readiness);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.Equals("/health/startup", StringComparison.OrdinalIgnoreCase))
|
if (path.Equals("/health/startup", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var startupStatus = status.IsStarted ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
var startupStatus = status.IsStarted ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
||||||
await WriteHealthAsync(context, startupStatus, "startup", status);
|
await WriteHealthAsync(context, startupStatus, "startup", status, readinessEvaluator.Evaluate(status));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await WriteHealthAsync(context, StatusCodes.Status200OK, "ok", status);
|
await WriteHealthAsync(context, StatusCodes.Status200OK, "ok", status, readinessEvaluator.Evaluate(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task WriteHealthAsync(HttpContext context, int statusCode, string status, GatewayServiceStatus serviceStatus)
|
private static Task WriteHealthAsync(
|
||||||
|
HttpContext context,
|
||||||
|
int statusCode,
|
||||||
|
string status,
|
||||||
|
GatewayServiceStatus serviceStatus,
|
||||||
|
GatewayReadinessReport readiness)
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = statusCode;
|
context.Response.StatusCode = statusCode;
|
||||||
context.Response.ContentType = "application/json; charset=utf-8";
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
@@ -67,7 +77,10 @@ public sealed class HealthCheckMiddleware
|
|||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
started = serviceStatus.IsStarted,
|
started = serviceStatus.IsStarted,
|
||||||
ready = serviceStatus.IsReady,
|
ready = readiness.IsReady,
|
||||||
|
transportReady = serviceStatus.IsReady,
|
||||||
|
requiredMicroservices = readiness.RequiredMicroservices,
|
||||||
|
missingMicroservices = readiness.MissingMicroservices,
|
||||||
traceId = context.TraceIdentifier
|
traceId = context.TraceIdentifier
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using StellaOps.Gateway.WebService.Configuration;
|
using StellaOps.Gateway.WebService.Configuration;
|
||||||
@@ -47,7 +48,7 @@ public sealed class RouteDispatchMiddleware
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var route = _resolver.Resolve(context.Request.Path);
|
var (route, regexMatch) = _resolver.Resolve(context.Request.Path);
|
||||||
if (route is null)
|
if (route is null)
|
||||||
{
|
{
|
||||||
await _next(context);
|
await _next(context);
|
||||||
@@ -83,13 +84,13 @@ public sealed class RouteDispatchMiddleware
|
|||||||
await HandleStaticFile(context, route);
|
await HandleStaticFile(context, route);
|
||||||
break;
|
break;
|
||||||
case StellaOpsRouteType.ReverseProxy:
|
case StellaOpsRouteType.ReverseProxy:
|
||||||
await HandleReverseProxy(context, route);
|
await HandleReverseProxy(context, route, regexMatch);
|
||||||
break;
|
break;
|
||||||
case StellaOpsRouteType.WebSocket:
|
case StellaOpsRouteType.WebSocket:
|
||||||
await HandleWebSocket(context, route);
|
await HandleWebSocket(context, route);
|
||||||
break;
|
break;
|
||||||
case StellaOpsRouteType.Microservice:
|
case StellaOpsRouteType.Microservice:
|
||||||
PrepareMicroserviceRoute(context, route);
|
PrepareMicroserviceRoute(context, route, regexMatch);
|
||||||
await _next(context);
|
await _next(context);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -178,17 +179,24 @@ public sealed class RouteDispatchMiddleware
|
|||||||
await stream.CopyToAsync(context.Response.Body, context.RequestAborted);
|
await stream.CopyToAsync(context.Response.Body, context.RequestAborted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleReverseProxy(HttpContext context, StellaOpsRoute route)
|
private async Task HandleReverseProxy(HttpContext context, StellaOpsRoute route, Match? regexMatch)
|
||||||
{
|
{
|
||||||
var requestPath = context.Request.Path.Value ?? string.Empty;
|
var requestPath = context.Request.Path.Value ?? string.Empty;
|
||||||
|
var resolvedTranslatesTo = ResolveCaptureGroups(route.TranslatesTo, regexMatch);
|
||||||
|
var captureGroupsResolved = !string.Equals(resolvedTranslatesTo, route.TranslatesTo, StringComparison.Ordinal);
|
||||||
var remainingPath = requestPath;
|
var remainingPath = requestPath;
|
||||||
|
|
||||||
if (!route.IsRegex && requestPath.StartsWith(route.Path, StringComparison.OrdinalIgnoreCase))
|
if (captureGroupsResolved)
|
||||||
|
{
|
||||||
|
// Capture groups resolved: TranslatesTo already contains the full target path.
|
||||||
|
remainingPath = string.Empty;
|
||||||
|
}
|
||||||
|
else if (!route.IsRegex && requestPath.StartsWith(route.Path, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
remainingPath = requestPath[route.Path.Length..];
|
remainingPath = requestPath[route.Path.Length..];
|
||||||
}
|
}
|
||||||
|
|
||||||
var upstreamBase = route.TranslatesTo!.TrimEnd('/');
|
var upstreamBase = resolvedTranslatesTo!.TrimEnd('/');
|
||||||
var upstreamUri = new Uri($"{upstreamBase}{remainingPath}{context.Request.QueryString}");
|
var upstreamUri = new Uri($"{upstreamBase}{remainingPath}{context.Request.QueryString}");
|
||||||
|
|
||||||
var client = _httpClientFactory.CreateClient("RouteDispatch");
|
var client = _httpClientFactory.CreateClient("RouteDispatch");
|
||||||
@@ -279,15 +287,34 @@ public sealed class RouteDispatchMiddleware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PrepareMicroserviceRoute(HttpContext context, StellaOpsRoute route)
|
private static void PrepareMicroserviceRoute(HttpContext context, StellaOpsRoute route, Match? regexMatch)
|
||||||
{
|
{
|
||||||
var translatedPath = ResolveTranslatedMicroservicePath(context.Request.Path.Value, route);
|
// If regex route with capture groups, resolve $1/$2/etc. in TranslatesTo
|
||||||
|
var effectiveRoute = route;
|
||||||
|
if (regexMatch is not null && !string.IsNullOrWhiteSpace(route.TranslatesTo))
|
||||||
|
{
|
||||||
|
var resolvedTranslatesTo = ResolveCaptureGroups(route.TranslatesTo, regexMatch);
|
||||||
|
if (!string.Equals(resolvedTranslatesTo, route.TranslatesTo, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
effectiveRoute = new StellaOpsRoute
|
||||||
|
{
|
||||||
|
Type = route.Type,
|
||||||
|
Path = route.Path,
|
||||||
|
IsRegex = route.IsRegex,
|
||||||
|
TranslatesTo = resolvedTranslatesTo,
|
||||||
|
DefaultTimeout = route.DefaultTimeout,
|
||||||
|
PreserveAuthHeaders = route.PreserveAuthHeaders
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var translatedPath = ResolveTranslatedMicroservicePath(context.Request.Path.Value, effectiveRoute);
|
||||||
if (!string.Equals(translatedPath, context.Request.Path.Value, StringComparison.Ordinal))
|
if (!string.Equals(translatedPath, context.Request.Path.Value, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
context.Items[RouterHttpContextKeys.TranslatedRequestPath] = translatedPath;
|
context.Items[RouterHttpContextKeys.TranslatedRequestPath] = translatedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetMicroservice = ResolveRouteTargetMicroservice(route);
|
var targetMicroservice = ResolveRouteTargetMicroservice(effectiveRoute);
|
||||||
if (!string.IsNullOrWhiteSpace(targetMicroservice))
|
if (!string.IsNullOrWhiteSpace(targetMicroservice))
|
||||||
{
|
{
|
||||||
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] = targetMicroservice;
|
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] = targetMicroservice;
|
||||||
@@ -300,6 +327,22 @@ public sealed class RouteDispatchMiddleware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? ResolveCaptureGroups(string? translatesTo, Match? regexMatch)
|
||||||
|
{
|
||||||
|
if (regexMatch is null || string.IsNullOrWhiteSpace(translatesTo))
|
||||||
|
{
|
||||||
|
return translatesTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = translatesTo;
|
||||||
|
for (var i = regexMatch.Groups.Count - 1; i >= 1; i--)
|
||||||
|
{
|
||||||
|
resolved = resolved.Replace($"${i}", regexMatch.Groups[i].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveTranslatedMicroservicePath(string? requestPathValue, StellaOpsRoute route)
|
private static string ResolveTranslatedMicroservicePath(string? requestPathValue, StellaOpsRoute route)
|
||||||
{
|
{
|
||||||
var requestPath = string.IsNullOrWhiteSpace(requestPathValue) ? "/" : requestPathValue!;
|
var requestPath = string.IsNullOrWhiteSpace(requestPathValue) ? "/" : requestPathValue!;
|
||||||
@@ -314,12 +357,18 @@ public sealed class RouteDispatchMiddleware
|
|||||||
return requestPath;
|
return requestPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For regex routes, the TranslatesTo (after capture group substitution)
|
||||||
|
// already contains the full target path. Use it directly.
|
||||||
|
if (route.IsRegex)
|
||||||
|
{
|
||||||
|
return NormalizePath(targetPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
var normalizedRoutePath = NormalizePath(route.Path);
|
var normalizedRoutePath = NormalizePath(route.Path);
|
||||||
var normalizedRequestPath = NormalizePath(requestPath);
|
var normalizedRequestPath = NormalizePath(requestPath);
|
||||||
var remainingPath = normalizedRequestPath;
|
var remainingPath = normalizedRequestPath;
|
||||||
|
|
||||||
if (!route.IsRegex &&
|
if (normalizedRequestPath.StartsWith(normalizedRoutePath, StringComparison.OrdinalIgnoreCase))
|
||||||
normalizedRequestPath.StartsWith(normalizedRoutePath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
remainingPath = normalizedRequestPath[normalizedRoutePath.Length..];
|
remainingPath = normalizedRequestPath[normalizedRoutePath.Length..];
|
||||||
if (!remainingPath.StartsWith('/'))
|
if (!remainingPath.StartsWith('/'))
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ builder.Services.Replace(ServiceDescriptor.Singleton<StellaOps.Router.Gateway.Au
|
|||||||
|
|
||||||
builder.Services.AddSingleton<GatewayServiceStatus>();
|
builder.Services.AddSingleton<GatewayServiceStatus>();
|
||||||
builder.Services.AddSingleton<GatewayMetrics>();
|
builder.Services.AddSingleton<GatewayMetrics>();
|
||||||
|
builder.Services.AddSingleton<GatewayReadinessEvaluator>();
|
||||||
|
|
||||||
// Load router transport plugins
|
// Load router transport plugins
|
||||||
var transportPluginLoader = new RouterTransportPluginLoader(
|
var transportPluginLoader = new RouterTransportPluginLoader(
|
||||||
@@ -128,7 +129,7 @@ builder.Services.AddSingleton(new IdentityHeaderPolicyOptions
|
|||||||
IdentityEnvelopeIssuer = bootstrapOptions.Auth.IdentityEnvelopeIssuer,
|
IdentityEnvelopeIssuer = bootstrapOptions.Auth.IdentityEnvelopeIssuer,
|
||||||
IdentityEnvelopeTtl = TimeSpan.FromSeconds(Math.Max(1, bootstrapOptions.Auth.IdentityEnvelopeTtlSeconds)),
|
IdentityEnvelopeTtl = TimeSpan.FromSeconds(Math.Max(1, bootstrapOptions.Auth.IdentityEnvelopeTtlSeconds)),
|
||||||
JwtPassthroughPrefixes = bootstrapOptions.Routes
|
JwtPassthroughPrefixes = bootstrapOptions.Routes
|
||||||
.Where(r => r.PreserveAuthHeaders)
|
.Where(r => r.PreserveAuthHeaders && !r.IsRegex)
|
||||||
.Select(r => r.Path)
|
.Select(r => r.Path)
|
||||||
.ToList(),
|
.ToList(),
|
||||||
ApprovedAuthPassthroughPrefixes = [.. bootstrapOptions.Auth.ApprovedAuthPassthroughPrefixes],
|
ApprovedAuthPassthroughPrefixes = [.. bootstrapOptions.Auth.ApprovedAuthPassthroughPrefixes],
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed class StellaOpsRouteResolver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public StellaOpsRoute? Resolve(PathString path)
|
public (StellaOpsRoute? Route, Match? RegexMatch) Resolve(PathString path)
|
||||||
{
|
{
|
||||||
var pathValue = path.Value ?? string.Empty;
|
var pathValue = path.Value ?? string.Empty;
|
||||||
|
|
||||||
@@ -35,9 +35,10 @@ public sealed class StellaOpsRouteResolver
|
|||||||
{
|
{
|
||||||
if (pattern is not null)
|
if (pattern is not null)
|
||||||
{
|
{
|
||||||
if (pattern.IsMatch(pathValue))
|
var match = pattern.Match(pathValue);
|
||||||
|
if (match.Success)
|
||||||
{
|
{
|
||||||
return route;
|
return (route, match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -47,12 +48,12 @@ public sealed class StellaOpsRouteResolver
|
|||||||
pathValue.StartsWith(route.Path, StringComparison.OrdinalIgnoreCase) &&
|
pathValue.StartsWith(route.Path, StringComparison.OrdinalIgnoreCase) &&
|
||||||
route.Path.EndsWith('/'))
|
route.Path.EndsWith('/'))
|
||||||
{
|
{
|
||||||
return route;
|
return (route, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using StellaOps.Gateway.WebService.Configuration;
|
||||||
|
using StellaOps.Router.Common.Abstractions;
|
||||||
|
using StellaOps.Router.Common.Enums;
|
||||||
|
|
||||||
|
namespace StellaOps.Gateway.WebService.Services;
|
||||||
|
|
||||||
|
public sealed record GatewayReadinessReport
|
||||||
|
{
|
||||||
|
public required bool IsReady { get; init; }
|
||||||
|
|
||||||
|
public required bool Started { get; init; }
|
||||||
|
|
||||||
|
public required bool TransportReady { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyList<string> RequiredMicroservices { get; init; } = [];
|
||||||
|
|
||||||
|
public IReadOnlyList<string> MissingMicroservices { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GatewayReadinessEvaluator
|
||||||
|
{
|
||||||
|
private readonly IGlobalRoutingState _routingState;
|
||||||
|
private readonly IOptions<GatewayOptions> _options;
|
||||||
|
|
||||||
|
public GatewayReadinessEvaluator(IGlobalRoutingState routingState, IOptions<GatewayOptions> options)
|
||||||
|
{
|
||||||
|
_routingState = routingState;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GatewayReadinessReport Evaluate(GatewayServiceStatus status)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
|
|
||||||
|
var requiredMicroservices = NormalizeRequiredMicroservices(_options.Value.Health.RequiredMicroservices);
|
||||||
|
if (!status.IsStarted || !status.IsReady)
|
||||||
|
{
|
||||||
|
return new GatewayReadinessReport
|
||||||
|
{
|
||||||
|
IsReady = false,
|
||||||
|
Started = status.IsStarted,
|
||||||
|
TransportReady = status.IsReady,
|
||||||
|
RequiredMicroservices = requiredMicroservices,
|
||||||
|
MissingMicroservices = requiredMicroservices
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredMicroservices.Length == 0)
|
||||||
|
{
|
||||||
|
return new GatewayReadinessReport
|
||||||
|
{
|
||||||
|
IsReady = true,
|
||||||
|
Started = true,
|
||||||
|
TransportReady = true,
|
||||||
|
RequiredMicroservices = requiredMicroservices,
|
||||||
|
MissingMicroservices = []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredServices = _routingState.GetAllConnections()
|
||||||
|
.Where(connection => connection.Status is InstanceHealthStatus.Healthy or InstanceHealthStatus.Degraded)
|
||||||
|
.Select(connection => NormalizeServiceKey(connection.Instance.ServiceName))
|
||||||
|
.Where(serviceName => !string.IsNullOrWhiteSpace(serviceName))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var missing = requiredMicroservices
|
||||||
|
.Where(required => !registeredServices.Any(registered => IsServiceMatch(registered, required)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new GatewayReadinessReport
|
||||||
|
{
|
||||||
|
IsReady = missing.Length == 0,
|
||||||
|
Started = true,
|
||||||
|
TransportReady = true,
|
||||||
|
RequiredMicroservices = requiredMicroservices,
|
||||||
|
MissingMicroservices = missing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] NormalizeRequiredMicroservices(IEnumerable<string>? services)
|
||||||
|
{
|
||||||
|
return (services ?? [])
|
||||||
|
.Select(NormalizeServiceKey)
|
||||||
|
.Where(serviceName => !string.IsNullOrWhiteSpace(serviceName))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsServiceMatch(string? registeredServiceName, string requiredServiceName)
|
||||||
|
{
|
||||||
|
var normalizedRegistered = NormalizeServiceKey(registeredServiceName);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedRegistered))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(normalizedRegistered, requiredServiceName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeServiceKey(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
var portSeparator = normalized.IndexOf(':');
|
||||||
|
if (portSeparator >= 0)
|
||||||
|
{
|
||||||
|
normalized = normalized[..portSeparator];
|
||||||
|
}
|
||||||
|
|
||||||
|
const string localDomain = ".stella-ops.local";
|
||||||
|
if (normalized.EndsWith(localDomain, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
normalized = normalized[..^localDomain.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized = StripSuffix(normalized, "-web");
|
||||||
|
normalized = StripSuffix(normalized, "-api");
|
||||||
|
normalized = StripSuffix(normalized, "-service");
|
||||||
|
normalized = StripSuffix(normalized, "-gateway");
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(normalized)
|
||||||
|
? null
|
||||||
|
: normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripSuffix(string value, string suffix)
|
||||||
|
{
|
||||||
|
return value.EndsWith(suffix, StringComparison.Ordinal)
|
||||||
|
? value[..^suffix.Length]
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,125 +71,91 @@
|
|||||||
"Health": {
|
"Health": {
|
||||||
"StaleThreshold": "30s",
|
"StaleThreshold": "30s",
|
||||||
"DegradedThreshold": "20s",
|
"DegradedThreshold": "20s",
|
||||||
"CheckInterval": "5s"
|
"CheckInterval": "5s",
|
||||||
|
"RequiredMicroservices": []
|
||||||
},
|
},
|
||||||
"Routes": [
|
"Routes": [
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/release-orchestrator", "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/release-orchestrator" },
|
{ "Type": "Microservice", "Path": "^/api/v1/vulnerabilities(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/approvals", "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/approvals" },
|
{ "Type": "Microservice", "Path": "^/api/v1/watchlist(.*)", "IsRegex": true, "TranslatesTo": "http://attestor.stella-ops.local/api/v1/watchlist$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/v1/vex" },
|
{ "Type": "Microservice", "Path": "^/api/v1/triage(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local/api/v1/vexlens" },
|
{ "Type": "Microservice", "Path": "^/api/v1/secrets(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/secrets$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/notify", "TranslatesTo": "http://notify.stella-ops.local/api/v1/notify" },
|
{ "Type": "Microservice", "Path": "^/api/v1/sources(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/sources$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/notifier", "TranslatesTo": "http://notifier.stella-ops.local/api/v1/notifier" },
|
{ "Type": "Microservice", "Path": "^/api/v1/witnesses(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/witnesses$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/concelier", "TranslatesTo": "http://concelier.stella-ops.local/api/v1/concelier" },
|
{ "Type": "Microservice", "Path": "^/api/v1/trust(.*)", "IsRegex": true, "TranslatesTo": "http://authority.stella-ops.local/api/v1/trust$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/cvss", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/cvss", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/evidence(.*)", "IsRegex": true, "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/evidence$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/evidence-packs", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs" },
|
{ "Type": "Microservice", "Path": "^/api/v1/proofs(.*)", "IsRegex": true, "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/proofs$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/runs", "TranslatesTo": "http://orchestrator.stella-ops.local/v1/runs" },
|
{ "Type": "Microservice", "Path": "^/api/v1/verdicts(.*)", "IsRegex": true, "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/verdicts$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" },
|
{ "Type": "Microservice", "Path": "^/api/v1/release-orchestrator(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/release-orchestrator$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/policy/simulations", "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/simulations", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/approvals(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/approvals$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/policy/shadow", "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/shadow", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/attestations(.*)", "IsRegex": true, "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestations$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
|
{ "Type": "Microservice", "Path": "^/api/v1/sbom(.*)", "IsRegex": true, "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sbom$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/lineage(.*)", "IsRegex": true, "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/lineage$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/resolve(.*)", "IsRegex": true, "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/analytics", "TranslatesTo": "http://platform.stella-ops.local/api/analytics" },
|
{ "Type": "Microservice", "Path": "^/api/v1/ops/binaryindex(.*)", "IsRegex": true, "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/ops/binaryindex$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/release-orchestrator", "TranslatesTo": "http://jobengine.stella-ops.local/api/release-orchestrator" },
|
{ "Type": "Microservice", "Path": "^/api/v1/policy(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/policy$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/releases", "TranslatesTo": "http://jobengine.stella-ops.local/api/releases" },
|
{ "Type": "Microservice", "Path": "^/api/v1/governance(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/governance$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/approvals", "TranslatesTo": "http://jobengine.stella-ops.local/api/approvals" },
|
{ "Type": "Microservice", "Path": "^/api/v1/determinization(.*)", "IsRegex": true, "TranslatesTo": "http://policy-engine.stella-ops.local/api/v1/determinization$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/platform", "TranslatesTo": "http://platform.stella-ops.local/api/v1/platform" },
|
{ "Type": "Microservice", "Path": "^/api/v1/workflows(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/workflows$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/scanner", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/scanner" },
|
{ "Type": "Microservice", "Path": "^/api/v1/authority/quotas(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/jobengine", "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/jobengine", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/release-control(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v1/release-control$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/findings", "TranslatesTo": "http://findings.stella-ops.local/api/v1/findings", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/gateway/rate-limits(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v1/gateway/rate-limits$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/integrations", "TranslatesTo": "http://integrations.stella-ops.local/api/v1/integrations", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/reachability(.*)", "IsRegex": true, "TranslatesTo": "http://reachgraph.stella-ops.local/api/v1/reachability$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/policy" },
|
{ "Type": "Microservice", "Path": "^/api/v1/timeline(.*)", "IsRegex": true, "TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/reachability", "TranslatesTo": "http://reachgraph.stella-ops.local/api/v1/reachability" },
|
{ "Type": "Microservice", "Path": "^/api/v1/audit(.*)", "IsRegex": true, "TranslatesTo": "http://timeline.stella-ops.local/api/v1/audit$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/attestor", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestor" },
|
{ "Type": "Microservice", "Path": "^/api/v1/export(.*)", "IsRegex": true, "TranslatesTo": "http://exportcenter.stella-ops.local/api/v1/export$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/attestations", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestations" },
|
{ "Type": "Microservice", "Path": "^/api/v1/advisory-sources(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/advisory-sources$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/sbom", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sbom" },
|
{ "Type": "Microservice", "Path": "^/api/v1/notifier/delivery(.*)", "IsRegex": true, "TranslatesTo": "http://notifier.stella-ops.local/api/v2/notify/deliveries$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/signals", "TranslatesTo": "http://signals.stella-ops.local/api/v1/signals" },
|
{ "Type": "Microservice", "Path": "^/api/v1/search(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/search$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/orchestrator" },
|
{ "Type": "Microservice", "Path": "^/api/v1/advisory-ai(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority/quotas", "TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/advisory(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "http://authority.stella-ops.local/api/v1/authority", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/vex(.*)", "IsRegex": true, "TranslatesTo": "http://vexhub.stella-ops.local/api/v1/vex$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/trust", "TranslatesTo": "http://authority.stella-ops.local/api/v1/trust", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/v1/doctor/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://doctor-scheduler.stella-ops.local/api/v1/doctor/scheduler$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/evidence", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/evidence" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/proofs", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/proofs" },
|
{ "Type": "Microservice", "Path": "^/api/v2/context(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/context$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/timeline", "TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline" },
|
{ "Type": "Microservice", "Path": "^/api/v2/releases(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/releases$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/search", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/search" },
|
{ "Type": "Microservice", "Path": "^/api/v2/security(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/security$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" },
|
{ "Type": "Microservice", "Path": "^/api/v2/topology(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/topology$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/advisory", "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory" },
|
{ "Type": "Microservice", "Path": "^/api/v2/integrations(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/integrations$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vulnerabilities", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/watchlist", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/watchlist" },
|
{ "Type": "Microservice", "Path": "^/api/v1/([^/]+)(.*)", "IsRegex": true, "TranslatesTo": "http://$1.stella-ops.local/api/v1/$1$2" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/resolve", "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve" },
|
{ "Type": "Microservice", "Path": "^/api/v2/([^/]+)(.*)", "IsRegex": true, "TranslatesTo": "http://$1.stella-ops.local/api/v2/$1$2" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/ops/binaryindex", "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/ops/binaryindex" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/verdicts", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/verdicts" },
|
{ "Type": "Microservice", "Path": "^/api/(cvss|gate|exceptions|policy)(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/api/$1$2" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/lineage", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/lineage" },
|
{ "Type": "Microservice", "Path": "^/api/(risk|risk-budget)(.*)", "IsRegex": true, "TranslatesTo": "http://policy-engine.stella-ops.local/api/$1$2" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/export", "TranslatesTo": "http://exportcenter.stella-ops.local/api/v1/export" },
|
{ "Type": "Microservice", "Path": "^/api/(release-orchestrator|releases|approvals)(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/api/$1$2" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/v1/audit-bundles", "TranslatesTo": "http://exportcenter.stella-ops.local/v1/audit-bundles" },
|
{ "Type": "Microservice", "Path": "^/api/(compare|change-traces|sbomservice)(.*)", "IsRegex": true, "TranslatesTo": "http://sbomservice.stella-ops.local/api/$1$2" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/triage", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage" },
|
{ "Type": "Microservice", "Path": "^/api/fix-verification(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/fix-verification$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/governance", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/governance" },
|
{ "Type": "Microservice", "Path": "^/api/verdicts(.*)", "IsRegex": true, "TranslatesTo": "http://evidencelocker.stella-ops.local/api/verdicts$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/determinization", "TranslatesTo": "http://policy-engine.stella-ops.local/api/v1/determinization" },
|
{ "Type": "Microservice", "Path": "^/api/vuln-explorer(.*)", "IsRegex": true, "TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/opsmemory", "TranslatesTo": "http://opsmemory.stella-ops.local/api/v1/opsmemory" },
|
{ "Type": "Microservice", "Path": "^/api/vex(.*)", "IsRegex": true, "TranslatesTo": "http://vexhub.stella-ops.local/api/vex$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/secrets", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/secrets" },
|
{ "Type": "Microservice", "Path": "^/api/admin/plans(.*)", "IsRegex": true, "TranslatesTo": "http://registry-token.stella-ops.local/api/admin/plans$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/sources", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/sources" },
|
{ "Type": "Microservice", "Path": "^/api/admin(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/admin$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/workflows", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/workflows" },
|
{ "Type": "Microservice", "Path": "^/api/analytics(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/analytics$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/witnesses", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/witnesses" },
|
{ "Type": "Microservice", "Path": "^/api/orchestrator(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/gate", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/gate", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/api/jobengine(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/jobengine$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/risk-budget", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk-budget" },
|
{ "Type": "Microservice", "Path": "^/api/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/fix-verification", "TranslatesTo": "http://scanner.stella-ops.local/api/fix-verification" },
|
{ "Type": "Microservice", "Path": "^/api/doctor(.*)", "IsRegex": true, "TranslatesTo": "http://doctor.stella-ops.local/api/doctor$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/compare", "TranslatesTo": "http://sbomservice.stella-ops.local/api/compare" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/change-traces", "TranslatesTo": "http://sbomservice.stella-ops.local/api/change-traces" },
|
{ "Type": "Microservice", "Path": "^/policy(.*)", "IsRegex": true, "TranslatesTo": "http://policy-gateway.stella-ops.local/policy$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/exceptions", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/exceptions", "PreserveAuthHeaders": true },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/verdicts", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/verdicts" },
|
{ "Type": "Microservice", "Path": "^/v1/evidence-packs(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator" },
|
{ "Type": "Microservice", "Path": "^/v1/runs(.*)", "IsRegex": true, "TranslatesTo": "http://jobengine.stella-ops.local/v1/runs$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/v1/gateway/rate-limits", "TranslatesTo": "http://platform.stella-ops.local/api/v1/gateway/rate-limits", "PreserveAuthHeaders": true },
|
{ "Type": "Microservice", "Path": "^/v1/advisory-ai(.*)", "IsRegex": true, "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/sbomservice", "TranslatesTo": "http://sbomservice.stella-ops.local/api/sbomservice" },
|
{ "Type": "Microservice", "Path": "^/v1/audit-bundles(.*)", "IsRegex": true, "TranslatesTo": "http://exportcenter.stella-ops.local/v1/audit-bundles$1" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/vuln-explorer", "TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/vex" },
|
{ "Type": "ReverseProxy", "Path": "/connect", "TranslatesTo": "http://authority.stella-ops.local/connect" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/admin/plans", "TranslatesTo": "http://registry-token.stella-ops.local/api/admin/plans", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/.well-known", "TranslatesTo": "http://authority.stella-ops.local/.well-known" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api/admin", "TranslatesTo": "http://platform.stella-ops.local/api/admin" },
|
{ "Type": "ReverseProxy", "Path": "/jwks", "TranslatesTo": "http://authority.stella-ops.local/jwks" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/api", "TranslatesTo": "http://platform.stella-ops.local/api" },
|
{ "Type": "ReverseProxy", "Path": "/authority/console", "TranslatesTo": "http://authority.stella-ops.local/console" },
|
||||||
|
{ "Type": "ReverseProxy", "Path": "/authority", "TranslatesTo": "http://authority.stella-ops.local/authority" },
|
||||||
|
{ "Type": "ReverseProxy", "Path": "/console", "TranslatesTo": "http://authority.stella-ops.local/console" },
|
||||||
|
{ "Type": "ReverseProxy", "Path": "/rekor", "TranslatesTo": "http://rekor.stella-ops.local:3322" },
|
||||||
|
{ "Type": "ReverseProxy", "Path": "/platform/envsettings.json", "TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json" },
|
||||||
|
{ "Type": "ReverseProxy", "Path": "/envsettings.json", "TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json" },
|
||||||
|
{ "Type": "ReverseProxy", "Path": "/api/v1/setup", "TranslatesTo": "http://platform.stella-ops.local/api/v1/setup" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/platform", "TranslatesTo": "http://platform.stella-ops.local/platform" },
|
{ "Type": "ReverseProxy", "Path": "/platform", "TranslatesTo": "http://platform.stella-ops.local/platform" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/connect", "TranslatesTo": "http://authority.stella-ops.local", "PreserveAuthHeaders": true },
|
{ "Type": "ReverseProxy", "Path": "/api", "TranslatesTo": "http://platform.stella-ops.local/api" },
|
||||||
{ "Type": "ReverseProxy", "Path": "/.well-known", "TranslatesTo": "http://authority.stella-ops.local/.well-known", "PreserveAuthHeaders": true },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/jwks", "TranslatesTo": "http://authority.stella-ops.local/jwks", "PreserveAuthHeaders": true },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/authority", "TranslatesTo": "http://authority.stella-ops.local/authority", "PreserveAuthHeaders": true },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/console", "TranslatesTo": "http://authority.stella-ops.local/console", "PreserveAuthHeaders": true },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/scanner", "TranslatesTo": "http://scanner.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/policyGateway", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/policyEngine", "TranslatesTo": "http://policy-engine.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/concelier", "TranslatesTo": "http://concelier.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/attestor", "TranslatesTo": "http://attestor.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/notify", "TranslatesTo": "http://notify.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/notifier", "TranslatesTo": "http://notifier.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/scheduler", "TranslatesTo": "http://scheduler.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/signals", "TranslatesTo": "http://signals.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/excititor", "TranslatesTo": "http://excititor.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/findingsLedger", "TranslatesTo": "http://findings.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/vexhub", "TranslatesTo": "http://vexhub.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/taskrunner", "TranslatesTo": "http://taskrunner.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/cartographer", "TranslatesTo": "http://cartographer.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/reachgraph", "TranslatesTo": "http://reachgraph.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/doctor", "TranslatesTo": "http://doctor.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/integrations", "TranslatesTo": "http://integrations.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/replay", "TranslatesTo": "http://replay.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/exportcenter", "TranslatesTo": "http://exportcenter.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/evidencelocker", "TranslatesTo": "http://evidencelocker.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/signer", "TranslatesTo": "http://signer.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/binaryindex", "TranslatesTo": "http://binaryindex.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/riskengine", "TranslatesTo": "http://riskengine.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/vulnexplorer", "TranslatesTo": "http://vulnexplorer.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/sbomservice", "TranslatesTo": "http://sbomservice.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/advisoryai", "TranslatesTo": "http://advisoryai.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/unknowns", "TranslatesTo": "http://unknowns.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/timelineindexer", "TranslatesTo": "http://timelineindexer.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/opsmemory", "TranslatesTo": "http://opsmemory.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/issuerdirectory", "TranslatesTo": "http://issuerdirectory.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/symbols", "TranslatesTo": "http://symbols.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/packsregistry", "TranslatesTo": "http://packsregistry.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/registryTokenservice", "TranslatesTo": "http://registry-token.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/airgapController", "TranslatesTo": "http://airgap-controller.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/airgapTime", "TranslatesTo": "http://airgap-time.stella-ops.local" },
|
|
||||||
{ "Type": "ReverseProxy", "Path": "/smremote", "TranslatesTo": "http://smremote.stella-ops.local" },
|
|
||||||
{ "Type": "StaticFiles", "Path": "/", "TranslatesTo": "/app/wwwroot", "Headers": { "x-spa-fallback": "true" } },
|
{ "Type": "StaticFiles", "Path": "/", "TranslatesTo": "/app/wwwroot", "Headers": { "x-spa-fallback": "true" } },
|
||||||
{ "Type": "NotFoundPage", "Path": "/_error/404", "TranslatesTo": "/app/wwwroot/index.html" },
|
{ "Type": "NotFoundPage", "Path": "/_error/404", "TranslatesTo": "/app/wwwroot/index.html" },
|
||||||
{ "Type": "ServerErrorPage", "Path": "/_error/500", "TranslatesTo": "/app/wwwroot/index.html" }
|
{ "Type": "ServerErrorPage", "Path": "/_error/500", "TranslatesTo": "/app/wwwroot/index.html" }
|
||||||
|
|||||||
@@ -31,14 +31,6 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
|
|||||||
private volatile InstanceHealthStatus _currentStatus = InstanceHealthStatus.Healthy;
|
private volatile InstanceHealthStatus _currentStatus = InstanceHealthStatus.Healthy;
|
||||||
private int _inFlightRequestCount;
|
private int _inFlightRequestCount;
|
||||||
private double _errorRate;
|
private double _errorRate;
|
||||||
private int _heartbeatCount;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of heartbeats between periodic re-registration (HELLO re-send).
|
|
||||||
/// Ensures the gateway picks up the service after a gateway restart.
|
|
||||||
/// Default: every 30 heartbeats (~5 min at 10s intervals).
|
|
||||||
/// </summary>
|
|
||||||
private const int ReRegistrationInterval = 30;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<ConnectionState> Connections => [.. _connections.Values];
|
public IReadOnlyList<ConnectionState> Connections => [.. _connections.Values];
|
||||||
@@ -136,21 +128,7 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
|
|||||||
{
|
{
|
||||||
// Listen for transport death to trigger automatic reconnection
|
// Listen for transport death to trigger automatic reconnection
|
||||||
_microserviceTransport.TransportDied += OnTransportDied;
|
_microserviceTransport.TransportDied += OnTransportDied;
|
||||||
|
await SendRegistrationRefreshAsync(cancellationToken);
|
||||||
var instance = new InstanceDescriptor
|
|
||||||
{
|
|
||||||
InstanceId = _options.InstanceId,
|
|
||||||
ServiceName = _options.ServiceName,
|
|
||||||
Version = _options.Version,
|
|
||||||
Region = _options.Region
|
|
||||||
};
|
|
||||||
|
|
||||||
await _microserviceTransport.ConnectAsync(
|
|
||||||
instance,
|
|
||||||
_endpoints,
|
|
||||||
_schemas,
|
|
||||||
_openApiInfo,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -330,20 +308,7 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
|
|||||||
|
|
||||||
if (_microserviceTransport is null || _endpoints is null) return;
|
if (_microserviceTransport is null || _endpoints is null) return;
|
||||||
|
|
||||||
var instance = new InstanceDescriptor
|
await SendRegistrationRefreshAsync(_cts.Token);
|
||||||
{
|
|
||||||
InstanceId = _options.InstanceId,
|
|
||||||
ServiceName = _options.ServiceName,
|
|
||||||
Version = _options.Version,
|
|
||||||
Region = _options.Region
|
|
||||||
};
|
|
||||||
|
|
||||||
await _microserviceTransport.ConnectAsync(
|
|
||||||
instance,
|
|
||||||
_endpoints,
|
|
||||||
_schemas,
|
|
||||||
_openApiInfo,
|
|
||||||
_cts.Token);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Messaging transport reconnected for {ServiceName}/{Version}",
|
"Messaging transport reconnected for {ServiceName}/{Version}",
|
||||||
@@ -361,57 +326,52 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
|
|||||||
|
|
||||||
private async Task HeartbeatLoopAsync(CancellationToken cancellationToken)
|
private async Task HeartbeatLoopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var nextHeartbeatDueUtc = DateTime.UtcNow + _options.HeartbeatInterval;
|
||||||
|
var nextRegistrationRefreshDueUtc = DateTime.UtcNow + _options.RegistrationRefreshInterval;
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(_options.HeartbeatInterval, cancellationToken);
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var delay = Min(nextHeartbeatDueUtc, nextRegistrationRefreshDueUtc) - nowUtc;
|
||||||
|
if (delay > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, cancellationToken);
|
||||||
|
nowUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
_heartbeatCount++;
|
if (_microserviceTransport is not null &&
|
||||||
|
_endpoints is not null &&
|
||||||
// Periodically re-send HELLO to handle gateway restarts.
|
nowUtc >= nextRegistrationRefreshDueUtc)
|
||||||
// The gateway loses all connection state on restart, and services
|
|
||||||
// only send HELLO once on initial connect. This ensures recovery.
|
|
||||||
if (_heartbeatCount % ReRegistrationInterval == 0 && _microserviceTransport is not null && _endpoints is not null)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var instance = new InstanceDescriptor
|
await SendRegistrationRefreshAsync(cancellationToken);
|
||||||
{
|
_logger.LogDebug(
|
||||||
InstanceId = _options.InstanceId,
|
"Sent periodic HELLO refresh for {ServiceName}/{Version}",
|
||||||
ServiceName = _options.ServiceName,
|
_options.ServiceName,
|
||||||
Version = _options.Version,
|
_options.Version);
|
||||||
Region = _options.Region
|
|
||||||
};
|
|
||||||
|
|
||||||
await _microserviceTransport.ConnectAsync(
|
|
||||||
instance,
|
|
||||||
_endpoints,
|
|
||||||
_schemas,
|
|
||||||
_openApiInfo,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
_logger.LogDebug("Periodic re-registration sent (heartbeat #{Count})", _heartbeatCount);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to send periodic re-registration");
|
_logger.LogWarning(ex, "Failed to send periodic HELLO refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextRegistrationRefreshDueUtc = nowUtc + _options.RegistrationRefreshInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build heartbeat payload with current status and metrics
|
if (_microserviceTransport is not null && nowUtc >= nextHeartbeatDueUtc)
|
||||||
var heartbeat = new HeartbeatPayload
|
|
||||||
{
|
{
|
||||||
InstanceId = _options.InstanceId,
|
var heartbeat = new HeartbeatPayload
|
||||||
Status = _currentStatus,
|
{
|
||||||
InFlightRequestCount = _inFlightRequestCount,
|
InstanceId = _options.InstanceId,
|
||||||
ErrorRate = _errorRate,
|
Status = _currentStatus,
|
||||||
TimestampUtc = DateTime.UtcNow
|
InFlightRequestCount = _inFlightRequestCount,
|
||||||
};
|
ErrorRate = _errorRate,
|
||||||
|
TimestampUtc = nowUtc
|
||||||
|
};
|
||||||
|
|
||||||
// Send heartbeat via transport
|
|
||||||
if (_microserviceTransport is not null)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _microserviceTransport.SendHeartbeatAsync(heartbeat, cancellationToken);
|
await _microserviceTransport.SendHeartbeatAsync(heartbeat, cancellationToken);
|
||||||
@@ -426,12 +386,13 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
|
|||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to send heartbeat");
|
_logger.LogWarning(ex, "Failed to send heartbeat");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update connection state local heartbeat times
|
foreach (var connection in _connections.Values)
|
||||||
foreach (var connection in _connections.Values)
|
{
|
||||||
{
|
connection.LastHeartbeatUtc = nowUtc;
|
||||||
connection.LastHeartbeatUtc = DateTime.UtcNow;
|
}
|
||||||
|
|
||||||
|
nextHeartbeatDueUtc = nowUtc + _options.HeartbeatInterval;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@@ -446,6 +407,34 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SendRegistrationRefreshAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_microserviceTransport is null || _endpoints is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _microserviceTransport.ConnectAsync(
|
||||||
|
CreateInstanceDescriptor(),
|
||||||
|
_endpoints,
|
||||||
|
_schemas,
|
||||||
|
_openApiInfo,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InstanceDescriptor CreateInstanceDescriptor()
|
||||||
|
{
|
||||||
|
return new InstanceDescriptor
|
||||||
|
{
|
||||||
|
InstanceId = _options.InstanceId,
|
||||||
|
ServiceName = _options.ServiceName,
|
||||||
|
Version = _options.Version,
|
||||||
|
Region = _options.Region
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime Min(DateTime left, DateTime right) => left <= right ? left : right;
|
||||||
|
|
||||||
private static IReadOnlyDictionary<string, SchemaDefinition> MergeSchemaDefinitions(
|
private static IReadOnlyDictionary<string, SchemaDefinition> MergeSchemaDefinitions(
|
||||||
IReadOnlyDictionary<string, SchemaDefinition>? generatedSchemas,
|
IReadOnlyDictionary<string, SchemaDefinition>? generatedSchemas,
|
||||||
IReadOnlyDictionary<string, SchemaDefinition>? discoveredSchemas)
|
IReadOnlyDictionary<string, SchemaDefinition>? discoveredSchemas)
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ public sealed partial class StellaMicroserviceOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(45);
|
public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(45);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum interval between HELLO refreshes on an already-live transport.
|
||||||
|
/// Default: 10 seconds so gateway restarts converge quickly without waiting for a full heartbeat window.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan RegistrationRefreshInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum reconnect backoff.
|
/// Gets or sets the maximum reconnect backoff.
|
||||||
/// Default: 1 minute.
|
/// Default: 1 minute.
|
||||||
@@ -88,6 +94,12 @@ public sealed partial class StellaMicroserviceOptions
|
|||||||
if (Routers.Count == 0)
|
if (Routers.Count == 0)
|
||||||
throw new InvalidOperationException("At least one router endpoint is required.");
|
throw new InvalidOperationException("At least one router endpoint is required.");
|
||||||
|
|
||||||
|
if (HeartbeatInterval <= TimeSpan.Zero)
|
||||||
|
throw new InvalidOperationException("HeartbeatInterval must be positive.");
|
||||||
|
|
||||||
|
if (RegistrationRefreshInterval <= TimeSpan.Zero)
|
||||||
|
throw new InvalidOperationException("RegistrationRefreshInterval must be positive.");
|
||||||
|
|
||||||
foreach (var router in Routers)
|
foreach (var router in Routers)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(router.Host))
|
if (string.IsNullOrWhiteSpace(router.Host))
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
| AUDIT-0387-A | DONE | Applied 2026-01-13; superseded by AUDIT-0598-A. |
|
| AUDIT-0387-A | DONE | Applied 2026-01-13; superseded by AUDIT-0598-A. |
|
||||||
| AUDIT-0598-A | DONE | Applied 2026-01-13; hotlist fixes and tests. |
|
| AUDIT-0598-A | DONE | Applied 2026-01-13; hotlist fixes and tests. |
|
||||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||||
|
| ROUTER-READY-004-M | DONE | 2026-03-10: Replaced the fixed 30-heartbeat HELLO replay cadence with an explicit bounded registration refresh interval so gateway restarts converge within seconds. |
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ public static class StellaRouterExtensions
|
|||||||
microserviceOptions.InstanceId = options.InstanceId;
|
microserviceOptions.InstanceId = options.InstanceId;
|
||||||
microserviceOptions.ServiceDescription = options.ServiceDescription;
|
microserviceOptions.ServiceDescription = options.ServiceDescription;
|
||||||
microserviceOptions.HeartbeatInterval = options.HeartbeatInterval;
|
microserviceOptions.HeartbeatInterval = options.HeartbeatInterval;
|
||||||
|
microserviceOptions.RegistrationRefreshInterval = options.RegistrationRefreshInterval;
|
||||||
microserviceOptions.ReconnectBackoffInitial = options.ReconnectBackoffInitial;
|
microserviceOptions.ReconnectBackoffInitial = options.ReconnectBackoffInitial;
|
||||||
microserviceOptions.ReconnectBackoffMax = options.ReconnectBackoffMax;
|
microserviceOptions.ReconnectBackoffMax = options.ReconnectBackoffMax;
|
||||||
|
|
||||||
@@ -226,6 +227,11 @@ public static class StellaRouterExtensions
|
|||||||
errors.Add("HeartbeatInterval must be positive");
|
errors.Add("HeartbeatInterval must be positive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.RegistrationRefreshInterval <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
errors.Add("RegistrationRefreshInterval must be positive");
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.Count > 0)
|
if (errors.Count > 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public static class StellaRouterIntegrationHelper
|
|||||||
opts.EnableStellaEndpoints = false;
|
opts.EnableStellaEndpoints = false;
|
||||||
opts.DefaultTimeout = TimeSpan.FromSeconds(routerOptions.DefaultTimeoutSeconds);
|
opts.DefaultTimeout = TimeSpan.FromSeconds(routerOptions.DefaultTimeoutSeconds);
|
||||||
opts.HeartbeatInterval = TimeSpan.FromSeconds(routerOptions.HeartbeatIntervalSeconds);
|
opts.HeartbeatInterval = TimeSpan.FromSeconds(routerOptions.HeartbeatIntervalSeconds);
|
||||||
|
opts.RegistrationRefreshInterval = TimeSpan.FromSeconds(routerOptions.RegistrationRefreshIntervalSeconds);
|
||||||
opts.AuthorizationTrustMode = routerOptions.AuthorizationTrustMode;
|
opts.AuthorizationTrustMode = routerOptions.AuthorizationTrustMode;
|
||||||
opts.IdentityEnvelopeSigningKey = routerOptions.IdentityEnvelopeSigningKey;
|
opts.IdentityEnvelopeSigningKey = routerOptions.IdentityEnvelopeSigningKey;
|
||||||
opts.IdentityEnvelopeClockSkewSeconds = routerOptions.IdentityEnvelopeClockSkewSeconds;
|
opts.IdentityEnvelopeClockSkewSeconds = routerOptions.IdentityEnvelopeClockSkewSeconds;
|
||||||
@@ -179,6 +180,10 @@ public static class StellaRouterIntegrationHelper
|
|||||||
transportConfiguration,
|
transportConfiguration,
|
||||||
resolvedRouterOptionsSection,
|
resolvedRouterOptionsSection,
|
||||||
configuredTransports);
|
configuredTransports);
|
||||||
|
ApplyImplicitRegistrationRefreshInterval(
|
||||||
|
routerOptions,
|
||||||
|
configuration,
|
||||||
|
resolvedRouterOptionsSection);
|
||||||
|
|
||||||
services.TryAddStellaRouter(
|
services.TryAddStellaRouter(
|
||||||
serviceName,
|
serviceName,
|
||||||
@@ -405,6 +410,30 @@ public static class StellaRouterIntegrationHelper
|
|||||||
routerOptions.HeartbeatIntervalSeconds = Math.Max(1, (int)Math.Ceiling(heartbeatInterval.TotalSeconds));
|
routerOptions.HeartbeatIntervalSeconds = Math.Max(1, (int)Math.Ceiling(heartbeatInterval.TotalSeconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasExplicitRegistrationRefreshInterval(
|
||||||
|
IConfiguration configuration,
|
||||||
|
string routerOptionsSection)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(configuration[$"{routerOptionsSection}:RegistrationRefreshIntervalSeconds"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyImplicitRegistrationRefreshInterval(
|
||||||
|
StellaRouterOptionsBase routerOptions,
|
||||||
|
IConfiguration configuration,
|
||||||
|
string routerOptionsSection)
|
||||||
|
{
|
||||||
|
if (HasExplicitRegistrationRefreshInterval(configuration, routerOptionsSection))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
routerOptions.RegistrationRefreshIntervalSeconds = Math.Max(
|
||||||
|
1,
|
||||||
|
Math.Min(
|
||||||
|
routerOptions.RegistrationRefreshIntervalSeconds,
|
||||||
|
routerOptions.HeartbeatIntervalSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
private static void CopySectionValues(
|
private static void CopySectionValues(
|
||||||
IConfigurationSection section,
|
IConfigurationSection section,
|
||||||
IDictionary<string, string?> destination,
|
IDictionary<string, string?> destination,
|
||||||
|
|||||||
@@ -103,6 +103,12 @@ public sealed class StellaRouterOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(45);
|
public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(45);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum interval between HELLO refreshes on an already-live transport.
|
||||||
|
/// Default: 10 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan RegistrationRefreshInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initial reconnect backoff delay.
|
/// Initial reconnect backoff delay.
|
||||||
/// Default: 1 second.
|
/// Default: 1 second.
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ public class StellaRouterOptionsBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int HeartbeatIntervalSeconds { get; set; } = 45;
|
public int HeartbeatIntervalSeconds { get; set; } = 45;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum interval in seconds between HELLO refreshes on an already-live transport.
|
||||||
|
/// Default: 10 seconds to keep gateway restarts from leaving the frontdoor cold for minutes.
|
||||||
|
/// </summary>
|
||||||
|
public int RegistrationRefreshIntervalSeconds { get; set; } = 10;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service trust mode for gateway-enforced authorization semantics.
|
/// Service trust mode for gateway-enforced authorization semantics.
|
||||||
/// Default: Hybrid.
|
/// Default: Hybrid.
|
||||||
|
|||||||
@@ -34,5 +34,5 @@ public sealed class StellaOpsRoute
|
|||||||
/// of stripping them. Use for upstream services that perform their own JWT
|
/// of stripping them. Use for upstream services that perform their own JWT
|
||||||
/// validation (e.g., Authority admin API).
|
/// validation (e.g., Authority admin API).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool PreserveAuthHeaders { get; set; }
|
public bool PreserveAuthHeaders { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using StellaOps.Router.Common.Abstractions;
|
using StellaOps.Router.Common.Abstractions;
|
||||||
using StellaOps.Router.Common;
|
using StellaOps.Router.Common;
|
||||||
|
using StellaOps.Router.Common.Enums;
|
||||||
using StellaOps.Router.Common.Models;
|
using StellaOps.Router.Common.Models;
|
||||||
|
|
||||||
namespace StellaOps.Router.Gateway.Middleware;
|
namespace StellaOps.Router.Gateway.Middleware;
|
||||||
@@ -36,18 +37,36 @@ public sealed class EndpointResolutionMiddleware
|
|||||||
? targetService as string
|
? targetService as string
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
EndpointDescriptor? endpoint;
|
EndpointResolutionResult resolution;
|
||||||
if (!string.IsNullOrWhiteSpace(targetMicroserviceHint))
|
if (!string.IsNullOrWhiteSpace(targetMicroserviceHint))
|
||||||
{
|
{
|
||||||
endpoint = ResolveEndpointForTargetService(routingState, method, path, targetMicroserviceHint!);
|
resolution = ResolveEndpointForTargetService(routingState, method, path, targetMicroserviceHint!);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
endpoint = routingState.ResolveEndpoint(method, path);
|
resolution = new EndpointResolutionResult(routingState.ResolveEndpoint(method, path), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var endpoint = resolution.Endpoint;
|
||||||
if (endpoint is null)
|
if (endpoint is null)
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(targetMicroserviceHint) && !resolution.ServiceRegistered)
|
||||||
|
{
|
||||||
|
await RouterErrorWriter.WriteAsync(
|
||||||
|
context,
|
||||||
|
statusCode: StatusCodes.Status503ServiceUnavailable,
|
||||||
|
error: "Target microservice unavailable",
|
||||||
|
message: "The configured microservice route exists, but the target service has not registered with the gateway yet.",
|
||||||
|
service: targetMicroserviceHint,
|
||||||
|
details: new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["translatedPath"] = path,
|
||||||
|
["targetMicroservice"] = targetMicroserviceHint
|
||||||
|
},
|
||||||
|
cancellationToken: context.RequestAborted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await RouterErrorWriter.WriteAsync(
|
await RouterErrorWriter.WriteAsync(
|
||||||
context,
|
context,
|
||||||
statusCode: StatusCodes.Status404NotFound,
|
statusCode: StatusCodes.Status404NotFound,
|
||||||
@@ -62,85 +81,108 @@ public sealed class EndpointResolutionMiddleware
|
|||||||
await _next(context);
|
await _next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EndpointDescriptor? ResolveEndpointForTargetService(
|
private static EndpointResolutionResult ResolveEndpointForTargetService(
|
||||||
IGlobalRoutingState routingState,
|
IGlobalRoutingState routingState,
|
||||||
string method,
|
string method,
|
||||||
string path,
|
string path,
|
||||||
string targetServiceHint)
|
string targetServiceHint)
|
||||||
{
|
{
|
||||||
var normalizedHint = NormalizeServiceKey(targetServiceHint);
|
var exactHint = NormalizeServiceKey(targetServiceHint, preserveGatewaySuffix: true);
|
||||||
if (string.IsNullOrWhiteSpace(normalizedHint))
|
if (string.IsNullOrWhiteSpace(exactHint))
|
||||||
{
|
{
|
||||||
return null;
|
return new EndpointResolutionResult(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
EndpointDescriptor? bestEndpoint = null;
|
var aliasHint = NormalizeServiceKey(targetServiceHint);
|
||||||
var bestScore = int.MinValue;
|
var allowAliasMatching = !IsExplicitServiceHint(exactHint);
|
||||||
|
|
||||||
|
var candidates = new List<(EndpointDescriptor Endpoint, ConnectionState Connection, int ServiceScore)>();
|
||||||
|
var serviceRegistered = false;
|
||||||
|
|
||||||
foreach (var connection in routingState.GetAllConnections())
|
foreach (var connection in routingState.GetAllConnections())
|
||||||
{
|
{
|
||||||
var serviceScore = GetServiceMatchScore(connection.Instance.ServiceName, normalizedHint);
|
var serviceScore = GetServiceMatchScore(connection.Instance.ServiceName, exactHint, aliasHint, allowAliasMatching);
|
||||||
if (serviceScore < 0)
|
if (serviceScore < 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var endpoint in connection.Endpoints.Values)
|
serviceRegistered = true;
|
||||||
|
|
||||||
|
foreach (var candidateEndpoint in connection.Endpoints.Values)
|
||||||
{
|
{
|
||||||
if (!string.Equals(endpoint.Method, method, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(candidateEndpoint.Method, method, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var matcher = new PathMatcher(endpoint.Path);
|
var matcher = new PathMatcher(candidateEndpoint.Path);
|
||||||
if (!matcher.IsMatch(path))
|
if (!matcher.IsMatch(path))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
candidates.Add((candidateEndpoint, connection, serviceScore));
|
||||||
var endpointScore = (serviceScore * 1000) + endpoint.Path.Length;
|
|
||||||
if (endpointScore <= bestScore)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bestEndpoint = endpoint;
|
|
||||||
bestScore = endpointScore;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestEndpoint;
|
var selectedEndpoint = candidates
|
||||||
|
.OrderByDescending(candidate => candidate.ServiceScore)
|
||||||
|
.ThenByDescending(candidate => candidate.Endpoint.Path.Length)
|
||||||
|
.ThenByDescending(candidate => GetHealthRank(candidate.Connection.Status))
|
||||||
|
.ThenByDescending(candidate => candidate.Connection.LastHeartbeatUtc)
|
||||||
|
.ThenBy(candidate => candidate.Connection.ConnectionId, StringComparer.Ordinal)
|
||||||
|
.Select(candidate => candidate.Endpoint)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return new EndpointResolutionResult(selectedEndpoint, serviceRegistered);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetServiceMatchScore(string? serviceName, string normalizedHint)
|
private static int GetServiceMatchScore(
|
||||||
|
string? serviceName,
|
||||||
|
string exactHint,
|
||||||
|
string? aliasHint,
|
||||||
|
bool allowAliasMatching)
|
||||||
{
|
{
|
||||||
var normalizedService = NormalizeServiceKey(serviceName);
|
var exactService = NormalizeServiceKey(serviceName, preserveGatewaySuffix: true);
|
||||||
if (string.IsNullOrWhiteSpace(normalizedService))
|
if (string.IsNullOrWhiteSpace(exactService))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(serviceName, normalizedHint, StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(serviceName, exactHint, StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(normalizedService, normalizedHint, StringComparison.OrdinalIgnoreCase))
|
string.Equals(exactService, exactHint, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliasService = NormalizeServiceKey(serviceName);
|
||||||
|
if (!string.IsNullOrWhiteSpace(aliasHint) &&
|
||||||
|
string.Equals(aliasService, aliasHint, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceName is not null &&
|
if (!allowAliasMatching || string.IsNullOrWhiteSpace(aliasHint))
|
||||||
(serviceName.StartsWith(normalizedHint + "-", StringComparison.OrdinalIgnoreCase) ||
|
{
|
||||||
serviceName.StartsWith(normalizedHint + "_", StringComparison.OrdinalIgnoreCase)))
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exactService.StartsWith(aliasHint + "-", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
exactService.StartsWith(aliasHint + "_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedService.StartsWith(normalizedHint, StringComparison.OrdinalIgnoreCase) ||
|
if (!string.IsNullOrWhiteSpace(aliasService) &&
|
||||||
normalizedHint.StartsWith(normalizedService, StringComparison.OrdinalIgnoreCase))
|
(aliasService.StartsWith(aliasHint, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
aliasHint.StartsWith(aliasService, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedService.Contains(normalizedHint, StringComparison.OrdinalIgnoreCase) ||
|
if (!string.IsNullOrWhiteSpace(aliasService) &&
|
||||||
normalizedHint.Contains(normalizedService, StringComparison.OrdinalIgnoreCase))
|
(aliasService.Contains(aliasHint, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
aliasHint.Contains(aliasService, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
@@ -148,7 +190,7 @@ public sealed class EndpointResolutionMiddleware
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? NormalizeServiceKey(string? value)
|
private static string? NormalizeServiceKey(string? value, bool preserveGatewaySuffix = false)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
@@ -172,17 +214,42 @@ public sealed class EndpointResolutionMiddleware
|
|||||||
normalized = StripSuffix(normalized, "-web");
|
normalized = StripSuffix(normalized, "-web");
|
||||||
normalized = StripSuffix(normalized, "-api");
|
normalized = StripSuffix(normalized, "-api");
|
||||||
normalized = StripSuffix(normalized, "-service");
|
normalized = StripSuffix(normalized, "-service");
|
||||||
normalized = StripSuffix(normalized, "-gateway");
|
if (!preserveGatewaySuffix)
|
||||||
|
{
|
||||||
|
normalized = StripSuffix(normalized, "-gateway");
|
||||||
|
}
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(normalized)
|
return string.IsNullOrWhiteSpace(normalized)
|
||||||
? null
|
? null
|
||||||
: normalized;
|
: normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsExplicitServiceHint(string? hint)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hint))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hint.Contains('-', StringComparison.Ordinal) ||
|
||||||
|
hint.Contains('_', StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static string StripSuffix(string value, string suffix)
|
private static string StripSuffix(string value, string suffix)
|
||||||
{
|
{
|
||||||
return value.EndsWith(suffix, StringComparison.Ordinal)
|
return value.EndsWith(suffix, StringComparison.Ordinal)
|
||||||
? value[..^suffix.Length]
|
? value[..^suffix.Length]
|
||||||
: value;
|
: value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int GetHealthRank(InstanceHealthStatus status) => status switch
|
||||||
|
{
|
||||||
|
InstanceHealthStatus.Healthy => 4,
|
||||||
|
InstanceHealthStatus.Degraded => 3,
|
||||||
|
InstanceHealthStatus.Unknown => 2,
|
||||||
|
InstanceHealthStatus.Draining => 1,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
private sealed record EndpointResolutionResult(EndpointDescriptor? Endpoint, bool ServiceRegistered);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
using StellaOps.Router.Common;
|
using StellaOps.Router.Common;
|
||||||
using StellaOps.Router.Common.Abstractions;
|
using StellaOps.Router.Common.Abstractions;
|
||||||
|
using StellaOps.Router.Common.Enums;
|
||||||
using StellaOps.Router.Common.Models;
|
using StellaOps.Router.Common.Models;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
@@ -85,7 +86,8 @@ internal sealed class InMemoryRoutingState : IGlobalRoutingState
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public EndpointDescriptor? ResolveEndpoint(string method, string path)
|
public EndpointDescriptor? ResolveEndpoint(string method, string path)
|
||||||
{
|
{
|
||||||
// First try exact match
|
var matches = new List<(EndpointDescriptor Endpoint, ConnectionState Connection)>();
|
||||||
|
|
||||||
foreach (var ((m, p), matcher) in _pathMatchers)
|
foreach (var ((m, p), matcher) in _pathMatchers)
|
||||||
{
|
{
|
||||||
if (!string.Equals(m, method, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(m, method, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -101,14 +103,20 @@ internal sealed class InMemoryRoutingState : IGlobalRoutingState
|
|||||||
if (_connections.TryGetValue(connectionId, out var conn) &&
|
if (_connections.TryGetValue(connectionId, out var conn) &&
|
||||||
conn.Endpoints.TryGetValue((m, p), out var endpoint))
|
conn.Endpoints.TryGetValue((m, p), out var endpoint))
|
||||||
{
|
{
|
||||||
return endpoint;
|
matches.Add((endpoint, conn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return matches
|
||||||
|
.OrderByDescending(match => match.Endpoint.Path.Length)
|
||||||
|
.ThenByDescending(match => GetHealthRank(match.Connection.Status))
|
||||||
|
.ThenByDescending(match => match.Connection.LastHeartbeatUtc)
|
||||||
|
.ThenBy(match => match.Connection.ConnectionId, StringComparer.Ordinal)
|
||||||
|
.Select(match => match.Endpoint)
|
||||||
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -194,4 +202,13 @@ internal sealed class InMemoryRoutingState : IGlobalRoutingState
|
|||||||
string.Equals(existing.Instance.InstanceId, candidate.Instance.InstanceId, StringComparison.Ordinal) &&
|
string.Equals(existing.Instance.InstanceId, candidate.Instance.InstanceId, StringComparison.Ordinal) &&
|
||||||
string.Equals(existing.Instance.Region, candidate.Instance.Region, StringComparison.OrdinalIgnoreCase);
|
string.Equals(existing.Instance.Region, candidate.Instance.Region, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int GetHealthRank(InstanceHealthStatus status) => status switch
|
||||||
|
{
|
||||||
|
InstanceHealthStatus.Healthy => 4,
|
||||||
|
InstanceHealthStatus.Degraded => 3,
|
||||||
|
InstanceHealthStatus.Unknown => 2,
|
||||||
|
InstanceHealthStatus.Draining => 1,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,56 @@ public sealed class GatewayOptionsValidatorTests
|
|||||||
Assert.Null(exception);
|
Assert.Null(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Validate_RegexWithValidCaptureGroupRefs_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var options = CreateValidOptions();
|
||||||
|
options.Routes.Add(new StellaOpsRoute
|
||||||
|
{
|
||||||
|
Type = StellaOpsRouteType.Microservice,
|
||||||
|
Path = @"^/api/v1/([^/]+)(.*)",
|
||||||
|
IsRegex = true,
|
||||||
|
TranslatesTo = "http://$1.stella-ops.local/api/v1/$1$2"
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Record.Exception(() => GatewayOptionsValidator.Validate(options));
|
||||||
|
Assert.Null(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Validate_RegexWithInvalidCaptureGroupRef_Throws()
|
||||||
|
{
|
||||||
|
var options = CreateValidOptions();
|
||||||
|
options.Routes.Add(new StellaOpsRoute
|
||||||
|
{
|
||||||
|
Type = StellaOpsRouteType.Microservice,
|
||||||
|
Path = @"^/api/v1/([^/]+)",
|
||||||
|
IsRegex = true,
|
||||||
|
TranslatesTo = "http://$1.stella-ops.local/$2"
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
GatewayOptionsValidator.Validate(options));
|
||||||
|
|
||||||
|
Assert.Contains("$2", exception.Message, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("capture group", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Validate_RegexWithNoTranslatesTo_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var options = CreateValidOptions();
|
||||||
|
options.Routes.Add(new StellaOpsRoute
|
||||||
|
{
|
||||||
|
Type = StellaOpsRouteType.Microservice,
|
||||||
|
Path = @"^/api/v1/([^/]+)(.*)",
|
||||||
|
IsRegex = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Record.Exception(() => GatewayOptionsValidator.Validate(options));
|
||||||
|
Assert.Null(exception);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(null)]
|
[InlineData(null)]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
|
|||||||
@@ -4,17 +4,33 @@ namespace StellaOps.Gateway.WebService.Tests.Configuration;
|
|||||||
|
|
||||||
public sealed class GatewayRouteSearchMappingsTests
|
public sealed class GatewayRouteSearchMappingsTests
|
||||||
{
|
{
|
||||||
private static readonly (string Path, string Target, string RouteType)[] RequiredMappings =
|
private static readonly (string Path, string Target, string RouteType, bool IsRegex)[] RequiredMappings =
|
||||||
[
|
[
|
||||||
("/api/v1/search", "http://advisoryai.stella-ops.local/v1/search", "ReverseProxy"),
|
("^/api/v1/search(.*)", "http://advisoryai.stella-ops.local/v1/search$1", "Microservice", true),
|
||||||
("/api/v1/advisory-ai", "http://advisoryai.stella-ops.local/v1/advisory-ai", "ReverseProxy")
|
("^/api/v1/advisory-ai(.*)", "http://advisoryai.stella-ops.local/v1/advisory-ai$1", "Microservice", true),
|
||||||
|
("^/api/v1/watchlist(.*)", "http://attestor.stella-ops.local/api/v1/watchlist$1", "Microservice", true),
|
||||||
|
("^/api/v1/audit(.*)", "http://timeline.stella-ops.local/api/v1/audit$1", "Microservice", true),
|
||||||
|
("^/api/v1/advisory-sources(.*)", "http://concelier.stella-ops.local/api/v1/advisory-sources$1", "Microservice", true),
|
||||||
|
("^/api/v1/notifier/delivery(.*)", "http://notifier.stella-ops.local/api/v2/notify/deliveries$1", "Microservice", true),
|
||||||
|
("^/api/v2/context(.*)", "http://platform.stella-ops.local/api/v2/context$1", "Microservice", true),
|
||||||
|
("^/api/v2/releases(.*)", "http://platform.stella-ops.local/api/v2/releases$1", "Microservice", true),
|
||||||
|
("^/api/v2/security(.*)", "http://platform.stella-ops.local/api/v2/security$1", "Microservice", true),
|
||||||
|
("^/api/v2/topology(.*)", "http://platform.stella-ops.local/api/v2/topology$1", "Microservice", true),
|
||||||
|
("^/api/v2/integrations(.*)", "http://platform.stella-ops.local/api/v2/integrations$1", "Microservice", true),
|
||||||
|
("^/api/jobengine(.*)", "http://orchestrator.stella-ops.local/api/jobengine$1", "Microservice", true),
|
||||||
|
("^/api/scheduler(.*)", "http://scheduler.stella-ops.local/api/scheduler$1", "Microservice", true)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly (string Path, string AppSettingsTarget, string LocalTarget)[] RequiredReverseProxyMappings =
|
||||||
|
[
|
||||||
|
("/connect", "http://authority.stella-ops.local/connect", "http://authority.stella-ops.local/connect"),
|
||||||
|
("/authority/console", "http://authority.stella-ops.local/console", "https://authority.stella-ops.local/console")
|
||||||
];
|
];
|
||||||
|
|
||||||
public static TheoryData<string> RouteConfigPaths => new()
|
public static TheoryData<string> RouteConfigPaths => new()
|
||||||
{
|
{
|
||||||
"src/Router/StellaOps.Gateway.WebService/appsettings.json",
|
"src/Router/StellaOps.Gateway.WebService/appsettings.json",
|
||||||
"devops/compose/router-gateway-local.json",
|
"devops/compose/router-gateway-local.json"
|
||||||
"devops/compose/router-gateway-local.reverseproxy.json"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -38,17 +54,19 @@ public sealed class GatewayRouteSearchMappingsTests
|
|||||||
route.GetProperty("Path").GetString() ?? string.Empty,
|
route.GetProperty("Path").GetString() ?? string.Empty,
|
||||||
route.TryGetProperty("TranslatesTo", out var translatesTo)
|
route.TryGetProperty("TranslatesTo", out var translatesTo)
|
||||||
? translatesTo.GetString() ?? string.Empty
|
? translatesTo.GetString() ?? string.Empty
|
||||||
: string.Empty))
|
: string.Empty,
|
||||||
|
route.TryGetProperty("IsRegex", out var isRegex) && isRegex.GetBoolean()))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var catchAllIndex = routes.FindIndex(route => string.Equals(route.Path, "/api", StringComparison.Ordinal));
|
var catchAllIndex = routes.FindIndex(route => string.Equals(route.Path, "/api", StringComparison.Ordinal));
|
||||||
|
|
||||||
foreach (var (requiredPath, requiredTarget, requiredType) in RequiredMappings)
|
foreach (var (requiredPath, requiredTarget, requiredType, requiredIsRegex) in RequiredMappings)
|
||||||
{
|
{
|
||||||
var route = routes.FirstOrDefault(candidate => string.Equals(candidate.Path, requiredPath, StringComparison.Ordinal));
|
var route = routes.FirstOrDefault(candidate => string.Equals(candidate.Path, requiredPath, StringComparison.Ordinal));
|
||||||
Assert.True(route is not null, $"Missing route '{requiredPath}' in {configRelativePath}.");
|
Assert.True(route is not null, $"Missing route '{requiredPath}' in {configRelativePath}.");
|
||||||
Assert.Equal(requiredType, route!.Type);
|
Assert.Equal(requiredType, route!.Type);
|
||||||
Assert.Equal(requiredTarget, route!.TranslatesTo);
|
Assert.Equal(requiredTarget, route!.TranslatesTo);
|
||||||
|
Assert.Equal(requiredIsRegex, route!.IsRegex);
|
||||||
|
|
||||||
if (catchAllIndex >= 0)
|
if (catchAllIndex >= 0)
|
||||||
{
|
{
|
||||||
@@ -57,6 +75,46 @@ public sealed class GatewayRouteSearchMappingsTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(RouteConfigPaths))]
|
||||||
|
public void RouteTable_ContainsRequiredReverseProxyMappings(string configRelativePath)
|
||||||
|
{
|
||||||
|
var repoRoot = FindRepositoryRoot();
|
||||||
|
var configPath = Path.Combine(repoRoot, configRelativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
Assert.True(File.Exists(configPath), $"Config file not found: {configPath}");
|
||||||
|
|
||||||
|
using var stream = File.OpenRead(configPath);
|
||||||
|
using var document = JsonDocument.Parse(stream);
|
||||||
|
|
||||||
|
var routes = document.RootElement
|
||||||
|
.GetProperty("Gateway")
|
||||||
|
.GetProperty("Routes")
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(route => new RouteEntry(
|
||||||
|
Index: -1,
|
||||||
|
route.GetProperty("Type").GetString() ?? string.Empty,
|
||||||
|
route.GetProperty("Path").GetString() ?? string.Empty,
|
||||||
|
route.TryGetProperty("TranslatesTo", out var translatesTo)
|
||||||
|
? translatesTo.GetString() ?? string.Empty
|
||||||
|
: string.Empty,
|
||||||
|
route.TryGetProperty("IsRegex", out var isRegex) && isRegex.GetBoolean()))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var isLocalComposeConfig = string.Equals(
|
||||||
|
configRelativePath,
|
||||||
|
"devops/compose/router-gateway-local.json",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
foreach (var (requiredPath, appSettingsTarget, localTarget) in RequiredReverseProxyMappings)
|
||||||
|
{
|
||||||
|
var route = routes.FirstOrDefault(candidate => string.Equals(candidate.Path, requiredPath, StringComparison.Ordinal));
|
||||||
|
Assert.True(route is not null, $"Missing route '{requiredPath}' in {configRelativePath}.");
|
||||||
|
Assert.Equal("ReverseProxy", route!.Type);
|
||||||
|
Assert.Equal(isLocalComposeConfig ? localTarget : appSettingsTarget, route.TranslatesTo);
|
||||||
|
Assert.False(route.IsRegex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string FindRepositoryRoot()
|
private static string FindRepositoryRoot()
|
||||||
{
|
{
|
||||||
for (var current = new DirectoryInfo(AppContext.BaseDirectory); current is not null; current = current.Parent)
|
for (var current = new DirectoryInfo(AppContext.BaseDirectory); current is not null; current = current.Parent)
|
||||||
@@ -70,5 +128,5 @@ public sealed class GatewayRouteSearchMappingsTests
|
|||||||
throw new InvalidOperationException($"Unable to locate repository root from {AppContext.BaseDirectory}.");
|
throw new InvalidOperationException($"Unable to locate repository root from {AppContext.BaseDirectory}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record RouteEntry(int Index, string Type, string Path, string TranslatesTo);
|
private sealed record RouteEntry(int Index, string Type, string Path, string TranslatesTo, bool IsRegex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using StellaOps.Gateway.WebService.Configuration;
|
||||||
using StellaOps.Gateway.WebService.Routing;
|
using StellaOps.Gateway.WebService.Routing;
|
||||||
using StellaOps.Router.Common.Abstractions;
|
using StellaOps.Router.Common.Abstractions;
|
||||||
using StellaOps.Router.Common.Enums;
|
using StellaOps.Router.Common.Enums;
|
||||||
@@ -51,6 +52,93 @@ public sealed class GatewayIntegrationTests : IClassFixture<GatewayWebApplicatio
|
|||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HealthReady_ReturnsServiceUnavailable_WhenRequiredMicroserviceIsMissing()
|
||||||
|
{
|
||||||
|
using var factory = CreateFactoryWithRequiredServices("policy");
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.GetAsync("/health/ready");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||||
|
|
||||||
|
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||||
|
Assert.Equal("policy", payload.RootElement.GetProperty("missingMicroservices")[0].GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HealthReady_ReturnsOk_WhenRequiredMicroserviceIsRegistered()
|
||||||
|
{
|
||||||
|
using var factory = CreateFactoryWithRequiredServices("policy");
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var routingState = scope.ServiceProvider.GetRequiredService<IGlobalRoutingState>();
|
||||||
|
routingState.AddConnection(new ConnectionState
|
||||||
|
{
|
||||||
|
ConnectionId = "conn-policy",
|
||||||
|
Instance = new InstanceDescriptor
|
||||||
|
{
|
||||||
|
InstanceId = "policy-01",
|
||||||
|
ServiceName = "policy-gateway",
|
||||||
|
Version = "1.0.0",
|
||||||
|
Region = "test"
|
||||||
|
},
|
||||||
|
Status = InstanceHealthStatus.Healthy,
|
||||||
|
TransportType = TransportType.Messaging
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
var response = await client.GetAsync("/health/ready");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HealthReady_ReturnsServiceUnavailable_WhenOnlySiblingServiceIsRegistered()
|
||||||
|
{
|
||||||
|
using var factory = CreateFactoryWithRequiredServices("policy");
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var routingState = scope.ServiceProvider.GetRequiredService<IGlobalRoutingState>();
|
||||||
|
routingState.AddConnection(new ConnectionState
|
||||||
|
{
|
||||||
|
ConnectionId = "conn-policy-engine",
|
||||||
|
Instance = new InstanceDescriptor
|
||||||
|
{
|
||||||
|
InstanceId = "policy-engine-01",
|
||||||
|
ServiceName = "policy-engine",
|
||||||
|
Version = "1.0.0",
|
||||||
|
Region = "test"
|
||||||
|
},
|
||||||
|
Status = InstanceHealthStatus.Healthy,
|
||||||
|
TransportType = TransportType.Messaging
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
var response = await client.GetAsync("/health/ready");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||||
|
|
||||||
|
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||||
|
Assert.Equal("policy", payload.RootElement.GetProperty("missingMicroservices")[0].GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebApplicationFactory<Program> CreateFactoryWithRequiredServices(params string[] requiredServices)
|
||||||
|
{
|
||||||
|
return new GatewayWebApplicationFactory().WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureTestServices(services =>
|
||||||
|
{
|
||||||
|
services.PostConfigure<GatewayOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Health.RequiredMicroservices = requiredServices.ToList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenApiJson_ReturnsValidOpenApiDocument()
|
public async Task OpenApiJson_ReturnsValidOpenApiDocument()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -259,6 +259,137 @@ public sealed class RouteDispatchMiddlewareMicroserviceTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_RegexCatchAll_CaptureGroupSubstitution_ResolvesServiceAndPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var resolver = new StellaOpsRouteResolver(
|
||||||
|
[
|
||||||
|
new StellaOpsRoute
|
||||||
|
{
|
||||||
|
Type = StellaOpsRouteType.Microservice,
|
||||||
|
Path = @"^/api/v1/([^/]+)(.*)",
|
||||||
|
IsRegex = true,
|
||||||
|
TranslatesTo = "http://$1.stella-ops.local/api/v1/$1$2"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||||
|
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||||
|
|
||||||
|
var nextCalled = false;
|
||||||
|
var middleware = new RouteDispatchMiddleware(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
nextCalled = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
resolver,
|
||||||
|
httpClientFactory.Object,
|
||||||
|
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||||
|
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Request.Path = "/api/v1/scanner/health";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await middleware.InvokeAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(nextCalled);
|
||||||
|
Assert.Equal(
|
||||||
|
"scanner",
|
||||||
|
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||||
|
// Path matches request: no translation needed
|
||||||
|
Assert.False(context.Items.ContainsKey(RouterHttpContextKeys.TranslatedRequestPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_RegexAlias_CaptureGroupSubstitution_ResolvesCorrectService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var resolver = new StellaOpsRouteResolver(
|
||||||
|
[
|
||||||
|
new StellaOpsRoute
|
||||||
|
{
|
||||||
|
Type = StellaOpsRouteType.Microservice,
|
||||||
|
Path = @"^/api/v1/vulnerabilities(.*)",
|
||||||
|
IsRegex = true,
|
||||||
|
TranslatesTo = "http://scanner.stella-ops.local/api/v1/vulnerabilities$1"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||||
|
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||||
|
|
||||||
|
var nextCalled = false;
|
||||||
|
var middleware = new RouteDispatchMiddleware(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
nextCalled = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
resolver,
|
||||||
|
httpClientFactory.Object,
|
||||||
|
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||||
|
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Request.Path = "/api/v1/vulnerabilities/cve-2024-1234";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await middleware.InvokeAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(nextCalled);
|
||||||
|
Assert.Equal(
|
||||||
|
"scanner",
|
||||||
|
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_RegexCatchAll_WithPathRewrite_SetsTranslatedPath()
|
||||||
|
{
|
||||||
|
// Arrange: advisoryai /api/v1/search -> /v1/search (path rewrite via capture group)
|
||||||
|
var resolver = new StellaOpsRouteResolver(
|
||||||
|
[
|
||||||
|
new StellaOpsRoute
|
||||||
|
{
|
||||||
|
Type = StellaOpsRouteType.Microservice,
|
||||||
|
Path = @"^/api/v1/search(.*)",
|
||||||
|
IsRegex = true,
|
||||||
|
TranslatesTo = "http://advisoryai.stella-ops.local/v1/search$1"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||||
|
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||||
|
|
||||||
|
var nextCalled = false;
|
||||||
|
var middleware = new RouteDispatchMiddleware(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
nextCalled = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
resolver,
|
||||||
|
httpClientFactory.Object,
|
||||||
|
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||||
|
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Request.Path = "/api/v1/search/entities";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await middleware.InvokeAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(nextCalled);
|
||||||
|
Assert.Equal(
|
||||||
|
"advisoryai",
|
||||||
|
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||||
|
Assert.Equal(
|
||||||
|
"/v1/search/entities",
|
||||||
|
context.Items[RouterHttpContextKeys.TranslatedRequestPath] as string);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_MicroserviceApiPath_DoesNotUseSpaFallback()
|
public async Task InvokeAsync_MicroserviceApiPath_DoesNotUseSpaFallback()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/dashboard"));
|
var result = resolver.Resolve(new PathString("/dashboard"));
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result.Route);
|
||||||
Assert.Equal("/dashboard", result.Path);
|
Assert.Equal("/dashboard", result.Route.Path);
|
||||||
|
Assert.Null(result.RegexMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -41,8 +42,9 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/app/index.html"));
|
var result = resolver.Resolve(new PathString("/app/index.html"));
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result.Route);
|
||||||
Assert.Equal("/app", result.Path);
|
Assert.Equal("/app", result.Route.Path);
|
||||||
|
Assert.Null(result.RegexMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -53,9 +55,28 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/api/v2/data"));
|
var result = resolver.Resolve(new PathString("/api/v2/data"));
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result.Route);
|
||||||
Assert.True(result.IsRegex);
|
Assert.True(result.Route.IsRegex);
|
||||||
Assert.Equal(@"^/api/v[0-9]+/.*", result.Path);
|
Assert.Equal(@"^/api/v[0-9]+/.*", result.Route.Path);
|
||||||
|
Assert.NotNull(result.RegexMatch);
|
||||||
|
Assert.True(result.RegexMatch.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Resolve_RegexRoute_ReturnsCaptureGroups()
|
||||||
|
{
|
||||||
|
var route = MakeRoute(
|
||||||
|
@"^/api/v1/([^/]+)(.*)",
|
||||||
|
isRegex: true,
|
||||||
|
translatesTo: "http://$1.stella-ops.local/api/v1/$1$2");
|
||||||
|
var resolver = new StellaOpsRouteResolver(new[] { route });
|
||||||
|
|
||||||
|
var result = resolver.Resolve(new PathString("/api/v1/scanner/health"));
|
||||||
|
|
||||||
|
Assert.NotNull(result.Route);
|
||||||
|
Assert.NotNull(result.RegexMatch);
|
||||||
|
Assert.Equal("scanner", result.RegexMatch.Groups[1].Value);
|
||||||
|
Assert.Equal("/health", result.RegexMatch.Groups[2].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -66,7 +87,8 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/unknown"));
|
var result = resolver.Resolve(new PathString("/unknown"));
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result.Route);
|
||||||
|
Assert.Null(result.RegexMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -78,8 +100,8 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/api/resource"));
|
var result = resolver.Resolve(new PathString("/api/resource"));
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result.Route);
|
||||||
Assert.Equal("http://first:5000", result.TranslatesTo);
|
Assert.Equal("http://first:5000", result.Route.TranslatesTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -90,7 +112,7 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/not-found"));
|
var result = resolver.Resolve(new PathString("/not-found"));
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result.Route);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -101,7 +123,7 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/error"));
|
var result = resolver.Resolve(new PathString("/error"));
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result.Route);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -112,8 +134,8 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/APP"));
|
var result = resolver.Resolve(new PathString("/APP"));
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result.Route);
|
||||||
Assert.Equal("/app", result.Path);
|
Assert.Equal("/app", result.Route.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -123,6 +145,6 @@ public sealed class StellaOpsRouteResolverTests
|
|||||||
|
|
||||||
var result = resolver.Resolve(new PathString("/anything"));
|
var result = resolver.Resolve(new PathString("/anything"));
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result.Route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
| RGH-01-T | DONE | 2026-02-22: Added route-dispatch unit tests for microservice SPA fallback and API-prefix bypass behavior. |
|
| RGH-01-T | DONE | 2026-02-22: Added route-dispatch unit tests for microservice SPA fallback and API-prefix bypass behavior. |
|
||||||
| RGH-03-T | DONE | 2026-03-05: Added deterministic route-table parity tests for unified search mappings across gateway runtime and compose configs; verified in gateway test run. |
|
| RGH-03-T | DONE | 2026-03-05: Added deterministic route-table parity tests for unified search mappings across gateway runtime and compose configs; verified in gateway test run. |
|
||||||
| LIVE-ROUTER-012-T1 | DONE | 2026-03-09: Added quota compatibility regressions for coarse-scope expansion and authorization against resolved gateway scopes. |
|
| LIVE-ROUTER-012-T1 | DONE | 2026-03-09: Added quota compatibility regressions for coarse-scope expansion and authorization against resolved gateway scopes. |
|
||||||
|
| ROUTER-READY-001-T | DONE | 2026-03-10: Added required-service readiness coverage and truthful warm-up status contracts for router frontdoor convergence. |
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
|||||||
Region = "test",
|
Region = "test",
|
||||||
InstanceId = "test-instance-1",
|
InstanceId = "test-instance-1",
|
||||||
HeartbeatInterval = TimeSpan.FromMilliseconds(50),
|
HeartbeatInterval = TimeSpan.FromMilliseconds(50),
|
||||||
|
RegistrationRefreshInterval = TimeSpan.FromMilliseconds(20),
|
||||||
ReconnectBackoffInitial = TimeSpan.FromMilliseconds(10),
|
ReconnectBackoffInitial = TimeSpan.FromMilliseconds(10),
|
||||||
ReconnectBackoffMax = TimeSpan.FromMilliseconds(100)
|
ReconnectBackoffMax = TimeSpan.FromMilliseconds(100)
|
||||||
};
|
};
|
||||||
@@ -385,6 +386,47 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
|||||||
capturedHeartbeat.ErrorRate.Should().Be(0.05);
|
capturedHeartbeat.ErrorRate.Should().Be(0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Trait("Category", TestCategories.Unit)]
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_ReplaysHelloWithinRegistrationRefreshInterval()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_options.Routers.Add(new RouterEndpointConfig
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 5000,
|
||||||
|
TransportType = TransportType.InMemory
|
||||||
|
});
|
||||||
|
|
||||||
|
var registrationReplayObserved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var connectCount = 0;
|
||||||
|
_transportMock
|
||||||
|
.Setup(t => t.ConnectAsync(
|
||||||
|
It.IsAny<InstanceDescriptor>(),
|
||||||
|
It.IsAny<IReadOnlyList<EndpointDescriptor>>(),
|
||||||
|
It.IsAny<IReadOnlyDictionary<string, SchemaDefinition>?>(),
|
||||||
|
It.IsAny<ServiceOpenApiInfo?>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.Callback(() =>
|
||||||
|
{
|
||||||
|
if (Interlocked.Increment(ref connectCount) >= 2)
|
||||||
|
{
|
||||||
|
registrationReplayObserved.TrySetResult();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
using var manager = CreateManager();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await manager.StartAsync(CancellationToken.None);
|
||||||
|
await registrationReplayObserved.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||||
|
await manager.StopAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
connectCount.Should().BeGreaterThanOrEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Dispose Tests
|
#region Dispose Tests
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
| AUDIT-0393-T | DONE | Revalidated 2026-01-07; test coverage audit for Router StellaOps.Microservice.Tests. |
|
| AUDIT-0393-T | DONE | Revalidated 2026-01-07; test coverage audit for Router StellaOps.Microservice.Tests. |
|
||||||
| AUDIT-0393-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
| AUDIT-0393-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||||
|
| ROUTER-READY-004-T | DONE | 2026-03-10: Added deterministic coverage for bounded HELLO replay cadence after gateway restarts. |
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ public sealed class StellaRouterIntegrationHelperTests
|
|||||||
["TimelineIndexer:Router:Messaging:RequestTimeout"] = "45s",
|
["TimelineIndexer:Router:Messaging:RequestTimeout"] = "45s",
|
||||||
["TimelineIndexer:Router:Messaging:LeaseDuration"] = "4m",
|
["TimelineIndexer:Router:Messaging:LeaseDuration"] = "4m",
|
||||||
["TimelineIndexer:Router:Messaging:HeartbeatInterval"] = "12s",
|
["TimelineIndexer:Router:Messaging:HeartbeatInterval"] = "12s",
|
||||||
|
["TimelineIndexer:Router:RegistrationRefreshIntervalSeconds"] = "7",
|
||||||
["TimelineIndexer:Router:Messaging:valkey:ConnectionString"] = "cache.stella-ops.local:6379",
|
["TimelineIndexer:Router:Messaging:valkey:ConnectionString"] = "cache.stella-ops.local:6379",
|
||||||
["TimelineIndexer:Router:Messaging:valkey:Database"] = "2"
|
["TimelineIndexer:Router:Messaging:valkey:Database"] = "2"
|
||||||
});
|
});
|
||||||
@@ -223,6 +224,7 @@ public sealed class StellaRouterIntegrationHelperTests
|
|||||||
|
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
Assert.Equal(TimeSpan.FromSeconds(12), options.HeartbeatInterval);
|
Assert.Equal(TimeSpan.FromSeconds(12), options.HeartbeatInterval);
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(7), options.RegistrationRefreshInterval);
|
||||||
Assert.Equal("router:requests:{service}", messaging.RequestQueueTemplate);
|
Assert.Equal("router:requests:{service}", messaging.RequestQueueTemplate);
|
||||||
Assert.Equal("router:responses", messaging.ResponseQueueName);
|
Assert.Equal("router:responses", messaging.ResponseQueueName);
|
||||||
Assert.Equal("timelineindexer", messaging.ConsumerGroup);
|
Assert.Equal("timelineindexer", messaging.ConsumerGroup);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using FluentAssertions;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Moq;
|
using Moq;
|
||||||
using StellaOps.Router.Common.Abstractions;
|
using StellaOps.Router.Common.Abstractions;
|
||||||
|
using StellaOps.Router.Common.Enums;
|
||||||
using StellaOps.Router.Common.Models;
|
using StellaOps.Router.Common.Models;
|
||||||
using StellaOps.Router.Gateway.Middleware;
|
using StellaOps.Router.Gateway.Middleware;
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ public sealed class EndpointResolutionMiddlewareTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Invoke_DoesNotFallbackToDifferentService_WhenTargetHintHasNoMatch()
|
public async Task Invoke_Returns503_WhenTargetServiceIsNotRegistered()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = new DefaultHttpContext();
|
var context = new DefaultHttpContext();
|
||||||
@@ -144,12 +145,91 @@ public sealed class EndpointResolutionMiddlewareTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
nextCalled.Should().BeFalse();
|
nextCalled.Should().BeFalse();
|
||||||
context.Response.StatusCode.Should().Be(StatusCodes.Status404NotFound);
|
context.Response.StatusCode.Should().Be(StatusCodes.Status503ServiceUnavailable);
|
||||||
routingState.Verify(
|
routingState.Verify(
|
||||||
state => state.ResolveEndpoint(It.IsAny<string>(), It.IsAny<string>()),
|
state => state.ResolveEndpoint(It.IsAny<string>(), It.IsAny<string>()),
|
||||||
Times.Never);
|
Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Invoke_Returns503_WhenSpecificGatewayHintWouldOtherwiseMatchSiblingService()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Request.Method = HttpMethods.Get;
|
||||||
|
context.Request.Path = "/api/v1/policy/__router_smoke__";
|
||||||
|
context.Items[RouterHttpContextKeys.TranslatedRequestPath] = "/api/v1/policy/__router_smoke__";
|
||||||
|
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] = "policy-gateway";
|
||||||
|
|
||||||
|
var siblingEndpoint = new EndpointDescriptor
|
||||||
|
{
|
||||||
|
ServiceName = "policy-engine",
|
||||||
|
Version = "1.0.0",
|
||||||
|
Method = HttpMethods.Get,
|
||||||
|
Path = "/api/v1/policy/__router_smoke__"
|
||||||
|
};
|
||||||
|
|
||||||
|
var routingState = new Mock<IGlobalRoutingState>();
|
||||||
|
routingState
|
||||||
|
.Setup(state => state.GetAllConnections())
|
||||||
|
.Returns(
|
||||||
|
[
|
||||||
|
CreateConnection("conn-policy-engine", "policy-engine", siblingEndpoint)
|
||||||
|
]);
|
||||||
|
|
||||||
|
var nextCalled = false;
|
||||||
|
var middleware = new EndpointResolutionMiddleware(_ =>
|
||||||
|
{
|
||||||
|
nextCalled = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
await middleware.Invoke(context, routingState.Object);
|
||||||
|
|
||||||
|
nextCalled.Should().BeFalse();
|
||||||
|
context.Response.StatusCode.Should().Be(StatusCodes.Status503ServiceUnavailable);
|
||||||
|
routingState.Verify(
|
||||||
|
state => state.ResolveEndpoint(It.IsAny<string>(), It.IsAny<string>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Invoke_Returns404_WhenTargetServiceIsRegisteredButEndpointDoesNotExist()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Request.Method = HttpMethods.Get;
|
||||||
|
context.Request.Path = "/api/v1/governance/not-real";
|
||||||
|
context.Items[RouterHttpContextKeys.TranslatedRequestPath] = "/api/v1/governance/not-real";
|
||||||
|
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] = "policy";
|
||||||
|
|
||||||
|
var otherEndpoint = new EndpointDescriptor
|
||||||
|
{
|
||||||
|
ServiceName = "policy-gateway",
|
||||||
|
Version = "1.0.0",
|
||||||
|
Method = HttpMethods.Get,
|
||||||
|
Path = "/api/v1/governance/staleness/config"
|
||||||
|
};
|
||||||
|
|
||||||
|
var routingState = new Mock<IGlobalRoutingState>();
|
||||||
|
routingState
|
||||||
|
.Setup(state => state.GetAllConnections())
|
||||||
|
.Returns(
|
||||||
|
[
|
||||||
|
CreateConnection("conn-policy", "policy-gateway", otherEndpoint)
|
||||||
|
]);
|
||||||
|
|
||||||
|
var nextCalled = false;
|
||||||
|
var middleware = new EndpointResolutionMiddleware(_ =>
|
||||||
|
{
|
||||||
|
nextCalled = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
await middleware.Invoke(context, routingState.Object);
|
||||||
|
|
||||||
|
nextCalled.Should().BeFalse();
|
||||||
|
context.Response.StatusCode.Should().Be(StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Invoke_MatchesRouteHintWithServicePrefixAlias()
|
public async Task Invoke_MatchesRouteHintWithServicePrefixAlias()
|
||||||
{
|
{
|
||||||
@@ -186,10 +266,55 @@ public sealed class EndpointResolutionMiddlewareTests
|
|||||||
context.Items[RouterHttpContextKeys.TargetMicroservice].Should().Be("findings-ledger");
|
context.Items[RouterHttpContextKeys.TargetMicroservice].Should().Be("findings-ledger");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Invoke_WhenMultipleMatchingConnectionsExist_PrefersHealthyMostRecentEndpoint()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Request.Method = HttpMethods.Get;
|
||||||
|
context.Request.Path = "/api/v1/governance/staleness/config";
|
||||||
|
context.Items[RouterHttpContextKeys.TranslatedRequestPath] = "/api/v1/governance/staleness/config";
|
||||||
|
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] = "policy";
|
||||||
|
|
||||||
|
var staleEndpoint = new EndpointDescriptor
|
||||||
|
{
|
||||||
|
ServiceName = "policy-gateway",
|
||||||
|
Version = "1.0.0",
|
||||||
|
Method = HttpMethods.Get,
|
||||||
|
Path = "/api/v1/governance/staleness/config"
|
||||||
|
};
|
||||||
|
|
||||||
|
var healthyEndpoint = new EndpointDescriptor
|
||||||
|
{
|
||||||
|
ServiceName = "policy-gateway",
|
||||||
|
Version = "1.0.1",
|
||||||
|
Method = HttpMethods.Get,
|
||||||
|
Path = "/api/v1/governance/staleness/config"
|
||||||
|
};
|
||||||
|
|
||||||
|
var routingState = new Mock<IGlobalRoutingState>();
|
||||||
|
routingState
|
||||||
|
.Setup(state => state.GetAllConnections())
|
||||||
|
.Returns(
|
||||||
|
[
|
||||||
|
CreateConnection("conn-stale", "policy-gateway", staleEndpoint, InstanceHealthStatus.Unhealthy, new DateTime(2026, 3, 10, 1, 10, 0, DateTimeKind.Utc)),
|
||||||
|
CreateConnection("conn-healthy", "policy-gateway", healthyEndpoint, InstanceHealthStatus.Healthy, new DateTime(2026, 3, 10, 1, 11, 0, DateTimeKind.Utc))
|
||||||
|
]);
|
||||||
|
|
||||||
|
var middleware = new EndpointResolutionMiddleware(_ => Task.CompletedTask);
|
||||||
|
|
||||||
|
await middleware.Invoke(context, routingState.Object);
|
||||||
|
|
||||||
|
context.Response.StatusCode.Should().NotBe(StatusCodes.Status404NotFound);
|
||||||
|
context.Items[RouterHttpContextKeys.EndpointDescriptor].Should().BeSameAs(healthyEndpoint);
|
||||||
|
context.Items[RouterHttpContextKeys.TargetMicroservice].Should().Be("policy-gateway");
|
||||||
|
}
|
||||||
|
|
||||||
private static ConnectionState CreateConnection(
|
private static ConnectionState CreateConnection(
|
||||||
string connectionId,
|
string connectionId,
|
||||||
string serviceName,
|
string serviceName,
|
||||||
EndpointDescriptor endpoint)
|
EndpointDescriptor endpoint,
|
||||||
|
InstanceHealthStatus status = InstanceHealthStatus.Healthy,
|
||||||
|
DateTime? lastHeartbeatUtc = null)
|
||||||
{
|
{
|
||||||
var instance = new InstanceDescriptor
|
var instance = new InstanceDescriptor
|
||||||
{
|
{
|
||||||
@@ -203,6 +328,8 @@ public sealed class EndpointResolutionMiddlewareTests
|
|||||||
{
|
{
|
||||||
ConnectionId = connectionId,
|
ConnectionId = connectionId,
|
||||||
Instance = instance,
|
Instance = instance,
|
||||||
|
Status = status,
|
||||||
|
LastHeartbeatUtc = lastHeartbeatUtc ?? new DateTime(2026, 3, 10, 1, 0, 0, DateTimeKind.Utc),
|
||||||
TransportType = StellaOps.Router.Common.Enums.TransportType.Messaging
|
TransportType = StellaOps.Router.Common.Enums.TransportType.Messaging
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,15 +54,46 @@ public sealed class InMemoryRoutingStateTests
|
|||||||
.Which.ConnectionId.Should().Be("conn-1");
|
.Which.ConnectionId.Should().Be("conn-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveEndpoint_WhenSamePathExistsAcrossVersions_PrefersHealthyMostRecentRegistration()
|
||||||
|
{
|
||||||
|
var state = new InMemoryRoutingState();
|
||||||
|
var staleConnection = CreateConnection(
|
||||||
|
connectionId: "conn-stale",
|
||||||
|
instanceId: "policy-gateway-old",
|
||||||
|
endpointPath: "/api/v1/governance/staleness/config",
|
||||||
|
version: "1.0.0",
|
||||||
|
status: InstanceHealthStatus.Unhealthy,
|
||||||
|
lastHeartbeatUtc: new DateTime(2026, 3, 10, 1, 10, 0, DateTimeKind.Utc));
|
||||||
|
var healthyConnection = CreateConnection(
|
||||||
|
connectionId: "conn-healthy",
|
||||||
|
instanceId: "policy-gateway-new",
|
||||||
|
endpointPath: "/api/v1/governance/staleness/config",
|
||||||
|
version: "1.0.1",
|
||||||
|
status: InstanceHealthStatus.Healthy,
|
||||||
|
lastHeartbeatUtc: new DateTime(2026, 3, 10, 1, 11, 0, DateTimeKind.Utc));
|
||||||
|
|
||||||
|
state.AddConnection(staleConnection);
|
||||||
|
state.AddConnection(healthyConnection);
|
||||||
|
|
||||||
|
var resolved = state.ResolveEndpoint("GET", "/api/v1/governance/staleness/config");
|
||||||
|
|
||||||
|
resolved.Should().NotBeNull();
|
||||||
|
resolved!.Version.Should().Be("1.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
private static ConnectionState CreateConnection(
|
private static ConnectionState CreateConnection(
|
||||||
string connectionId,
|
string connectionId,
|
||||||
string instanceId,
|
string instanceId,
|
||||||
string endpointPath)
|
string endpointPath,
|
||||||
|
string version = "1.0.0",
|
||||||
|
InstanceHealthStatus status = InstanceHealthStatus.Healthy,
|
||||||
|
DateTime? lastHeartbeatUtc = null)
|
||||||
{
|
{
|
||||||
var endpoint = new EndpointDescriptor
|
var endpoint = new EndpointDescriptor
|
||||||
{
|
{
|
||||||
ServiceName = "integrations",
|
ServiceName = "integrations",
|
||||||
Version = "1.0.0",
|
Version = version,
|
||||||
Method = "GET",
|
Method = "GET",
|
||||||
Path = endpointPath
|
Path = endpointPath
|
||||||
};
|
};
|
||||||
@@ -73,10 +104,12 @@ public sealed class InMemoryRoutingStateTests
|
|||||||
Instance = new InstanceDescriptor
|
Instance = new InstanceDescriptor
|
||||||
{
|
{
|
||||||
InstanceId = instanceId,
|
InstanceId = instanceId,
|
||||||
ServiceName = "integrations",
|
ServiceName = endpointPath.Contains("/governance/", StringComparison.Ordinal) ? "policy-gateway" : "integrations",
|
||||||
Version = "1.0.0",
|
Version = version,
|
||||||
Region = "local"
|
Region = "local"
|
||||||
},
|
},
|
||||||
|
Status = status,
|
||||||
|
LastHeartbeatUtc = lastHeartbeatUtc ?? new DateTime(2026, 3, 10, 1, 0, 0, DateTimeKind.Utc),
|
||||||
TransportType = TransportType.Messaging
|
TransportType = TransportType.Messaging
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user