// gateway is the Lakehouse-Go HTTP ingress. D6 promotes the D1 // stub endpoints into real reverse-proxies fronting all four backing // services (storaged, catalogd, ingestd, queryd) on a single bind. // // Routes: // /v1/storage/* → storaged // /v1/catalog/* → catalogd // /v1/ingest → ingestd // /v1/sql → queryd // // The /v1 prefix lives at the edge — internal services route on // /storage, /catalog, /ingest, /sql. Per Kimi K2 finding from the // D1 plan review: httputil.NewSingleHostReverseProxy preserves the // inbound path by default, so the proxy helper strips /v1 in its // Director before forwarding. package main import ( "flag" "log/slog" "net/url" "os" "github.com/go-chi/chi/v5" "git.agentview.dev/profit/golangLAKEHOUSE/internal/gateway" "git.agentview.dev/profit/golangLAKEHOUSE/internal/shared" ) func main() { configPath := flag.String("config", "lakehouse.toml", "path to TOML config") flag.Parse() cfg, err := shared.LoadConfig(*configPath) if err != nil { slog.Error("config", "err", err) os.Exit(1) } upstreams := map[string]string{ "storaged_url": cfg.Gateway.StoragedURL, "catalogd_url": cfg.Gateway.CatalogdURL, "ingestd_url": cfg.Gateway.IngestdURL, "queryd_url": cfg.Gateway.QuerydURL, "vectord_url": cfg.Gateway.VectordURL, "embedd_url": cfg.Gateway.EmbeddURL, "pathwayd_url": cfg.Gateway.PathwaydURL, "matrixd_url": cfg.Gateway.MatrixdURL, "observerd_url": cfg.Gateway.ObserverdURL, } for k, v := range upstreams { if v == "" { slog.Error("config", "err", "gateway."+k+" is required") os.Exit(1) } } // Per scrum O-WARN2 (Opus): url.Parse is permissive — a typo // like "127.0.0.1:3211" (missing scheme) parses without error // but produces empty Host, and every proxied request 502s. Fail // fast at startup if scheme/host are missing so misconfigs // surface in `systemctl status gateway` rather than at first traffic. storagedURL := mustParseUpstream("storaged_url", cfg.Gateway.StoragedURL) catalogdURL := mustParseUpstream("catalogd_url", cfg.Gateway.CatalogdURL) ingestdURL := mustParseUpstream("ingestd_url", cfg.Gateway.IngestdURL) querydURL := mustParseUpstream("queryd_url", cfg.Gateway.QuerydURL) vectordURL := mustParseUpstream("vectord_url", cfg.Gateway.VectordURL) embeddURL := mustParseUpstream("embedd_url", cfg.Gateway.EmbeddURL) pathwaydURL := mustParseUpstream("pathwayd_url", cfg.Gateway.PathwaydURL) matrixdURL := mustParseUpstream("matrixd_url", cfg.Gateway.MatrixdURL) observerdURL := mustParseUpstream("observerd_url", cfg.Gateway.ObserverdURL) storagedProxy := gateway.NewProxyHandler(storagedURL) catalogdProxy := gateway.NewProxyHandler(catalogdURL) ingestdProxy := gateway.NewProxyHandler(ingestdURL) querydProxy := gateway.NewProxyHandler(querydURL) vectordProxy := gateway.NewProxyHandler(vectordURL) embeddProxy := gateway.NewProxyHandler(embeddURL) pathwaydProxy := gateway.NewProxyHandler(pathwaydURL) matrixdProxy := gateway.NewProxyHandler(matrixdURL) observerdProxy := gateway.NewProxyHandler(observerdURL) if err := shared.Run("gateway", cfg.Gateway.Bind, func(r chi.Router) { // Storage / catalog have multi-segment paths under their // prefix (e.g. /v1/storage/get/). chi's `*` wildcard // captures the rest of the path. r.Handle("/v1/storage/*", storagedProxy) r.Handle("/v1/catalog/*", catalogdProxy) // Ingest + sql are single endpoints. We accept any method // (GET/POST/etc) and let the backing service decide. ingestd // only accepts POST; queryd only accepts POST. Other methods // will get the backend's 405. r.Handle("/v1/ingest", ingestdProxy) r.Handle("/v1/sql", querydProxy) // Vector search routes — /v1/vectors/index, /v1/vectors/index/{name}/... r.Handle("/v1/vectors/*", vectordProxy) // Embedding service — /v1/embed r.Handle("/v1/embed", embeddProxy) // Pathway memory — /v1/pathway/* r.Handle("/v1/pathway/*", pathwaydProxy) // Matrix indexer — /v1/matrix/* (multi-corpus retrieve+merge per SPEC §3.4) r.Handle("/v1/matrix/*", matrixdProxy) // Observer — /v1/observer/* (autonomous-iteration witness loop) r.Handle("/v1/observer/*", observerdProxy) }, cfg.Auth); err != nil { slog.Error("server", "err", err) os.Exit(1) } } // mustParseUpstream parses an upstream URL string and validates that // scheme + host are non-empty. Exits the process on failure — gateway // can't function without a valid upstream so failing fast is the // right call. Per scrum O-WARN2. func mustParseUpstream(name, raw string) *url.URL { u, err := url.Parse(raw) if err != nil { slog.Error("config", "err", "parse "+name+": "+err.Error()) os.Exit(1) } if u.Scheme == "" || u.Host == "" { slog.Error("config", "err", name+" must include scheme + host (got "+raw+")") os.Exit(1) } return u }