Contributions were node-local, so a mesh-scoped provider — the one case that most needs them — never heard from its consumers. A database was given a password and no idea what to create it for. Cross-node consumers now reach the provider's `receives` file, merged in with the ones on its own machine: from the provider's side they are the same thing, and a provider that had to read two lists would read one of them. Each names the file its credential is in rather than carrying it, because the mesh discarded the value and could not put it there. The readable half therefore stays readable. And examples/postgres-provisioner, which is the last step: it reads what the host wrote and makes PostgreSQL accept it. Explicitly not part of the control plane — the control plane decides and never touches a machine. This runs on the machine and touches it, and a real one ships with the module that ships PostgreSQL. It lives here because this is where the contract is defined, written as something that runs so it can be read. It reconciles rather than applying a change, because it is never told what changed. Three things that follow, and each is a fault somebody has shipped: - the password is set every time, not only on creation, or a rotation reports success and changes nothing - what it made and nobody asks for any more is revoked, or a departed consumer keeps a working login for ever - what it did not make is left alone, or it cannot be run on a database that predates it Proven in the lab against a real PostgreSQL, each assertion confirmed to fail with the behaviour removed. The suite is in mesh-lab, which also records the two ways the test itself was wrong first.
217 lines
7.2 KiB
Go
217 lines
7.2 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"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"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() {
|
|
if err := run(context.Background()); err != nil {
|
|
fmt.Fprintf(os.Stderr, "mesh-provision-postgres: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
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, `'`, `''`) + `'` }
|