mirror of
https://github.com/hanzoai/compute.git
synced 2026-11-02 22:00:43 +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]>
57 lines
2.2 KiB
Go
57 lines
2.2 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 routers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/visor/object"
|
|
)
|
|
|
|
// Ping is what a probe sends: nothing. A health check asks one question and
|
|
// carries no arguments to ask it with, and saying so as a type is what lets the
|
|
// op be declared like every other — one shape, no special case.
|
|
type Ping struct{}
|
|
|
|
// Health is visor's answer to a probe.
|
|
//
|
|
// Status is "ok" or the response is not a 200 at all, so a reader never has to
|
|
// parse the body to learn the verdict — which matters because the reader is
|
|
// kubelet, and kubelet reads the status code and nothing else. Backend names the
|
|
// store that was reached, so a human looking at a curl can tell a Base pod from
|
|
// a Postgres one without consulting the deployment.
|
|
type Health struct {
|
|
Status string `json:"status"`
|
|
Backend string `json:"backend"`
|
|
}
|
|
|
|
// health answers whether visor can do its job, which reduces to whether it can
|
|
// reach its store: every route on this service reads or writes one.
|
|
//
|
|
// The failure is a 503 and not a 200-with-a-sad-field. A readiness probe that
|
|
// always answers 200 cannot take a broken pod out of the Service, and that is
|
|
// precisely the bug this op was written to end — visor's probes used to request
|
|
// /api/health, which is not a route, so TransparentStatic served the SPA's
|
|
// index.html and every probe passed with the database face down.
|
|
func health(_ context.Context, _ *Ping) (*Health, error) {
|
|
if err := object.Ready(); err != nil {
|
|
return nil, zip.Errorf(http.StatusServiceUnavailable, "visor: store unreachable: %v", err)
|
|
}
|
|
return &Health{Status: "ok", Backend: string(object.ConfiguredBackend())}, nil
|
|
}
|