Files
mesh-host/internal/compose/resolution/handed_bound_test.go
T

169 lines
9.2 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, whatever the vault judged of a plan not made yet (issue
// 1966); where it does not the node-engine must fill it from what the provider serves statically, cannot, and refuses
// it as a handed module not handed, saying why the machine does not take it (issue 1966), where an unmarked module is
// refused with the keys the binding says and that it is not moved.
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)
}
// Issue 1966: the vault judged it not handed (no plan yet); it is handed all the same.
noPlan := handingTo("store:5000")
noPlan.Assigned = map[string]bool{"bot": false}
if c, err := Compose(withDatabase(t, moved, text, true), noPlan); err != nil || len(c.Filled) == 0 {
t.Fatalf("with no plan yet, the module is not handed: %v", err)
}
for name, with := range map[string]struct {
with mcat.Rendering
why string
}{
"an engine that does not take a filled file alone": {mcat.Rendering{Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"},
"has not stated that it writes a filled file"},
"no provisioner client on the machine": {mcat.Rendering{Hands: true, ArtifactStore: "store:5000"},
"names no account the provisioner's client runs as"},
} {
_, err := Compose(withDatabase(t, moved, text, true), sealedForBot(with.with))
if err == nil || !strings.Contains(err.Error(), `handed module not handed: bot asks its postgres-database binding for "known_host"`) ||
!strings.Contains(err.Error(), with.why) {
t.Fatalf("%s: the key is not refused as a handed module not handed: %v", name, err)
}
_, before := Compose(withDatabase(t, unmoved, text, false), sealedForBot(with.with))
if before == nil || !strings.Contains(before.Error(), `bot asks its postgres-database binding for "known_host", and what answers it says "as", "at" or "from" (bot is not moved`) {
t.Fatalf("%s: the unmarked module is refused %v", name, before)
}
}
// And where nothing keeps it from being handed yet the composition filled the file itself, the reason says it is a fault
// in this code (the review of the vault's judgement, issue 1966), never a reason about the module or the machine.
whole := mcat.Manifest{Module: "bot", Moved: true, Handed: &mcat.Handed{Templates: map[string]string{"f": "templates/f"}}}
if why, handedFile := notHandedWhy(handingTo("store:5000"), whole, map[string]any{"id": "f"}); !handedFile ||
!strings.Contains(why, "a fault in the node-engine's composition") {
t.Errorf("the impossible branch says %q (handed file %v)", why, handedFile)
}
}