Documentation
¶
Overview ¶
Package envx reads typed configuration from environment variables, the standard way a containerized app is configured.
Every getter takes a fallback and never fails: an unset or empty variable returns the fallback silently, and a set-but-malformed value returns the fallback with one Warn line through slog's default logger, so a typo in a deployment surfaces in the logs instead of silently changing behavior. Boolean parsing is tolerant (true/1/yes/on, false/0/no/off, case-insensitive, trimmed) because that is what deployment YAML tends to contain.
Two calls handle the values an app cannot default: Require returns an error for a missing mandatory variable, and Secret additionally supports the Docker convention of an adjacent KEY_FILE variable pointing at a mounted secret file, read once, size-bounded, and returned as written apart from one trailing line ending. Each way a secret file can be unusable is named by a sentinel, so a caller can report the class with errors.Is instead of matching message text or echoing the configured path. A KEY_FILE that is present but blank names no file, so it resolves as if unset; IsBlankSecretFilePath reports that state for the caller who must refuse a broken secret pointer rather than fall back to the plain variable.
For the caller that must own the malformed-value decision instead of accepting warn-and-fallback — reject startup, apply bounds, keep an existing value — BoolStrict, IntStrict and DurationStrict return the parse result as (value, ok, error) and never log. BoolStrict shares Bool's parser, so the two accept exactly the same spellings; prefer it over Bool for a key whose value may be sensitive (the Warn line names the raw value) or when the caller owns its own diagnostics.
envx reads the process environment at call time; it holds no state, starts no goroutines, and depends on nothing beyond the standard library and github.com/cplieger/pathinside, the standard-library-only path-name predicates the Secret file rule is built from.
Example ¶
package main
import (
"fmt"
"os"
"time"
"github.com/cplieger/envx"
)
func main() {
os.Setenv("APP_LISTEN", ":9090")
os.Setenv("APP_DEBUG", "yes")
defer os.Unsetenv("APP_LISTEN")
defer os.Unsetenv("APP_DEBUG")
addr := envx.String("APP_LISTEN", ":8080")
debug := envx.Bool("APP_DEBUG", false)
interval := envx.Duration("APP_INTERVAL", 6*time.Hour)
fmt.Println(addr, debug, interval)
}
Output: :9090 true 6h0m0s
Index ¶
- Variables
- func Bool(key string, fallback bool) bool
- func BoolStrict(key string) (value, ok bool, err error)
- func Duration(key string, fallback time.Duration) time.Duration
- func DurationStrict(key string) (value time.Duration, ok bool, err error)
- func Int(key string, fallback int) int
- func IntStrict(key string) (value int, ok bool, err error)
- func IsBlankSecretFilePath(key string) bool
- func Require(key string) (string, error)
- func Secret(key string) (string, error)
- func String(key, fallback string) string
- type MissingError
- type SecretSource
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrSecretFilePathRejected means KEY_FILE named a path this package // refuses to open: not already clean, or containing a ".." path component. ErrSecretFilePathRejected = errors.New("envx: secret file path rejected") // ErrSecretFileTooLarge means the file was already over maxSecretFileSize // when it was opened. ErrSecretFileTooLarge = errors.New("envx: secret file exceeds the size limit") // ErrSecretFileGrew means the file passed the size gate and then grew past // maxSecretFileSize while it was being read, so the content would have been // silently truncated. ErrSecretFileGrew = errors.New("envx: secret file grew past the size limit during read") // ErrSecretFileUnreadable means the operating system refused the open, stat // or read. The underlying *os.PathError stays reachable with errors.As, so // a caller can still report the syscall and its reason (without the path, // which the PathError also carries). ErrSecretFileUnreadable = errors.New("envx: secret file could not be read") )
The failure classes of a KEY_FILE secret read. Every error Secret and SecretWithSource return for a configured secret file wraps exactly one of these or ErrBlankSecretFile, so a caller can name WHY the file was unusable with errors.Is and apply a different policy per class.
The classes exist because the alternative was worse: without them the only way to tell a rejected path from an oversized file was matching this package's error TEXT, so callers folded every non-blank failure into one generic message instead. That mattered here more than in most packages, because these errors embed the operator-supplied PATH and a caller whose KEY_FILE was misconfigured to hold the secret ITSELF (rather than a path to it) must be able to describe the failure without echoing the value into a log. Naming the class no longer requires touching the path.
The classes are deliberately narrow rather than one "unusable file" sentinel: their remediations differ (fix the variable, shrink the file, stop rewriting it, fix the mount) and only a caller writing an operator-facing message knows which of those it wants to say.
var ErrBlankSecretFile = errors.New("envx: secret file is blank")
ErrBlankSecretFile is returned by Secret and SecretWithSource when KEY_FILE names a readable file whose content is blank — empty, or whitespace only.
Blankness is the one judgement made on the whitespace-trimmed content: a file holding only spaces or newlines is a broken secret mount, not a secret. The value a readable file DOES carry is returned with at most a trailing line ending removed, so the two operations do not share a rule (see trimTrailingLineEnding).
It is a distinct sentinel because a caller often has a policy for a blank secret that differs from its policy for an unusable one. cert-converter is the motivating case: it offers PFX_ALLOW_EMPTY_PASSWORD as an explicit opt-in to running without a password, and that opt-in has to mean the same thing whether the blank value arrived through the environment or through a mounted file. Without a sentinel the file channel could only be classified by matching this package's error TEXT, so the opt-in silently applied to one channel and not the other.
It is deliberately NOT a *MissingError: the operator did configure a secret file, and treating "you forgot to set this" the same as "the file you mounted is blank" would hide a broken secret mount behind a caller's default.
Functions ¶
func Bool ¶
Bool returns the boolean value of the environment variable key, or fallback when the variable is unset or empty.
Parsing is tolerant of the spellings deployment files actually contain: true/1/yes/on and false/0/no/off, case-insensitive, surrounding whitespace ignored. Any other value logs one Warn through slog's default logger and returns fallback, so a typo ("ture") is visible in the logs instead of silently flipping a flag.
The Warn line carries the raw value. Use BoolStrict for a key whose value may be sensitive, or when the caller owns its own diagnostics.
func BoolStrict ¶ added in v1.5.0
BoolStrict returns the boolean value of the environment variable key with the parse result owned by the caller: a set-but-malformed value is returned as an error instead of the tolerant getters' warn-and-fallback.
The accepted spellings are exactly Bool's (true/1/yes/on and false/0/no/off, case-insensitive, surrounding whitespace ignored) because both route through the same parser and cannot drift apart. The three states match IntStrict: unset or empty (false, false, nil), malformed (false, false, err), valid (b, true, nil). value is false in two of those three, so ok — never value alone — distinguishes "set to false" from "not set".
Unlike IntStrict and DurationStrict, the error carries no fragment of the value: there is no parse error to wrap, and the reason to reach for BoolStrict is usually that the value must not be repeated anywhere. It names the key and the accepted vocabulary. Strict variants never log.
Use Bool when a malformed value should fall back with a Warn naming the raw value; use BoolStrict for a key whose value may be sensitive (one an operator could wire to a secret by mistake, so no diagnostic may echo it) or when the caller owns its own diagnostics.
(value and ok share their bool type in the signature at the linter's insistence; they mean what the family's other strict variants mean.)
func Duration ¶
Duration returns the value of the environment variable key parsed with time.ParseDuration ("30s", "6h", "1h30m"), or fallback when the variable is unset or empty. A set-but-unparseable value logs one Warn through slog's default logger and returns fallback.
A bare number without a unit is deliberately not accepted: "30" is ambiguous between seconds and minutes across tools, and time.ParseDuration rejecting it (with the Warn line naming the key) is clearer than guessing.
func DurationStrict ¶ added in v1.2.0
DurationStrict returns the value of the environment variable key parsed with time.ParseDuration ("30s", "6h", "1h30m"), with the parse result owned by the caller: a set-but-malformed value is returned as an error instead of the tolerant getters' warn-and-fallback.
The three states match IntStrict: unset or empty (0, false, nil), malformed (0, false, err), valid (d, true, nil). As with Duration, a bare number without a unit is rejected ("30" is ambiguous between seconds and minutes across tools). Strict variants never log.
func Int ¶
Int returns the integer value of the environment variable key, or fallback when the variable is unset or empty. A set-but-unparseable value logs one Warn through slog's default logger and returns fallback.
func IntStrict ¶ added in v1.2.0
IntStrict returns the integer value of the environment variable key with the parse result owned by the caller: a set-but-malformed value is returned as an error instead of the tolerant getters' warn-and-fallback.
ok reports a successfully parsed value; it is false when the variable is unset or empty (empty equals unset, as with every getter) and false when the value did not parse. err is non-nil only for a set-but-malformed value; it names the key and wraps the underlying strconv error. Exactly one of the three states holds: unset (0, false, nil), malformed (0, false, err), or valid (n, true, nil). Strict variants never log.
Use Int when a malformed value should fall back with a Warn; use IntStrict when the caller must decide what a malformed value means (reject startup, apply bounds, keep an existing value).
func IsBlankSecretFilePath ¶ added in v1.4.0
IsBlankSecretFilePath reports whether the KEY_FILE companion variable for key is present in the environment but blank — set to the empty string, or to whitespace only — and therefore names no secret file.
SecretWithSource cannot report this state, and deliberately still does not: it gates the file channel on a non-empty KEY_FILE, so `KEY_FILE=` is indistinguishable from unset and resolves through KEY exactly as if the operator had never written it. That is the right rule for a VALUE, which is why every getter here treats empty as unset: compose files and CI matrices materialize `KEY=` for a knob left blank. KEY_FILE holds a POINTER, though, and a blank pointer is a broken secret mount rather than an operator declining to configure one — `KEY_FILE=${SECRETS_DIR}/token` with SECRETS_DIR undefined produces exactly this shape.
The state is reported instead of acted on because only the caller knows what its secret's absence means, and the two answers are far apart. For a mandatory credential the fallthrough surfaces as *MissingError and startup fails anyway; for an OPTIONAL one — a bearer token whose absence serves an endpoint unauthenticated — a blank KEY_FILE silently disarms the gate, so the fallthrough is fail-OPEN. A caller that must refuse that asks before resolving:
if envx.IsBlankSecretFilePath("BEAT_TOKEN") {
return errors.New("BEAT_TOKEN_FILE is set but empty: unset it to configure BEAT_TOKEN directly, or point it at a secret file")
}
token, src, err := envx.SecretWithSource("BEAT_TOKEN")
Whitespace counts as blank, the rule ErrBlankSecretFile already applies to a secret file's CONTENT: a whitespace-only path is a quoting slip, never a filename an operator meant to write. Resolution treats the two shapes differently — an empty KEY_FILE is ignored, a whitespace-only one is opened and fails — so for that shape this predicate buys a diagnostic naming the blank variable in place of a "no such file or directory" for a filename nobody can see, not a change of fail direction.
It reports the state and changes none of it: a deployment where `KEY_FILE=` falls through to KEY keeps doing so, including for callers that never ask.
Example ¶
package main
import (
"fmt"
"os"
"github.com/cplieger/envx"
)
func main() {
// The shape compose interpolation of an undefined variable produces: the
// pointer is present, but it names no file.
os.Setenv("APP_API_KEY_FILE", "")
os.Setenv("APP_API_KEY", "inline-key")
defer os.Unsetenv("APP_API_KEY_FILE")
defer os.Unsetenv("APP_API_KEY")
// Resolution cannot see it: the inline value wins as if the operator had
// never written APP_API_KEY_FILE.
_, source, _ := envx.SecretWithSource("APP_API_KEY")
fmt.Println("resolved from:", source)
fmt.Println("blank secret pointer:", envx.IsBlankSecretFilePath("APP_API_KEY"))
}
Output: resolved from: env blank secret pointer: true
func Require ¶
Require returns the value of the environment variable key, or a *MissingError when it is unset or empty. It returns an error rather than exiting so the caller controls startup failure (collect every missing key, log through the configured handler, then exit once).
Example ¶
package main
import (
"fmt"
"github.com/cplieger/envx"
)
func main() {
// Collect every missing variable, then fail once.
var missing []error
if _, err := envx.Require("APP_TOKEN_UNSET_A"); err != nil {
missing = append(missing, err)
}
if _, err := envx.Require("APP_TOKEN_UNSET_B"); err != nil {
missing = append(missing, err)
}
fmt.Println(len(missing), "missing")
}
Output: 2 missing
func Secret ¶
Secret returns a required secret from the environment, supporting the Docker secrets convention: when KEY_FILE is set to a non-empty value, the secret is read from that file (size-bounded, with at most one trailing line ending removed); otherwise the value of KEY itself is returned. An unset or empty result is a *MissingError, and a configured file whose content is blank is ErrBlankSecretFile.
The file channel returns the secret as written apart from that single trailing newline, so a value whose leading or trailing SPACES are part of the credential survives the round trip: the two channels must not disagree about what the secret IS, and KEY is never rewritten either. Only the line ending an editor or `kubectl create secret` appends is removed, because that one is an artifact of storing a value in a file rather than part of the value.
The KEY_FILE indirection keeps the secret value out of `docker inspect` output and compose files; the file path must be clean (no ".." traversal), and the read uses a single handle so the size check and the read cannot race. The secret value itself never appears in an error or a log line; errors carry the key name and file path only. Each failure class is named by a sentinel (ErrSecretFilePathRejected, ErrSecretFileTooLarge, ErrSecretFileGrew, ErrSecretFileUnreadable, ErrBlankSecretFile) so a caller can report WHY the file was unusable without echoing the path.
Use SecretWithSource when the caller needs to know WHICH channel supplied the value — for example to warn that a KEY it also set was ignored in favour of KEY_FILE. Secret delegates to it, so the two cannot drift. Use IsBlankSecretFilePath when a KEY_FILE that is present but blank must be refused instead of read as absent.
func String ¶
String returns the value of the environment variable key, or fallback when the variable is unset or empty. An empty value is treated as unset because compose files and CI matrices routinely materialize `KEY=` for a knob the operator left blank; distinguishing that from absence is almost never what a config reader wants (use os.LookupEnv directly when it is).
Unlike the parsing getters (Bool, Int, Duration), String does not trim the value: whitespace can be meaningful in a free-form string, and the caller knows whether its value is a path, a token, or a list. A whitespace-only value therefore counts as set.
Types ¶
type MissingError ¶
type MissingError struct {
// Key is the environment variable name that was required.
Key string
}
MissingError reports a required environment variable that is unset or empty. It carries the key so a caller can aggregate several missing variables into one startup failure.
func (*MissingError) Error ¶
func (e *MissingError) Error() string
Error implements the error interface.
type SecretSource ¶ added in v1.3.0
type SecretSource string
SecretSource reports which channel a secret value came from.
const ( // SourceNone means no value was found in either channel. SourceNone SecretSource = "none" // SourceEnv means the value came from KEY itself. SourceEnv SecretSource = "env" // SourceFile means the value came from the file named by KEY_FILE. SourceFile SecretSource = "file" )
The secret delivery channels SecretWithSource distinguishes.
func SecretWithSource ¶ added in v1.3.0
func SecretWithSource(key string) (value string, source SecretSource, err error)
SecretWithSource is Secret, additionally reporting which channel supplied the value.
Secret resolves KEY_FILE ahead of KEY and returns only the value, so a caller cannot tell whether an environment variable it also set was used or silently ignored. That matters when both are set: the operator expressed two intentions and exactly one takes effect. Only the caller knows whether that is worth a warning in its own configuration vocabulary, so this reports the fact rather than logging it here — consistent with this package's rule that the caller owns the policy and envx owns the parse.
A caller that needs the both-channels-set condition composes it in one line:
v, src, err := envx.SecretWithSource("PFX_PASSWORD")
if src == envx.SourceFile && os.Getenv("PFX_PASSWORD") != "" { /* warn */ }
That works on the error paths too, which is where it matters most: an unreadable file still reports SourceFile, so the caller can tell the operator that the environment variable they also set is not a fallback.
Precedence is unchanged from Secret: KEY_FILE wins when set to a non-empty value, because the entire point of the file channel is keeping the value out of the process environment. A KEY_FILE that is present but blank names no file and is therefore not the file channel at all: resolution falls through to KEY, and IsBlankSecretFilePath is how a caller detects that a secret pointer it was given is broken.