Raise the mesh as it is in the gate, call a baseline that does not compose an error, and let a check run by hand as the seat runs it
The gate composed 0 of 4 machines with the change and without, and passed every change: the store it raised held each module's bus credential but no account for it (issue 203's refusal), no outward links (so no filter could be composed), and refused settings the mesh holds. Now the account is minted with its credential, the facts carry each machine's outward links (a stand-in for an older snapshot), the mesh's layers are kept as held, and a withheld path keeps a path's shape. A machine the mesh composes that the gate cannot raise makes the verdict an error, never a pass; the verdict alone is on stdout. A merge-check.sh that passed on an agent's machine failed on the build seat: a newer gofmt, siblings at a feature branch, another user. `mesh-controller check-here` runs builder.Check with the ask the controller would make, from facts that now name the toolchains and the refs cloned beside; a failed script is said by what failed. (novox/hq issues 282, 283)
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/artifacts"
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
snapshot "github.com/novox/mesh-controller/internal/facts"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
@@ -103,9 +104,12 @@ type mergeVerdict struct {
|
||||
Verdict string `json:"verdict"` // pass, warning or fail
|
||||
Summary string `json:"summary"`
|
||||
// Judge is the controller build that judged it, and Facts when the snapshot it judged against was taken.
|
||||
Judge string `json:"judge"`
|
||||
Facts time.Time `json:"facts"`
|
||||
Failures []string `json:"failures,omitempty"`
|
||||
Judge string `json:"judge"`
|
||||
Facts time.Time `json:"facts"`
|
||||
Failures []string `json:"failures,omitempty"`
|
||||
// Errors are what kept the gate from judging: a machine the mesh composes that the gate could not
|
||||
// raise as it is. Any one makes the verdict an error — never a pass (novox/hq issue 282).
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Notes []string `json:"notes,omitempty"`
|
||||
Machines []mergeMachine `json:"machines"`
|
||||
@@ -182,11 +186,18 @@ func mergeGateCommand(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
out := io.Writer(os.Stdout)
|
||||
stdout := os.Stdout
|
||||
if *asJSON {
|
||||
out = os.Stderr
|
||||
// **The verdict is the only thing on standard output** when it is asked for as JSON: composition
|
||||
// says what it finds as it goes (a bus user without a credential, a module left out) and prints it,
|
||||
// and a line of that before the verdict makes the verdict unreadable — the build seat would read no
|
||||
// verdict at all. What is said goes beside the report.
|
||||
os.Stdout = os.Stderr
|
||||
}
|
||||
v, err := judgeChange(ctx, mergeCheckInput{facts: f, repository: *repository, tree: *tree, changed: splitList(*changed),
|
||||
group: others, admin: *admin, say: out})
|
||||
os.Stdout = stdout
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -201,12 +212,19 @@ func mergeGateCommand(ctx context.Context, args []string) error {
|
||||
} else {
|
||||
fmt.Print(v.Report())
|
||||
}
|
||||
if v.Verdict == "fail" {
|
||||
switch v.Verdict {
|
||||
case "fail":
|
||||
return errMergeGateFailed
|
||||
case "error":
|
||||
return errMergeGateCouldNotJudge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// errMergeGateCouldNotJudge is the gate's own error: the mesh as it is could not be raised, so nothing
|
||||
// the change does to it can be seen. Never a pass (novox/hq issue 282).
|
||||
var errMergeGateCouldNotJudge = errors.New("the merge gate could not judge the change")
|
||||
|
||||
// errMergeGateFailed is the gate's own failure: the verdict says why, so the error says nothing more.
|
||||
var errMergeGateFailed = errors.New("the change fails the merge gate")
|
||||
|
||||
@@ -333,10 +351,12 @@ func judgeChange(ctx context.Context, in mergeCheckInput) (mergeVerdict, error)
|
||||
v.Failures = append(v.Failures, fmt.Sprintf("%s: nothing could be sent to it with this change — %s",
|
||||
gm.Described, firstOr(newOnly(gm.Change.Problems, gm.Base.Problems), gm.Change.Problems)))
|
||||
case !gm.Base.Composes && m.Declaration.Composes:
|
||||
// The mesh composes it and the gate could not raise it as it is: a fact the snapshot does
|
||||
// not carry. Said, and judged by what the change adds.
|
||||
v.Notes = append(v.Notes, fmt.Sprintf("%s composes on the mesh and not as the snapshot raised it: %s — "+
|
||||
"judged by what the change adds", gm.Described, firstOr(gm.Base.Problems, nil)))
|
||||
// **The mesh composes it and the gate could not raise it as it is** (novox/hq issue 282): a fact
|
||||
// the snapshot does not carry, or one the gate does not raise. Then the change is judged against a
|
||||
// machine that is not the mesh's — broken against broken, which passes whatever the change does —
|
||||
// so the gate cannot judge, and says so: an error, never a pass.
|
||||
v.Errors = append(v.Errors, fmt.Sprintf("%s composes on the mesh and not as the gate raised it from the "+
|
||||
"snapshot: %s", gm.Described, firstOr(gm.Base.Problems, nil)))
|
||||
if added := newOnly(gm.Change.Problems, gm.Base.Problems); len(added) > 0 {
|
||||
v.Failures = append(v.Failures, fmt.Sprintf("%s: the change adds — %s", gm.Described, added[0]))
|
||||
}
|
||||
@@ -387,10 +407,16 @@ func judgeChange(ctx context.Context, in mergeCheckInput) (mergeVerdict, error)
|
||||
|
||||
sort.Strings(v.Failures)
|
||||
v.Failures = slices.Compact(v.Failures)
|
||||
sort.Strings(v.Errors)
|
||||
switch {
|
||||
case len(v.Failures) > 0:
|
||||
v.Verdict = "fail"
|
||||
v.Summary = fmt.Sprintf("%d problem(s) the change brings; the first: %s", len(v.Failures), v.Failures[0])
|
||||
case len(v.Errors) > 0:
|
||||
v.Verdict = "error"
|
||||
v.Summary = fmt.Sprintf("the mesh as it is could not be raised, so the change cannot be judged against it "+
|
||||
"(%d of %d machines compose on the mesh and not in the gate); the first: %s", len(v.Errors),
|
||||
len(v.Machines), v.Errors[0])
|
||||
case len(v.Warnings) > 0:
|
||||
v.Verdict = "warning"
|
||||
v.Summary = v.Warnings[0]
|
||||
@@ -423,6 +449,7 @@ func (v mergeVerdict) Report() string {
|
||||
}
|
||||
}
|
||||
list("fails", v.Failures)
|
||||
list("could not judge", v.Errors)
|
||||
list("warns", v.Warnings)
|
||||
if len(v.Modules) > 0 {
|
||||
var changed []string
|
||||
@@ -961,6 +988,18 @@ func raiseFromFacts(ctx context.Context, open *stores, f snapshot.Facts, shelf m
|
||||
return notes, err
|
||||
}
|
||||
}
|
||||
// The links it faces outside by, which a filter is written around (ADR 0140). A snapshot from a
|
||||
// controller that did not carry them stands in one for a machine the mesh composes: composing, it
|
||||
// had reported them.
|
||||
outward := m.OutwardLinks
|
||||
if len(outward) == 0 && m.Declaration.Composes {
|
||||
outward = []string{"outside0"}
|
||||
}
|
||||
if len(outward) > 0 {
|
||||
if err := inv.RecordOutwardLinks(ctx, node.ID, outward); err != nil {
|
||||
return notes, err
|
||||
}
|
||||
}
|
||||
if m.Account != "" {
|
||||
if err := inv.SetAccount(ctx, m.Name, m.Account, m.AccountHome); err != nil {
|
||||
return notes, err
|
||||
@@ -994,7 +1033,7 @@ func raiseFromFacts(ctx context.Context, open *stores, f snapshot.Facts, shelf m
|
||||
}
|
||||
}
|
||||
for _, s := range f.Settings {
|
||||
if err := inv.SetSettings(ctx, "", s.Module, s.Values); err != nil {
|
||||
if err := inv.KeepSettings(ctx, "", s.Module, standInPaths(s.Values)); err != nil {
|
||||
notes = append(notes, fmt.Sprintf("the mesh's settings of %s are not kept: %s", s.Module, oneLine(err.Error())))
|
||||
}
|
||||
}
|
||||
@@ -1006,13 +1045,31 @@ func raiseFromFacts(ctx context.Context, open *stores, f snapshot.Facts, shelf m
|
||||
}
|
||||
}
|
||||
for _, s := range m.Settings {
|
||||
if err := inv.SetSettings(ctx, m.Name, s.Module, s.Values); err != nil {
|
||||
if err := inv.KeepSettings(ctx, m.Name, s.Module, standInPaths(s.Values)); err != nil {
|
||||
notes = append(notes, fmt.Sprintf("%s's settings of %s are not kept: %s", m.Described(), s.Module,
|
||||
oneLine(err.Error())))
|
||||
}
|
||||
}
|
||||
for _, a := range m.Accepted {
|
||||
standIn := fmt.Sprintf("gate-stand-in-%x", sha256.Sum256([]byte(m.Name+a.Module+a.Name+a.Provider+a.Local)))[:40]
|
||||
if a.Provider == "" && a.Name == "broker" {
|
||||
// **The bus credential is an account, not a given value** (novox/hq issue 203): composition asks
|
||||
// whether one was issued for the module, so the raised store mints the account the mesh issued
|
||||
// as well as holding the sealed value — otherwise every machine running a module on the bus
|
||||
// fails to compose, with the change and without, and the gate compares broken to broken.
|
||||
// A credential the mesh still holds for a module that no longer reads one composes nothing,
|
||||
// so it is not raised.
|
||||
if !readsBusCredential(shelf, a.Module) {
|
||||
continue
|
||||
}
|
||||
if _, err := inv.MintBusPassword(ctx, inventory.BusUser{Kind: busKindOf(a.Module), Node: m.Name,
|
||||
Module: a.Module, Username: broker.Principal{Kind: broker.KindModule, Node: m.Name,
|
||||
Module: a.Module}.Username()}); err != nil {
|
||||
notes = append(notes, fmt.Sprintf("%s's bus account for %s is not kept: %s", m.Described(),
|
||||
a.Module, oneLine(err.Error())))
|
||||
continue
|
||||
}
|
||||
}
|
||||
var err error
|
||||
if a.Provider == "" {
|
||||
err = inv.AcceptSecretForModule(ctx, m.Name, a.Module, a.Name, standIn)
|
||||
@@ -1028,6 +1085,64 @@ func raiseFromFacts(ctx context.Context, open *stores, f snapshot.Facts, shelf m
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
// standInPaths is a settings layer with every value the snapshot withheld where a path stands — an
|
||||
// access's or a place's — given a path of its own: a path that reads as a key is withheld by the scrub,
|
||||
// and a bare "withheld" is no absolute path, which composition refuses (a machine the mesh composes
|
||||
// would not compose as the gate raised it). Each stand-in is distinct, so two withheld places stay two.
|
||||
func standInPaths(values map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(values))
|
||||
for k, v := range values {
|
||||
out[k] = v
|
||||
}
|
||||
if accesses, ok := values["accesses"].(map[string]any); ok {
|
||||
kept := make(map[string]any, len(accesses))
|
||||
for name, at := range accesses {
|
||||
if withheldPath(at) {
|
||||
at = "/" + snapshot.Withheld + "/access/" + name
|
||||
}
|
||||
kept[name] = at
|
||||
}
|
||||
out["accesses"] = kept
|
||||
}
|
||||
if places, ok := values["places"].(map[string]any); ok {
|
||||
kept := make(map[string]any, len(places))
|
||||
for name, p := range places {
|
||||
if place, ok := p.(map[string]any); ok && withheldPath(place["path"]) {
|
||||
copied := make(map[string]any, len(place))
|
||||
for k, v := range place {
|
||||
copied[k] = v
|
||||
}
|
||||
copied["path"] = "/" + snapshot.Withheld + "/place/" + name
|
||||
p = copied
|
||||
}
|
||||
kept[name] = p
|
||||
}
|
||||
out["places"] = kept
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// withheldPath is a value the scrub withheld where a path stood: bare, or keeping a path's shape.
|
||||
func withheldPath(v any) bool {
|
||||
return v == snapshot.Withheld || v == "/"+snapshot.Withheld
|
||||
}
|
||||
|
||||
// readsBusCredential is whether a module of the shelf, or one that came with the controller, declares the
|
||||
// own secret its bus account is delivered as.
|
||||
func readsBusCredential(shelf map[string]catalogue.Manifest, module string) bool {
|
||||
if m, held := shelf[module]; held {
|
||||
_, reads := m.OwnSecrets["broker"]
|
||||
return reads
|
||||
}
|
||||
for _, m := range provided {
|
||||
if m.Module == module {
|
||||
_, reads := m.OwnSecrets["broker"]
|
||||
return reads
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// standInKey is a key a machine could have reported; its private half is never kept.
|
||||
func standInKey() (string, error) {
|
||||
k, err := ecdh.X25519().GenerateKey(rand.Reader)
|
||||
@@ -1057,12 +1172,21 @@ func reachOfChange(f snapshot.Facts, repository string, paths []string, tree str
|
||||
}
|
||||
}
|
||||
}
|
||||
entries, read, edges, err := graphOfFacts(f)
|
||||
if err != nil {
|
||||
return mergeReach{}, err
|
||||
}
|
||||
return reachOfMerge(m, entries, read, edges), nil
|
||||
}
|
||||
|
||||
// graphOfFacts is the module graph the snapshot carries, as the planner reads it from the store.
|
||||
func graphOfFacts(f snapshot.Facts) ([]inventory.Entry, map[string][]inventory.ReadRepository, []inventory.Edge, error) {
|
||||
var entries []inventory.Entry
|
||||
read := map[string][]inventory.ReadRepository{}
|
||||
for _, mod := range f.Modules {
|
||||
var manifest catalogue.Manifest
|
||||
if err := json.Unmarshal(mod.Manifest, &manifest); err != nil {
|
||||
return mergeReach{}, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
entries = append(entries, inventory.Entry{Manifest: manifest, Provided: mod.Provided,
|
||||
Source: inventory.Source{Repository: mod.Repository, Path: mod.Path, BuiltFrom: mod.Commit}})
|
||||
@@ -1074,7 +1198,7 @@ func reachOfChange(f snapshot.Facts, repository string, paths []string, tree str
|
||||
for _, e := range f.Edges {
|
||||
edges = append(edges, inventory.Edge{From: e.From, To: e.To, Kind: e.Kind})
|
||||
}
|
||||
return reachOfMerge(m, entries, read, edges), nil
|
||||
return entries, read, edges, nil
|
||||
}
|
||||
|
||||
// widthOf is how wide the rebuild of a reach is.
|
||||
|
||||
Reference in New Issue
Block a user