Hold the proofs that a manifest composes, which left the judge's and route-proxy's tests
ADR 0593 §7: the composition is the node-engine's, so its tests hold what the indexer's judge tests and route-proxy's tests proved by composing: a bundle's words per machine, a Go bundle's delivery, the credential form in a handed plan, settings refusals, placeholder and vault refusals, the session's files, a field set aside composed whole, route-proxy's manifest composing for a machine, a route endpoint's machine port in six cases and the plan's route entry end to end. Two table tests, one subtest per item; route-proxy's manifest is captured beside the catalogue. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
|
||||
"github.com/novox/mesh-host/internal/beside"
|
||||
)
|
||||
|
||||
// The proofs that a manifest composes, which the indexer's judge tests and route-proxy's tests held while the
|
||||
// composition was mesh-sdk's (novox/hq ADR 0593 §7): the composition is the node-engine's, so they are held here, each
|
||||
// case as it was (sdk1, .work/sdk1/proofs-for-node-engine.md; mesh-delivery 6fb743f, mesh-networking a74f9db).
|
||||
|
||||
// declared is the declaration of r with, failing the test on a refusal.
|
||||
func declared(t *testing.T, r mcat.Resolution, with mcat.Rendering) []map[string]any {
|
||||
t.Helper()
|
||||
out, err := Declaration(r, with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func twoMergeableFiles(app, sidecar []any) mcat.Manifest {
|
||||
appFile := map[string]any{"id": "app", "type": "file", "path": "/etc/app.json", "merge": mcat.MergeJSON,
|
||||
"content": `{"server":{"port":8080},"search":{"safe":1}}`}
|
||||
sideFile := map[string]any{"id": "sidecar", "type": "file", "path": "/etc/sidecar.json", "merge": mcat.MergeJSON,
|
||||
"content": `{"url":"http://127.0.0.1:8080"}`}
|
||||
if app != nil {
|
||||
appFile[mcat.TakesSettingsField] = app
|
||||
}
|
||||
if sidecar != nil {
|
||||
sideFile[mcat.TakesSettingsField] = sidecar
|
||||
}
|
||||
return mcat.Manifest{Module: "search", Resources: []map[string]any{appFile, sideFile,
|
||||
{"id": "env", "type": "file", "path": "/etc/search.env", "content": "SITE=${setting:site-name}\n"}}}
|
||||
}
|
||||
|
||||
// The indexer's judge's proofs, items 1 to 13 of its list.
|
||||
func TestTheJudgesProofsOfAComposition(t *testing.T) {
|
||||
runtimeWith := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
|
||||
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "dash": {"token": "sealed-token"}}}
|
||||
processOf := func(out []map[string]any) map[string]any {
|
||||
return fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())
|
||||
}
|
||||
|
||||
t.Run("1 a bundle's words resolve per machine into the runtime's tool env", func(t *testing.T) {
|
||||
dash := mcat.Manifest{Module: "dash", Version: "1", Tools: []string{"status"},
|
||||
Listens: []mcat.Listening{{Name: "web", Port: 3000, Protocol: "tcp"}},
|
||||
OwnSecrets: mcat.OwnSecrets{"token": {Path: "${dir:mesh-state}/token"}},
|
||||
Resources: []map[string]any{
|
||||
{"id": "mesh-state", "type": "directory", "mode": "0700", "place": "mesh"},
|
||||
{"id": "config", "type": "file", "path": "${dir:mesh-state}/config.json", "mode": "0600", "content": "{}\n"},
|
||||
{"id": "unrelated", "type": "file", "path": "/etc/dash.conf", "content": "x\n"},
|
||||
},
|
||||
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "tools", Kind: mcat.ArtifactBundle, Language: "typescript",
|
||||
Entrypoints: []string{"tools/index.js"},
|
||||
Env: map[string]string{"DASH_CONFIG_FILE": "${dir:mesh-state}/config.json", "DASH_TOKEN_FILE": "${dir:mesh-state}/token",
|
||||
"DASH_URL": "http://127.0.0.1:${port:3000}", "DASH_ADMIN": "mesh-admin"}}}}}
|
||||
dash, err := dash.Resolve([]mcat.Built{{Name: "tools", Kind: mcat.ArtifactBundle,
|
||||
Reference: mcat.ArtifactStoreScheme + "dash/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other := aToolsModule(t, "nftables", "tools/index.js")
|
||||
out := declared(t, mcat.Resolution{Node: "anchor", Account: "ops", Modules: []mcat.Manifest{dash, other, theRuntime(t)}}, runtimeWith)
|
||||
dir := fileNamed(out, "dash.mesh-state")
|
||||
if dir == nil {
|
||||
t.Fatalf("no directory: %v", ids(out))
|
||||
}
|
||||
at := fmt.Sprint(dir["path"])
|
||||
process := processOf(out)
|
||||
env := process["env"].(map[string]string)
|
||||
var given map[string]map[string]string
|
||||
if err := json.Unmarshal([]byte(env[mcat.RuntimeToolEnv]), &given); err != nil {
|
||||
t.Fatalf("the runtime's %s is not JSON: %q", mcat.RuntimeToolEnv, env[mcat.RuntimeToolEnv])
|
||||
}
|
||||
want := map[string]string{"DASH_CONFIG_FILE": at + "/config.json", "DASH_TOKEN_FILE": at + "/token",
|
||||
"DASH_URL": "http://127.0.0.1:3000", "DASH_ADMIN": "mesh-admin"}
|
||||
if fmt.Sprint(given["dash"]) != fmt.Sprint(want) {
|
||||
t.Errorf("dash is given %v, want %v", given["dash"], want)
|
||||
}
|
||||
if _, has := given["nftables"]; has {
|
||||
t.Errorf("a module that declares no words was given some: %v", given)
|
||||
}
|
||||
for _, id := range []string{"dash.mesh-state", "dash.config", "dash." + mcat.NeedID("token")} {
|
||||
if r := fileNamed(out, id); r == nil || r["owner"] != "ops" {
|
||||
t.Errorf("%s is not the account's to read: %v", id, r)
|
||||
}
|
||||
}
|
||||
if r := fileNamed(out, "dash.unrelated"); r == nil || r["owner"] != nil {
|
||||
t.Errorf("a file no word names was given an owner: %v", r)
|
||||
}
|
||||
restarts := fmt.Sprint(process["restart-on"])
|
||||
for _, want := range []string{"dash.config", "dash." + mcat.NeedID("token")} {
|
||||
if !strings.Contains(restarts, want) {
|
||||
t.Errorf("the runtime is not restarted when %s changes: %s", want, restarts)
|
||||
}
|
||||
}
|
||||
if strings.Contains(restarts, "dash.unrelated") || strings.Contains(restarts, "dash.mesh-state") {
|
||||
t.Errorf("the runtime restarts for something no word names as a file: %s", restarts)
|
||||
}
|
||||
// No account: the runtime is root and nothing is re-owned.
|
||||
if r := fileNamed(declared(t, mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{dash, theRuntime(t)}}, runtimeWith),
|
||||
"dash.config"); r["owner"] != nil {
|
||||
t.Errorf("re-owned with no account: %v", r)
|
||||
}
|
||||
// A change to a module's words changes the runtime's process.
|
||||
changed := dash
|
||||
changed.Bundles = append([]mcat.Bundle(nil), dash.Bundles...)
|
||||
changed.Bundles[0].Env = map[string]string{"DASH_ADMIN": "somebody-else"}
|
||||
out2 := declared(t, mcat.Resolution{Node: "anchor", Account: "ops", Modules: []mcat.Manifest{changed, theRuntime(t)}}, runtimeWith)
|
||||
if fmt.Sprint(processOf(out2)["env"]) == fmt.Sprint(env) {
|
||||
t.Error("the runtime's process is the same after a module's words changed, so it would not restart")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("2 a Go bundle is delivered and named to the runtime by its binary", func(t *testing.T) {
|
||||
lamp := mcat.Manifest{Module: "lamp", Version: "1", Tools: []string{"on"},
|
||||
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "tools", Kind: mcat.ArtifactBundle, Language: "go",
|
||||
System: "arch", From: "cmd/lamp-tools"}}}}
|
||||
lamp, err := lamp.Resolve([]mcat.Built{{Name: "tools", Kind: mcat.ArtifactBundle,
|
||||
Reference: mcat.ArtifactStoreScheme + "lamp/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := declared(t, mcat.Resolution{Node: "anchor", Account: "ops", Modules: []mcat.Manifest{lamp, theRuntime(t)}}, runtimeWith)
|
||||
if fileNamed(out, "lamp."+BundleID("tools")) == nil {
|
||||
t.Errorf("the Go bundle is not delivered: %v", ids(out))
|
||||
}
|
||||
if env := processOf(out)["env"].(map[string]string); env[mcat.RuntimeToolModules] != "lamp="+BundlePath("lamp", "tools")+"/lamp-tools" {
|
||||
t.Errorf("the runtime is told %q, want the binary", env[mcat.RuntimeToolModules])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("3 a handed module's plan names its provider's credential form", func(t *testing.T) {
|
||||
storeOffering := func(credential string) mcat.Manifest {
|
||||
raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` +
|
||||
credential + `}],"serves":{"postgres-database":{"port":5432}}`
|
||||
if strings.Contains(credential, `"own"`) {
|
||||
raw += `,"own-secrets":{"admin":{"path":"/var/lib/store/admin","taken":"at-start"},` +
|
||||
`"token":{"path":"/var/lib/store/token","taken":"at-start","issued-by":"outside"}}`
|
||||
}
|
||||
return mustParse(t, raw+`}`)
|
||||
}
|
||||
for credential, want := range map[string]string{
|
||||
``: "",
|
||||
`,"credential":{"form":"pair"}`: mcat.CredentialFormPair,
|
||||
`,"credential":{"form":"none"}`: mcat.CredentialFormNone,
|
||||
`,"credential":{"own":"admin"}`: mcat.CredentialFormShared,
|
||||
`,"credential":{"own":"token"}`: mcat.CredentialFormGiven,
|
||||
`,"credential":{"issued-by":"outside"}`: mcat.CredentialFormGiven,
|
||||
`,"credential":{"issued-by":"software","tool":"store_issued_token"}`: mcat.CredentialFormIssued,
|
||||
} {
|
||||
node := workstation()
|
||||
node.At = "workstation.internal"
|
||||
consumer := notifierConsumer(t, true, false)
|
||||
r, err := Resolve(shelf(consumer, storeOffering(credential)), []string{consumer.Module}, node, twoStores("anchor"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Account = "op"
|
||||
c, _ := composeWith(t, r, handingTo("store:5000"))
|
||||
if got := c.Plans["notify"].Provisions; len(got) != 1 || got[0].CredentialForm != want {
|
||||
t.Errorf("an offer with %q: the plan names %+v, want the form %q", credential, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("4 a stored definition from before the settings rule still composes", func(t *testing.T) {
|
||||
m := twoMergeableFiles(nil, nil)
|
||||
m.Resources[2]["content"] = "WORKERS=${setting:workers}\n"
|
||||
raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1", "resources": m.Resources,
|
||||
"settings": map[string]any{"workers": map[string]any{"kind": mcat.KindPreference, "default": 4, "why": "a number"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, err := mcat.Decode(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("a stored build from before the rule is not read again: %v", err)
|
||||
}
|
||||
if err := JudgeSettings(read, nil, false); err != nil {
|
||||
t.Errorf("a stored definition is composed as before, and was refused: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("5 a placeholder outside its holder is refused at composition", func(t *testing.T) {
|
||||
for _, c := range []struct{ content, want string }{
|
||||
{"${environment:posix}", "toolchain's resource rc names ${environment:posix} and toolchain does not claim node-environment"},
|
||||
{"${shell:zsh:normal}", "toolchain's resource rc names ${shell:zsh:normal} and toolchain does not claim node-login-shell"},
|
||||
} {
|
||||
m := mcat.Manifest{Module: "toolchain", Resources: []map[string]any{
|
||||
{"id": "rc", "type": "file", "path": "/etc/rc", "content": c.content}}}
|
||||
r := mcat.Resolution{Node: "workstation", Account: "op", Modules: []mcat.Manifest{m}}
|
||||
if _, err := Declaration(r, mcat.Rendering{}); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("composition accepted %s outside its holder: %v", c.content, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("6 a provider read from its tree composes where it would be handed", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "files", "consumers.json"), []byte(consumersText), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m mcat.Manifest
|
||||
if err := json.Unmarshal([]byte(handedProvider), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filled, err := mcat.WithTemplatesFrom(m, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("a provider read from its tree is refused: %v", err)
|
||||
}
|
||||
if _, err := Compose(notifierOn(t, filled), handingTo("store:5000")); err != nil {
|
||||
t.Fatalf("a provider read from its tree does not compose where it would be handed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("7 a module whose field is set aside is composed whole", func(t *testing.T) {
|
||||
for where, raw := range map[string]string{
|
||||
"the top": `{"module": "later", "version": "2", "a-field-from-later": {"x": 1}}`,
|
||||
"a state": `{"module": "later", "version": "2", "state": [{"name": "s", "a-field-from-later": 1}]}`,
|
||||
} {
|
||||
var m mcat.Manifest
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
t.Fatalf("%s: %v", where, err)
|
||||
}
|
||||
if left := LeftOut(mcat.Resolution{Node: "laptop", Modules: []mcat.Manifest{m, notifier()}}, nil, false); len(left) != 0 {
|
||||
t.Errorf("%s: a module was left out for a field set aside: %v", where, left)
|
||||
}
|
||||
}
|
||||
var later mcat.Manifest
|
||||
if err := json.Unmarshal([]byte(`{"module": "later", "version": "2", "a-field-from-later": 1,
|
||||
"resources": [{"id": "d", "type": "directory", "mode": "0700"}, {"id": "rc", "type": "file", "path": "/etc/later.conf", "content": "x\n"}],
|
||||
"data": {"own": [{"id": "d", "path": "${dir:d}", "class": "valuable"}]}}`), &later); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
holder := mcat.Manifest{Module: "backups", Version: "1",
|
||||
Claims: []mcat.Claim{{Name: mcat.BackupSeat, Scope: mcat.ScopeNode}},
|
||||
Resources: []map[string]any{{"id": "list", "type": "file", "path": "/etc/backups.list", "mode": "0644",
|
||||
"content": "${contribution:" + mcat.BackupSeat + ":backup}"}}}
|
||||
r := anAdoptedAnchor()
|
||||
r.Modules = append(r.Modules, holder, later)
|
||||
composed, err := Compose(r, anchorRendering(false))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, left := composed.LeftOut["later"]; left {
|
||||
t.Fatalf("left out: %v", composed.LeftOut)
|
||||
}
|
||||
got := byID(composed.Resources)
|
||||
if _, declared := got["later.rc"]; !declared {
|
||||
t.Fatal("the module's file is not declared")
|
||||
}
|
||||
if list, _ := got["backups.list"]["content"].(string); !strings.Contains(list, "# later") || !strings.Contains(list, "path /var/lib/later/d") {
|
||||
t.Fatalf("the module's data is not backed up:\n%s", list)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("8 composing refuses a silent sidecar", func(t *testing.T) {
|
||||
if err := JudgeSettings(twoMergeableFiles([]any{"server"}, nil), nil, false); err == nil || !strings.Contains(err.Error(), "sidecar") {
|
||||
t.Errorf("composing it should refuse it, and said: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("9 ApplySettings refuses a file that does not merge and names keys", func(t *testing.T) {
|
||||
plain := map[string]any{"id": "plain", "type": "file", "path": "/x", "content": "x", mcat.TakesSettingsField: []any{"a"}}
|
||||
if _, err := ApplySettings(plain, nil); err == nil {
|
||||
t.Error("a file that does not merge and names keys was settled")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("10 the runtime is told whom agents run as", func(t *testing.T) {
|
||||
envOf := func(r mcat.Resolution) map[string]string {
|
||||
r.Node, r.Modules = "anchor", []mcat.Manifest{aToolsModule(t, "nftables", "tools/index.js"), theRuntime(t)}
|
||||
process := processOf(declared(t, r, runtimeWith))
|
||||
if process == nil {
|
||||
t.Fatal("no runtime process was composed")
|
||||
}
|
||||
return process["env"].(map[string]string)
|
||||
}
|
||||
env := envOf(mcat.Resolution{Account: "ops", AgentAccount: "agent"})
|
||||
if env[mcat.RuntimeAgentAccount] != "agent" || env[mcat.RuntimeAgentHome] != "/home/agent" || env[mcat.RuntimeOperatorAccount] != "ops" {
|
||||
t.Errorf("the runtime is not told whom agents run as: %v", env)
|
||||
}
|
||||
env = envOf(mcat.Resolution{Account: "ops"})
|
||||
if env[mcat.RuntimeAgentAccount] != "ops" || env[mcat.RuntimeAgentHome] != "/home/ops" {
|
||||
t.Errorf("with no agent account, agents run as the operator: %v", env)
|
||||
}
|
||||
if _, set := envOf(mcat.Resolution{})[mcat.RuntimeAgentAccount]; set {
|
||||
t.Error("a machine with no account names an agent account")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("11 the session's files are placed by the display server's holder alone", func(t *testing.T) {
|
||||
xorg := mcat.Manifest{Module: "xorg", Claims: []mcat.Claim{{Name: mcat.DisplayServerSeat}},
|
||||
Shell: []mcat.ShellCode{{For: "xinitrc", Slot: "first", Code: "xset s off"}},
|
||||
Resources: []map[string]any{
|
||||
{"id": "xinitrc", "type": "file", "path": "/home/op/.xinitrc",
|
||||
"content": "${shell:xinitrc:first}${shell:xinitrc:normal}${shell:xinitrc:last}"},
|
||||
{"id": "xresources", "type": "file", "path": "/home/op/.Xresources", "content": "${shell:xresources:normal}"},
|
||||
}}
|
||||
i3 := mcat.Manifest{Module: "i3", Shell: []mcat.ShellCode{{For: "xinitrc", Slot: "last", Code: "exec i3"}}}
|
||||
theme := mcat.Manifest{Module: "theme", Shell: []mcat.ShellCode{
|
||||
{For: "xresources", Slot: "normal", Code: "Xft.dpi: 96"}, {For: "zsh", Slot: "normal", Code: "not for the session"}}}
|
||||
out := declared(t, mcat.Resolution{Node: "workstation", Account: "op", Modules: []mcat.Manifest{xorg, i3, theme}}, mcat.Rendering{})
|
||||
by := map[string]any{}
|
||||
for _, res := range out {
|
||||
by[res["id"].(string)] = res["content"]
|
||||
}
|
||||
if got := by["xorg.xinitrc"]; got != "# xorg\nxset s off\n# i3\nexec i3\n" {
|
||||
t.Errorf("the .xinitrc is %q", got)
|
||||
}
|
||||
if got := by["xorg.xresources"]; got != "# theme\nXft.dpi: 96\n" {
|
||||
t.Errorf("the .Xresources is %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("12 what a handed module cannot have from the vault is refused", func(t *testing.T) {
|
||||
requires := strings.Replace(botMoved(), `"own-secrets":`, `"requires":["database"],"secrets":{"database":"/etc/bot/db"},"own-secrets":`, 1)
|
||||
r, err := Resolve(shelf(held(t, requires, builtBot("db = ${secret:database}\n")),
|
||||
mcat.Manifest{Module: "db", Version: "1", Provides: []mcat.Offer{{Name: "database", Scope: mcat.ScopeNode}}}),
|
||||
[]string{"bot"}, workstation(), mcat.World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Account = "op"
|
||||
if _, err := Compose(r, handingTo("store:5000")); err == nil || !strings.Contains(err.Error(), "moves with its provider") {
|
||||
t.Errorf("a template naming a required credential: %v", err)
|
||||
}
|
||||
inline := strings.Replace(botMoved(), `{"id":"daemon"`,
|
||||
`{"id":"legacy","type":"file","path":"/etc/bot/legacy","content":"t = ${secret:token}\n"},{"id":"daemon"`, 1)
|
||||
if _, err := Compose(notifierOn(t, held(t, inline, builtBot(botText))), handingTo("store:5000")); err == nil ||
|
||||
!strings.Contains(err.Error(), "keep it in a template") {
|
||||
t.Errorf("an inline file naming a secret in a handed module: %v", err)
|
||||
}
|
||||
})
|
||||
// 13, a held manifest's template checked by HeldTemplateProblem: TestAManifestStillNamingATemplateIsNotHeld in template_test.go, moved
|
||||
// with the catalogue's tests, and the held fixture each case above reads its manifests through.
|
||||
}
|
||||
|
||||
// route-proxy's proofs, items 1 to 3 of its list.
|
||||
func TestRouteProxysProofsOfAComposition(t *testing.T) {
|
||||
proxy := mcat.Manifest{Module: "route-proxy", Version: "1",
|
||||
Provides: []mcat.Offer{{Name: "route", Scope: mcat.ScopeMesh, AnsweredBy: mcat.AnsweredByHelper}}}
|
||||
world := mcat.World{Offered: map[string][]mcat.Provider{"route": {{Node: "anchor", At: "anchor.internal", Module: "route-proxy"}}}}
|
||||
|
||||
t.Run("1 route-proxy's manifest composes for a machine", func(t *testing.T) {
|
||||
root := beside.Dir(t, "mesh-networking")
|
||||
m, err := mcat.ReadTreeManifest(root, filepath.Join(root, "modules", "route-proxy"))
|
||||
if err != nil {
|
||||
t.Fatalf("route-proxy's manifest does not parse:\n%v", err)
|
||||
}
|
||||
// As a build would leave it: each artifact a container names resolved to an image; an archive unpacks a bundle.
|
||||
for _, res := range m.Resources {
|
||||
if artifact, ok := res["artifact"].(string); ok && res["type"] != "archive" {
|
||||
delete(res, "artifact")
|
||||
res["image"] = "registry.invalid/route-proxy/" + artifact +
|
||||
"@sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
}
|
||||
}
|
||||
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{m}, Needs: []mcat.Needed{{
|
||||
Name: "internal-acme-ca", From: "home", At: "home.internal", For: "route-proxy",
|
||||
Serves: map[string]any{"port": float64(9000), "path": "/acme/acme/directory", "roots": "/roots.pem"}}}}
|
||||
// Its own broker credential, sealed as the mesh would.
|
||||
out := declared(t, r, mcat.Rendering{Needed: map[string]map[string]string{"route-proxy": {"broker": "sealed"}}})
|
||||
if fileNamed(out, "route-proxy.acme-env") == nil {
|
||||
t.Fatalf("nothing writes the public issuer's environment: %v", ids(out))
|
||||
}
|
||||
if fileNamed(out, "route-proxy.bound-acme-ca") != nil {
|
||||
t.Error("a binding to acme-ca is still written")
|
||||
}
|
||||
sealed := false
|
||||
for _, res := range out {
|
||||
sealed = sealed || strings.Contains(fmt.Sprint(res), "sealed")
|
||||
}
|
||||
internal := false
|
||||
for _, res := range out {
|
||||
internal = internal || strings.Contains(fmt.Sprint(res["content"]), "home.internal:9000")
|
||||
}
|
||||
if !sealed || !internal {
|
||||
t.Errorf("the broker credential sealed: %v; the internal-acme-ca need resolved: %v (%v)", sealed, internal, ids(out))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("2 and 3 a route endpoint's machine port, and the plan's provision entry", func(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name, node, module string
|
||||
port int
|
||||
published []any
|
||||
placed map[int]int
|
||||
at int
|
||||
}{
|
||||
{"beside the proxy, moved by the placement (gitea)", "anchor", "gitea", 3000, []any{"3000"}, map[int]int{3000: 20000}, 20000},
|
||||
{"on another machine, moved by the placement (grafana)", "home", "grafana", 3000, []any{"3000"}, map[int]int{3000: 20012}, 20012},
|
||||
{"a bare port the placement holds none for (influxdb)", "home", "influxdb", 8086, []any{"8086"}, nil, 8086},
|
||||
{"a fixed mapping (mailu 7080:80)", "anchor", "mailu", 7080, []any{"25", "7080:80"}, nil, 7080},
|
||||
{"a fixed mapping the placement names another port of (photos 9102:9000)", "anchor", "photos", 9102,
|
||||
[]any{"9102:9000"}, map[int]int{9000: 20011}, 9102},
|
||||
{"the host's network (home-assistant)", "anchor", "home-assistant", 8123, nil, nil, 8123},
|
||||
} {
|
||||
container := map[string]any{"type": "container", "name": c.module, "image": "x"}
|
||||
if c.published != nil {
|
||||
container["ports"] = c.published
|
||||
} else {
|
||||
container["network"] = "host"
|
||||
}
|
||||
module := mcat.Manifest{Module: c.module, Version: "1", Requires: []string{"route"},
|
||||
Listens: []mcat.Listening{{Name: "web", Port: c.port, Protocol: "tcp", From: "mesh"}},
|
||||
Contributes: map[string]map[string]any{"route": {"label": c.module, "endpoint": "web"}},
|
||||
Resources: []map[string]any{container}}
|
||||
node := mcat.Node{Name: c.node, At: c.node + ".internal", PublicDomain: "example.org",
|
||||
Capabilities: map[string]bool{"container-runtime": true}}
|
||||
r, err := Resolve(map[string]mcat.Manifest{module.Module: module, proxy.Module: proxy}, []string{module.Module}, node, world)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.name, err)
|
||||
}
|
||||
r.Account = "op"
|
||||
for i, n := range r.Needs {
|
||||
r.Needs[i].AnsweredByHelper = n.Name == "route" // as the plan sets it (compose's answeredByHelper)
|
||||
}
|
||||
ports := map[int]int{} // as compose's portsFor gives them
|
||||
for _, l := range module.Listens {
|
||||
where, mayAssign := module.MachineSide(l.Port)
|
||||
if at, known := c.placed[l.Port]; mayAssign && known {
|
||||
where = at
|
||||
}
|
||||
ports[l.Port] = where
|
||||
}
|
||||
if ports[c.port] != c.at {
|
||||
t.Errorf("%s: the endpoint's machine port is %d, want %d", c.name, ports[c.port], c.at)
|
||||
}
|
||||
composed, err := Compose(r, mcat.Rendering{Hands: true, Provisioner: "mesh-provisioner-client",
|
||||
ArtifactStore: "store:5000", Planned: true, Ports: map[string]map[int]int{c.module: ports}})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.name, err)
|
||||
}
|
||||
// The plan's provision entry names the proxy and carries a Contributed the contract accepts, end to end.
|
||||
value, err := provisions.EncodePlan(provisions.Plan{Node: c.node, Module: c.module, Plan: composed.Plans[c.module]})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: the plan does not fit the contract: %v", c.name, err)
|
||||
}
|
||||
back, err := provisions.DecodePlan(value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.name, err)
|
||||
}
|
||||
var entry *mcat.PlanProvision
|
||||
for i := range back.Plan.Provisions {
|
||||
if back.Plan.Provisions[i].Provision == "route" {
|
||||
entry = &back.Plan.Provisions[i]
|
||||
}
|
||||
}
|
||||
if entry == nil || entry.ProviderModule != "route-proxy" || len(entry.Contributed) == 0 ||
|
||||
!strings.Contains(string(entry.Contributed), fmt.Sprint(c.at)) {
|
||||
t.Errorf("%s: the plan's route entry %+v names no proxy, or not the port %d", c.name, entry, c.at)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user