Skip to content

Phase 1: cloud-mount cutover; Gin sealed as inner handler#33

Open
hanzo-dev wants to merge 1 commit into
mainfrom
feat/phase1-cloud-mount
Open

Phase 1: cloud-mount cutover; Gin sealed as inner handler#33
hanzo-dev wants to merge 1 commit into
mainfrom
feat/phase1-cloud-mount

Conversation

@hanzo-dev
Copy link
Copy Markdown
Member

Summary

Phase 1 of the staged Gin → zip migration. Same binary, two boot modes — the cutover is a flag, not a fork.

  • legacy (default): bootLegacy() — direct-Gin behind net/http. Production deployments today.
  • cloud (--cloud flag OR COMMERCE_MODE=cloud env): bootCloud()zip.App with the Gin engine adapted via zip.AdaptNetHTTP and mounted at /v1/commerce + /_/commerce per HIP-0106. Gin keeps all 403 handler files and its full middleware chain; from the outside it has no public route surface of its own.

Default stays legacy until cloud-mount is validated under production load. Cloud boot requires -tags cloud at compile time; the Dockerfile now passes that tag so the deployed ghcr.io/hanzoai/commerce image carries both paths.

mount.go gained the missing api.Route(/v1) call — the legacy cmd/commerce and cmd/commerced binaries already did this imperatively after Embed() returned (the route-setup hook fires before Embed returns, so the OnRouteSetup callback was a silent no-op). Cloud mount now does the same, so /v1/billing, /v1/checkout, /v1/subscription etc. resolve identically under both modes.

Test plan

  • go build ./... — exit 0 (legacy mode)
  • go build -tags cloud ./... — exit 0 (cloud mode)
  • go vet -tags cloud ./... — clean
  • TestMount_RegistersHealth — native /_/commerce/healthz returns 200 + {"service":"commerce"}
  • TestMount_GinSurfaceReachable/v1/commerce/tenant reaches a real Gin handler through the AdaptNetHTTP wrap (not NoRoute fallthrough)
  • TestMount_EquivalentToLegacy — both modes return identical status + body for /v1/commerce/health and /v1/commerce/tenant
  • TestRoutes_WiredInProduction + TestRoutes_PublicTenantReachesStore + TestRoutes_AdminTenantCreatePathExists — legacy route wiring unchanged
  • Live binary smoke (local):
    • legacy GET /v1/commerce/health → 404 {"error":"not found"}
    • cloud GET /v1/commerce/health → 404 {"error":"not found"} (byte-for-byte equal)
    • legacy GET /v1/commerce/tenant → 404 {"error":"unknown tenant"}
    • cloud GET /v1/commerce/tenant → 404 {"error":"unknown tenant"} (byte-for-byte equal)
    • cloud GET /_/commerce/healthz → 200 {"status":"ok","service":"commerce"} (native zip)
    • legacy GET /healthz → 200 {...full version JSON...} (legacy Gin root)
  • --cloud without -tags cloud build → clear stderr error + exit 1

Health endpoint asymmetry — intentional

Path Legacy Cloud Reason
/_/commerce/healthz not registered 200 (native zip) Cloud-scoped, survives Gin router outage
/healthz 200 (Gin root) not registered Cloud surfaces scope to /v1/commerce + /_/commerce so the same binary can host iam/base/kms/gateway side-by-side without root-path collisions
/v1/commerce/* Gin Gin via zip wrap Frontend contract preserved

K8s liveness probes will need to flip from /healthz to /_/commerce/healthz at the deploy that flips COMMERCE_MODE=cloud. Tracked in Phase 2 deploy plan.

Phase 2 plan (separate sprint) — Gin → native zip, package-by-package

Carve handler packages onto native zip routes in order of risk-ascending and dependency-graph leaves first. After each carve, retire that package's Gin route registration and prove via TestMount_EquivalentToLegacy-style golden-body diffs that no behavior changed.

Ordering (each row = one sprint slice):

  1. pkg/healthz + pkg/version — tiny, no auth, no state. Native zip endpoints (/v1/commerce/healthz, /v1/commerce/version). Effort: 0.5d. Risk: low.
  2. checkout/public/v1/commerce/tenant, /v1/commerce/deposits/*, /v1/commerce/webhooks/:provider. Already uses gin.WrapH over plain http.Handler per checkout/mount.go; the carve is mostly a route-table rewrite. Effort: 1d. Risk: low.
  3. checkout/admin (/_/commerce/providers, /_/commerce/methods, …) — needs the IAM admin-role guard adapted to zip middleware. Effort: 1.5d. Risk: med (auth gate).
  4. api/account — login/signup/session. Hot path. Effort: 2d. Risk: high (session cookies + IAM JWT validation).
  5. api/billing/v1/billing/*. Already permission-gated by permission.Admin. Effort: 2d. Risk: med.
  6. api/store — public storefront. SPA-like, no auth on most routes. Effort: 2d. Risk: low.
  7. api/checkout (payment charge/authorize/capture) — payment intent surface. Effort: 3d. Risk: high (PCI scope adjacent — must NEVER touch a PAN, only tokens).
  8. api/subscription — recurring billing. Effort: 2d. Risk: med.
  9. api/analytics — event ingestion + pixel/AST. Effort: 1d. Risk: low.
  10. api/admin — Next.js admin SPA mount + legacy /admin/* dispatch. Effort: 2d. Risk: med (admin SPA bundle wiring).
  11. hooks.TriggerRouteSetup — last extension hook fan-out point. Effort: 1d. Risk: low.

Total Phase 2: ~18 dev-days, ~3 sprints.

After Phase 2 lands: zero c *gin.Context references in handlers → Phase 3 (separate sprint, 0.5d): drop github.com/gin-gonic/gin from go.mod, drop bootLegacy, drop cloud_stub.go, drop --cloud flag, retire cmd/commerced. One binary, one boot path, one router.

Constraints honored

  • 403 Gin handler files: untouched
  • gin-gonic/gin dependency: still required
  • Route paths: unchanged (frontend contract preserved — PR feat(frontend): admin UI on @hanzogui/admin canonical shell #32 talks to /v1/commerce/* and that holds)
  • Same ghcr.io/hanzoai/commerce image (additive -tags cloud)
  • No major bump (Phase 1 ships as a v1.x patch tag)

Single binary, two boot modes:

- legacy (default):  bootLegacy → direct-Gin behind net/http
- cloud  (--cloud):  bootCloud → zip.App with gin adapted as
                     inner handler, mounted at /v1/commerce + /_/commerce
                     per HIP-0106 unified Hanzo Cloud binary

Default stays legacy until cloud-mount is validated in production.
Cloud boot requires -tags cloud at build time; without it the --cloud
flag prints a clear error and exits non-zero. Dockerfile now builds
with -tags cloud so the deployed image carries both paths.

mount.go gains the missing api.Route(/v1) call — the legacy
commerce/commerced binaries did this imperatively after Embed()
returned; mounting through cloud must do the same so /v1/billing,
/v1/checkout, /v1/subscription etc. all resolve under the cloud
listener.

Three new tests proving byte-for-byte parity:
- TestMount_RegistersHealth        — native zip /_/commerce/healthz
- TestMount_GinSurfaceReachable    — gin /v1/commerce/tenant reachable
                                     through the AdaptNetHTTP wrap
- TestMount_EquivalentToLegacy     — same status + body on both modes

Phase 2 carves handler packages onto native zip routes one at a time;
Phase 3 drops the gin import. Neither lands here — this is the cutover.
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.

2 participants