Compose as the controller does, so a node's shadow can answer same (issue 1126, C1 C2 C3 C5 C6, T1)
Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
This commit is contained in:
+60
-4
@@ -77,8 +77,9 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
||||
if !found {
|
||||
return nil, nil, nil, fmt.Errorf("compose: the mesh has no node %q", node)
|
||||
}
|
||||
// A folded module built by a build of the old form (Node.RunsOwn) is left to the node: composed as a stub, so what
|
||||
// it provides and requires still shapes the rest, and its resources are then the ones the node runs.
|
||||
// A folded module built by a build of the old form (Node.RunsOwn) is left to the node: composed from what runs of it
|
||||
// (Carried), so what it provides and requires still shapes the rest and what the others read of it (its published
|
||||
// ports, the paths it owns) is what runs, and its resources are then the ones the node runs.
|
||||
carried := map[string]bool{}
|
||||
var runsOwn *delivery.Composed
|
||||
for i := range reading.Nodes {
|
||||
@@ -103,7 +104,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
||||
for k, v := range n.Kept {
|
||||
kept[k] = v
|
||||
}
|
||||
kept[m] = Stub(man)
|
||||
kept[m] = Carried(man, m, runsOwn)
|
||||
n.Kept = kept
|
||||
}
|
||||
}
|
||||
@@ -259,7 +260,8 @@ func NamesBuild(m catalogue.Manifest) bool {
|
||||
|
||||
// Stub is m with nothing to build and no resource but its directories: what it provides, requires, serves and grants
|
||||
// still shape the rest of the composition, the places its own secrets, data and grants name (${dir:…}) still resolve,
|
||||
// and nothing else of its own is composed (issue 1126: nats's ${dir:mesh-state} on novox).
|
||||
// and nothing else of its own is composed (issue 1126: nats's ${dir:mesh-state} on novox). A module refused in a
|
||||
// node's answer is composed so; a folded one the node runs is Carried.
|
||||
func Stub(m catalogue.Manifest) catalogue.Manifest {
|
||||
m.Build = nil
|
||||
var dirs []map[string]any
|
||||
@@ -272,6 +274,60 @@ func Stub(m catalogue.Manifest) catalogue.Manifest {
|
||||
return m
|
||||
}
|
||||
|
||||
// Carried is m, folded into its node's own composition at a build of the old form, as the node runs it (issue 1126,
|
||||
// C5): nothing to build; its containers and directories those the node's own composition (runs) carries of it, their
|
||||
// module's word taken off their ids, with the manifest's directories it does not run; and the path of every resource it
|
||||
// runs (catalogue.Manifest.RunsAt). So what other modules read of it is what runs: the machine ports its containers
|
||||
// publish (the filter's rules, the routes reaching it: MachineSide) and the paths it owns (the holder of node-files is
|
||||
// told them). Its other resources are not taken, since the composition makes some of them again from its manifest (its
|
||||
// secrets' files) and one id twice is refused; none of them is composed into what is applied either way (compositions
|
||||
// leaves a carried module's out, and carries the ones that run). What it provides, requires, listens on and grants is
|
||||
// its manifest's. A module whose resources runs holds none of is its Stub, as before.
|
||||
func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catalogue.Manifest {
|
||||
if runs == nil {
|
||||
return Stub(m)
|
||||
}
|
||||
var taken []map[string]any
|
||||
var at []string
|
||||
ids := map[string]bool{}
|
||||
for _, raw := range runs.Resources {
|
||||
var r map[string]any
|
||||
if json.Unmarshal(raw, &r) != nil {
|
||||
continue
|
||||
}
|
||||
id, _ := r["id"].(string)
|
||||
owner, _ := r["module"].(string)
|
||||
if owner == "" {
|
||||
owner, _ = declaration.ModuleGuess(id)
|
||||
}
|
||||
if owner != module {
|
||||
continue
|
||||
}
|
||||
if p, ok := r["path"].(string); ok && p != "" {
|
||||
at = append(at, p)
|
||||
}
|
||||
if typ := r["type"]; typ != "container" && typ != "directory" {
|
||||
continue
|
||||
}
|
||||
r["id"] = strings.TrimPrefix(id, module+".")
|
||||
delete(r, "module")
|
||||
ids[r["id"].(string)] = true
|
||||
taken = append(taken, r)
|
||||
}
|
||||
if len(at) == 0 && len(taken) == 0 {
|
||||
return Stub(m)
|
||||
}
|
||||
stub := Stub(m)
|
||||
for _, r := range stub.Resources {
|
||||
if id, _ := r["id"].(string); !ids[id] {
|
||||
taken = append(taken, r)
|
||||
}
|
||||
}
|
||||
stub.Resources = taken
|
||||
stub.RunsAt = at
|
||||
return stub
|
||||
}
|
||||
|
||||
// carry puts into the node's resources (kept, in the declaration's order) the resources of the node's own composition as
|
||||
// it runs (runs) that belong to a carried module, each after the resource it runs after, or first when that one is
|
||||
// not among them; a file among them is a folded path.
|
||||
|
||||
@@ -165,6 +165,92 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
!strings.Contains(err.Error(), "spiegel has not been built for this mesh") {
|
||||
t.Errorf("composed spiegel without what runs: %v", err)
|
||||
}
|
||||
|
||||
// (C) Issue 1126, C5: what the others read of a module carried as it runs is what runs, never its stub's: the filter
|
||||
// opens the machine port its running container publishes (de-spiegel's 20005 on novox, not its software's 8080; a
|
||||
// stub had no container, so snake's 80 on ace was opened beside the route proxy's). And C6: the operator export's file
|
||||
// a module that keeps it runs is carried as it runs (mesh-vault.kept on novox), since the node holds no export.
|
||||
listening := []byte(`{"module":"spiegel","version":"1","capabilities":["container-runtime"],` +
|
||||
`"listens":[{"port":8080,"from":"mesh","why":"the site"}],"own-secrets":{"token":"${dir:state}/token"},` +
|
||||
`"resources":[{"id":"state","type":"directory","path":"/var/lib/spiegel","mode":"0700"},` +
|
||||
`{"id":"server","type":"container","name":"spiegel","artifact":"site","ports":["8080"]}],` +
|
||||
`"build":{"artifacts":[{"name":"site","kind":"upstream","from":"` + image + `"}]}}`)
|
||||
nft := []byte(`{"module":"nft","version":"1","capabilities":["systemd"],"filtering":{"into":"/etc/nftables.conf"}}`)
|
||||
vault := []byte(`{"module":"vault","version":"1","capabilities":["systemd"],"keeps":"/var/lib/vault/root",` +
|
||||
`"resources":[{"id":"service","type":"service","unit":"vault.service","state":"running","boot":"enabled"}]}`)
|
||||
b = mk("lemurs", overlay.Name, "spiegel", "nft", "vault")
|
||||
for m, raw := range map[string][]byte{"spiegel": listening, "nft": nft, "vault": vault} {
|
||||
b.manifests[delivery.ManifestDigest(raw)] = raw
|
||||
b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
|
||||
}
|
||||
p = b.placements["n1"]
|
||||
p.Decided.Endpoint = "n1.example:51820"
|
||||
for i := range p.Modules {
|
||||
if p.Modules[i].Module == "spiegel" {
|
||||
p.Modules[i].Ports = map[string]int{"8080": 20005}
|
||||
}
|
||||
}
|
||||
b.placements["n1"] = p
|
||||
rec := b.records["n1"]
|
||||
rec.OutwardLinks = []string{"eth0"}
|
||||
b.records["n1"] = rec
|
||||
fps = map[string]string{"lemurs": fp("e"), "nft": fp("f"), "vault": fp("9")}
|
||||
if in, err = Input(ctx, b, Mesh{}, stub); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in.Nodes[0].Kept = map[string]catalogue.Manifest{"spiegel": compose.Stub(in.Catalogue["spiegel"])}
|
||||
if base, _, err = compose.Compositions(in, "n1", fps, compose.Reading); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keptFile := json.RawMessage(`{"digest":"sha256:` + strings.Repeat("d", 64) + `","id":"vault.kept","mode":"0600","module":"vault","path":"/var/lib/vault/root/export.json","size":143604,"type":"file"}`)
|
||||
running = nil
|
||||
for _, c := range base {
|
||||
switch c.Module {
|
||||
case "":
|
||||
c.Resources = append(append([]json.RawMessage{}, c.Resources...),
|
||||
json.RawMessage(`{"id":"spiegel.state","type":"directory","path":"/var/lib/spiegel","mode":"0700"}`),
|
||||
json.RawMessage(`{"id":"spiegel.server","type":"container","name":"spiegel","image":"registry.example/spiegel@sha256:`+strings.Repeat("c", 64)+`","ports":["10.77.0.1:20005:8080"]}`),
|
||||
json.RawMessage(`{"id":"spiegel.server-env","type":"file","path":"/var/lib/spiegel/server.env","mode":"0600","content":"A=1\n"}`))
|
||||
case "vault":
|
||||
c.Resources = append(append([]json.RawMessage{}, c.Resources...), keptFile)
|
||||
}
|
||||
running = append(running, c)
|
||||
}
|
||||
got, _, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}, Running: running}, fps)
|
||||
if err != nil {
|
||||
t.Fatalf("the node does not compose itself with spiegel carried: %v", err)
|
||||
}
|
||||
var rules string
|
||||
var keptComposed json.RawMessage
|
||||
for _, c := range got {
|
||||
for _, raw := range c.Resources {
|
||||
var r struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &r)
|
||||
switch r.ID {
|
||||
case "nft." + catalogue.FilteringID():
|
||||
rules = r.Content
|
||||
case "vault." + catalogue.KeptID():
|
||||
keptComposed = raw
|
||||
}
|
||||
}
|
||||
}
|
||||
if !strings.Contains(rules, "# spiegel — the site") || !strings.Contains(rules, "dport 20005 accept") || strings.Contains(rules, "dport 8080") {
|
||||
t.Errorf("the filter does not open the machine port spiegel runs at:\n%s", rules)
|
||||
}
|
||||
for _, c := range running {
|
||||
if c.Module == "" && !slices.Contains(compose.Carried(in.Catalogue["spiegel"], "spiegel", &c).RunsAt, "/var/lib/spiegel/server.env") {
|
||||
t.Errorf("spiegel carried does not own the path its running file is at")
|
||||
}
|
||||
}
|
||||
var k1, k2 any
|
||||
_ = json.Unmarshal(keptComposed, &k1)
|
||||
_ = json.Unmarshal(keptFile, &k2)
|
||||
if !reflect.DeepEqual(k1, k2) {
|
||||
t.Errorf("the operator export is composed as %s, want the file that runs %s", keptComposed, keptFile)
|
||||
}
|
||||
}
|
||||
|
||||
// A module another node cannot run in this composition offers nothing in it: the refusal names it and why
|
||||
|
||||
@@ -297,6 +297,21 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
|
||||
n.InOwn[module] = true
|
||||
}
|
||||
for i := range s.Running {
|
||||
// The operator export as the vault runs it: the controller composes it from its store, which the node
|
||||
// does not read; until the export is on the bus, the node carries the file it runs (issue 1126, C6:
|
||||
// mesh-vault.kept on novox, "in what runs, and not composed").
|
||||
if m := s.Running[i].Module; m != "" {
|
||||
if kept := keptOf(s.Running[i]); kept != nil {
|
||||
if in.KeptRuns == nil {
|
||||
in.KeptRuns = map[string]map[string]map[string]any{}
|
||||
}
|
||||
if in.KeptRuns[s.Node] == nil {
|
||||
in.KeptRuns[s.Node] = map[string]map[string]any{}
|
||||
}
|
||||
in.KeptRuns[s.Node][m] = kept
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Running[i].Module == "" {
|
||||
own := s.Running[i]
|
||||
n.RunsOwn = &own
|
||||
@@ -708,6 +723,17 @@ func storeSought(in compose.Input) string {
|
||||
", none of it on the private network with a port"
|
||||
}
|
||||
|
||||
// keptOf is the operator export's file (catalogue.KeptID) of a module's composition as it runs, or nil.
|
||||
func keptOf(c delivery.Composed) map[string]any {
|
||||
for _, raw := range c.Resources {
|
||||
var r map[string]any
|
||||
if json.Unmarshal(raw, &r) == nil && r["id"] == c.Module+"."+catalogue.KeptID() {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// membershipOf is the sealed value of the bus membership resource (catalogue.BusMembershipID) of a node's own
|
||||
// composition, or "".
|
||||
func membershipOf(own delivery.Composed) string {
|
||||
|
||||
@@ -68,6 +68,10 @@ type Input struct {
|
||||
// OperatorExport is every operator-sealed secret in the mesh, for a module that keeps them (inv.OperatorExport): sealed
|
||||
// values the composition carries without opening. Nil when the mesh has no operator key.
|
||||
OperatorExport *catalogue.KeptExport
|
||||
// KeptRuns is, by node and module, the file of the export as that node runs it (its resource of a module's running
|
||||
// composition): what a node composing its own carries where it holds no OperatorExport (catalogue.Rendering.KeptRuns,
|
||||
// issue 1126, C6). Used only while OperatorExport is nil.
|
||||
KeptRuns map[string]map[string]map[string]any
|
||||
|
||||
// Issued is the credentials made for the consumers of each provider (inv.SecretsFrom): one row for each pair credential
|
||||
// that exists, by the provider node it is for. The grants a provider must create are read from these, so a credential not
|
||||
|
||||
@@ -128,6 +128,9 @@ func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) {
|
||||
continue
|
||||
}
|
||||
r.Kept = c.in.OperatorExport
|
||||
if r.Kept == nil {
|
||||
r.KeptRuns = c.in.KeptRuns[node.Name]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user