Files
mesh-catalog/modules/postgres/cmd/postgres-provider/main.go
T
lead-57 deb2020bd4
mesh/merge-gate head deb2020bd47d merged into trunk 5652a1494493 — pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine …
mesh/repo-check head deb2020bd47d merged into trunk 5652a1494493 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Serve postgres's consumers the port its server is published on, not the container's 5432 (issue 957)
On novox postgres is published 6852:5432; handed 5432, gitea and keycloak reached
nothing once their files were filled from postgres's entry.
2026-10-16 21:56:15 +02:00

165 lines
6.2 KiB
Go

// postgres-provider: postgres's code, one binary the node's runtime launches and speaks MCP to over
// stdio through the Go SDK (novox/hq ADR 0188, 0193). It serves postgres's tools and the mesh-store
// seat's verbs and, beside them, runs long: the provisioner that makes postgres the provider of the
// mesh `postgres-database` interface, and an audit line for each database it provisions or withdraws.
//
// stdout is the MCP channel; everything this module says, it says on stderr.
package main
import (
"context"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func say(format string, args ...any) {
fmt.Fprintf(os.Stderr, "[postgres] "+format+"\n", args...)
}
func main() {
pg, err := ClientFromEnv(os.Getenv)
if err != nil {
// Without the server there is nothing to serve and nothing to provision; said, not fatal,
// so the runtime does not restart a process that cannot do better.
say("%v; serving no tools and provisioning nothing", err)
if err := stdio.Serve("", nil); err != nil {
say("%v", err)
os.Exit(1)
}
return
}
var h *harness.Harness
if consumers := handedConsumersFile(os.Getenv, os.Stat); consumers != "" {
// Handed to the provisioner (novox/hq ADR 0373): the provider helper answers the consumers its
// machine's client fills the file with, and the grant file still gives the consumers not moved
// (issue 937). Retirement is the harness's, never run, so its guards and its tools stand (issue 780).
h = &harness.Harness{
Resource: ProvisionName,
Receives: os.Getenv("MESH_RECEIVES"),
Adapter: provisioner{pg: pg, announce: announce},
Log: func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) },
Announce: func(event string, body map[string]any) {
if err := stdio.Emit(event, body); err != nil {
say("emit %s failed: %v", event, err)
}
},
Node: os.Getenv("MESH_NODE"),
}
run, p, bus, err := moved(consumers, h, publishedPort(os.Getenv))
if err != nil {
say("%v", err)
} else {
go func() {
if err := run.Run(context.Background(), p, bus); err != nil {
say("the provider helper stopped: %v", err)
}
}()
}
} else if receives := os.Getenv("MESH_RECEIVES"); receives == "" {
say("MESH_RECEIVES is not set — the provisioner cannot run without it")
} else {
h = &harness.Harness{
Resource: "postgres-database",
Receives: receives,
Adapter: provisioner{pg: pg, announce: announce},
Log: func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) },
// A consumer failed for minutes is said on the bus, where the controller hears it and
// `status` names it (novox/hq ADR 0224).
Announce: func(event string, body map[string]any) {
if err := stdio.Emit(event, body); err != nil {
say("emit %s failed: %v", event, err)
}
},
Node: os.Getenv("MESH_NODE"),
}
go h.Run(context.Background())
}
// Started on the grant path while the module is to be handed, it restarts itself when its client first fills
// the consumers' file, so the first fill moves it onto the helper without a hand restart (issue 780): `restart-on`
// names a service or a container, and the only one here is the database.
if at := os.Getenv("MESH_CONSUMERS_FILE"); at != "" && handedConsumersFile(os.Getenv, os.Stat) == "" {
go exitWhenFilled(at, os.Stat, 5*time.Second, func() {
say("%s is filled: restarting to answer from it", at)
os.Exit(0)
})
}
go audit()
// The store's reader is narrowed to what modules declare readable, at once and every minute, whether
// or not the provisioner runs: without a contributions file it is given nothing (novox/hq issue 337).
go pg.RunReader(context.Background(), os.Getenv("MESH_RECEIVES"), time.Minute, say)
// The retirement tools beside postgres's own: what is retired here, approving or rejecting what waits
// for a person, and deleting a retired consumer (novox/hq ADR 0230).
if err := stdio.Serve("", append(Tools(pg), harness.RetirementTools(h)...)); err != nil {
say("%v", err)
os.Exit(1)
}
}
// handedConsumersFile is the consumers' file when postgres is handed to the provisioner: the file MESH_CONSUMERS_FILE
// names, which the controller places only where it hands the module. Empty, where it is not set or the file is not
// there, and postgres then reads the grant directory exactly as it always did (issue 780).
func handedConsumersFile(getenv func(string) string, stat func(string) (os.FileInfo, error)) string {
at := getenv("MESH_CONSUMERS_FILE")
if at == "" {
return ""
}
if _, err := stat(at); err != nil {
return ""
}
return at
}
// exitWhenFilled calls filled once the file is there, asking every so often.
func exitWhenFilled(at string, stat func(string) (os.FileInfo, error), every time.Duration, filled func()) {
for {
if _, err := stat(at); err == nil {
filled()
return
}
time.Sleep(every)
}
}
// announce emits a lifecycle event without letting a broker hiccup fail the provisioning itself.
func announce(event string, body map[string]string) {
if err := stdio.Emit(event, body); err != nil {
say("emit %s failed: %v", event, err)
}
}
// audit keeps a line for each database granted and withdrawn — observability the provider is best
// placed to log. The events are its own, read back from its consumer (`consumes`).
func audit() {
subscribe := func() error {
return stdio.Subscribe("postgres.database.*", func(e stdio.Envelope) error {
var body struct {
Consumer string `json:"consumer"`
Database string `json:"database"`
}
_ = json.Unmarshal(e.Body, &body)
switch e.Key {
case "postgres.database.provisioned":
say("database provisioned for %s (db %s)", body.Consumer, body.Database)
case "postgres.database.deprovisioned":
say("database retired, kept (db %s)", body.Database)
}
return nil
})
}
// Asked again until the runtime takes it: the first ask may come before Serve is running.
for wait := time.Second; ; wait = min(wait*2, time.Minute) {
if err := subscribe(); err == nil {
say("auditing database lifecycle events")
return
} else if wait >= 8*time.Second {
say("subscribing to the lifecycle events failed, will retry: %v", err)
}
time.Sleep(wait)
}
}