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]>
138 lines
5.0 KiB
Go
138 lines
5.0 KiB
Go
// Copyright 2023 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.
|
|
|
|
// Command visor is the Hanzo Visor cloud compute/VM/GPU management service:
|
|
// zip-native on the hanzoai/orm store. It serves two addresses — the HTTP edge
|
|
// the ingress reaches, and the canonical ZAP socket the rest of the fleet dials
|
|
// it by name on.
|
|
//
|
|
// Run it with no arguments to serve (the container contract); `visor version`
|
|
// prints the build version.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/hanzoai/ha"
|
|
"github.com/spf13/cobra"
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/visor/conf"
|
|
"github.com/hanzoai/visor/object"
|
|
"github.com/hanzoai/visor/pkg/visor"
|
|
)
|
|
|
|
// version is set at build time via -ldflags "-X main.version=vX.Y.Z".
|
|
var version = "(dev)"
|
|
|
|
func main() {
|
|
// One context rooted at the process signals, so SIGINT/SIGTERM drives a
|
|
// graceful shutdown.
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
var zapAddr, httpAddr string
|
|
var createDatabase bool
|
|
|
|
// The root command IS the server — the container runs a bare `/visor`
|
|
// (optionally `--createDatabase=true`), so serve is the default action.
|
|
root := &cobra.Command{
|
|
Use: "visor",
|
|
Short: "Hanzo Visor — cloud compute/VM/GPU management service (zip-native)",
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
return serve(cmd.Context(), zapAddr, httpAddr)
|
|
},
|
|
}
|
|
f := root.Flags()
|
|
f.StringVar(&zapAddr, "zap", zip.SocketPath(visor.Name), "ZAP listen address (empty = HTTP edge only)")
|
|
f.StringVar(&httpAddr, "http", defaultHTTPAddr(), "HTTP edge listen address")
|
|
// Accepted for container-command compatibility; the store creates its schema
|
|
// on open (object.InitAdapter) regardless, so this is informational.
|
|
f.BoolVar(&createDatabase, "createDatabase", false, "create the database schema on boot (always applied)")
|
|
|
|
root.AddCommand(&cobra.Command{
|
|
Use: "version",
|
|
Short: "Print the visor build version",
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "visor %s\n", version)
|
|
},
|
|
})
|
|
|
|
if err := root.ExecuteContext(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "visor: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// serve boots visor and binds the listener(s). visor.Bootstrap is the single
|
|
// in-process boot path — DB, authz, parsers, filters, routes and background
|
|
// tickers — shared verbatim with the embedded cloud mount, so standalone and
|
|
// fused never drift. Here main() owns the listener.
|
|
//
|
|
// TWO addresses, and the ZAP one is not optional in practice. The HTTP edge is
|
|
// what a browser and the ingress reach; the socket is how the rest of the fleet
|
|
// reaches visor BY NAME, and a peer that binds no socket does not exist to a
|
|
// caller — zip resolves a peer through zip.SocketPath, so the failure is
|
|
// ErrNoPeer rather than a connection refused anyone would recognise. Defaulting
|
|
// the socket ON is what makes `visor` a name a sibling service can dial; an
|
|
// operator who genuinely wants an edge-only process passes --zap="".
|
|
func serve(ctx context.Context, zapAddr, httpAddr string) error {
|
|
// The standalone Deployment runs replicas: 1 (universe
|
|
// charts/app/values/hanzo/visor.yaml), so this process is the only writer and
|
|
// correctly elects itself. Declared HERE and not in visor.Bootstrap on
|
|
// purpose: Bootstrap is shared verbatim with the embedded cloud mount, whose
|
|
// replica count is cloud's, not ours. Registering it there would hand a
|
|
// multi-replica cloud the claim "I am alone" and double-debit every hour.
|
|
//
|
|
// This is the one topology assertion in the binary, and it is only true while
|
|
// that values file says 1. Raising replicas REQUIRES replacing this with a
|
|
// real membership source first — see object/coordinator.go.
|
|
object.RegisterMembership(ha.Static(object.SelfID()))
|
|
|
|
app := visor.Bootstrap()
|
|
|
|
// Translate ctx cancellation (SIGINT/SIGTERM) into a graceful shutdown.
|
|
go func() {
|
|
<-ctx.Done()
|
|
_ = app.Shutdown()
|
|
}()
|
|
|
|
addrs := make([]string, 0, 2)
|
|
if zapAddr != "" {
|
|
addrs = append(addrs, zapAddr)
|
|
}
|
|
addrs = append(addrs, httpAddr)
|
|
|
|
if err := app.Listen(addrs...); err != nil {
|
|
return fmt.Errorf("serve: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// defaultHTTPAddr honours the historical httpport (app.conf / env), defaulting
|
|
// to :19000 — the port the container EXPOSEs and the compose maps.
|
|
func defaultHTTPAddr() string {
|
|
port := conf.GetConfigString("httpport")
|
|
if port == "" {
|
|
port = "19000"
|
|
}
|
|
return "http://:" + port
|
|
}
|