diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 25e555c7..a1ff719a 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -578,6 +578,17 @@ func applyScoped( } if declaration.Type(orphan.Type) == declaration.TypeService { if by, held := heldUnits[unitKey(orphan.Scope, orphan.User, orphan.Target)]; held { + // **What the going record found passes to the record that stays** (design 42 zfs-owns-zfs §2, answer + // A-21), as a file is handed to the resource still declaring its path (ADR 0223): the unit as the machine + // had it before the mesh is what the staying record gives back when it goes, not what the mesh made of + // it. Only where the staying record has no finding of its own, kept as an unfinished apply's is. + if orphan.Found != nil && declared[by] { + if rec, has := known.Find(by); !has || rec.Found == nil { + if _, pending := known.FoundFirst[by]; !pending { + known.KeepFound(by, origin, *orphan.Found) + } + } + } known.Forget(orphan.ID) detail := "no longer declared; " + by + " still holds the unit, so it was left as it is" report.Outcomes = append(report.Outcomes, Outcome{ diff --git a/internal/apply/into_handover_test.go b/internal/apply/into_handover_test.go index 30283519..fc533904 100644 --- a/internal/apply/into_handover_test.go +++ b/internal/apply/into_handover_test.go @@ -112,18 +112,77 @@ func TestTheRegistryMovesToTheRuntimesModuleWithoutLeavingTheList(t *testing.T) } } -// And the preview says the same before it happens. -func TestThePlanForgetsARecordOfAUnitStillHeld(t *testing.T) { - path := filepath.Join(t.TempDir(), "daemon.json") - var commands []string - _, state, err := Apply(context.Background(), archHost(t), runtimeDecl(t, path, true), store.State{}, - store.OriginDeclared, recordingServices(&commands), nil, nil) - if err != nil { - t.Fatal(err) - } - for _, step := range Plan(runtimeDecl(t, path, false), state, store.OriginDeclared) { - if step.ID == "networking.registry-trust-reload" && step.Verb != "forget" { - t.Errorf("the plan would %s the network's record of a unit docker.runtime holds: %s", step.Verb, step.Why) +// A unit two records name, under opposite conditions, passes between them (design 42 zfs-owns-zfs §2, answer A-21): the +// going record is forgotten, the preview says so before it happens, and what it found of the unit before the mesh is what +// the staying record gives back when its module leaves, not what the mesh made of it. +func TestARecordOfAUnitStillHeldIsForgottenAndPassesWhatItFound(t *testing.T) { + t.Run("the preview forgets it", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "daemon.json") + var commands []string + _, state, err := Apply(context.Background(), archHost(t), runtimeDecl(t, path, true), store.State{}, + store.OriginDeclared, recordingServices(&commands), nil, nil) + if err != nil { + t.Fatal(err) } - } + for _, step := range Plan(runtimeDecl(t, path, false), state, store.OriginDeclared) { + if step.ID == "networking.registry-trust-reload" && step.Verb != "forget" { + t.Errorf("the plan would %s the network's record of a unit docker.runtime holds: %s", step.Verb, step.Why) + } + } + }) + t.Run("what it found passes to the record that stays", func(t *testing.T) { + // A unit found stopped and disabled, so the mesh's first record of it started it. + active, enabled := false, false + var commands []string + run := func(_ context.Context, name string, args ...string) (string, error) { + args = oldShape(name, args) + line := name + " " + strings.Join(args, " ") + commands = append(commands, line) + switch { + case strings.Contains(line, " start "): + active = true + case strings.Contains(line, " stop "): + active = false + case strings.Contains(line, " enable "): + enabled = true + case strings.Contains(line, " disable "): + enabled = false + case strings.Contains(line, "is-enabled"): + if enabled { + return "enabled", nil + } + return "disabled", nil + case strings.Contains(line, "show") && strings.Contains(line, "ActiveState"): + if active { + return "LoadState=loaded\nActiveState=active\nSubState=running", nil + } + return "LoadState=loaded\nActiveState=inactive\nSubState=dead", nil + } + return "", nil + } + unit := func(id, state, boot string) *declaration.Declaration { + return parse(t, `{"declaration":1,"resources":[{"id":"`+id+`","type":"service","unit":"z.service","state":"`+ + state+`","boot":"`+boot+`"}]}`) + } + apply := func(d *declaration.Declaration, known store.State) store.State { + t.Helper() + _, known, err := Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, run, nil, nil) + if err != nil { + t.Fatal(err) + } + return known + } + known := apply(unit("z.on", "running", "enabled"), store.State{}) + known = apply(unit("z.off", "stopped", "disabled"), known) // the value changed: one record goes, the other stays + if rec, _ := known.Find("z.off"); rec.Found == nil || rec.Found.State != "stopped" || rec.Found.Boot != "disabled" { + t.Fatalf("the staying record did not receive what the going one found: %+v", rec.Found) + } + commands = nil + apply(somethingElse(t), known) // the module leaves + for _, c := range commands { + if strings.Contains(c, " start ") || strings.Contains(c, " enable ") { + t.Errorf("the unit was given back as the mesh made it, not as it was found: %q", c) + } + } + }) }