package apply import ( "context" "os" "path/filepath" "strings" "testing" "time" "github.com/novox/mesh-host/internal/store" ) // A directory used as found is never quiet, and a person at the machine can hand it over (novox/hq issue 339). func TestAFoundDirectoryIsSaidAndCanBeHandedOver(t *testing.T) { dir := t.TempDir() found := filepath.Join(dir, "found") if err := os.Mkdir(found, 0o700); err != nil { t.Fatal(err) } decl := `{"id":"notes.data","type":"directory","path":"` + found + `","mode":"0755"}` var said []string log := func(line string) { said = append(said, line) } _, state, err := Apply(context.Background(), archHost(t), declare(t, decl), store.State{}, store.OriginDeclared, noServices, log, nil) if err != nil { t.Fatal(err) } if !strings.Contains(strings.Join(said, "\n"), "WARNING notes.data") { t.Fatalf("the apply did not warn of a directory used as found: %q", said) } if a, _ := state.Find("notes.data"); !a.AsFound || !strings.Contains(a.Declared, "755") { t.Fatalf("the record does not say what was declared: %+v", a) } // Said again at the next apply: a warning once is a warning missed. said = nil if _, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared, noServices, log, nil); err != nil { t.Fatal(err) } if !strings.Contains(strings.Join(said, "\n"), "WARNING notes.data") { t.Fatalf("the next apply did not warn again: %q", said) } // Handed over: the next apply gives it the declared mode and it is the mesh's. before := HandedOver HandedOver = func() map[string]bool { return map[string]bool{handOverKey("notes.data", found): true} } t.Cleanup(func() { HandedOver = before }) if _, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared, noServices, nil, nil); err != nil { t.Fatal(err) } if info, _ := os.Stat(found); info.Mode().Perm() != 0o755 { t.Fatalf("a directory handed over was not given its declared mode: %o", info.Mode().Perm()) } if a, _ := state.Find("notes.data"); a.AsFound { t.Fatal("a directory handed over is still recorded as found") } } // The review's additions to the guard: the spool, /opt, the container runtimes' data wherever the runtime keeps // it, and an account's .ssh as an access or a mount, or as a directory anywhere but its own home. func TestTheRuntimesDataAndAnySSHAreRefused(t *testing.T) { cfg := filepath.Join(t.TempDir(), "daemon.json") if err := os.WriteFile(cfg, []byte(`{"data-root":"/storage/docker"}`), 0o644); err != nil { t.Fatal(err) } before := runtimeConfigs runtimeConfigs = runtimeFiles{daemonJSON: cfg} t.Cleanup(func() { runtimeConfigs = before }) for _, kind := range []placing{placingDirectory, placingAccess, placingMount} { for _, path := range []string{"/var/spool/cron", "/opt/app", "/var/lib/docker/volumes/x", "/var/lib/containers/storage", "/storage/docker", "/storage/docker/overlay2"} { if err := refusePath(path, kind, "notes", ""); err == nil { t.Errorf("%v at %s was taken", kind, path) } } } for _, path := range []string{"/srv/backup/.ssh", "/srv/backup/.ssh/keys"} { for _, kind := range []placing{placingDirectory, placingAccess, placingMount} { if err := refusePath(path, kind, "notes", ""); err == nil { t.Errorf("%v at %s was taken", kind, path) } } } if err := refusePath("/storage/dockerish", placingAccess, "notes", ""); err != nil { t.Errorf("a path beside the data root: %v", err) } } // When a directory was found is kept from the first apply that found it (novox/hq issue 339): the controller // tells a directory a send found from one found before it by that time, so it must not move with every apply. func TestWhenADirectoryWasFoundIsKept(t *testing.T) { dir := t.TempDir() found := filepath.Join(dir, "found") if err := os.Mkdir(found, 0o700); err != nil { t.Fatal(err) } decl := `{"id":"notes.data","type":"directory","path":"` + found + `","mode":"0755"}` _, state, err := Apply(context.Background(), archHost(t), declare(t, decl), store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } first, _ := state.Find("notes.data") if first.FoundAt.IsZero() { t.Fatal("no time the directory was found") } time.Sleep(10 * time.Millisecond) _, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if again, _ := state.Find("notes.data"); !again.FoundAt.Equal(first.FoundAt) { t.Fatalf("found at %v, then %v: the time moved with the apply", first.FoundAt, again.FoundAt) } } // A hand-over is for the resource that was used as found there: another declaration that later points another // resource at the same path is not handed anything (novox/hq issue 339). func TestAHandOverIsForTheResourceThatAskedForIt(t *testing.T) { dir := t.TempDir() found := filepath.Join(dir, "found") if err := os.Mkdir(found, 0o700); err != nil { t.Fatal(err) } before := HandedOver HandedOver = func() map[string]bool { return map[string]bool{handOverKey("notes.data", found): true} } t.Cleanup(func() { HandedOver = before }) other := `{"id":"other.data","type":"directory","path":"` + found + `","mode":"0755"}` if _, _, err := Apply(context.Background(), archHost(t), declare(t, other), store.State{}, store.OriginDeclared, noServices, nil, nil); err != nil { t.Fatal(err) } if info, _ := os.Stat(found); info.Mode().Perm() != 0o700 { t.Fatalf("a hand-over for notes.data gave other.data the directory: mode %o", info.Mode().Perm()) } } // **A handed-over directory is changed through no link, at the moment of use** (review of issue 356). The // hand-over refused a link when it was recorded; between then and the apply its parent is swapped for a link to // a victim tree. The apply refuses, and the victim's directory keeps its mode — and so does a directory whose own // name became a link, and one whose grandparent did. func TestAHandedOverDirectoryIsChangedThroughNoLink(t *testing.T) { for _, c := range []struct { swap string applyOnce bool }{{"parent", false}, {"grandparent", false}, {"itself", false}, {"parent", true}, {"grandparent", true}} { swap, applyOnce := c.swap, c.applyOnce name := swap if applyOnce { name += " after an apply gave it its owner" } t.Run(name, func(t *testing.T) { dir, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { t.Fatal(err) } found := filepath.Join(dir, "top", "parent", "data") if err := os.MkdirAll(found, 0o700); err != nil { t.Fatal(err) } decl := `{"id":"notes.data","type":"directory","path":"` + found + `","mode":"0755"}` _, state, err := Apply(context.Background(), archHost(t), declare(t, decl), store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if a, _ := state.Find("notes.data"); !a.AsFound { t.Fatalf("not used as found: %+v", a) } before := HandedOver HandedOver = func() map[string]bool { return map[string]bool{handOverKey("notes.data", found): true} } t.Cleanup(func() { HandedOver = before }) if applyOnce { // The hand-over takes: the directory is given its mode, and the record is no longer as found. if _, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared, noServices, nil, nil); err != nil { t.Fatal(err) } if a, _ := state.Find("notes.data"); a.AsFound { t.Fatalf("still as found after the hand-over took: %+v", a) } if info, _ := os.Stat(found); info.Mode().Perm() != 0o755 { t.Fatalf("the hand-over did not take: %o", info.Mode().Perm()) } } // The victim: a tree of the same shape somewhere else, its data directory mode 0700. victim := filepath.Join(dir, "victim", "top", "parent", "data") if err := os.MkdirAll(victim, 0o700); err != nil { t.Fatal(err) } var swapped, target string switch swap { case "parent": swapped, target = filepath.Join(dir, "top", "parent"), filepath.Join(dir, "victim", "top", "parent") case "grandparent": swapped, target = filepath.Join(dir, "top"), filepath.Join(dir, "victim", "top") case "itself": swapped, target = found, victim } if err := os.Rename(swapped, swapped+".real"); err != nil { t.Fatal(err) } if err := os.Symlink(target, swapped); err != nil { t.Fatal(err) } _, _, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared, noServices, nil, nil) if err == nil { t.Fatal("a handed-over directory reached through a link was applied") } // Refused by the path's re-check before any descriptor is opened (a link at the name itself is refused // earlier still, by the directory applier's own look at it). want := "reached through a link" if swap == "itself" { want = "symbolic link" } if !strings.Contains(err.Error(), want) { t.Fatalf("refused as %v; want %q", err, want) } if info, _ := os.Stat(victim); info.Mode().Perm() != 0o700 { t.Fatalf("the victim's directory was given the declared mode through the link: %o", info.Mode().Perm()) } }) } } // The check at the moment of use, without the path's re-check: a component swapped for a link is refused by the // descriptor walk itself, whatever was resolved a moment before. func TestGiveHandedOverFollowsNoLink(t *testing.T) { dir, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { t.Fatal(err) } victim := filepath.Join(dir, "victim", "data") if err := os.MkdirAll(victim, 0o700); err != nil { t.Fatal(err) } if err := os.Symlink(filepath.Join(dir, "victim"), filepath.Join(dir, "parent")); err != nil { t.Fatal(err) } if _, _, _, err := giveHandedOver(filepath.Join(dir, "parent", "data"), 0o755, 0, 0, false); err == nil { t.Fatal("a directory reached through a link was changed") } if info, _ := os.Stat(victim); info.Mode().Perm() != 0o700 { t.Fatalf("the victim was changed: %o", info.Mode().Perm()) } mode, _, _, err := giveHandedOver(victim, 0o750, 0, 0, false) if err != nil || mode.Perm() != 0o700 { t.Fatalf("the directory itself: before %o, %v", mode, err) } if info, _ := os.Stat(victim); info.Mode().Perm() != 0o750 { t.Fatalf("the directory itself was not given its mode: %o", info.Mode().Perm()) } }