H-2: every service and container is started with MESH_FINGERPRINT, the fingerprint of the want it applies

Builds: design 47 G §201, §205, §239

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 12:03:47 +01:00
parent decbce4c54
commit 730860b274
3 changed files with 86 additions and 1 deletions
+4 -1
View File
@@ -746,7 +746,10 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
}
folded, err := declaration.Parse([]byte(`{"declaration":1,"resources":[` +
`{"id":"nats.server-conf","type":"file","path":"` + conf + `","mode":"0644","content":"listen: old\n"},` +
`{"id":"nats.server","type":"container","name":"` + server + `","image":"` + image + `"}]}`))
`{"id":"nats.server","type":"container","name":"` + server + `","image":"` + image + `",` +
// As the node-engine starts every container since the fingerprint handover (47/G W13.1): the fingerprint of the
// want it applies, here the first want's, so that want changes the server's spec in nothing.
`"env":{"MESH_FINGERPRINT":"` + fpIt("e") + `"}}]}`))
if err != nil {
t.Fatal(err)
}
+46
View File
@@ -165,4 +165,50 @@ func TestComparingCompositions(t *testing.T) {
if c := CompareComposed(map[string]Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" {
t.Errorf("a composition not run is added: %+v", c)
}
// Design 47, *The fingerprint handover* (the composed resources are split here): a daemon or a container of a module
// is started with MESH_FINGERPRINT, the fingerprint of the want it applies, over any value its module declared of that
// name; a file, or a resource of no module, carries none.
split, err := Split("novox", []json.RawMessage{
json.RawMessage(`{"id":"d","type":"process","module":"nats","run":["x"],"env":{"A":"1","MESH_FINGERPRINT":"stale"}}`),
json.RawMessage(`{"id":"c","type":"container","module":"nats","image":"i"}`),
file("conf", "a"),
json.RawMessage(`{"id":"own","type":"process","run":["x"]}`),
}, map[string]string{"nats": fp}, nil, NodeOwn{})
if err != nil {
t.Fatal(err)
}
if len(split) != 2 {
t.Fatalf("split into %d compositions, want the module's and the node's own", len(split))
}
if n := len(split[mustKey(t, "novox", "nats", fp)].Resources); n != 3 {
t.Fatalf("the module's composition holds %d resources, want 3", n)
}
for _, raw := range split[mustKey(t, "novox", "nats", fp)].Resources {
var r struct {
ID string
Env map[string]string
}
_ = json.Unmarshal(raw, &r)
want := fp
if r.ID == "conf" {
want = ""
}
if r.Env[FingerprintVar] != want || (r.ID == "d" && r.Env["A"] != "1") {
t.Errorf("%s starts with %v, want %s=%q and its own words kept", r.ID, r.Env, FingerprintVar, want)
}
}
for _, raw := range split["novox"].Resources {
if strings.Contains(string(raw), FingerprintVar) {
t.Errorf("the node's own carries %s: %s", FingerprintVar, raw)
}
}
}
func mustKey(t *testing.T, node, module, fp string) string {
k, err := Key(node, module, fp)
if err != nil {
t.Fatal(err)
}
return k
}
+36
View File
@@ -234,6 +234,11 @@ func Split(node string, resources []json.RawMessage, fps map[string]string, fill
if head.Type == string(declaration.TypeFile) && head.Module != "" {
pathOf[head.Path] = key
}
if head.Module != "" {
if raw, err = withFingerprint(raw, head.Type, fps[head.Module]); err != nil {
return nil, err
}
}
c := out[key]
c.Resources = append(c.Resources, raw)
out[key] = c
@@ -249,3 +254,34 @@ func Split(node string, resources []json.RawMessage, fps map[string]string, fill
}
return out, nil
}
// FingerprintVar is the environment value every process the node-engine starts, a daemon or a container, holds: the
// fingerprint of the want it applies. mesh-sdk's runtime reads it once at the process's start as the process's own build
// (novox/hq ADR 0656 §1, design 47 *The fingerprint handover*).
const FingerprintVar = "MESH_FINGERPRINT"
// withFingerprint is raw, when it is a daemon or a container, with FingerprintVar in its env set to fp. The value is
// the node-engine's to say, so one a module declared of that name is replaced. Another resource, or no fingerprint, is
// returned as it is.
func withFingerprint(raw json.RawMessage, kind, fp string) (json.RawMessage, error) {
if fp == "" || (kind != string(declaration.TypeProcess) && kind != string(declaration.TypeContainer)) {
return raw, nil
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return nil, fmt.Errorf("compositions: a %s resource is not JSON: %w", kind, err)
}
env := map[string]string{}
if had, ok := fields["env"]; ok {
if err := json.Unmarshal(had, &env); err != nil {
return nil, fmt.Errorf("compositions: the env of a %s resource is not words by name: %w", kind, err)
}
}
env[FingerprintVar] = fp
body, err := json.Marshal(env)
if err != nil {
return nil, err
}
fields["env"] = body
return json.Marshal(fields)
}