mesh/merge-gate head 41cdd032084e merged into trunk 6268f8f94bda — pass: builds keycloak, postgres, sent nowhere; no bus step; 3 wait(s) for a person; ever…
mesh/repo-check head 41cdd032084e merged into trunk 6268f8f94bda — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Only a moved consumer's plan names postgres, so on the moved path every consumer still given by the controller's grant file was counted unasked and offered for retirement (ace 5 of 5, novox 7 of 9), and could have been retired unasked where the bound allowed. The moved path now reads MESH_RECEIVES again: its logins are asked, the ones no plan names are made and checked by the harness's grant pass until R1 (issue 682), and an unreadable grant file retires nothing (issue 241). The harness's grant pass becomes apply, identically in keycloak's copy.
164 lines
6.1 KiB
Go
164 lines
6.1 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"
|
|
"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
|
|
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{
|
|
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)
|
|
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{
|
|
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), 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)
|
|
}
|
|
}
|