WIP: recheck a take's filesystem, follow only the module's branch, count users in an access change, hold the identity until its data is taken (hq ADR 0325 review)
This commit is contained in:
@@ -257,6 +257,9 @@ type Rendering struct {
|
||||
Identities map[string]ModuleIdentity
|
||||
// KeptFolders is every folder the operator let the mesh keep on this machine (ADR 0325 §8), by its clean path.
|
||||
KeptFolders map[string]bool
|
||||
// ReadyFolders is every folder the engine states it keeps on this machine (its access healthy), by clean path: a
|
||||
// folder it found empty and kept, or took itself. With KeptFolders, what lets a container switch to its identity.
|
||||
ReadyFolders map[string]bool
|
||||
}
|
||||
|
||||
// RootField is a user resource's field saying the account must never become root without a person
|
||||
@@ -583,6 +586,9 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
*identities = append(*identities, ident)
|
||||
}
|
||||
mountedByIdentity := dirsMountedByIdentity(m)
|
||||
// Its containers run as it only once every read-write folder they mount is taken (the operator's question on
|
||||
// ADR 0325): until then the identity is allocated, sent and named in the take's users, and not switched to.
|
||||
runsAsIdent := hasIdent && !identityHeld(m, with)
|
||||
|
||||
// What the mesh computes for this module goes FIRST, before the module's own resources.
|
||||
//
|
||||
@@ -1036,7 +1042,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
ownerInto(copied, placed)
|
||||
// And its identity (novox/hq ADR 0325 §2, §3): a container's resolved, or taken out where none is composed;
|
||||
// the owner of a directory its identity's containers mount. After the placement's owner: the identity wins.
|
||||
if err := identityInto(copied, m, ident, hasIdent, mountedByIdentity); err != nil {
|
||||
if err := identityInto(copied, m, ident, runsAsIdent, mountedByIdentity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// **After settings, and that is the whole reason it is here.** A module's file
|
||||
|
||||
@@ -187,6 +187,24 @@ func volumeSource(v string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
// AccessUsersShape is who among a module's containers uses which access as the module's identity, in a form that
|
||||
// compares: each container declaring identity with how it is told, and the accesses it mounts, sorted. Two manifests
|
||||
// with the same shape give their accesses the same container users.
|
||||
func AccessUsersShape(m Manifest) string {
|
||||
var out []string
|
||||
for _, r := range m.Resources {
|
||||
v, declares := r[IdentityField]
|
||||
if fmt.Sprint(r["type"]) != "container" || !declares {
|
||||
continue
|
||||
}
|
||||
_, accesses := containerMounts(r)
|
||||
sort.Strings(accesses)
|
||||
out = append(out, fmt.Sprint(r["id"], v, accesses))
|
||||
}
|
||||
sort.Strings(out)
|
||||
return strings.Join(out, "\x01")
|
||||
}
|
||||
|
||||
// accessMode is the mode an access a container mounts was declared at, by id or by "path:<path>"; "" when the
|
||||
// manifest declares no such access.
|
||||
func accessMode(m Manifest, ref string) string {
|
||||
@@ -387,6 +405,49 @@ func identityInto(resource map[string]any, m Manifest, id ModuleIdentity, has bo
|
||||
return nil
|
||||
}
|
||||
|
||||
// identityHeld says module m's containers are not switched to its identity yet (the operator's question on ADR 0325):
|
||||
// a container declaring `identity` mounts a read-write access whose folder is neither taken (KeptFolders) nor kept by
|
||||
// the engine (ReadyFolders). Switched before the take adds the identity's entry, the app would run as the new uid and
|
||||
// lose write on the operator's existing files; held, it runs as it did (its environment's PUID and PGID), the take's
|
||||
// users already name the identity, and the switch comes in the apply after every such folder is taken.
|
||||
func identityHeld(m Manifest, with Rendering) bool {
|
||||
_, byID, err := accessesFor(m, with.Settings[m.Module])
|
||||
if err != nil {
|
||||
return true // refused where the module itself is composed; never switched on a guess
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if _, declares := r[IdentityField]; !declares || fmt.Sprint(r["type"]) != "container" {
|
||||
continue
|
||||
}
|
||||
_, refs := containerMounts(r)
|
||||
for _, ref := range refs {
|
||||
if accessMode(m, ref) != AccessReadWrite {
|
||||
continue
|
||||
}
|
||||
path := byID[ref]
|
||||
if strings.HasPrefix(ref, "path:") {
|
||||
path = strings.TrimPrefix(ref, "path:")
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if path != "." && !with.KeptFolders[path] && !with.ReadyFolders[path] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// identityContainers is the composed ids of module m's containers that declare `identity`, whether or not it is sent.
|
||||
func identityContainers(m Manifest) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, r := range m.Resources {
|
||||
if _, declares := r[IdentityField]; declares && fmt.Sprint(r["type"]) == "container" {
|
||||
out[m.Module+"."+fmt.Sprint(r["id"])] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dirsMountedByIdentity is the ids of module m's directories one of its containers declaring `identity` mounts.
|
||||
func dirsMountedByIdentity(m Manifest) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
@@ -442,12 +503,13 @@ func (r Resolution) toolsAccount(m Manifest, with Rendering) string {
|
||||
// module's tools run as it, each at the access's use (§4: the engine keeps the widest).
|
||||
func usersInto(out []map[string]any, owner map[string]string, m Manifest, id ModuleIdentity, has bool, account string,
|
||||
with Rendering) {
|
||||
// The containers that declare identity, whether or not it is sent yet: the take adds the entries of the identity
|
||||
// to come, so they exist before the switch.
|
||||
declaring := identityContainers(m)
|
||||
var containers []map[string]any
|
||||
for _, res := range out {
|
||||
if res["type"] == "container" && owner[fmt.Sprint(res["id"])] == m.Module {
|
||||
if _, declares := res[IdentityField]; declares && has {
|
||||
containers = append(containers, res)
|
||||
}
|
||||
if res["type"] == "container" && owner[fmt.Sprint(res["id"])] == m.Module && declaring[fmt.Sprint(res["id"])] && has {
|
||||
containers = append(containers, res)
|
||||
}
|
||||
}
|
||||
for _, res := range out {
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestAModuleRunsAsItsIdentityTheSameOnEveryMachine(t *testing.T) {
|
||||
var seen []string
|
||||
for _, node := range []string{"ace", "novox"} {
|
||||
r := Resolution{Node: node, Account: "jochen", Modules: []Manifest{aLibrarian(t), aPlayer(t), theRuntime(t)}}
|
||||
composed := composeFolders(t, r, keepingFolders(identities))
|
||||
composed := composeFolders(t, r, keepingFolders(identities, "/storage/media/movies")) // its read-write folder taken: switched
|
||||
got := byID(composed.Resources)
|
||||
|
||||
app := got["librarian.app"]
|
||||
@@ -331,3 +331,40 @@ func TestAModuleWithoutIdentityIsWarnedNotRefused(t *testing.T) {
|
||||
t.Error("an identity module was warned rather than refused")
|
||||
}
|
||||
}
|
||||
|
||||
// A container is not switched to its identity while a read-write folder it mounts holds data the mesh has not taken
|
||||
// (the operator's question on ADR 0325): the app would run as the new uid before the take adds its entry, and lose
|
||||
// write on the existing files. Its identity is held — no `identity` sent, its directories keep their owner — while
|
||||
// the take's users already name the identity, so the entries exist before the switch; once every such folder is
|
||||
// taken (or kept by the engine), the identity is sent in one apply.
|
||||
func TestAContainerKeepsItsOldIdentityUntilItsDataIsTaken(t *testing.T) {
|
||||
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aLibrarian(t), theRuntime(t)}}
|
||||
held := byID(composeFolders(t, r, keepingFolders(identities)).Resources)
|
||||
if _, has := held["librarian.app"][IdentityField]; has {
|
||||
t.Fatalf("the container was told its identity while its movies are not taken: %v", held["librarian.app"][IdentityField])
|
||||
}
|
||||
if owner, has := held["librarian.config"]["owner"]; has {
|
||||
t.Fatalf("its directory was given the owner %v while its identity is held", owner)
|
||||
}
|
||||
users, _ := json.Marshal(held["librarian."+AccessID("/storage/media/movies")]["users"])
|
||||
if !strings.Contains(string(users), `"identity":"mesh-librarian","uid":50001`) {
|
||||
t.Fatalf("the access does not name the identity to come, so the take would not add its entry: %s", users)
|
||||
}
|
||||
|
||||
for name, with := range map[string]Rendering{
|
||||
"taken": keepingFolders(identities, "/storage/media/movies"),
|
||||
"kept by the engine": func() Rendering {
|
||||
w := keepingFolders(identities)
|
||||
w.ReadyFolders = map[string]bool{"/storage/media/movies": true}
|
||||
return w
|
||||
}(),
|
||||
} {
|
||||
got := byID(composeFolders(t, r, with).Resources)
|
||||
if _, has := got["librarian.app"][IdentityField]; !has {
|
||||
t.Errorf("%s: the container is not told its identity", name)
|
||||
}
|
||||
if got["librarian.config"]["owner"] != "50001:50001" {
|
||||
t.Errorf("%s: its directory is owned by %v", name, got["librarian.config"]["owner"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user