Skip to content

fix: split platform control-plane env from site env in Context - #148

Merged
birme merged 1 commit into
mainfrom
fix/platform-env-split
Jul 29, 2026
Merged

birme merged 1 commit into
mainfrom
fix/platform-env-split

Conversation

@birme

@birme birme commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

Closes #147.

MyApps running on Elastx (prod-se) fail to fetch their env vars from eyevinn-app-config-svc at container startup whenever Elastx's own catalog-manager/token-service/money-manager are down (e.g. during migration/scale-down ops) — even though the actual data source (the tenant's app-config-svc instance) is healthy and reachable throughout. Confirmed root cause during a 2026-07-29 11:40–13:04 UTC production incident.

prod-se (Elastx) is a site of the single prod environment, not an independent peer environment — infra-osaas/osaas/prod-estx.yml declares environment: "prod" on every Elastx platform ConfigMap, and pat-secret/mac-keys are byte-identical across both clusters. Context previously reused one environment value for both site resolution (which cluster hosts the instance data — already fully data-driven via service.apiUrl) and platform/auth calls (subscription check + SAT mint). A context configured with environment=prod-se therefore called Elastx's own catalog-manager and token-service, which pull in Elastx's own money-manager over in-cluster DNS with no cross-cluster fallback — turning a transient Elastx control-plane blip into a MyApp startup outage.

Changes

  • packages/core/src/context.ts: split Context into environment (site) and a new platformEnv (control plane). Added PRIMARY_ENV (process.env.OSC_PRIMARY_ENV || 'prod') and PROD_SITE_ENVS = ['prod', 'prod-se']. platformEnv defaults to PRIMARY_ENV only when environment is prod or prod-se — dev and stage are independent single-cluster environments with their own control plane and are intentionally excluded, so platformEnv stays equal to environment for them. An explicit config.platformEnv always wins. Added getPlatformEnvironment() accessor. Switched /mysubscriptions (GET + POST) and /servicetoken to use platformEnv.
  • packages/core/src/core.ts: getService()'s /mysubscriptions call now uses context.getPlatformEnvironment().
  • packages/core/src/admin.ts: listSubscriptions() and removeSubscription()'s /mysubscriptions calls now use context.getPlatformEnvironment() (same auth/entitlement concern as context.ts, previously missed the same coupling).
  • Left unchanged (verified, genuinely site-scoped): core.ts saveSecret/listSecrets/getSecret/updateSecret/deleteSecret (deploy.svc/mysecrets), admin.ts listReservedNodes (deploy.svc/mynodes), and all of myapp.ts (deploy.svc/myapps) — deploy-manager owns cluster-local K8s resources per site, so these must keep following environment.
  • Out of scope (verified, different mechanism): packages/core/src/platform.ts's Platform class (used by packages/cli/src/admin/syncfork.ts and packages/core/src/platform/maker.ts for maker.svc.* calls) uses a separate OSC_API_KEY auth model for admin/build tooling, not the PAT-based MyApps runtime path implicated in this incident. packages/ai/src/chat.ts/embeddings.ts's ai.svc.${environment} calls are an application service endpoint, not an auth/entitlement endpoint, and were not touched.

Tests added in packages/core/src/context.test.ts cover: default platformEnv for prod/prod-se/dev/stage, explicit override wins, and PROD_SITE_ENVS contents. packages/core/src/core.test.ts gained a test confirming saveSecret still targets deploy.svc.<environment> (site) even when environment and platformEnv diverge on prod-se.

Test plan

  • npx tsc --noEmit -p tsconfig.json (packages/core) — passes
  • npx eslint src/context.ts src/context.test.ts src/core.ts src/core.test.ts src/admin.ts — 0 errors (6 pre-existing no-explicit-any warnings in unrelated lines of core.ts)
  • npx prettier --check on all changed files — passes
  • npx jest (packages/core, full suite) — 33/33 passing, including new tests

Lessons

A previous investigation session's git pull --ff-only on all five runner repos (web/python/golang/dotnet/wasm-runner) returned quietly without actually fast-forwarding, leaving those checkouts 3-5 commits behind origin/main and producing a false "no --env flag propagated" conclusion. git pull succeeding is not sufficient confirmation of being current — always follow with git rev-list --count HEAD..origin/main and confirm it's 0 before drawing conclusions from a local checkout. Applied here: client-ts was confirmed at 0 commits behind before any file was read.

Separately, the original fix instructions assumed platformEnv should unconditionally default to PRIMARY_ENV. That's correct for prod/prod-se but wrong for dev/stage, which are genuinely independent single-cluster environments with their own control plane — unconditionally forcing them onto prod would have broken dev/stage platform calls. The PROD_SITE_ENVS gate (['prod', 'prod-se']) exists specifically to scope the redirect to the one pair of environments where it applies.

Also worth flagging for a follow-up: admin.ts's listSubscriptions/removeSubscription had the exact same /mysubscriptions + context.getEnvironment() coupling as context.ts, and would have silently kept the same bug for anyone managing subscriptions from a prod-se-configured Context if not caught by grepping the whole packages/core/src tree for all /mysubscriptions and /servicetoken call sites rather than trusting the 4 call sites named in the original bug report.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

MyApps on Elastx (prod-se) fail to fetch env vars from
eyevinn-app-config-svc at container startup whenever Elastx's own
catalog-manager/token-service/money-manager are down, even though the
actual data source (the tenant's app-config-svc instance) is healthy.

prod-se is a site of the prod environment, not an independent peer
environment (infra-osaas/osaas/prod-estx.yml declares environment:
"prod" on every Elastx platform ConfigMap, and pat-secret/mac-keys are
byte-identical across both clusters). Context previously reused a
single `environment` value for both site resolution (which cluster
hosts the instance data) and platform/auth calls (subscription check
+ SAT mint), so a context configured with environment=prod-se called
Elastx's own catalog-manager and token-service, which in turn hit
Elastx's own money-manager over in-cluster DNS with no cross-cluster
fallback.

Splits Context into `environment` (site, used by deploy-manager calls
which are genuinely per-cluster) and `platformEnv` (control plane,
used by /mysubscriptions and /servicetoken). `platformEnv` defaults to
PRIMARY_ENV only when `environment` is `prod` or `prod-se`; `dev` and
`stage` are independent single-cluster environments and keep
platformEnv equal to environment.

Closes #147

Co-Authored-By: Claude <noreply@anthropic.com>
@birme birme self-assigned this Jul 29, 2026
@birme

birme commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Research Review

Verified

  • context.ts getServiceAccessToken() / activateService(): Both /mysubscriptions (GET+POST) and /servicetoken now build their URL from this.platformEnv instead of this.environment — confirmed at context.ts:83 (catalog.svc.${this.platformEnv}), :112 (token.svc.${this.platformEnv}/servicetoken), :127 (catalog.svc.${this.platformEnv} for POST). Not a guess — read directly from the diff.
  • core.ts getService(): catalog.svc.${context.getPlatformEnvironment()}.osaas.io/mysubscriptions confirmed at core.ts:10.
  • admin.ts listSubscriptions() / removeSubscription(): both confirmed switched to context.getPlatformEnvironment() at admin.ts:52 and :76. This is a genuine catch, not a missed-then-guessed fix — same /mysubscriptions auth/entitlement coupling as context.ts, correctly identified as previously overlooked.
  • "Left unchanged" claims verified by direct grep, not taken on faith:
    • core.ts secrets functions (saveSecret/etc., lines 533/563/617/643) still use ctx.getEnvironment() against deploy.svc.* — genuinely site-scoped (deploy-manager is per-cluster).
    • admin.ts listReservedNodes (line 100) still uses context.getEnvironment() against deploy.svc.*/mynodes — same site-scoped reasoning, confirmed.
    • myapp.ts (4 call sites) all still use ctx.getEnvironment() against deploy.svc.* — confirmed unchanged.
    • platform.ts's Platform class uses its own OSC_API_KEY-based config, entirely separate from Context, confirmed unchanged and consumed only by platform/maker.ts and cli/admin/syncfork.ts (maker.svc.*) — matches the PR's "different auth mechanism" claim.
    • packages/ai/src/chat.ts:23-24 and embeddings.ts:33-34 still resolve ai.svc.${environment} from getEnvironment() — confirmed unchanged, and the PR's characterization ("application service endpoint, not auth/entitlement") is consistent with how those two files use it (no subscription/token minting there).
  • Default logic exactly matches spec, no off-by-one: context.ts constructor — this.platformEnv = config?.platformEnv ? config.platformEnv : PROD_SITE_ENVS.includes(this.environment) ? PRIMARY_ENV : this.environment; with PROD_SITE_ENVS = ['prod', 'prod-se']. dev/stage correctly fall through to this.environment unchanged; prod/prod-se correctly collapse to PRIMARY_ENV ('prod' by default, overridable via OSC_PRIMARY_ENV). Explicit config.platformEnv always wins regardless of environment. No logic error found.
  • context.test.ts: all four environments (prod, prod-se, dev, stage) exercised individually, plus explicit-override tests for both prod-se and dev, plus a direct assertion on PROD_SITE_ENVS contents. Full coverage of the claimed matrix.
  • core.test.ts: new test constructs a prod-se context, asserts getPlatformEnvironment() returns 'prod' while getEnvironment() stays 'prod-se', then calls the real saveSecret(serviceId, name, value, ctx) (signature matches core.ts:525-530) and asserts the mocked fetch URL hostname is deploy.svc.prod-se.osaas.io — genuinely proves site-scoped calls are untouched by the split, not a superficial test.
  • Architectural claim about prod-se verified against source of truth: infra-osaas/osaas/prod-estx.yml does set environment: "prod" on every Elastx platform ConfigMap block (checked lines 51, 84, 119, 159, 196, 243) — the PR's central architectural justification is factually grounded, not asserted from memory.
  • CI: gh pr checks 148 shows the ts check passing (1m57s).
  • PR body: contains both 🤖 Generated with [Claude Code](https://claude.ai/code) and Co-Authored-By: Claude <noreply@anthropic.com> trailer, and a filled, non-placeholder ## Lessons section with two substantive entries (stale-checkout gotcha and the dev/stage exclusion rationale). Passes the mandatory structural check.

Needs Verification

  • None found. This PR was unusually well-researched — every named call site, every "left unchanged" claim, and the underlying architectural premise were all independently confirmed against the actual source rather than taken on the PR author's word.

Adjacent Stubs Detected

None. Swept context.ts, core.ts, admin.ts (full files, not just diff lines) for undefined : undefined, || undefined, TODO/FIXME/XXX/HACK, dead-code comments, empty returns, console.log-only bodies, disabled/hidden UI props, and empty catch blocks. The only console.log hit is inside a JSDoc @example comment in core.ts:89, not executable code.

Orphaned Files Detected

Not applicable — this PR modifies existing, already-wired library source files (context.ts, core.ts, admin.ts) that are part of the public package API surface consumed by other repos over npm, plus two test files that Jest picks up directly. No new standalone component/page files were added.

Impact Analysis

  • Blast radius: @osaas/client-core's Context class is consumed via new Context(...) in at least osaas-deploy-manager (many call sites: api_myapps.ts, api_mybackups.ts, api_paramstores.ts, api_mysuspended.ts, api_agentic_sdlc.ts, agentic-sdlc/source-info.ts, etc.), osaas-app (src/server/chat.ts), and osaas-ai (src/mcp/context.ts). All three declare "@osaas/client-core" as a dependency (^0.25.1, ^0.11.0, ^0.25.0 respectively).
  • Downstream consumers: the change is additive and backward-compatible for existing callers — platformEnv is a new optional config field with a computed default, and getPlatformEnvironment() is a new method; no existing method signature changed. Callers using environment: 'dev'/'stage'/'prod' see no behavior change. Callers using environment: 'prod-se' get the intended behavior change (auth/entitlement calls now correctly target prod, not prod-se's own control plane) — this is the fix, not a regression.
  • Coordinated changes needed: package version is still 0.25.1 on this branch (unchanged) — consistent with this repo's release process, where chore(release): publish is a separate commit that triggers the npm publish workflow (.github/workflows/main.yml), not something bundled into feature PRs. Recommend the follow-up release be a patch bump (e.g., 0.25.2), not minor — osaas-deploy-manager and osaas-app/osaas-ai pin ^0.25.x/^0.11.x ranges, and a patch bump lets npm install in those repos pick up the fix automatically on next lockfile refresh without requiring manual package.json edits across three repos. If the maintainer instead bumps to 0.26.0, downstream repos will need explicit dependency bumps to actually receive the fix — worth flagging so the MyApps-on-Elastx incident is actually resolved end-to-end, not just fixed in the library.
  • Breaking change risk: low. No existing method signature or default behavior changed for dev/stage/prod callers; only prod-se callers change behavior, and that's the explicit intent of the fix.

Risks Not Tested

  • Runtime behavior against a live Elastx cluster during an actual control-plane outage was not (and cannot be) verified from a diff review — the fix is validated by unit tests against mocked fetch calls, not an integration test against real catalog.svc.prod-se.osaas.io vs catalog.svc.prod.osaas.io endpoints.
  • Whether any other repo outside this monorepo constructs Context with environment: 'prod-se' and separately, independently works around the old bug (e.g., a manual second catalog/token call hardcoded to prod) was checked only for osaas-deploy-manager, osaas-app, osaas-ai locally — no hits found, but this repo list is not necessarily exhaustive of every client-core consumer.
  • Actual downstream release/version-bump follow-through (whether npm version patch is run, and whether osaas-deploy-manager/osaas-app/osaas-ai subsequently pick up the new version) is a post-merge action not verifiable from this PR alone.

Research Suggestions

  • After merge, run npm version patch (not minor) per this repo's established release convention, and confirm the chore(release): publish commit triggers main.yml's publish job successfully.
  • Once published, verify osaas-deploy-manager picks up the new version (npm install / lockfile refresh) and that a prod-se-configured Context in a real deploy-manager request path now resolves getPlatformEnvironment() to prod end-to-end, not just in this repo's unit tests.

Reviewed by PR Research Validator. This review checks whether code is based on verified sources rather than assumptions.

@birme
birme merged commit a04549b into main Jul 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MyApp config fetch on Elastx (prod-se) fails when Elastx catalog/token/money-manager are down

1 participant