Hold an archive, a process's unit and a user found on an adopted node for an untaken module (hq ADR 0103)

This commit is contained in:
2026-09-22 18:32:20 +02:00
parent 491e04fb8f
commit aef4993d10
3 changed files with 134 additions and 2 deletions
+74
View File
@@ -24,6 +24,7 @@ type machine struct {
// named volumes.
units map[string]*fakeUnit
volumes map[string]bool
users map[string]bool
}
type fakeUnit struct {
@@ -80,6 +81,12 @@ func (m *machine) run(_ context.Context, name string, args ...string) (string, e
if name == "systemctl" {
return m.systemctl(args)
}
if name == "getent" {
if m.users[args[len(args)-1]] {
return args[len(args)-1] + ":x:1500:1500::/home/" + args[len(args)-1] + ":/bin/bash\n", nil
}
return "", errors.New("exit status 2")
}
if name != "docker" {
return "", nil
}
@@ -680,3 +687,70 @@ func TestAnActionInAHeldContainerIsHeldUntilItsModuleIsTaken(t *testing.T) {
t.Errorf("holds outlived the take: %+v", state.Held)
}
}
const sixtyFourZeros = "0000000000000000000000000000000000000000000000000000000000000000"
func TestAnArchiveOverSomethingFoundIsNotUnpacked(t *testing.T) {
dir := t.TempDir()
at := filepath.Join(dir, "site")
if err := os.Mkdir(at, 0o750); err != nil {
t.Fatal(err)
}
theirs := filepath.Join(at, "index.html")
_ = os.WriteFile(theirs, []byte("the predecessor's site\n"), 0o640)
m := &machine{containers: map[string]*fakeContainer{}}
// The source is unreachable: fetching it would fail the apply, so a pass means it was not tried.
report, state := applyAdopted(t, adopted(t, untaken("hello-web.site"),
`{"id":"hello-web.site","type":"archive","source":"http://192.0.2.1/site.tar.gz",
"digest":"sha256:`+sixtyFourZeros+`","path":"`+at+`","owner":"root"}`), store.State{}, m, dir)
if o := outcomeOf(report, "hello-web.site"); o.Action != "held" || !strings.Contains(o.Detail, "nothing unpacked") {
t.Errorf("an archive over found files was not held: %+v", o)
}
if got, _ := os.ReadFile(theirs); string(got) != "the predecessor's site\n" {
t.Errorf("the found files were touched: %q", got)
}
if _, ok := state.HeldAt("hello-web.site"); !ok {
t.Error("the hold was not recorded")
}
}
func TestAProcessWhoseUnitIsFoundIsNotWrittenOverOrRestarted(t *testing.T) {
dir := t.TempDir()
was := unitDir
unitDir = dir
t.Cleanup(func() { unitDir = was })
unit := filepath.Join(dir, "hello-daemon.service")
_ = os.WriteFile(unit, []byte("[Service]\nExecStart=/opt/predecessor/hello\n"), 0o644)
m := &machine{containers: map[string]*fakeContainer{}}
report, state := applyAdopted(t, adopted(t, untaken("hello-web.daemon"),
`{"id":"hello-web.daemon","type":"process","name":"hello-daemon","source":"http://192.0.2.1/d.tar.gz",
"digest":"sha256:`+sixtyFourZeros+`","run":["hello"]}`), store.State{}, m, dir)
if o := outcomeOf(report, "hello-web.daemon"); o.Action != "held" || !strings.Contains(o.Detail, "not written over or restarted") {
t.Errorf("a process whose unit was found was not held: %+v", o)
}
if got, _ := os.ReadFile(unit); string(got) != "[Service]\nExecStart=/opt/predecessor/hello\n" {
t.Errorf("the found unit was written over: %q", got)
}
if m.did("systemctl") {
t.Errorf("the found unit was touched: %v", m.asked)
}
if _, ok := state.HeldAt("hello-web.daemon"); !ok {
t.Error("the hold was not recorded")
}
}
func TestAUserFoundOnTheMachineKeepsItsShellAndGroups(t *testing.T) {
dir := t.TempDir()
m := &machine{containers: map[string]*fakeContainer{}, users: map[string]bool{"hello": true}}
report, _ := applyAdopted(t, adopted(t, untaken("hello-web.user"),
`{"id":"hello-web.user","type":"user","name":"hello","shell":"/bin/zsh","groups":["docker"]}`),
store.State{}, m, dir)
if o := outcomeOf(report, "hello-web.user"); o.Action != "held" || !strings.Contains(o.Detail, "shell and groups") {
t.Errorf("a found user was not held: %+v", o)
}
for _, a := range m.asked {
if strings.HasPrefix(a, "usermod") || strings.HasPrefix(a, "useradd") {
t.Errorf("a found user was changed: %s", a)
}
}
}