Files
mesh-controller/examples/redis-provisioner/main.go
T
jschoubben 8c4a478b09 Four more of the catalogue: registry, redis, umami, grafana
Converted from the arrangement being replaced, in its shapes rather
than theirs.

The registry is the manifest the lab already proved, promoted: names
its image by digest and is never built (04-ISSUES/029), provides the
artifact store, claims it once per machine.

Redis is the third provision after a database and a bucket, and the
first whose tenancy is a pattern in a shared keyspace rather than a
namespace something else enforces. Its provisioner mirrors the postgres
one's contract line for line — the manifest, the sealed per-consumer
files, the mark, the withdrawal of orphans — and speaks RESP directly:
five commands are needed, and a client library large enough to hide
them would be most of the program's size. A prefix Redis would read as
a pattern is refused, because the grant must mean what the manifest
said; grants are persisted with ACL SAVE, or said loudly, because a
cache that forgets its tenants on restart reports success until then.

Umami asks the mesh for its database and a generated app secret, and
carries no state of its own — the arrangement being replaced ran a
bundled second postgres beside it. Grafana keeps its dashboards in a
declared directory with the image's own owner. Both listen on 3000, as
does the forge — which is the mesh's port assignment earning its keep.

Traefik is deliberately not converted: the mesh's route provider is
mesh-route-proxy, which speaks route grants natively, and a traefik
that consumed them would be an adapter nobody has written pretending
to be a conversion.

All images pinned by real digests, resolved on this workstation today.
2026-09-01 22:44:53 +02:00

361 lines
11 KiB
Go

// A provisioner for Redis, 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 Redis to start accepting it. Something on that machine reads what
// the host wrote and makes it true. This is that something, for the third provision after a
// database and a bucket: a cache.
//
// **It is an example, not part of the control plane** — the same standing as the postgres one,
// whose contract this mirrors line for line:
//
// $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
//
// What a consumer is given is an ACL user scoped to a key prefix. Redis has no databases to hand
// out — SELECT-numbered ones are deprecated in clusters and shared in spirit — so the unit of
// tenancy is the keyspace pattern: a consumer contributing `prefix: photos` gets a user that can
// touch `photos:*` and nothing else. Administration and the dangerous category stay withheld;
// nothing a tenant is granted can flush the store or read another tenant's keys.
//
// Redis is spoken to in RESP directly, over one connection, with no client library. Five commands
// are needed — AUTH, PING, ACL SETUSER, ACL USERS, ACL DELUSER, ACL SAVE — and a dependency large
// enough to hide what they do would be most of this program's size.
package main
import (
"bufio"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
)
// mark is what this provisioner names the users it owns, so it never removes one a person made by
// hand — the mesh's rule about origins, one level down (novox/hq 04-ISSUES/010).
const mark = "mesh_"
type contribution struct {
From string `json:"from"`
Node string `json:"node"`
// As is what to call the user this consumer will authenticate as. Given by the mesh, never
// invented here (novox/hq 04-ISSUES/023): the consumer has to present it, so both ends must
// hold the same derivation.
As string `json:"as"`
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()
if len(os.Args) > 1 && os.Args[1] == "--watch" {
if err := watch(ctx); err != nil {
fmt.Fprintf(os.Stderr, "mesh-provision-redis: %v\n", err)
os.Exit(1)
}
return
}
if err := run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "mesh-provision-redis: %v\n", err)
os.Exit(1)
}
}
// watch reconciles now, and again whenever what the mesh delivered changes — by content, not by
// modification time, for the reasons the postgres provisioner records.
func watch(ctx context.Context) error {
const every = 10 * time.Second
var last string
for {
state, err := given()
switch {
case err != nil:
fmt.Fprintf(os.Stderr, "cannot read what was granted: %v\n", err)
case state != last:
if err := run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
} else {
last = state
}
}
select {
case <-ctx.Done():
return nil
case <-time.After(every):
}
}
}
// given digests everything delivered, so a change of any of it is one comparison and no secret is
// held beyond its hash.
func given() (string, error) {
entries, err := os.ReadDir(grantsDir())
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(grantsDir(), name))
if err != nil {
return "", err
}
fmt.Fprintf(sum, "%s:%x\n", name, sha256.Sum256(body))
}
return hex.EncodeToString(sum.Sum(nil)), nil
}
func grantsDir() string {
if grants := os.Getenv("GRANTS"); grants != "" {
return grants
}
return "/var/lib/redis-module/grants"
}
func run(ctx context.Context) error {
raw, err := os.ReadFile(filepath.Join(grantsDir(), "mesh.json"))
if err != nil {
if os.IsNotExist(err) {
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", grantsDir(), err)
}
r, err := connect(ctx)
if err != nil {
return err
}
defer r.Close()
// Reconciling, not applying a change: the same state from wherever it starts.
wanted := map[string]bool{}
for _, c := range m.Given {
if c.Node == "" {
continue
}
prefix, _ := c.Values["prefix"].(string)
if prefix == "" {
return fmt.Errorf("%s asked for a cache and did not say its key prefix", c.From)
}
if err := usablePrefix(prefix); err != nil {
return fmt.Errorf("%s: %w", c.From, err)
}
password, err := os.ReadFile(c.Secret)
if err != nil {
return fmt.Errorf("%s's credential should be at %s and is not there", c.Node, c.Secret)
}
user := c.As
if user == "" {
return fmt.Errorf("%s on %s was granted a cache and the mesh did not say what to "+
"call its user, so there is no name both ends would agree on", c.From, c.Node)
}
if !strings.HasPrefix(user, mark) {
return fmt.Errorf("%s on %s is to be called %q, which does not begin with %q — "+
"withdrawal finds this provisioner's work by that prefix, so it would never let "+
"this one go", c.From, c.Node, user, mark)
}
wanted[user] = true
// One SETUSER, from reset: the whole grant every time, so a rotation replaces the
// password and a changed prefix replaces the keyspace, with no residue of what was.
if _, err := r.do("ACL", "SETUSER", user, "reset", "on",
">"+strings.TrimSpace(string(password)),
"~"+prefix+":*", "&"+prefix+":*",
"+@all", "-@admin", "-@dangerous"); err != nil {
return fmt.Errorf("granting %s: %w", user, err)
}
fmt.Printf("granted %s the keyspace %s:*\n", user, prefix)
}
// And every user this provisioner made that nobody asks for any more — the half usually
// missing, without which a departed consumer keeps a working login for ever.
users, err := r.strings("ACL", "USERS")
if err != nil {
return err
}
for _, user := range users {
if !strings.HasPrefix(user, mark) || wanted[user] {
continue
}
if _, err := r.do("ACL", "DELUSER", user); err != nil {
return fmt.Errorf("revoking %s: %w", user, err)
}
fmt.Printf("revoked %s\n", user)
}
// Persisted, or the next restart forgets every grant. The server runs with an aclfile for
// exactly this; a server without one refuses the save, and saying so beats a cache that
// silently loses its tenants on restart.
if _, err := r.do("ACL", "SAVE"); err != nil {
return fmt.Errorf("the grants were applied and could not be persisted — a restart would "+
"forget them: %w", err)
}
return nil
}
// resp is the five commands this needs, spoken directly.
type resp struct {
conn net.Conn
in *bufio.Reader
}
func connect(ctx context.Context) (*resp, error) {
where := os.Getenv("MESH_PROVISION_REDIS")
if where == "" {
where = "127.0.0.1:6379"
}
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", where)
if err != nil {
return nil, err
}
r := &resp{conn: conn, in: bufio.NewReader(conn)}
file := os.Getenv("MESH_PROVISION_PASSWORD_FILE")
if file == "" {
return nil, fmt.Errorf("MESH_PROVISION_PASSWORD_FILE is not set, and an unauthenticated " +
"provisioner would mean an unauthenticated store")
}
password, err := os.ReadFile(file)
if err != nil {
return nil, err
}
if _, err := r.do("AUTH", strings.TrimSpace(string(password))); err != nil {
return nil, fmt.Errorf("the store did not accept the password the mesh sealed here: %w", err)
}
if _, err := r.do("PING"); err != nil {
return nil, err
}
return r, nil
}
func (r *resp) Close() { _ = r.conn.Close() }
// do sends one command and returns the reply, flattened to a string for the simple kinds.
func (r *resp) do(args ...string) (any, error) {
var out strings.Builder
fmt.Fprintf(&out, "*%d\r\n", len(args))
for _, a := range args {
fmt.Fprintf(&out, "$%d\r\n%s\r\n", len(a), a)
}
if _, err := r.conn.Write([]byte(out.String())); err != nil {
return nil, err
}
return r.read()
}
// strings is do, for the replies that are arrays of bulk strings.
func (r *resp) strings(args ...string) ([]string, error) {
reply, err := r.do(args...)
if err != nil {
return nil, err
}
items, ok := reply.([]any)
if !ok {
return nil, fmt.Errorf("expected an array and the store said %v", reply)
}
var out []string
for _, item := range items {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out, nil
}
func (r *resp) read() (any, error) {
line, err := r.in.ReadString('\n')
if err != nil {
return nil, err
}
line = strings.TrimRight(line, "\r\n")
if line == "" {
return nil, fmt.Errorf("the store sent an empty reply")
}
body := line[1:]
switch line[0] {
case '+', ':':
return body, nil
case '-':
// The store's own words, verbatim: it says exactly what it refused and why.
return nil, fmt.Errorf("%s", body)
case '$':
if body == "-1" {
return nil, nil
}
var n int
fmt.Sscanf(body, "%d", &n)
buf := make([]byte, n+2)
if _, err := readFull(r.in, buf); err != nil {
return nil, err
}
return string(buf[:n]), nil
case '*':
var n int
fmt.Sscanf(body, "%d", &n)
items := make([]any, 0, n)
for range n {
item, err := r.read()
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
return nil, fmt.Errorf("the store began a reply with %q, which this does not speak", line[0])
}
func readFull(in *bufio.Reader, buf []byte) (int, error) {
total := 0
for total < len(buf) {
n, err := in.Read(buf[total:])
if err != nil {
return total, err
}
total += n
}
return total, nil
}
// usablePrefix refuses a prefix that would grant more keyspace than anyone read in the manifest.
//
// The grant is written into an ACL pattern, so a prefix carrying pattern characters stops meaning
// itself: `pho*` granted as `pho*:*` matches every tenant whose name starts with pho. The grant
// must mean what it says, so anything Redis would read as a pattern is refused rather than
// escaped — escaping would create a second naming scheme the mesh does not know about.
func usablePrefix(prefix string) error {
if prefix == "" {
return fmt.Errorf("the key prefix is empty, which would grant the whole keyspace")
}
if strings.ContainsAny(prefix, " \t\n\r*?[]^{}") {
return fmt.Errorf("the key prefix %q contains characters Redis reads as a pattern, so "+
"the grant would match more than it names", prefix)
}
return nil
}