Apply one module at a time: a scope of the module and what the others hold, and an owner record, so a removal is the module's own (R6-A A3, A4)
This commit is contained in:
+89
-9
@@ -300,6 +300,60 @@ func ApplyMindingWindows(
|
||||
unseal Unseal,
|
||||
keep Keep,
|
||||
windows *Windows,
|
||||
) (Report, store.State, error) {
|
||||
return applyScoped(ctx, sys, d, nil, known, origin, run, log, unseal, keep, windows)
|
||||
}
|
||||
|
||||
// Scope is what makes an apply one module's (novox/hq ADR 0429 §4, ADR 0539 §9): the module whose want changed, and
|
||||
// the resources the node's other modules hold, so a path, a unit or a group another module still asks for is neither
|
||||
// removed nor taken from it. Only that module's own records are orphans; a removed module is a Scope with an empty
|
||||
// declaration.
|
||||
type Scope struct {
|
||||
// Module is the module applied. NodeModule (store.NodeModule) is the node's own composition.
|
||||
Module string
|
||||
// Others are the resources of every other module the node has a composition for.
|
||||
Others []declaration.Resource
|
||||
}
|
||||
|
||||
// owns reports whether a record belongs to the scope's module.
|
||||
func (sc *Scope) owns(a store.Applied) bool {
|
||||
return store.OwnerOf(a, declaration.ModuleGuess) == sc.Module
|
||||
}
|
||||
|
||||
// isNode is whether the scope is the node's own composition, the one that carries identities, groups, libraries and
|
||||
// the adoption.
|
||||
func (sc *Scope) isNode() bool { return sc == nil || sc.Module == store.NodeModule }
|
||||
|
||||
// ApplyModule makes the machine match one module's declaration, and only that module's: everything else the node
|
||||
// runs is left as it is. d holds the module's resources; scope names the module and what the others hold.
|
||||
func ApplyModule(
|
||||
ctx context.Context,
|
||||
sys system.System,
|
||||
d *declaration.Declaration,
|
||||
scope Scope,
|
||||
known store.State,
|
||||
origin string,
|
||||
run Runner,
|
||||
log func(string),
|
||||
unseal Unseal,
|
||||
keep Keep,
|
||||
windows *Windows,
|
||||
) (Report, store.State, error) {
|
||||
return applyScoped(ctx, sys, d, &scope, known, origin, run, log, unseal, keep, windows)
|
||||
}
|
||||
|
||||
func applyScoped(
|
||||
ctx context.Context,
|
||||
sys system.System,
|
||||
d *declaration.Declaration,
|
||||
scope *Scope,
|
||||
known store.State,
|
||||
origin string,
|
||||
run Runner,
|
||||
log func(string),
|
||||
unseal Unseal,
|
||||
keep Keep,
|
||||
windows *Windows,
|
||||
) (report Report, _ store.State, _ error) {
|
||||
// Said whichever way the apply ended, failure included: a window being open is a fact about the
|
||||
// machine, not about this apply.
|
||||
@@ -326,7 +380,9 @@ func ApplyMindingWindows(
|
||||
}
|
||||
// What an unfinished apply found a unit as is kept only while its service is declared: one
|
||||
// declared again later is read afresh, as any resource with no record is (novox/hq ADR 0118).
|
||||
known.DropFoundUndeclared(declared, origin)
|
||||
if scope == nil {
|
||||
known.DropFoundUndeclared(declared, origin)
|
||||
}
|
||||
|
||||
// Which units the mesh made, read before any removal can take a unit file's record away — so
|
||||
// whichever order a module declared a unit and its file in, the unit is known for the mesh's
|
||||
@@ -351,7 +407,11 @@ func ApplyMindingWindows(
|
||||
// it back would stop or disable a unit the declaration says is running, every container on it
|
||||
// with it, only for the declared one to start it again in the same apply. The record is
|
||||
// forgotten; the unit's found state is the remaining record's to give back.
|
||||
heldUnits := unitsHeld(d.Resources)
|
||||
everything := d.Resources
|
||||
if scope != nil {
|
||||
everything = append(append([]declaration.Resource{}, d.Resources...), scope.Others...)
|
||||
}
|
||||
heldUnits := unitsHeld(everything)
|
||||
|
||||
// **A whole file still declared at its path by another resource is handed to it, never removed
|
||||
// first** (novox/hq ADR 0223). A file can change owners between modules — the machine's resolver
|
||||
@@ -361,13 +421,13 @@ func ApplyMindingWindows(
|
||||
// and stayed so until the new resource's turn came, with every resource in between applied on a
|
||||
// machine without it. The record going is kept until the one declaring the path now is recorded
|
||||
// in its place: then it is forgotten, and what it kept of the original goes with the file.
|
||||
heldFiles := filesHeld(d.Resources)
|
||||
heldFiles := filesHeld(everything)
|
||||
handedFiles := map[string]store.Applied{}
|
||||
|
||||
// **A group still asked for by another resource is not given back when one record of it goes**
|
||||
// (novox/hq ADR 0252), on the unit's rule above: two modules may each want the account in one group,
|
||||
// and the one going takes nothing the other still needs.
|
||||
wanted := groupsWanted(d.Resources)
|
||||
wanted := groupsWanted(everything)
|
||||
// And what a record going hands to a resource that still asks for its group (novox/hq ADR 0252 §3).
|
||||
handed := Handed{}
|
||||
|
||||
@@ -375,6 +435,18 @@ func ApplyMindingWindows(
|
||||
var action, detail string
|
||||
var err error
|
||||
if by, held := heldFiles[fileKey(orphan)]; held && by != orphan.ID {
|
||||
if scope != nil && !declared[by] {
|
||||
// Another module declares the path: it is that module's, so this record is let go and the file is
|
||||
// neither removed nor taken from it (ADR 0539 §9).
|
||||
known.Forget(orphan.ID)
|
||||
detail := "no longer declared; " + by + " of another module still holds the path, so it was left as it is"
|
||||
report.Outcomes = append(report.Outcomes, Outcome{
|
||||
ID: orphan.ID, Type: orphan.Type, Target: orphan.Target,
|
||||
Action: "forgotten", Detail: detail,
|
||||
})
|
||||
log(fmt.Sprintf(" forgotten %s (%s): %s", orphan.ID, orphan.Target, detail))
|
||||
return nil
|
||||
}
|
||||
handedFiles[by] = orphan
|
||||
return nil
|
||||
}
|
||||
@@ -482,6 +554,9 @@ func ApplyMindingWindows(
|
||||
}
|
||||
handover := map[string][]store.Applied{}
|
||||
for _, orphan := range known.Orphans(declared, origin) {
|
||||
if scope != nil && !scope.owns(orphan) {
|
||||
continue
|
||||
}
|
||||
if d.Adoption == nil && strings.HasPrefix(orphan.ID, declaration.AdoptionPrefix) {
|
||||
protecting = append(protecting, orphan)
|
||||
continue
|
||||
@@ -538,7 +613,7 @@ func ApplyMindingWindows(
|
||||
// what is assigned: a carried bundle's silence is not an unassignment.
|
||||
if origin == store.OriginDeclared {
|
||||
for _, h := range append([]store.Held{}, known.Held...) {
|
||||
if declared[h.ID] {
|
||||
if declared[h.ID] || (scope != nil && h.Module != scope.Module) {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(d.LeftOut, h.Module) {
|
||||
@@ -745,7 +820,7 @@ func ApplyMindingWindows(
|
||||
in.held = heldRoots(verdicts, d.Libraries)
|
||||
// A tool runner no longer put in a group leaves it before anything is applied, so its unit, which names the
|
||||
// change under restart-on, is restarted in this same apply to lose it (ADR 0328 §11, ADR 0334 §2, ADR 0252 §3).
|
||||
if origin == store.OriginDeclared {
|
||||
if origin == store.OriginDeclared && scope.isNode() {
|
||||
for _, o := range leaveRunners(ctx, sys, d.Libraries, d.Identities, &known, run, wanted) {
|
||||
if o.Action == "restored" {
|
||||
changed[o.changes] = true
|
||||
@@ -1037,6 +1112,11 @@ func ApplyMindingWindows(
|
||||
}
|
||||
// Only now. The record follows the fact, never leads it.
|
||||
recordApplied(&known, origin, resource, outcome, kept, keptMode, keptOwner)
|
||||
if scope != nil {
|
||||
known.SetModule(resource.Identity(), scope.Module)
|
||||
} else if module, ours := d.ModuleOfID(resource.Identity()); ours {
|
||||
known.SetModule(resource.Identity(), module)
|
||||
}
|
||||
appliedOK[resource.Identity()] = true
|
||||
// Its module has been taken, and what was held for it is now the mesh's. A file written
|
||||
// into, or a service whose lifecycle is the machine's, replaced nothing that was found, so
|
||||
@@ -1139,7 +1219,7 @@ func ApplyMindingWindows(
|
||||
}
|
||||
|
||||
// Only the mesh says what is assigned: a carried bundle's silence undeclares no identity and no folder.
|
||||
if origin == store.OriginDeclared {
|
||||
if origin == store.OriginDeclared && scope.isNode() {
|
||||
// And an identity, then a library group, no longer declared goes, once what ran as it went above (ADR 0328
|
||||
// §3, §4).
|
||||
if tool, err := toolFor(sys); err == nil {
|
||||
@@ -1158,7 +1238,7 @@ func ApplyMindingWindows(
|
||||
// the mesh's derived filter among it — applied cleanly (novox/hq ADR 0100), and on every
|
||||
// converged apply, not once (ADR 0168). Skipped, it is said: a step that does nothing is never
|
||||
// silent (issue 143).
|
||||
if len(failures) == 0 {
|
||||
if len(failures) == 0 && scope.isNode() {
|
||||
did, err := retireFirewall(ctx, d, origin, &known, run, log)
|
||||
if err != nil {
|
||||
return report, known, &Error{Resource: "the firewall found on this machine", Err: err, Done: report}
|
||||
@@ -1169,7 +1249,7 @@ func ApplyMindingWindows(
|
||||
return report, known, err
|
||||
}
|
||||
}
|
||||
} else if rec := known.Firewall; origin == store.OriginDeclared && d.Adoption == nil && rec != nil &&
|
||||
} else if rec := known.Firewall; len(failures) > 0 && scope.isNode() && origin == store.OriginDeclared && d.Adoption == nil && rec != nil &&
|
||||
rec.Kind == string(firewall.UFW) && rec.WasActive && firewall.Active(ctx, run) {
|
||||
report.Firewall = fmt.Sprintf("left in force: %d resource(s) failed, and the found firewall is retired only after a clean apply", len(failures))
|
||||
log(" kept ufw in force: " + report.Firewall)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// R6-A AC3: given two modules declaring one path, when one module's want changes, the shared path is neither removed
|
||||
// nor taken from the other module (novox/hq ADR 0539 §9).
|
||||
func TestASharedPathIsNeitherRemovedNorTakenFromTheOtherModule(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
shared := filepath.Join(dir, "shared.conf")
|
||||
own := filepath.Join(dir, "a-own.conf")
|
||||
a := declare(t, `{"id":"a.shared","type":"file","path":"`+shared+`","content":"x\n","module":"a"},
|
||||
{"id":"a.own","type":"file","path":"`+own+`","content":"a\n","module":"a"}`)
|
||||
b := declare(t, `{"id":"b.shared","type":"file","path":"`+shared+`","content":"x\n","module":"b"}`)
|
||||
|
||||
state := store.State{}
|
||||
var err error
|
||||
_, state, err = ApplyModule(context.Background(), archHost(t), a, Scope{Module: "a", Others: b.Resources},
|
||||
state, store.OriginDeclared, noServices, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, state, err = ApplyModule(context.Background(), archHost(t), b, Scope{Module: "b", Others: a.Resources},
|
||||
state, store.OriginDeclared, noServices, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A's want changes: it no longer declares the shared path, and b still does.
|
||||
a2 := declare(t, `{"id":"a.own","type":"file","path":"`+own+`","content":"a\n","module":"a"}`)
|
||||
report, state, err := ApplyModule(context.Background(), archHost(t), a2, Scope{Module: "a", Others: b.Resources},
|
||||
state, store.OriginDeclared, noServices, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists(shared) {
|
||||
t.Fatal("the path another module still declares was removed")
|
||||
}
|
||||
if _, still := state.Find("b.shared"); !still {
|
||||
t.Fatal("the other module's record was taken away")
|
||||
}
|
||||
if _, still := state.Find("a.shared"); still {
|
||||
t.Fatal("the module's own record of the shared path was kept")
|
||||
}
|
||||
if o := outcomeOf(report, "a.shared"); o.Action != "forgotten" {
|
||||
t.Fatalf("the shared path: %+v", o)
|
||||
}
|
||||
}
|
||||
|
||||
// R6-A AC4: given modules A and B on a node, when A's want is removed, only A's resources are removed and B is
|
||||
// untouched.
|
||||
func TestARemovedModuleTakesOnlyItsOwnResources(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fa, fb := filepath.Join(dir, "a.conf"), filepath.Join(dir, "b.conf")
|
||||
d := declare(t, `{"id":"a.conf","type":"file","path":"`+fa+`","content":"a\n","module":"a"},
|
||||
{"id":"b.conf","type":"file","path":"`+fb+`","content":"b\n","module":"b"}`)
|
||||
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec, _ := state.Find("a.conf"); rec.Module != "a" {
|
||||
t.Fatalf("the owner was not recorded: %+v", rec)
|
||||
}
|
||||
|
||||
gone := &declaration.Declaration{}
|
||||
b := declare(t, `{"id":"b.conf","type":"file","path":"`+fb+`","content":"b\n","module":"b"}`)
|
||||
report, state, err := ApplyModule(context.Background(), archHost(t), gone, Scope{Module: "a", Others: b.Resources},
|
||||
state, store.OriginDeclared, noServices, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exists(fa) || !exists(fb) {
|
||||
t.Fatalf("a's file exists %v, b's file exists %v", exists(fa), exists(fb))
|
||||
}
|
||||
if o := outcomeOf(report, "a.conf"); o.Action != "removed" {
|
||||
t.Fatalf("a's file: %+v", o)
|
||||
}
|
||||
if _, still := state.Find("b.conf"); !still {
|
||||
t.Fatal("b's record was removed")
|
||||
}
|
||||
if _, still := state.Find("a.conf"); still {
|
||||
t.Fatal("a's record is still there")
|
||||
}
|
||||
}
|
||||
|
||||
// A record from before the owner was kept is read by its id, so the first removal after the switch is the module's too.
|
||||
func TestARecordWithoutAnOwnerBelongsToTheModuleItsIdNames(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fa, fb := filepath.Join(dir, "a.conf"), filepath.Join(dir, "b.conf")
|
||||
d := declare(t, `{"id":"a.conf","type":"file","path":"`+fa+`","content":"a\n"},
|
||||
{"id":"b.conf","type":"file","path":"`+fb+`","content":"b\n"}`)
|
||||
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range state.Resources {
|
||||
state.Resources[i].Module = ""
|
||||
}
|
||||
_, state, err = ApplyModule(context.Background(), archHost(t), &declaration.Declaration{}, Scope{Module: "a"},
|
||||
state, store.OriginDeclared, noServices, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exists(fa) || !exists(fb) {
|
||||
t.Fatalf("a's file exists %v, b's file exists %v", exists(fa), exists(fb))
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,10 @@ const DefaultPath = "/var/lib/mesh-host/state.json"
|
||||
type Applied struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
// Module is the module whose want this resource was applied for (novox/hq ADR 0429 §4, ADR 0539 §9): the owner
|
||||
// record that limits a removal to the module whose want changed. Absent on a record from before; then the
|
||||
// module is read from the id (OwnerOf).
|
||||
Module string `json:"module,omitempty"`
|
||||
// Origin is who asked for this: the bundle this host carries, or the mesh.
|
||||
//
|
||||
// Recorded because the two must not remove each other. A node raises its own foundation from
|
||||
@@ -680,6 +684,33 @@ func (s State) Orphans(declared map[string]bool, origin string) []Applied {
|
||||
return out
|
||||
}
|
||||
|
||||
// NodeModule is the owner of what belongs to no module: the node's own resources (adoption, firewall openings,
|
||||
// accounts, the resolver, identities and groups; novox/hq ADR 0539 §2).
|
||||
const NodeModule = "node"
|
||||
|
||||
// OwnerOf is the module a record belongs to: the one recorded with it, else the one its id names, else the node.
|
||||
// guess reads a module from an id (declaration.ModuleGuess), kept as a parameter so the store imports nothing.
|
||||
func OwnerOf(a Applied, guess func(id string) (string, bool)) string {
|
||||
if a.Module != "" {
|
||||
return a.Module
|
||||
}
|
||||
if guess != nil {
|
||||
if m, ok := guess(a.ID); ok {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return NodeModule
|
||||
}
|
||||
|
||||
// SetModule records which module a resource was applied for.
|
||||
func (s *State) SetModule(id, module string) {
|
||||
for i := range s.Resources {
|
||||
if s.Resources[i].ID == id {
|
||||
s.Resources[i].Module = module
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Origins a resource can have.
|
||||
const (
|
||||
// Carried is the bundle this host was built with.
|
||||
|
||||
Reference in New Issue
Block a user