Files
mesh-catalog/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/environment.go
T

235 lines
7.4 KiB
Go

package main
import (
"cmp"
"fmt"
"regexp"
"slices"
"strings"
)
// The session environment as a collection (novox/hq ADR 0203, ADR 0349 clause 13, design 57 L3, issue 795). One
// collection, `environment`, whose items are typed by attributes: an item is a variable (`kind=variable`,
// `name=<NAME>`) or a PATH entry (`kind=path`, `at=start|end`), and its template's text is the variable's value or
// the entry. The collector names the rendering, not a selector: `${collected:environment:posix}` and
// `${collected:environment:systemd}` (and the old `${environment:posix|systemd}`) are the collection `environment`
// written for a POSIX shell or for environment.d. Both renderings are the ones mesh-controller internal/catalogue
// environment_into.go writes for a collector not handed, byte for byte (its golden files are this client's).
// Environment collection and renderers.
const (
EnvironmentCollection = "environment"
EnvironmentPOSIX = "posix"
EnvironmentSystemd = "systemd"
)
// environmentName is an environment variable's name, which only this collection's `name` attribute may be.
var environmentName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
// attributeFits is whether an item's attribute is of the shape the collection allows.
func attributeFits(collection, k, v string) bool {
if collection == EnvironmentCollection && k == "name" {
return environmentName.MatchString(v)
}
return attributeKeyShape.MatchString(k) && attributeValueShape.MatchString(v)
}
// environmentRenderer is the rendering a need names, when it is the environment's: `${collected:environment:posix}`
// or `${environment:posix}`.
func environmentRenderer(n Need) (string, bool) {
name := n.Name
switch {
case n.Kind == "environment":
case n.Kind == CollectedKind:
rest, is := strings.CutPrefix(name, EnvironmentCollection+":")
if !is {
return "", false
}
name = rest
default:
return "", false
}
return name, name == EnvironmentPOSIX || name == EnvironmentSystemd
}
type envVariable struct{ module, name, value string }
type envEntry struct {
module, entry string
order int
item string
}
// literalProblem is why a value cannot be written, unquoted by either reader, as the same string in both formats
// (mesh-controller internal/catalogue environment_into.go).
func literalProblem(v string) string {
switch {
case strings.ContainsAny(v, `'"`):
return "holds a quote"
case strings.Contains(v, `\`):
return "holds a backslash"
case strings.ContainsAny(v, "\n\r"):
return "holds a line break"
case strings.ContainsRune(v, 0):
return "holds a NUL"
case strings.Contains(v, "$"):
return "holds a $, which the two readers expand differently"
}
return ""
}
// environmentText is the collection's items written for one renderer: modules in name order, each module's variables
// sorted by name under a line naming it, then PATH's entries, each once where the first puts it, as the old form
// wrote them. An empty collection is empty text.
func environmentText(items map[string]itemValue, renderer string) (string, error) {
type owned struct {
id string
itemValue
}
var all []owned
for id, it := range items {
all = append(all, owned{id, it})
}
slices.SortFunc(all, func(a, b owned) int {
ma, na := splitID(a.id)
mb, nb := splitID(b.id)
return cmp.Or(cmp.Compare(ma, mb), cmp.Compare(na, nb))
})
var variables []envVariable
var start, end []envEntry
setter := map[string]string{}
placed := map[string]bool{}
// Items of one module in the order the old form declared them: by order, then name.
slices.SortStableFunc(all, func(a, b owned) int {
ma, _ := splitID(a.id)
mb, _ := splitID(b.id)
if c := cmp.Compare(ma, mb); c != 0 {
return c
}
if c := cmp.Compare(a.order(), b.order()); c != 0 {
return c
}
_, na := splitID(a.id)
_, nb := splitID(b.id)
return cmp.Compare(na, nb)
})
for _, it := range all {
module, _ := splitID(it.id)
value := strings.TrimSuffix(it.text, "\n")
if why := literalProblem(value); why != "" {
return "", fmt.Errorf("%s's item %s is %q on this machine, which %s: a value is a literal both readers "+
"take alike", module, it.id, value, why)
}
switch it.attributes["kind"] {
case "variable":
n := it.attributes["name"]
if n == "" {
return "", fmt.Errorf("%s's item %s is a variable and has no name", module, it.id)
}
if first, taken := setter[n]; taken {
return "", fmt.Errorf("%s and %s both set %s on this machine; the account has one environment, so "+
"one of them must stop setting it (novox/hq ADR 0203)", first, module, n)
}
setter[n] = module
variables = append(variables, envVariable{module, n, value})
case "path":
if strings.Contains(value, ":") {
return "", fmt.Errorf("%s puts %q on PATH on this machine, which holds a colon, PATH's own separator",
module, value)
}
if value == "" {
return "", fmt.Errorf("%s's item %s is a PATH entry and says none", module, it.id)
}
if placed[value] {
continue
}
placed[value] = true
e := envEntry{module: module, entry: value, order: it.order(), item: it.id}
switch it.attributes["at"] {
case "start":
start = append(start, e)
case "end":
end = append(end, e)
default:
return "", fmt.Errorf("%s's item %s is a PATH entry and is not at start or end", module, it.id)
}
default:
return "", fmt.Errorf("%s's item %s is of the environment and is not kind=variable or kind=path", module,
it.id)
}
}
// Variables per module, sorted by name within it.
slices.SortStableFunc(variables, func(a, b envVariable) int {
return cmp.Or(cmp.Compare(a.module, b.module), cmp.Compare(a.name, b.name))
})
if renderer == EnvironmentSystemd {
return systemdText(variables, start, end), nil
}
return posixText(variables, start, end), nil
}
func posixText(variables []envVariable, start, end []envEntry) string {
var b strings.Builder
named := ""
for _, v := range variables {
if v.module != named {
fmt.Fprintf(&b, "# %s\n", v.module)
named = v.module
}
fmt.Fprintf(&b, "export %s='%s'\n", v.name, v.value)
}
named = ""
for i := len(start) - 1; i >= 0; i-- {
p := start[i]
if p.module != named {
fmt.Fprintf(&b, "# %s\n", p.module)
named = p.module
}
fmt.Fprintf(&b, "case \":${PATH}:\" in *':%s:'*) ;; *) PATH='%s'\"${PATH:+:${PATH}}\" ;; esac\n",
p.entry, p.entry)
}
named = ""
for _, p := range end {
if p.module != named {
fmt.Fprintf(&b, "# %s\n", p.module)
named = p.module
}
fmt.Fprintf(&b, "case \":${PATH}:\" in *':%s:'*) ;; *) PATH=\"${PATH:+${PATH}:}\"'%s' ;; esac\n",
p.entry, p.entry)
}
if len(start)+len(end) > 0 {
b.WriteString("export PATH\n")
}
return b.String()
}
func systemdText(variables []envVariable, start, end []envEntry) string {
var b strings.Builder
named := ""
for _, v := range variables {
if v.module != named {
fmt.Fprintf(&b, "# %s\n", v.module)
named = v.module
}
fmt.Fprintf(&b, "%s=%s\n", v.name, v.value)
}
line := func(entries []envEntry, format string) {
if len(entries) == 0 {
return
}
var modules, paths []string
seen := map[string]bool{}
for _, p := range entries {
if !seen[p.module] {
seen[p.module] = true
modules = append(modules, p.module)
}
paths = append(paths, p.entry)
}
fmt.Fprintf(&b, "# %s\n"+format+"\n", strings.Join(modules, ", "), strings.Join(paths, ":"))
}
line(start, "PATH=%s${PATH:+:$PATH}")
line(end, "PATH=${PATH:+$PATH:}%s")
return b.String()
}