Phase 1: cloud-mount cutover; Gin sealed as inner handler#33
Open
hanzo-dev wants to merge 1 commit into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 behindnet/http. Production deployments today.cloud(--cloudflag ORCOMMERCE_MODE=cloudenv):bootCloud()—zip.Appwith the Gin engine adapted viazip.AdaptNetHTTPand mounted at/v1/commerce+/_/commerceper 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 cloudat compile time; the Dockerfile now passes that tag so the deployedghcr.io/hanzoai/commerceimage carries both paths.mount.gogained the missingapi.Route(/v1)call — the legacycmd/commerceandcmd/commercedbinaries already did this imperatively afterEmbed()returned (the route-setup hook fires beforeEmbedreturns, so the OnRouteSetup callback was a silent no-op). Cloud mount now does the same, so/v1/billing,/v1/checkout,/v1/subscriptionetc. 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 ./...— cleanTestMount_RegistersHealth— native/_/commerce/healthzreturns 200 +{"service":"commerce"}TestMount_GinSurfaceReachable—/v1/commerce/tenantreaches a real Gin handler through the AdaptNetHTTP wrap (not NoRoute fallthrough)TestMount_EquivalentToLegacy— both modes return identical status + body for/v1/commerce/healthand/v1/commerce/tenantTestRoutes_WiredInProduction+TestRoutes_PublicTenantReachesStore+TestRoutes_AdminTenantCreatePathExists— legacy route wiring unchangedGET /v1/commerce/health→ 404{"error":"not found"}GET /v1/commerce/health→ 404{"error":"not found"}(byte-for-byte equal)GET /v1/commerce/tenant→ 404{"error":"unknown tenant"}GET /v1/commerce/tenant→ 404{"error":"unknown tenant"}(byte-for-byte equal)GET /_/commerce/healthz→ 200{"status":"ok","service":"commerce"}(native zip)GET /healthz→ 200{...full version JSON...}(legacy Gin root)--cloudwithout-tags cloudbuild → clear stderr error + exit 1Health endpoint asymmetry — intentional
/_/commerce/healthz/healthz/v1/commerce+/_/commerceso the same binary can host iam/base/kms/gateway side-by-side without root-path collisions/v1/commerce/*K8s liveness probes will need to flip from
/healthzto/_/commerce/healthzat the deploy that flipsCOMMERCE_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):
pkg/healthz+pkg/version— tiny, no auth, no state. Native zip endpoints (/v1/commerce/healthz,/v1/commerce/version). Effort: 0.5d. Risk: low.checkout/public—/v1/commerce/tenant,/v1/commerce/deposits/*,/v1/commerce/webhooks/:provider. Already usesgin.WrapHover plainhttp.Handlerpercheckout/mount.go; the carve is mostly a route-table rewrite. Effort: 1d. Risk: low.checkout/admin(/_/commerce/providers,/_/commerce/methods, …) — needs the IAM admin-role guard adapted to zip middleware. Effort: 1.5d. Risk: med (auth gate).api/account— login/signup/session. Hot path. Effort: 2d. Risk: high (session cookies + IAM JWT validation).api/billing—/v1/billing/*. Already permission-gated bypermission.Admin. Effort: 2d. Risk: med.api/store— public storefront. SPA-like, no auth on most routes. Effort: 2d. Risk: low.api/checkout(payment charge/authorize/capture) — payment intent surface. Effort: 3d. Risk: high (PCI scope adjacent — must NEVER touch a PAN, only tokens).api/subscription— recurring billing. Effort: 2d. Risk: med.api/analytics— event ingestion + pixel/AST. Effort: 1d. Risk: low.api/admin— Next.js admin SPA mount + legacy/admin/*dispatch. Effort: 2d. Risk: med (admin SPA bundle wiring).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.Contextreferences in handlers → Phase 3 (separate sprint, 0.5d): dropgithub.com/gin-gonic/ginfromgo.mod, dropbootLegacy, dropcloud_stub.go, drop--cloudflag, retirecmd/commerced. One binary, one boot path, one router.Constraints honored
gin-gonic/gindependency: still required/v1/commerce/*and that holds)ghcr.io/hanzoai/commerceimage (additive-tags cloud)