Hold a joining module at the build it ran, own its directories by its own group, and take a library only when its act is done (hq ADR 0328 review, ADR 0334)
A held module is composed exactly as before it joined, at the build its machine runs, so its port, resources and contributions stay. A joining module's directories belong to its user and its own group, and its tools' directories are refused closed to that group; the runner joins it where the engine can say so. A take records the root and is taken only when the engine states the act done. Libraries' paths and verdicts count modules pulled in by a requirement, and a unit takes the identity.
This commit is contained in:
@@ -261,9 +261,10 @@ type Rendering struct {
|
||||
LibraryGroups map[string]int
|
||||
TakenLibraries map[string]bool
|
||||
LibraryPaths map[string][]string
|
||||
// Held is, by module, the library a joining module that ran here before waits on: until every library it uses
|
||||
// is taken its containers keep the user they ran as, so it is left out of the declaration (§8).
|
||||
Held map[string]string
|
||||
// RunnerJoinsOwnGroups says this machine's engine puts the tool runner's account in a joining module's own group
|
||||
// (novox/hq ADR 0334 §2: its statement's contract is link.OwnGroupContract or later); to an older one the
|
||||
// identity's runner is not sent.
|
||||
RunnerJoinsOwnGroups bool
|
||||
}
|
||||
|
||||
// RootField is a user resource's field saying the account must never become root without a person
|
||||
|
||||
@@ -371,6 +371,7 @@ func libraryProblems(m Manifest) []string {
|
||||
if !Joins(m) {
|
||||
return problems
|
||||
}
|
||||
problems = append(problems, toolsDirectoryProblems(m)...)
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) == "directory" {
|
||||
if owner, said := r["owner"]; said {
|
||||
@@ -383,6 +384,55 @@ func libraryProblems(m Manifest) []string {
|
||||
return problems
|
||||
}
|
||||
|
||||
// toolsDirectoryProblems refuses a joining module whose tools name one of its directories whose mode closes it to its
|
||||
// group (novox/hq ADR 0334 §3): the tool runner reads it through the module's own group, so it needs group read and
|
||||
// enter.
|
||||
func toolsDirectoryProblems(m Manifest) []string {
|
||||
modes := map[string]string{}
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) == "directory" {
|
||||
mode, _ := r["mode"].(string)
|
||||
modes[fmt.Sprint(r["id"])] = mode
|
||||
}
|
||||
}
|
||||
var words []string
|
||||
for _, b := range m.Bundles {
|
||||
for _, v := range b.Env {
|
||||
words = append(words, v)
|
||||
}
|
||||
}
|
||||
if m.Build != nil {
|
||||
for _, a := range m.Build.Artifacts {
|
||||
for _, v := range a.Env {
|
||||
words = append(words, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
var problems []string
|
||||
for _, w := range words {
|
||||
for _, match := range dirRef.FindAllStringSubmatch(w, -1) {
|
||||
id := match[1]
|
||||
mode, declared := modes[id]
|
||||
if !declared || seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
var n uint64
|
||||
if _, err := fmt.Sscanf(mode, "%o", &n); err != nil || mode == "" {
|
||||
continue // its mode is judged where the directory is
|
||||
}
|
||||
if n&0o050 != 0o050 {
|
||||
problems = append(problems, fmt.Sprintf("%s's tools name its directory %s, whose mode %s closes it to its "+
|
||||
"own group: the tool runner reads it through that group, so it needs group read and enter (0750) "+
|
||||
"(novox/hq ADR 0334 §3)", m.Module, id, mode))
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(problems)
|
||||
return problems
|
||||
}
|
||||
|
||||
// LibraryWarnings is what the module check warns of a module's libraries (§4): an image that drops added groups
|
||||
// reaches only the first library the module uses.
|
||||
func LibraryWarnings(m Manifest) []string {
|
||||
@@ -511,8 +561,9 @@ func joinedOf(m Manifest, with Rendering) (Joined, error) {
|
||||
}
|
||||
|
||||
// libraryLeftOut is why module m is left out of this machine's declaration for its libraries, or "": a library it
|
||||
// uses not defined or not reaching the machine (§2), an engine that cannot be told it joins, or a container held at
|
||||
// the user it ran as until every library it uses is taken (§8).
|
||||
// uses not defined or not reaching the machine (§2), or an engine that cannot be told it joins. A module held at the
|
||||
// user it ran as until its libraries are taken (§8) is not left out: it is composed at the build it ran before it
|
||||
// joined (cmd/mesh-controller holdJoining).
|
||||
func libraryLeftOut(m Manifest, node string, with Rendering) string {
|
||||
if !Joins(m) {
|
||||
return ""
|
||||
@@ -534,10 +585,6 @@ func libraryLeftOut(m Manifest, node string, with Rendering) string {
|
||||
if _, has := with.Identities[m.Module]; !has {
|
||||
return fmt.Sprintf("%s joins, and its number is allocated at its first send", m.Module)
|
||||
}
|
||||
if lib := with.Held[m.Module]; lib != "" {
|
||||
return fmt.Sprintf("%s keeps the user it ran as until the library %s is taken: the operator is asked once "+
|
||||
"(%s §8)", m.Module, lib, ADR0328)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -583,8 +630,10 @@ func joinedInto(resource map[string]any, m Manifest, j Joined, paths map[string]
|
||||
}
|
||||
resource[k] = filled
|
||||
}
|
||||
// Its own directories belong to its user and its own group (ADR 0334 §1): the library's group is its processes'
|
||||
// primary, so what they make in a library keeps that group, and its own data stays its own.
|
||||
if fmt.Sprint(resource["type"]) == "directory" {
|
||||
resource["owner"] = fmt.Sprintf("%d:%d", j.UID, j.PrimaryGID)
|
||||
resource["owner"] = fmt.Sprintf("%d:%d", j.UID, j.UID)
|
||||
}
|
||||
if fmt.Sprint(resource["type"]) != "container" {
|
||||
return nil
|
||||
@@ -734,6 +783,9 @@ type IdentityEntry struct {
|
||||
UID int `json:"uid"`
|
||||
GID int `json:"gid"`
|
||||
Groups []int `json:"groups,omitempty"`
|
||||
// Runner is the tool runner's account, put in this module's own group, where its tools run there (ADR 0334 §2).
|
||||
// Only to an engine that reads it (Rendering.RunnerJoinsOwnGroups).
|
||||
Runner string `json:"runner,omitempty"`
|
||||
}
|
||||
|
||||
// libraryEnvelope is what a machine's declaration says of libraries and joining modules: the group of every library
|
||||
@@ -753,13 +805,20 @@ func (r Resolution) libraryEnvelope(with Rendering, joined []Joined) ([]GroupEnt
|
||||
for _, j := range joined {
|
||||
e := IdentityEntry{Module: j.Module, Name: IdentityName(j.Module), UID: j.UID, GID: j.PrimaryGID,
|
||||
Groups: append([]int(nil), j.AddedGIDs...)}
|
||||
if with.RunnerJoinsOwnGroups {
|
||||
for _, m := range r.Modules {
|
||||
if m.Module == j.Module {
|
||||
e.Runner = r.toolsAccount(m, with)
|
||||
}
|
||||
}
|
||||
}
|
||||
ids = append(ids, e)
|
||||
}
|
||||
sort.Slice(ids, func(a, b int) bool { return ids[a].Module < ids[b].Module })
|
||||
used := map[string]map[string]bool{}
|
||||
runner := map[string]string{}
|
||||
for _, m := range r.Modules {
|
||||
if with.Held[m.Module] == "" && libraryLeftOut(m, r.Node, with) != "" {
|
||||
if libraryLeftOut(m, r.Node, with) != "" {
|
||||
continue
|
||||
}
|
||||
for _, u := range m.Libraries {
|
||||
|
||||
@@ -88,8 +88,8 @@ func TestAJoiningModuleIsComposedWithItsIdentityAndLibraries(t *testing.T) {
|
||||
"PUID=50020\nPGID=50010\nUSER=mesh-radarr\nGROUP=mesh-media\nUMASK=002\n" {
|
||||
t.Errorf("the identity file reads %q", content)
|
||||
}
|
||||
if owner := got["radarr.config"]["owner"]; owner != "50020:50010" {
|
||||
t.Errorf("its directory is owned by %v; want its user and primary group", owner)
|
||||
if owner := got["radarr.config"]["owner"]; owner != "50020:50020" {
|
||||
t.Errorf("its directory is owned by %v; want its user and own group (ADR 0334 §1)", owner)
|
||||
}
|
||||
if len(composed.Identities) != 1 || !reflect.DeepEqual(composed.Identities[0], IdentityEntry{Module: "radarr",
|
||||
Name: "mesh-radarr", UID: 50020, GID: 50010, Groups: []int{50011}}) {
|
||||
@@ -144,37 +144,46 @@ func TestALibraryNotReachingTheMachineLeavesItsModuleOut(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Row 8: a joining module that ran here before keeps the user it ran as until its libraries are taken — left out, its
|
||||
// containers untouched — while its libraries are still named, so the engine states the wait and the take can come.
|
||||
func TestAJoiningModuleIsHeldUntilItsLibrariesAreTaken(t *testing.T) {
|
||||
with := librariesRendering()
|
||||
with.TakenLibraries["media"] = false
|
||||
with.Held = map[string]string{"radarr": "media"}
|
||||
composed := composeLibraries(t, Resolution{Node: "ace", Account: "jochen",
|
||||
Modules: []Manifest{aRadarr(t), theRuntime(t)}}, with)
|
||||
if why := composed.LeftOut["radarr"]; !strings.Contains(why, "keeps the user it ran as until the library media is taken") {
|
||||
t.Fatalf("radarr while media is not taken: %q", why)
|
||||
}
|
||||
if byID(composed.Resources)["radarr.app"] != nil {
|
||||
t.Fatal("a held module's container was sent")
|
||||
}
|
||||
found := false
|
||||
for _, l := range composed.Libraries {
|
||||
found = found || (l.Library == "media" && !l.Taken)
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("the library a held module waits on is not named: %+v", composed.Libraries)
|
||||
}
|
||||
}
|
||||
|
||||
// Row 14: a module that neither declares libraries nor uses its identity is composed exactly as before, on an engine
|
||||
// that keeps libraries or not; and a joining module on an older engine is left out, naming why.
|
||||
// Row 14: a module that neither declares libraries nor uses its identity is composed exactly as before — the whole
|
||||
// declaration: its resources, the filter its port reaches, the contributions it makes — on an engine that keeps
|
||||
// libraries or not; on a machine a library reaches, only that library's group is added. A joining module on an older
|
||||
// engine is left out, naming why.
|
||||
func TestAModuleThatDoesNotJoinIsComposedAsBefore(t *testing.T) {
|
||||
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aPlain(), theRuntime(t)}}
|
||||
before := composeLibraries(t, r, Rendering{})
|
||||
after := composeLibraries(t, r, librariesRendering())
|
||||
if !reflect.DeepEqual(before.Resources, after.Resources) {
|
||||
t.Fatalf("a module that does not join composed otherwise:\n %v\n %v", before.Resources, after.Resources)
|
||||
plain := aPlain()
|
||||
plain.Listens = []Listening{{Port: 8080, From: FromEverywhere, Name: "web", Why: "its page"}}
|
||||
plain.Contributes = map[string]map[string]any{"reverse-proxy": {"host": "plain", "port": 8080}}
|
||||
plain.Requires = []string{"reverse-proxy"}
|
||||
modules := []Manifest{plain, proxy(), {Module: "nftables", Filtering: &Filtering{Into: "/etc/mesh/filter.nft"}},
|
||||
theRuntime(t)}
|
||||
base := Rendering{Mesh: []string{"198.51.100.2"}, OutwardLinks: []string{"eth0"}, TunnelInterface: "mesh0"}
|
||||
keeping := func(node string) Rendering {
|
||||
with := librariesRendering()
|
||||
with.Mesh, with.OutwardLinks, with.TunnelInterface = base.Mesh, base.OutwardLinks, base.TunnelInterface
|
||||
return with
|
||||
}
|
||||
for _, node := range []string{"far", "ace"} {
|
||||
r := Resolution{Node: node, Account: "jochen", Modules: modules}
|
||||
before := composeLibraries(t, r, base)
|
||||
after := composeLibraries(t, r, keeping(node))
|
||||
if node == "far" {
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
t.Fatalf("on a machine no library reaches, a module that does not join composed otherwise:\n %+v\n %+v",
|
||||
before, after)
|
||||
}
|
||||
continue
|
||||
}
|
||||
groups := after.Groups
|
||||
after.Groups, after.Libraries = nil, nil
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
t.Fatalf("on %s, a module that does not join composed otherwise:\n %+v\n %+v", node, before, after)
|
||||
}
|
||||
if len(groups) == 0 || len(after.Identities) > 0 {
|
||||
t.Fatalf("on the library's home: groups %v, identities %v", groups, after.Identities)
|
||||
}
|
||||
filter := fmt.Sprint(byID(before.Resources)["nftables."+FilteringID()]["content"])
|
||||
if !strings.Contains(filter, "8080") || received(t, before.Resources)[0].From != "plain" {
|
||||
t.Fatalf("the comparison held no filter or contribution of the module: %s", filter)
|
||||
}
|
||||
}
|
||||
if Joins(aPlain()) || !Joins(aRadarr(t)) {
|
||||
t.Fatal("joining is not read from the libraries and the identity")
|
||||
@@ -338,3 +347,73 @@ func TestTheEngineTakesWhatIsComposedForLibraries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0334 §1, §2 (the review of 7ea6d36e, item 2): a joining module's own directories are owned by its user
|
||||
// and its own group, not the library's; and the tool runner's account joins the own group of every joining module whose
|
||||
// tools run on the machine — sent only to an engine that reads it (link.OwnGroupContract).
|
||||
func TestAJoiningModulesOwnDirectoriesAreItsOwnGroups(t *testing.T) {
|
||||
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aRadarr(t), theRuntime(t)}}
|
||||
got := byID(composeLibraries(t, r, librariesRendering()).Resources)
|
||||
if owner := got["radarr.config"]["owner"]; owner != "50020:50020" {
|
||||
t.Errorf("its directory is owned by %v; want its user and its own group", owner)
|
||||
}
|
||||
if env := got["radarr.app"]["env"].(map[string]any); env["PGID"] != "50010" {
|
||||
t.Errorf("its process's primary group is %v; want the library's, so what it makes there keeps the group", env["PGID"])
|
||||
}
|
||||
older := composeLibraries(t, r, librariesRendering())
|
||||
if older.Identities[0].Runner != "" {
|
||||
t.Errorf("an engine that cannot put the runner in a module's own group was told %q", older.Identities[0].Runner)
|
||||
}
|
||||
with := librariesRendering()
|
||||
with.RunnerJoinsOwnGroups = true
|
||||
if got := composeLibraries(t, r, with).Identities[0].Runner; got != "jochen" {
|
||||
t.Errorf("the runner joins %q's own group; want jochen, whose tools it runs", got)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0334 §3: a joining module whose tools name a directory whose mode closes it to its group is refused at
|
||||
// the module check, naming the directory.
|
||||
func TestAToolsDirectoryIsOpenToItsGroup(t *testing.T) {
|
||||
closed := map[string]any{
|
||||
"libraries": []any{map[string]any{"id": "movies", "library": "media", "path": "movies", "use": "read"}},
|
||||
"resources": []any{
|
||||
map[string]any{"id": "config", "type": "directory", "mode": "0700"},
|
||||
map[string]any{"id": "app", "type": "container", "name": "app", "image": libraryImage,
|
||||
"volumes": []any{"${dir:config}:/config", "${library:movies}:/movies"}}},
|
||||
"build": map[string]any{"artifacts": []any{map[string]any{"name": "tools", "kind": "bundle",
|
||||
"language": "go", "system": "arch", "from": "cmd/app", "binary": "app", "loads": []any{"app"},
|
||||
"env": map[string]any{"APP_CONFIG": "${dir:config}/config.xml"}}}}}
|
||||
_, err := ParseManifest(libraryManifest(t, closed))
|
||||
if err == nil || !strings.Contains(err.Error(), "config") || !strings.Contains(err.Error(), "ADR 0334") {
|
||||
t.Fatalf("a tools' directory closed to its group: %v", err)
|
||||
}
|
||||
open := closed
|
||||
open["resources"] = []any{
|
||||
map[string]any{"id": "config", "type": "directory", "mode": "0750"},
|
||||
map[string]any{"id": "app", "type": "container", "name": "app", "image": libraryImage,
|
||||
"volumes": []any{"${dir:config}:/config", "${library:movies}:/movies"}}}
|
||||
if _, err := ParseManifest(libraryManifest(t, open)); err != nil {
|
||||
t.Fatalf("a tools' directory open to its group was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The review of 7ea6d36e, item 6: a service's unit takes the identity too — `User=${identity:user}` — at the module
|
||||
// check and in what is composed.
|
||||
func TestAServicesUnitTakesTheIdentity(t *testing.T) {
|
||||
unit := "[Service]\nUser=${identity:user}\nGroup=${identity:group}\nUMask=0002\nExecStart=/usr/bin/importer\n"
|
||||
raw := libraryManifest(t, map[string]any{"resources": []any{
|
||||
map[string]any{"id": "app", "type": "container", "name": "app", "image": libraryImage,
|
||||
"volumes": []any{"${library:movies}:/movies"}},
|
||||
map[string]any{"id": "watch", "type": "service", "name": "app-watch", "unit": unit}}})
|
||||
m, err := ParseManifest(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("a unit naming the identity was refused: %v", err)
|
||||
}
|
||||
with := librariesRendering()
|
||||
with.Identities["app"] = ModuleIdentity{Module: "app", UID: 50030}
|
||||
composed := composeLibraries(t, Resolution{Node: "ace", Modules: []Manifest{m}}, with)
|
||||
got := fmt.Sprint(byID(composed.Resources)["app.watch"]["unit"])
|
||||
if !strings.Contains(got, "User=mesh-app\nGroup=mesh-media\n") {
|
||||
t.Fatalf("the unit reads %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ var fillers = []filler{
|
||||
{namespace: "access", patterns: []*regexp.Regexp{accessRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
||||
// A joining module's identity, wherever its resources take values; a library's host path, only in a container's
|
||||
// volumes, whose source libraryProblems holds it to (novox/hq ADR 0328 §5).
|
||||
{namespace: "identity", patterns: []*regexp.Regexp{identityRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file", "run", "command", "user", "name"}}},
|
||||
{namespace: "identity", patterns: []*regexp.Regexp{identityRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file", "run", "command", "user", "name", "unit"}}},
|
||||
{namespace: "library", patterns: []*regexp.Regexp{libraryRef}, fields: map[string][]string{"container": {"volumes"}},
|
||||
why: "a library's path is a host path, filled only in a container's volume source; the process reads the " +
|
||||
"container side its volume gives (novox/hq ADR 0328 §5)"},
|
||||
|
||||
Reference in New Issue
Block a user