mesh-delivery-indexer: the judge's tests judge manifests and compose nothing
ADR 0593 §7: the catalogue's composition (Resolution.Compose and Declaration, LeftOut, Resolve, JudgeSettings, ApplySettings, HeldTemplateProblem) leaves mesh-sdk for the node-engine, so the judge's tests keep only its verdicts and refusals; each proof that a manifest composes goes to the node-engine's tests, and the helpers only those proofs used go with them. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -121,15 +121,10 @@ func TestABuildsBundlesAreCarriedOnTheResolvedManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0192: a tools bundle says what it is given; the composer resolves it per machine as
|
||||
// a container's environment, hands it to the runtime as the module's words, and makes what the
|
||||
// words name readable by the account the runtime runs as.
|
||||
// novox/hq ADR 0192: a tools bundle says what it is given, written with ${dir:…} and ${port:…}, and the judge takes
|
||||
// it; how the node-engine resolves the words per machine and hands them to the runtime is its own composition's to test
|
||||
// (ADR 0593 §7).
|
||||
func TestABundleIsGivenItsWordsResolvedForThisMachine(t *testing.T) {
|
||||
with := catalogue.Rendering{ArtifactStore: "anchor.internal:5101",
|
||||
Needed: map[string]map[string]string{
|
||||
catalogue.RuntimeModule: {"broker": "sealed-credential"},
|
||||
"dash": {"token": "sealed-token"},
|
||||
}}
|
||||
dash := catalogue.Manifest{Module: "dash", Version: "1", Tools: []string{"status"},
|
||||
Listens: []catalogue.Listening{{Name: "web", Port: 3000, Protocol: "tcp"}},
|
||||
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "${dir:mesh-state}/token"}},
|
||||
@@ -149,82 +144,6 @@ func TestABundleIsGivenItsWordsResolvedForThisMachine(t *testing.T) {
|
||||
if problems := buildProblems(dash.Build, dash.Module); len(problems) > 0 {
|
||||
t.Fatalf("a bundle's words written with ${dir:…} and ${port:…} were refused: %v", problems)
|
||||
}
|
||||
dash, err := dash.Resolve([]catalogue.Built{{Name: "tools", Kind: catalogue.ArtifactBundle,
|
||||
Reference: catalogue.ArtifactStoreScheme + "dash/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other := aToolsModule(t, "nftables", "tools/index.js")
|
||||
|
||||
out, err := catalogue.Resolution{Node: "anchor", Account: "ops", Modules: []catalogue.Manifest{dash, other, theRuntime(t)}}.Declaration(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := fileNamed(out, "dash.mesh-state")
|
||||
if dir == nil {
|
||||
t.Fatalf("no directory: %v", ids(out))
|
||||
}
|
||||
at := fmt.Sprint(dir["path"])
|
||||
process := fileNamed(out, catalogue.RuntimeModule+"."+catalogue.RuntimeProcessID())
|
||||
env := process["env"].(map[string]string)
|
||||
var given map[string]map[string]string
|
||||
if err := json.Unmarshal([]byte(env[catalogue.RuntimeToolEnv]), &given); err != nil {
|
||||
t.Fatalf("the runtime's %s is not JSON: %q", catalogue.RuntimeToolEnv, env[catalogue.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)
|
||||
}
|
||||
// What the words name is the account's to read; nothing else of the module's is touched.
|
||||
for _, id := range []string{"dash.mesh-state", "dash.config", "dash." + catalogue.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)
|
||||
}
|
||||
// A file a word names restarts the runtime when it changes, as it restarted the tool container.
|
||||
restarts := fmt.Sprint(process["restart-on"])
|
||||
for _, want := range []string{"dash.config", "dash." + catalogue.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)
|
||||
}
|
||||
|
||||
t.Run("on a machine with no account the runtime is root and nothing is re-owned", func(t *testing.T) {
|
||||
out, err := catalogue.Resolution{Node: "anchor", Modules: []catalogue.Manifest{dash, theRuntime(t)}}.Declaration(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := fileNamed(out, "dash.config"); r["owner"] != nil {
|
||||
t.Errorf("re-owned with no account: %v", r)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a change to a module's words changes the runtime's process", func(t *testing.T) {
|
||||
changed := dash
|
||||
changed.Bundles = append([]catalogue.Bundle(nil), dash.Bundles...)
|
||||
changed.Bundles[0].Env = map[string]string{"DASH_ADMIN": "somebody-else"}
|
||||
out2, err := catalogue.Resolution{Node: "anchor", Account: "ops", Modules: []catalogue.Manifest{changed, theRuntime(t)}}.Declaration(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fmt.Sprint(fileNamed(out2, catalogue.RuntimeModule+"."+catalogue.RuntimeProcessID())["env"]) == fmt.Sprint(env) {
|
||||
t.Error("the runtime's process is the same after a module's words changed, so it would not restart")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// novox/hq 04-ISSUES/216: a bundle nothing loads, runs or unpacks is refused at registration; saying
|
||||
@@ -685,8 +604,6 @@ func TestAGoBundleMayNameItsCommand(t *testing.T) {
|
||||
// novox/hq ADR 0193: a Go tools bundle is served — its binary is what the runtime starts, delivered
|
||||
// like any tools bundle, named to the runtime where a TypeScript bundle names its launcher.
|
||||
func TestAGoToolsBundleIsServedByItsBinary(t *testing.T) {
|
||||
with := catalogue.Rendering{ArtifactStore: "anchor.internal:5101",
|
||||
Needed: map[string]map[string]string{catalogue.RuntimeModule: {"broker": "sealed-credential"}}}
|
||||
lamp := catalogue.Manifest{Module: "lamp", Version: "1", Tools: []string{"on"},
|
||||
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "go",
|
||||
System: "arch", From: "cmd/lamp-tools"}}}}
|
||||
@@ -701,17 +618,6 @@ func TestAGoToolsBundleIsServedByItsBinary(t *testing.T) {
|
||||
if fmt.Sprint(lamp.Bundles[0].Loads) != "[lamp-tools]" {
|
||||
t.Fatalf("the runtime loads %v from a Go bundle, want its binary", lamp.Bundles[0].Loads)
|
||||
}
|
||||
out, err := catalogue.Resolution{Node: "anchor", Account: "ops", Modules: []catalogue.Manifest{lamp, theRuntime(t)}}.Declaration(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fileNamed(out, "lamp."+catalogue.BundleID("tools")) == nil {
|
||||
t.Errorf("the Go bundle is not delivered: %v", ids(out))
|
||||
}
|
||||
env := fileNamed(out, catalogue.RuntimeModule+"."+catalogue.RuntimeProcessID())["env"].(map[string]string)
|
||||
if env[catalogue.RuntimeToolModules] != "lamp="+catalogue.BundlePath("lamp", "tools")+"/lamp-tools" {
|
||||
t.Errorf("the runtime is told %q, want the binary", env[catalogue.RuntimeToolModules])
|
||||
}
|
||||
|
||||
// An artifact may say it explicitly; naming anything but the binary is refused.
|
||||
said := catalogue.Manifest{Module: "lamp", Version: "1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "tools",
|
||||
@@ -758,18 +664,10 @@ func TestAHandedModulesPlanNamesTheCredentialFormOfItsProvidersOffer(t *testing.
|
||||
`,"credential":{"issued-by":"outside"}`: catalogue.CredentialFormGiven,
|
||||
`,"credential":{"issued-by":"software","tool":"store_issued_token"}`: catalogue.CredentialFormIssued,
|
||||
} {
|
||||
node := workstation()
|
||||
node.At = "workstation.internal"
|
||||
consumer := notifierConsumer(t, true, false)
|
||||
r, err := catalogue.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"))
|
||||
got := c.Plans["notify"].Provisions
|
||||
if len(got) != 1 || got[0].CredentialForm != want {
|
||||
t.Errorf("an offer with %q: the plan names %+v, want the form %q", credential, got, want)
|
||||
// The form the provider's offer declares, read from its manifest as the judge reads it; a plan naming it is the
|
||||
// node-engine's composition (ADR 0593 §7).
|
||||
if got := storeOffering(credential).CredentialFormOf("postgres-database"); got != want {
|
||||
t.Errorf("an offer with %q: the form %q, want %q", credential, got, want)
|
||||
}
|
||||
// Every form a plan names is in the one list each reader checks (issue 774); an undeclared one is not.
|
||||
if catalogue.IsCredentialForm(want) != (want != "") {
|
||||
@@ -1597,13 +1495,9 @@ func TestAModuleThatDeclaresASettingNamesItsFilesKeys(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, err := ParseManifest(raw)
|
||||
if err != nil {
|
||||
if _, err := ParseManifest(raw); err != nil {
|
||||
t.Fatalf("a stored build from before the rule is not read again: %v", err)
|
||||
}
|
||||
if err := catalogue.JudgeSettings(read, nil, false); err != nil {
|
||||
t.Errorf("a stored definition is composed as before, and was refused: %v", err)
|
||||
}
|
||||
|
||||
if problems := WrittenSettingsProblems(declaring([]any{"server"}, []any{})); len(problems) != 0 {
|
||||
t.Errorf("every mergeable file names its keys, and was refused: %v", problems)
|
||||
@@ -2451,8 +2345,8 @@ func TestAPathThroughAnAccessIsJudgedWithTheAccessFilledIn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0203 §5 and ADR 0204 §3: a placeholder outside the seat's holder is refused — by the parser,
|
||||
// which is what the catalogue check and registration run, and again at composition, in the same words.
|
||||
// ADR 0203 §5 and ADR 0204 §3: a placeholder outside the seat's holder is refused by the parser, which is what the
|
||||
// catalogue check and registration run (its refusal again at composition is the node-engine's, ADR 0593 §7).
|
||||
func TestAPlaceholderOutsideTheHolderIsRefused(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"},
|
||||
@@ -2462,12 +2356,6 @@ func TestAPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
|
||||
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("the catalogue check accepted %s outside its holder: %v", c.content, err)
|
||||
}
|
||||
m := catalogue.Manifest{Module: "toolchain", Resources: []map[string]any{
|
||||
{"id": "rc", "type": "file", "path": "/etc/rc", "content": c.content}}}
|
||||
r := catalogue.Resolution{Node: "workstation", Account: "op", Modules: []catalogue.Manifest{m}}
|
||||
if _, err := r.Declaration(catalogue.Rendering{}); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("composition accepted %s outside its holder: %v", c.content, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2638,7 +2526,7 @@ func TestAPromptHoldingMemberOnASingleSecretIsRefused(t *testing.T) {
|
||||
}
|
||||
|
||||
// What `module check` and the merge gate judge: the provider read from its tree, its text inline. Judged by the same
|
||||
// rules, a provider naming a provision it does not provide is refused there too, and one that does composes.
|
||||
// rules, a provider naming a provision it does not provide is refused there too, and one that does is taken.
|
||||
func TestAProviderReadFromItsTreeIsJudgedByTheSameRules(t *testing.T) {
|
||||
read := func(text string) (catalogue.Manifest, error) {
|
||||
dir := t.TempDir()
|
||||
@@ -2654,13 +2542,9 @@ func TestAProviderReadFromItsTreeIsJudgedByTheSameRules(t *testing.T) {
|
||||
}
|
||||
return WithTemplatesFrom(m, dir)
|
||||
}
|
||||
filled, err := read(consumersText)
|
||||
if err != nil {
|
||||
if _, err := read(consumersText); err != nil {
|
||||
t.Fatalf("a provider read from its tree is refused: %v", err)
|
||||
}
|
||||
if _, err := notifierOn(t, filled).Compose(handingTo("store:5000")); err != nil {
|
||||
t.Fatalf("a provider read from its tree does not compose where it would be handed: %v", err)
|
||||
}
|
||||
if _, err := read("${consumers:cache}"); err == nil || !strings.Contains(err.Error(), "does not provide") {
|
||||
t.Fatalf("a provider read from its tree naming a provision it does not provide: %v", err)
|
||||
}
|
||||
@@ -3028,7 +2912,6 @@ func TestAStateNameIsLocalAndOneToken(t *testing.T) {
|
||||
// refuses it. One case per block whose decoder wraps the decoder's words in its own.
|
||||
func TestAStoredManifestWithAnUnknownKeyIsSetAsideAndTheJudgeRefusesIt(t *testing.T) {
|
||||
t.Run("it still provides what it provides", aModuleWithAFieldSetAsideStillProvidesWhatItProvides)
|
||||
t.Run("it is composed whole", aModuleWithAFieldSetAsideIsComposedWhole)
|
||||
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}]}`,
|
||||
@@ -3050,10 +2933,6 @@ func TestAStoredManifestWithAnUnknownKeyIsSetAsideAndTheJudgeRefusesIt(t *testin
|
||||
if where != "the top" && !strings.Contains(m.UnknownField(), ": json: unknown field") {
|
||||
t.Errorf("%s: the block's decoder did not say it: %q", where, m.UnknownField())
|
||||
}
|
||||
left := catalogue.Resolution{Node: "laptop", Modules: []catalogue.Manifest{m, notifier()}}.LeftOut(nil, false)
|
||||
if len(left) != 0 {
|
||||
t.Errorf("%s: a module was left out for a field set aside: %v", where, left)
|
||||
}
|
||||
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "a-field-from-later") {
|
||||
t.Errorf("%s: the module check took it: %v", where, err)
|
||||
}
|
||||
@@ -4049,9 +3928,6 @@ func TestOneNamingFileMakesEveryMergeableFileName(t *testing.T) {
|
||||
if len(problems) != 1 || !strings.Contains(problems[0], "sidecar merges and names no takes-settings") {
|
||||
t.Fatalf("problems: %v", problems)
|
||||
}
|
||||
if err := catalogue.JudgeSettings(m, nil, false); err == nil || !strings.Contains(err.Error(), "sidecar") {
|
||||
t.Errorf("composing it should refuse it too, and said: %v", err)
|
||||
}
|
||||
raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1", "resources": m.Resources})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -4291,12 +4167,6 @@ func TestTakesSettingsIsRefusedWhereItCannotMeanAnything(t *testing.T) {
|
||||
t.Errorf("%s: want a problem saying %q, got %v", name, c.says, problems)
|
||||
}
|
||||
}
|
||||
// And a file that does not merge is refused where it is settled, not handed to the host.
|
||||
plain := map[string]any{"id": "plain", "type": "file", "path": "/x", "content": "x",
|
||||
catalogue.TakesSettingsField: []any{"a"}}
|
||||
if _, err := catalogue.ApplySettings(plain, nil); err == nil {
|
||||
t.Error("a file that does not merge and names keys was settled")
|
||||
}
|
||||
}
|
||||
|
||||
// The module that provides the artifact store may not be delivered through it.
|
||||
@@ -4881,34 +4751,9 @@ func TestTheResolvedPathRulesAreTheNodeEnginesOwn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Whom agents run as is the runtime's, told by the node-engine's composition (its own to test, ADR 0593 §7): a bundle
|
||||
// may not say it.
|
||||
func TestTheRuntimeIsToldTheAgentAccount(t *testing.T) {
|
||||
with := catalogue.Rendering{ArtifactStore: "anchor.internal:5101",
|
||||
Needed: map[string]map[string]string{catalogue.RuntimeModule: {"broker": "sealed-credential"}}}
|
||||
envOf := func(r catalogue.Resolution) map[string]string {
|
||||
t.Helper()
|
||||
r.Node, r.Modules = "anchor", []catalogue.Manifest{aToolsModule(t, "nftables", "tools/index.js"), theRuntime(t)}
|
||||
out, err := r.Declaration(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
process := fileNamed(out, catalogue.RuntimeModule+"."+catalogue.RuntimeProcessID())
|
||||
if process == nil {
|
||||
t.Fatal("no runtime process was composed")
|
||||
}
|
||||
return process["env"].(map[string]string)
|
||||
}
|
||||
env := envOf(catalogue.Resolution{Account: "ops", AgentAccount: "agent"})
|
||||
if env[catalogue.RuntimeAgentAccount] != "agent" || env[catalogue.RuntimeAgentHome] != "/home/agent" || env[catalogue.RuntimeOperatorAccount] != "ops" {
|
||||
t.Errorf("the runtime is not told whom agents run as: %v", env)
|
||||
}
|
||||
env = envOf(catalogue.Resolution{Account: "ops"})
|
||||
if env[catalogue.RuntimeAgentAccount] != "ops" || env[catalogue.RuntimeAgentHome] != "/home/ops" {
|
||||
t.Errorf("with no agent account, agents run as the operator: %v", env)
|
||||
}
|
||||
env = envOf(catalogue.Resolution{})
|
||||
if _, set := env[catalogue.RuntimeAgentAccount]; set {
|
||||
t.Errorf("a machine with no account names an agent account: %v", env)
|
||||
}
|
||||
if problems := bundleEnvProblems("x", catalogue.Artifact{Name: "b", Kind: catalogue.ArtifactBundle, Loads: []string{"x"},
|
||||
Env: map[string]string{catalogue.RuntimeAgentAccount: "me"}}); len(problems) == 0 {
|
||||
t.Error("a bundle may tell the runtime whom agents run as")
|
||||
@@ -5052,37 +4897,10 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// §4: xinitrc and xresources slots, placed by the display server's holder alone.
|
||||
// §4: xinitrc and xresources slots, placed by the display server's holder alone: the judge takes a session
|
||||
// contribution and refuses its placeholder outside the holder (the files the holder's composition places are the
|
||||
// node-engine's to test, ADR 0593 §7).
|
||||
func TestTheSessionsFilesArePlacedByTheDisplayServersHolderAlone(t *testing.T) {
|
||||
xorg := catalogue.Manifest{Module: "xorg", Claims: []catalogue.Claim{{Name: catalogue.DisplayServerSeat}},
|
||||
Shell: []catalogue.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 := catalogue.Manifest{Module: "i3", Shell: []catalogue.ShellCode{{For: "xinitrc", Slot: "last", Code: "exec i3"}}}
|
||||
theme := catalogue.Manifest{Module: "theme", Shell: []catalogue.ShellCode{
|
||||
{For: "xresources", Slot: "normal", Code: "Xft.dpi: 96"},
|
||||
{For: "zsh", Slot: "normal", Code: "not for the session"},
|
||||
}}
|
||||
r := catalogue.Resolution{Node: "workstation", Account: "op", Modules: []catalogue.Manifest{xorg, i3, theme}}
|
||||
out, err := r.Declaration(catalogue.Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// The contributions parse; the placeholders parse only in the holder.
|
||||
if _, err := ParseManifest([]byte(`{"module":"i3","shell":[{"for":"xinitrc","slot":"last","code":"exec i3"},` +
|
||||
`{"for":"xresources","slot":"normal","code":"i3.font: x"}]}`)); err != nil {
|
||||
@@ -5301,25 +5119,6 @@ func TestUsingASeatNobodyDeclaresIsRefused(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWhatAHandedModuleCannotHaveFromTheVaultIsRefused(t *testing.T) {
|
||||
// A template naming a credential the module requires: a binding's, which moves with its provider.
|
||||
requires := strings.Replace(botMoved(), `"own-secrets":`, `"requires":["database"],"secrets":{"database":"/etc/bot/db"},"own-secrets":`, 1)
|
||||
r, err := catalogue.Resolve(shelf(held(t, requires, builtBot("db = ${secret:database}\n")),
|
||||
catalogue.Manifest{Module: "db", Version: "1", Provides: []catalogue.Offer{{Name: "database", Scope: catalogue.ScopeNode}}}),
|
||||
[]string{"bot"}, workstation(), catalogue.World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Account = "op"
|
||||
if _, err := r.Compose(handingTo("store:5000")); err == nil || !strings.Contains(err.Error(), "moves with its provider") {
|
||||
t.Errorf("a template naming a required credential: %v", err)
|
||||
}
|
||||
// A file with its text inline naming a secret, in a handed module.
|
||||
inline := strings.Replace(botMoved(), `{"id":"daemon"`,
|
||||
`{"id":"legacy","type":"file","path":"/etc/bot/legacy","content":"t = ${secret:token}\n"},{"id":"daemon"`, 1)
|
||||
r = notifierOn(t, held(t, inline, builtBot(botText)))
|
||||
if _, err := r.Compose(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)
|
||||
}
|
||||
// A family and a secret issued outside the mesh are not handed by name yet: the mark is refused.
|
||||
for why, raw := range map[string]string{
|
||||
"a family": strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"own-secrets":{"token":"${dir:state}/token","key-*":"${dir:state}/keys"}`, 1),
|
||||
@@ -5445,39 +5244,6 @@ func aToolsModule(t *testing.T, name string, entrypoints ...string) catalogue.Ma
|
||||
|
||||
var bundleDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
|
||||
// theRuntime is the runtime module as the catalogue holds it: its own bundle, run rather than
|
||||
// loaded, and its broker secret to receive the node's credential in.
|
||||
func theRuntime(t *testing.T) catalogue.Manifest {
|
||||
t.Helper()
|
||||
m := catalogue.Manifest{Module: catalogue.RuntimeModule, Version: "1",
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + catalogue.RuntimeModule + "/broker"}},
|
||||
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "runtime", Kind: catalogue.ArtifactBundle, Language: "typescript",
|
||||
Entrypoints: []string{"src/main.js"}}}}}
|
||||
resolved, err := m.Resolve([]catalogue.Built{{Name: "runtime", Kind: catalogue.ArtifactBundle,
|
||||
Reference: catalogue.ArtifactStoreScheme + catalogue.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
func fileNamed(out []map[string]any, id string) map[string]any {
|
||||
for _, r := range out {
|
||||
if r["id"] == id {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ids(out []map[string]any) []string {
|
||||
var names []string
|
||||
for _, r := range out {
|
||||
names = append(names, r["id"].(string))
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func busyManifest(t *testing.T, on string, busy any) []byte {
|
||||
t.Helper()
|
||||
extra := map[string]any{"id": "agent", "type": "container", "name": "app-agent",
|
||||
@@ -5566,60 +5332,6 @@ const templatedTwin = `{"module":"tidy","version":"1",
|
||||
{"id":"main","type":"file","path":"${dir:conf}/tidy.conf","mode":"0644","template":"files/tidy.conf"},
|
||||
{"id":"extra","type":"file","path":"${dir:conf}/extra.conf","template":"files/deeper/extra.conf"}]}`
|
||||
|
||||
func workstation() catalogue.Node {
|
||||
return catalogue.Node{Name: "workstation", Site: "house",
|
||||
Capabilities: map[string]bool{"seat": true, "container-runtime": true}}
|
||||
}
|
||||
|
||||
// notifierConsumer is the notifier requiring a mesh-scoped database, marked moved when moved, with its credential
|
||||
// kept under two local names (ADR 0094) when locals is set.
|
||||
func notifierConsumer(t *testing.T, moved, locals bool) catalogue.Manifest {
|
||||
t.Helper()
|
||||
raw := handedNotifier
|
||||
if moved {
|
||||
raw = notifierMoved()
|
||||
}
|
||||
add := `"requires":["postgres-database"],`
|
||||
if locals {
|
||||
add += `"secrets":{"postgres-database":{"hot":"/var/lib/notify/hot","cold":"/var/lib/notify/cold"}},`
|
||||
}
|
||||
raw = strings.Replace(raw, `"build":`, add+`"build":`, 1)
|
||||
return held(t, raw, builtNotifier())
|
||||
}
|
||||
|
||||
func shelf(ms ...catalogue.Manifest) map[string]catalogue.Manifest {
|
||||
out := map[string]catalogue.Manifest{}
|
||||
for _, m := range ms {
|
||||
out[m.Module] = m
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// twoStores is a mesh where two machines each run a store providing the database, the consumer pinned to one.
|
||||
func twoStores(pin string) catalogue.World {
|
||||
return catalogue.World{Offered: map[string][]catalogue.Provider{"postgres-database": {
|
||||
{Node: "anchor", At: "anchor.internal", Module: "store", Serves: map[string]any{"port": 5432}},
|
||||
{Node: "home", At: "home.internal", Module: "store-two", Serves: map[string]any{"port": 5434}},
|
||||
}}, Pinned: map[string]catalogue.Chosen{"postgres-database": {Node: pin}}}
|
||||
}
|
||||
|
||||
func composeWith(t *testing.T, r catalogue.Resolution, with catalogue.Rendering) (catalogue.Composed, string) {
|
||||
t.Helper()
|
||||
c, err := r.Compose(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := json.Marshal(c.Resources)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c, string(raw)
|
||||
}
|
||||
|
||||
func handingTo(store string) catalogue.Rendering {
|
||||
return catalogue.Rendering{Hands: true, Provisioner: "mesh-provisioner-client", ArtifactStore: store}
|
||||
}
|
||||
|
||||
const barTemplate = `{{if eq .shows "battery"}}[[block]]
|
||||
block = "battery"
|
||||
{{with .options}}{{with .device}}device = {{quote .}}
|
||||
@@ -5905,16 +5617,6 @@ const handedProvider = `{"module":"db","version":"1","moved":true,
|
||||
|
||||
const consumersText = "${consumers:postgres-database}"
|
||||
|
||||
func notifierOn(t *testing.T, m catalogue.Manifest) catalogue.Resolution {
|
||||
t.Helper()
|
||||
r, err := catalogue.Resolve(shelf(m), []string{m.Module}, workstation(), catalogue.World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Account = "op"
|
||||
return r
|
||||
}
|
||||
|
||||
func placedModule() catalogue.Manifest {
|
||||
return catalogue.Manifest{
|
||||
Module: "photos",
|
||||
@@ -6042,38 +5744,6 @@ func aModuleWithAFieldSetAsideStillProvidesWhatItProvides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A module with a field set aside is composed whole: its files are declared and its data backed up (novox/hq ADR 0578
|
||||
// §6; before, ADR 0262 left it out and backed up its data alone).
|
||||
func aModuleWithAFieldSetAsideIsComposedWhole(t *testing.T) {
|
||||
var later catalogue.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 := catalogue.Manifest{Module: "backups", Version: "1",
|
||||
Claims: []catalogue.Claim{{Name: catalogue.BackupSeat, Scope: catalogue.ScopeNode}},
|
||||
Resources: []map[string]any{{"id": "list", "type": "file", "path": "/etc/backups.list", "mode": "0644",
|
||||
"content": "${contribution:" + catalogue.BackupSeat + ":backup}"}}}
|
||||
r := anAdoptedAnchor()
|
||||
r.Modules = append(r.Modules, holder, later)
|
||||
composed, err := r.Compose(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")
|
||||
}
|
||||
list, _ := got["backups.list"]["content"].(string)
|
||||
if !strings.Contains(list, "# later") || !strings.Contains(list, "path /var/lib/later/d") {
|
||||
t.Fatalf("the left-out module's data is no longer backed up:\n%s", list)
|
||||
}
|
||||
}
|
||||
|
||||
func notifier() catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: "notifier",
|
||||
Settings: map[string]catalogue.SettingDeclaration{
|
||||
@@ -6282,9 +5952,6 @@ func held(t *testing.T, raw string, built []catalogue.Built) catalogue.Manifest
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if problem := catalogue.HeldTemplateProblem(out); problem != "" {
|
||||
t.Fatal(problem)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -6313,66 +5980,6 @@ func notifierMoved() string {
|
||||
return strings.Replace(handedNotifier, `"version":"1",`, `"version":"1","moved":true,`, 1)
|
||||
}
|
||||
|
||||
func builtNotifier() []catalogue.Built {
|
||||
return []catalogue.Built{{Name: "files", Kind: catalogue.ArtifactArchive,
|
||||
Reference: catalogue.ArtifactStoreScheme + "notify/files/blobs/sha256:" + strings.Repeat("d", 64),
|
||||
Digest: "sha256:" + strings.Repeat("d", 64), Texts: map[string]string{"rc": notifierText}}}
|
||||
}
|
||||
|
||||
// anAdoptedAnchor is the control-node's set: the store, the bus, the registry, the private network,
|
||||
// a served module and the filter module.
|
||||
func anAdoptedAnchor() catalogue.Resolution {
|
||||
return catalogue.Resolution{Node: "anchor", Modules: []catalogue.Manifest{
|
||||
{Module: "network", Computed: "overlay"},
|
||||
{Module: "postgres", Guards: []int{5432},
|
||||
Listens: []catalogue.Listening{{Port: 5432, From: catalogue.FromMesh}},
|
||||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store",
|
||||
"ports": []any{"5432:5432"}}}},
|
||||
{Module: "lavinmq", Guards: []int{15672},
|
||||
Listens: []catalogue.Listening{{Port: 5671, From: catalogue.FromMesh}, {Port: 5672, From: catalogue.FromMesh}},
|
||||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-broker",
|
||||
"ports": []any{"5671:5671", "5672:5672", "127.0.0.1:15672:15672"}}}},
|
||||
{Module: "distribution",
|
||||
Listens: []catalogue.Listening{{Port: 5000, From: catalogue.FromMesh}},
|
||||
Resources: []map[string]any{{"id": "store", "type": "container", "name": "registry",
|
||||
"ports": []any{"5000"}}}},
|
||||
{Module: "hello-web",
|
||||
Listens: []catalogue.Listening{{Port: 8080, From: catalogue.FromEverywhere}},
|
||||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "hello-web",
|
||||
"ports": []any{"8080"}}}},
|
||||
{Module: "helper", Listens: []catalogue.Listening{{Port: 9000, From: catalogue.FromMachine}}},
|
||||
{Module: "nftables", Filtering: &catalogue.Filtering{Into: "/etc/nftables.conf"},
|
||||
Resources: []map[string]any{{"id": "load", "type": "service", "unit": "mesh-filter.service",
|
||||
"state": "running", "restart-on": []any{"filtering"}}}},
|
||||
}}
|
||||
}
|
||||
|
||||
func anchorRendering(adopted bool) catalogue.Rendering {
|
||||
return catalogue.Rendering{
|
||||
Generators: map[string]catalogue.Generator{"overlay": hub{}},
|
||||
Ports: map[string]map[int]int{"distribution": {5000: 5000}, "hello-web": {8080: 20001}},
|
||||
Settings: catalogue.SettingsBy{"distribution": {{From: "node anchor",
|
||||
Placement: map[string]any{catalogue.ExposeSetting: map[string]any{"5000": catalogue.FromEverywhere}}}}},
|
||||
Mesh: []string{"10.42.0.1"},
|
||||
Foundation: []int{5671},
|
||||
// What the machine reported faces outside, which every rule in the filter is written
|
||||
// around (novox/hq ADR 0140).
|
||||
OutwardLinks: []string{"eth0"},
|
||||
TunnelInterface: "mesh0",
|
||||
Adopted: adopted,
|
||||
// Genesis takes the foundation's modules.
|
||||
Taken: map[string]bool{"postgres": true, "lavinmq": true},
|
||||
}
|
||||
}
|
||||
|
||||
func byID(resources []map[string]any) map[string]map[string]any {
|
||||
out := map[string]map[string]any{}
|
||||
for _, r := range resources {
|
||||
out[r["id"].(string)] = r
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const mainText = `# tidy\nroot = ${dir:conf}\nquoted = \"yes\" é\n`
|
||||
|
||||
const extraText = `one\ntwo\n`
|
||||
|
||||
Reference in New Issue
Block a user