serve: --ops, so a standalone iam is probeable again
image / test (push) Successful in 4m52s
image / build (push) Successful in 1m8s

The graft moved /healthz, /readyz and /metrics off the public listener onto
zip's ops listener, which is right: a host owns liveness for what it composes,
and a child registering /healthz silently takes over the shared binary's. But
nothing brought that listener up. Standalone, iam answered 8000 with no /healthz
on it, both probes took 404, the pod never went Ready, its Service kept zero
endpoints, and every caller resolving identity through iam.hanzo.svc got
connection refused.

The ops listener is now stated the way the other two already were — an address,
in the same grammar, on the same line:

  iam serve --zap :9653 --http http://:8000 --ops http://:9090

Default on, because this binary's deployment is standalone. --ops "" is the
grafted case, where the host owns the ops port (HIP-0106 §1.3(f)).

Needs zip v1.18.23: OPS_PORT built a bare address, a bare address is ZAP, and a
kubelet cannot probe a ZAP socket.

Verified: --ops http://:9090 gives three listeners, ops on http, /healthz 200
"ok" /readyz 200 "ready" /metrics 200, and 8000 404s all three. --ops ""
gives two listeners and nothing on 9090.

gofmt also reordered a pre-existing import; main.go was unformatted at HEAD.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
2026-08-01 21:21:56 -07:00
parent 4444a09c8b
commit ecaad4514f
3 changed files with 14 additions and 6 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ retract [v1.0.0, v1.31.37] // Casdoor lineage; moved to github.com/hanzoai/iam-v
require (
github.com/hanzoai/orm v0.6.16
github.com/spf13/cobra v1.10.2
github.com/zap-proto/zip v1.18.22
github.com/zap-proto/zip v1.18.23
golang.org/x/crypto v0.53.0
)
+2
View File
@@ -211,6 +211,8 @@ github.com/zap-proto/zip v1.18.21 h1:h90f1DReRU2yAigoAPjSM6BFX0gRbata2TDVNmaeKbM
github.com/zap-proto/zip v1.18.21/go.mod h1:EKMmUX9wCPvpkhpMBQRqa17YVXNXRYEjj7X+65Y+J9E=
github.com/zap-proto/zip v1.18.22 h1:lTQeI1+uIiZpCCWkV2N3ti6mPTfO1CI2oTj2b4DFLtk=
github.com/zap-proto/zip v1.18.22/go.mod h1:EKMmUX9wCPvpkhpMBQRqa17YVXNXRYEjj7X+65Y+J9E=
github.com/zap-proto/zip v1.18.23 h1:bcJbDYxXa78OGVwF/+YWKd77rXfOil6txVwrGsW/2ko=
github.com/zap-proto/zip v1.18.23/go.mod h1:EKMmUX9wCPvpkhpMBQRqa17YVXNXRYEjj7X+65Y+J9E=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+11 -5
View File
@@ -37,8 +37,8 @@ import (
"github.com/hanzoai/iam/internal/oidc"
"github.com/hanzoai/iam/internal/provision"
"github.com/hanzoai/iam/internal/routes"
_ "github.com/hanzoai/iam/pkg/schema" // registers the v2 entity kinds
"github.com/hanzoai/iam/internal/seed"
_ "github.com/hanzoai/iam/pkg/schema" // registers the v2 entity kinds
"github.com/hanzoai/iam/pkg/store"
)
@@ -66,12 +66,12 @@ func main() {
}
func serveCmd() *cobra.Command {
var storeBackend, dbPath, zapAddr, httpAddr, initData string
var storeBackend, dbPath, zapAddr, httpAddr, opsAddr, initData string
cmd := &cobra.Command{
Use: "serve",
Short: "Open the entity store and serve the IAM API",
RunE: func(cmd *cobra.Command, _ []string) error {
return serve(cmd.Context(), storeBackend, dbPath, zapAddr, httpAddr, initData)
return serve(cmd.Context(), storeBackend, dbPath, zapAddr, httpAddr, opsAddr, initData)
},
}
f := cmd.Flags()
@@ -79,11 +79,17 @@ func serveCmd() *cobra.Command {
f.StringVar(&dbPath, "db", "data/iam.db", "SQLite database path (store=sqlite)")
f.StringVar(&zapAddr, "zap", ":9653", "ZAP primary listen address")
f.StringVar(&httpAddr, "http", "http://:8080", "HTTP edge listen address")
// The third listener, said the same way as the other two. /healthz, /readyz
// and /metrics live here and never on the public port, so a probe does not
// queue behind public traffic (HIP-0119 §1). Empty means this process owns no
// ops listener — right for a grafted iam, where the HOST owns liveness
// (HIP-0106 §1.3(f)); wrong for a standalone one, which is why it defaults on.
f.StringVar(&opsAddr, "ops", zip.DefaultOpsAddr, "ops listen address for /healthz, /readyz and /metrics (empty: none)")
f.StringVar(&initData, "init-data", "", "path to init_data.json to seed on boot (new-only; ${VAR} from env)")
return cmd
}
func serve(ctx context.Context, storeBackend, dbPath, zapAddr, httpAddr, initData string) error {
func serve(ctx context.Context, storeBackend, dbPath, zapAddr, httpAddr, opsAddr, initData string) error {
db, err := openStore(storeBackend, dbPath)
if err != nil {
return err
@@ -115,7 +121,7 @@ func serve(ctx context.Context, storeBackend, dbPath, zapAddr, httpAddr, initDat
// endpoint. The authz Guard gates it like any other route (fail-closed), but
// an identity service has no need to expose its admin CRUD as an agent tool
// surface, so it is disabled outright — one fewer surface to defend.
app := zip.New(zip.Config{AppName: "iam", MCP: zip.MCPConfig{Disabled: true}})
app := zip.New(zip.Config{AppName: "iam", MCP: zip.MCPConfig{Disabled: true}, OpsAddr: opsAddr})
routes.Route(app, db)
app.OnShutdown(func(context.Context) error { return db.Close() })