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
246 lines
9.5 KiB
Go
246 lines
9.5 KiB
Go
package resolution
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
func fileNamed(out []map[string]any, id string) map[string]any {
|
|
for _, r := range out {
|
|
if r["id"] == id {
|
|
return r
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A module's own credential, put into a file it wrote: the value sealed to the node, or, in a composition asked ahead of
|
|
// the send or for a plan alone, the stand-in its own file has (issue 774: baserow's config.json refused ace's plans).
|
|
func TestAFileGetsTheSecretItsContentAsksFor(t *testing.T) {
|
|
for _, row := range []struct {
|
|
name string
|
|
with mcat.Rendering
|
|
want string
|
|
}{
|
|
{"sealed", mcat.Rendering{Needed: map[string]map[string]string{"gitea": {"admin": "sealed-admin"}}}, "sealed-admin"},
|
|
{"foreseen", mcat.Rendering{Foreseen: map[string]map[string]bool{"gitea": {"admin": true}}}, mcat.ForeseenSealed},
|
|
} {
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{{
|
|
Module: "gitea",
|
|
OwnSecrets: mcat.OwnSecrets{"admin": {Path: "/var/lib/gitea/admin.env"}},
|
|
Resources: []map[string]any{{
|
|
"id": "conf", "type": "file", "path": "/etc/gitea/app.ini",
|
|
"content": "[security]\nSECRET_KEY = ${secret:admin}\n",
|
|
}},
|
|
}}}
|
|
out, err := Declaration(r, row.with)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", row.name, err)
|
|
}
|
|
file := fileNamed(out, "gitea.conf")
|
|
if file == nil {
|
|
t.Fatalf("%s: no file in %v", row.name, out)
|
|
}
|
|
got, _ := file["secrets"].(map[string]any)
|
|
if got["admin"] != row.want {
|
|
t.Errorf("%s: the file was sent without its secret: %v", row.name, file)
|
|
}
|
|
// The hole is still a hole on the wire. The mesh does not fill it in; the host does, on the
|
|
// machine, which is the only place both halves exist.
|
|
if !strings.Contains(file["content"].(string), "${secret:admin}") {
|
|
t.Errorf("%s: the mesh substituted it itself: %v", row.name, file["content"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// The gap this exists for: a granted credential arrives as a bare password, and the program that
|
|
// needs it reads KEY=value.
|
|
func TestAGrantedCredentialCanBeShapedIntoAnEnvFile(t *testing.T) {
|
|
r := mcat.Resolution{
|
|
Node: "anchor",
|
|
Modules: []mcat.Manifest{{
|
|
Module: "keycloak",
|
|
Requires: []string{"postgres-database"},
|
|
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"},
|
|
Resources: []map[string]any{{
|
|
"id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env",
|
|
"content": "KC_DB_USERNAME=keycloak\nKC_DB_PASSWORD=${secret:postgres-database}\n",
|
|
"mode": "0600",
|
|
}},
|
|
}},
|
|
Needs: []mcat.Needed{{Name: "postgres-database", For: "keycloak", From: "anchor", Sealed: "sealed-db"}},
|
|
}
|
|
out, err := Declaration(r, mcat.Rendering{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file := fileNamed(out, "keycloak.dbenv")
|
|
got, _ := file["secrets"].(map[string]any)
|
|
if got["postgres-database"] != "sealed-db" {
|
|
t.Errorf("the credential never reached the file it was needed in: %v", file)
|
|
}
|
|
}
|
|
|
|
// Two modules on one node requiring the same provision each get THEIR OWN credential in a
|
|
// ${secret:…} file — not whichever need happened to come last. This is the placeholder-path twin of
|
|
// the guard novox/hq 04-ISSUES/022 put on the secrets:-map path; without it, a node with baserow and
|
|
// letta both consuming postgres gave baserow letta's password and authentication failed.
|
|
func TestTwoConsumersOfOneProvisionEachGetTheirOwnInAPlaceholderFile(t *testing.T) {
|
|
r := mcat.Resolution{
|
|
Node: "anchor",
|
|
Modules: []mcat.Manifest{
|
|
{
|
|
Module: "keycloak",
|
|
Requires: []string{"postgres-database"},
|
|
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"},
|
|
Resources: []map[string]any{{
|
|
"id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env",
|
|
"content": "PW=${secret:postgres-database}\n", "mode": "0600",
|
|
}},
|
|
},
|
|
{
|
|
Module: "grafana",
|
|
Requires: []string{"postgres-database"},
|
|
Secrets: map[string]string{"postgres-database": "/var/lib/grafana/db.secret"},
|
|
Resources: []map[string]any{{
|
|
"id": "dbenv", "type": "file", "path": "/var/lib/grafana/database.env",
|
|
"content": "PW=${secret:postgres-database}\n", "mode": "0600",
|
|
}},
|
|
},
|
|
},
|
|
Needs: []mcat.Needed{
|
|
{Name: "postgres-database", For: "keycloak", From: "anchor", Sealed: "sealed-kc"},
|
|
{Name: "postgres-database", For: "grafana", From: "anchor", Sealed: "sealed-gf"},
|
|
},
|
|
}
|
|
out, err := Declaration(r, mcat.Rendering{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kc, _ := fileNamed(out, "keycloak.dbenv")["secrets"].(map[string]any)
|
|
if kc["postgres-database"] != "sealed-kc" {
|
|
t.Errorf("keycloak got the wrong credential (someone else's password): %v", kc)
|
|
}
|
|
gf, _ := fileNamed(out, "grafana.dbenv")["secrets"].(map[string]any)
|
|
if gf["postgres-database"] != "sealed-gf" {
|
|
t.Errorf("grafana got the wrong credential (someone else's password): %v", gf)
|
|
}
|
|
}
|
|
|
|
// A name nothing declared is a typo, and it is refused here rather than on the machine.
|
|
func TestAFileNamingASecretTheModuleDoesNotHaveIsRefused(t *testing.T) {
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{{
|
|
Module: "gitea",
|
|
OwnSecrets: mcat.OwnSecrets{"admin": {Path: "/var/lib/gitea/admin.env"}},
|
|
Resources: []map[string]any{{
|
|
"id": "conf", "type": "file", "path": "/etc/gitea/app.ini",
|
|
"content": "SECRET_KEY = ${secret:adnim}\n",
|
|
}},
|
|
}}}
|
|
_, err := Declaration(r, mcat.Rendering{
|
|
Needed: map[string]map[string]string{"gitea": {"admin": "sealed-admin"}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("a file asked for a secret that does not exist and the mesh sent it anyway")
|
|
}
|
|
// It says what could have been meant, because the answer is nearly always one of two names.
|
|
if !strings.Contains(err.Error(), `"admin"`) {
|
|
t.Errorf("the refusal does not say what it could have named: %v", err)
|
|
}
|
|
}
|
|
|
|
// One module may not read another's credential by guessing its name.
|
|
func TestAFileCannotNameAnotherModulesSecret(t *testing.T) {
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{
|
|
{Module: "postgres", OwnSecrets: mcat.OwnSecrets{"superuser": {Path: "/var/lib/postgres/su.env"}}},
|
|
{Module: "gitea", Resources: []map[string]any{{
|
|
"id": "conf", "type": "file", "path": "/etc/gitea/app.ini",
|
|
"content": "PASSWORD=${secret:superuser}\n",
|
|
}}},
|
|
}}
|
|
_, err := Declaration(r, mcat.Rendering{Needed: map[string]map[string]string{
|
|
"postgres": {"superuser": "sealed-su"},
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("gitea read postgres's credential by naming it")
|
|
}
|
|
}
|
|
|
|
// A token inside a JSON document, where a setting put the placeholder there.
|
|
//
|
|
// **The case this whole shape was argued from**: a desktop client that reads its token from an
|
|
// attribute inside a JSON file in a home directory, not from an environment variable. The module
|
|
// ships an empty document, a setting says which attribute, and the credential never passes
|
|
// through the mesh in the open.
|
|
func TestASettingThatCarriesAPlaceholderIsStillFilled(t *testing.T) {
|
|
r := mcat.Resolution{Node: "workstation", Modules: []mcat.Manifest{{
|
|
Module: "chat",
|
|
OwnSecrets: mcat.OwnSecrets{"api-token": {Path: "/home/operator/.config/chat/token"}},
|
|
Resources: []map[string]any{{
|
|
"id": "settings", "type": "file", "merge": "json",
|
|
"path": "/home/operator/.config/chat/settings.json", "content": "{}",
|
|
}},
|
|
}}}
|
|
out, err := Declaration(r, mcat.Rendering{
|
|
Needed: map[string]map[string]string{"chat": {"api-token": "sealed-token"}},
|
|
Settings: mcat.SettingsBy{"chat": {{From: "node", Values: map[string]any{
|
|
"chat.atlassian.token": "${secret:api-token}",
|
|
}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file := fileNamed(out, "chat.settings")
|
|
got, _ := file["secrets"].(map[string]any)
|
|
if got["api-token"] != "sealed-token" {
|
|
t.Errorf("a placeholder a setting put there was never filled: %v", file)
|
|
}
|
|
// Filling secrets runs after settings for exactly this reason: before the merge, the content
|
|
// is "{}" and asks for nothing at all.
|
|
if !strings.Contains(file["content"].(string), "${secret:api-token}") {
|
|
t.Errorf("the hole did not survive to the machine: %v", file["content"])
|
|
}
|
|
}
|
|
|
|
// An own secret and a requirement of the same name would make ${secret:x} mean either.
|
|
func TestANameMeaningTwoThingsIsRefused(t *testing.T) {
|
|
r := mcat.Resolution{
|
|
Node: "anchor",
|
|
Modules: []mcat.Manifest{{
|
|
Module: "thing",
|
|
OwnSecrets: mcat.OwnSecrets{"store": {Path: "/var/lib/thing/own.env"}},
|
|
Secrets: map[string]string{"store": "/var/lib/thing/granted.env"},
|
|
}},
|
|
Needs: []mcat.Needed{{Name: "store", From: "anchor", Sealed: "sealed-granted"}},
|
|
}
|
|
_, err := Declaration(r, mcat.Rendering{
|
|
Needed: map[string]map[string]string{"thing": {"store": "sealed-own"}},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "rename") {
|
|
t.Errorf("one name meant two credentials and nothing said so: %v", err)
|
|
}
|
|
}
|
|
|
|
// A file with no placeholder is not given a secrets map. The host refuses a secret its content
|
|
// never asks for, so an empty map added helpfully would break every file that has none.
|
|
func TestAFileWithNoPlaceholderIsLeftAlone(t *testing.T) {
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{{
|
|
Module: "gitea",
|
|
OwnSecrets: mcat.OwnSecrets{"admin": {Path: "/var/lib/gitea/admin.env"}},
|
|
Resources: []map[string]any{{
|
|
"id": "conf", "type": "file", "path": "/etc/gitea/app.ini", "content": "RUN_MODE=prod\n",
|
|
}},
|
|
}}}
|
|
out, err := Declaration(r, mcat.Rendering{
|
|
Needed: map[string]map[string]string{"gitea": {"admin": "sealed-admin"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, given := fileNamed(out, "gitea.conf")["secrets"]; given {
|
|
t.Error("a file that asks for nothing was given a secrets map, which the host refuses")
|
|
}
|
|
}
|