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 `${:`, case-insensitively, unless a shell's parameter operator follows // its colon — `${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. // And an operator — `:-`, `:=`, `:+`, `:?` — after a word the mesh also uses is the shell's too: // `${PORT:-8080}`, `${SHELL:-/bin/sh}`, `${SECRET:?unset}` and `${dir:-/tmp}` are among the commonest // lines of a script or an env file, and pass whatever the word's case. // 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 `${:` and what follows it up to its brace or the end of // its line, whatever its case, unless what follows the colon is a shell's parameter operator (`-`, // `=`, `+`, `?`): what remains of one once every pass's pattern is set aside is a misspelling. var ofKnownNamespace = regexp.MustCompile(`(?im)\$\{(?:` + strings.Join(namespacesOf(fillers), "|") + `):(?:[^-=+?}\n][^}\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, "; ") }