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
79 lines
3.4 KiB
Go
79 lines
3.4 KiB
Go
package resolution
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
const handedBot = `{"module":"bot","version":"1",
|
|
"own-secrets":{"token":"${dir:state}/token"},
|
|
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
|
|
"resources":[
|
|
{"id":"state","type":"directory","mode":"0700","place":"."},
|
|
{"id":"conf","type":"file","path":"/etc/bot/bot.conf","mode":"0640","template":"files/bot.conf"},
|
|
{"id":"daemon","type":"process","name":"bot","run":["bot"],"env":{"BOT_TOKEN_FILE":"${dir:state}/token"}}]}`
|
|
|
|
const botText = "token = ${secret:token}\nhome = ${machine:account-home}\n"
|
|
|
|
func botMoved() string {
|
|
return strings.Replace(handedBot, `"version":"1",`, `"version":"1","moved":true,`, 1)
|
|
}
|
|
|
|
func builtBot(text string) []mcat.Built {
|
|
return []mcat.Built{{Name: "files", Kind: mcat.ArtifactArchive,
|
|
Reference: mcat.ArtifactStoreScheme + "bot/files/blobs/sha256:" + strings.Repeat("e", 64),
|
|
Digest: "sha256:" + strings.Repeat("e", 64), Texts: map[string]string{"bot.conf": text}}}
|
|
}
|
|
|
|
// What the controller seals for the bot when it fills it the old way: a stand-in sealed value.
|
|
func sealedForBot(with mcat.Rendering) mcat.Rendering {
|
|
with.Needed = map[string]map[string]string{"bot": {"token": "sealed-to-the-node"}}
|
|
return with
|
|
}
|
|
|
|
func TestAHandedModulesSecretsAreNamedAndNoValueIsSealed(t *testing.T) {
|
|
r := notifierOn(t, held(t, botMoved(), builtBot(botText)))
|
|
c, got := composeWith(t, r, handingTo("store:5000"))
|
|
if strings.Contains(got, "sealed") || strings.Contains(got, "${secret:") {
|
|
t.Fatalf("a handed module's declaration carries a sealed value or a secret placeholder: %s", got)
|
|
}
|
|
var token map[string]any
|
|
for _, res := range c.Resources {
|
|
if res["id"] == "bot.needs-token" {
|
|
token = res
|
|
}
|
|
}
|
|
if token == nil {
|
|
t.Fatalf("the token's file is not declared: %s", got)
|
|
}
|
|
if token["path"] == nil || strings.Contains(token["path"].(string), "${") || token["mode"] != "0600" ||
|
|
token["owner-at-runtime"] != "owned" || token[mcat.HandedSecretField] != nil {
|
|
t.Fatalf("the token's file is declared %v: placed, 0600, owned at runtime, no value", token)
|
|
}
|
|
path := token["path"].(string)
|
|
if !reflect.DeepEqual(c.Filled, []string{path, "/etc/bot/bot.conf"}) {
|
|
t.Fatalf("the filled files are %v", c.Filled)
|
|
}
|
|
if want := []mcat.PlanSecret{{Name: "token", Path: path}}; !reflect.DeepEqual(c.Plans["bot"].Secrets, want) {
|
|
t.Fatalf("the plan names secrets %+v, want %+v", c.Plans["bot"].Secrets, want)
|
|
}
|
|
// Issue 774 (postgres on novox, 2026-10-31): where the vault judged it handed, its secrets are the vault's even on a
|
|
// machine the node reads as not taking a filled file, and nothing is refused for a value the vault never answers.
|
|
judged, err := Compose(r, mcat.Rendering{ArtifactStore: "store:5000", Assigned: map[string]bool{"bot": true}})
|
|
if err != nil {
|
|
t.Fatalf("a module the vault judged handed is refused: %v", err)
|
|
}
|
|
if want := []mcat.PlanSecret{{Name: "token", Path: path}}; !reflect.DeepEqual(judged.Plans["bot"].Secrets, want) {
|
|
t.Errorf("judged handed by the vault, the plan names secrets %+v, want %+v", judged.Plans["bot"].Secrets, want)
|
|
}
|
|
// A process naming the token's file still restarts when its client writes it.
|
|
for _, res := range c.Resources {
|
|
if res["id"] == "bot.daemon" && !strings.Contains(strings.Join(stringsIn(res["restart-on"]), " "), "bot.needs-token") {
|
|
t.Errorf("the daemon does not restart on its token's file: %v", res["restart-on"])
|
|
}
|
|
}
|
|
}
|