Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
package main
|
||||
|
||||
// postgres handed to the provisioner (novox/hq ADR 0373, ADR 0349 clause 12, issue 780): where its consumers file
|
||||
// is set, postgres runs on the SDK's provider helper instead of the harness reading the controller's grant files.
|
||||
//
|
||||
// The helper learns who binds postgres from the plans, composes each consumer's entry and reads its credential
|
||||
// through ConsumersFile, the file this machine's provisioner client fills. It leaves the rest to the two hooks
|
||||
// here: Make brings the consumer's database and role into being exactly as the harness's adapter does, and Gone
|
||||
// retires what was kept for a consumer that no longer binds, the reversible way (retire_pg.go): the login is
|
||||
// locked and marked to delete, nothing is dropped.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/provider"
|
||||
)
|
||||
|
||||
// ProvisionName is what postgres provides, as its manifest says.
|
||||
const ProvisionName = "postgres-database"
|
||||
|
||||
// servedPort is the port its manifest serves (`serves.postgres-database.port`).
|
||||
const servedPort = 5432
|
||||
|
||||
// movedRun is postgres on the provider helper, with the harness's own memory and retirement behind it: what is held,
|
||||
// what is counted unasked, what waits for a person (retirement.go). The harness is never Run here; the helper's
|
||||
// Make and Gone and the loop below drive the same state, so the guards are the harness's, not a copy.
|
||||
type movedRun struct {
|
||||
h *Harness
|
||||
|
||||
mu sync.Mutex // guards assignments and loaded
|
||||
assignments map[string][]byte
|
||||
loaded bool
|
||||
verified map[string]time.Time
|
||||
}
|
||||
|
||||
func newMovedRun(h *Harness) *movedRun {
|
||||
h.mu.Lock()
|
||||
h.init()
|
||||
h.mu.Unlock()
|
||||
return &movedRun{h: h, assignments: map[string][]byte{}, verified: map[string]time.Time{}}
|
||||
}
|
||||
|
||||
// Make brings one consumer's database and role into being under its login and the vault's credential, as the
|
||||
// harness does: at once when it is new or changed, and otherwise only when the server no longer holds it, which is
|
||||
// asked every VerifyEvery.
|
||||
func (m *movedRun) Make(ctx context.Context, c provider.Consumer) error {
|
||||
h, e := m.h, c.Entry
|
||||
if e.User == "" {
|
||||
return fmt.Errorf("%s has no login to make", e.Key())
|
||||
}
|
||||
if len(c.Credential) == 0 {
|
||||
return fmt.Errorf("%s has no credential to make its login with", e.Key())
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
as, password := e.User, string(c.Credential)
|
||||
p := Provision{As: as, Password: password, Values: orEmpty(e.Contributes), Consumer: e.Consumer}
|
||||
hash := hashOf(as, password, p.Values, nil)
|
||||
if was, ok := h.applied[as]; ok && was.hash == hash {
|
||||
if h.Now().Sub(m.verified[as]) < h.VerifyEvery {
|
||||
return nil
|
||||
}
|
||||
m.verified[as] = h.Now()
|
||||
hctx, cancel := context.WithTimeout(ctx, h.HoldsTimeout)
|
||||
held, err := h.Adapter.Holds(hctx, p)
|
||||
cancel()
|
||||
if err != nil {
|
||||
text := scrub(err, password)
|
||||
h.failed(as, e.Consumer, h.classOf(err, text), text)
|
||||
return errors.New(text)
|
||||
}
|
||||
if held {
|
||||
h.succeeded(as)
|
||||
return nil
|
||||
}
|
||||
h.say("%s: the backend no longer holds it; applying again", as)
|
||||
}
|
||||
if err := h.Adapter.Create(ctx, p); err != nil {
|
||||
text := scrub(err, password)
|
||||
h.failed(as, e.Consumer, h.classOf(err, text), text)
|
||||
return errors.New(text)
|
||||
}
|
||||
h.succeeded(as)
|
||||
h.applied[as] = appliedEntry{hash: hash, node: e.Consumer}
|
||||
m.verified[as] = h.Now()
|
||||
h.reenabled(as, e.Consumer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gone is the helper asking whether it may forget a consumer that no longer binds. **It never retires**: retiring is
|
||||
// the harness's rule, the same unasked set over StablePasses passes and StableFor, at most RetireAtOnce at a time
|
||||
// (retirement.go, ADR 0230), run by retire below. So a consumer missing from one read of the plans loses nothing:
|
||||
// the helper is told no, and asks again at its next pass, until the login is retired, or asked for again.
|
||||
func (m *movedRun) Gone(_ context.Context, e provider.Entry) error {
|
||||
h := m.h
|
||||
as := e.User
|
||||
if as == "" {
|
||||
return nil // refused: nothing was made for it
|
||||
}
|
||||
// The plans as they are now, not as the last retirement pass read them: with none read, nothing is let go.
|
||||
want, read := m.asked(h.Node)
|
||||
asked := want[as]
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
_, held := h.applied[as]
|
||||
_, retired := h.r.retired[as]
|
||||
switch {
|
||||
case !read:
|
||||
case asked:
|
||||
return nil // another holder of the same login still binds: nothing to retire
|
||||
case retired, !held && h.r.seeded:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s is kept until its retirement is stable (%d passes and %s) and within the bound, or a "+
|
||||
"person approves it", as, h.StablePasses, h.StableFor)
|
||||
}
|
||||
|
||||
// plan is as much of an assignment as is read to learn who binds postgres.
|
||||
type plan struct {
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
Provisioning *struct {
|
||||
Provisions []struct {
|
||||
Provision string `json:"provision"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderModule string `json:"provider-module"`
|
||||
// As is who the consumer is at its provider's end, its slug first (issue 788).
|
||||
As string `json:"as"`
|
||||
} `json:"provisions"`
|
||||
} `json:"provisioning"`
|
||||
}
|
||||
|
||||
// asked is the logins the plans bind postgres for, and whether they could all be read: a plan that is not the
|
||||
// controller's, or no read yet, is not an answer, so nothing is counted unasked from it (novox/hq issue 241).
|
||||
func (m *movedRun) asked(node string) (map[string]bool, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.loaded || len(m.assignments) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
want := map[string]bool{}
|
||||
for key, raw := range m.assignments {
|
||||
var a plan
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Node+"."+a.Module != key {
|
||||
return nil, false
|
||||
}
|
||||
if a.Provisioning == nil {
|
||||
continue
|
||||
}
|
||||
for _, b := range a.Provisioning.Provisions {
|
||||
if b.Provider == node && b.ProviderModule == "postgres" && b.Provision == ProvisionName {
|
||||
as := b.As
|
||||
if as == "" {
|
||||
as = provider.ConsumerIdentity(a.Node, a.Module) // a plan that names none
|
||||
}
|
||||
want[as] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return want, true
|
||||
}
|
||||
|
||||
// retire is one pass of the harness's retirement over what the plans bind now.
|
||||
func (m *movedRun) retire(ctx context.Context) {
|
||||
h := m.h
|
||||
want, ok := m.asked(h.Node)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if !ok {
|
||||
// Not a result: the passes that must agree before anything is retired start again.
|
||||
h.r.key, h.r.count = "", 0
|
||||
return
|
||||
}
|
||||
h.seed(ctx, want)
|
||||
h.retireUnasked(ctx, want)
|
||||
h.r.lastWant = want
|
||||
for as := range h.failing {
|
||||
if !want[as] {
|
||||
delete(h.failing, as)
|
||||
}
|
||||
}
|
||||
for as := range h.trouble {
|
||||
if _, held := h.applied[as]; !want[as] && !held {
|
||||
h.recovered(as, "no longer asked for")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watch keeps the plans as the bus holds them, from the first read on.
|
||||
func (m *movedRun) watch(ctx context.Context, bus provider.Bus) error {
|
||||
err := bus.Watch(ctx, provider.AssignmentsBucket, func(key string, value []byte) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if value == nil {
|
||||
delete(m.assignments, key)
|
||||
} else {
|
||||
m.assignments[key] = append([]byte(nil), value...)
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
m.mu.Lock()
|
||||
m.loaded = true
|
||||
m.mu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Run answers the consumers through the helper and retires through the harness's rules, until ctx ends.
|
||||
func (m *movedRun) Run(ctx context.Context, p *provider.Provider, bus provider.Bus) error {
|
||||
if err := m.watch(ctx, bus); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
m.retire(ctx)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(m.h.Every):
|
||||
}
|
||||
}
|
||||
}()
|
||||
return p.Run(ctx)
|
||||
}
|
||||
|
||||
// moved is postgres handed to the provisioner: the helper answering its consumers, and the harness (never run) whose
|
||||
// retirement and tools it shares. consumersFile is where its machine's client fills its consumers.
|
||||
func moved(consumersFile string, h *Harness) (*movedRun, *provider.Provider, provider.Bus, error) {
|
||||
if consumersFile == "" {
|
||||
return nil, nil, nil, errors.New("no consumers' file")
|
||||
}
|
||||
run := newMovedRun(h)
|
||||
bus := provider.StateBus("postgres")
|
||||
return run, provider.New(provider.Options{
|
||||
Node: h.Node,
|
||||
Module: "postgres",
|
||||
Offers: map[string]provider.Offer{ProvisionName: {
|
||||
Credential: provider.PerConsumer,
|
||||
Serves: map[string]any{"port": servedPort},
|
||||
}},
|
||||
Bus: bus,
|
||||
Vault: provider.ConsumersFile{Paths: []string{consumersFile}},
|
||||
Make: run.Make,
|
||||
Gone: run.Gone,
|
||||
Every: h.Every,
|
||||
Logf: func(f string, a ...any) { h.say(f, a...) },
|
||||
}), bus, nil
|
||||
}
|
||||
Reference in New Issue
Block a user