Keep libraries under groups of their own, and give modules users in them (hq ADR 0328, issue 567)
ADR 0328 supersedes ADR 0325: shared data is a library with one home, kept usable by its group rather than by ACL entries. The engine now makes each library's group and each module's user in its libraries' groups; keeps a library's root and requested sub-paths in its group with group write and setgid on its home, making missing sub-paths as the root is; waits for the operator's word before touching a root that holds data, and on the take act walks it by descriptor (no link, same filesystem, bounded and resumed, files with other names left); holds the containers of a library whose disk or mount is not there; and puts the tool runner's account in the group. The ACL keeping, mask repair and watch are gone; an access is again as before ADR 0325.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LibrariesName is the file, beside the node's state, of every library this machine is home to (novox/hq ADR 0328
|
||||
// §6, §7, §9): which filesystem its root was kept on, whether and how it is kept, and the last walk under it. Its own
|
||||
// file for the reason the hand-overs have theirs: the take act runs beside an apply, whose save of the state would
|
||||
// lose a record written into it meanwhile.
|
||||
const LibrariesName = "libraries.json"
|
||||
|
||||
// LibrariesPath is that file beside the state at statePath.
|
||||
func LibrariesPath(statePath string) string {
|
||||
return filepath.Join(filepath.Dir(statePath), LibrariesName)
|
||||
}
|
||||
|
||||
// How a library came to be kept.
|
||||
const (
|
||||
// KeptByWarrant is a root that held data, taken on the operator's word (ADR 0328 §7).
|
||||
KeptByWarrant = "warrant"
|
||||
// KeptEmpty is a root that held nothing yet when it was first kept.
|
||||
KeptEmpty = "empty"
|
||||
// KeptInShape is a root found already in the library's group with group write and setgid.
|
||||
KeptInShape = "in-shape"
|
||||
)
|
||||
|
||||
// LibraryRecord is one library as the engine keeps it on its home. Counts, names and mount points only beside its
|
||||
// root: never a path under it.
|
||||
type LibraryRecord struct {
|
||||
Library string `json:"library"`
|
||||
Root string `json:"root"`
|
||||
// GID is the library's group as it was last kept: what a walk stopped at its bound goes on with.
|
||||
GID int `json:"gid,omitempty"`
|
||||
// Mount is the mount point that held the root when the engine first saw it; Placed and Fs name that filesystem
|
||||
// (the engine's PlacedID, and what it was made from: UUID, filesystem id or source).
|
||||
Mount string `json:"mount,omitempty"`
|
||||
Placed string `json:"placed,omitempty"`
|
||||
Fs string `json:"fs,omitempty"`
|
||||
// Kept is that the mesh keeps it, on the filesystem KeptOn names (a Placed): another filesystem at the root is
|
||||
// judged afresh. How is why; Warrant and By the warrant and who asked, for one taken on a warrant.
|
||||
Kept bool `json:"kept,omitempty"`
|
||||
KeptOn string `json:"kept_on,omitempty"`
|
||||
How string `json:"how,omitempty"`
|
||||
Warrant string `json:"warrant,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
At time.Time `json:"at,omitzero"`
|
||||
// Act is the last walk under the root; Cursor where a walk that met its bound stopped (the names of each level
|
||||
// below the root, never said), for the next to go on from.
|
||||
Act *KeptAct `json:"act,omitempty"`
|
||||
Cursor []string `json:"cursor,omitempty"`
|
||||
}
|
||||
|
||||
// KeptAct is one walk under a library's root, as the engine records it. Counts only: never a path.
|
||||
type KeptAct struct {
|
||||
Act string `json:"act"`
|
||||
Warrant string `json:"warrant,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
State string `json:"state"`
|
||||
Started time.Time `json:"started"`
|
||||
Ended time.Time `json:"ended,omitzero"`
|
||||
// On names the filesystem it walked (a Placed).
|
||||
On string `json:"on,omitempty"`
|
||||
// Folders and Files are the entries changed; Kept the ones that already had what they need; Links the links
|
||||
// passed by unfollowed; Other the entries neither a file nor a folder; Mounts the folders on another filesystem
|
||||
// not entered; Shared the files with another name elsewhere (more than one link), left as they are.
|
||||
Folders int `json:"folders,omitempty"`
|
||||
Files int `json:"files,omitempty"`
|
||||
Kept int `json:"kept,omitempty"`
|
||||
Links int `json:"links,omitempty"`
|
||||
Other int `json:"other,omitempty"`
|
||||
Mounts int `json:"mounts,omitempty"`
|
||||
Shared int `json:"shared,omitempty"`
|
||||
// Why is what stopped or failed it, in words with no path.
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// LibraryFile is the libraries' file, one writer at a time in this process.
|
||||
type LibraryFile struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewLibraryFile is the libraries' file beside the state at statePath.
|
||||
func NewLibraryFile(statePath string) *LibraryFile {
|
||||
return &LibraryFile{path: LibrariesPath(statePath)}
|
||||
}
|
||||
|
||||
// All is every library's record, by name. None is an empty map; a file that cannot be read is an error.
|
||||
func (k *LibraryFile) All() (map[string]LibraryRecord, error) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
return k.load()
|
||||
}
|
||||
|
||||
// Update changes one library's record under the file's lock: change is given the record (empty when there is none)
|
||||
// and whether there was one, and says whether to keep it; a record not kept is removed.
|
||||
func (k *LibraryFile) Update(name string, change func(r *LibraryRecord, had bool) bool) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
all, err := k.load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, had := all[name]
|
||||
r.Library = name
|
||||
if change(&r, had) {
|
||||
all[name] = r
|
||||
} else {
|
||||
delete(all, name)
|
||||
}
|
||||
return k.save(all)
|
||||
}
|
||||
|
||||
func (k *LibraryFile) load() (map[string]LibraryRecord, error) {
|
||||
raw, err := os.ReadFile(k.path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return map[string]LibraryRecord{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading the libraries: %w", err)
|
||||
}
|
||||
out := map[string]LibraryRecord{}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, fmt.Errorf("the libraries (%s) are unreadable: %w", k.path, err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (k *LibraryFile) save(all map[string]LibraryRecord) error {
|
||||
raw, err := json.MarshalIndent(all, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(k.path), ".libraries-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
if _, err := tmp.Write(append(raw, '\n')); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp.Name(), k.path)
|
||||
}
|
||||
Reference in New Issue
Block a user