pacman_rollback plans from the bus: each package now -> record from the node's cache, else the archive's exact version, else the archive's repository on the record's date; added packages listed and removed only when asked; foreign packages untouched. apply is an ask the node's keeper runs as a rollback run through the root helper's new Rollback act (RootActs, upgrades.go), with rollback.started, rollback.finished and the after-upgrade check (TakeAsk's rollback case, transaction.go).
196 lines
7.7 KiB
Go
196 lines
7.7 KiB
Go
package main
|
|
|
|
// A rollback to a record (novox/hq ADR 0508 §7; acceptance 1841/AC8): the plan says each package now -> record with
|
|
// its source, the packages added and no foreign package; apply is an ask the node's keeper runs as one
|
|
// transaction, emitting rollback.started and rollback.finished and ending with the after-upgrade check.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
var recordAt = time.Date(2026, 10, 3, 4, 0, 12, 0, time.UTC)
|
|
|
|
// rollbackRecord is node-a's record of run 20261003T040012Z; rollbackNow its packages since.
|
|
var (
|
|
rollbackRecord = Record{Node: "node-a", Run: "20261003T040012Z", At: recordAt, Installed: InstalledValue{At: recordAt, Packages: []InstalledPackage{
|
|
{"bash", "5.3.15-1", true, "core"}, {"glibc", "2.42-1", false, "core"}, {"linux", "7.2.8.arch1-2", true, "core"},
|
|
{"made-up-old-library", "1.0-1", false, "extra"}, {"made-up-foreign", "1-1", true, RepoForeign},
|
|
}}}
|
|
rollbackNow = InstalledValue{At: recordAt.Add(time.Hour), Packages: []InstalledPackage{
|
|
{"bash", "5.3.20-1", true, "core"}, {"glibc", "2.42-1", false, "core"}, {"linux", "7.3.1.arch1-1", true, "core"},
|
|
{"made-up-foreign", "2-1", true, RepoForeign}, {"made-up-library", "1.0-1", false, "extra"},
|
|
}}
|
|
)
|
|
|
|
// rollbackCache is the node's package cache: bash's recorded version, and linux at its new version only.
|
|
const rollbackCache = "bash-5.3.15-1-x86_64.pkg.tar.zst\nbash-5.3.20-1-x86_64.pkg.tar.zst\nlinux-7.3.1.arch1-1-x86_64.pkg.tar.zst\n" +
|
|
"not-a-package-file\n"
|
|
|
|
func rollbackStates(t *testing.T) (*fakeStates, *Machine, *[]call) {
|
|
t.Helper()
|
|
states := &fakeStates{}
|
|
states.store(StateRecords).Put(NodeKey("node-a", rollbackRecord.Run), rollbackRecord)
|
|
states.store(StateInstalled).Put("node-a", rollbackNow)
|
|
var calls []call
|
|
m := machine(byLine(map[string]Ran{
|
|
"find " + CacheDir + " -mindepth 1 -maxdepth 1 -name *.pkg.tar.* -printf %f\n": {Stdout: rollbackCache},
|
|
}, &calls), 1000)
|
|
m.Node, m.States = "node-a", readerOf(states)
|
|
return states, m, &calls
|
|
}
|
|
|
|
func TestThePlanSaysEachPackageNowToTheRecordWithItsSourceTheAddedAndNoForeign(t *testing.T) {
|
|
_, m, calls := rollbackStates(t)
|
|
p, err := PlanRollback(m, m.States(StateInstalled), m.States(StateRecords), "node-a", rollbackRecord.Run, false, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []RollbackItem{
|
|
{"bash", "5.3.20-1", "5.3.15-1", SourceCache, CacheDir + "/bash-5.3.15-1-x86_64.pkg.tar.zst"},
|
|
{"linux", "7.3.1.arch1-1", "7.2.8.arch1-2", SourceArchive, "https://archive.archlinux.org/packages/l/linux/linux-7.2.8.arch1-2-x86_64.pkg.tar.zst"},
|
|
{"made-up-old-library", "", "1.0-1", SourceRepository, "https://archive.archlinux.org/repos/2026/10/03/extra/os/$arch"},
|
|
}
|
|
if len(p.Items) != len(want) {
|
|
t.Fatalf("items: %+v", p.Items)
|
|
}
|
|
for i := range want {
|
|
if p.Items[i] != want[i] {
|
|
t.Errorf("item %d: %+v, want %+v", i, p.Items[i], want[i])
|
|
}
|
|
}
|
|
if len(p.Added) != 1 || p.Added[0] != (PackageVersion{"made-up-library", "1.0-1"}) || p.Foreign != 1 || !p.CacheRead {
|
|
t.Errorf("added %+v, foreign %d, cache %v", p.Added, p.Foreign, p.CacheRead)
|
|
}
|
|
for _, c := range *calls {
|
|
if strings.Contains(c.String(), "made-up-foreign") {
|
|
t.Errorf("a command named the foreign package: %s", c)
|
|
}
|
|
}
|
|
// asked to remove what was added: the added package is removed too.
|
|
p, _ = PlanRollback(m, m.States(StateInstalled), m.States(StateRecords), "node-a", rollbackRecord.Run, true, true)
|
|
if len(p.Items) != 4 || p.Items[2] != (RollbackItem{Name: "made-up-library", From: "1.0-1", Source: SourceRemoved}) {
|
|
t.Errorf("with remove_added: %+v", p.Items)
|
|
}
|
|
}
|
|
|
|
func TestAnotherNodesPlanReadsNoCacheAndTakesUnknownArchitecturesFromTheDatedRepository(t *testing.T) {
|
|
_, m, calls := rollbackStates(t)
|
|
m.Node = "node-b"
|
|
r, err := m.Rollback("node-a", rollbackRecord.Run, false, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := r["plan"].(RollbackPlan)
|
|
if p.CacheRead || len(*calls) != 0 {
|
|
t.Errorf("another node's cache was read: %v", *calls)
|
|
}
|
|
for _, i := range p.Items {
|
|
if i.Source != SourceRepository || !strings.HasPrefix(i.Location, "https://archive.archlinux.org/repos/2026/10/03/") {
|
|
t.Errorf("%+v", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoPlanFromPackagesNotCurrentOrWithoutARecord(t *testing.T) {
|
|
states, m, _ := rollbackStates(t)
|
|
if _, err := m.Rollback("node-a", "20250101T000000Z", false, false); err == nil || !strings.Contains(err.Error(), "no record") {
|
|
t.Errorf("no record: %v", err)
|
|
}
|
|
states.store(StateInstalled).Put(NotCurrentKey("node-a"), NotCurrent{Bytes: 300000, Since: recordAt})
|
|
if _, err := m.Rollback("node-a", rollbackRecord.Run, false, false); err == nil || !strings.Contains(err.Error(), "not current") {
|
|
t.Errorf("not current: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApplyWritesAnAskForTheNodesKeeper(t *testing.T) {
|
|
_, m, _ := rollbackStates(t)
|
|
asks := newBucket()
|
|
saved := upgradeState
|
|
upgradeState = func(name string) Bucket { return asks }
|
|
t.Cleanup(func() { upgradeState = saved })
|
|
r, err := m.Rollback("", rollbackRecord.Run, true, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := r["asked"].(Ask)
|
|
var got Ask
|
|
if !asks.value(NodeKey("node-a", a.ID), &got) || got.Act != ActRollback || got.Of != rollbackRecord.Run || !got.RemoveAdded {
|
|
t.Errorf("the ask: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestAnAskedRollbackIsOneTransactionWithItsEventsAndTheCheck(t *testing.T) {
|
|
k := newKeeperBed(t)
|
|
k.records.Put(NodeKey("node-a", rollbackRecord.Run), rollbackRecord)
|
|
k.installed.Put("node-a", rollbackNow)
|
|
inner := k.u.Machine.Run
|
|
k.u.Machine.Run = func(ctx context.Context, name string, args ...string) Ran {
|
|
if name == "find" {
|
|
return Ran{Stdout: rollbackCache}
|
|
}
|
|
return inner(ctx, name, args...)
|
|
}
|
|
a := Ask{ID: "20261004T120000Z-abc123", Node: "node-a", Act: ActRollback, At: k.now, Of: rollbackRecord.Run}
|
|
if err := k.takeAsk(t, a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var r Run
|
|
keys, _ := k.runs.Keys()
|
|
for _, key := range keys {
|
|
var x Run
|
|
k.runs.value(key, &x)
|
|
if x.Kind == RunRollback {
|
|
r = x
|
|
}
|
|
}
|
|
if r.Of != rollbackRecord.Run || r.Ask != a.ID || steps(r) != "started" || len(r.Steps[0].Plan) != 3 {
|
|
t.Fatalf("the rollback run: %+v", r)
|
|
}
|
|
if strings.Join(k.root.started, ";") != "rollback "+r.Run+" plan=bash:5.3.15-1:cache,linux:7.2.8.arch1-2:archive,made-up-old-library:1.0-1:repository" {
|
|
t.Errorf("the root helper was asked: %v", k.root.started)
|
|
}
|
|
if _, there := k.asks.values[NodeKey("node-a", a.ID)]; there {
|
|
t.Error("the ask is deleted once the run names it")
|
|
}
|
|
k.unitState, k.now = "LoadState=not-found\n", k.now.Add(time.Minute)
|
|
if err := k.u.Follow(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
k.runs.value(NodeKey("node-a", r.Run), &r)
|
|
if steps(r) != "started finished checked-after" || r.Steps[2].Check == nil {
|
|
t.Errorf("steps: %s", steps(r))
|
|
}
|
|
if got := strings.Join(k.events.names(), " "); got != "rollback.started rollback.finished upgrade.checked" {
|
|
t.Errorf("events: %s", got)
|
|
}
|
|
// the same record again: nothing to bring back once the packages are the record's.
|
|
k.installed.Put("node-a", rollbackRecord.Installed)
|
|
k.now = k.now.Add(time.Minute)
|
|
if err := k.takeAsk(t, Ask{ID: "20261004T120200Z-def456", Node: "node-a", Act: ActRollback, At: k.now, Of: rollbackRecord.Run}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var again Run
|
|
k.runs.value(NodeKey("node-a", "20261004T120200Z"), &again)
|
|
if steps(again) != "refused" || !strings.Contains(again.Steps[0].Why, "nothing to bring back") {
|
|
t.Errorf("a rollback with nothing to do: %+v", again)
|
|
}
|
|
}
|
|
|
|
// takeAsk writes an ask and hands its change to the keeper, as the watch of `asks` would.
|
|
func (k *keeperBed) takeAsk(t *testing.T, a Ask) error {
|
|
t.Helper()
|
|
key := NodeKey(a.Node, a.ID)
|
|
rev, err := k.asks.Create(key, a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(a)
|
|
return k.u.TakeAsk(stdio.StateChange{State: StateAsks, Key: key, Op: "put", Value: raw, Revision: rev})
|
|
}
|