From cdebb7d1a5941464c8c76327fbe019fa76d85adb Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:27:42 +0200 Subject: [PATCH] Compose a process's environment as a container's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A module's own code moving out of its container (novox/hq to-be 38 WP4c) becomes a process on the machine, and still has to be told what its container was: the port this machine gave the module and where the foundation's seats are. ${port:…} and ${seat:…} were filled only in a file's content and a container's env, so in a process's env they reached the machine as literals, and the modules that moved first (mesh-catalog #245) wrote their run-once steps a 0600 env file instead. A process's env now takes the same resolution and the same refusals; ${dir:…} and ${access:…} already did, and a bundle's env (ADR 0192) already resolves ${dir:…} and ${port:…}. --- internal/catalogue/port_into.go | 15 +++-- internal/catalogue/process_env_test.go | 80 ++++++++++++++++++++++++++ internal/catalogue/seat_into.go | 10 ++-- 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 internal/catalogue/process_env_test.go diff --git a/internal/catalogue/port_into.go b/internal/catalogue/port_into.go index f0066d0..071f36e 100644 --- a/internal/catalogue/port_into.go +++ b/internal/catalogue/port_into.go @@ -31,7 +31,7 @@ import ( // // So a module asks. `${port:8080}` is "the machine-side port you gave me for the 8080 I said I // listen on", and the module writes that where it would otherwise have written a literal — in a -// file's content, or in a value of a container's `env`. +// file's content, or in a value of a container's or a process's `env`. // // **The environment is filled by the control plane, exactly as a bound value is.** A port is not // secret — the mesh holds it in the clear — so there is nothing for the host to be the only @@ -64,7 +64,12 @@ func portsUsed(content string) []int { } // portInto replaces a resource's ${port:…} placeholders with what this machine assigned — in a -// file's content, and in a value of a container's environment. +// file's content, and in a value of a container's or a process's environment. +// +// **A process's environment is a container's** (novox/hq to-be 38 WP4c). A module's code moving out +// of its container becomes a process on the machine and still has to be told what the container +// was told; filled for one kind and not the other, the literal reached the process and was read as +// a port, and the modules that moved first wrote their run-once steps a 0600 env file instead. // // A port the module did not say it listens on is refused, for the same reason a binding's unknown // key is: the module is asking about something it never declared, and the answer would be a guess. @@ -84,7 +89,7 @@ func portInto(resource map[string]any, module string, listens []Listening, with } resource["content"] = filled - case "container": + case "container", "process": env, ok := resource["env"].(map[string]any) if !ok { return nil @@ -106,8 +111,8 @@ func portInto(resource map[string]any, module string, listens []Listening, with continue } value, err := portsFilledInto(written, - fmt.Sprintf("%s's container %s sets %s to something that", - module, resource["name"], key), module, listens, with) + fmt.Sprintf("%s's %s %s sets %s to something that", + module, resource["type"], resource["name"], key), module, listens, with) if err != nil { return err } diff --git a/internal/catalogue/process_env_test.go b/internal/catalogue/process_env_test.go new file mode 100644 index 0000000..bbf0347 --- /dev/null +++ b/internal/catalogue/process_env_test.go @@ -0,0 +1,80 @@ +package catalogue + +import ( + "strings" + "testing" +) + +// **A process's environment is composed as a container's is** (novox/hq to-be 38 WP4c). +// +// A module's code moving out of its container becomes a process on the machine, and what its +// container's environment asked for — the port this machine gave the module, the place it put the +// module's directory — it still has to be told. Filled for a container and not for a process, the +// literal `${port:8080}` reached the process as its environment and was read as a port; the modules +// that moved first wrote their run-once steps an env file instead. +func processModule(env map[string]any) Manifest { + return Manifest{ + Module: "showcase", + Listens: []Listening{{Port: 8080, From: FromMesh}}, + Resources: []map[string]any{ + {"id": "data", "type": "directory", "mode": "0700"}, + {"id": "setup", "type": "process", "name": "showcase-setup", "run-once": true, + "run": []any{"/usr/bin/showcase", "setup"}, "env": env}, + }, + } +} + +func TestAProcessIsToldItsPortAndItsPlaceInItsEnvironment(t *testing.T) { + env := map[string]any{ + "SHOWCASE_URL": "http://127.0.0.1:${port:8080}", + "SHOWCASE_DATA": "${dir:data}/objects", + "SHOWCASE_DB": "127.0.0.1:${seat:mesh-store:5432}", + "GREETING": "hello", + } + out, err := Resolution{Node: "anchor", Modules: []Manifest{processModule(env)}}.Declaration(Rendering{ + Ports: map[string]map[int]int{"showcase": {8080: 21000}}, + Seats: map[string]map[int]int{"mesh-store": {5432: 6852}}, + }) + if err != nil { + t.Fatalf("a process asking for its port and its place does not compose: %v", err) + } + setup := fileNamed(out, "showcase.setup") + if setup == nil { + t.Fatalf("the process is not in the declaration: %v", out) + } + got, _ := setup["env"].(map[string]any) + for key, want := range map[string]string{ + "SHOWCASE_URL": "http://127.0.0.1:21000", + "SHOWCASE_DATA": "/var/lib/showcase/data/objects", + "SHOWCASE_DB": "127.0.0.1:6852", + "GREETING": "hello", + } { + if got[key] != want { + t.Errorf("the process is told %s=%v, want %q", key, got[key], want) + } + } + if env["SHOWCASE_URL"] != "http://127.0.0.1:${port:8080}" { + t.Fatalf("composing for one machine edited the module's own manifest: %v", env) + } +} + +// An unknown reference in a process's environment is refused as a container's is, naming the +// process and the variable — left alone, it would reach the machine as a literal. +func TestAProcessAskingAboutAnUndeclaredPortIsRefused(t *testing.T) { + env := map[string]any{"SHOWCASE_URL": "http://127.0.0.1:${port:9999}"} + _, err := Resolution{Node: "anchor", Modules: []Manifest{processModule(env)}}.Declaration(Rendering{}) + if err == nil { + t.Fatal("a process was told a port its module never said it listens on") + } + for _, said := range []string{"showcase-setup", "SHOWCASE_URL", "${port:9999}", "8080"} { + if !strings.Contains(err.Error(), said) { + t.Errorf("the refusal does not say %q: %v", said, err) + } + } + + env = map[string]any{"SHOWCASE_DATA": "${dir:date}/objects"} + if _, err := (Resolution{Node: "anchor", Modules: []Manifest{processModule(env)}}).Declaration(Rendering{}); err == nil || + !strings.Contains(err.Error(), "${dir:date}") { + t.Fatalf("a process naming no directory of its module was not refused: %v", err) + } +} diff --git a/internal/catalogue/seat_into.go b/internal/catalogue/seat_into.go index c127c9d..a38909a 100644 --- a/internal/catalogue/seat_into.go +++ b/internal/catalogue/seat_into.go @@ -43,8 +43,8 @@ import ( var ofSeat = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):([0-9]+)\}`) // seatInto replaces a resource's ${seat:…} placeholders with where this machine put each seat's -// holder — in a file's content, and in a value of a container's environment. The same two places -// portInto fills, for the same reason: they are where a process reads a number from. +// holder — in a file's content, and in a value of a container's or a process's environment. The +// same places portInto fills, for the same reason: they are where a program reads a number from. func seatInto(resource map[string]any, module string, with Rendering) error { switch fmt.Sprint(resource["type"]) { case "file": @@ -58,7 +58,7 @@ func seatInto(resource map[string]any, module string, with Rendering) error { } resource["content"] = filled - case "container": + case "container", "process": env, ok := resource["env"].(map[string]any) if !ok { return nil @@ -78,8 +78,8 @@ func seatInto(resource map[string]any, module string, with Rendering) error { continue } value, err := seatsFilledInto(written, - fmt.Sprintf("%s's container %s sets %s to something that", - module, resource["name"], key), with) + fmt.Sprintf("%s's %s %s sets %s to something that", + module, resource["type"], resource["name"], key), with) if err != nil { return err } -- 2.54.0