Compare commits

...
Author SHA1 Message Date
hanzo-dev 18749656a2 refactor: MountSpec is AppSpec — they are apps, mount is the verb
Renamed across 18 files. The composition root lists apps; "mount" is what you do
to one, not what one is.

Also carried in, because both were blocking a build:
  - PluginSpec takes every prefix an app owns, not one. o11y owns /v1/o11y AND
    /v1/sentry (the Sentry-protocol ingest), and a prefix left out is not an
    error — it is a silent 404 on that subtree, which for Sentry ingest means
    quietly dropping every error event in the fleet.
  - clients/websearch used zip.AdaptNetHTTPFunc, removed in zip v1.12.0. main
    does not compile without this. Now AdaptNetHTTP(http.HandlerFunc(f)) —
    http.HandlerFunc already IS an http.Handler, which is why the func-shaped
    adapter was redundant.

NOT DONE, deliberately: currying Mount on Deps. Measured first — it costs 102
app signature changes across 103 files, and the payoff does not arrive. The
point of currying was that a cloud app would become a zip.Service and the
adapters would vanish; but cloud's Mount takes Router, not *zip.App, and that
distinction is load-bearing — newScope bounds which prefixes an app's middleware
may touch, and apps.TestWireFrozen fails on any new Global grant (only 6 of ~108
apps hold one). Curried, PluginSpec STILL needs its type assertion. 102
signatures for an adapter that stays is not a trade worth making; unifying the
router types first would be, and that is a different change.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 16:04:21 -07:00
21 changed files with 68 additions and 56 deletions
+9 -9
View File
@@ -1,7 +1,7 @@
// Package apps is the composition root: the single, explicit list of which
// Hanzo cloud subsystems are linked into the binary AND the order they mount in.
//
// Wire() returns []cloud.MountSpec in mount order (slice position == order). There
// Wire() returns []cloud.AppSpec in mount order (slice position == order). There
// is no init()-registry and no order-int: adding, removing, or reordering a
// subsystem is a one-line edit to Wire(), read top-to-bottom. cmd/cloud and
// cmd/hanzo both call Wire() and thread the slice into cloud.Serve — the set is
@@ -196,13 +196,13 @@ func init() {
})
}
// Wire returns every linked subsystem as a cloud.MountSpec, in mount order. The
// Wire returns every linked subsystem as a cloud.AppSpec, in mount order. The
// slice position IS the order: cloud.MountAll iterates it as-given, registering each
// subsystem's teardown as a zip shutdown hook so teardown runs in reverse (LIFO).
// Enablement is a separate axis: cloud.Serve mounts only the specs cfg.Enabled(name)
// admits, so a STAGED subsystem is linked but inert until named.
func Wire() []cloud.MountSpec {
return []cloud.MountSpec{
func Wire() []cloud.AppSpec {
return []cloud.AppSpec{
// embedded NATS :4222 + JetStream.
{Name: "pubsub", Mount: pubsub.Mount, Shutdown: pubsub.Shutdown},
// embedded Kafka adaptor :9092.
@@ -254,11 +254,11 @@ func Wire() []cloud.MountSpec {
// otel-collector, prometheus and gonum are here and nowhere else — and it is
// imported by NOTHING but this line, so unlinking it is a pure subtraction.
//
// KNOWN GAP, see the branch report: o11y also owns /v1/sentry/* (mountSentry),
// which is a SECOND public prefix. zip.Load takes one, so /v1/sentry/* is not
// mounted on the host by this line and 404s until zip.Plugin can name more than
// one prefix. Do not merge this to main before that is closed.
cloud.PluginSpec("o11y", "/v1/o11y", o11yPlugin()),
// o11y owns TWO public prefixes — /v1/o11y and /v1/sentry/* (mountSentry,
// the Sentry-protocol ingest). Both are named here: a prefix left out
// would 404 silently rather than fail, which for Sentry ingest means
// quietly dropping every error event in the fleet.
cloud.PluginSpec("o11y", o11yPlugin(), "/v1/o11y", "/v1/sentry"),
{Name: "authz", Mount: cloud.Global(authz.Mount), Global: true},
// Embedded commerce plane /v1/commerce/*, /_/commerce/* — the hanzoai/commerce
// MODULE via the adapter in commerce.go (un-forked; the in-process
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/hanzoai/cloud/clients/principal"
)
// wire_seams.go wires cross-subsystem in-process seams that cannot be a MountSpec
// wire_seams.go wires cross-subsystem in-process seams that cannot be a AppSpec
// because they compose functions ACROSS packages that must not import each other.
//
// The coding orchestrator (clients/coding) needs git's CloneURL + VerifyRef, but
+1 -1
View File
@@ -22,7 +22,7 @@ var frozen = []struct {
name string
ownsHealth bool
hasShutdown bool
global bool // receives the bare *zip.App — see MountSpec.Global
global bool // receives the bare *zip.App — see AppSpec.Global
}{
{"pubsub", false, true, false}, // was order 5
{"kafka", false, true, false}, // was order 6
+4 -4
View File
@@ -963,7 +963,7 @@ func pickVaultClient(cfg *Config, log luxlog.Logger) VaultClient {
// signature, so Wire references each one directly and the compiler checks it.
//
// app is a Router, not the concrete *zip.App, and that is the whole safety
// property: middleware a subsystem installs lands on the subtrees its MountSpec
// property: middleware a subsystem installs lands on the subtrees its AppSpec
// declares, never over the binary. Routes register exactly as before — absolute
// paths, same precedence. See scope.go. A subsystem that genuinely gates
// everything says so with Global: true and gets the bare app.
@@ -975,11 +975,11 @@ type MountFunc func(app Router, deps Deps) error
// deadline so a slow teardown is cut off rather than hanging SIGTERM.
type ShutdownFunc func(ctx context.Context) error
// MountSpec describes one subsystem to mount. There is NO Order field: the slice
// AppSpec describes one subsystem to mount. There is NO Order field: the slice
// position in apps.Wire() IS the mount order — the composition root lists
// subsystems in the exact sequence they mount (and, reversed, tear down), so order
// is data read top-to-bottom in one file, not ints scattered across the tree.
type MountSpec struct {
type AppSpec struct {
Name string
Mount MountFunc
Shutdown ShutdownFunc // optional; nil means the subsystem has nothing to tear down.
@@ -1020,7 +1020,7 @@ type MountSpec struct {
// its dependents is torn down after them) with no subsystem torn down while a
// request still uses it. Only ENABLED specs mount, so only they register a hook;
// teardown needs no separate enablement gate.
func MountAll(app *zip.App, specs []MountSpec, cfg *Config, deps Deps) error {
func MountAll(app *zip.App, specs []AppSpec, cfg *Config, deps Deps) error {
logger := deps.Logger
for _, spec := range specs {
if !cfg.Enabled(spec.Name) {
+2 -2
View File
@@ -70,7 +70,7 @@ func TestMountAll_ShutdownHooksLIFOAfterDrain(t *testing.T) {
}
// Mount order a, b, c ⇒ LIFO teardown must be c, b, a.
specs := []cloud.MountSpec{
specs := []cloud.AppSpec{
{Name: "a", Mount: noopMount, Shutdown: record("a")},
{Name: "b", Mount: noopMount, Shutdown: record("b")},
{Name: "c", Mount: noopMount, Shutdown: record("c")},
@@ -183,7 +183,7 @@ func TestMountAll_ShutdownRegistration_EnablementAndNil(t *testing.T) {
}
}
specs := []cloud.MountSpec{
specs := []cloud.AppSpec{
{Name: "enabled", Mount: noopMount, Shutdown: record("enabled")},
{Name: "disabled", Mount: noopMount, Shutdown: record("disabled")},
{Name: "nilsd", Mount: noopMount}, // enabled, but no Shutdown
+1 -1
View File
@@ -39,7 +39,7 @@ import (
// drive the SAME single implementation. The subsystem is a stateless orchestrator over
// framework (which holds the state) + the AI/social edges — it opens no store of its own.
//
// Registration is a one-line cloud.MountSpec in apps.Wire() (after framework +
// Registration is a one-line cloud.AppSpec in apps.Wire() (after framework +
// knowledge, before the AI /v1/* catch-all); the module fixtures + lifecycle hooks are
// registered in doctypes.go's init(), process-global and mount-order-independent.
+1 -1
View File
@@ -63,7 +63,7 @@ import (
// Prefixes are the canonical absolute prefixes the IAM identity surface owns —
// the ONE list. It registers the real routes (safeMount), serves the fail-closed 503
// when IAM cannot boot, and is the MountSpec.Prefixes apps.Wire() hands MountAll, so
// when IAM cannot boot, and is the AppSpec.Prefixes apps.Wire() hands MountAll, so
// IAM's middleware can only ever land on identity's own subtrees. Everything outside
// them belongs to cloud, so the console catch-all keeps serving the SPA.
//
+3 -3
View File
@@ -1,7 +1,7 @@
package kms_test
// Integration tests for the embedded KMS subsystem, exercised through the REAL
// orchestrator path (BuildDeps → the init()-registered MountSpec → the zip/Fiber
// orchestrator path (BuildDeps → the init()-registered AppSpec → the zip/Fiber
// stack), mirroring cmd/cloud/main_test.go. Requests run in-process via
// app.Fiber().Test — no listener, no external KMS, no PostgreSQL.
//
@@ -49,8 +49,8 @@ func masterKeyB64(t *testing.T) string {
// mountSpecs is the kms subsystem's composition-root entry, built locally so these
// tests mount exactly kms (the same spec apps.Wire() carries) without linking
// the whole bundle. cfg.Enable still gates it, exactly as in production.
func mountSpecs() []cloud.MountSpec {
return []cloud.MountSpec{{Name: "kms", Mount: kms.Mount, OwnsHealth: true}}
func mountSpecs() []cloud.AppSpec {
return []cloud.AppSpec{{Name: "kms", Mount: kms.Mount, OwnsHealth: true}}
}
// newApp wires BuildDeps + the canonical middleware + MountAll for the kms
+1 -1
View File
@@ -36,7 +36,7 @@ func newDualApp(t *testing.T, mk string) *zip.App {
app.Use(middleware.Recover())
app.Use(middleware.RequestID())
app.Use(middleware.Logger(deps.Logger))
specs := []cloud.MountSpec{
specs := []cloud.AppSpec{
{Name: "kms", Mount: kms.Mount, OwnsHealth: true},
{Name: "admin", Mount: admin.Mount},
}
+2 -2
View File
@@ -1,7 +1,7 @@
package storage_test
// Integration tests for the /v1/s3 file-manager subsystem, driven through the
// REAL orchestrator path (BuildDeps → the init()-registered MountSpec → the
// REAL orchestrator path (BuildDeps → the init()-registered AppSpec → the
// zip/Fiber stack), exactly like clients/kms/kms_test.go. Requests run in-process
// via app.Fiber().Test — no listener, no live SeaweedFS.
//
@@ -67,7 +67,7 @@ func newApp(t *testing.T, creds bool) *zip.App {
deps := cloud.BuildDeps(cfg)
app := zip.New(zip.Config{Logger: deps.Logger})
app.Use(middleware.Recover())
specs := []cloud.MountSpec{
specs := []cloud.AppSpec{
{Name: "storage", Mount: storage.Mount, OwnsHealth: true},
{Name: "provisioning", Mount: provisioning.Mount},
}
+1 -1
View File
@@ -315,7 +315,7 @@ func Mount(app cloud.Router, deps cloud.Deps) error {
return searchKeyed(c)
})
scrape := zip.AdaptNetHTTPFunc(scrapeHandler)
scrape := zip.AdaptNetHTTP(http.HandlerFunc(scrapeHandler))
// Firecrawl builds {apiUrl}/{version}/scrape; pin firecrawlVersion:v1 so the
// client POSTs /v1/websearch/v1/scrape. Also accept the bare /scrape.
g.Post("/v1/scrape", scrape)
+1 -1
View File
@@ -75,7 +75,7 @@ func main() {
}
// wireNames parses apps.go and returns the {Name: "..."} string literals from
// Wire()'s returned []MountSpec composite literal — the app names, in mount
// Wire()'s returned []AppSpec composite literal — the app names, in mount
// order. It is a light parse (no type-check), so it stays fast and depends only
// on the literal shape, which TestWireOrderMatchesFrozen already pins.
func wireNames(path string) []string {
+5 -5
View File
@@ -12,7 +12,7 @@
// the fused cloud control plane.
//
// Design — one mechanism, not many. The subsystem set is the explicit list
// apps.Wire() returns — []cloud.MountSpec in mount order (kms first-tier,
// apps.Wire() returns — []cloud.AppSpec in mount order (kms first-tier,
// iam 50, commerce 100, …, ai last), no init()-registry. A subcommand is just a
// *selection* over that slice:
//
@@ -142,7 +142,7 @@ func main() {
// isServeTarget reports whether sub names something this binary serves in-process —
// the full fused surface (cloud), standalone IAM, the datastore doc target, or any
// registered subsystem. Everything else is delegated to the Rust CLI (passthrough).
func isServeTarget(sub string, specs []cloud.MountSpec) bool {
func isServeTarget(sub string, specs []cloud.AppSpec) bool {
if _, ok := nonRegistrySubcommands[sub]; ok {
return true
}
@@ -150,7 +150,7 @@ func isServeTarget(sub string, specs []cloud.MountSpec) bool {
}
// dispatch routes a subcommand to its serve entrypoint.
func dispatch(sub string, specs []cloud.MountSpec) error {
func dispatch(sub string, specs []cloud.AppSpec) error {
switch sub {
case "cloud":
// Full fused surface: --enable governs the set (empty = all).
@@ -193,7 +193,7 @@ func dispatch(sub string, specs []cloud.MountSpec) error {
}
// registryHas reports whether name is a registered subsystem.
func registryHas(specs []cloud.MountSpec, name string) bool {
func registryHas(specs []cloud.AppSpec, name string) bool {
for _, spec := range specs {
if spec.Name == name {
return true
@@ -204,7 +204,7 @@ func registryHas(specs []cloud.MountSpec, name string) bool {
// usage prints the subcommand list: the non-registry targets (cloud, iam,
// datastore) plus every subsystem in the composition root (Wire()), sorted.
func usage(w *os.File, specs []cloud.MountSpec) {
func usage(w *os.File, specs []cloud.AppSpec) {
fmt.Fprintf(w, "hanzo %s — the unified Hanzo Go binary\n\n", version)
fmt.Fprintf(w, "Usage:\n hanzo <command> [flags]\n\n")
+1 -1
View File
@@ -59,7 +59,7 @@ func newCloudApp(t *testing.T) (*zip.App, string, cloud.Deps) {
app.Use(middleware.Recover())
app.Use(middleware.RequestID())
app.Use(middleware.Logger(deps.Logger))
if err := cloud.MountAll(app, []cloud.MountSpec{{Name: "kms", Mount: kms.Mount, OwnsHealth: true}}, cfg, deps); err != nil {
if err := cloud.MountAll(app, []cloud.AppSpec{{Name: "kms", Mount: kms.Mount, OwnsHealth: true}}, cfg, deps); err != nil {
t.Fatalf("MountAll: %v", err)
}
return app, dir, deps
+3 -3
View File
@@ -1,6 +1,6 @@
module github.com/hanzoai/cloud
go 1.26.4
go 1.26.5
// Dependencies will be added as subsystems are mounted per HIP-0106.
@@ -43,7 +43,7 @@ require (
github.com/zap-proto/fiber/v3 v3.2.1
github.com/zap-proto/go v1.3.0
github.com/zap-proto/md v0.1.0
github.com/zap-proto/zip v1.10.0
github.com/zap-proto/zip v1.16.0
go.opentelemetry.io/collector/component v1.54.0
go.opentelemetry.io/collector/confmap v1.54.0
go.opentelemetry.io/collector/confmap/provider/envprovider v1.50.0
@@ -104,7 +104,7 @@ require (
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/vultr/govultr/v3 v3.30.0 // indirect
github.com/zap-proto/http v0.3.0 // indirect
github.com/zap-proto/http v0.3.1 // indirect
github.com/zap-proto/zap2pb v0.2.0 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
+4
View File
@@ -2103,12 +2103,16 @@ github.com/zap-proto/go v1.3.0 h1:S3rMoawwhH/BbSZ4G8zG05hJoQnMSMDPzIq75diCTqE=
github.com/zap-proto/go v1.3.0/go.mod h1:914SNGTH6Rv3Yu1MweWJBPEN8FZlo5C39QyhaB0C7Q0=
github.com/zap-proto/http v0.3.0 h1:l7DvlngiYqmzNY6fzyRYw2ZIAhF35FqwOe6mvAOqpMg=
github.com/zap-proto/http v0.3.0/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zap-proto/http v0.3.1 h1:A2rCPWYCX866eAsdiWuns0dvWnBmViZtGm4pwX7jwlY=
github.com/zap-proto/http v0.3.1/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zap-proto/md v0.1.0 h1:1R6w/i1FYAdGIIiOvNggKO0RjikzhWWRodQUOgzEEpc=
github.com/zap-proto/md v0.1.0/go.mod h1:pmMx2F4Dwj1H48PIuLzRZxB2R5qVvqeg8ZScKQIntyQ=
github.com/zap-proto/zap2pb v0.2.0 h1:sos6HnayhGMGLRO54px1InzimDzTZ2o5TSMEatYBjzs=
github.com/zap-proto/zap2pb v0.2.0/go.mod h1:wD97Z2VTPabDq/4AMNL++PWnQ0YwEtajiuNkLGg3/18=
github.com/zap-proto/zip v1.10.0 h1:0Swzr+SNr+4VeO8pUw+Umdkh3z/lnD0hJ4N1kObdP60=
github.com/zap-proto/zip v1.10.0/go.mod h1:9R3FOq2ItZa7G+9QilsB/punEpVSHtdXiQji0P84LSE=
github.com/zap-proto/zip v1.16.0 h1:Bb3StSa9xMHzWUOVDxeMUZFTrgRsBGyrm1tCJMvKRxw=
github.com/zap-proto/zip v1.16.0/go.mod h1:BxFNqjnAVhArMJ+s7VnXdwAtfOVy47GAi62wwqyu8go=
github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=
github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
+14 -6
View File
@@ -8,36 +8,44 @@ import (
"github.com/zap-proto/zip"
)
// PluginSpec returns a MountSpec that serves prefix from a SEPARATE binary
// PluginSpec returns a AppSpec that serves prefix from a SEPARATE binary
// instead of code linked into this one.
//
// It exists so that where a subsystem runs stops being a property of the source.
// zip.Load returns a zip.Service — the same type a linked-in service is — so the
// only difference between "compiled in" and "its own process" is which MountSpec
// only difference between "compiled in" and "its own process" is which AppSpec
// Wire() lists. Moving one out is a one-line edit at the composition root, and
// nothing downstream (routing, health, shutdown ordering) can tell the difference.
//
// Pass EVERY prefix the subsystem owns. o11y owns both /v1/o11y and /v1/sentry,
// and a prefix left out is not an error — it is a silent 404 on that subtree,
// which is the worst way for this to fail.
//
// The plugin names exactly one of Addr (already listening), Bin (the binary,
// normally go:embed'd) or Path. For Bin and Path, zip starts it as a child on a
// normally go:embed'd), Path, or URL+Sum (a release artifact, fetched and
// verified by digest). For Bin and Path, zip starts it as a child on a
// private unix socket and mounts the routes onto it; the child is stopped when
// Shutdown runs, so a plugin subsystem tears down with the rest.
//
// Global is set because zip.Load registers under the prefix it was given. Handing
// it a scoped Router would nest that prefix under the subsystem name and the
// routes would answer somewhere nobody is asking.
func PluginSpec(name, prefix string, p zip.Plugin) MountSpec {
func PluginSpec(name string, p zip.Plugin, prefixes ...string) AppSpec {
if p.Name == "" {
p.Name = name
}
return MountSpec{
return AppSpec{
Name: name,
Global: true,
// Deps are irrelevant to a plugin — it runs in its own process and
// receives nothing from this one — so this is zip.Load's Service with the
// router narrowed to the app it needs.
Mount: func(router Router, _ Deps) error {
app, ok := router.(*zip.App)
if !ok {
return fmt.Errorf("pluginspec %q: needs the root app, got %T — Global must stay set", name, router)
}
return zip.Load(prefix, p)(app)
return zip.Load(p, prefixes...)(app)
},
}
}
+4 -4
View File
@@ -10,10 +10,10 @@ import (
)
// A plugin subsystem must look like every other one at the composition root:
// same MountSpec type, so Wire() can swap in-process for out-of-process by
// same AppSpec type, so Wire() can swap in-process for out-of-process by
// editing one line.
func TestPluginSpec_IsAnOrdinaryMountSpec(t *testing.T) {
s := PluginSpec("search", "/v1/search", zip.Plugin{Addr: "127.0.0.1:1"})
func TestPluginSpec_IsAnOrdinaryAppSpec(t *testing.T) {
s := PluginSpec("search", zip.Plugin{Addr: "127.0.0.1:1"}, "/v1/search")
if s.Name != "search" {
t.Fatalf("name = %q, want search", s.Name)
}
@@ -28,7 +28,7 @@ func TestPluginSpec_IsAnOrdinaryMountSpec(t *testing.T) {
// Mounting onto a scoped Router is a wiring mistake, not something to paper
// over: the routes would answer under a doubled prefix. Fail loudly.
func TestPluginSpec_RefusesAScopedRouter(t *testing.T) {
s := PluginSpec("bad", "/v1/bad", zip.Plugin{Addr: "127.0.0.1:1"})
s := PluginSpec("bad", zip.Plugin{Addr: "127.0.0.1:1"}, "/v1/bad")
err := s.Mount(scopedStub{}, Deps{})
if err == nil {
t.Fatal("mounting on a non-root Router must fail")
+2 -2
View File
@@ -128,7 +128,7 @@ func (s *scope) err() error {
return nil
}
return fmt.Errorf(
"%s installed middleware at %s, outside the prefixes it owns (%s) — declare those prefixes in its MountSpec, or Global: true if it really gates the whole binary",
"%s installed middleware at %s, outside the prefixes it owns (%s) — declare those prefixes in its AppSpec, or Global: true if it really gates the whole binary",
s.name, strings.Join(*s.escaped, ", "), strings.Join(s.prefixes, ", "))
}
@@ -141,7 +141,7 @@ func Global(fn func(*zip.App, Deps) error) MountFunc {
return func(r Router, deps Deps) error {
app, ok := r.(*zip.App)
if !ok {
return fmt.Errorf("this subsystem takes the bare *zip.App; its MountSpec needs Global: true")
return fmt.Errorf("this subsystem takes the bare *zip.App; its AppSpec needs Global: true")
}
return fn(app, deps)
}
+7 -7
View File
@@ -36,7 +36,7 @@ func get(t *testing.T, app *zip.App, path string) int {
return resp.StatusCode
}
func mountAll(t *testing.T, app *zip.App, specs []cloud.MountSpec) error {
func mountAll(t *testing.T, app *zip.App, specs []cloud.AppSpec) error {
t.Helper()
enable := make([]string, 0, len(specs))
for _, s := range specs {
@@ -59,7 +59,7 @@ func newApp() *zip.App {
// the /v1/<name> convention every subsystem already follows.
func TestScopeConfinesUseToTheSubsystem(t *testing.T) {
app := newApp()
err := mountAll(t, app, []cloud.MountSpec{
err := mountAll(t, app, []cloud.AppSpec{
{Name: "guard", Mount: func(r cloud.Router, _ cloud.Deps) error {
r.Use(deny)
r.Get("/v1/guard/whoami", pong)
@@ -87,7 +87,7 @@ func TestScopeConfinesUseToTheSubsystem(t *testing.T) {
// it owns two subtrees and neither of them is /v1/iam alone.
func TestScopeHonoursDeclaredPrefixes(t *testing.T) {
app := newApp()
err := mountAll(t, app, []cloud.MountSpec{
err := mountAll(t, app, []cloud.AppSpec{
{Name: "identity", Prefixes: []string{"/v1/identity", "/login/oauth"},
Mount: func(r cloud.Router, _ cloud.Deps) error {
r.Use(deny)
@@ -120,7 +120,7 @@ func TestScopeHonoursDeclaredPrefixes(t *testing.T) {
// even in the failed attempt.
func TestScopeRefusesMiddlewareOutsideItsPrefixes(t *testing.T) {
app := newApp()
err := mountAll(t, app, []cloud.MountSpec{
err := mountAll(t, app, []cloud.AppSpec{
{Name: "neighbour", Mount: func(r cloud.Router, _ cloud.Deps) error {
r.Get("/v1/neighbour/ping", pong)
return nil
@@ -142,7 +142,7 @@ func TestScopeRefusesMiddlewareOutsideItsPrefixes(t *testing.T) {
// rate-limiting a leaf of its OWN subtree is the normal case and must pass.
func TestScopeAllowsGroupInsideItsPrefixes(t *testing.T) {
app := newApp()
err := mountAll(t, app, []cloud.MountSpec{
err := mountAll(t, app, []cloud.AppSpec{
{Name: "vault", Mount: func(r cloud.Router, _ cloud.Deps) error {
r.Group("/v1/vault/auth", deny)
r.Get("/v1/vault/auth/login", pong)
@@ -166,7 +166,7 @@ func TestScopeAllowsGroupInsideItsPrefixes(t *testing.T) {
// it has always meant. That is the capability, and it is spelled out in Wire().
func TestGlobalIsTheOnlyAppWideDoor(t *testing.T) {
app := newApp()
err := mountAll(t, app, []cloud.MountSpec{
err := mountAll(t, app, []cloud.AppSpec{
{Name: "edge", Global: true, Mount: cloud.Global(func(a *zip.App, _ cloud.Deps) error {
a.Use(deny)
return nil
@@ -189,7 +189,7 @@ func TestGlobalIsTheOnlyAppWideDoor(t *testing.T) {
// fails the mount instead of silently receiving a scope it cannot use.
func TestGlobalMountNeedsTheGlobalFlag(t *testing.T) {
app := newApp()
err := mountAll(t, app, []cloud.MountSpec{
err := mountAll(t, app, []cloud.AppSpec{
{Name: "edge", Mount: cloud.Global(func(*zip.App, cloud.Deps) error { return nil })},
})
if err == nil {
+1 -1
View File
@@ -39,7 +39,7 @@ import (
// every enabled subsystem) before MountAll, runs the canonical middleware
// pipeline (Recover → RequestID → Logger), and shuts down gracefully on
// SIGINT/SIGTERM.
func Serve(specs []MountSpec, enable []string) error {
func Serve(specs []AppSpec, enable []string) error {
cfg := LoadConfig()
if enable != nil {
cfg.Enable = enable