Placement carries a settings-form module's config values, which mesh-config does not hold, and the export writes them (issue 1126, ADR 0555 §1): the media modules on one node would compose without them
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: ace: novox/mesh-catalog#505 does not merge into the trunk: files in conflict: .gitignore | 9713c3023a18 on a7a1fbd34e1d | unit 1245…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: ace: novox/mesh-catalog#505 does not merge into the trunk: files in conflict: .gitignore | 9713c3023a18 on a7a1fbd34e1d | unit 1245…
This commit is contained in:
@@ -61,11 +61,20 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
|
||||
}
|
||||
m.Bindings[provision] = delivery.Provider{Node: to.Node, Module: to.Module}
|
||||
}
|
||||
// A module in the settings form keeps its config values in its layers, not in mesh-config: they are exported with
|
||||
// the placement. A module in the config form has mesh-config's values laid into its layer, which are not.
|
||||
settingsForm := !in.Catalogue[module].DeclaresConfig()
|
||||
for _, layer := range n.Settings[module] {
|
||||
if layer.From == catalogue.MeshWideLayer {
|
||||
m.HasMesh, m.MeshWide = true, layer.Placement
|
||||
if settingsForm && len(layer.Values) > 0 {
|
||||
m.MeshValues = layer.Values
|
||||
}
|
||||
} else {
|
||||
m.HasLayer, m.Layer = true, layer.Placement
|
||||
if settingsForm && len(layer.Values) > 0 {
|
||||
m.Values = layer.Values
|
||||
}
|
||||
}
|
||||
}
|
||||
p.Modules = append(p.Modules, m)
|
||||
|
||||
@@ -71,6 +71,12 @@ func TestInput(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := delivery.ManifestDigest(xterm)
|
||||
// A module in the settings form (plex and the media modules on one node, 2026-10-26): its config values are in its
|
||||
// layers, not mesh-config, so the placement carries them (MeshValues, Values) and mesh-config's are not read for it.
|
||||
media := []byte(`{"module":"media","version":"1","settings":{"lines":{"kind":"preference","default":0,"why":"kept"},` +
|
||||
`"name":{"kind":"preference","default":"x","why":"shown"}},"resources":[{"id":"conf","type":"file",` +
|
||||
`"path":"/srv/media/conf","mode":"0644","content":"uid=${setting:lines} name=${setting:name}\n"}]}`)
|
||||
mediaDigest := delivery.ManifestDigest(media)
|
||||
keys := map[string]string{"anchor": publicKey(t), "laptop": publicKey(t)}
|
||||
caps := []string{"container-runtime", "systemd", "wireguard"}
|
||||
tunnel := &delivery.RecordTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
|
||||
@@ -84,16 +90,19 @@ func TestInput(t *testing.T) {
|
||||
Holds: []delivery.Holding{{Claim: "mesh-private-network", Scope: "mesh", Module: overlay.Name}}},
|
||||
"laptop": {Node: "laptop", Decided: delivery.NodeDecided{Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"},
|
||||
Modules: []delivery.PlacedModule{{Module: overlay.Name},
|
||||
{Module: "xterm", HasLayer: true, Ports: map[string]int{"80": 8080}}}},
|
||||
{Module: "xterm", HasLayer: true, Ports: map[string]int{"80": 8080}},
|
||||
{Module: "media", HasMesh: true, MeshValues: map[string]any{"name": "mesh"}, HasLayer: true,
|
||||
Layer: map[string]any{"places": map[string]any{"data": "/srv/data"}}, Values: map[string]any{"lines": float64(1000)}}}},
|
||||
},
|
||||
records: map[string]delivery.NodeRecord{
|
||||
"anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Contract: 2},
|
||||
"laptop": {Node: "laptop", TunnelKey: keys["laptop"], Capabilities: caps, Authority: "the mesh's certificate"},
|
||||
},
|
||||
modules: map[string]delivery.Module{"xterm": {Manifest: digest}, "unread": {}},
|
||||
manifests: map[string][]byte{digest: xterm},
|
||||
values: map[string]map[string]any{"laptop.xterm": {"font": "mono"}, "anchor.xterm": {"font": "serif"}},
|
||||
own: []compose.OwnSecretRow{{Node: "laptop", Module: "xterm", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
||||
modules: map[string]delivery.Module{"xterm": {Manifest: digest}, "media": {Manifest: mediaDigest}, "unread": {}},
|
||||
manifests: map[string][]byte{digest: xterm, mediaDigest: media},
|
||||
values: map[string]map[string]any{"laptop.xterm": {"font": "mono"}, "anchor.xterm": {"font": "serif"},
|
||||
"laptop.media": {"lines": "not mesh-config's"}},
|
||||
own: []compose.OwnSecretRow{{Node: "laptop", Module: "xterm", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,18 +116,24 @@ func TestInput(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
parsedMedia, err := catalogue.ParseManifestForInputs(media)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
capsSet := map[string]bool{"container-runtime": true, "systemd": true, "wireguard": true}
|
||||
tun := compose.Tunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
|
||||
Peers: []compose.TunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}}
|
||||
want := compose.Input{
|
||||
Catalogue: map[string]catalogue.Manifest{overlay.Name: private, "xterm": parsed},
|
||||
Catalogue: map[string]catalogue.Manifest{overlay.Name: private, "xterm": parsed, "media": parsedMedia},
|
||||
Nodes: []compose.Node{
|
||||
{Name: "anchor", Place: compose.Place{Key: keys["anchor"], Endpoint: "anchor.example:51820", Site: "here", Hub: true,
|
||||
Address: "10.77.0.1"}, Assigned: []string{overlay.Name}, Capabilities: capsSet, Contract: 2, Stated: true},
|
||||
{Name: "laptop", Place: compose.Place{Key: keys["laptop"], Endpoint: "laptop.example:51820", Site: "here",
|
||||
Address: "10.77.0.2"}, Assigned: []string{overlay.Name, "xterm"}, Capabilities: capsSet,
|
||||
Ports: map[string]map[int]int{"xterm": {80: 8080}},
|
||||
Settings: map[string][]catalogue.Layer{"xterm": {{From: "laptop", Values: map[string]any{"font": "mono"}}}}},
|
||||
Address: "10.77.0.2"}, Assigned: []string{overlay.Name, "xterm", "media"}, Capabilities: capsSet,
|
||||
Ports: map[string]map[int]int{"xterm": {80: 8080}},
|
||||
Settings: map[string][]catalogue.Layer{"xterm": {{From: "laptop", Values: map[string]any{"font": "mono"}}},
|
||||
"media": {{From: catalogue.MeshWideLayer, Values: map[string]any{"name": "mesh"}},
|
||||
{From: "laptop", Values: map[string]any{"lines": float64(1000)}, Placement: map[string]any{"places": map[string]any{"data": "/srv/data"}}}}}},
|
||||
},
|
||||
Holdings: []catalogue.Held{{Claim: "mesh-private-network", Scope: "mesh", Node: "anchor", Module: overlay.Name}},
|
||||
OwnSecrets: []compose.OwnSecretRow{{Node: "laptop", Module: "xterm", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
||||
@@ -142,12 +157,13 @@ func TestInput(t *testing.T) {
|
||||
wj, _ := json.MarshalIndent(want, "", " ")
|
||||
t.Fatalf("Input from the bus differs from the store's:\n got %s\nwant %s", gj, wj)
|
||||
}
|
||||
if !reflect.DeepEqual(b.valuesRead, []string{"laptop." + overlay.Name, "laptop.xterm"}) {
|
||||
if !reflect.DeepEqual(b.valuesRead, []string{"laptop." + overlay.Name, "laptop.xterm", "laptop.media"}) {
|
||||
t.Errorf("laptop read config values %v; a node reads its own slice alone", b.valuesRead)
|
||||
}
|
||||
|
||||
// The same composer over both: the compositions from the bus are the store's, byte for byte.
|
||||
fps := map[string]string{overlay.Name: "src2:" + strings.Repeat("a", 64), "xterm": "src2:" + strings.Repeat("b", 64)}
|
||||
fps := map[string]string{overlay.Name: "src2:" + strings.Repeat("a", 64), "xterm": "src2:" + strings.Repeat("b", 64),
|
||||
"media": "src2:" + strings.Repeat("c", 64)}
|
||||
fromBus, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop"}, fps)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -180,7 +196,7 @@ func TestInput(t *testing.T) {
|
||||
if every, err := Input(ctx, b, Mesh{}, Scope{Every: true}); err != nil || every.OwnSecrets != nil {
|
||||
t.Fatalf("composing every node: %v, own secrets %v (the vault answers a node about itself alone)", err, every.OwnSecrets)
|
||||
}
|
||||
if !reflect.DeepEqual(b.valuesRead, []string{"anchor." + overlay.Name, "laptop." + overlay.Name, "laptop.xterm"}) {
|
||||
if !reflect.DeepEqual(b.valuesRead, []string{"anchor." + overlay.Name, "laptop." + overlay.Name, "laptop.xterm", "laptop.media"}) {
|
||||
// Every node's placed modules, and nothing placed nowhere: anchor places no xterm.
|
||||
t.Errorf("the check read %v", b.valuesRead)
|
||||
}
|
||||
|
||||
+28
-10
@@ -83,14 +83,21 @@ func (p Provider) Chosen() catalogue.Chosen { return catalogue.Chosen{Node: p.No
|
||||
// layers, the mesh's first. Each node's file repeats the mesh-wide layer of the modules placed on it, so a node reads
|
||||
// its own key alone. HasLayer says the node has a layer of its own even when it holds no placement, since a layer
|
||||
// that is there and empty still carries the module's config values (Layers).
|
||||
//
|
||||
// MeshValues and Values are the config values of a module in the settings form (its manifest declares no config, so
|
||||
// mesh-config holds none for it): what a person set in the whole mesh's layer and this node's, beside the placement
|
||||
// (issue 1126; on 2026-10-26 the media modules on one node, plex's puid and pgid among them). A module in the config
|
||||
// form takes its values from mesh-config and has none here.
|
||||
type PlacedModule struct {
|
||||
Module string `json:"module"`
|
||||
Ports map[string]int `json:"ports,omitempty"`
|
||||
Bindings map[string]Provider `json:"bindings,omitempty"`
|
||||
MeshWide map[string]any `json:"mesh-wide,omitempty"`
|
||||
HasMesh bool `json:"has-mesh-wide,omitempty"`
|
||||
Layer map[string]any `json:"layer,omitempty"`
|
||||
HasLayer bool `json:"has-layer,omitempty"`
|
||||
Module string `json:"module"`
|
||||
Ports map[string]int `json:"ports,omitempty"`
|
||||
Bindings map[string]Provider `json:"bindings,omitempty"`
|
||||
MeshWide map[string]any `json:"mesh-wide,omitempty"`
|
||||
HasMesh bool `json:"has-mesh-wide,omitempty"`
|
||||
Layer map[string]any `json:"layer,omitempty"`
|
||||
HasLayer bool `json:"has-layer,omitempty"`
|
||||
MeshValues map[string]any `json:"mesh-wide-values,omitempty"`
|
||||
Values map[string]any `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
// Holding is one seat a module placed on the node holds (catalogue.Held without its node, which is the file's).
|
||||
@@ -326,11 +333,22 @@ func (p Placement) HostPorts() map[string]map[int]int {
|
||||
// Layers is one module's settings layers on the node, as the composition reads them (compose.Node's Settings): the
|
||||
// whole mesh's layer when it has one, then the node's, which carries the module's config values as mesh-config resolved
|
||||
// them for this node (values, nil when none) beside its placement; a module with no layer and no value has none. This is
|
||||
// the controller's SettingsFor for a module that takes its config values from mesh-config (issue 664).
|
||||
// the controller's SettingsFor for a module that takes its config values from mesh-config (issue 664). A module in the
|
||||
// settings form carries its values in the file (MeshValues, Values), which then stand for mesh-config's.
|
||||
func (m PlacedModule) Layers(node string, values map[string]any) []catalogue.Layer {
|
||||
var out []catalogue.Layer
|
||||
if m.HasMesh || len(m.MeshWide) > 0 {
|
||||
out = append(out, catalogue.Layer{From: catalogue.MeshWideLayer, Values: map[string]any{}, Placement: m.MeshWide})
|
||||
if m.HasMesh || len(m.MeshWide) > 0 || len(m.MeshValues) > 0 {
|
||||
mesh := map[string]any{}
|
||||
for k, v := range m.MeshValues {
|
||||
mesh[k] = v
|
||||
}
|
||||
out = append(out, catalogue.Layer{From: catalogue.MeshWideLayer, Values: mesh, Placement: m.MeshWide})
|
||||
}
|
||||
if len(m.Values) > 0 {
|
||||
values = make(map[string]any, len(m.Values))
|
||||
for k, v := range m.Values {
|
||||
values[k] = v
|
||||
}
|
||||
}
|
||||
if m.HasLayer || len(m.Layer) > 0 || len(values) > 0 {
|
||||
if values == nil {
|
||||
|
||||
Reference in New Issue
Block a user