The grants part must compose with the controller's own composer before it can take over the list; in shadow it proves the two agree, names every input the bus does not carry yet, and takes no effect. It claims no seat (mesh-sdk's seat list does not name mesh-grants for the running composer), serves module tools, and keeps running when a read or write is refused.
128 lines
5.0 KiB
Go
128 lines
5.0 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// Item is one bucket, log or stream a manifest declares, as composed, beside what the bus holds of it.
|
|
type Item struct {
|
|
Kind string `json:"kind"`
|
|
Stream string `json:"stream"`
|
|
// Composed is what an assertion would ask for; Held what the bus holds, absent when it holds no such stream.
|
|
Composed Limits `json:"composed"`
|
|
Held *Limits `json:"held,omitempty"`
|
|
// Differences says how they differ, in words; empty when they agree in what can be read from here.
|
|
Differences []string `json:"differences,omitempty"`
|
|
}
|
|
|
|
// Limits are the facts of a stream an assertion sets and the bus module's nats_streams reads back. Its size cap is not
|
|
// read back by that tool, so it is composed and not compared.
|
|
type Limits struct {
|
|
Subjects []string `json:"subjects"`
|
|
Retention string `json:"retention"`
|
|
MaxAgeHours int `json:"max_age_hours,omitempty"`
|
|
MaxPerSubject int `json:"max_per_subject,omitempty"`
|
|
MaxBytes int64 `json:"max_bytes,omitempty"`
|
|
}
|
|
|
|
// Assertion is one module's declared buckets, logs and streams, composed and compared. Asserted is always false in
|
|
// shadow: nothing is created or changed.
|
|
type Assertion struct {
|
|
Module string `json:"module"`
|
|
Manifest string `json:"manifest"`
|
|
Items []Item `json:"items"`
|
|
Asserted bool `json:"asserted"`
|
|
Says string `json:"says"`
|
|
}
|
|
|
|
// ShadowAsserts is what every assertion says in this milestone.
|
|
const ShadowAsserts = "in shadow: composed and compared, nothing asserted (ADR 0547 clause 5 asserts from take-over)"
|
|
|
|
// Composed is a module's buckets, logs and streams as an assertion would ask for them. Pure.
|
|
func Composed(m catalogue.Manifest) []Item {
|
|
var out []Item
|
|
for _, b := range bucketsOf(m) {
|
|
per := b.History
|
|
if per < 1 {
|
|
per = 1
|
|
}
|
|
out = append(out, Item{Kind: "bucket", Stream: "KV_" + b.Bucket(), Composed: Limits{
|
|
Subjects: []string{"$KV." + b.Bucket() + ".>"}, Retention: string(buscompose.RetentionLimits),
|
|
MaxAgeHours: b.TTLSeconds / 3600, MaxPerSubject: per}})
|
|
}
|
|
for _, l := range logsOf(m) {
|
|
c := l.Config()
|
|
out = append(out, Item{Kind: "log", Stream: l.Stream(), Composed: Limits{Subjects: set(c.Subjects),
|
|
Retention: string(buscompose.RetentionLimits), MaxAgeHours: int(c.MaxAge.Hours()), MaxBytes: c.MaxBytes}})
|
|
}
|
|
for _, s := range streamsOf(m) {
|
|
st := s.Stream()
|
|
out = append(out, Item{Kind: "stream", Stream: st.Name, Composed: Limits{Subjects: set(st.Subjects),
|
|
Retention: string(st.Retention), MaxAgeHours: st.MaxAge / 3600, MaxBytes: st.MaxBytes}})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HeldStreams reads every stream the bus holds through its module's nats_streams, by name.
|
|
func HeldStreams(c Caller) (map[string]Limits, error) {
|
|
raw, err := c.Call(CallStreams, map[string]any{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", CallStreams, err)
|
|
}
|
|
var got struct {
|
|
Detail []struct {
|
|
Name string `json:"name"`
|
|
Subjects []string `json:"subjects"`
|
|
Retention string `json:"retention"`
|
|
MaxAgeHours int `json:"max_age_hours"`
|
|
MaxPerSubject int `json:"max_per_subject"`
|
|
} `json:"detail"`
|
|
}
|
|
if err := json.Unmarshal(unwrap(raw), &got); err != nil {
|
|
return nil, fmt.Errorf("%s answered no streams: %w", CallStreams, err)
|
|
}
|
|
out := map[string]Limits{}
|
|
for _, s := range got.Detail {
|
|
out[s.Name] = Limits{Subjects: set(s.Subjects), Retention: s.Retention, MaxAgeHours: s.MaxAgeHours, MaxPerSubject: s.MaxPerSubject}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Assert composes a module's items and compares each with what the bus holds; held nil is not read. It asserts nothing.
|
|
func Assert(module, digest string, m catalogue.Manifest, held map[string]Limits, heldErr error) Assertion {
|
|
a := Assertion{Module: module, Manifest: digest, Items: Composed(m), Says: ShadowAsserts}
|
|
for i := range a.Items {
|
|
it := &a.Items[i]
|
|
if heldErr != nil {
|
|
it.Differences = []string{"what the bus holds was not read: " + heldErr.Error()}
|
|
continue
|
|
}
|
|
h, ok := held[it.Stream]
|
|
if !ok {
|
|
it.Differences = []string{"the bus holds no stream " + it.Stream}
|
|
continue
|
|
}
|
|
it.Held = &h
|
|
if !slices.Equal(h.Subjects, it.Composed.Subjects) {
|
|
it.Differences = append(it.Differences, fmt.Sprintf("subjects %v held, %v composed", h.Subjects, it.Composed.Subjects))
|
|
}
|
|
if h.Retention != it.Composed.Retention {
|
|
it.Differences = append(it.Differences, fmt.Sprintf("retention %s held, %s composed", h.Retention, it.Composed.Retention))
|
|
}
|
|
if h.MaxAgeHours != it.Composed.MaxAgeHours {
|
|
it.Differences = append(it.Differences, fmt.Sprintf("age %d h held, %d h composed", h.MaxAgeHours, it.Composed.MaxAgeHours))
|
|
}
|
|
if it.Kind == "bucket" && max(h.MaxPerSubject, 1) != it.Composed.MaxPerSubject {
|
|
it.Differences = append(it.Differences, fmt.Sprintf("history %d held, %d composed", h.MaxPerSubject, it.Composed.MaxPerSubject))
|
|
}
|
|
}
|
|
sort.SliceStable(a.Items, func(i, j int) bool { return a.Items[i].Stream < a.Items[j].Stream })
|
|
return a
|
|
}
|