R4g+R5 (mesh-controller): the controller asks no build, build-agent and the merge gate go, register by reference (design 47) #448
@@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// The grants issued (novox/hq ADR 0528 clause 2, ADR 0429 §6 rule 6, issue 1394): what the assigner's sixth rule reads,
|
||||
// that a module's bus credential for a fingerprint exists. A module's permissions on the bus are what the bus's user list
|
||||
// composes from its registered manifest (inv.BusRecords), and a module's manifest is registered at a fingerprint when the
|
||||
// controller takes it in from the builder's key; so once the list is sent to the machine that runs the bus, the
|
||||
// credential of every module it carried is issued at the fingerprint of the build it is registered at. The controller
|
||||
// writes `<module>.<fingerprint hex>` then, naming the accounts it composed for the module, never removed. A module one
|
||||
// of whose accounts has no password yet (the list leaves it out) has no grant until a later send carries it.
|
||||
|
||||
// grantsFrom is the grant of every module whose accounts all have a password in hashes, at the fingerprint current names
|
||||
// for it; a module whose current build names no fingerprint of the new form has none. principals are the bus's users
|
||||
// as broker.Users composes them.
|
||||
func grantsFrom(principals []broker.Principal, hashes map[string]string, current map[string]string) map[string]sdkdelivery.ControllerGrant {
|
||||
accounts, missing := map[string][]string{}, map[string]bool{}
|
||||
for _, p := range principals {
|
||||
if p.Module == "" || (p.Kind != broker.KindModule && p.Kind != broker.KindProcess) {
|
||||
continue
|
||||
}
|
||||
if hashes[p.Username()] == "" {
|
||||
missing[p.Module] = true
|
||||
continue
|
||||
}
|
||||
accounts[p.Module] = append(accounts[p.Module], p.Username())
|
||||
}
|
||||
out := map[string]sdkdelivery.ControllerGrant{}
|
||||
for module, fp := range current {
|
||||
if missing[module] || !strings.HasPrefix(fp, sdkdelivery.SourcePrefix2) {
|
||||
continue
|
||||
}
|
||||
key, err := sdkdelivery.ControllerGrantKey(module, fp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
names := accounts[module]
|
||||
sort.Strings(names)
|
||||
out[key] = sdkdelivery.ControllerGrant{Accounts: names}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// currentFingerprints is, by module, the fingerprint of the build each registered module is registered at: its
|
||||
// manifest's commit (built_from), that commit's newest successful build, and the fingerprint that build names.
|
||||
func currentFingerprints(ctx context.Context, inv *inventory.Inventory) (map[string]string, error) {
|
||||
builds, err := inv.CurrentBuilds(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for module, b := range builds {
|
||||
if b.Commit == "" {
|
||||
continue
|
||||
}
|
||||
id, err := inv.BuildOf(ctx, module, b.Commit)
|
||||
if err != nil || id == "" {
|
||||
continue
|
||||
}
|
||||
build, found, err := inv.BuildByID(ctx, id)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
out[module] = build.SourceFingerprint
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// grantsKV is what writing the grants needs of their bucket.
|
||||
type grantsKV interface {
|
||||
Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error)
|
||||
Create(ctx context.Context, key string, value []byte, opts ...jetstream.KVCreateOpt) (uint64, error)
|
||||
Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error)
|
||||
}
|
||||
|
||||
// keepGrants writes every grant the list just sent issued that the bucket does not hold as it is: a create for a new one,
|
||||
// a compare-and-set for one whose accounts changed. It answers how many it wrote.
|
||||
func keepGrants(ctx context.Context, kv grantsKV, grants map[string]sdkdelivery.ControllerGrant) (int, error) {
|
||||
keys := make([]string, 0, len(grants))
|
||||
for k := range grants {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
wrote := 0
|
||||
var errs []error
|
||||
for _, key := range keys {
|
||||
value, err := sdkdelivery.EncodeControllerGrant(grants[key])
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", key, err))
|
||||
continue
|
||||
}
|
||||
e, err := kv.Get(ctx, key)
|
||||
switch {
|
||||
case errors.Is(err, jetstream.ErrKeyNotFound):
|
||||
_, err = kv.Create(ctx, key, value)
|
||||
case err != nil:
|
||||
default:
|
||||
if before, derr := sdkdelivery.DecodeControllerGrant(e.Value()); derr == nil && sdkdelivery.SameControllerGrant(before, withVersion(grants[key])) {
|
||||
continue
|
||||
}
|
||||
_, err = kv.Update(ctx, key, value, e.Revision())
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", key, err))
|
||||
continue
|
||||
}
|
||||
wrote++
|
||||
}
|
||||
return wrote, errors.Join(errs...)
|
||||
}
|
||||
|
||||
func withVersion(g sdkdelivery.ControllerGrant) sdkdelivery.ControllerGrant {
|
||||
g.V = sdkdelivery.Version
|
||||
return g
|
||||
}
|
||||
|
||||
// grantsIssued writes the grants the bus's user list just sent to its machine issued (ADR 0528 clause 2). Said, never
|
||||
// fatal: a grant not written now is written at the next send of the list, and the assigner's wants wait for it.
|
||||
func grantsIssued(ctx context.Context, inv *inventory.Inventory, js jetstream.JetStream, indent string) {
|
||||
kv, err := js.KeyValue(ctx, broker.GrantsBucket)
|
||||
if err != nil {
|
||||
fmt.Printf("%sthe grants issued are not written: the bucket %s: %v\n", indent, broker.GrantsBucket, err)
|
||||
return
|
||||
}
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("%sthe grants issued are not written: %v\n", indent, err)
|
||||
return
|
||||
}
|
||||
principals, err := broker.Users(records)
|
||||
if err != nil {
|
||||
fmt.Printf("%sthe grants issued are not written: %v\n", indent, err)
|
||||
return
|
||||
}
|
||||
kept, err := inv.BusUsers(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("%sthe grants issued are not written: %v\n", indent, err)
|
||||
return
|
||||
}
|
||||
hashes := make(map[string]string, len(kept))
|
||||
for name, u := range kept {
|
||||
hashes[name] = u.PasswordHash
|
||||
}
|
||||
current, err := currentFingerprints(ctx, inv)
|
||||
if err != nil {
|
||||
fmt.Printf("%sthe grants issued are not written: %v\n", indent, err)
|
||||
return
|
||||
}
|
||||
wrote, err := keepGrants(ctx, kv, grantsFrom(principals, hashes, current))
|
||||
if wrote > 0 {
|
||||
fmt.Printf("%s%d grant(s) issued written for the assigner\n", indent, wrote)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("%ssome grants issued are not written: %v\n", indent, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// 1394/AC1 of C9 (ADR 0528 clause 2): when the controller has issued mesh-x's credential for fingerprint F (mesh-x
|
||||
// registered at a build of F, its account minted, the bus's user list sent), then grants.mesh-x.F exists on the bus,
|
||||
// and a second pass writes it again as nothing new.
|
||||
func TestTheGrantOfAnIssuedCredentialIsOnTheBus(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
fp := grantFP('d')
|
||||
m := catalogue.Manifest{Module: "mesh-x", Version: "1", OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh-x/broker"}}}
|
||||
if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-x1", Repository: "novox/mesh-x", Module: "mesh-x", Commit: "c0ffee1",
|
||||
SourceFingerprint: fp}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: "novox/mesh-x", BuiltFrom: "c0ffee1", Head: "c0ffee1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := assign(ctx, open, "anchor", "mesh-x"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user := broker.Principal{Kind: broker.KindModule, Node: "anchor", Module: "mesh-x"}.Username()
|
||||
if _, err := inv.MintBusPassword(ctx, inventory.BusUser{Username: user, Kind: inventory.BusModule, Node: "anchor", Module: "mesh-x"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn, err := nats.Connect(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(conn.Close)
|
||||
js, err := jetstream.New(conn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := broker.OnConn(conn).EnsureControllerBuckets(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grantsIssued(ctx, inv, js, "")
|
||||
kv, err := js.KeyValue(ctx, broker.GrantsBucket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key, _ := sdkdelivery.ControllerGrantKey("mesh-x", fp)
|
||||
e, err := kv.Get(ctx, key)
|
||||
if err != nil {
|
||||
t.Fatalf("no grant %s after the list was issued: %v", key, err)
|
||||
}
|
||||
g, err := sdkdelivery.DecodeControllerGrant(e.Value())
|
||||
if err != nil || len(g.Accounts) != 1 || g.Accounts[0] != user {
|
||||
t.Fatalf("the grant reads %+v, %v; want the account %s", g, err, user)
|
||||
}
|
||||
grantsIssued(ctx, inv, js, "")
|
||||
if again, _ := kv.Get(ctx, key); again.Revision() != e.Revision() {
|
||||
t.Errorf("a second pass wrote the grant again (revision %d, was %d)", again.Revision(), e.Revision())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
)
|
||||
|
||||
func grantFP(c byte) string { return sdkdelivery.SourcePrefix2 + strings.Repeat(string(c), 64) }
|
||||
|
||||
// 1394/AC (unit half): the list sent issues the credential of every module whose accounts all have a password, at the
|
||||
// fingerprint its build names; a module with an account the list leaves out, and one registered at a fingerprint not of
|
||||
// the new form, get no grant; a module with no account of its own is granted with none.
|
||||
func TestTheGrantsAListIssues(t *testing.T) {
|
||||
x := broker.Principal{Kind: broker.KindModule, Node: "novox", Module: "mesh-x"}
|
||||
xp := broker.Principal{Kind: broker.KindProcess, Node: "novox", Module: "mesh-x", Process: "run"}
|
||||
y := broker.Principal{Kind: broker.KindModule, Node: "ace", Module: "mesh-y"}
|
||||
node := broker.Principal{Kind: broker.KindNode, Node: "novox"}
|
||||
hashes := map[string]string{x.Username(): "h1", xp.Username(): "h2", node.Username(): "h3"}
|
||||
got := grantsFrom([]broker.Principal{x, xp, y, node}, hashes,
|
||||
map[string]string{"mesh-x": grantFP('a'), "mesh-y": grantFP('b'), "web": grantFP('c'), "old": "src1:abc"})
|
||||
xKey, _ := sdkdelivery.ControllerGrantKey("mesh-x", grantFP('a'))
|
||||
webKey, _ := sdkdelivery.ControllerGrantKey("web", grantFP('c'))
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("grants = %v, want mesh-x's and web's", got)
|
||||
}
|
||||
if g := got[xKey]; len(g.Accounts) != 2 || g.Accounts[0] != x.Username() && g.Accounts[1] != x.Username() {
|
||||
t.Errorf("mesh-x's grant = %+v, want both its accounts", g)
|
||||
}
|
||||
if g, ok := got[webKey]; !ok || len(g.Accounts) != 0 {
|
||||
t.Errorf("web, with no account of its own, is granted %+v, %v", g, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeGrants is the grants bucket in memory.
|
||||
type fakeGrants struct {
|
||||
values map[string][]byte
|
||||
creates int
|
||||
updates int
|
||||
}
|
||||
|
||||
type fakeEntry struct {
|
||||
key string
|
||||
value []byte
|
||||
rev uint64
|
||||
}
|
||||
|
||||
func (e fakeEntry) Bucket() string { return broker.GrantsBucket }
|
||||
func (e fakeEntry) Key() string { return e.key }
|
||||
func (e fakeEntry) Value() []byte { return e.value }
|
||||
func (e fakeEntry) Revision() uint64 { return e.rev }
|
||||
func (e fakeEntry) Created() time.Time { return time.Time{} }
|
||||
func (e fakeEntry) Delta() uint64 { return 0 }
|
||||
func (e fakeEntry) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePut }
|
||||
|
||||
func (f *fakeGrants) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
|
||||
v, ok := f.values[key]
|
||||
if !ok {
|
||||
return nil, jetstream.ErrKeyNotFound
|
||||
}
|
||||
return fakeEntry{key: key, value: v, rev: 1}, nil
|
||||
}
|
||||
|
||||
func (f *fakeGrants) Create(_ context.Context, key string, value []byte, _ ...jetstream.KVCreateOpt) (uint64, error) {
|
||||
if _, ok := f.values[key]; ok {
|
||||
return 0, jetstream.ErrKeyExists
|
||||
}
|
||||
f.values[key] = value
|
||||
f.creates++
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (f *fakeGrants) Update(_ context.Context, key string, value []byte, _ uint64) (uint64, error) {
|
||||
f.values[key] = value
|
||||
f.updates++
|
||||
return 2, nil
|
||||
}
|
||||
|
||||
// 1394/AC (unit half): a grant is created once; a second send of the same list writes nothing; a reissue for other
|
||||
// accounts is a new revision of the same key.
|
||||
func TestAGrantIsWrittenOnceAndMovedOnAReissue(t *testing.T) {
|
||||
kv := &fakeGrants{values: map[string][]byte{}}
|
||||
key, _ := sdkdelivery.ControllerGrantKey("mesh-x", grantFP('a'))
|
||||
grants := map[string]sdkdelivery.ControllerGrant{key: {Accounts: []string{"novox.mesh-x"}}}
|
||||
for range 2 {
|
||||
if _, err := keepGrants(t.Context(), kv, grants); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if kv.creates != 1 || kv.updates != 0 {
|
||||
t.Errorf("creates %d, updates %d, want one create", kv.creates, kv.updates)
|
||||
}
|
||||
grants[key] = sdkdelivery.ControllerGrant{Accounts: []string{"ace.mesh-x"}}
|
||||
if wrote, err := keepGrants(t.Context(), kv, grants); err != nil || wrote != 1 || kv.updates != 1 {
|
||||
t.Errorf("a reissue wrote %d (%v), updates %d", wrote, err, kv.updates)
|
||||
}
|
||||
if g, err := sdkdelivery.DecodeControllerGrant(kv.values[key]); err != nil || g.Accounts[0] != "ace.mesh-x" {
|
||||
t.Errorf("the grant reads %+v, %v", g, err)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
@@ -1154,6 +1155,15 @@ func (b overTheBus) declare(ctx context.Context, s readyNode, body []byte) (stri
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// The list sent issued the credential of every module it carried, at the fingerprint each is registered at:
|
||||
// the grants the assigner's sixth rule reads (novox/hq ADR 0528 clause 2, issue 1394).
|
||||
if bus, ok := b.server.Bus().(link.OverNATS); ok {
|
||||
if js, err := jetstream.New(bus.Conn); err == nil {
|
||||
writing, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
|
||||
grantsIssued(writing, b.open.inventory, js, b.indent)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("%ssent %s %d resource(s)\n", b.indent, s.node, len(s.declared.Resources))
|
||||
return digest, nil
|
||||
|
||||
Reference in New Issue
Block a user