The vault judged a module handed only when its assignment already carried a handing plan, and the node-engine followed that word: a module with no plan was composed the old way, refused for a key only the provider's helper answers (forge-account's ssh_alias), and so never got its plan. resolution.Handing is now the manifest, its build and the machine alone; the vault's word is read only to say where the two disagree. A refusal of a file or an own secret the provisioner's client should fill now says "handed module not handed" and why, never only the keys a binding lacks or a secret to make. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
91 lines
4.2 KiB
Go
91 lines
4.2 KiB
Go
package resolution
|
|
|
|
import (
|
|
"errors"
|
|
"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 1966 (ace, 2026-11-04): the vault judged it not handed, its assignment carrying no plan yet; the manifest,
|
|
// its build and the machine hand it all the same, so the plan that would hand it is made.
|
|
withoutPlan := handingTo("store:5000")
|
|
withoutPlan.Assigned = map[string]bool{"bot": false}
|
|
first, err := Compose(r, withoutPlan)
|
|
if err != nil {
|
|
t.Fatalf("a module the vault judged not handed, with no plan yet, is refused: %v", err)
|
|
}
|
|
if want := []mcat.PlanSecret{{Name: "token", Path: path}}; !reflect.DeepEqual(first.Plans["bot"].Secrets, want) {
|
|
t.Errorf("with no plan yet, the plan names secrets %+v, want %+v", first.Plans["bot"].Secrets, want)
|
|
}
|
|
// Issue 774 (postgres on novox, 2026-10-31), as issue 1966 leaves it: where the vault judged it handed and the machine
|
|
// cannot take it, the vault answered none of its secrets, and the refusal says a handed module is not handed, never
|
|
// a secret to make.
|
|
_, err = Compose(r, mcat.Rendering{ArtifactStore: "store:5000", Assigned: map[string]bool{"bot": true}})
|
|
var notMade *NotMadeError
|
|
if err == nil || errors.As(err, ¬Made) || !strings.Contains(err.Error(), "handed module not handed: bot needs its own secret \"token\"") ||
|
|
!strings.Contains(err.Error(), "has not stated that it writes a filled file") {
|
|
t.Errorf("judged handed by the vault on a machine that cannot take it, the composition says %v", err)
|
|
}
|
|
// 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"])
|
|
}
|
|
}
|
|
}
|