Keep the newest trunk build's list of predefined keys, and prove each refusal
The list itself, read strictly (catalogue/predefined.go); the controller keeps a catalogue trunk build's list in its store when its commit's generation is larger than the one kept (migration 0098), so an older build's list heard later never replaces a newer one, and loads it with its store (hq ADR 0348 clause 7, issue 661). Part 2 of 2: the new files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The list of predefined keys the mesh publishes (novox/hq ADR 0348 §7, design 57 package K2, issue 661).
|
||||||
|
//
|
||||||
|
// **The newest trunk build of the catalogue wins.** A build of the catalogue reads predefined.json at the commit
|
||||||
|
// it builds and carries it with the commit's generation (link.PredefinedList). Registration keeps it in the
|
||||||
|
// controller's store when that generation is larger than the one kept (inventory.KeepPredefined), so an older
|
||||||
|
// build's list heard later never replaces a newer one, and every manifest the controller reads again is judged
|
||||||
|
// against the list kept (catalogue.UsePredefined). mesh-config publishes it on the bus from there (design 57
|
||||||
|
// package K3); the controller keeps no config value, only the keys.
|
||||||
|
|
||||||
|
// fromTheCatalogue is whether a build is of the catalogue's repository, whose list alone the mesh publishes: by
|
||||||
|
// its name, in the URL form or the seat form.
|
||||||
|
func fromTheCatalogue(result link.BuildResult) bool {
|
||||||
|
for _, repository := range []string{result.Repository, sourceRepository(result)} {
|
||||||
|
name := strings.TrimSuffix(path.Base(strings.TrimRight(repository, "/")), ".git")
|
||||||
|
if name == catalogue.CatalogueRepository {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceRepository(result link.BuildResult) string {
|
||||||
|
if result.Source == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return result.Source.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// carriedList is the list a build of the catalogue carried, read strictly; nil for a build of another
|
||||||
|
// repository, or one that carried none.
|
||||||
|
func carriedList(result link.BuildResult) (catalogue.PredefinedList, error) {
|
||||||
|
if result.Predefined == nil || !fromTheCatalogue(result) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
list, err := catalogue.ParsePredefined(result.Predefined.Keys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s carried a list of predefined keys that is refused: %w", result.ID, err)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// judgedWith is the list a build's manifest is first judged against at registration: the catalogue's own at the
|
||||||
|
// commit built, which its build judged it against, where it carried one; the list kept otherwise.
|
||||||
|
func judgedWith(result link.BuildResult) (catalogue.PredefinedList, error) {
|
||||||
|
list, err := carriedList(result)
|
||||||
|
if err != nil || list != nil {
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
return catalogue.PublishedPredefined(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadPredefined makes the list kept in the store the one this controller judges manifests against. A store
|
||||||
|
// keeping none leaves the list it has.
|
||||||
|
func loadPredefined(ctx context.Context, inv *inventory.Inventory) error {
|
||||||
|
kept, has, err := inv.PredefinedKept(ctx)
|
||||||
|
if err != nil || !has {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
catalogue.UsePredefined(kept.Keys)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishPredefined keeps a trunk build's list when it is newer than the one kept, says so, and loads what the
|
||||||
|
// store keeps then — this build's, or a newer one another controller process kept.
|
||||||
|
func publishPredefined(ctx context.Context, inv *inventory.Inventory, result link.BuildResult,
|
||||||
|
list catalogue.PredefinedList) error {
|
||||||
|
kept, err := inv.KeepPredefined(ctx, inventory.PredefinedRecord{Keys: list, Repository: result.Repository,
|
||||||
|
Commit: result.Commit, Generation: result.Predefined.Generation, Build: result.ID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if kept {
|
||||||
|
fmt.Printf("%s: the list of predefined keys at %s (generation %d) is published: %s\n", result.ID,
|
||||||
|
short(result.Commit), result.Predefined.Generation, strings.Join(list.Keys(), ", "))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: the list of predefined keys at %s (generation %d) is not newer than the one kept, which "+
|
||||||
|
"stands\n", result.ID, short(result.Commit), result.Predefined.Generation)
|
||||||
|
}
|
||||||
|
return loadPredefined(ctx, inv)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- The list of predefined keys (novox/hq ADR 0348 clause 7, design 57 package K2, issue 661).
|
||||||
|
--
|
||||||
|
-- Mesh config and node config hold values only for the keys in the catalogue's `predefined.json`. A build of
|
||||||
|
-- the catalogue reads the file at the commit it builds and carries it; the controller keeps the newest trunk
|
||||||
|
-- build's, which every manifest it reads again is judged against, and which mesh-config publishes from here
|
||||||
|
-- (design 57 package K3). One list for the mesh: one row.
|
||||||
|
--
|
||||||
|
-- **The newest trunk build wins**, by the generation of the commit built — how many commits it stands on, which
|
||||||
|
-- of two commits on one trunk is larger for the later — so a list heard late from an older build never replaces
|
||||||
|
-- a newer one. Not a config value: a key, its type, description, validation and whether it is trusted, all of it
|
||||||
|
-- public in the catalogue's repository.
|
||||||
|
create table predefined_list (
|
||||||
|
one boolean primary key default true check (one),
|
||||||
|
keys jsonb not null,
|
||||||
|
repository text not null,
|
||||||
|
commit text not null,
|
||||||
|
generation bigint not null check (generation > 0),
|
||||||
|
build text not null,
|
||||||
|
kept_at timestamptz not null default now()
|
||||||
|
);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package inventory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The list of predefined keys the mesh publishes (novox/hq ADR 0348 §7, design 57 package K2): the newest trunk
|
||||||
|
// build's of the catalogue, by the generation of the commit built.
|
||||||
|
|
||||||
|
// PredefinedRecord is the list kept, and the build it came from.
|
||||||
|
type PredefinedRecord struct {
|
||||||
|
Keys catalogue.PredefinedList
|
||||||
|
Repository string
|
||||||
|
Commit string
|
||||||
|
Generation int
|
||||||
|
Build string
|
||||||
|
KeptAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeepPredefined keeps a trunk build's list when its commit is newer than the one kept — a larger generation —
|
||||||
|
// and answers whether it did. An older build's list, or one of the same generation, heard later is not kept: the
|
||||||
|
// newer one stands, whatever order the outcomes arrive in. One statement, so two builds heard at once cannot both
|
||||||
|
// win.
|
||||||
|
func (i *Inventory) KeepPredefined(ctx context.Context, r PredefinedRecord) (bool, error) {
|
||||||
|
if r.Keys == nil {
|
||||||
|
return false, errors.New("no list to keep")
|
||||||
|
}
|
||||||
|
if r.Generation <= 0 {
|
||||||
|
return false, fmt.Errorf("the build %s did not say its commit's generation, so whether its list is newer "+
|
||||||
|
"than the one kept cannot be said", r.Build)
|
||||||
|
}
|
||||||
|
if problems := catalogue.PredefinedProblems(r.Keys); len(problems) > 0 {
|
||||||
|
return false, fmt.Errorf("the list of %s is refused: %v", r.Build, problems)
|
||||||
|
}
|
||||||
|
text, err := r.Keys.Canonical()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
tag, err := i.store.Pool().Exec(ctx, `
|
||||||
|
insert into predefined_list (keys, repository, commit, generation, build)
|
||||||
|
values ($1, $2, $3, $4, $5)
|
||||||
|
on conflict (one) do update set keys = excluded.keys, repository = excluded.repository,
|
||||||
|
commit = excluded.commit, generation = excluded.generation, build = excluded.build, kept_at = now()
|
||||||
|
where predefined_list.generation < excluded.generation`,
|
||||||
|
text, r.Repository, r.Commit, r.Generation, r.Build)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return tag.RowsAffected() == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PredefinedKept is the list kept, and false when none is yet.
|
||||||
|
func (i *Inventory) PredefinedKept(ctx context.Context) (PredefinedRecord, bool, error) {
|
||||||
|
var r PredefinedRecord
|
||||||
|
var text []byte
|
||||||
|
err := i.store.Pool().QueryRow(ctx,
|
||||||
|
`select keys, repository, commit, generation, build, kept_at from predefined_list`).
|
||||||
|
Scan(&text, &r.Repository, &r.Commit, &r.Generation, &r.Build, &r.KeptAt)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return PredefinedRecord{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return PredefinedRecord{}, false, err
|
||||||
|
}
|
||||||
|
if r.Keys, err = catalogue.ParsePredefined(text); err != nil {
|
||||||
|
return PredefinedRecord{}, false, fmt.Errorf("the list kept from %s cannot be read: %w", r.Build, err)
|
||||||
|
}
|
||||||
|
return r, true, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package inventory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The list of predefined keys the controller keeps (novox/hq ADR 0348 §7 and its check "a newer trunk build's
|
||||||
|
// list replaces the published one, and an older build's list heard later does not"; design 57 package K2).
|
||||||
|
|
||||||
|
func listOf(t *testing.T, text string) catalogue.PredefinedList {
|
||||||
|
t.Helper()
|
||||||
|
l, err := catalogue.ParsePredefined([]byte(text))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestANewerBuildsListReplacesTheKeptOneAndAnOlderOneHeardLaterDoesNot(t *testing.T) {
|
||||||
|
inv := ForTest(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
if _, has, err := inv.PredefinedKept(ctx); err != nil || has {
|
||||||
|
t.Fatalf("a fresh store keeps a list: %v %v", has, err)
|
||||||
|
}
|
||||||
|
older := PredefinedRecord{Keys: listOf(t, `{"font-size": {"type": "number", "description": "points"}}`),
|
||||||
|
Repository: "novox/mesh-catalog", Commit: "aaaa", Generation: 100, Build: "b-older"}
|
||||||
|
newer := PredefinedRecord{Keys: listOf(t, `{"font-size": {"type": "number", "description": "points"},
|
||||||
|
"agent-managed-settings": {"type": "object", "description": "a JSON object", "trusted": true}}`),
|
||||||
|
Repository: "novox/mesh-catalog", Commit: "bbbb", Generation: 101, Build: "b-newer"}
|
||||||
|
|
||||||
|
if kept, err := inv.KeepPredefined(ctx, older); err != nil || !kept {
|
||||||
|
t.Fatalf("the first list was not kept: %v %v", kept, err)
|
||||||
|
}
|
||||||
|
if kept, err := inv.KeepPredefined(ctx, newer); err != nil || !kept {
|
||||||
|
t.Fatalf("a newer build's list did not replace the kept one: %v %v", kept, err)
|
||||||
|
}
|
||||||
|
// The older build's outcome heard after the newer one's.
|
||||||
|
if kept, err := inv.KeepPredefined(ctx, older); err != nil || kept {
|
||||||
|
t.Fatalf("an older build's list heard later replaced the newer one: %v %v", kept, err)
|
||||||
|
}
|
||||||
|
// Nor one of the same generation: the one kept stands.
|
||||||
|
same := newer
|
||||||
|
same.Keys, same.Build = older.Keys, "b-again"
|
||||||
|
if kept, err := inv.KeepPredefined(ctx, same); err != nil || kept {
|
||||||
|
t.Fatalf("a list of the same generation replaced the kept one: %v %v", kept, err)
|
||||||
|
}
|
||||||
|
got, has, err := inv.PredefinedKept(ctx)
|
||||||
|
if err != nil || !has {
|
||||||
|
t.Fatal(has, err)
|
||||||
|
}
|
||||||
|
if got.Build != "b-newer" || got.Commit != "bbbb" || got.Generation != 101 || len(got.Keys) != 2 ||
|
||||||
|
!got.Keys["agent-managed-settings"].Trusted {
|
||||||
|
t.Fatalf("kept %+v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A build that could not say its generation, or carried a wrong list, is never kept.
|
||||||
|
unordered := newer
|
||||||
|
unordered.Generation = 0
|
||||||
|
if kept, err := inv.KeepPredefined(ctx, unordered); err == nil || kept {
|
||||||
|
t.Fatalf("a list without a generation was kept: %v", err)
|
||||||
|
}
|
||||||
|
wrong := newer
|
||||||
|
wrong.Generation, wrong.Keys = 200, catalogue.PredefinedList{"accent": {Type: "object", Description: "x"}}
|
||||||
|
if kept, err := inv.KeepPredefined(ctx, wrong); err == nil || kept {
|
||||||
|
t.Fatalf("a wrong list was kept: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user