diff --git a/README.md b/README.md index 681a624..de7436d 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,11 @@ process was started with and the file's own key. A variable's value is parsed as the same text in the file would be. The three lists take comma-separated entries, with the spaces around each trimmed; an empty variable is an empty list. A value that does not parse or is invalid aborts -startup, naming the variable. +startup, naming the variable. A variable whose name starts with `PIXA_` but +is not in the table below, such as a misspelled one or `PIXA_PORT`, aborts +startup naming it, as an unknown config key does. The one other accepted +name is `PIXA_CONFIG_PATH`, the config file's path (like `--config`). The +variables set by the file's `env:` section are not checked. | Variable | Config key | Meaning | | ------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------- | diff --git a/TODO.md b/TODO.md index 3e8c7b4..85bda88 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,13 @@ exhaustion # Completed Steps +- 2026-09-28 unknown `PIXA_` environment variables abort startup (closes + #133): a variable whose name starts with `PIXA_` but is neither a + setting's variable nor `PIXA_CONFIG_PATH` aborts startup naming it, as + an unknown config key does, and `PIXA_PORT` is named with a pointer to + `PORT`; the check runs before the config file loads, so the variables + the file's `env:` section sets are not checked; documented in + `README.md`. - 2026-09-28 start on a fresh upaas volume (closes #129): the image starts as root only to give `/var/lib/pixa` to `pixad` when `pixad` does not own it (`deploy/docker-entrypoint.sh`), then runs the server diff --git a/internal/config/config.go b/internal/config/config.go index a2e2037..9c604b8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,6 +58,7 @@ var ( errValueRequired = errors.New("a value is required") errValueEmpty = errors.New("value must not be empty") errUnknownConfigKeys = errors.New("unknown config keys") + errUnknownEnvVars = errors.New("unknown environment variables") errNotAString = errors.New("not a string") errNotAnInteger = errors.New("not an integer") errNotABoolean = errors.New("not a boolean") @@ -146,6 +147,11 @@ type Config struct { func New(_ fx.Lifecycle, params Params) (*Config, error) { log := params.Logger.Get() + err := validateKnownEnvVars() + if err != nil { + return nil, err + } + // Look for the config file under the project name (/etc/pixa/, // ~/.config/pixa/), matching the /var/lib/pixa state directory, // not under the daemon name pixad. @@ -377,6 +383,44 @@ func envVarNames() map[string]string { } } +// validateKnownEnvVars rejects environment variables whose names start +// with PIXA_ but that are neither a setting's variable nor +// PIXA_CONFIG_PATH, so a misspelled variable fails at startup instead of +// being silently ignored, as validateKnownKeys does for config file keys. +// New calls it before loading the config file, so the variables the +// file's env section sets are not checked, just as validateKnownKeys +// leaves that section's contents alone. +func validateKnownEnvVars() error { + known := map[string]bool{"PIXA_CONFIG_PATH": true} + + for _, name := range envVarNames() { + known[name] = true + } + + var unknown []string + + for _, entry := range os.Environ() { + name, _, _ := strings.Cut(entry, "=") + + switch { + case !strings.HasPrefix(name, "PIXA_") || known[name]: + continue + case name == "PIXA_PORT": + unknown = append(unknown, name+" (use PORT for the port)") + default: + unknown = append(unknown, name) + } + } + + if len(unknown) > 0 { + sort.Strings(unknown) + + return fmt.Errorf("%w: %s", errUnknownEnvVars, strings.Join(unknown, ", ")) + } + + return nil +} + // lookupValue returns the value set for key and whether one is set. The // key's environment variable wins when it is present, even when empty; // its value is a string, read exactly as the same text quoted in the