ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
package resolution
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// ofPort is where a module asks which port it was given: ${port:<the port its software uses>}.
|
|
var ofPort = regexp.MustCompile(`\$\{port:([0-9]+)\}`)
|
|
|
|
// portsUsed are the ports a written value asks about, first appearance first.
|
|
func portsUsed(content string) []int {
|
|
var used []int
|
|
seen := map[int]bool{}
|
|
for _, m := range ofPort.FindAllStringSubmatch(content, -1) {
|
|
n, err := strconv.Atoi(m[1])
|
|
if err != nil || seen[n] {
|
|
continue
|
|
}
|
|
seen[n] = true
|
|
used = append(used, n)
|
|
}
|
|
return used
|
|
}
|
|
|
|
// portInto replaces a resource's ${port:…} placeholders with what this machine assigned — in a
|
|
// file's content, and in a value of a container's or a process's environment.
|
|
//
|
|
// **A process's environment is a container's** (novox/hq to-be 38 WP4c). A module's code moving out
|
|
// of its container becomes a process on the machine and still has to be told what the container
|
|
// was told; filled for one kind and not the other, the literal reached the process and was read as
|
|
// a port, and the modules that moved first wrote their run-once steps a 0600 env file instead.
|
|
//
|
|
// A port the module did not say it listens on is refused, for the same reason a binding's unknown
|
|
// key is: the module is asking about something it never declared, and the answer would be a guess.
|
|
// Left alone, the literal would be written into a configuration file, or handed to a process as
|
|
// its environment, and read as a port number.
|
|
func portInto(resource map[string]any, module string, listens []mcat.Listening, with mcat.Rendering) error {
|
|
switch fmt.Sprint(resource["type"]) {
|
|
case "file":
|
|
content, ok := resource["content"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
filled, err := portsFilledInto(content,
|
|
fmt.Sprintf("%s has a file that", module), module, listens, with)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resource["content"] = filled
|
|
|
|
case "container", "process":
|
|
env, ok := resource["env"].(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
// In a stated order, so a container with two bad values always refuses on the same one.
|
|
named := make([]string, 0, len(env))
|
|
for key := range env {
|
|
named = append(named, key)
|
|
}
|
|
sort.Strings(named)
|
|
|
|
// **A fresh map, and only when something changes.** This map came out of the module's
|
|
// manifest and the resource around it is a shallow copy, so filling a value in place would
|
|
// change what the catalogue holds for every other machine running the module.
|
|
var filled map[string]any
|
|
for _, key := range named {
|
|
written, ok := env[key].(string)
|
|
if !ok || len(portsUsed(written)) == 0 {
|
|
continue
|
|
}
|
|
value, err := portsFilledInto(written,
|
|
fmt.Sprintf("%s's %s %s sets %s to something that",
|
|
module, resource["type"], resource["name"], key), module, listens, with)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if filled == nil {
|
|
filled = map[string]any{}
|
|
for k, v := range env {
|
|
filled[k] = v
|
|
}
|
|
}
|
|
filled[key] = value
|
|
}
|
|
if filled != nil {
|
|
resource["env"] = filled
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// portsFilledInto answers every ${port:…} in one written value, or refuses. `where` names the
|
|
// place it was written, so a refusal is one edit from right whichever kind of resource it came
|
|
// out of.
|
|
func portsFilledInto(written, where, module string, listens []mcat.Listening, with mcat.Rendering) (
|
|
string, error) {
|
|
for _, wanted := range portsUsed(written) {
|
|
var declared bool
|
|
for _, l := range listens {
|
|
if l.Port == wanted {
|
|
declared = true
|
|
}
|
|
}
|
|
if !declared {
|
|
return "", fmt.Errorf(
|
|
"%s says ${port:%d}, and %s does not say it listens on %d. A module is told the "+
|
|
"port it was given for something it declared, and %s",
|
|
where, wanted, module, wanted, orNoListens(listens))
|
|
}
|
|
written = strings.ReplaceAll(written, fmt.Sprintf("${port:%d}", wanted),
|
|
strconv.Itoa(renderingMachinePort(with, module, wanted)))
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
// orNoListens says what would have worked, so a refusal is one edit from right.
|
|
func orNoListens(listens []mcat.Listening) string {
|
|
if len(listens) == 0 {
|
|
return "it declares no ports at all"
|
|
}
|
|
said := make([]string, 0, len(listens))
|
|
for _, l := range listens {
|
|
said = append(said, strconv.Itoa(l.Port))
|
|
}
|
|
return "it declares " + strings.Join(said, ", ")
|
|
}
|