diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index f8da072e..44c753ad 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -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) } diff --git a/internal/compositions/compare_test.go b/internal/compositions/compare_test.go index 8a3c1e5d..a4987123 100644 --- a/internal/compositions/compare_test.go +++ b/internal/compositions/compare_test.go @@ -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 } diff --git a/internal/compositions/composed.go b/internal/compositions/composed.go index 8107422f..16296023 100644 --- a/internal/compositions/composed.go +++ b/internal/compositions/composed.go @@ -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) +}