package resolution import ( "fmt" "regexp" "strings" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) // 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"}, "directory": {"owner"}, "container": {"groups"}}}, {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"}}}, // What the machine's modules back up, placed by the backup holder's file (backup_placement.go, issue 794). {namespace: mcat.BackupNamespace, patterns: []*regexp.Regexp{ofBackup}, fields: map[string][]string{"*": {"content"}}}, // A collection, placed by its collector (collections.go): filled by the provisioner's client. {namespace: mcat.CollectedNamespace, patterns: []*regexp.Regexp{ofCollected}, fields: map[string][]string{"*": {"content"}}}, // A provider's consumers' credentials (consumers.go): filled by the provisioner's client. {namespace: mcat.ConsumersNamespace, patterns: []*regexp.Regexp{ofConsumers}, fields: map[string][]string{"file": {"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, "; ") }