Refuse a placement at the machine's own directories, and use a found directory as found
A module's places setting can move a directory anywhere, and the engine chowned whatever it was pointed at as root: a directory at /etc owned by the agent account would hand it /etc (hq ADR 0266). Refuse the machine's roots, the kernel's and the engine's trees, another account's home, and an archive or written-into file below an agent's home; and leave the owner and mode of a directory the mesh did not make.
This commit is contained in:
+54
-3
@@ -75,6 +75,8 @@ type Outcome struct {
|
||||
groups []string
|
||||
// unpacked is, for an archive, what it put on the machine (novox/hq issue 162).
|
||||
unpacked *store.Unpacked
|
||||
// asFound is, for a directory, that it is used as it was found (novox/hq ADR 0266).
|
||||
asFound bool
|
||||
// reads is, for a container, the digest of each file it was created reading, by path — so
|
||||
// the next apply can say which one changed (novox/hq 04-ISSUES/103).
|
||||
reads map[string]string
|
||||
@@ -459,9 +461,15 @@ func ApplyMindingWindows(
|
||||
// And one patience with the artifact store for the whole apply (novox/hq issue 291): a fetch it
|
||||
// does not answer during a maintenance window waits for the window instead of failing.
|
||||
in := inputs{declares: map[string]string{}, known: &known, windows: windows, away: newStoreAway(windows, log),
|
||||
groups: wanted}
|
||||
groups: wanted, agentHomes: rootNeverHomes(d)}
|
||||
in.dirsBefore = map[string]bool{}
|
||||
for _, resource := range d.Resources {
|
||||
in.declares[resource.Identity()] = declaredDigest(resource)
|
||||
if dir, ok := resource.(*declaration.Directory); ok {
|
||||
if _, err := os.Lstat(dir.Path); err == nil {
|
||||
in.dirsBefore[filepath.Clean(dir.Path)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Everything is attempted, and every failure is reported.
|
||||
@@ -725,6 +733,7 @@ func ApplyMindingWindows(
|
||||
Linger: outcome.linger,
|
||||
Groups: outcome.groups,
|
||||
Unpacked: outcome.unpacked,
|
||||
AsFound: outcome.asFound,
|
||||
Holds: holds(resource),
|
||||
})
|
||||
if outcome.found != nil {
|
||||
@@ -919,15 +928,19 @@ func applyOne(ctx context.Context, sys system.System, r declaration.Resource, ru
|
||||
changed map[string]bool, in inputs, previous store.Applied,
|
||||
unseal Unseal, keepFound Keep) (Outcome, error) {
|
||||
// Nothing below a home is touched through a link an account put there (novox/hq ADR 0266).
|
||||
// And nothing is placed where no module places anything, whoever asked (placement_guard.go).
|
||||
switch r.(type) {
|
||||
case *declaration.Directory, *declaration.File, *declaration.Archive:
|
||||
if err := refusePlacement(r, in.agentHomes); err != nil {
|
||||
return begin(r), err
|
||||
}
|
||||
if err := refuseLinksUnderHome(r.Target()); err != nil {
|
||||
return begin(r), err
|
||||
}
|
||||
}
|
||||
switch res := r.(type) {
|
||||
case *declaration.Directory:
|
||||
return applyDirectory(res)
|
||||
return applyDirectory(res, previous, in.dirsBefore[filepath.Clean(res.Path)])
|
||||
case *declaration.File:
|
||||
return applyFile(res, previous, unseal, keepFound)
|
||||
case *declaration.Service:
|
||||
@@ -972,7 +985,15 @@ func modeOf(spec string, fallback os.FileMode) (os.FileMode, error) {
|
||||
return os.FileMode(parsed), nil
|
||||
}
|
||||
|
||||
func applyDirectory(r *declaration.Directory) (Outcome, error) {
|
||||
// applyDirectory makes a directory what was declared.
|
||||
//
|
||||
// **A directory found here, that the mesh did not make, is used as found** (novox/hq ADR 0266): its owner and
|
||||
// mode are left, and the outcome says what was declared and what was found. Changing them would be root
|
||||
// handing a directory it never made — wherever a declaration pointed it — to whatever account was named. A
|
||||
// directory is the mesh's when its record says the mesh applied it before (a record from before this rule
|
||||
// counts, which is every directory on a running machine), or when it already has the declared owner and mode,
|
||||
// so a person who sets them by hand at the machine hands it to the mesh.
|
||||
func applyDirectory(r *declaration.Directory, previous store.Applied, wasBefore bool) (Outcome, error) {
|
||||
out := begin(r)
|
||||
mode, err := modeOf(r.Mode, 0o755)
|
||||
if err != nil {
|
||||
@@ -987,6 +1008,26 @@ func applyDirectory(r *declaration.Directory) (Outcome, error) {
|
||||
if existed && !before.IsDir() {
|
||||
return out, fmt.Errorf("%s exists and is not a directory", r.Path)
|
||||
}
|
||||
if existed && wasBefore {
|
||||
ours := previous.Target != "" && filepath.Clean(previous.Target) == filepath.Clean(r.Path) && !previous.AsFound
|
||||
if !ours {
|
||||
owned, err := ownedBy(r.Path, r.Owner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if !owned || before.Mode().Perm() != mode.Perm() {
|
||||
out.Action, out.asFound = "unchanged", true
|
||||
owner := r.Owner
|
||||
if owner == "" {
|
||||
owner = "root"
|
||||
}
|
||||
out.Detail = fmt.Sprintf("found here before the mesh and used as found: its owner and mode %o are "+
|
||||
"left, though %s and %o were declared (novox/hq ADR 0266); set them by hand to hand it to the mesh",
|
||||
before.Mode().Perm(), owner, mode.Perm())
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !existed {
|
||||
if err := makeDirs(r.Path, mode, r.Owner); err != nil {
|
||||
@@ -1647,6 +1688,10 @@ func remove(ctx context.Context, sys system.System, a store.Applied, run Runner,
|
||||
// This is the host's own line, applied to the one shape where getting it wrong is not
|
||||
// recoverable: it removes what it made and leaves what it merely configured. An empty
|
||||
// directory is what it made. A full one is not.
|
||||
if a.AsFound {
|
||||
// Found here before the mesh, and never the mesh's (novox/hq ADR 0266).
|
||||
return "forgotten", "found here before the mesh and used as found: left as it is", nil
|
||||
}
|
||||
entries, err := os.ReadDir(a.Target)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "forgotten", "no longer there", nil
|
||||
@@ -1874,6 +1919,12 @@ type inputs struct {
|
||||
// groups is every group the declaration asks an account to be in, and by which resources
|
||||
// (novox/hq ADR 0252): a group one user resource stops asking for stays while another asks.
|
||||
groups Wanted
|
||||
// agentHomes is the home of every account the declaration says never becomes root (novox/hq ADR
|
||||
// 0266): nothing is unpacked or written into below one (placement_guard.go).
|
||||
agentHomes []string
|
||||
// dirsBefore is every declared directory that was on the machine when this apply began (novox/hq ADR
|
||||
// 0266): one the apply itself made — a file's parent — is the mesh's, one that was there may not be.
|
||||
dirsBefore map[string]bool
|
||||
}
|
||||
|
||||
// fileDigest is what a file the container reads holds, by digest.
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package apply
|
||||
|
||||
// Where the node-engine places nothing, whoever asks (novox/hq ADR 0266, the third review of 2026-10-08).
|
||||
//
|
||||
// A directory, a file or an archive names its path, and the controller resolves part of that path from what
|
||||
// a person or a verb set: a module's `places` setting moves a directory anywhere. The engine runs as root, so
|
||||
// a path it accepts blindly is a path any caller of the controller's settings could hand to any account — a
|
||||
// directory resource at /etc owned by the agent account gives the agent /etc. So the engine itself refuses,
|
||||
// whatever the declaration says:
|
||||
//
|
||||
// 1. **a directory that is one of the machine's own roots**, or an ancestor of one: /, /etc, /usr, /var,
|
||||
// /var/lib, /home, /run and the rest of protectedRoots. Modules place files and directories BELOW /etc or
|
||||
// /var/lib, never the root itself; owning one is owning everything in it;
|
||||
// 2. **anything below /proc, /sys, /dev or /boot**, and **anything in the node-engine's own trees** (its
|
||||
// state, its identity, its installed builds) but its own module's;
|
||||
// 3. **anything below a person's or an agent's home, for an owner other than that home's account**. A
|
||||
// directory, file or archive below /home/<account> belongs to that account or is not placed: a module
|
||||
// placing root's, or another account's, file there is placing it where the account controls every parent;
|
||||
// and a home itself is its account's, so a directory resource naming a home exactly is refused;
|
||||
// 4. **an archive, or a file written into (`into`), below the home of an account declared `root: never`**,
|
||||
// however the path is spelled. The engine writes those after checking the path, not through descriptors,
|
||||
// and that account owns every parent and could swap a link in between.
|
||||
//
|
||||
// Each is a refusal of the resource, said in words; nothing is touched.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// protectedRoots are directories no directory resource may be, nor be an ancestor of.
|
||||
var protectedRoots = []string{"/", "/bin", "/boot", "/dev", "/etc", "/home", "/lib", "/lib64", "/media", "/mnt",
|
||||
"/opt", "/proc", "/root", "/run", "/sbin", "/srv", "/sys", "/tmp", "/usr", "/usr/bin", "/usr/lib",
|
||||
"/usr/lib64", "/usr/local", "/usr/local/bin", "/usr/local/lib", "/usr/local/sbin", "/usr/sbin", "/usr/share",
|
||||
"/var", "/var/cache", "/var/lib", "/var/lib/mesh", "/var/log", "/var/tmp"}
|
||||
|
||||
// forbiddenBelow are trees nothing is placed in: the kernel's and the boot loader's. engineTrees are the
|
||||
// engine's own, which only its own module (`mesh-host`, whose builds are installed there) places in.
|
||||
var (
|
||||
forbiddenBelow = []string{"/proc", "/sys", "/dev", "/boot"}
|
||||
engineTrees = []string{"/var/lib/mesh-host", "/usr/lib/nox-mesh-host"}
|
||||
)
|
||||
|
||||
// engineModule is the module whose resources may place in the engine's own trees.
|
||||
const engineModule = "mesh-host."
|
||||
|
||||
// PlacementRefusedError is a resource the engine will not place where it says.
|
||||
type PlacementRefusedError struct {
|
||||
Path, Why string
|
||||
}
|
||||
|
||||
func (e *PlacementRefusedError) Error() string {
|
||||
return fmt.Sprintf("%s is not placed: %s (novox/hq ADR 0266); nothing was touched", e.Path, e.Why)
|
||||
}
|
||||
|
||||
// accountsOfHomes is each person's or agent's home and the account it belongs to, from the user database —
|
||||
// the same homes homeAbove reads. A variable so a test names its own.
|
||||
var accountsOfHomes = func() map[string]homeAccount {
|
||||
f, err := os.Open(passwdFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
out := map[string]homeAccount{}
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
fields := strings.Split(sc.Text(), ":")
|
||||
if len(fields) < 6 {
|
||||
continue
|
||||
}
|
||||
uid, err := strconv.Atoi(fields[2])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
home := filepath.Clean(fields[5])
|
||||
if home == "/" || home == "." || home == "" {
|
||||
continue
|
||||
}
|
||||
if (uid >= 1000 && uid != 65534) || strings.HasPrefix(home, "/home/") {
|
||||
out[home] = homeAccount{Name: fields[0], UID: uid}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type homeAccount struct {
|
||||
Name string
|
||||
UID int
|
||||
}
|
||||
|
||||
// below says whether path is strictly below dir.
|
||||
func below(path, dir string) bool {
|
||||
if dir == "/" {
|
||||
return path != "/"
|
||||
}
|
||||
return strings.HasPrefix(path, dir+string(os.PathSeparator))
|
||||
}
|
||||
|
||||
// ownerName is the owner a resource declares, "" for root.
|
||||
func ownerName(r declaration.Resource) string {
|
||||
switch res := r.(type) {
|
||||
case *declaration.Directory:
|
||||
return res.Owner
|
||||
case *declaration.File:
|
||||
return res.Owner
|
||||
case *declaration.Archive:
|
||||
return res.Owner
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ownedByAccount says whether a declared owner is that account: by name, or by its uid ("1001", "1001:1001").
|
||||
func ownedByAccount(owner string, a homeAccount) bool {
|
||||
if owner == a.Name {
|
||||
return true
|
||||
}
|
||||
user, _, _ := strings.Cut(owner, ":")
|
||||
if uid, err := strconv.Atoi(user); err == nil {
|
||||
return uid == a.UID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// refusePlacement says why a directory, file or archive is not placed; nil when it may be. agentHomes are the
|
||||
// homes of the accounts the declaration says never become root.
|
||||
func refusePlacement(r declaration.Resource, agentHomes []string) error {
|
||||
path := filepath.Clean(r.Target())
|
||||
if !filepath.IsAbs(path) {
|
||||
return nil // the declaration refuses a relative path already
|
||||
}
|
||||
if _, isDir := r.(*declaration.Directory); isDir {
|
||||
for _, root := range protectedRoots {
|
||||
if path == root || below(root, path) {
|
||||
return &PlacementRefusedError{Path: path, Why: root + " is one of the machine's own directories, " +
|
||||
"and owning it would be owning everything in it"}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, tree := range forbiddenBelow {
|
||||
if path == tree || below(path, tree) {
|
||||
return &PlacementRefusedError{Path: path, Why: "nothing is placed in " + tree}
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(r.Identity(), engineModule) {
|
||||
for _, tree := range engineTrees {
|
||||
if path == tree || below(path, tree) {
|
||||
return &PlacementRefusedError{Path: path, Why: tree + " is the node-engine's own, placed in by its own module alone"}
|
||||
}
|
||||
}
|
||||
}
|
||||
homes := accountsOfHomes()
|
||||
if a, isHome := homes[path]; isHome {
|
||||
return &PlacementRefusedError{Path: path, Why: "it is " + a.Name + "'s home, which is that account's"}
|
||||
}
|
||||
var deepest string
|
||||
for home := range homes {
|
||||
if below(path, home) && len(home) > len(deepest) {
|
||||
deepest = home
|
||||
}
|
||||
}
|
||||
if deepest != "" {
|
||||
a := homes[deepest]
|
||||
if owner := ownerName(r); !ownedByAccount(owner, a) {
|
||||
if owner == "" {
|
||||
owner = "root"
|
||||
}
|
||||
return &PlacementRefusedError{Path: path, Why: fmt.Sprintf("it is below %s's home and declared %s's; "+
|
||||
"below a home only that account's files are placed", a.Name, owner)}
|
||||
}
|
||||
}
|
||||
risky := ""
|
||||
switch res := r.(type) {
|
||||
case *declaration.Archive:
|
||||
risky = "an archive unpacked"
|
||||
case *declaration.File:
|
||||
if res.Into != "" {
|
||||
risky = "a file written into (" + res.Into + ")"
|
||||
}
|
||||
}
|
||||
if risky != "" {
|
||||
for _, home := range agentHomes {
|
||||
if path == home || below(path, home) {
|
||||
return &PlacementRefusedError{Path: path, Why: risky + " below the home of an account that never " +
|
||||
"becomes root, which owns every parent there and could swap a link in between the check and the write"}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rootNeverHomes is the home of every account the declaration says never becomes root, from the user database;
|
||||
// an account not made yet has no home to protect.
|
||||
func rootNeverHomes(d *declaration.Declaration) []string {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
homes := accountsOfHomes()
|
||||
var out []string
|
||||
for _, r := range d.Resources {
|
||||
u, ok := r.(*declaration.User)
|
||||
if !ok || u.Root != declaration.RootNever {
|
||||
continue
|
||||
}
|
||||
for home, a := range homes {
|
||||
if a.Name == u.Name {
|
||||
out = append(out, home)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package apply
|
||||
|
||||
// Defends novox/hq ADR 0266 (the third review): the engine places nothing at one of the machine's own
|
||||
// directories, in the kernel's or its own trees, below a home for another account, or — for an archive or a
|
||||
// file written into — below an agent's home; and a directory it did not make is used as found.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
func withHomes(t *testing.T, homes map[string]homeAccount) {
|
||||
t.Helper()
|
||||
was := accountsOfHomes
|
||||
accountsOfHomes = func() map[string]homeAccount { return homes }
|
||||
t.Cleanup(func() { accountsOfHomes = was })
|
||||
}
|
||||
|
||||
func TestAMachinesOwnDirectoryIsNeverPlaced(t *testing.T) {
|
||||
withHomes(t, nil)
|
||||
for _, path := range []string{"/", "/etc", "/etc/", "/usr", "/var/lib", "/var/lib/mesh", "/home", "/root", "/run"} {
|
||||
err := refusePlacement(&declaration.Directory{ID: "m.d", Type: declaration.TypeDirectory, Path: path, Owner: "agent"}, nil)
|
||||
var refused *PlacementRefusedError
|
||||
if !errors.As(err, &refused) {
|
||||
t.Errorf("%s as a directory: refused, got %v", path, err)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"/etc/sudoers.d/x", "/var/lib/mesh/daemons", "/var/lib/postgres", "/usr/local/bin/claude-agent"} {
|
||||
if err := refusePlacement(&declaration.Directory{ID: "m.d", Type: declaration.TypeDirectory, Path: path}, nil); err != nil {
|
||||
t.Errorf("%s: a module's own place below a root passes, got %v", path, err)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"/proc/sys/x", "/sys/x", "/dev/x", "/boot/x", "/var/lib/mesh-host/state.json", "/var/lib/mesh-host"} {
|
||||
if err := refusePlacement(&declaration.File{ID: "m.f", Type: declaration.TypeFile, Path: path, Content: "x"}, nil); err == nil {
|
||||
t.Errorf("%s: nothing is placed there", path)
|
||||
}
|
||||
}
|
||||
if err := refusePlacement(&declaration.File{ID: "mesh-host.launcher", Type: declaration.TypeFile,
|
||||
Path: "/usr/lib/nox-mesh-host/launch", Content: "x"}, nil); err != nil {
|
||||
t.Errorf("the engine's own module places its builds: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBelowAHomeOnlyThatAccountsFilesArePlaced(t *testing.T) {
|
||||
withHomes(t, map[string]homeAccount{"/home/operator": {"operator", 1000}, "/home/agent": {"agent", 1001}})
|
||||
cases := []struct {
|
||||
r declaration.Resource
|
||||
ok bool
|
||||
}{
|
||||
{&declaration.Directory{ID: "a.d", Type: declaration.TypeDirectory, Path: "/home/agent/.claude", Owner: "agent"}, true},
|
||||
{&declaration.Directory{ID: "a.d", Type: declaration.TypeDirectory, Path: "/home/agent/.claude", Owner: "1001:1001"}, true},
|
||||
{&declaration.Directory{ID: "a.d", Type: declaration.TypeDirectory, Path: "/home/agent/.claude"}, false},
|
||||
{&declaration.Directory{ID: "a.d", Type: declaration.TypeDirectory, Path: "/home/agent/.claude", Owner: "operator"}, false},
|
||||
{&declaration.Directory{ID: "a.d", Type: declaration.TypeDirectory, Path: "/home/agent", Owner: "agent"}, false},
|
||||
{&declaration.File{ID: "a.f", Type: declaration.TypeFile, Path: "/home/operator/.zshrc", Owner: "operator", Content: "x"}, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := refusePlacement(c.r, nil); (err == nil) != c.ok {
|
||||
t.Errorf("%s owned by %q: ok %v, got %v", c.r.Target(), ownerName(c.r), c.ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNothingIsUnpackedOrWrittenIntoBelowAnAgentsHome(t *testing.T) {
|
||||
withHomes(t, map[string]homeAccount{"/home/agent": {"agent", 1001}})
|
||||
agent := []string{"/home/agent"}
|
||||
if err := refusePlacement(&declaration.Archive{ID: "a.x", Type: declaration.TypeArchive, Path: "/home/agent/.local/x", Owner: "agent"}, agent); err == nil {
|
||||
t.Error("an archive below an agent's home is refused")
|
||||
}
|
||||
if err := refusePlacement(&declaration.File{ID: "a.f", Type: declaration.TypeFile, Path: "/home/agent/.claude.json", Owner: "agent", Into: "json", Content: "{}"}, agent); err == nil {
|
||||
t.Error("a file written into below an agent's home is refused")
|
||||
}
|
||||
if err := refusePlacement(&declaration.File{ID: "a.f", Type: declaration.TypeFile, Path: "/home/agent/x", Owner: "agent", Content: "x"}, agent); err != nil {
|
||||
t.Errorf("a whole file there passes: %v", err)
|
||||
}
|
||||
d := parse(t, `{"declaration":1,"resources":[{"id":"c.agent","type":"user","name":"agent","root":"never"},`+
|
||||
`{"id":"c.op","type":"user","name":"operator"}]}`)
|
||||
if got := rootNeverHomes(d); len(got) != 1 || got[0] != "/home/agent" {
|
||||
t.Errorf("the agent's home is known by the user database: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestADirectoryAtEtcIsRefusedThroughTheApplyAndNothingIsTouched(t *testing.T) {
|
||||
withHomes(t, nil)
|
||||
l := &logins{shells: map[string]string{}}
|
||||
report, _, err := Apply(context.Background(), archHost(t), parse(t,
|
||||
`{"declaration":1,"resources":[{"id":"m.state","type":"directory","path":"/etc","owner":"agent","mode":"0755"}]}`),
|
||||
store.State{}, store.OriginDeclared, l.run, nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "machine's own directories") {
|
||||
t.Fatalf("refused: %v %+v", err, report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestADirectoryFoundHereIsUsedAsFound(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "found")
|
||||
if err := os.Mkdir(dir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := &declaration.Directory{ID: "m.d", Type: declaration.TypeDirectory, Path: dir, Mode: "0755"}
|
||||
out, err := applyDirectory(r, store.Applied{}, true)
|
||||
if err != nil || !out.asFound || !strings.Contains(out.Detail, "used as found") {
|
||||
t.Fatalf("a found directory is used as found: %+v %v", out, err)
|
||||
}
|
||||
if info, _ := os.Stat(dir); info.Mode().Perm() != 0o700 {
|
||||
t.Fatalf("its mode was changed: %o", info.Mode().Perm())
|
||||
}
|
||||
// Recorded as found, it stays found.
|
||||
out, _ = applyDirectory(r, store.Applied{ID: "m.d", Target: dir, AsFound: true}, true)
|
||||
if !out.asFound {
|
||||
t.Fatal("a directory recorded as found stays found")
|
||||
}
|
||||
// The mesh's by its record from an earlier apply: converged as before.
|
||||
out, err = applyDirectory(r, store.Applied{ID: "m.d", Target: dir}, true)
|
||||
if err != nil || out.asFound {
|
||||
t.Fatalf("a directory the mesh applied before is converged: %+v %v", out, err)
|
||||
}
|
||||
if info, _ := os.Stat(dir); info.Mode().Perm() != 0o755 {
|
||||
t.Fatalf("not converged: %o", info.Mode().Perm())
|
||||
}
|
||||
// Already as declared: the mesh's from here on.
|
||||
other := filepath.Join(t.TempDir(), "same")
|
||||
if err := os.Mkdir(other, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out, _ := applyDirectory(&declaration.Directory{ID: "m.e", Type: declaration.TypeDirectory, Path: other, Mode: "0755"}, store.Applied{}, true); out.asFound {
|
||||
t.Fatal("a found directory already as declared is the mesh's")
|
||||
}
|
||||
if action, _, err := remove(context.Background(), nil, store.Applied{Type: "directory", Target: dir, AsFound: true}, nil, nil); err != nil || action != "forgotten" {
|
||||
t.Fatalf("a directory used as found is never removed: %s %v", action, err)
|
||||
}
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
t.Fatal("it is still there")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user