Fall back to a captured copy only for mesh-host, which the checker places beside no mesh-delivery check (issue 1585)
Review of fb96216: a missing mesh-sdk or catalogue in a merge check is the checker's fault and fails, never judged
against a stale copy. The captured placement guard is refreshed to mesh-host's main 233bb23.
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -50,6 +50,11 @@ const Env = "MESH_CHECK_BESIDE"
|
|||||||
// CapturedSuffix is what a captured file's name carries that the repository's own does not.
|
// CapturedSuffix is what a captured file's name carries that the repository's own does not.
|
||||||
const CapturedSuffix = ".captured"
|
const CapturedSuffix = ".captured"
|
||||||
|
|
||||||
|
// notPlaced are the repositories the checker places beside no mesh-delivery check (besideRules in
|
||||||
|
// mesh-delivery-checker/internal/check/beside.go places mesh-sdk and the catalogue): a test reads them from the captured
|
||||||
|
// copy even in a merge check, and says so (issue 1585). Every other sibling missing from a merge check fails the test.
|
||||||
|
var notPlaced = map[string]bool{"mesh-host": true}
|
||||||
|
|
||||||
// Dir is the named repository a test reads — the clone beside a merge check, or the captured copy — and
|
// Dir is the named repository a test reads — the clone beside a merge check, or the captured copy — and
|
||||||
// says which in the test's log.
|
// says which in the test's log.
|
||||||
func Dir(t testing.TB, repository string) string {
|
func Dir(t testing.TB, repository string) string {
|
||||||
@@ -57,17 +62,20 @@ func Dir(t testing.TB, repository string) string {
|
|||||||
root := os.Getenv(Env)
|
root := os.Getenv(Env)
|
||||||
if root != "" {
|
if root != "" {
|
||||||
dir := filepath.Join(root, repository)
|
dir := filepath.Join(root, repository)
|
||||||
if _, err := os.Stat(dir); err == nil {
|
_, err := os.Stat(dir)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
t.Logf("judged against %s as cloned beside this check", dir)
|
t.Logf("judged against %s as cloned beside this check", dir)
|
||||||
return dir
|
return dir
|
||||||
|
case !notPlaced[repository]:
|
||||||
|
// A sibling the checker places that is missing is the checker's fault, never judged against a stale copy.
|
||||||
|
t.Fatalf("%s is not beside this check in %s=%s, so its agreement with this repository cannot be "+
|
||||||
|
"judged here: %v", repository, Env, root, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not beside this run: the checker places beside mesh-delivery only what its besideRules name for it (mesh-sdk and
|
|
||||||
// the catalogue, issue 1585), so a sibling it does not place (mesh-host) is read from the captured copy, said, and a
|
|
||||||
// check is never skipped. Only a sibling with neither fails.
|
|
||||||
from := filepath.Join(Captured(), repository)
|
from := filepath.Join(Captured(), repository)
|
||||||
if _, err := os.Stat(from); err != nil {
|
if _, err := os.Stat(from); err != nil {
|
||||||
t.Fatalf("%s is neither beside this check (%s=%q) nor captured at %s: %v", repository, Env, root, from, err)
|
t.Fatalf("no captured copy of %s at %s: %v", repository, from, err)
|
||||||
}
|
}
|
||||||
if root != "" {
|
if root != "" {
|
||||||
t.Logf("%s is not beside this check in %s=%s (the checker places it beside no mesh-delivery check): read from "+
|
t.Logf("%s is not beside this check in %s=%s (the checker places it beside no mesh-delivery check): read from "+
|
||||||
|
|||||||
+8
-6
@@ -7,20 +7,22 @@ The copy carries the catalogue's private details: domains, first names in module
|
|||||||
private addresses. That is acceptable while mesh-sdk is private; if it ever goes public, this copy is
|
private addresses. That is acceptable while mesh-sdk is private; if it ever goes public, this copy is
|
||||||
a second place to clean besides the catalogue itself.
|
a second place to clean besides the catalogue itself.
|
||||||
|
|
||||||
Only what go/catalogue's tests read is captured (R4b, issue 1379). Refreshed 2026-10-21 from the mains of both
|
Only what go/catalogue's tests read is captured (R4b, issue 1379), copied here from mesh-sdk with the judge's tests
|
||||||
repositories; before that it was mesh-controller's copy, which still held two modules that had left the catalogue.
|
(issue 1585). mesh-host refreshed 2026-11-01 from its main, the catalogue 2026-10-21 from its main; before that it
|
||||||
|
was mesh-controller's copy, which still held two modules that had left the catalogue.
|
||||||
|
|
||||||
mesh-catalog b5dbfbb784507e211372a7c9640d91f7d6dadd84 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json
|
mesh-catalog b5dbfbb784507e211372a7c9640d91f7d6dadd84 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json
|
||||||
mesh-host cdcba9ca73755854d21e4e657d7e46c32b9b5265 internal/apply/placement_guard.go
|
mesh-host 233bb23bbcb1a829ce2a6164a31a5c5cc4f3d950 internal/apply/placement_guard.go
|
||||||
|
|
||||||
To move them, from this repository's go/ directory, with the two repositories checked out beside mesh-sdk:
|
To move them, from mesh-delivery's modules/mesh-delivery-indexer directory, with the two repositories checked out
|
||||||
|
beside mesh-delivery:
|
||||||
|
|
||||||
rm -rf testdata/beside/mesh-catalog testdata/beside/mesh-host
|
rm -rf testdata/beside/mesh-catalog testdata/beside/mesh-host
|
||||||
mkdir -p testdata/beside/mesh-catalog testdata/beside/mesh-host/internal/apply
|
mkdir -p testdata/beside/mesh-catalog testdata/beside/mesh-host/internal/apply
|
||||||
git -C ../../mesh-catalog archive <commit> -- predefined.json ':(glob)modules/*/module.json' \
|
git -C ../../../mesh-catalog archive <commit> -- predefined.json ':(glob)modules/*/module.json' \
|
||||||
':(glob)modules/*/files/**' ':(glob)modules/*/templates/**' | tar -x -C testdata/beside/mesh-catalog
|
':(glob)modules/*/files/**' ':(glob)modules/*/templates/**' | tar -x -C testdata/beside/mesh-catalog
|
||||||
find testdata/beside -name module.json -exec mv {} {}.captured \;
|
find testdata/beside -name module.json -exec mv {} {}.captured \;
|
||||||
git -C ../../mesh-host show <commit>:internal/apply/placement_guard.go \
|
git -C ../../../mesh-host show <commit>:internal/apply/placement_guard.go \
|
||||||
> testdata/beside/mesh-host/internal/apply/placement_guard.go.captured
|
> testdata/beside/mesh-host/internal/apply/placement_guard.go.captured
|
||||||
|
|
||||||
and write the commits here. The placement guard is kept as .captured so no Go tool reads it as this
|
and write the commits here. The placement guard is kept as .captured so no Go tool reads it as this
|
||||||
|
|||||||
Vendored
+99
-4
@@ -56,7 +56,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/novox/mesh-host/internal/declaration"
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||||
)
|
)
|
||||||
|
|
||||||
// protectedRoots are paths no directory, access or mount source may be, nor hold.
|
// protectedRoots are paths no directory, access or mount source may be, nor hold.
|
||||||
@@ -384,6 +384,9 @@ func ownedByAccount(owner string, a homeAccount) bool {
|
|||||||
if owner == a.Name {
|
if owner == a.Name {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if a.UID < 0 {
|
||||||
|
return false // an account the declaration makes has no uid yet: by name alone, never by a number
|
||||||
|
}
|
||||||
user, _, _ := strings.Cut(owner, ":")
|
user, _, _ := strings.Cut(owner, ":")
|
||||||
if uid, err := strconv.Atoi(user); err == nil {
|
if uid, err := strconv.Atoi(user); err == nil {
|
||||||
return uid == a.UID
|
return uid == a.UID
|
||||||
@@ -413,12 +416,91 @@ const (
|
|||||||
// refusePath says why a path is not placed or mounted; nil when it may be. module is the resource's module,
|
// refusePath says why a path is not placed or mounted; nil when it may be. module is the resource's module,
|
||||||
// owner a directory's declared owner.
|
// owner a directory's declared owner.
|
||||||
func refusePath(path string, kind placing, module, owner string) error {
|
func refusePath(path string, kind placing, module, owner string) error {
|
||||||
|
return refusePathWith(path, kind, module, owner, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// declaredHomes is the home of each account the declaration itself makes (a user resource with a home), by the
|
||||||
|
// account's name; the account is not in the user database yet when the declaration is judged, and the engine makes
|
||||||
|
// it where it is declared, before the directory that is owned by it is reached (novox/hq issue 1542). Its uid is
|
||||||
|
// not known, so a directory is owned by it by name alone. Only for the directory placement guard; no other
|
||||||
|
// reader of accountsOfHomes sees these.
|
||||||
|
func declaredHomes(resources []declaration.Resource) map[string]homeAccount {
|
||||||
|
var accounts []passwdEntry
|
||||||
|
var out map[string]homeAccount
|
||||||
|
for _, r := range resources {
|
||||||
|
u, ok := r.(*declaration.User)
|
||||||
|
if !ok || u.Name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
home := filepath.Clean(u.Home)
|
||||||
|
if u.Home == "" || !filepath.IsAbs(home) || home == "/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if accounts == nil {
|
||||||
|
accounts = passwdEntries()
|
||||||
|
}
|
||||||
|
// Only ever adds a home. An account the machine has keeps the home it has (applyUser never moves it), so
|
||||||
|
// a declared home of one is not its home, unless it is the very home it has: the account an earlier apply
|
||||||
|
// made, as declared, which must be judged the same at every later apply. And a home at or below another
|
||||||
|
// account's, or at or above a protected root, is not a new one.
|
||||||
|
taken := false
|
||||||
|
for _, e := range accounts {
|
||||||
|
if e.name == u.Name && e.home != home {
|
||||||
|
taken = true
|
||||||
|
}
|
||||||
|
if e.name != u.Name && e.home != "" && atOrBelow(home, e.home) {
|
||||||
|
taken = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, root := range protectedRoots {
|
||||||
|
taken = taken || home == root || below(root, home)
|
||||||
|
}
|
||||||
|
if taken {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
out = map[string]homeAccount{}
|
||||||
|
}
|
||||||
|
out[home] = homeAccount{Name: u.Name, UID: -1}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwdEntry is an account of the user database and its home, whatever its uid.
|
||||||
|
type passwdEntry struct{ name, home string }
|
||||||
|
|
||||||
|
// passwdEntries is every account of the user database with its home (empty and root homes as "").
|
||||||
|
func passwdEntries() []passwdEntry {
|
||||||
|
out := []passwdEntry{}
|
||||||
|
f, err := os.Open(passwdFile)
|
||||||
|
if err != nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
sc := bufio.NewScanner(f)
|
||||||
|
for sc.Scan() {
|
||||||
|
fields := strings.Split(sc.Text(), ":")
|
||||||
|
if len(fields) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
home := filepath.Clean(fields[5])
|
||||||
|
if home == "." || home == "/" || home == "/nonexistent" {
|
||||||
|
home = ""
|
||||||
|
}
|
||||||
|
out = append(out, passwdEntry{fields[0], home})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// refusePathWith is refusePath with the homes of the accounts the declaration makes (declaredHomes) counted
|
||||||
|
// among the machine's.
|
||||||
|
func refusePathWith(path string, kind placing, module, owner string, declared map[string]homeAccount) error {
|
||||||
clean := filepath.Clean(path)
|
clean := filepath.Clean(path)
|
||||||
if !filepath.IsAbs(clean) {
|
if !filepath.IsAbs(clean) {
|
||||||
return nil // the declaration refuses a relative path already; a named volume is not a path
|
return nil // the declaration refuses a relative path already; a named volume is not a path
|
||||||
}
|
}
|
||||||
for _, p := range []string{clean, resolved(clean)} {
|
for _, p := range []string{clean, resolved(clean)} {
|
||||||
if err := refuseOne(p, kind, module, owner); err != nil {
|
if err := refuseOne(p, kind, module, owner, declared); err != nil {
|
||||||
if p != clean {
|
if p != clean {
|
||||||
err.Why = fmt.Sprintf("through a link, it is %s, and %s", p, err.Why)
|
err.Why = fmt.Sprintf("through a link, it is %s, and %s", p, err.Why)
|
||||||
err.Path = clean
|
err.Path = clean
|
||||||
@@ -429,7 +511,7 @@ func refusePath(path string, kind placing, module, owner string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func refuseOne(path string, kind placing, module, owner string) *PlacementRefusedError {
|
func refuseOne(path string, kind placing, module, owner string, declared map[string]homeAccount) *PlacementRefusedError {
|
||||||
for _, root := range protectedRoots {
|
for _, root := range protectedRoots {
|
||||||
if path == root || below(root, path) {
|
if path == root || below(root, path) {
|
||||||
return &PlacementRefusedError{Path: path, Why: root + " is one of the machine's own directories, " +
|
return &PlacementRefusedError{Path: path, Why: root + " is one of the machine's own directories, " +
|
||||||
@@ -482,6 +564,18 @@ func refuseOne(path string, kind placing, module, owner string) *PlacementRefuse
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
homes := accountsOfHomes()
|
homes := accountsOfHomes()
|
||||||
|
if ssh && len(declared) > 0 {
|
||||||
|
merged := map[string]homeAccount{}
|
||||||
|
for home, a := range homes {
|
||||||
|
merged[home] = a
|
||||||
|
}
|
||||||
|
for home, a := range declared {
|
||||||
|
if _, held := merged[home]; !held {
|
||||||
|
merged[home] = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
homes = merged
|
||||||
|
}
|
||||||
var deepest string
|
var deepest string
|
||||||
for home := range homes {
|
for home := range homes {
|
||||||
if below(path, home) && len(home) > len(deepest) {
|
if below(path, home) && len(home) > len(deepest) {
|
||||||
@@ -518,11 +612,12 @@ func moduleOfID(id string) string {
|
|||||||
// a file into it, because nothing judged the file's path.
|
// a file into it, because nothing judged the file's path.
|
||||||
func refusedPlaces(resources []declaration.Resource) map[string]error {
|
func refusedPlaces(resources []declaration.Resource) map[string]error {
|
||||||
out := map[string]error{}
|
out := map[string]error{}
|
||||||
|
declared := declaredHomes(resources)
|
||||||
for _, r := range resources {
|
for _, r := range resources {
|
||||||
var err error
|
var err error
|
||||||
switch res := r.(type) {
|
switch res := r.(type) {
|
||||||
case *declaration.Directory:
|
case *declaration.Directory:
|
||||||
err = refusePath(res.Path, placingDirectory, moduleOfID(res.ID), res.Owner)
|
err = refusePathWith(res.Path, placingDirectory, moduleOfID(res.ID), res.Owner, declared)
|
||||||
// And outside every home, the walk's judgement before anything is applied (novox/hq issue 358): a
|
// And outside every home, the walk's judgement before anything is applied (novox/hq issue 358): a
|
||||||
// link another account could make in its path refuses it, and with it what is below it, whatever
|
// link another account could make in its path refuses it, and with it what is below it, whatever
|
||||||
// order they are declared in. The apply walks again at the moment of use. A directory handed over
|
// order they are declared in. The apply walks again at the moment of use. A directory handed over
|
||||||
|
|||||||
Reference in New Issue
Block a user