Self-upgrade installer: SDK-by-version, mesh-controller rename, foundation adopted #26
@@ -456,6 +456,12 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
||||
// assigned to. Beside the bound values because it is the same kind of fact — the
|
||||
// mesh's own, held in the clear — and because a module that must name itself to
|
||||
// something else has no binding to learn it from (novox/hq ADR 0066).
|
||||
// Which port this machine gave it, for a module that binds directly rather than
|
||||
// through a runtime that can remap (ADR 0038). Applied before the machine's facts so
|
||||
// a refusal names the port rather than whatever came after it.
|
||||
if err := portInto(copied, m.Module, m.Listens, with); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := machineInto(copied, thisMachine, m.Module); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Telling a module which port it was given.
|
||||
//
|
||||
// **The mesh assigns the machine-side port and a module does not choose one**
|
||||
// ([ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md)). For a container that is
|
||||
// invisible: the mesh rewrites `ports` into `assigned:wanted`, the software inside binds the number
|
||||
// it has always bound, and the machine publishes a different one.
|
||||
//
|
||||
// **A process has no such layer.** It runs on the machine, there is nothing to rewrite, and it
|
||||
// binds whatever its configuration says — so without this, every process binds the number written
|
||||
// in its own config, two modules declaring the same one collide, and the mesh's whole reason for
|
||||
// assigning ports is defeated by the resource kind that most needs it.
|
||||
//
|
||||
// So a module asks. `${port:8080}` is "the machine-side port you gave me for the 8080 I said I
|
||||
// listen on", and the module writes that into its own configuration exactly as it writes an
|
||||
// address it was bound to.
|
||||
|
||||
// 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 file's content 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 file's ${port:…} placeholders with what this machine assigned.
|
||||
//
|
||||
// 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 and read as a port number.
|
||||
func portInto(resource map[string]any, module string, listens []Listening, with Rendering) error {
|
||||
if fmt.Sprint(resource["type"]) != "file" {
|
||||
return nil
|
||||
}
|
||||
content, ok := resource["content"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, wanted := range portsUsed(content) {
|
||||
var declared bool
|
||||
for _, l := range listens {
|
||||
if l.Port == wanted {
|
||||
declared = true
|
||||
}
|
||||
}
|
||||
if !declared {
|
||||
return fmt.Errorf(
|
||||
"%s has a file that 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",
|
||||
module, wanted, module, wanted, orNoListens(listens))
|
||||
}
|
||||
content = strings.ReplaceAll(content, fmt.Sprintf("${port:%d}", wanted),
|
||||
strconv.Itoa(with.machinePort(module, wanted)))
|
||||
resource["content"] = content
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// orNoListens says what would have worked, so a refusal is one edit from right.
|
||||
func orNoListens(listens []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, ", ")
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **A process binds the port the mesh gave it, not the one it wrote down.**
|
||||
//
|
||||
// A container never needed this: the mesh rewrites its `ports` into assigned:wanted, so the
|
||||
// software binds the number it always bound and the machine publishes another. A process runs on
|
||||
// the machine with nothing to rewrite, so without a way to ask, every process binds the number in
|
||||
// its own configuration and two modules declaring the same one collide — which is the whole
|
||||
// problem ADR 0038 exists to prevent, reintroduced by the resource kind that most needs it.
|
||||
func TestAModuleIsToldWhichPortItWasGiven(t *testing.T) {
|
||||
file := map[string]any{
|
||||
"type": "file", "id": "settings",
|
||||
"content": "LISTEN=${port:8080}\n",
|
||||
}
|
||||
listens := []Listening{{Port: 8080, From: FromMesh}}
|
||||
with := Rendering{Ports: map[string]map[int]int{"showcase": {8080: 21000}}}
|
||||
|
||||
if err := portInto(file, "showcase", listens, with); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := file["content"].(string); got != "LISTEN=21000\n" {
|
||||
t.Fatalf("the module was not told its assigned port: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// With nothing assigned yet, it is told the port it asked about — so a mesh that has not made an
|
||||
// assignment still composes something coherent rather than writing a zero.
|
||||
func TestWithNoAssignmentAModuleIsToldWhatItAskedFor(t *testing.T) {
|
||||
file := map[string]any{"type": "file", "content": "LISTEN=${port:8080}\n"}
|
||||
if err := portInto(file, "showcase", []Listening{{Port: 8080}}, Rendering{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := file["content"].(string); got != "LISTEN=8080\n" {
|
||||
t.Fatalf("an unassigned port did not fall back to what was declared: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// **Asking about a port it never declared is refused**, and the refusal says what it did declare.
|
||||
// The module is asking about something the mesh has no opinion on, and answering would be a guess
|
||||
// written into a configuration file as a port number.
|
||||
func TestAskingAboutAnUndeclaredPortIsRefused(t *testing.T) {
|
||||
file := map[string]any{"type": "file", "content": "LISTEN=${port:9999}\n"}
|
||||
err := portInto(file, "showcase", []Listening{{Port: 8080}}, Rendering{})
|
||||
if err == nil {
|
||||
t.Fatal("a module was told a port it never said it listens on")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "8080") {
|
||||
t.Fatalf("the refusal does not say what would have worked: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// And a file mentioning no port is left exactly as it was.
|
||||
func TestAFileWithNoPortIsUntouched(t *testing.T) {
|
||||
file := map[string]any{"type": "file", "content": "GREETING=hello\n"}
|
||||
if err := portInto(file, "showcase", nil, Rendering{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := file["content"].(string); got != "GREETING=hello\n" {
|
||||
t.Fatalf("a file with no port was changed: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **The showcase module is parsed by the real parser, in the real test suite.**
|
||||
//
|
||||
// A module that exercises every capability is only worth having if something checks it still does.
|
||||
// Written as a test rather than a script so it runs whenever anything about manifests changes —
|
||||
// which is exactly when a module using all of it would quietly stop being valid.
|
||||
func TestTheShowcaseModuleIsAValidManifest(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../../mesh-catalog/modules/showcase/module.json")
|
||||
if err != nil {
|
||||
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
||||
}
|
||||
m, err := ParseManifest(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("the module that exercises everything does not parse:\n%v", err)
|
||||
}
|
||||
|
||||
// Every resource kind a MODULE may use, actually in it.
|
||||
//
|
||||
// Two of the host's eleven are deliberately absent, and the reasons are worth keeping:
|
||||
//
|
||||
// - `action` is refused to modules outright. The link may not carry a command to run (ADR
|
||||
// 0005), so a module that needs something done ships a program that reads what the mesh
|
||||
// delivered and reconciles — which is what a run-once `process` is.
|
||||
// - `service` puts an EXISTING unit into a state and deliberately installs none, which is
|
||||
// right for software that ships its own unit. A module whose code the mesh built has no
|
||||
// such unit until the mesh writes one, and that is a `process`.
|
||||
kinds := map[string]bool{}
|
||||
for _, r := range m.Resources {
|
||||
kind, _ := r["type"].(string)
|
||||
kinds[kind] = true
|
||||
}
|
||||
for _, want := range []string{
|
||||
"access", "archive", "container", "directory", "file", "network", "package",
|
||||
"process", "user",
|
||||
} {
|
||||
if !kinds[want] {
|
||||
t.Errorf("showcase no longer exercises %q", want)
|
||||
}
|
||||
}
|
||||
|
||||
// And all three ways a module's own code can run, which is the thing most easily lost.
|
||||
var stays, once, scheduled bool
|
||||
for _, r := range m.Resources {
|
||||
if kind, _ := r["type"].(string); kind != "process" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case r["run-once"] == true:
|
||||
once = true
|
||||
case r["schedule"] != nil:
|
||||
scheduled = true
|
||||
default:
|
||||
stays = true
|
||||
}
|
||||
}
|
||||
if !stays || !once || !scheduled {
|
||||
t.Errorf("showcase does not exercise all three process modes: stays=%v once=%v scheduled=%v",
|
||||
stays, once, scheduled)
|
||||
}
|
||||
|
||||
// And the artifact kinds, including the one that compiles.
|
||||
var bundle, archive, upstream bool
|
||||
for _, a := range m.Build.Artifacts {
|
||||
switch a.Kind {
|
||||
case ArtifactBundle:
|
||||
bundle = true
|
||||
case ArtifactArchive:
|
||||
archive = true
|
||||
case ArtifactUpstream:
|
||||
upstream = true
|
||||
}
|
||||
}
|
||||
if !bundle || !archive || !upstream {
|
||||
t.Errorf("showcase does not exercise every artifact kind: bundle=%v archive=%v upstream=%v",
|
||||
bundle, archive, upstream)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user