Files
mesh-controller/examples/postgres-provisioner/main.go
T
jschoubben c37d368f65 A module may need a secret of its own, and the provisioner watches
Two things, both found by trying to write a real postgres module and
discovering it could not be said.

A database has a superuser password, a broker an administrator, a
registry an account. None of them is *for* anybody — they are not the
credential a consumer is given, and the mechanism that hands those out
has a consumer in the middle of it. So a module may declare what it needs
and where to put it, and the mesh generates one per node, seals it, and
reads it no more than it reads any other.

Per node, deliberately: a module running on three machines has three
passwords. One in the manifest instead would put the same secret on every
machine that ever runs it, in a file anybody can read, for ever. Made
once and kept, or a running database would be handed a password it was
not started with; remade when the machine's sealing key changes, like
everything else sealed here.

A need declared and not made is refused rather than skipped, because a
module whose own credential is silently absent starts, fails to
authenticate, and the reason is three layers from the machine reporting
it.

And the provisioner can watch. That is what lets it be a module rather
than a binary somebody places: run once, it needs invoking after every
declaration by a timer or a unit wired to a file; watching, it is an
ordinary long-running service the host already supervises. It polls
rather than watching the filesystem, because the host writes atomically —
the file is replaced, so a watch on the path stops seeing anything after
the first replacement, and a watcher that silently stops working is worse
than a poll. Credentials are compared by digest and never held: this runs
for as long as the machine is up.
2026-08-30 18:22:05 +02:00

309 lines
10 KiB
Go

// A provisioner, in the form the mesh expects one.
//
// The mesh generated a password, sealed it to the machine that must accept it, and discarded the
// plaintext — so it cannot tell PostgreSQL to start accepting it. Something on that machine reads
// what the host wrote and makes it true. This is that something.
//
// **It is an example, not part of the control plane.** The control plane decides and never
// touches a machine; this runs on the machine and touches it. A real one ships with the module
// that ships PostgreSQL (novox/hq ADR 0001 — third-party software runs *on* the mesh, not *of*
// it). What lives here is the contract, written as something that runs so it can be read rather
// than described.
//
// What it is given, both written by the host from an ordinary declaration:
//
// $GRANTS/mesh.json every consumer, what it asked for, and where its credential is
// $GRANTS/<node>.secret one consumer's password, alone in the file
//
// Two files because the mesh discarded the value and could not compose a document containing it.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
"github.com/jackc/pgx/v5"
)
// mark is what this provisioner names the roles it owns.
//
// So it never removes one a person made by hand — the mesh's own rule about origins, one level
// down (novox/hq 04-ISSUES/010). A provisioner that dropped every role it did not recognise would
// be a provisioner nobody could safely run on a database that predates it.
const mark = "mesh_"
// contribution is one consumer, as the mesh described it.
type contribution struct {
From string `json:"from"`
// Node is empty for a module on this machine, which is asking for something local and is not
// this provisioner's business.
Node string `json:"node"`
Secret string `json:"secret"`
Values map[string]any `json:"values"`
}
type manifest struct {
Requirement string `json:"requirement"`
Given []contribution `json:"given"`
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Once, or whenever what it was given changes.
//
// **Watching is what lets this be a module.** Run once, it has to be invoked by something
// after every declaration — a timer that runs it when nothing changed, or a unit wired to
// restart on a file. Watching, it is an ordinary long-running service, which is a shape the
// mesh already delivers and the host already supervises.
//
// The file it watches is the manifest the mesh writes. A credential changing rewrites the
// file beside it and not the manifest, so the manifest is stamped whenever either is written
// — which is why this compares content rather than modification time.
if len(os.Args) > 1 && os.Args[1] == "--watch" {
if err := watch(ctx); err != nil {
fmt.Fprintf(os.Stderr, "mesh-provision-postgres: %v\n", err)
os.Exit(1)
}
return
}
if err := run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "mesh-provision-postgres: %v\n", err)
os.Exit(1)
}
}
// watch reconciles now, and again whenever what the mesh delivered changes.
//
// By polling rather than by watching the filesystem, because the file is replaced rather than
// written in place — the host writes atomically, so an inotify watch on the path stops seeing
// anything after the first replacement, which is a watcher that silently stops working.
func watch(ctx context.Context) error {
const every = 10 * time.Second
var last string
for {
state, err := given()
switch {
case err != nil:
// Said and retried. A provisioner that exits because the mesh has not written
// anything yet is one that has to be restarted by hand after the first push.
fmt.Fprintf(os.Stderr, "cannot read what was granted: %v\n", err)
case state != last:
if err := run(ctx); err != nil {
// Reported and retried. The usual reason is that the database has not finished
// starting, and giving up would mean a module that works only if the two
// containers happen to come up in the right order.
fmt.Fprintf(os.Stderr, "%v\n", err)
} else {
last = state
}
}
select {
case <-ctx.Done():
return nil
case <-time.After(every):
}
}
}
// given is everything the mesh has delivered, as one string, so a change of any of it is one
// comparison.
//
// The credentials are included by their **digest**, never their content: this is compared, logged
// on nothing, and held in memory for as long as the process runs, and a secret does not belong in
// any of that when a hash answers the same question.
func given() (string, error) {
grants := os.Getenv("GRANTS")
if grants == "" {
grants = "/var/lib/postgres/grants"
}
entries, err := os.ReadDir(grants)
if err != nil {
return "", err
}
var names []string
for _, e := range entries {
names = append(names, e.Name())
}
sort.Strings(names)
sum := sha256.New()
for _, name := range names {
body, err := os.ReadFile(filepath.Join(grants, name))
if err != nil {
return "", err
}
fmt.Fprintf(sum, "%s:%x\n", name, sha256.Sum256(body))
}
return hex.EncodeToString(sum.Sum(nil)), nil
}
func run(ctx context.Context) error {
grants := os.Getenv("GRANTS")
if grants == "" {
grants = "/var/lib/postgres/grants"
}
raw, err := os.ReadFile(filepath.Join(grants, "mesh.json"))
if err != nil {
if os.IsNotExist(err) {
// Nothing has been granted here. Not a failure: a provider with no consumers is an
// ordinary state, and one this must be able to reach from any other.
fmt.Printf("nothing has been granted to this machine\n")
return nil
}
return err
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
return fmt.Errorf("the manifest at %s is not readable: %w", grants, err)
}
db, err := pgx.Connect(ctx, os.Getenv("MESH_PROVISION_POSTGRES"))
if err != nil {
return err
}
defer db.Close(ctx)
// **Reconciling, not applying a change.** It runs after every declaration and is never told
// what changed, so it must reach the same state from wherever it starts.
wanted := map[string]bool{}
for _, c := range sorted(m.Given) {
if c.Node == "" {
continue
}
name, _ := c.Values["name"].(string)
if name == "" {
return fmt.Errorf("%s asked for a database and did not name it", c.Node)
}
password, err := os.ReadFile(c.Secret)
if err != nil {
// The manifest says there is a credential and the host has not written it. Refused
// rather than creating a role with no password — a login nothing can use, which
// nothing would report until something tried to connect.
return fmt.Errorf("%s's credential should be at %s and is not there", c.Node, c.Secret)
}
role := mark + c.Node
wanted[role] = true
if err := ensureRole(ctx, db, role, strings.TrimSpace(string(password))); err != nil {
return err
}
if err := ensureDatabase(ctx, db, name, role); err != nil {
return err
}
}
// And everything this provisioner made that nobody asks for any more. **The half usually
// missing**: a consumer that goes away otherwise keeps a working login for ever and nothing
// says so.
return revokeOrphans(ctx, db, wanted)
}
func ensureRole(ctx context.Context, db *pgx.Conn, role, password string) error {
var exists bool
if err := db.QueryRow(ctx,
`select true from pg_roles where rolname = $1`, role).Scan(&exists); err != nil && err != pgx.ErrNoRows {
return err
}
// Set every time rather than only on creation. The mesh replaces the file when it rotates,
// and a provisioner that only ever created would leave the old password working — a rotation
// that reports success and changes nothing.
verb := "create"
if exists {
verb = "alter"
}
_, err := db.Exec(ctx, fmt.Sprintf("%s role %s with login password %s",
verb, quoteName(role), quoteString(password)))
if err != nil {
return err
}
if !exists {
fmt.Printf("created %s\n", role)
}
return nil
}
func ensureDatabase(ctx context.Context, db *pgx.Conn, name, owner string) error {
var exists bool
if err := db.QueryRow(ctx,
`select true from pg_database where datname = $1`, name).Scan(&exists); err != nil && err != pgx.ErrNoRows {
return err
}
if exists {
return nil
}
if _, err := db.Exec(ctx, fmt.Sprintf("create database %s owner %s",
quoteName(name), quoteName(owner))); err != nil {
return err
}
fmt.Printf("created database %s owned by %s\n", name, owner)
return nil
}
func revokeOrphans(ctx context.Context, db *pgx.Conn, wanted map[string]bool) error {
rows, err := db.Query(ctx,
`select rolname from pg_roles where rolname like $1 and rolcanlogin order by rolname`,
mark+"%")
if err != nil {
return err
}
var found []string
for rows.Next() {
var role string
if err := rows.Scan(&role); err != nil {
rows.Close()
return err
}
found = append(found, role)
}
rows.Close()
if err := rows.Err(); err != nil {
return err
}
for _, role := range found {
if wanted[role] {
continue
}
// Login removed rather than the role dropped. Dropping fails while the role owns
// anything, and a provisioner that failed there would stop reconciling everything else —
// so the credential stops working immediately and what it owns is somebody's to decide
// about.
if _, err := db.Exec(ctx, fmt.Sprintf("alter role %s with nologin",
quoteName(role))); err != nil {
return err
}
fmt.Printf("revoked %s — nothing in the mesh asks for it\n", role)
}
return nil
}
// sorted puts consumers in a stable order, so two runs do the same work in the same sequence and
// the output of one can be compared with another.
func sorted(given []contribution) []contribution {
out := append([]contribution{}, given...)
sort.Slice(out, func(i, j int) bool { return out[i].Node < out[j].Node })
return out
}
// quoteName and quoteString exist because PostgreSQL takes no parameters in DDL.
//
// Both double the quote character, which is the whole of the escaping rule. Worth doing properly
// even here: a password is chosen by the mesh and a node name by a person, and "the value happens
// to be safe today" is not a property anything should rest on.
func quoteName(s string) string { return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` }
func quoteString(s string) string { return `'` + strings.ReplaceAll(s, `'`, `''`) + `'` }