//go:build linux package apply import ( "context" "errors" "fmt" "time" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // The take act on a library, and the walk that goes on with one stopped at its bound (novox/hq ADR 0328 §7). // actRecord is a walk's counts as the record keeps them. func actRecord(warrant, by, on string, started time.Time, w *treeWalk, err error) *store.KeptAct { c := w.counts rec := &store.KeptAct{Act: "take", Warrant: warrant, By: by, On: on, State: "done", Started: started, Ended: time.Now().UTC(), Folders: c.Folders, Files: c.Files, Kept: c.Kept, Links: c.Links, Other: c.Other, Mounts: c.Mounts, Shared: c.Shared} switch { case errors.Is(err, ErrWalkBound): rec.State, rec.Why = "stopped", "it met its bound of entries or time; it goes on from where it stopped" case err != nil: rec.State, rec.Why = "failed", err.Error() case w.Failed != "": rec.State = "failed" rec.Why = fmt.Sprintf("%d entries could not be given the group: %s; walked again on the next look", c.Failed, w.Failed) } return rec } // TakeLibrary is the take act (ADR 0328 §7): on the operator's warrant, the library's group, group read and write, // and on folders group enter and setgid, set on its root and everything under it — no link followed, no other // filesystem entered, the owner and the owner's and others' bits unchanged, a file with another name elsewhere left // as it is. It judges the ask against the declaration last applied and the root as it is, and starts the walk: what // it answers at once, and a channel closed when the walk is through or stopped at its bound. Refused, nothing is // changed. func TakeLibrary(ctx context.Context, d *declaration.Declaration, access, path, warrant, by string) (<-chan struct{}, string, error) { if d == nil { return nil, "", errors.New("this engine has applied no declaration, and takes no library it was not told of") } var lib *declaration.Library for i := range d.Libraries { if LibraryAccess(d.Libraries[i].Library) == access { lib = &d.Libraries[i] } } switch { case lib == nil: return nil, "", fmt.Errorf("the access %s names no library declared on this machine", access) case !lib.Home: return nil, "", fmt.Errorf("this machine is not the home of the library %s, and a library is taken on its "+ "home", lib.Library) case lib.Root != path: return nil, "", fmt.Errorf("the library %s is declared at another root than the one asked", lib.Library) } lock := libraryLock(lib.Library) if !lock.TryLock() { return nil, "", errors.New("a walk of the library is running; asked again once it is through, the act goes on") } rec, _ := libraryRecord(lib.Library) v := assessLibrary(ctx, *lib, rec) var refused error switch { case v.err != nil: refused = v.err case v.kind == libraryWaitDisk: refused = errors.New(v.why) case v.kind == libraryUnknown: refused = errors.New(v.why + ", so a warrant could name no filesystem it was given for") } if refused != nil { lock.Unlock() return nil, "", refused } if _, _, err := keepFolders(lib.Root, lib.Paths, lib.GID); err != nil { lock.Unlock() return nil, "", err } started := time.Now().UTC() _ = Libraries.Update(lib.Library, func(r *store.LibraryRecord, _ bool) bool { r.Root, r.Mount, r.Placed, r.Fs, r.GID = lib.Root, v.mount, v.placed, v.fs, lib.GID r.Kept, r.KeptOn, r.How, r.At = true, v.placed, store.KeptByWarrant, started r.Warrant, r.By, r.Cursor = warrant, by, nil r.Act = &store.KeptAct{Act: "take", Warrant: warrant, By: by, On: v.placed, State: "running", Started: started} return true }) done := make(chan struct{}) go func() { defer close(done) defer lock.Unlock() walkLibrary(lib.Library, lib.Root, lib.GID, warrant, by, v.placed, nil) }() return done, fmt.Sprintf("the take act started under the warrant %s: %s's group, group read and write, and "+ "setgid on its folders are set on everything under its root, following no link and entering no other "+ "filesystem", warrant, lib.Library), nil } // walkLibrary walks a library's root from resume with its group, and records what it did: a walk stopped at its // bound keeps its cursor for the next. The caller holds the library's lock. func walkLibrary(name, root string, gid int, warrant, by, on string, resume []string) { started := time.Now().UTC() w, err := walkRoot(root, ActLimits, machineStats, resume, groupVisit(gid)) rec := actRecord(warrant, by, on, started, w, err) _ = Libraries.Update(name, func(r *store.LibraryRecord, _ bool) bool { r.Act = rec r.Cursor = nil if errors.Is(err, ErrWalkBound) { r.Cursor = w.Cursor } return true }) } // ReconcileLibraries goes on with every take act that stopped at its bound or left entries it could not change, // from where it stopped — only while the library is still kept on the filesystem it was taken on (judged as an apply // judges it). What it did, in words, for the engine to say. func ReconcileLibraries(ctx context.Context, d *declaration.Declaration) []string { if d == nil { return nil } var said []string for _, l := range d.Libraries { if ctx.Err() != nil { break } if !l.Home { continue } rec, _ := libraryRecord(l.Library) if rec.Act == nil || (rec.Act.State != "stopped" && rec.Act.State != "failed") || rec.How != store.KeptByWarrant { continue } lock := libraryLock(l.Library) if !lock.TryLock() { continue } v := assessLibrary(ctx, l, rec) if v.err != nil || v.kind != libraryKeep || v.placed != rec.KeptOn || v.moved { lock.Unlock() continue } walkLibrary(l.Library, l.Root, l.GID, rec.Warrant, rec.By, rec.KeptOn, rec.Cursor) lock.Unlock() after, _ := libraryRecord(l.Library) said = append(said, fmt.Sprintf("the library %s: the take act went on, %s (%d folders and %d files changed)", l.Library, after.Act.State, after.Act.Folders, after.Act.Files)) } return said }