diff --git a/internal/compose/composed.go b/internal/compose/composed.go index 5bdc1c0a..7516d9e3 100644 --- a/internal/compose/composed.go +++ b/internal/compose/composed.go @@ -388,8 +388,8 @@ func ToolsRunning(running []engcomp.Composed) map[string][]catalogue.Bundle { } var own map[string]string for w, v := range words[module] { - if w == catalogue.FilesOwnedPaths || w == catalogue.ReadsSecretPlaces { - continue // the mesh's to say, composed again + if w == catalogue.FilesOwnedPaths || w == catalogue.ReadsSecretPlaces || w == engcomp.FingerprintVar { + continue // the mesh's to say, composed again (the fingerprint is the want's, §519) } if own == nil { own = map[string]string{} diff --git a/internal/compose/composed_test.go b/internal/compose/composed_test.go index c33c7490..6abb069e 100644 --- a/internal/compose/composed_test.go +++ b/internal/compose/composed_test.go @@ -180,7 +180,12 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { } } } - if env[catalogue.RuntimeToolModules] != served || env[catalogue.RuntimeToolEnv] != `{"lamp":{"LAMP_CONF":"/var/lib/lamp/conf"}}` { + // §519: the words each bundle's module is given hold the fingerprint of the want it applies, where there is one. + words := `{"lamp":{"LAMP_CONF":"/var/lib/lamp/conf"}}` + if fp := row.fps["lamp"]; fp != "" { + words = `{"lamp":{"LAMP_CONF":"/var/lib/lamp/conf","MESH_FINGERPRINT":"` + fp + `"}}` + } + if env[catalogue.RuntimeToolModules] != served || env[catalogue.RuntimeToolEnv] != words { t.Errorf("%s: the runtime serves %v with %v, want what runs of lamp, %s", row.name, env[catalogue.RuntimeToolModules], env[catalogue.RuntimeToolEnv], served) } diff --git a/internal/compositions/compare_test.go b/internal/compositions/compare_test.go index a4987123..434deff4 100644 --- a/internal/compositions/compare_test.go +++ b/internal/compositions/compare_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/catalogue" "slices" "strings" "testing" @@ -203,6 +204,26 @@ func TestComparingCompositions(t *testing.T) { t.Errorf("the node's own carries %s: %s", FingerprintVar, raw) } } + + // §519, §618: the tool runner's process hands each bundle's module its own want's fingerprint in the words of its + // bundles, a module with bundles and no words included, and the runner's own fingerprint reaches no other module's. + fpLamp, fpBare := "src2:"+strings.Repeat("1", 64), "src2:"+strings.Repeat("2", 64) + runner, err := Split("novox", []json.RawMessage{json.RawMessage(`{"id":"r","type":"process","module":"nats","run":["x"],"env":{` + + `"` + catalogue.RuntimeToolModules + `":"bare=/b/x,lamp=/l/y","` + catalogue.RuntimeToolEnv + `":"{\"lamp\":{\"LAMP_CONF\":\"/c\"}}"}}`)}, + map[string]string{"nats": fp, "lamp": fpLamp, "bare": fpBare}, nil, NodeOwn{}) + if err != nil { + t.Fatal(err) + } + var proc struct{ Env map[string]string } + _ = json.Unmarshal(runner[mustKey(t, "novox", "nats", fp)].Resources[0], &proc) + var given map[string]map[string]string + if err := json.Unmarshal([]byte(proc.Env[catalogue.RuntimeToolEnv]), &given); err != nil { + t.Fatal(err) + } + if given["lamp"][FingerprintVar] != fpLamp || given["lamp"]["LAMP_CONF"] != "/c" || given["bare"][FingerprintVar] != fpBare || + len(given["nats"]) != 0 || proc.Env[FingerprintVar] != fp { + t.Errorf("the bundles' words are %v and the runner's env %v", given, proc.Env) + } } func mustKey(t *testing.T, node, module, fp string) string { diff --git a/internal/compositions/composed.go b/internal/compositions/composed.go index 924a0eeb..a2d5223f 100644 --- a/internal/compositions/composed.go +++ b/internal/compositions/composed.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "regexp" + "strings" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" @@ -239,6 +240,9 @@ func Split(node string, resources []json.RawMessage, fps map[string]string, fill if raw, err = withFingerprint(raw, head.Type, fps[head.Module]); err != nil { return nil, err } + if raw, err = withBundleFingerprints(raw, head.Type, fps); err != nil { + return nil, err + } } c := out[key] c.Resources = append(c.Resources, raw) @@ -286,3 +290,62 @@ func withFingerprint(raw json.RawMessage, kind, fp string) (json.RawMessage, err fields["env"] = body return json.Marshal(fields) } + +// withBundleFingerprints is raw, when it is a process whose env hands tools bundles their words (catalogue.RuntimeToolEnv), +// with each module's words holding FingerprintVar set to that module's fingerprint in fps: a bundle reads the fingerprint of +// its own module's want from the environment the tool runner starts it with (design 47 G §519). A module fps names no +// fingerprint for keeps its words, and the runner's own fingerprint reaches no bundle but its own module's (§618). +func withBundleFingerprints(raw json.RawMessage, kind string, fps map[string]string) (json.RawMessage, error) { + if kind != string(declaration.TypeProcess) { + 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) + } + var env map[string]string + if had, ok := fields["env"]; !ok || json.Unmarshal(had, &env) != nil || env[catalogue.RuntimeToolModules] == "" { + return raw, nil + } + given := map[string]map[string]string{} + if had := env[catalogue.RuntimeToolEnv]; had != "" { + if err := json.Unmarshal([]byte(had), &given); err != nil { + return nil, fmt.Errorf("compositions: the %s of a process is not words by module: %w", catalogue.RuntimeToolEnv, err) + } + } + for module, words := range given { + if fp := fps[module]; fp != "" { + if words == nil { + words = map[string]string{} + given[module] = words + } + words[FingerprintVar] = fp + } + } + for module, fp := range fps { // a module with bundles and no words of its own still reads its fingerprint + if _, has := given[module]; !has && fp != "" && hasLoadedBundle(env, module) { + given[module] = map[string]string{FingerprintVar: fp} + } + } + body, err := json.Marshal(given) + if err != nil { + return nil, err + } + env[catalogue.RuntimeToolEnv] = string(body) + envBody, err := json.Marshal(env) + if err != nil { + return nil, err + } + fields["env"] = envBody + return json.Marshal(fields) +} + +// hasLoadedBundle is whether the runtime's served list (catalogue.RuntimeToolModules, `module=path` entries) names a bundle of module. +func hasLoadedBundle(env map[string]string, module string) bool { + for _, entry := range strings.Split(env[catalogue.RuntimeToolModules], ",") { + if strings.HasPrefix(entry, module+"=") { + return true + } + } + return false +}