mirror of
https://github.com/hanzoai/compute.git
synced 2026-10-30 14:50:41 +00:00
Two things visor could not do. It bound no socket, so a sibling reaching it
through zip.SocketPath got ErrNoPeer and cloud kept talking HTTP to
visor.hanzo.svc:19000 — there was nothing on the plane to convert to. And it
declared no typed op, so it was in no registry: no OpenAPI schema, no MCP tool,
no CLI verb, nothing zip.Here could call.
--zap now defaults to zip.SocketPath(visor.Name), so the canonical socket comes
up beside the HTTP edge and zip.Serving("visor") resolves. Name is stated once,
in pkg/visor, and is both the app name and the socket's — a second spelling of
a name is a peer nobody can find. --zap="" opts out.
/v1/health is the first typed op, and it fixes a probe that could not fail. The
liveness and readiness probes requested /api/health, which is not a route:
outside /v1/ it fell to TransparentStatic, which serves index.html as a 200. So
both probes measured the static file server, and a pod with its database face
down stayed in the Service. /v1/health answers 503 when object.Ready() cannot
reach the store — Ready lives in object because `store` is unexported there and
anywhere else would be a second copy of the backend-selection rule. It pings the
shared engine, not every org's: a check whose cost grows with the customer list
is an outage waiting for a big enough customer list.
Health registers AHEAD of the filter chain, and the position is the design.
ApiFilter would judge a credential-less probe as "anonymous" and authz.IsAllowed
panics on a nil Enforcer — so an authz mistake would read to kubelet as every
pod being sick. RecordMessage is the other half: an audit row per request, twice
every ten seconds, describing nothing. The routers test proves the bypass with a
control case, because an assertion about a bypass is only meaningful against a
route that is still gated.
The contract test is retargeted rather than retired. It was written as the
migration's acceptance criterion; the property it actually pins outlives the
migration — what visor serves equals what visor says it serves — so it now reads
that way and covers both registration functions. 73 routes, GET 32.
Co-authored-by: Hanzo Dev <[email protected]>
92 lines
3.3 KiB
Go
92 lines
3.3 KiB
Go
// Copyright 2026 Hanzo Industries Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package visor exposes visor's in-process bootstrap so a parent binary
|
|
// (github.com/hanzoai/cloud) can mount visor's /v1 surface in the same address
|
|
// space — the same routers, controllers and filters the standalone server runs,
|
|
// minus the listener.
|
|
//
|
|
// There is ONE boot path. Both cmd main() and the embedded cloud mount call
|
|
// Bootstrap; the only difference is who owns the listener (main.go calls
|
|
// app.Listen; the cloud mount serves Handler() behind its own listener).
|
|
package visor
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/zap-proto/fiber/v3/middleware/adaptor"
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/visor/authz"
|
|
"github.com/hanzoai/visor/object"
|
|
"github.com/hanzoai/visor/routers"
|
|
"github.com/hanzoai/visor/task"
|
|
"github.com/hanzoai/visor/util"
|
|
)
|
|
|
|
// Name is what visor is called on the fleet, stated once. It is the app name zip
|
|
// serves under AND the name its canonical socket is derived from
|
|
// (zip.SocketPath(Name)), because those are the same fact: a caller reaches a
|
|
// peer BY NAME, so a second spelling of the name is a peer that cannot be found.
|
|
const Name = "visor"
|
|
|
|
// initState runs visor's stateful process initialization — DB adapter, authz,
|
|
// IP/UA parsers and background tickers — the half of boot that is independent of
|
|
// the HTTP framework. Shared by the standalone server and the embedded mount so
|
|
// the two can never drift.
|
|
func initState() {
|
|
object.InitAdapter()
|
|
authz.InitAuthz()
|
|
util.InitIpDb()
|
|
util.InitParser()
|
|
|
|
task.NewTicker().SetupTicker()
|
|
}
|
|
|
|
// Bootstrap runs visor's full in-process initialization and returns the wired
|
|
// zip App — the ONE boot path. The caller owns the listener: cmd main() calls
|
|
// app.Listen; the embedded cloud mount serves Handler().
|
|
func Bootstrap() *zip.App {
|
|
initState()
|
|
|
|
// MCP is derived from the TYPED ops, so it is worth having now that visor
|
|
// declares one. Its path is routers' to state, because routers is what has to
|
|
// let it past the static filter.
|
|
app := zip.New(zip.Config{
|
|
AppName: Name,
|
|
DisableStartupMessage: true,
|
|
MCP: zip.MCPConfig{Path: routers.MCPPath},
|
|
})
|
|
routers.Route(app)
|
|
return app
|
|
}
|
|
|
|
// Handler boots visor in-process and returns its HTTP handler for a parent
|
|
// binary to mount behind its own listener. Any panic from the underlying boot
|
|
// is recovered and returned as an error so a parent's mount fails cleanly
|
|
// (fail-closed) rather than crashing the whole process.
|
|
func Handler() (h http.Handler, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
h, err = nil, fmt.Errorf("visor.Handler: bootstrap panicked: %v", r)
|
|
}
|
|
}()
|
|
app := Bootstrap()
|
|
if err := app.Build(); err != nil {
|
|
return nil, fmt.Errorf("visor.Handler: %w", err)
|
|
}
|
|
return adaptor.FiberApp(app.Fiber()), nil
|
|
}
|