The case-insensitive rule for the mesh's namespace words took ${PORT:-8080},
${SHELL:-/bin/sh}, ${SECRET:?unset} and ${dir:-/tmp} for misspellings, though they
are among the commonest lines of a script or env file. A :-, :=, :+ or :? after the
colon is the shell's, whatever the word's case.
160 lines
8.9 KiB
Go
160 lines
8.9 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// novox/hq issue 231 — a misspelled placeholder is written out as text.
|
|
//
|
|
// The four placeholders of the issue, in one file: two misspelled namespaces, a setting key no
|
|
// definition could declare, and the shell's own syntax. The first three are refused by name, at the
|
|
// catalogue check and at composition; the fourth reaches the file as written.
|
|
const (
|
|
misspelledShell = "${shel:zsh:first}"
|
|
undeclaredSetting = "${setting:Undeclared}"
|
|
misspelledMachine = "${machnie:address}"
|
|
shellsOwn = "${XDG_CACHE_HOME:-x}"
|
|
// The shell's operators after a word the mesh also uses, in any case: the shell's, and passed.
|
|
shellsOperators = "${PORT:-8080} ${SHELL:-/bin/sh} ${SECRET:?unset} ${dir:-/tmp}"
|
|
)
|
|
|
|
// The catalogue check — the strict parse registration runs too — refuses each by name, with the
|
|
// module and the field it stands in, and says nothing about the shell's own syntax.
|
|
func TestAMisspelledPlaceholderIsRefusedAtTheCheck(t *testing.T) {
|
|
raw := `{"module":"speller","version":"1","resources":[{"id":"rc","type":"file","path":"/etc/speller.rc",` +
|
|
`"content":"a=` + misspelledShell + `\nb=` + undeclaredSetting + `\nc=` + misspelledMachine +
|
|
`\nd=` + shellsOwn + `\n"}]}`
|
|
_, err := ParseManifest([]byte(raw))
|
|
if err == nil {
|
|
t.Fatal("a file holding three placeholders no pass consumes was accepted")
|
|
}
|
|
for _, token := range []string{misspelledShell, undeclaredSetting, misspelledMachine} {
|
|
if !strings.Contains(err.Error(), "speller's resource rc holds "+token+" in its content") {
|
|
t.Errorf("the refusal does not name %s with its module and field: %v", token, err)
|
|
}
|
|
}
|
|
if strings.Contains(err.Error(), "XDG_CACHE_HOME") {
|
|
t.Errorf("the shell's own syntax was refused: %v", err)
|
|
}
|
|
|
|
// A namespace the mesh knows, misspelt in any way its own pattern does not take, and the same
|
|
// namespace in a field its pass does not read: refused at the check as at composition.
|
|
for _, c := range []struct{ resource, token, field string }{
|
|
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${Machine:address}"}`, "${Machine:address}", "content"},
|
|
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${machine:.address}"}`, "${machine:.address}", "content"},
|
|
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${machine: address}"}`, "${machine: address}", "content"},
|
|
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${Dir:x}"}`, "${Dir:x}", "content"},
|
|
{`{"id":"rc","type":"file","path":"/etc/rc","content":"a=${machine:address\nb=1"}`, "${machine:address", "content"},
|
|
{`{"id":"rc","type":"process","name":"speller","env":{"NAME":"${machine:name}"}}`, "${machine:name}", "env.NAME"},
|
|
} {
|
|
raw := `{"module":"speller","version":"1","resources":[` + c.resource + `]}`
|
|
_, err := ParseManifest([]byte(raw))
|
|
if err == nil || !strings.Contains(err.Error(), "speller's resource rc holds "+c.token+" in its "+c.field) {
|
|
t.Errorf("%s in its %s was accepted at the check, or not named: %v", c.token, c.field, err)
|
|
}
|
|
}
|
|
|
|
// And the shell's syntax alone, beside placeholders every pass knows, is accepted — as is the
|
|
// shape a lower-case shell variable takes with an operator after its colon.
|
|
raw = `{"module":"speller","version":"1","resources":[{"id":"rc","type":"file","path":"${machine:account-home}/.rc",` +
|
|
`"content":"` + shellsOwn + ` ${(%):-%n} ${1:-.} ${count:-} ${trial:+ on trial} ${machine:address} ` +
|
|
shellsOperators + `\n"},` +
|
|
`{"id":"server","type":"container","name":"server","env":{"PORT":"${PORT:-80}"}}]}`
|
|
if _, err := ParseManifest([]byte(raw)); err != nil {
|
|
t.Fatalf("the shell's own syntax was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// Composition refuses the same placeholders in the same words — a manifest the store already holds
|
|
// was never parsed by this binary — and a namespace the mesh knows, written in a field its pass does
|
|
// not read, is refused there too, because it would reach the machine as the same literal text.
|
|
func TestAMisspelledPlaceholderIsRefusedAtComposition(t *testing.T) {
|
|
for _, c := range []struct {
|
|
field string
|
|
r map[string]any
|
|
token string
|
|
}{
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "a=" + misspelledShell + "\n"}, misspelledShell},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "b=" + undeclaredSetting + "\n"}, undeclaredSetting},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "c=" + misspelledMachine + "\n"}, misspelledMachine},
|
|
{"env.NAME", map[string]any{"id": "rc", "type": "process", "name": "speller", "env": map[string]any{"NAME": "${machine:name}"}}, "${machine:name}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${Machine:address}"}, "${Machine:address}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${machine:.address}"}, "${machine:.address}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${machine: address}"}, "${machine: address}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${Dir:x}"}, "${Dir:x}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "a=${machine:address\nb=1"}, "${machine:address"},
|
|
} {
|
|
m := Manifest{Module: "speller", Version: "1", Resources: []map[string]any{c.r}}
|
|
r := Resolution{Node: "workstation", Account: "op", Modules: []Manifest{m}}
|
|
_, err := r.Declaration(Rendering{})
|
|
if err == nil || !strings.Contains(err.Error(), "speller's resource rc holds "+c.token+" in its "+c.field) {
|
|
t.Errorf("%s in its %s was composed rather than refused by name: %v", c.token, c.field, err)
|
|
}
|
|
}
|
|
|
|
m := Manifest{Module: "speller", Version: "1", Resources: []map[string]any{
|
|
{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "d=" + shellsOwn + " " + shellsOperators + "\n"},
|
|
{"id": "server", "type": "process", "name": "server", "env": map[string]any{"PORT": "${PORT:-80}"}},
|
|
}}
|
|
out, err := Resolution{Node: "workstation", Account: "op", Modules: []Manifest{m}}.Declaration(Rendering{})
|
|
if err != nil {
|
|
t.Fatalf("the shell's own syntax was refused: %v", err)
|
|
}
|
|
if got := contentOf(t, out, "speller.rc"); got != "d="+shellsOwn+" "+shellsOperators+"\n" {
|
|
t.Fatalf("the shell's own syntax did not pass through as written: %q", got)
|
|
}
|
|
}
|
|
|
|
// contentOf is the content of the composed resource with this id, failing when it was not composed.
|
|
func contentOf(t *testing.T, out []map[string]any, id string) string {
|
|
t.Helper()
|
|
for _, res := range out {
|
|
if res["id"] == id {
|
|
return plainly(res["content"])
|
|
}
|
|
}
|
|
t.Fatalf("%s was not composed: %v", id, out)
|
|
return ""
|
|
}
|
|
|
|
// What a value puts into a file is its software's text, not the definition's, and is never swept
|
|
// (review of mesh-controller #211): an operator's setting holding `${labels:instance}` filled through
|
|
// ${setting:…}, and a JSON setting `${level:upper}` merged into a mergeable file, reach the machine as
|
|
// set — software that templates its own configuration (Log4j's `${env:…}`, Spring's `${timeout:30}`)
|
|
// is configured exactly this way.
|
|
func TestAValueHoldingAPlaceholderShapeComposesUnchanged(t *testing.T) {
|
|
m := Manifest{Module: "templater", Version: "1", Resources: []map[string]any{
|
|
{"id": "conf", "type": "file", "path": "/etc/templater.conf", "content": "template=${setting:template}\n"},
|
|
{"id": "json", "type": "file", "path": "/etc/templater.json", "merge": MergeJSON, "content": `{"fmt":"plain"}`},
|
|
}}
|
|
settings := SettingsBy{"templater": {{From: "the operator", Values: map[string]any{
|
|
"template": "${labels:instance}", "fmt": "${level:upper}",
|
|
}}}}
|
|
out, err := Resolution{Node: "workstation", Account: "op", Modules: []Manifest{m}}.Declaration(Rendering{Settings: settings})
|
|
if err != nil {
|
|
t.Fatalf("a value holding a placeholder's shape was refused as the definition's: %v", err)
|
|
}
|
|
if got := contentOf(t, out, "templater.conf"); got != "template=${labels:instance}\n" {
|
|
t.Errorf("the setting did not reach the file as set: %q", got)
|
|
}
|
|
if got := contentOf(t, out, "templater.json"); !strings.Contains(got, `${level:upper}`) {
|
|
t.Errorf("the merged setting did not reach the file as set: %q", got)
|
|
}
|
|
}
|
|
|
|
// Contributed shell code is the shell's, and no pass reads it (novox/hq ADR 0204): zsh's own
|
|
// `${path:t}` is namespace-shaped, and reaches the holder's file untouched.
|
|
func TestContributedShellCodeIsNotSwept(t *testing.T) {
|
|
modules := []Manifest{zshHolder(), {Module: "modifier", Shell: []ShellCode{
|
|
{For: "zsh", Slot: "normal", Code: "echo ${path:t} ${shel:zsh:first}"},
|
|
}}}
|
|
out, err := Resolution{Node: "workstation", Account: "op", Modules: modules}.Declaration(Rendering{})
|
|
if err != nil {
|
|
t.Fatalf("contributed shell code was swept: %v", err)
|
|
}
|
|
if got := contentOf(t, out, "zsh.zshrc"); !strings.Contains(got, "echo ${path:t} ${shel:zsh:first}") {
|
|
t.Fatalf("the contributed code did not reach the holder's file as written: %q", got)
|
|
}
|
|
}
|