Skip to content

refactor(gateway): dynamic gRPC-reflection routing, Postgres migration, deployment modes, and critical bug fixes - #29

Merged
miladhzzzz merged 29 commits into
mainfrom
refactor/api-gateway-cleanup
Jul 24, 2026
Merged

refactor(gateway): dynamic gRPC-reflection routing, Postgres migration, deployment modes, and critical bug fixes#29
miladhzzzz merged 29 commits into
mainfrom
refactor/api-gateway-cleanup

Conversation

@miladhzzzz

Copy link
Copy Markdown
Member

Summary

Large refactor of persys-gateway's routing, auth, and persistence layers,
plus fixes for a stale-naming problem, a hardcoded secret, and a couple
of bugs the refactor itself introduced along the way (documented below,
not swept under the rug).

Dynamic, reflection-based routing

  • New internal/grpcbridge: discovers RPC methods on AgentControl
    (scheduler) and ForgeryControl (CI/CD) via gRPC server reflection,
    and dispatches generically via dynamicpb/protojson — no more
    hand-written controller + route + service-wrapper triplet per RPC.
  • Backward-compatible by design: falls back to the compiled-in proto
    descriptor (controlv1.File_control_proto / forgeryv1.File_forgery_proto)
    if a backend doesn't implement reflection yet. Works against existing
    scheduler/forgery deployments unmodified; upgrades to live discovery
    automatically once they add reflection.Register.
  • New discovery endpoints: GET /clusters/:cluster_id/rpc/_meta and
    .../forgery/rpc/_meta list every callable method, aliased or not.
  • Single source of truth for the whole dynamic surface:
    internal/router/bindings.go.
  • Routes are mounted twice — once cluster-scoped
    (/clusters/:cluster_id/workloads/...) and once flat
    (/workloads/..., /nodes/..., /cluster/metrics,
    /forgery/...) — so existing persysctl installs keep working
    against the gateway's default cluster with zero changes required.

Renames (dead "Prow" naming removed)

ProwServiceClusterControlService, ProwController
ClusterMetaController (trimmed to the 4 handlers that were never
RPC-shaped), ProwConfigLegacySchedulerConfig. "Prow" never
corresponded to any real Persys service. Forgery was also split out of
ClusterControlService into its own ForgeryService — it's a single
fixed address with no pool/failover, and sharing the scheduler pool's
shape was never an honest description of what it is.

Security fixes

  • Hardcoded JWT secret ("unicornsAreAwesome", committed in 3 places)
    app.jwt_secret, env-sourced (PERSYS_GATEWAY_JWT_SECRET),
    auto-generated with a startup warning in self-hosted mode, required
    in managed mode (fails fast at startup otherwise).
  • github.routes.go: authController was never actually injected —
    Auth() ran on a zero-value struct and only appeared to work via an
    accidental dependency on package-level globals set by a different
    controller. Fixed.
  • OAuth CSRF state was a package-level Go variable, overwritten on every
    login attempt (race under concurrent logins). Now a real row per
    attempt, consumed exactly once atomically.
  • auth.impl.service.go: fixed a bug where SignInUser ran an
    unconditional insert regardless of whether the user already existed,
    relying on a unique index that was never actually created — silently
    producing duplicate user rows on every login.
  • A live MongoDB Atlas username/password was hardcoded in
    tests/auth_test.go. Removed; test now requires
    PERSYS_TEST_POSTGRES_DSN and skips otherwise. Rotate that
    credential regardless of this PR — it was public.

Deployment modes

New deployment.mode: self-hosted (default) or managed.

  • Self-hosted: no GitHub OAuth app needed, /auth/* and /github/*
    aren't mounted, mTLS is the only trust boundary, no database
    required
    .
  • Managed: OAuth mounts, JWT auth enforced on customer-facing routes,
    database required (fails fast if unset).
  • GET /health reports both deployment_mode and database_enabled.

Database: MongoDB → Postgres, and made optional

  • New internal/store (pgx), three tables (users, oauth_sessions,
    webhook_events) replacing Mongo entirely. Schema applies as
    idempotent CREATE TABLE IF NOT EXISTS on startup, no separate
    migration step.
  • Dropped two Mongo collections that were created but never read or
    written anywhere (repos, cluster_state), plus two dead
    *mongo.Collection constructor params that were never used.
  • database.dsn is optional in self-hosted mode — the only things
    that touch it are gated to managed mode already, or (webhook audit
    persistence) already degrade to in-memory-only behavior on a nil store.

Other fixes found and fixed along the way

  • A real bug in grpcbridge itself: generic dispatch only decoded the
    JSON body, never URL path params — so GET /workloads/:id etc. sent
    an empty ID to the backend. MethodAlias gained PathParams to
    map path segments onto proto fields; verified against the actual
    generated .pb.go field names rather than assumed.
  • TaintNodeRequest.Taint is a nested message, unlike its siblings —
    fixed the request shape accordingly.

Config / docs

config.yaml, README.md updated to match all of the above
(legacy_scheduler: renamed section, deployment:/database: blocks,
dynamic-routing docs). New PERSYS_GATEWAY_DEPLOYMENT_MODE env override
added (didn't previously exist — needed for container/compose
deployments).

Breaking changes

  • Config keys: database.mongo_uri/database.namedatabase.dsn;
    prow:legacy_scheduler: (with renamed fields).
  • Requires a persysctl client update (companion PR/patch) for
    TaintNode — its request body shape was wrong before this fix in a
    way the old hand-written controller had masked.

Follow-ups (not in this PR)

  • go.mod/go.sum: run go mod tidy once after merging — added
    pgx/v5, removed mongo-driver.
  • cluster_owners (multi-tenant ownership) designed but not wired in;
    needs its own migration once managed-mode ownership checks land.
  • Consider an integration test that exercises GET /workloads/:id
    end-to-end — the path-param bug above would have been caught by one.

…C-shaped and

 so have nothing for grpcbridge to discover via reflection: health,
 list-clusters, get-cluster. Everything else that used to live on
 ProwController (workload/node CRUD, cluster metrics, forgery
passthrough) is now served dynamically — see
 internal/router/bindings.go — since it's a straight AgentControl or
 ForgeryControl RPC with no logic of its own beyond what the dynamic
bridge already does.
…e into a set

of gin routes with zero generated-stub boilerplate: no per-RPC wrapper
method, no per-RPC controller handler, no per-RPC route line.
…c RPC surface is declared. Adding a method for persysctl or the SDK to call

is very likely a one-line addition here — a MethodAlias, or nothing at
all if the generic /rpc/<Service>/<Method> path is fine.
@miladhzzzz
miladhzzzz requested a review from parmisk80 July 24, 2026 09:12
@miladhzzzz miladhzzzz self-assigned this Jul 24, 2026
@miladhzzzz miladhzzzz added bug Something isn't working enhancement New feature or request labels Jul 24, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@miladhzzzz
miladhzzzz merged commit 037483e into main Jul 24, 2026
4 checks passed
@miladhzzzz
miladhzzzz deleted the refactor/api-gateway-cleanup branch July 24, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request Security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant