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.
169 lines
8.6 KiB
Go
169 lines
8.6 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// A placeholder no pass consumes is refused, never written out as text (novox/hq issue 231).
|
|
//
|
|
// Each namespace is filled by its own pass with its own pattern, in the fields that pass reads. A
|
|
// word in that shape that no pattern matched — `${machnie:address}`, `${shel:zsh:first}`, a setting
|
|
// key no definition could declare such as `${setting:Undeclared}` — was left in the file as it was
|
|
// written and reached a machine as a value: the predecessor's failure the namespaced placeholders
|
|
// were meant to end (novox/hq ADR 0164). So the definition is swept, field by field, against the
|
|
// same table of what each pass fills where.
|
|
//
|
|
// **The definition, never the values.** Swept as the manifest wrote it, at the catalogue check and
|
|
// again at composition before any pass has run: what an operator's setting, a binding or a merged
|
|
// JSON setting puts into a file is its own software's text — `${env:HOME}` for Log4j, `${timeout:30}`
|
|
// for Spring — and refusing it there would refuse something its author never wrote, on a machine the
|
|
// check had passed (novox/hq issue 231, review of mesh-controller #211).
|
|
//
|
|
// What is refused:
|
|
// - a placeholder of a namespace the mesh knows, in a field that namespace's pass does not read: it
|
|
// would reach the machine as the same text;
|
|
// - any other `${<known namespace>:`, case-insensitively and whatever follows — `${Machine:address}`,
|
|
// `${machine:.address}`, `${machine: address}`, an unclosed `${machine:address` — because no
|
|
// pass's pattern takes it;
|
|
// - a namespace-shaped placeholder of a word the mesh does not know: a lower-case word, a colon, and
|
|
// a key that begins with a letter or a digit and holds no space, brace or `$`.
|
|
//
|
|
// **The shell's own syntax is not that shape, and passes.** `${NAME:-…}` has an upper-case word,
|
|
// `${(%):-…}` and `${1:-.}` begin with no letter, and a lower-case variable with an operator after its
|
|
// colon — `${count:-}`, `${trial:+…}`, `${state:=…}` — has no key that begins with a letter or a digit.
|
|
// What the shape does catch is a zsh modifier (`${path:t}`) or a substring (`${where:0:12}`) in a
|
|
// resource's own text: shell code of that kind belongs in the module's contributed shell code, which
|
|
// is not a resource and is never swept (novox/hq ADR 0204).
|
|
|
|
// filler is one namespace's pass: the patterns it fills with, and the fields it reads them in, by
|
|
// resource type ("*" for any type).
|
|
type filler struct {
|
|
namespace string
|
|
patterns []*regexp.Regexp
|
|
fields map[string][]string
|
|
// why, when set, is the rule that keeps the namespace out of every other field, said with a
|
|
// refusal of one written there.
|
|
why string
|
|
}
|
|
|
|
// fillers is the table of what each pass fills where — read from the passes themselves: settingInto,
|
|
// dirInto, accessInto, intoFile (whose ${secret:…} the node-engine fills), boundInto, portInto,
|
|
// seatInto, machineInto and contributionsInto. A pass that comes to read another field adds it here,
|
|
// or the sweep refuses the placeholder it would have filled.
|
|
var fillers = []filler{
|
|
{namespace: "setting", patterns: []*regexp.Regexp{settingRef}, fields: map[string][]string{"file": {"content"}, "service": {"unit"}}},
|
|
{namespace: "dir", patterns: []*regexp.Regexp{dirRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
|
{namespace: "access", patterns: []*regexp.Regexp{accessRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
|
{namespace: "secret", patterns: []*regexp.Regexp{placeholder}, fields: map[string][]string{"file": {"content"}},
|
|
why: "a secret is never filled into an environment variable or any other field: put it in a file the " +
|
|
"module declares and mount that (novox/hq ADR 0086)"},
|
|
{namespace: "bound", patterns: []*regexp.Regexp{bound}, fields: map[string][]string{"file": {"content"}}},
|
|
{namespace: "port", patterns: []*regexp.Regexp{ofPort}, fields: map[string][]string{"file": {"content"}, "container": {"env"}, "process": {"env"}}},
|
|
{namespace: "seat", patterns: []*regexp.Regexp{ofSeat, ofSeatReach}, fields: map[string][]string{"file": {"content"}, "container": {"env"}, "process": {"env"}}},
|
|
{namespace: "machine", patterns: []*regexp.Regexp{ofMachine}, fields: map[string][]string{"*": {"path", "owner", "content", "name", "user", "root", "home"}}},
|
|
// Placed in a file's content by their holders, and judged there by their own rules
|
|
// (placeholderProblems, seatPlaceholderProblems).
|
|
{namespace: "environment", patterns: []*regexp.Regexp{ofEnvironment}, fields: map[string][]string{"*": {"content"}}},
|
|
{namespace: "shell", patterns: []*regexp.Regexp{ofShell}, fields: map[string][]string{"*": {"content"}}},
|
|
{namespace: "contribution", patterns: []*regexp.Regexp{ofContribution}, fields: map[string][]string{"*": {"content"}}},
|
|
// Filled in what a provider serves (consumer_into_serves.go), never in a resource.
|
|
{namespace: "consumer", patterns: []*regexp.Regexp{consumerFact}},
|
|
}
|
|
|
|
// reads is whether this pass fills a field of a resource of this type.
|
|
func (f filler) reads(kind, field string) bool {
|
|
return oneOf(f.fields[kind], field) || oneOf(f.fields["*"], field)
|
|
}
|
|
|
|
// ofKnownNamespace is any `${<known namespace>:` and what follows it up to its brace or the end of
|
|
// its line, whatever its case: what remains of one once every pass's pattern is set aside is a
|
|
// misspelling.
|
|
var ofKnownNamespace = regexp.MustCompile(`(?i)\$\{(?:` + strings.Join(namespacesOf(fillers), "|") + `):[^}\n]*\}?`)
|
|
|
|
// namespaceShaped is a placeholder in the mesh's shape: a lower-case word, a colon, and a key that
|
|
// begins with a letter or a digit and holds no space, brace or `$`.
|
|
var namespaceShaped = regexp.MustCompile(`\$\{[a-z][a-z0-9_-]*:[A-Za-z0-9][^\s{}$]*\}`)
|
|
|
|
func namespacesOf(fs []filler) []string {
|
|
out := make([]string, len(fs))
|
|
for i, f := range fs {
|
|
out[i] = f.namespace
|
|
}
|
|
return out
|
|
}
|
|
|
|
// unconsumedPlaceholders is every placeholder in one resource of a definition that no pass fills
|
|
// where it stands, each named with the module, the resource, the field and the token. The same
|
|
// function at the catalogue check and at composition, over the resource as the manifest wrote it.
|
|
func unconsumedPlaceholders(module string, r map[string]any) []string {
|
|
kind := fmt.Sprint(r["type"])
|
|
var problems []string
|
|
var sweep func(top, field string, v any)
|
|
sweep = func(top, field string, v any) {
|
|
switch v := v.(type) {
|
|
case string:
|
|
rest := v
|
|
for _, f := range fillers {
|
|
for _, p := range f.patterns {
|
|
if !f.reads(kind, top) {
|
|
for _, token := range p.FindAllString(rest, -1) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v holds %s in its %s, and ${%s:…} is not filled in this field: "+
|
|
"it would reach the machine as that text (novox/hq issue 231)%s",
|
|
module, r["id"], token, field, f.namespace, whereFilled(f)))
|
|
}
|
|
}
|
|
rest = p.ReplaceAllString(rest, "")
|
|
}
|
|
}
|
|
for _, token := range ofKnownNamespace.FindAllString(rest, -1) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v holds %s in its %s, which is no placeholder the mesh fills: a "+
|
|
"misspelt key, or a namespace in the wrong case, would reach the machine as that "+
|
|
"text (novox/hq issue 231)", module, r["id"], token, field))
|
|
}
|
|
rest = ofKnownNamespace.ReplaceAllString(rest, "")
|
|
for _, token := range namespaceShaped.FindAllString(rest, -1) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v holds %s in its %s, and no pass of the mesh fills it: a misspelt "+
|
|
"placeholder would reach the machine as that text (novox/hq issue 231). The mesh "+
|
|
"fills ${%s:…}; the shell's own syntax belongs in the module's shell code (ADR 0204)",
|
|
module, r["id"], token, field, strings.Join(namespacesOf(fillers), ":…}, ${")))
|
|
}
|
|
case []any:
|
|
for i, e := range v {
|
|
sweep(top, fmt.Sprintf("%s[%d]", field, i), e)
|
|
}
|
|
case map[string]any:
|
|
for _, k := range sortedKeys(v) {
|
|
sweep(top, field+"."+k, v[k])
|
|
}
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(r) {
|
|
sweep(k, k, r[k])
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// whereFilled says where a namespace's pass does fill, for a refusal of one written elsewhere.
|
|
func whereFilled(f filler) string {
|
|
if f.why != "" {
|
|
return ". " + strings.ToUpper(f.why[:1]) + f.why[1:]
|
|
}
|
|
if len(f.fields) == 0 {
|
|
return "; it is filled only in what a provider serves"
|
|
}
|
|
var where []string
|
|
for _, kind := range sortedKeys(f.fields) {
|
|
of := "a " + kind + "'s"
|
|
if kind == "*" {
|
|
of = "any resource's"
|
|
}
|
|
where = append(where, of+" "+strings.Join(f.fields[kind], ", "))
|
|
}
|
|
return "; it is filled in " + strings.Join(where, "; ")
|
|
}
|