Skip to content

stuft2/envchain

Repository files navigation

envchain

Overview

envchain is a tiny helper for Go services that backfills environment variables from one or more providers — without overwriting anything that’s already set. It also provides a small helper, GetEnv, for reading environment variables with defaults.

Providers Available:

  • Dotenv: read a local .env file
  • HashiCorp Vault (KV v2): fetch secrets over HTTP using a Vault token

Order matters and establishes precedence: existing process envfirst providersecond provider ➜ ...

Anything already set in the process wins. Missing keys are filled by the first provider you pass; still-missing keys are filled by the next provider, and so on.


CLI Usage

Local dev:

  1. Put a .env file beside your app.

  2. Optionally set VAULT_ADDR and authenticate to backfill missing secrets from Vault:

    VAULT_ADDR=https://vault.byu.edu vault login -method=oidc -path=byu-sso
  3. Install the CLI:

    go install github.com/stuft2/envchain/cmd/envchain@latest
  4. Create a sample .env:

    cat > .env <<'EOF'
    APP_NAME=envchain-demo
    PORT=8080
    EOF
  5. Run a command with backfilled env vars:

    envchain -- env | grep -E '^(APP_NAME|PORT)='
  6. Verify output:

    APP_NAME=envchain-demo
    PORT=8080
    

Optional Vault check:

envchain -vault-path "kvv2/<service>/dev/env-vars" -- env | grep '^YOUR_KEY='

CLI usage:

envchain [flags] -- <command> [args...]

Flags:

  • -dotenv (default .env): path to a dotenv file to backfill from. Pass an empty string to skip it.
  • -vault-path: KV v2 path to load from HashiCorp Vault. Leave empty to skip Vault.
  • -verbose: emit debug logs detailing how each provider resolves environment variables.

Environment variables such as VAULT_ADDR, VAULT_TOKEN, and VAULT_NAMESPACE still control Vault behaviour.

CI/Prod: rely on process env only. If you don’t set VAULT_ADDR, the Vault provider is inert, and a missing .env is ignored.

Programmatic Usage

Injecting Environment Variables

Construct the providers you want and pass them to envchain.Inject in precedence order:

package main

import (
    "log"

    "github.com/stuft2/envchain"
    "github.com/stuft2/envchain/providers/dotenv"
    "github.com/stuft2/envchain/providers/vault"
)

func main() {
	// Precedence: keep existing env, then .env, then Vault. 
	if err := envchain.Inject(dotenv.NewProvider(".env"), vault.NewProvider("kvv2/byuapi-persons-v4/dev/env-vars")); err != nil {
	    // could make provider errors fatal, but we're assuming that deployed environments
	    // will always have config and secrets injected before server start.
	    log.Printf("env injection warnings: %v", err)
	}

	// ... retrieve env vars with os.GetEnv or envchain.GetEnv
}

You can include any number of providers; only unset keys are written.

Vault provider requirements

The Vault provider is active when these are satisfied:

  • VAULT_ADDR — base URL for Vault (e.g., https://vault.byu.edu or https://vault.byu.edu/v1)
  • A token:
    • VAULT_TOKEN, or
    • ~/.vault-token (created by vault login)
  • Secret path you pass to vault.NewProvider(...) (KV v2 path, e.g. kvv2/<service-name>/dev/env-vars)
  • Optional: VAULT_NAMESPACE → sent as X-Vault-Namespace

Timeouts: Vault HTTP requests use a 10s timeout.

Context: The Vault provider uses a background context by default. To override:

p := vault.NewProvider("/app/web")
p.Context = ctx // set deadlines, cancellation, etc.
if err := envchain.Inject(p); err != nil { /* ... */ }

Or pass one shared context across all context-aware providers:

if err := envchain.InjectWithContext(ctx, dotenv.NewProvider(".env"), vault.NewProvider("/app/web")); err != nil {
	// handle joined provider errors
}

Reading Environment Variables with Defaults

Instead of manually checking for missing values, use GetEnv:

// GetEnv returns an EnvContainer with the looked-up value and ok state.
func GetEnv(key string) EnvContainer

// WithDefault returns the same container if the env key was set.
// If unset, it returns a container with the provided default value.
func (c EnvContainer) WithDefault(def string) EnvContainer {
    // ...
}

This lets you simplify configuration code:

package main

import (
	"fmt"
	"os"

	"github.com/stuft2/envchain"
)

func main() {
	// Example: PORT will default to 8080 if not set.
	port := envchain.GetEnv("PORT").WithDefault("8080").asString()
	addr := envchain.GetEnv("ADDR").WithDefault(":http").asString()

	fmt.Println("Starting server on", addr, "port", port)

	// Example with empty string (treated as set):
	_ = os.Setenv("DEBUG", "")
	debug := envchain.GetEnv("DEBUG").WithDefault("false").asString()
	fmt.Println("Debug mode =", debug)
}

External Dependencies

CI / Local Checks

CI runs on push and pull request via .github/workflows/ci.yml.

Run the same checks locally:

task fmt
task lint
task test

Troubleshooting

Symptom: vault: VAULT_ADDR is required to inject environment variables
Cause: -vault-path is set, but VAULT_ADDR is missing.
Fix: export VAULT_ADDR (for example, https://vault.byu.edu) or remove -vault-path.

Symptom: vault: VAULT_ADDR set but no token found (VAULT_TOKEN or ~/.vault-token)
Cause: Vault address is configured, but auth token is unavailable.
Fix: set VAULT_TOKEN or run vault login so ~/.vault-token exists.

Symptom: vault: invalid VAULT_ADDR "...": ...
Cause: VAULT_ADDR is malformed (missing scheme, invalid host, or invalid URL).
Fix: use a valid URL such as https://vault.byu.edu (or https://vault.byu.edu/v1).

Symptom: Usage: envchain [flags] -- command [args...]
Cause: command was not passed after --.
Fix: provide a command after separator, for example envchain -- env.

Symptom: envchain: failed to execute "..."
Cause: the command after -- is missing from PATH or not executable.
Fix: verify the executable name and run which <command> to confirm availability.

Symptom: expected value from .env/Vault is not applied
Cause: existing process env takes precedence over providers.
Fix: unset the key before running, for example:

export PORT=3000
envchain -- env | grep '^PORT='    # PORT=3000 (existing env wins)
unset PORT
envchain -- env | grep '^PORT='    # PORT from .env or Vault

Verbose mode note: -verbose logs provider flow and key names, not secret values. Secret-safe diagnostics and redaction guarantees will continue to improve as diagnostics features evolve.

Project Docs

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages