diff --git a/examples/modules/redis.json b/examples/modules/redis.json index 74045d9..5f223c6 100644 --- a/examples/modules/redis.json +++ b/examples/modules/redis.json @@ -55,15 +55,7 @@ "type": "file", "path": "/var/lib/redis-module/redis.conf", "mode": "0600", - "content": "requirepass ${secret:default}\naclfile /data/users.acl\nappendonly yes\ndir /data\n", - "owner": "999:999" - }, - { - "id": "acl-seed", - "type": "file", - "path": "/services/redis/data/users.acl", - "mode": "0600", - "content": "", + "content": "requirepass ${secret:default}\nappendonly yes\ndir /data\n", "owner": "999:999" }, { diff --git a/examples/redis-provisioner/main.go b/examples/redis-provisioner/main.go index 371f920..92ca3d1 100644 --- a/examples/redis-provisioner/main.go +++ b/examples/redis-provisioner/main.go @@ -86,9 +86,15 @@ func watch(ctx context.Context) error { switch { case err != nil: fmt.Fprintf(os.Stderr, "cannot read what was granted: %v\n", err) - case state != last: + case state != last || !granted(ctx): + // Re-run when the inputs changed — or when the store no longer holds what was + // granted. The store keeps its users in memory, so a restart forgets every tenant; + // nothing rewrites the grants when that happens, and a watch that only re-read its + // inputs would leave the store empty until the next unrelated change. Reconciling is + // against reality, not against a diff of instructions. if err := run(ctx); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) + last = "" } else { last = state } @@ -207,16 +213,58 @@ func run(ctx context.Context) error { 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) - } + // Not persisted in the store, on purpose. An ACL file was tried and lost twice over: with + // one configured, redis takes the default user from the file and quietly ignores + // `requirepass`, so an empty seed left the store without any password at all — and the file + // is host-declared content, which the host reconciles, so every re-apply would have wiped + // what ACL SAVE wrote. Durability lives in the watch instead: a restarted store comes back + // empty and is re-granted within a tick, because the watch checks the store and not only + // its inputs. return nil } +// granted says whether the store still holds every user the mesh has granted — cheaply, so the +// watch can ask every tick. +func granted(ctx context.Context) bool { + raw, err := os.ReadFile(filepath.Join(grantsDir(), "mesh.json")) + if err != nil { + // Nothing granted (or nothing readable): nothing to be missing. + return true + } + var m manifest + if err := json.Unmarshal(raw, &m); err != nil { + return true + } + wanted := map[string]bool{} + for _, c := range m.Given { + if c.Node != "" && strings.HasPrefix(c.As, mark) { + wanted[c.As] = true + } + } + if len(wanted) == 0 { + return true + } + r, err := connect(ctx) + if err != nil { + return false + } + defer r.Close() + users, err := r.strings("ACL", "USERS") + if err != nil { + return false + } + holds := map[string]bool{} + for _, u := range users { + holds[u] = true + } + for u := range wanted { + if !holds[u] { + return false + } + } + return true +} + // resp is the five commands this needs, spoken directly. type resp struct { conn net.Conn