Skip to content

Set up LocalStack AWS profile#92

Open
carole-lavillonniere wants to merge 3 commits intomainfrom
carole/drg-565
Open

Set up LocalStack AWS profile#92
carole-lavillonniere wants to merge 3 commits intomainfrom
carole/drg-565

Conversation

@carole-lavillonniere
Copy link
Collaborator

@carole-lavillonniere carole-lavillonniere commented Mar 10, 2026

Auto-configure AWS credentials for new users to immediately execute AWS CLI commands against LocalStack.
Closes DRG-565

  • Adds internal/awsconfig package to manage the LocalStack AWS profile in ~/.aws/config and ~/.aws/credentials
  • Adds internal/endpoint package with DNS-aware host resolution (falls back to 127.0.0.1 when DNS rebind protection blocks localhost.localstack.cloud). Can be overwritten with env var LOCALSTACK_HOST for consistency with container
  • Post-start hook mechanism in container/start.go to initiate the profile setup automatically on lstk start: prompts in interactive mode, emits a note in non-interactive mode
  • Profile content validation: detects stale/incorrect profiles and prompts to update only the files that need it, not both unconditionally
  • Prompt scope is specific: tells the user exactly which files (~/.aws/config and/or ~/.aws/credentials) will be modified
image

@carole-lavillonniere carole-lavillonniere changed the title feat(awsconfig): set up LocalStack AWS profile on start Set up LocalStack AWS profile Mar 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Warning

Rate limit exceeded

@carole-lavillonniere has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 55 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 8e2d4033-65cb-43fb-9564-030140ce032c

📥 Commits

Reviewing files that changed from the base of the PR and between 35e8996 and 1024e41.

📒 Files selected for processing (6)
  • cmd/non_interactive_test.go
  • internal/awsconfig/awsconfig.go
  • internal/awsconfig/awsconfig_test.go
  • internal/awsconfig/ini.go
  • internal/ui/app_test.go
  • test/integration/awsconfig_test.go
📝 Walkthrough

Walkthrough

Threads a LocalStackHost value through CLI/UI/container flows, adds DNS-resilient LocalStack host resolution, runs per-emulator post-start setups (AWS profile setup), implements awsconfig and INI helpers, and adds unit and integration tests for AWS profile management.

Changes

Cohort / File(s) Summary
Command & UI
cmd/root.go, internal/ui/run.go
Propagated new localStackHost parameter into CLI/UI entry points; use internal/endpoint.ResolveHost for host resolution and pass localStackHost into container start.
Container startup
internal/container/start.go, internal/container/start_test.go
Extended Start(...) signature to accept localStackHost; added post-start orchestration to run per-emulator setups and invoke awsconfig.Setup for AWS-type emulators after containers start.
AWS config package
internal/awsconfig/awsconfig.go, internal/awsconfig/ini.go, internal/awsconfig/awsconfig_test.go
New package to detect/update LocalStack AWS profile: status checks, interactive prompts, INI upsert helpers using gopkg.in/ini.v1, and comprehensive unit tests.
Endpoint resolution
internal/endpoint/endpoint.go
New DNS-resilient host resolution API: ResolveHost(port, override) (host string, dnsOK bool) and exported Hostname constant.
Environment wiring & deps
internal/env/env.go, go.mod
Added LocalStackHost and consolidated DisableEvents on Env; added gopkg.in/ini.v1 dependency.
Integration tests & test helpers
test/integration/awsconfig_test.go, test/integration/env/env.go
Added PTY-driven integration tests for interactive/non-interactive AWS profile setup and Home env key for test helpers.
Minor tests/cleanup
cmd/non_interactive_test.go, internal/ui/app_test.go
Small test control-flow adjustments (early returns added after t.Fatal calls).

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant UI as internal/ui.Run
    participant Container as internal/container.Start
    participant Orch as Post-Start Orchestrator
    participant AWSSetup as internal/awsconfig.Setup
    participant Files as "~/.aws files"

    User->>UI: Start(localStackHost)
    UI->>Container: Start(..., localStackHost)
    Container->>Container: launch containers
    Container->>Orch: runPostStartSetups(types, firstContainer, localStackHost)
    Orch->>AWSSetup: Setup(ctx, sink, interactive, port, localStackHost)
    AWSSetup->>Files: Check config & credentials
    alt missing or stale profile
        AWSSetup->>User: Prompt (if interactive)
        User-->>AWSSetup: Confirm
        AWSSetup->>Files: Write/update profile sections
    end
    AWSSetup-->>Orch: result
    Orch-->>Container: post-start complete
    Container-->>User: Ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • silv-io
  • anisaoshafi
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Set up LocalStack AWS profile' accurately and concisely summarizes the main objective of the pull request: automating AWS profile configuration for LocalStack users.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the AWS profile auto-configuration feature, DNS-aware host resolution, post-start hooks, and profile validation logic.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 3

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

25-28: Avoid a mutable package-level map for credential defaults.

credentialsKeys is shared mutable state inside a domain package. Keeping these expected pairs local to the call sites, or returning a fresh map from a helper, removes the hidden global dependency.

As per coding guidelines, "Avoid package-level global variables. Use constructor functions that return fresh instances and inject dependencies explicitly."

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

In `@internal/awsconfig/awsconfig.go` around lines 25 - 28, The package-level
mutable map credentialsKeys should be removed and replaced with a factory that
returns a fresh map or by inlining the literal at call sites; create a function
(e.g., newCredentialsKeys or buildCredentialsDefaults) that returns
map[string]string{"aws_access_key_id":"test","aws_secret_access_key":"test"} and
update all usages to call that function so each caller gets a fresh instance
instead of sharing global state (or move the literal into the local scope where
it's used), ensuring no package-level mutable variables remain.
test/integration/awsconfig_test.go (1)

77-81: Assert both credential lines explicitly.

This still passes if only aws_access_key_id = test is written. Checking aws_secret_access_key = test as well makes the test catch incomplete credentials files.

Suggested assertion tightening
 	assert.Contains(t, string(credsContent), "[localstack]")
-	assert.Contains(t, string(credsContent), "aws_access_key_id")
-	assert.Contains(t, string(credsContent), "= test")
+	assert.Contains(t, string(credsContent), "aws_access_key_id = test")
+	assert.Contains(t, string(credsContent), "aws_secret_access_key = test")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/awsconfig_test.go` around lines 77 - 81, The test currently
only asserts that the credentials file contains "aws_access_key_id" and a
generic "= test"; update the assertions to explicitly check for both credential
lines by asserting credsContent (from reading filepath.Join(tmpHome, ".aws",
"credentials")) contains "aws_access_key_id = test" and also contains
"aws_secret_access_key = test" so the test fails if either key is missing or
incomplete.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/awsconfig/awsconfig.go`:
- Around line 30-45: The isValidLocalStackEndpoint function currently checks
fixed string prefixes and misses non-default LOCALSTACK_HOST values and
malformed ports; update it to parse the input URL (using net/url.Parse) and
compare the parsed host and port against the resolved host and port produced by
Setup (use the same resolved hostname/port variables that Setup returns or
stores), i.e., extract u.Hostname() and u.Port() and validate equality with
Setup's host and port rather than relying on prefix matching; also return false
on parse errors or missing port to avoid accepting malformed endpoints.

In `@internal/awsconfig/ini.go`:
- Around line 28-49: upsertSection currently writes credentials via
ini.File.SaveTo(path) which can create a world-readable file; after saving the
file call os.Chmod(path, 0600) and return any chmod error (i.e., after
f.SaveTo(path) add err = os.Chmod(path, 0600) and handle it), or alternatively
open the destination with os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
0600) and write the INI contents to that file so the created file has explicit
0600 permissions; ensure errors from Chmod or OpenFile/write are returned so
upsertSection fails on permission issues.

In `@test/integration/awsconfig_test.go`:
- Around line 117-135: The test asserts absence of the profile prompt too early;
move the NotContains assertion to after the PTY reader finishes draining so we
check the final output. Keep the existing require.Eventually check for
bytes.Contains(out.Bytes(), []byte(" ready")) but change the sequence so you
call cmd.Process.Kill(), cmd.Wait(), then block on <-outputCh (the goroutine
that does io.Copy from ptmx into the syncBuffer out) and only after that run
assert.NotContains(t, out.String(), "Set up LocalStack AWS profile") to ensure
the post-start AWS profile check had time to run.

---

Nitpick comments:
In `@internal/awsconfig/awsconfig.go`:
- Around line 25-28: The package-level mutable map credentialsKeys should be
removed and replaced with a factory that returns a fresh map or by inlining the
literal at call sites; create a function (e.g., newCredentialsKeys or
buildCredentialsDefaults) that returns
map[string]string{"aws_access_key_id":"test","aws_secret_access_key":"test"} and
update all usages to call that function so each caller gets a fresh instance
instead of sharing global state (or move the literal into the local scope where
it's used), ensuring no package-level mutable variables remain.

In `@test/integration/awsconfig_test.go`:
- Around line 77-81: The test currently only asserts that the credentials file
contains "aws_access_key_id" and a generic "= test"; update the assertions to
explicitly check for both credential lines by asserting credsContent (from
reading filepath.Join(tmpHome, ".aws", "credentials")) contains
"aws_access_key_id = test" and also contains "aws_secret_access_key = test" so
the test fails if either key is missing or incomplete.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 4cb6a292-a8c7-4d5a-b552-fad73196ebeb

📥 Commits

Reviewing files that changed from the base of the PR and between c04da9c and b3b4457.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • cmd/root.go
  • go.mod
  • internal/awsconfig/awsconfig.go
  • internal/awsconfig/awsconfig_test.go
  • internal/awsconfig/ini.go
  • internal/container/start.go
  • internal/container/start_test.go
  • internal/endpoint/endpoint.go
  • internal/env/env.go
  • internal/ui/run.go
  • test/integration/awsconfig_test.go
  • test/integration/env/env.go

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 (1)
test/integration/awsconfig_test.go (1)

101-107: Pre-written endpoint URL may not match resolved host, causing test flakiness.

The pre-configured endpoint_url = http://127.0.0.1:4566 might not match the actual resolved LocalStack host. Per the PR description, the endpoint package falls back to 127.0.0.1 only when DNS rebind protection blocks localhost.localstack.cloud. If DNS resolution succeeds in the CI/test environment, the resolved host would be localhost.localstack.cloud:4566, triggering a config update prompt and failing the assertion.

Consider using the same host resolution logic or a deterministic endpoint that will always match:

 	require.NoError(t, os.WriteFile(filepath.Join(awsDir, "config"),
-		[]byte("[profile localstack]\nregion = us-east-1\noutput = json\nendpoint_url = http://127.0.0.1:4566\n"), 0600))
+		[]byte("[profile localstack]\nregion = us-east-1\noutput = json\nendpoint_url = http://localhost.localstack.cloud:4566\n"), 0600))

Alternatively, set LOCALSTACK_HOST=127.0.0.1:4566 in the test environment to force consistent host resolution.

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

In `@test/integration/awsconfig_test.go` around lines 101 - 107, The test writes a
hardcoded endpoint_url (http://127.0.0.1:4566) into the AWS config which can
mismatch the runtime-resolved LocalStack host and make the test flaky; update
the pre-written config generation (the block that builds awsDir using tmpHome
and writes the "config" file) to use the same host resolution as production
(either call the project’s LocalStack host resolver function or read
LOCALSTACK_HOST) and write endpoint_url using that resolved host, or
alternatively set LOCALSTACK_HOST=127.0.0.1:4566 for the test environment so the
pre-written config (the file written via filepath.Join(awsDir, "config")) always
matches the resolved host.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/integration/awsconfig_test.go`:
- Around line 101-107: The test writes a hardcoded endpoint_url
(http://127.0.0.1:4566) into the AWS config which can mismatch the
runtime-resolved LocalStack host and make the test flaky; update the pre-written
config generation (the block that builds awsDir using tmpHome and writes the
"config" file) to use the same host resolution as production (either call the
project’s LocalStack host resolver function or read LOCALSTACK_HOST) and write
endpoint_url using that resolved host, or alternatively set
LOCALSTACK_HOST=127.0.0.1:4566 for the test environment so the pre-written
config (the file written via filepath.Join(awsDir, "config")) always matches the
resolved host.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: c8e4055c-50ad-4b5e-84dd-dccc388a3723

📥 Commits

Reviewing files that changed from the base of the PR and between 4e403b5 and 6708175.

📒 Files selected for processing (4)
  • internal/awsconfig/awsconfig.go
  • internal/awsconfig/awsconfig_test.go
  • internal/awsconfig/ini.go
  • test/integration/awsconfig_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/awsconfig/awsconfig.go

@carole-lavillonniere carole-lavillonniere force-pushed the carole/drg-565 branch 2 times, most recently from 35e8996 to bfbb649 Compare March 10, 2026 15:03
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.

♻️ Duplicate comments (1)
test/integration/awsconfig_test.go (1)

124-128: ⚠️ Potential issue | 🟡 Minor

Potential race: " ready" matches the early log line, not post-start completion.

The substring " ready" appears in "Waiting for localstack-aws to be ready..." which is logged before the container is actually ready and before post-start setup runs. This could cause the test to proceed and kill the process before the AWS profile check has a chance to run (or not run).

Consider waiting for a more specific marker like "localstack-aws ready" or the full "ready (containerId:" pattern to ensure post-start setup has completed.

Suggested fix
 	require.Eventually(t, func() bool {
-		return bytes.Contains(out.Bytes(), []byte(" ready"))
+		return bytes.Contains(out.Bytes(), []byte("localstack-aws ready"))
 	}, 2*time.Minute, 200*time.Millisecond, "container should become ready")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/awsconfig_test.go` around lines 124 - 128, The test
currently waits for a generic substring by calling require.Eventually with
bytes.Contains(out.Bytes(), []byte(" ready")), which can match the early log
"Waiting for localstack-aws to be ready..." and cause a race; update the
predicate used in the require.Eventually call (the bytes.Contains check) to look
for a more specific post-start completion marker such as "localstack-aws ready"
or the full "ready (containerId:" pattern so the wait only succeeds after
post-start setup has finished (modify the predicate in the require.Eventually
block in test/integration/awsconfig_test.go).
🧹 Nitpick comments (1)
internal/awsconfig/awsconfig_test.go (1)

222-257: Add test case for "neither needed" scenario.

The profileStatus{configNeeded: false, credsNeeded: false} case is missing. While filesToModify() likely returns an empty slice, explicitly testing this documents the expected behavior and guards against regressions.

Suggested addition
 	tests := []struct {
 		name      string
 		status    profileStatus
 		wantFiles []string
 	}{
+		{
+			name:      "neither needed",
+			status:    profileStatus{configNeeded: false, credsNeeded: false},
+			wantFiles: []string{},
+		},
 		{
 			name:      "both needed",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/awsconfig/awsconfig_test.go` around lines 222 - 257, Add a test case
to TestFilesToModify for the scenario profileStatus{configNeeded:false,
credsNeeded:false} that asserts filesToModify() returns an empty slice; update
the tests table (in TestFilesToModify) to include a new entry with name "neither
needed" and wantFiles set to []string{}, and in the existing loop the current
length and element checks will verify the empty result (or you can explicitly
assert len(got)==0) so the behavior of profileStatus.filesToModify is documented
and guarded against regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/integration/awsconfig_test.go`:
- Around line 124-128: The test currently waits for a generic substring by
calling require.Eventually with bytes.Contains(out.Bytes(), []byte(" ready")),
which can match the early log "Waiting for localstack-aws to be ready..." and
cause a race; update the predicate used in the require.Eventually call (the
bytes.Contains check) to look for a more specific post-start completion marker
such as "localstack-aws ready" or the full "ready (containerId:" pattern so the
wait only succeeds after post-start setup has finished (modify the predicate in
the require.Eventually block in test/integration/awsconfig_test.go).

---

Nitpick comments:
In `@internal/awsconfig/awsconfig_test.go`:
- Around line 222-257: Add a test case to TestFilesToModify for the scenario
profileStatus{configNeeded:false, credsNeeded:false} that asserts
filesToModify() returns an empty slice; update the tests table (in
TestFilesToModify) to include a new entry with name "neither needed" and
wantFiles set to []string{}, and in the existing loop the current length and
element checks will verify the empty result (or you can explicitly assert
len(got)==0) so the behavior of profileStatus.filesToModify is documented and
guarded against regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 901ee45c-c8f5-487e-a177-dd38cdcb7e64

📥 Commits

Reviewing files that changed from the base of the PR and between 6708175 and 35e8996.

📒 Files selected for processing (6)
  • cmd/non_interactive_test.go
  • internal/awsconfig/awsconfig.go
  • internal/awsconfig/awsconfig_test.go
  • internal/awsconfig/ini.go
  • internal/ui/app_test.go
  • test/integration/awsconfig_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/awsconfig/awsconfig.go
  • internal/awsconfig/ini.go

Copy link
Contributor

@anisaoshafi anisaoshafi left a comment

Choose a reason for hiding this comment

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

LGTM, my manual tests worked as expected 🎉

}
// Use a "test." subdomain: *.localhost.localstack.cloud has wildcard DNS that resolves
// to 127.0.0.1, so any subdomain works as a probe without hitting the actual service.
addrs, err := net.LookupHost("test." + Hostname)
Copy link
Contributor

Choose a reason for hiding this comment

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

small FYI: I had endpoint_url http://127.0.0.1:4566 in my profile, but it asked me to override the profile (with http://localhost.localstack.cloud:4566), if I skip it it will keep asking me each time I start lstk again.
I was wondering if we want to prompt the user to override their profile if they have http://127.0.0.1:4566 or http://localhost which also work fine (if I remember correctly localstack toolkit doesn't prompt in this case).
I'm fine with how this looks now, no need to overcomplicate the code by adding another condition.

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!

Comment on lines 84 to 88
if isInteractiveMode(cfg) {
return ui.Run(ctx, rt, version.Version(), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL)
return ui.Run(ctx, rt, version.Version(), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, cfg.LocalStackHost)
}
return container.Start(ctx, rt, output.NewPlainSink(os.Stdout), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, false)
return container.Start(ctx, rt, output.NewPlainSink(os.Stdout), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, false, cfg.LocalStackHost)
}
Copy link
Member

Choose a reason for hiding this comment

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

thought: Both of these have quite a long list of parameters now. Is there a way to do this more nicely? 🤔

}

var dnsOK bool
localStackHost, dnsOK = endpoint.ResolveHost(port, localStackHost)
Copy link
Member

Choose a reason for hiding this comment

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

nit: we're resolving this also in the ui.Run. Maybe we can unify that somehow?

}
}
output.EmitSuccess(sink, fmt.Sprintf("LocalStack AWS profile written to %s", files))
output.EmitNote(sink, fmt.Sprintf("Try: aws s3 mb s3://test --profile %s", profileName))
Copy link
Member

Choose a reason for hiding this comment

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

praise: YASSS! ❤️ This will resolve DRG-363.

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.

4 participants