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:
@@ -1,212 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// KeptName is the file, beside the node's state, of every shared folder the engine keeps (novox/hq ADR 0325 §5):
|
||||
// whose entries it added, on which filesystem it was placed, whether it was taken and under which warrant, and how
|
||||
// far its sweep got. Its own file for the reason the hand-overs have theirs: the take act, the watch and the sweep
|
||||
// run beside an apply, whose save of the state would lose a record written into it meanwhile.
|
||||
const KeptName = "kept-folders.json"
|
||||
|
||||
// KeptPath is that file beside the state at statePath.
|
||||
func KeptPath(statePath string) string {
|
||||
return filepath.Join(filepath.Dir(statePath), KeptName)
|
||||
}
|
||||
|
||||
// KeptUser is one identity the mesh keeps an entry for on a folder: its name, its number, its use and where it
|
||||
// runs (a container, or the tool runner standing for every module whose tools use the folder, ADR 0325 §4).
|
||||
type KeptUser struct {
|
||||
Identity string `json:"identity"`
|
||||
UID uint32 `json:"uid"`
|
||||
Use string `json:"use"`
|
||||
From string `json:"from"`
|
||||
}
|
||||
|
||||
// How a folder came to be kept.
|
||||
const (
|
||||
// KeptByWarrant is a folder that held data, taken on the operator's word (ADR 0325 §8).
|
||||
KeptByWarrant = "warrant"
|
||||
// KeptEmpty is a folder that held nothing yet when it was first kept (§5).
|
||||
KeptEmpty = "empty"
|
||||
// KeptMade is a folder the engine made under a parent on its placed filesystem (§9).
|
||||
KeptMade = "made"
|
||||
)
|
||||
|
||||
// KeptFolder is one shared folder the engine keeps, or has seen and waits to keep. Counts and mount points only
|
||||
// beside its own path: never a path under it.
|
||||
type KeptFolder struct {
|
||||
Path string `json:"path"`
|
||||
// Mount is the mount point of the filesystem the folder was placed on when the engine first saw it there:
|
||||
// later, a nearest existing parent on another one is the disk not there (ADR 0325 §9).
|
||||
Mount string `json:"mount,omitempty"`
|
||||
// Kept is that the mesh keeps it; How why (KeptByWarrant, KeptEmpty, KeptMade); Warrant and By the warrant and
|
||||
// who asked, for a folder taken on one; At since when.
|
||||
Kept bool `json:"kept,omitempty"`
|
||||
How string `json:"how,omitempty"`
|
||||
Warrant string `json:"warrant,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
At time.Time `json:"at,omitzero"`
|
||||
// Users are the identities whose entries are on everything under the folder; Want what the last apply
|
||||
// declared. Where they differ a walk adds or removes exactly the difference.
|
||||
Users []KeptUser `json:"users,omitempty"`
|
||||
Want []KeptUser `json:"want,omitempty"`
|
||||
// IDs are the accesses that last named the folder: what a leftover is said under once none does.
|
||||
IDs []string `json:"ids,omitempty"`
|
||||
// Placed names the filesystem the folder was kept on (the engine's PlacedID: its mount point, source and type),
|
||||
// so another filesystem mounted at the same point is a move, as another mount point is.
|
||||
Placed string `json:"placed,omitempty"`
|
||||
// Fs is what Placed was made from beside the mount point: "uuid:<the filesystem's UUID>", "fsid:<statfs's id>"
|
||||
// or "source:<type>:<mountinfo's source>". Kept on this machine only, never said. Absent on a record from an
|
||||
// earlier build, whose Placed is then not compared.
|
||||
Fs string `json:"fs,omitempty"`
|
||||
// TakenOn names, as Placed does, the filesystem the operator's warrant was carried out on (ADR 0325 §8): the
|
||||
// controller's word that the folder was taken holds for that filesystem alone, and a folder moved to another
|
||||
// asks again. One that names no filesystem this way (a mount point, from an unmerged build) is not taken.
|
||||
TakenOn string `json:"taken_on,omitempty"`
|
||||
// Left is the entries the mesh added on a filesystem no longer at the folder (a disk mounted over it, or
|
||||
// one gone): taken out by a drop walk once that filesystem holds the folder again, said until then.
|
||||
Left []KeptLeft `json:"left,omitempty"`
|
||||
// Act is the last walk under the folder: a take, an add or a drop.
|
||||
Act *KeptAct `json:"act,omitempty"`
|
||||
// Sweep is how far the sweep got (ADR 0325 §10).
|
||||
Sweep KeptSweep `json:"sweep"`
|
||||
// Watches is how many folders under it are watched; OutOfWatches that the machine's watch limit was met, and
|
||||
// the sweep alone finds what is made where no watch is (§6).
|
||||
Watches int `json:"watches,omitempty"`
|
||||
OutOfWatches bool `json:"out_of_watches,omitempty"`
|
||||
}
|
||||
|
||||
// KeptLeft is the mesh's entries left on one filesystem: its mount point and whose entries they are.
|
||||
type KeptLeft struct {
|
||||
Mount string `json:"mount"`
|
||||
// Placed names that filesystem as KeptFolder.Placed does: entries left on the filesystem the folder is on
|
||||
// now are the folder's own, never walked away.
|
||||
Placed string `json:"placed,omitempty"`
|
||||
Fs string `json:"fs,omitempty"`
|
||||
Users []KeptUser `json:"users"`
|
||||
// Since is when they were left: a filesystem not back at its mount point within LeftFor is gone, and what was
|
||||
// left on it is forgotten (said once).
|
||||
Since time.Time `json:"since,omitzero"`
|
||||
}
|
||||
|
||||
// KeptAct is one walk under a kept folder, as the engine records it. Counts only: never a path.
|
||||
type KeptAct struct {
|
||||
// Act is take, add or drop.
|
||||
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"`
|
||||
// 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.
|
||||
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"`
|
||||
// Why is what stopped or failed it, in words with no path.
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// KeptSweep is the sweep's place in a kept folder: where the last look stopped, below the folder, by the names
|
||||
// of each level (never said, only resumed from); when it last covered the folder whole; and what this pass found.
|
||||
type KeptSweep struct {
|
||||
Cursor []string `json:"cursor,omitempty"`
|
||||
Covered time.Time `json:"covered,omitzero"`
|
||||
// Read is how many entries this pass read so far; Narrowed how many gave a user less than its right and were
|
||||
// put back; Unrepaired how many could not be (an owner's bits, which the mesh never changes, or a write that
|
||||
// failed).
|
||||
Read int `json:"read,omitempty"`
|
||||
Narrowed int `json:"narrowed,omitempty"`
|
||||
Unrepaired int `json:"unrepaired,omitempty"`
|
||||
// LastUnrepaired is what the last whole pass could not put back: said until the next pass is through.
|
||||
LastUnrepaired int `json:"last_unrepaired,omitempty"`
|
||||
// Bound is that the last look ran out of its bound before the folder was covered.
|
||||
Bound bool `json:"bound,omitempty"`
|
||||
}
|
||||
|
||||
// KeptFile is the kept folders' file, one writer at a time in this process.
|
||||
type KeptFile struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewKeptFile is the kept folders' file beside the state at statePath.
|
||||
func NewKeptFile(statePath string) *KeptFile { return &KeptFile{path: KeptPath(statePath)} }
|
||||
|
||||
// All is every kept or seen folder, by path. None is an empty map; a file that cannot be read is an error.
|
||||
func (k *KeptFile) All() (map[string]KeptFolder, error) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
return k.load()
|
||||
}
|
||||
|
||||
// Update changes the record of one folder 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 *KeptFile) Update(path string, change func(f *KeptFolder, had bool) bool) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
all, err := k.load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, had := all[path]
|
||||
f.Path = path
|
||||
if change(&f, had) {
|
||||
all[path] = f
|
||||
} else {
|
||||
delete(all, path)
|
||||
}
|
||||
return k.save(all)
|
||||
}
|
||||
|
||||
func (k *KeptFile) load() (map[string]KeptFolder, error) {
|
||||
raw, err := os.ReadFile(k.path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return map[string]KeptFolder{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading the kept folders: %w", err)
|
||||
}
|
||||
out := map[string]KeptFolder{}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, fmt.Errorf("the kept folders (%s) are unreadable: %w", k.path, err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (k *KeptFile) save(all map[string]KeptFolder) error {
|
||||
raw, err := json.MarshalIndent(all, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(k.path), ".kept-folders-*.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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+23
-1
@@ -242,10 +242,32 @@ type State struct {
|
||||
// assigned again finds the tunnel's configuration retired rather than missing.
|
||||
Retired []Retired `json:"retired,omitempty"`
|
||||
|
||||
// Identities is every module identity the engine made here (novox/hq ADR 0325 §3): its account and group,
|
||||
// Identities is every module identity the engine made here (novox/hq ADR 0328 §4): its account and group,
|
||||
// by name and number, and which of the two it made. An identity no longer declared is removed, and only
|
||||
// what the engine made: an account or group that was there before is never the mesh's to remove.
|
||||
Identities []MadeIdentity `json:"identities,omitempty"`
|
||||
|
||||
// Groups is every library group the engine made here (novox/hq ADR 0328 §3), removed when no longer declared;
|
||||
// a group that was there before is never the mesh's to remove.
|
||||
Groups []MadeGroup `json:"groups,omitempty"`
|
||||
|
||||
// Members is every library group the engine put a tool runner's account in that it was not in before (ADR 0328
|
||||
// §11, as ADR 0252 records an account's groups): taken back when no longer declared.
|
||||
Members []Membership `json:"members,omitempty"`
|
||||
}
|
||||
|
||||
// MadeGroup is one library group the engine made.
|
||||
type MadeGroup struct {
|
||||
Name string `json:"name"`
|
||||
GID int `json:"gid"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// Membership is one account the engine put in one library's group.
|
||||
type Membership struct {
|
||||
Account string `json:"account"`
|
||||
Group string `json:"group"`
|
||||
Library string `json:"library"`
|
||||
}
|
||||
|
||||
// MadeIdentity is one module identity as the engine made it.
|
||||
|
||||
Reference in New Issue
Block a user