package apply import ( "fmt" "os" "strconv" "strings" "syscall" ) // Applier makes this machine match one kind of resource, and reads back to prove it took. // // Read-back is not optional and it is not this package's habit alone: setting a value is not // evidence the value took (novox/hq how-we-build §5, and 05-the-node-host.md as a component // requirement). So Apply writes AND confirms, and returns an error if the machine does not then // match — a firewall is asked whether the rule loaded, and a file is read back byte for byte. type Applier interface { // Type is the resource type this handles — the key in the shape table. Type() string // Apply makes the machine match r and reads back to confirm. created reports whether the // host brought the resource into being (as opposed to adopting one already present), which // is what the store needs so removal never deletes what the host did not create. Apply(r Resource) (created bool, err error) // Remove undoes a resource the host created. Only ever called for a store Record whose // Created is true, and written to never destroy data it did not put there. Remove(rec Record) error } // Appliers is the set of types this host can apply, keyed by type name. func Appliers() map[string]Applier { return map[string]Applier{ "directory": directoryApplier{}, "file": fileApplier{}, } } // --- directory --- type directoryApplier struct{} func (directoryApplier) Type() string { return "directory" } func (directoryApplier) Apply(r Resource) (bool, error) { path := r.Path() mode, err := parseMode(r.stringField("mode"), 0o755) if err != nil { return false, err } created := false info, statErr := os.Lstat(path) switch { case statErr == nil: if !info.IsDir() { return false, fmt.Errorf("%s exists and is not a directory", path) } case os.IsNotExist(statErr): if err := os.Mkdir(path, mode); err != nil { return false, fmt.Errorf("creating %s: %w", path, err) } created = true default: return false, fmt.Errorf("inspecting %s: %w", path, statErr) } if err := os.Chmod(path, mode); err != nil { return created, fmt.Errorf("setting mode on %s: %w", path, err) } if err := applyOwner(path, r.stringField("owner")); err != nil { return created, err } // Read back: the directory must now exist, be a directory, and hold the mode and owner // asked for. Anything else is a value that did not take. if err := verifyPathState(path, true, mode, r.stringField("owner")); err != nil { return created, fmt.Errorf("%s did not take: %w", path, err) } return created, nil } func (directoryApplier) Remove(rec Record) error { // os.Remove, never RemoveAll: it fails on a non-empty directory, and that failure is the // point. A directory the host created but that now holds something is not the host's to // delete — data outlives the mesh that declared it (ADR 0030). err := os.Remove(rec.Path) if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("removing directory %s (left in place): %w", rec.Path, err) } return nil } // --- file --- type fileApplier struct{} func (fileApplier) Type() string { return "file" } func (fileApplier) Apply(r Resource) (bool, error) { path := r.Path() mode, err := parseMode(r.stringField("mode"), 0o644) if err != nil { return false, err } content := []byte(r.stringField("content")) _, statErr := os.Lstat(path) created := os.IsNotExist(statErr) if statErr != nil && !created { return false, fmt.Errorf("inspecting %s: %w", path, statErr) } // Idempotent: the file is rewritten only when the bytes differ, so applying the same // declaration twice changes nothing the second time. Mode and owner are still reconciled // below, because those can drift without the content doing so. needsWrite := created if !created { existing, readErr := os.ReadFile(path) needsWrite = readErr != nil || string(existing) != string(content) } if needsWrite { if err := os.WriteFile(path, content, mode); err != nil { return created, fmt.Errorf("writing %s: %w", path, err) } } if err := os.Chmod(path, mode); err != nil { return created, fmt.Errorf("setting mode on %s: %w", path, err) } if err := applyOwner(path, r.stringField("owner")); err != nil { return created, err } // Read back: the file must now hold exactly these bytes and this mode. A file whose content // was composed on the machine, or whose write was short, is a value that did not take. got, err := os.ReadFile(path) if err != nil { return created, fmt.Errorf("%s did not take: reading it back: %w", path, err) } if string(got) != string(content) { return created, fmt.Errorf("%s did not take: content read back does not match", path) } if err := verifyPathState(path, false, mode, r.stringField("owner")); err != nil { return created, fmt.Errorf("%s did not take: %w", path, err) } return created, nil } func (fileApplier) Remove(rec Record) error { err := os.Remove(rec.Path) if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("removing file %s: %w", rec.Path, err) } return nil } // --- shared --- func parseMode(s string, fallback os.FileMode) (os.FileMode, error) { if s == "" { return fallback, nil } n, err := strconv.ParseUint(s, 8, 32) if err != nil { return 0, fmt.Errorf("mode %q is not an octal number like \"0700\": %w", s, err) } return os.FileMode(n), nil } // applyOwner sets uid:gid when an owner is named. Owners are numeric because a container's user // has no name on the machine (novox/mesh-control: "an owner may be numeric"). An empty owner is // left untouched — not every resource asserts one. func applyOwner(path, owner string) error { if owner == "" { return nil } uid, gid, err := parseOwner(owner) if err != nil { return err } if err := os.Chown(path, uid, gid); err != nil { return fmt.Errorf("setting owner %s on %s: %w", owner, path, err) } return nil } func parseOwner(owner string) (int, int, error) { parts := strings.SplitN(owner, ":", 2) if len(parts) != 2 { return 0, 0, fmt.Errorf("owner %q is not \"uid:gid\"", owner) } uid, err := strconv.Atoi(parts[0]) if err != nil { return 0, 0, fmt.Errorf("owner %q: uid is not a number", owner) } gid, err := strconv.Atoi(parts[1]) if err != nil { return 0, 0, fmt.Errorf("owner %q: gid is not a number", owner) } return uid, gid, nil } // verifyPathState reads back mode and (when asserted) owner, and reports the first mismatch. func verifyPathState(path string, wantDir bool, mode os.FileMode, owner string) error { info, err := os.Lstat(path) if err != nil { return err } if info.IsDir() != wantDir { return fmt.Errorf("expected directory=%v, found directory=%v", wantDir, info.IsDir()) } if info.Mode().Perm() != mode.Perm() { return fmt.Errorf("expected mode %04o, found %04o", mode.Perm(), info.Mode().Perm()) } if owner != "" { wantUID, wantGID, err := parseOwner(owner) if err != nil { return err } st, ok := info.Sys().(*syscall.Stat_t) if !ok { return fmt.Errorf("cannot read owner back on this platform") } if int(st.Uid) != wantUID || int(st.Gid) != wantGID { return fmt.Errorf("expected owner %d:%d, found %d:%d", wantUID, wantGID, st.Uid, st.Gid) } } return nil }