Files
mesh-host/internal/compose/resolution/handed_bound_test.go
T
jschoubben d68e241724 Take the catalogue's composition rules into the node-engine
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
2026-11-02 19:15:55 +01:00

147 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package resolution
import (
"reflect"
"strings"
"testing"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
// gitea's server.env, as the catalogue keeps it: its database as the provider told it and the consumer's own login.
const giteaServerEnv = "GITEA__security__INTERNAL_TOKEN=${secret:internal-token}\n" +
"GITEA__database__DB_TYPE=postgres\n" +
"GITEA__database__HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\n" +
"GITEA__database__NAME=${bound:postgres-database:as}\n" +
"GITEA__database__USER=${bound:postgres-database:as}\n" +
"GITEA__database__PASSWD=${secret:postgres-database}\n"
// databaseConsumer is a handed module, with the template gitea's server.env is, requiring a database from a provider
// that has moved or not (issue 788); slug is the module's identity source when it declares one.
func databaseConsumer(t *testing.T, slug string, moved bool) mcat.Resolution {
t.Helper()
raw := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`,
`"requires":["postgres-database"],"own-secrets":{"internal-token":"${dir:state}/internal-token"}`, 1)
if slug != "" {
raw = strings.Replace(raw, `"version":"1",`, `"version":"1","slug":"`+slug+`",`, 1)
}
return withDatabase(t, raw, giteaServerEnv, moved)
}
// withDatabase resolves the module in raw, whose template is text, against a provider of postgres-database that has
// moved or not.
func withDatabase(t *testing.T, raw, text string, moved bool) mcat.Resolution {
t.Helper()
r, err := Resolve(shelf(held(t, raw, builtBot(text)), mcat.Manifest{Module: "postgres", Version: "1", Provides: []mcat.Offer{{Name: "postgres-database", Scope: mcat.ScopeNode}}}),
[]string{"bot"}, workstation(), mcat.World{})
if err != nil {
t.Fatal(err)
}
r.Account = "op"
// Answered on this machine, it states no need; one from the provider's machine is stated, as the plan does.
r.Needs = []mcat.Needed{{Name: "postgres-database", For: "bot", Module: "postgres", From: "novox", At: "10.7.0.1",
CredentialForm: mcat.CredentialFormPair, IssuedToHolder: moved, ProviderMoved: moved}}
return r
}
// The forge's server.env composes for a handed module when what it requires has moved, its plan entry carrying the
// provider's address, machine and the consumer's identity as today's binding file gives them; and is refused with the
// text it has always had when the provider has not.
func TestAHandedModuleMayNameWhatItRequiresWhereItsProviderHasMoved(t *testing.T) {
c, got := composeWith(t, databaseConsumer(t, "", true), handingTo("store:5000"))
if strings.Contains(got, "sealed") {
t.Fatalf("a handed module's declaration carries a sealed value: %s", got)
}
want := []mcat.PlanProvision{{Name: "postgres-database", Provision: "postgres-database", Provider: "novox",
ProviderModule: "postgres", CredentialForm: mcat.CredentialFormPair, At: "10.7.0.1", From: "novox",
As: mcat.ConsumerIdentity("workstation", "bot")}}
if !reflect.DeepEqual(c.Plans["bot"].Provisions, want) {
t.Fatalf("the plan names %+v, want %+v", c.Plans["bot"].Provisions, want)
}
for _, unmoved := range []mcat.Resolution{databaseConsumer(t, "", false)} {
_, err := Compose(unmoved, handingTo("store:5000"))
if err == nil || !strings.Contains(err.Error(), "which is no secret of its own; a credential it requires moves "+
"with its provider (design 57 B1–B3)") {
t.Fatalf("a provider not moved, the credential: %v", err)
}
bound := strings.Replace(giteaServerEnv, "${secret:postgres-database}", "x", 1)
r := withDatabase(t, strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`,
`"requires":["postgres-database"],"own-secrets":{"internal-token":"${dir:state}/internal-token"}`, 1),
bound, false)
_, err = Compose(r, handingTo("store:5000"))
if err == nil || !strings.Contains(err.Error(), "the provisioner's client fills only config, secrets, "+
"collections and placement until that domain moves (design 57)") {
t.Fatalf("a provider not moved, the binding: %v", err)
}
}
}
// A handed module's credentials the vault keeps as one login per local name (issue 793): gitea's internal token and
// admin password, which the `secret` provision hands as `${secret:<local>}`. Each is a plan entry naming its local,
// its file declared placed and without a value; the template naming the local is allowed.
func TestAHandedModulesLocalCredentialsOfTheVaultAreNamedByLocalInItsPlan(t *testing.T) {
raw := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`,
`"requires":["secret"],"secrets":{"secret":{"internal-token":"${dir:state}/internal-token.secret",`+
`"admin":"${dir:state}/admin.secret"}},"own-secrets":{"token":"${dir:state}/token"}`, 1)
r, err := Resolve(shelf(held(t, raw, builtBot("T=${secret:internal-token}\n")), mcat.Manifest{Module: "vault", Version: "1", Provides: []mcat.Offer{{Name: "secret", Scope: mcat.ScopeNode}}}),
[]string{"bot"}, workstation(), mcat.World{})
if err != nil {
t.Fatal(err)
}
r.Account = "op"
r.Needs = []mcat.Needed{
{Name: "secret", Local: "internal-token", For: "bot", Module: "vault", From: "novox",
CredentialForm: mcat.CredentialFormPair, IssuedToHolder: true, ProviderMoved: true},
{Name: "secret", Local: "admin", For: "bot", Module: "vault", From: "novox",
CredentialForm: mcat.CredentialFormPair, IssuedToHolder: true, ProviderMoved: true},
}
c, got := composeWith(t, r, handingTo("store:5000"))
if strings.Contains(got, "sealed") {
t.Fatalf("the declaration carries a sealed value: %s", got)
}
var names []string
for _, p := range c.Plans["bot"].Provisions {
if p.Provision != "secret" || p.Provider != "novox" || p.Path == "" || p.Name != p.Local {
t.Errorf("the plan entry is %+v", p)
}
names = append(names, p.Name)
}
if !reflect.DeepEqual(names, []string{"admin", "internal-token"}) {
t.Fatalf("the plan names %v, want admin and internal-token", names)
}
}
// **Issue 969: a key its provider serves only at run time.** A moved module's file may name `${bound:…}` of a
// provision whose provider tells it at run time (ssh-client's known_host of the forge). Where the machine takes
// handed files the client fills it and the module composes; where it does not the controller must fill it from what
// the provider serves statically, cannot, and refuses it with the very words an unmarked module is refused with
// (ADR 0349 §17: the old way, byte for byte).
func TestAHandedModuleNamingARunTimeBoundKeyComposesOnlyWhereItIsHanded(t *testing.T) {
text := "Host ${bound:postgres-database:at}\n KnownHost ${bound:postgres-database:known_host}\n"
moved := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`,
`"requires":["postgres-database"],"own-secrets":{"token":"${dir:state}/token"}`, 1)
unmoved := strings.Replace(moved, `"moved":true,`, ``, 1)
if unmoved == moved {
t.Fatal("the twin is still marked moved")
}
c, got := composeWith(t, withDatabase(t, moved, text, true), handingTo("store:5000"))
if strings.Contains(got, "known_host") || len(c.Filled) == 0 {
t.Fatalf("handed, the module is filled by the client and the controller names nothing: %v %s", c.Filled, got)
}
for name, with := range map[string]mcat.Rendering{
"an engine that does not take a filled file alone": {Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"},
"no provisioner client on the machine": {Hands: true, ArtifactStore: "store:5000"},
} {
_, err := Compose(withDatabase(t, moved, text, true), sealedForBot(with))
if err == nil || !strings.Contains(err.Error(), `asks its postgres-database binding for "known_host"`) {
t.Fatalf("%s: the key is not refused: %v", name, err)
}
_, before := Compose(withDatabase(t, unmoved, text, false), sealedForBot(with))
if before == nil || before.Error() != err.Error() {
t.Fatalf("%s: refused otherwise than the unmarked module: %v / %v", name, err, before)
}
}
}