envinput

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 10 Imported by: 0

README

envinput

Typed, validated container inputs from environment variables, *_FILE secrets, and mounted directories.

Status Go Report Card Go Reference


envinput is a command line tool and Go library that resolves configuration inputs passed to containers through INPUT_* environment variables, Docker-style *_FILE secrets, and mounted input directories. Every input is typed, validated, and checked in a single pass at startup.

For an input named api_token the value is looked up in this order:

  1. The environment variable INPUT_API_TOKEN, if set.
  2. The file named by the environment variable INPUT_API_TOKEN_FILE.
  3. A file named api_token inside the directory named by INPUTS_DIR.
  4. The default value, if any.

Setting both INPUT_API_TOKEN and INPUT_API_TOKEN_FILE is an error. Values can be parsed as string, boolean (true/1/yes/on, false/0/no/off), integer, or json.

Input names must match [_a-zA-Z][_a-zA-Z0-9]*; the environment variable is simply INPUT_ plus the uppercased name. Names ending in _file are rejected, because INPUT_<NAME>_FILE is reserved for the file variant of another input — name inputs that hold a file path with a _path suffix instead.

Two behaviors are deliberate defaults, both steerable:

  • Empty values are treated as not supplied and fall back to the default (docker run -e INPUT_FOO without a value, or compose interpolation from an unset variable, would otherwise silently override your default with an empty string). Set Empty / --empty to value to accept empty values, or error to reject them.
  • A single final newline is trimmed from file-based values (files written by editors, echo, or secret managers almost always carry one). Set Newline / --newline to keep to disable.

File loading is hardened: only regular files are accepted, contents must be valid UTF-8, and files larger than a limit (1 MiB by default) are rejected. Optionally, files can be restricted to configured root directories (symlinks are fully resolved before the containment check), and paths that are symbolic links can be rejected outright. Inputs marked secret never appear in error messages.

Package

import "codeberg.org/0x5a17ed/envinput"

// Typed resolution: string, bool, int and int64 use their obvious
// parsers; any other type is unmarshalled from JSON.
token, err := envinput.ResolveAs[string](envinput.Spec{
    Name:     envinput.MustParseName("api_token"),
    Required: true,
    Secret:   true,
})

type Config struct {
    Host  string `json:"host"`
    Ports []int  `json:"ports"`
}
cfg, err := envinput.ResolveAs[Config](envinput.Spec{
    Name: envinput.MustParseName("config"),
    File: envinput.FilePolicy{AllowedRoots: []string{"/run/secrets"}},
})

ResolveAs returns a Resolved[T] whose Value is a T; its String method prints a placeholder for secret inputs, making it safe to log. Resolve is the dynamically typed variant driven by Spec.Type (used by the CLI), and LookupSource reports where an input would be read from without reading it. The *With variants accept an os.LookupEnv-shaped function for testing. Spec.Validate takes an optional hook that can reject parsed values (and defaults) with an error — the manifest constraints below compile down to it.

Command line tool

$ go install codeberg.org/0x5a17ed/envinput/cmd/envinput@latest

envinput get resolves an input and prints it:

$ INPUT_API_TOKEN=hello envinput get api_token
hello
$ envinput get retries --type=integer --default=42
42
$ INPUT_API_TOKEN_FILE=/run/secrets/token envinput get api_token --secret --output=json
{"name":"api_token","value":"s3cret","source":"file","secret":true}

Flags: --type / -t (string, boolean, integer, json), --default / -d (parsed according to --type), --required / -r, --secret, --empty (unset, value, error), --max-bytes, --newline (trim, keep), --allowed-root (repeatable), --reject-symlinks, and --output / -o (value or json).

envinput check validates every input declared in a YAML manifest in one pass — run it at container start to report all misconfigurations at once (exit code 1 if anything failed; values are never printed):

$ cat inputs.yaml
inputs:
  api_token:
    required: true
    secret: true
  log_level:
    enum: [debug, info, warn, error]
    default: info
  port:
    type: integer
    min: 1
    max: 65535
  endpoints:
    type: json
    allowed_roots: [/run/secrets]
    schema:
      type: array
      minItems: 1
      items:
        type: string
        pattern: "^[a-z]+://"
$ envinput check inputs.yaml
fail  api_token                required input api_token was not supplied
ok    endpoints                source=environment
ok    log_level                source=default
ok    port                     source=environment

Per-input manifest fields: type, required, default, secret, empty, max_bytes, newline, allowed_roots, reject_symlinks, plus value constraints. Defaults must satisfy the constraints too — a self-contradictory manifest fails at check time.

  • enum (string and integer inputs), pattern (string inputs, unanchored RE2 — anchor with ^/$), and min/max (integer inputs, inclusive).
  • schema (an inline JSON Schema written as YAML) or schema_path (a JSON Schema file, resolved relative to the manifest) for json inputs, validated with santhosh-tekuri/jsonschema. Schemas can $ref local files but never load anything from the network.

The manifest format itself is described by manifest.schema.json; point your editor at it for completion and validation:

# yaml-language-server: $schema=https://codeberg.org/0x5a17ed/envinput/raw/branch/main/manifest.schema.json
inputs:
  ...

The manifest loader is also available as the Go package codeberg.org/0x5a17ed/envinput/manifest.

envinput has tests whether an input is supplied without printing it — exit code 0 when supplied, 1 when not, 2 on configuration errors. Note that has reports raw presence: an input supplied as an empty string still counts as supplied.

$ if envinput has api_token; then echo supplied; fi

envinput key prints the environment variable mapped to an input name:

$ envinput key api_token
INPUT_API_TOKEN
$ envinput key api_token --file
INPUT_API_TOKEN_FILE

License

See LICENSE.

Documentation

Overview

Package envinput resolves configuration values ("inputs") passed to a container or process through environment variables or files.

For an input named "api_token" the value is looked up in this order:

  1. The environment variable INPUT_API_TOKEN, if set.
  2. The file named by the environment variable INPUT_API_TOKEN_FILE.
  3. A file named "api_token" inside the directory named by INPUTS_DIR.
  4. The default value, if any.

Setting both INPUT_API_TOKEN and INPUT_API_TOKEN_FILE is an error. By default a supplied-but-empty value is treated as not supplied at all (see EmptyMode) and a single final newline is trimmed from file-based values (see NewlineMode).

Index

Constants

View Source
const DefaultMaxBytes = 1 << 20

DefaultMaxBytes is the file size limit applied when FilePolicy.MaxBytes is zero.

View Source
const InputsDirVar = "INPUTS_DIR"

InputsDirVar is the environment variable naming the directory that is searched for per-input files as the third resolution step.

Variables

This section is empty.

Functions

func ParseValue

func ParseValue(raw string, typ Type) (any, error)

ParseValue parses a raw input value according to the given type. See Resolved.Value for the Go types produced. The zero Type means TypeString.

Types

type EmptyMode

type EmptyMode string

EmptyMode controls how a supplied-but-empty value is handled. Empty values are common accidents in container deployments: `docker run -e INPUT_FOO` without a value, or compose interpolation from an unset variable, both set the variable to an empty string.

const (
	// EmptyUnset treats an empty value as if the input were not supplied,
	// falling back to the default. This is the behavior of the zero value.
	EmptyUnset EmptyMode = "unset"
	// EmptyValue accepts an empty value as a value like any other.
	EmptyValue EmptyMode = "value"
	// EmptyError rejects an empty value with an error.
	EmptyError EmptyMode = "error"
)

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error is returned for invalid input values and misconfigured inputs. Errors from the operating system (for example a missing or unreadable file) are returned as-is instead.

func (*Error) Error

func (e *Error) Error() string

type FilePolicy

type FilePolicy struct {
	// MaxBytes caps the file size. Zero or negative means DefaultMaxBytes.
	MaxBytes int64

	// Newline controls whether a single final newline is trimmed (the
	// default) or kept.
	Newline NewlineMode

	// AllowedRoots, when non-empty, restricts input files to descendants of
	// the listed directories (for example /run/secrets). The file path is
	// fully resolved — symbolic links included — before the containment
	// check, so links cannot escape a root. Roots that do not exist are
	// ignored.
	AllowedRoots []string

	// RejectSymlinks refuses paths whose final component is a symbolic
	// link. Note that Kubernetes secret mounts present files as symlinks,
	// so this must stay off for those.
	RejectSymlinks bool
}

FilePolicy configures how file-based values (INPUT_<NAME>_FILE and INPUTS_DIR) are loaded. The zero value reads regular files of up to DefaultMaxBytes anywhere on the filesystem, requires their contents to be valid UTF-8, and trims a single final newline.

type LookupFunc

type LookupFunc func(key string) (string, bool)

LookupFunc looks up an environment variable, reporting whether it is set. os.LookupEnv satisfies this signature.

type Name

type Name struct {
	// contains filtered or unexported fields
}

Name is a validated input name. The zero value is not usable; construct names with ParseName or MustParseName.

A name must match [_a-zA-Z][_a-zA-Z0-9]* and must not end in "_file" (in any case combination): the INPUT_<NAME>_FILE environment variable is reserved for pointing an input at a file, so a logical input named "token_file" would collide with the file variant of "token". Inputs whose value is a file path should use a "_path" suffix instead.

func MustParseName

func MustParseName(s string) Name

MustParseName is ParseName, panicking on invalid names. It is intended for names known at compile time.

func ParseName

func ParseName(s string) (Name, error)

ParseName validates s and returns it as a Name.

func (Name) EnvironmentKey

func (n Name) EnvironmentKey() string

EnvironmentKey returns the environment variable holding the input's value: "INPUT_" followed by the uppercased name. It returns "" for the zero Name.

func (Name) FileEnvironmentKey

func (n Name) FileEnvironmentKey() string

FileEnvironmentKey returns the environment variable naming the file the input's value is read from: EnvironmentKey plus a "_FILE" suffix. It returns "" for the zero Name.

func (Name) IsZero

func (n Name) IsZero() bool

IsZero reports whether n is the unusable zero Name.

func (Name) String

func (n Name) String() string

String returns the name as given to ParseName.

type NewlineMode

type NewlineMode string

NewlineMode controls how a final newline in file-based values is handled. It never applies to values from environment variables.

const (
	// NewlineTrim removes a single trailing "\n" or "\r\n". This is the
	// behavior of the zero value: files written by editors, echo, or
	// secret managers almost always carry a final newline that is not part
	// of the value.
	NewlineTrim NewlineMode = "trim"
	// NewlineKeep keeps the file contents exactly as read.
	NewlineKeep NewlineMode = "keep"
)

type Resolved

type Resolved[T any] struct {
	// Value holds the parsed value. For the dynamically typed Resolve, T is
	// any: string for TypeString, bool for TypeBoolean, int64 for
	// TypeInteger, the result of unmarshalling (numbers as json.Number) for
	// TypeJSON, the unparsed Spec.Default when Source is SourceDefault, and
	// nil when an optional input was not supplied and had no default. For
	// ResolveAs[T] it is a T (the zero T when an optional input was not
	// supplied and had no default).
	Value T

	// Source reports where the value came from.
	Source Source

	// Secret is copied from the spec.
	Secret bool
}

Resolved is the result of resolving an input.

func Resolve

func Resolve(spec Spec) (Resolved[any], error)

Resolve resolves a dynamically typed input against the process environment: Resolved.Value holds the Go type corresponding to Spec.Type. Use ResolveAs when the wanted type is known at compile time.

func ResolveAs

func ResolveAs[T any](spec Spec) (Resolved[T], error)

ResolveAs resolves an input against the process environment, parsed into T. The spec's Type field is not needed: string, bool, int and int64 select their obvious parsers, and any other T — including structs, maps, and any itself — is unmarshalled from JSON.

func ResolveAsWith

func ResolveAsWith[T any](spec Spec, lookup LookupFunc) (Resolved[T], error)

ResolveAsWith is ResolveAs with a custom environment lookup.

func ResolveWith

func ResolveWith(spec Spec, lookup LookupFunc) (Resolved[any], error)

ResolveWith is Resolve with a custom environment lookup. Files referenced by the environment are still read from the real filesystem.

func (Resolved[T]) String

func (r Resolved[T]) String() string

String prints the resolved value, or a placeholder when the input is marked secret, making Resolved safe to pass to log statements.

type Source

type Source string

Source reports where a resolved input value came from.

const (
	SourceEnvironment    Source = "environment"
	SourceFile           Source = "file"
	SourceInputDirectory Source = "input-directory"
	SourceDefault        Source = "default"
)

func LookupSource

func LookupSource(name Name) (source Source, ok bool, err error)

LookupSource reports the source Resolve would read the input from, without reading or parsing the value. It returns ok == false when the input is not supplied and resolution would fall back to the default. Because the value is not read, EmptyMode does not apply: an input supplied as an empty string or an empty file still reports ok == true.

func LookupSourceWith

func LookupSourceWith(name Name, lookup LookupFunc) (Source, bool, error)

LookupSourceWith is LookupSource with a custom environment lookup.

type Spec

type Spec struct {
	// Name is the validated input name; see ParseName.
	Name Name

	// Type selects how the raw value is parsed by Resolve. The zero value
	// means TypeString. ResolveAs ignores it (the type parameter decides)
	// but rejects a Type that contradicts the type parameter.
	Type Type

	// Required makes resolution fail when the input is not supplied and no
	// Default is set.
	Required bool

	// Default is used when the input is not supplied. Resolve returns it
	// as-is, without parsing; ResolveAs requires it to be assignable to the
	// type parameter. A nil Default means the input has no default.
	Default any

	// Secret marks the input as sensitive: error messages will not contain
	// the value, and Resolved.String prints a placeholder instead of it.
	Secret bool

	// Validate, when set, is called with the parsed value — or with the
	// default, when the default is used — and can reject it by returning an
	// error. The error is reported with the input name prepended. For
	// secret inputs the hook must not include the value in its errors;
	// envinput cannot redact them.
	Validate func(value any) error

	// Empty controls how a supplied-but-empty value is handled.
	Empty EmptyMode

	// File configures how file-based values (INPUT_<NAME>_FILE and
	// INPUTS_DIR) are loaded.
	File FilePolicy
}

Spec describes a single input to resolve.

type Type

type Type string

Type identifies how a raw input value is parsed.

const (
	TypeString  Type = "string"
	TypeBoolean Type = "boolean"
	TypeInteger Type = "integer"
	TypeJSON    Type = "json"
)

Directories

Path Synopsis
cmd
envinput command
Command envinput resolves container inputs from environment variables, files, or an inputs directory.
Command envinput resolves container inputs from environment variables, files, or an inputs directory.
Package manifest loads YAML manifests that declare the inputs a container expects, and checks all of them against the environment at once — typically at container start, so every misconfigured input is reported in a single pass instead of one failure at a time.
Package manifest loads YAML manifests that declare the inputs a container expects, and checks all of them against the environment at once — typically at container start, so every misconfigured input is reported in a single pass instead of one failure at a time.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL