Skip to content

Added environment variable expansion for tunnel Host field in config#38

Open
nickman wants to merge 5 commits into
alebeck:mainfrom
nickman:env-vars
Open

Added environment variable expansion for tunnel Host field in config#38
nickman wants to merge 5 commits into
alebeck:mainfrom
nickman:env-vars

Conversation

@nickman
Copy link
Copy Markdown

@nickman nickman commented May 22, 2026

Expand environment variables in tunnel fields

Summary

Adds support for environment variable references (${VAR_NAME}) in tunnel configuration fields, including default values via ${VAR:-default} syntax. This allows users to dynamically configure tunnels without hardcoding values in the config file.

Supported Fields

Environment variable expansion is now supported in the following tunnel fields:

  • name
  • host
  • local
  • remote
  • user
  • identity
  • group

Motivation

Users may need to reference hostnames or connection details that vary across environments (e.g., dev vs. prod, CI pipelines, or shared configs among team members). Supporting env var expansion in the supported fields enables more flexible and portable configurations.

Examples

Basic Expansion:

[[tunnels]]
name = "prod-db"
host = "${DB_HOST}"
local = 5432
remote = "localhost:5432"

With default values:

[[tunnels]]
name = "${TUNNEL_NAME:-staging-db}"
host = "${DB_HOST:-localhost}"
local = "${LOCAL_PORT:-5433}"
remote = "${REMOTE_ADDR:-localhost:5432}"
user = "${DB_USER:-postgres}"
identity = "${SSH_KEY:-/home/nicholas/.ssh/id_ed25519}"
group = "${TUNNEL_GROUP:-databases}"

If DB_HOST is set to prod-db.internal.example.com, the tunnel connects to that host.
If DB_HOST is unset or empty, the tunnel falls back to localhost.

Changes

  • internal/config/config.go — Added expandWithDefault() function that supports ${VAR:-default} syntax
    After loading the config and setting keep-alive defaults, expand environment variable references in all supported tunnel fields using os.Expand with the custom mapping function
  • internal/config/config_test.go (new) — Unit tests covering:
    • TestEnvVarExpansionInHost — Single and multiple env var references in host field
    • TestEnvVarExpansionUnsetVar — Unset env vars resolve to empty string
    • TestEnvVarExpansionWithDefault — Default values via ${VAR:-default} syntax
    • TestEnvVarExpansionAllFields — Env var expansion in all supported fields
    • TestEnvVarExpansionAllFieldsWithDefaults — Default values for all fields
    • TestEnvVarExpansionMixedFields — Mix of set vars, defaults, and empty values

Behavior

  • Uses Go's os.Expand which supports ${VAR} and $VAR syntax
  • Supports ${VAR:-default} — if the variable is unset or empty, the default value is used
  • Empty default (${VAR:-}) is valid and results in empty string when var is unset
  • Unset variables without a default are replaced with an empty string (consistent with shell behavior)
  • Expansion occurs after config parsing but before validation, so expanded values must still meet validation requirements (e.g., tunnel names cannot contain spaces)

@nickman
Copy link
Copy Markdown
Author

nickman commented May 22, 2026

I should add...
I was motivated to implement this because I have a combination of tunnel configurations.
Some are relatively static and un-changing.
Others are based on AWS EC2 instances that are constantly changing, but have common tags, so I have a script that generates a shell script that exports name=IP for each matching EC2 instance, so being able to reference an env-var in the boring toml is super helpful.

@alebeck
Copy link
Copy Markdown
Owner

alebeck commented May 23, 2026

Hi @nickman, thanks for your contribution, I think it's a good idea! However, before implementing/merging anything, let's discuss a bit what the scope should be. Few questions arise:

  • An env variable would be evaluated during tunnel opening and persists through re-connections, does this not diminish the utility of such dynamic configs? I.e. in your case if the EC2 instance changes you need to manually reconnect.
  • Why not include Host, User, IdentityFile, LocalAddress and RemoteAddress (I dont think we should include Name and Group for that matter)
  • Silent empty string on unset var: This should probably raise during opening a tunnel. Additionally on could consider ${VAR:-default} syntax.

Wdyt?

@nickman
Copy link
Copy Markdown
Author

nickman commented May 23, 2026

You make excellent points.
In the order of your comments:

  1. I had not really thought about this. I did not consider the state management aspects. My assumption was that the env.vars would be resolved on any operation that reads the Host field.
  2. At first, I thought I would cover all string (and StringOrInt) fields, but I noted you asked for small PRs, so I figured I would start with one field.
  3. Good point. To make the behaviour deterministic, I suggest empty string on unset var, but also support the default syntax. I will amend the PR.

For item #1, I will think on this. It's working nicely for me, but I see there could be issues there.

@alebeck
Copy link
Copy Markdown
Owner

alebeck commented May 24, 2026

Great points, but actually I think 1. is fine. Reconnection is mostly for recovering from transient network failures, in which case we wouldn't want a silent switch of (e.g.) host anyway. For 2. that makes sense but I think in this case it's ok for consistency reasons, and actually useful. For 3. maybe we can start without default syntax to keep it simple, we can still iterate on top of it. Let me know if you have any questions and thanks!

@nickman
Copy link
Copy Markdown
Author

nickman commented May 26, 2026

Done.

@alebeck
Copy link
Copy Markdown
Owner

alebeck commented May 29, 2026

Thank you! I should get to it soon.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds environment-variable expansion support for selected tunnel configuration fields during config load, including a ${VAR:-default} fallback syntax, and introduces fixture-based unit tests for the behavior.

Changes:

  • Expand env vars in tunnel host, user, identity, local, and remote fields via os.Expand with custom ${VAR:-default} handling.
  • Add expandWithDefault() to support default values when env vars are unset/empty.
  • Add config fixtures and unit tests validating expansion, unset behavior, and default fallbacks (including an explicit non-expansion test for name/group).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/config/config.go Performs env var expansion during Load() and adds ${VAR:-default} mapping helper.
internal/config/config_test.go Adds fixture loader and tests covering env var expansion and default handling.
test/testdata/config/expand/vars.toml Fixture for multi-field expansion.
test/testdata/config/expand/unset.toml Fixture for unset-var expansion to empty string.
test/testdata/config/expand/literal_fields.toml Fixture asserting name/group remain literal (no expansion).
test/testdata/config/expand/defaults.toml Fixture for ${VAR:-fallback} behavior across set/empty/unset vars.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/config/config.go
Comment thread internal/config/config_test.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants