Skip to content

Centralize env vars with Viper#39

Open
carole-lavillonniere wants to merge 4 commits intomainfrom
carole/drg-529
Open

Centralize env vars with Viper#39
carole-lavillonniere wants to merge 4 commits intomainfrom
carole/drg-529

Conversation

@carole-lavillonniere
Copy link
Collaborator

@carole-lavillonniere carole-lavillonniere commented Feb 16, 2026

  • Introduces centralized environment variable management using Viper, replacing scattered os.Getenv() calls throughout the codebase, eliminating raw string env var names.
  • Introduces test/integration/env to do the same for integration tests.

@carole-lavillonniere
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@carole-lavillonniere carole-lavillonniere marked this pull request as ready for review February 17, 2026 12:12
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Introduces a centralized environment package (internal/env) and updates CLI startup and multiple packages/tests to read configuration from env.Vars instead of ad-hoc os.Getenv calls.

Changes

Cohort / File(s) Summary
Environment package
internal/env/env.go
Add Env struct, global Vars, and Init() using Viper to load LOCALSTACK_* vars and defaults (api_endpoint, web_app_url, keyring).
CLI startup
cmd/root.go
Call env.Init() in PersistentPreRunE before config.Init() so env is initialized at startup.
API & Auth callers
internal/api/client.go, internal/auth/auth.go, internal/auth/login.go, internal/auth/token_storage.go
Replace direct os.Getenv usages with env.Vars accessors (APIEndpoint, AuthToken, WebAppURL, Keyring); remove local fallbacks/hard-coded values.
Example env file
env.example
Add LOCALSTACK_API_ENDPOINT and LOCALSTACK_WEB_APP_URL examples and relocate/comment KEYRING=file as LOCALSTACK_KEYRING optional line.
Integration test helpers
test/integration/env/env.go
New test helper: Key constants, Get()/Require() and Environ with With()/Without() for composable test env manipulation.
Integration tests
test/integration/*.go (license, login_browser_flow, login_device_flow, main, start)
Switch tests to use the new test helper (env.Require, env.With, env.Without, env.Get) instead of direct os.Getenv and manual env assembly.
Makefile
Makefile
Change Darwin test invocation to set LOCALSTACK_KEYRING=file instead of KEYRING=file.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant CLI
participant Env as internal/env
participant Config as config
participant API as internal/api
participant Auth as internal/auth

CLI->>Env: env.Init()
Env-->>CLI: Vars populated (AuthToken, APIEndpoint, WebAppURL, Keyring)
CLI->>Config: config.Init()
API->>Env: read Vars.APIEndpoint
Auth->>Env: read Vars.AuthToken / Vars.WebAppURL / Vars.Keyring
API-->>CLI: use configured baseURL
Auth-->>CLI: use token/keyring/web app URL

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • silv-io
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the PR: centralizing environment variables management using Viper, which is the primary change across all modified files.
Description check ✅ Passed The description accurately explains the changeset by highlighting the introduction of centralized environment variable management using Viper and the creation of a test integration env package.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch carole/drg-529

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
internal/env/env.go (1)

16-16: Consider initializing Vars to a zero-value Env to prevent nil pointer panics.

If any code accesses env.Vars before Init() is called, it will panic with a nil pointer dereference. While the intended usage is to call Init() early in the application lifecycle, a defensive initialization provides a safer default.

💡 Suggested defensive initialization
-var Vars *Env
+var Vars = &Env{}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/env/env.go` at line 16, Vars is currently declared as a nil pointer
(var Vars *Env) which can cause nil-pointer panics if accessed before Init();
change its declaration to a defensive zero-value pointer (e.g., initialize Vars
to &Env{} at declaration) so callers can safely read default fields before
Init() runs, and ensure Init() still overwrites or reinitializes Vars as needed;
look for the Vars variable and the Init() function in the env package to make
this change.
test/integration/env/env.go (1)

65-68: With() appends without removing existing keys, which may not override as expected.

When duplicate environment variable entries exist, behavior is platform-dependent—often the first occurrence takes precedence. If an entry for key already exists in e, appending a new value won't reliably override it.

Consider removing the existing key before appending:

♻️ Suggested fix to ensure override behavior
 // With returns a copy of e with key=value appended.
 func (e Environ) With(key Key, value string) Environ {
-	return append(e, string(key)+"="+value)
+	return e.Without(key).append(string(key) + "=" + value)
 }
+
+func (e Environ) append(entry string) Environ {
+	return append(e, entry)
+}

Or more simply:

 // With returns a copy of e with key=value set, replacing any existing value.
 func (e Environ) With(key Key, value string) Environ {
-	return append(e, string(key)+"="+value)
+	filtered := e.Without(key)
+	return append(filtered, string(key)+"="+value)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/env/env.go` around lines 65 - 68, The With method on Environ
appends a new "key=value" entry but does not remove any existing entries for the
same key, so duplicates may not override as expected; update Environ.With to
filter out any existing entries whose prefix matches string(key)+"=" (using the
Key type) and then append the new "key=value" entry so the returned Environ
contains a single authoritative value for that key. Ensure you operate on the
receiver Environ (e) and return the filtered slice with the new entry appended
to preserve expected override semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@env.example`:
- Around line 1-11: The env example uses KEYRING but viper is configured with
the LOCALSTACK prefix and code calls viper.GetString("keyring"), so update the
example to export LOCALSTACK_KEYRING instead of KEYRING; change the commented
line in env.example (the KEYRING entry) to export LOCALSTACK_KEYRING (e.g. use
LOCALSTACK_KEYRING=file) so the env var matches viper's prefix and the
viper.GetString("keyring") call reads the intended value.

In `@internal/api/client.go`:
- Around line 68-71: NewPlatformClient dereferences env.Vars directly which can
panic if env.Init() hasn't run; update NewPlatformClient to guard against nil
env.Vars (or call a safe accessor) before using env.Vars.APIEndpoint and
fallback to a sensible default or trigger lazy env initialization; specifically
modify NewPlatformClient and any PlatformClient construction to use a nil-check
on env.Vars (or an env.GetAPIEndpoint()/env.MustInit() helper) so creating a
PlatformClient in tests or early library usage won't panic.

In `@internal/auth/token_storage.go`:
- Around line 48-50: Update the misleading inline comment above the KEYRING
check to reference the actual environment variable name used
(LOCALSTACK_KEYRING) so it matches env.Vars.Keyring; edit the comment near the
keyringConfig.AllowedBackends assignment in token_storage.go (the block that
checks env.Vars.Keyring == "file") to say "Force file backend if
LOCALSTACK_KEYRING env var is set to 'file'".

In `@test/integration/main_test.go`:
- Around line 65-67: Update the Makefile to set the new environment variable
name LOCALSTACK_KEYRING=file (replacing any KEYRING=file entry) so
env.Get(env.Keyring) in the test will see the file backend, and update the
comment near the test (around the env.Get(env.Keyring) check) to reference
LOCALSTACK_KEYRING instead of KEYRING; this ensures
keyringConfig.AllowedBackends = []keyring.BackendType{keyring.FileBackend} is
applied when running the integration tests.

---

Duplicate comments:
In `@internal/auth/auth.go`:
- Around line 38-39: The code reads env.Vars.AuthToken without guarding against
env.Vars being nil; update the accessor in internal/auth/auth.go to mirror the
central fix used in internal/api/client.go by checking that env.Vars is non-nil
before reading AuthToken (return a clear error if nil), or call the shared
helper that safely returns environment vars; ensure you reference env.Vars and
AuthToken when adding the nil-check so all env.Vars accessors follow the same
centralized pattern.

In `@internal/auth/login.go`:
- Around line 123-124: getWebAppURL currently returns env.Vars.WebAppURL without
a nil check; update getWebAppURL to guard against env.Vars being nil (same
central fix used in internal/api/client.go) by checking env.Vars and returning a
safe default (empty string or configured fallback) when nil, and apply the
identical nil-guard pattern to all other accessors that reference env.Vars to
ensure consistent behavior (identify accessors by function name such as
getWebAppURL and mirror the nil-check logic used in the api client fix).

---

Nitpick comments:
In `@internal/env/env.go`:
- Line 16: Vars is currently declared as a nil pointer (var Vars *Env) which can
cause nil-pointer panics if accessed before Init(); change its declaration to a
defensive zero-value pointer (e.g., initialize Vars to &Env{} at declaration) so
callers can safely read default fields before Init() runs, and ensure Init()
still overwrites or reinitializes Vars as needed; look for the Vars variable and
the Init() function in the env package to make this change.

In `@test/integration/env/env.go`:
- Around line 65-68: The With method on Environ appends a new "key=value" entry
but does not remove any existing entries for the same key, so duplicates may not
override as expected; update Environ.With to filter out any existing entries
whose prefix matches string(key)+"=" (using the Key type) and then append the
new "key=value" entry so the returned Environ contains a single authoritative
value for that key. Ensure you operate on the receiver Environ (e) and return
the filtered slice with the new entry appended to preserve expected override
semantics.

@carole-lavillonniere
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
internal/env/env.go (2)

21-21: The SetEnvKeyReplacer has no effect with current key names.

The replacer converts dots to underscores, but all your Viper keys already use underscores (auth_token, api_endpoint, etc.). If this is intentional for future flexibility (e.g., hierarchical config like api.endpoint), consider adding a brief comment. Otherwise, it can be removed to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/env/env.go` at line 21, SetEnvKeyReplacer call is ineffective
because Viper keys in this code use underscores (e.g., auth_token,
api_endpoint); either remove the
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) call or add a brief
comment above it clarifying it’s kept intentionally for future dot-to-underscore
mapping (e.g., supporting hierarchical keys like api.endpoint) so readers aren’t
confused; update the code around the viper.SetEnvKeyReplacer invocation (symbol:
viper.SetEnvKeyReplacer) accordingly.

16-16: Consider documenting or enforcing initialization order.

Vars is initialized as an empty &Env{} and only populated when Init() is called. Any code accessing Vars before Init() will silently receive empty strings. Since cmd/root.go calls env.Init() in PersistentPreRunE, this should be fine for the CLI flow, but it may be worth adding a brief doc comment noting that Init() must be called before accessing Vars.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/env/env.go` at line 16, Vars is currently created as an empty &Env{}
and can be read before Init(), so add a clear doc comment above the
package-level variable Vars stating that Init() must be called before any use;
to harden further, change Vars to nil by default and add a small accessor
function (e.g., GetVars() or MustVars()) that checks Vars != nil and returns it
or panics/errors with a clear message, and update callers to use that accessor
or ensure Init() is invoked early (Init(), Vars, GetVars/MustVars are the
symbols to update).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/env/env.go`:
- Line 21: SetEnvKeyReplacer call is ineffective because Viper keys in this code
use underscores (e.g., auth_token, api_endpoint); either remove the
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) call or add a brief
comment above it clarifying it’s kept intentionally for future dot-to-underscore
mapping (e.g., supporting hierarchical keys like api.endpoint) so readers aren’t
confused; update the code around the viper.SetEnvKeyReplacer invocation (symbol:
viper.SetEnvKeyReplacer) accordingly.
- Line 16: Vars is currently created as an empty &Env{} and can be read before
Init(), so add a clear doc comment above the package-level variable Vars stating
that Init() must be called before any use; to harden further, change Vars to nil
by default and add a small accessor function (e.g., GetVars() or MustVars())
that checks Vars != nil and returns it or panics/errors with a clear message,
and update callers to use that accessor or ensure Init() is invoked early
(Init(), Vars, GetVars/MustVars are the symbols to update).

Copy link
Member

@silv-io silv-io left a comment

Choose a reason for hiding this comment

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

LGTM! Just some ideas about how to handle cli env vars vs. container env vars


// Init initializes environment variable configuration
func Init() {
viper.SetEnvPrefix("LOCALSTACK")
Copy link
Member

Choose a reason for hiding this comment

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

When I did this for the jam project I was super annoyed that you can't set two prefixes. I'd love to have a LSTK prefix for the CLI and a LOCALSTACK prefix for the container. You'd then need to handle both manually.

Then the LOCALSTACK prefix would just contain things that configure the container and are defined in here and the LSTK prefix would just be for things like the Keyring.

Maybe it's worth it to investigate how to do that best in a future iteration. Could be that it is possible but I just missed it. WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm thinking all container-specific env vars should probably rather go in the config, wdyt? Otherwise it could get messy when having multiple containers.
But in any case for now I could rename our env vars to be prefixed with LSTK_ rather than LOCALSTACK_ since they are CLI-specific, makes sense?

Copy link
Member

Choose a reason for hiding this comment

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

yeah let's do LSTK instead, faster to type as well :P

Copy link
Member

Choose a reason for hiding this comment

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

nit: some maybe not necessary comments here. But I guess for hover mode in vim it's nice to see these

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That was way too much comments indeed. Removed.

envWithout("LOCALSTACK_AUTH_TOKEN"),
"LOCALSTACK_API_ENDPOINT="+mockServer.URL,
)
cmd.Env = env.Without(env.AuthToken).With(env.APIEndpoint, mockServer.URL)
Copy link
Member

Choose a reason for hiding this comment

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

praise: looks so clean 🤩

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/integration/env/env.go`:
- Around line 58-59: The With method on Environ currently appends a new
"key=value" string which can leave earlier entries for the same key and cause
non-deterministic behavior; modify Environ.With (the With function on type
Environ that accepts Key) to first remove any existing entries whose prefix
equals string(key)+"=" (filter the slice), then append the new
string(key)+"="+value so the environment deterministically contains only the
updated value for that key.

Comment on lines +58 to +59
func (e Environ) With(key Key, value string) Environ {
return append(e, string(key)+"="+value)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ensure With overrides deterministically by removing existing keys first.
Appending without dedup can leave multiple entries for the same key; depending on the consumer, earlier entries might win, causing flaky behavior.

🔧 Suggested fix
 func (e Environ) With(key Key, value string) Environ {
-	return append(e, string(key)+"="+value)
+	e = e.Without(key)
+	return append(e, string(key)+"="+value)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/env/env.go` around lines 58 - 59, The With method on Environ
currently appends a new "key=value" string which can leave earlier entries for
the same key and cause non-deterministic behavior; modify Environ.With (the With
function on type Environ that accepts Key) to first remove any existing entries
whose prefix equals string(key)+"=" (filter the slice), then append the new
string(key)+"="+value so the environment deterministically contains only the
updated value for that key.

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.

2 participants

Comments