Write each module's have and the node's own facts beside the report, grouped per module and keyed by the module the controller names (issue 1132)
The node-engine reads a resource's module and a declaration's builds, says it does in every report (reads_modules), groups the apply's outcome per module with every failing resource named, and writes have.<node>.<module> and have.<node>.node to the bucket only when a value differs, never ahead of or in place of the report (novox/hq ADR 0429 section 4).
This commit is contained in:
+27
-13
@@ -179,6 +179,23 @@ type Error struct {
|
||||
// (novox/hq ADR 0136) or a directory the engine refused (issue 495). Named, because "everything
|
||||
// was attempted" is then untrue, and a report that says so after skipping a module lies.
|
||||
Modules []string
|
||||
|
||||
// Failures is every resource that failed, the first among them, each with its error (issue 1132, novox/hq ADR
|
||||
// 0429 §4): Others counts the rest and names none, and a module's state on the bus names what failed in it.
|
||||
Failures []Failure
|
||||
}
|
||||
|
||||
// Failure is one resource that failed in an apply, and why.
|
||||
type Failure struct {
|
||||
Resource string
|
||||
Err error
|
||||
}
|
||||
|
||||
// ModuleResult is how one module's part of an apply ended (issue 1132).
|
||||
type ModuleResult struct {
|
||||
Failed bool
|
||||
// Detail names each failing resource with its error, or why the module was not attempted.
|
||||
Detail string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
@@ -747,6 +764,7 @@ func ApplyMindingWindows(
|
||||
first := failures[0]
|
||||
first.Done = report
|
||||
first.Others = len(failures) - 1
|
||||
first.Failures = failuresOf(failures)
|
||||
first.Modules = gatedModules()
|
||||
log(" kept " + guardPrefix + "*: the guard is not up, so what it replaces was left in force")
|
||||
return report, known, first
|
||||
@@ -758,7 +776,7 @@ func ApplyMindingWindows(
|
||||
// **A module whose step did not complete is skipped from there on** (novox/hq ADR 0136).
|
||||
// Reported rather than passed over in silence: "not attempted" and "nothing to do" are
|
||||
// different answers, and only one of them is somebody's to fix.
|
||||
if module, ours := moduleOf(resource.Identity()); ours && gated[module] {
|
||||
if module, ours := d.ModuleOfID(resource.Identity()); ours && gated[module] {
|
||||
report.Outcomes = append(report.Outcomes, Outcome{
|
||||
ID: resource.Identity(), Type: string(resource.Kind()), Target: resource.Target(),
|
||||
Action: "skipped", Detail: gatedWhy[module],
|
||||
@@ -920,7 +938,7 @@ func ApplyMindingWindows(
|
||||
in.refused[filepath.Clean(dir.Path)] = err
|
||||
}
|
||||
if dir, ok := resource.(*declaration.Directory); ok && in.refused[filepath.Clean(dir.Path)] != nil {
|
||||
if module, ours := moduleOf(resource.Identity()); ours {
|
||||
if module, ours := d.ModuleOfID(resource.Identity()); ours {
|
||||
gated[module] = true
|
||||
gatedWhy[module] = fmt.Sprintf("its directory %s was refused (novox/hq issue 495), so "+
|
||||
"nothing more of this module was attempted", filepath.Clean(dir.Path))
|
||||
@@ -969,7 +987,7 @@ func ApplyMindingWindows(
|
||||
//
|
||||
// An action still gates the machine: the bootstrap is a row of them, each making the
|
||||
// next possible, and they belong to no module.
|
||||
if module, ours := moduleOf(resource.Identity()); ours {
|
||||
if module, ours := d.ModuleOfID(resource.Identity()); ours {
|
||||
gated[module] = true
|
||||
gatedWhy[module] = "a step this module declares did not complete"
|
||||
log(fmt.Sprintf(" gated %s.*: a step it declares did not complete, so the rest "+
|
||||
@@ -979,6 +997,7 @@ func ApplyMindingWindows(
|
||||
failed.Stopped = true
|
||||
failed.Done = report
|
||||
failed.Others = len(failures) - 1
|
||||
failed.Failures = failuresOf(failures)
|
||||
failed.Modules = gatedModules()
|
||||
return report, known, failed
|
||||
}
|
||||
@@ -1162,6 +1181,7 @@ func ApplyMindingWindows(
|
||||
first := failures[0]
|
||||
first.Done = report
|
||||
first.Others = len(failures) - 1
|
||||
first.Failures = failuresOf(failures)
|
||||
first.Modules = gatedModules()
|
||||
return report, known, first
|
||||
}
|
||||
@@ -3696,16 +3716,10 @@ func unitKey(scope, user, unit string) string {
|
||||
// False for what the mesh declares in its own right: the foundation's resources carry no dot at all,
|
||||
// and the adoption's are named for the mesh rather than for a module. Both belong to no module, and
|
||||
// their gate is therefore the machine's.
|
||||
func moduleOf(identity string) (string, bool) {
|
||||
if strings.HasPrefix(identity, declaration.AdoptionPrefix) {
|
||||
return "", false
|
||||
}
|
||||
at := strings.LastIndex(identity, ".")
|
||||
if at <= 0 {
|
||||
return "", false
|
||||
}
|
||||
return identity[:at], true
|
||||
}
|
||||
//
|
||||
// This is the guess from an id alone, for a caller with no declaration in hand; a caller with one asks
|
||||
// Declaration.ModuleOfID, which reads the module the controller said (issue 1132).
|
||||
func moduleOf(identity string) (string, bool) { return declaration.ModuleGuess(identity) }
|
||||
|
||||
// handOver removes what a process replaces, once the process is running and still is a moment later
|
||||
// (novox/hq issue 213, ADR 0184). A replacement that is not up keeps what it replaces in place
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// failuresOf is every failure of an apply as the error carries it: the first among them, then the rest, so a module
|
||||
// whose resource failed second is still named (issue 1132, and issue 500, which counted the rest and named none).
|
||||
func failuresOf(failures []*Error) []Failure {
|
||||
out := make([]Failure, 0, len(failures))
|
||||
for _, f := range failures {
|
||||
out = append(out, Failure{Resource: f.Resource, Err: f.Err})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ModuleResults groups an apply's outcome per module the declaration names (novox/hq ADR 0429 §4, issue 1132): the
|
||||
// module's resources all applied, or it failed and Detail says which resource failed with its error, which resources
|
||||
// were skipped behind a step that did not complete, or that its resources were not attempted because a step of no
|
||||
// module stopped the apply. A module is named by what the declaration says of each resource (ModuleOfID), so one
|
||||
// whose id would guess another is still its own.
|
||||
//
|
||||
// A failure that is no resource's (an error that is not an Error, or a resource of no module) belongs to no module and
|
||||
// changes none: the node says it in its report. The report is the outcome of an apply that did not fail; an apply that
|
||||
// failed carries its own account in the Error, and that is read in its place.
|
||||
func ModuleResults(d *declaration.Declaration, report Report, err error) map[string]ModuleResult {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
out := map[string]ModuleResult{}
|
||||
attempted := map[string]bool{}
|
||||
var failed *Error
|
||||
errors.As(err, &failed)
|
||||
outcomes := report.Outcomes
|
||||
if failed != nil {
|
||||
outcomes = append(append([]Outcome(nil), outcomes...), failed.Done.Outcomes...)
|
||||
}
|
||||
skipped := map[string]string{}
|
||||
for _, o := range outcomes {
|
||||
module, ok := d.ModuleOfID(o.ID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if o.Action == "skipped" {
|
||||
skipped[module] = o.Detail
|
||||
continue
|
||||
}
|
||||
attempted[o.ID] = true
|
||||
}
|
||||
var reasons = map[string][]string{}
|
||||
for _, r := range d.Resources {
|
||||
if module, ok := d.ModuleOfID(r.Identity()); ok {
|
||||
out[module] = ModuleResult{}
|
||||
if failed != nil && failed.Stopped && !attempted[r.Identity()] {
|
||||
reasons[module] = append(reasons[module], fmt.Sprintf("%s was not attempted: %s stopped the apply", r.Identity(), failed.Resource))
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed != nil {
|
||||
for _, f := range failed.Failures {
|
||||
if module, ok := d.ModuleOfID(f.Resource); ok {
|
||||
if _, named := out[module]; named {
|
||||
reasons[module] = append(reasons[module], fmt.Sprintf("%s failed: %v", f.Resource, f.Err))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failed.Failures) == 0 {
|
||||
if module, ok := d.ModuleOfID(failed.Resource); ok {
|
||||
if _, named := out[module]; named {
|
||||
reasons[module] = append(reasons[module], fmt.Sprintf("%s failed: %v", failed.Resource, failed.Err))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, module := range failed.Modules {
|
||||
if _, named := out[module]; named {
|
||||
why := skipped[module]
|
||||
if why == "" {
|
||||
why = "a step it declares did not complete"
|
||||
}
|
||||
reasons[module] = append(reasons[module], "gated: "+why)
|
||||
}
|
||||
}
|
||||
}
|
||||
for module, why := range skipped {
|
||||
if _, named := out[module]; named && len(reasons[module]) == 0 {
|
||||
reasons[module] = append(reasons[module], "gated: "+why)
|
||||
}
|
||||
}
|
||||
for module, why := range reasons {
|
||||
sort.Strings(why)
|
||||
out[module] = ModuleResult{Failed: true, Detail: strings.Join(why, "; ")}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
func twoModules(t *testing.T) *declaration.Declaration {
|
||||
t.Helper()
|
||||
d, err := declaration.Parse([]byte(`{"declaration":1,"builds":{"a":"b-1","b":"b-2"},"resources":[
|
||||
{"id":"a.dir","type":"directory","path":"/srv/a","module":"a"},
|
||||
{"id":"b.dir","type":"directory","path":"/srv/b","module":"b"},
|
||||
{"id":"b.conf","type":"file","path":"/srv/b/c","content":"x","module":"b"},
|
||||
{"id":"c.dir","type":"directory","path":"/srv/c","module":"c"}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// 1132/AC3: every resource of module A applied and one of module B failed: A is a success, B failed with the failing
|
||||
// resource and its error named; a module gated by another's failure is failed too.
|
||||
func TestAnApplyIsGroupedPerModule(t *testing.T) {
|
||||
d := twoModules(t)
|
||||
done := Report{Outcomes: []Outcome{
|
||||
{ID: "a.dir", Action: "created"},
|
||||
{ID: "c.dir", Action: "skipped", Detail: "a step this module declares did not complete"},
|
||||
}}
|
||||
err := &Error{Resource: "b.dir", Err: errors.New("no space left"), Done: done, Others: 1,
|
||||
Failures: []Failure{{Resource: "b.dir", Err: errors.New("no space left")}, {Resource: "b.conf", Err: errors.New("denied")}},
|
||||
Modules: []string{"c"}}
|
||||
got := ModuleResults(d, Report{}, err)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("modules %v, want a, b and c", got)
|
||||
}
|
||||
if got["a"].Failed || got["a"].Detail != "" {
|
||||
t.Errorf("a: %+v, want a clean success", got["a"])
|
||||
}
|
||||
b := got["b"]
|
||||
if !b.Failed || !strings.Contains(b.Detail, "b.dir") || !strings.Contains(b.Detail, "no space left") ||
|
||||
!strings.Contains(b.Detail, "b.conf") || !strings.Contains(b.Detail, "denied") {
|
||||
t.Errorf("b: %+v, want both failing resources and their errors", b)
|
||||
}
|
||||
c := got["c"]
|
||||
if !c.Failed || !strings.Contains(c.Detail, "did not complete") {
|
||||
t.Errorf("c: %+v, want it failed as gated, with why", c)
|
||||
}
|
||||
}
|
||||
|
||||
// 1132/AC3: an apply that did not fail is a success for every module the declaration names.
|
||||
func TestACleanApplyIsASuccessForEveryModule(t *testing.T) {
|
||||
got := ModuleResults(twoModules(t), Report{}, nil)
|
||||
for _, m := range []string{"a", "b", "c"} {
|
||||
if r, ok := got[m]; !ok || r.Failed {
|
||||
t.Errorf("%s: %+v (%v), want a success", m, r, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1132/AC3: a step that belongs to no module stops the apply there; a module with a resource not attempted is not a
|
||||
// success.
|
||||
func TestAModuleNotAttemptedIsNotASuccess(t *testing.T) {
|
||||
d := twoModules(t)
|
||||
err := &Error{Resource: "boot", Err: errors.New("boom"), Stopped: true, Failures: []Failure{{Resource: "boot", Err: errors.New("boom")}},
|
||||
Done: Report{Outcomes: []Outcome{{ID: "a.dir", Action: "created"}}}}
|
||||
got := ModuleResults(d, Report{}, err)
|
||||
if got["a"].Failed {
|
||||
t.Errorf("a was done: %+v", got["a"])
|
||||
}
|
||||
if !got["b"].Failed || !strings.Contains(got["b"].Detail, "boot") {
|
||||
t.Errorf("b was not attempted: %+v", got["b"])
|
||||
}
|
||||
}
|
||||
@@ -350,7 +350,7 @@ func moveOutPass(d *declaration.Declaration, known store.State, applied map[stri
|
||||
if unknown != declaration.UnknownMoveOut || !applied[r.Identity()] {
|
||||
continue
|
||||
}
|
||||
module, _ := moduleOf(r.Identity())
|
||||
module, _ := d.ModuleOfID(r.Identity())
|
||||
if module == "" {
|
||||
module = r.Identity()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user