The first sweep ran at composition over the resource after settings, bindings and
machine facts were filled, so an operator's value such as ${labels:instance} was
refused as a misspelling its author never wrote, and the whole machine got nothing.
Both points now sweep the resource as the manifest wrote it, against one table of
which fields each pass fills, so the check and composition refuse the same things.
Any ${<known namespace>: its pattern does not take is refused whatever its case or key.
Issue 231's undeclared-setting half is not met here: refusing a key the module does
not declare (ADR 0164) is not built and is handed to issue 398.
153 lines
8.3 KiB
Go
153 lines
8.3 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 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":"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}\n"}]}`
|
|
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": "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 + "\n",
|
|
}}}
|
|
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+"\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)
|
|
}
|
|
}
|