Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions internal/awsconfig/awsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func EnsureProfile(ctx context.Context, sink output.Sink, interactive bool, reso
return nil
}
if interactive && !configOK && !credsOK {
return Setup(ctx, sink, resolvedHost, status)
return Setup(ctx, sink, resolvedHost, status, false)
}

emitMissingProfileNote(sink)
Expand All @@ -246,16 +246,21 @@ func EnsureProfile(ctx context.Context, sink output.Sink, interactive bool, reso
// Setup checks for the localstack AWS profile and prompts to create or update it if needed.
// resolvedHost must be a host:port string (e.g. "localhost.localstack.cloud:4566").
// status is passed in from EnsureProfile to avoid re-checking the profile status.
func Setup(ctx context.Context, sink output.Sink, resolvedHost string, status profileStatus) error {
//
// explicit is true for the user-invoked `lstk setup aws` / `lstk config profile`
// commands, where writing the profile is the command's whole purpose, so a write
// failure must surface a non-zero exit. It is false for the best-effort post-start
// convenience flow (EnsureProfile during `lstk start`), where a write failure must
// only warn and must not abort an already-running emulator.
func Setup(ctx context.Context, sink output.Sink, resolvedHost string, status profileStatus, explicit bool) error {
if !status.anyNeeded() {
sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: "LocalStack AWS profile is already configured."})
return nil
}

configPath, credsPath, err := awsPaths()
if err != nil {
sink.Emit(output.MessageEvent{Severity: output.SeverityWarning, Text: fmt.Sprintf("could not determine AWS config paths: %v", err)})
return nil
return reportSetupErr(sink, "could not determine AWS config paths", err, explicit)
}

responseCh := make(chan output.InputResponse, 1)
Expand All @@ -276,14 +281,12 @@ func Setup(ctx context.Context, sink output.Sink, resolvedHost string, status pr
}
if status.configNeeded {
if err := writeConfigProfile(configPath, resolvedHost); err != nil {
sink.Emit(output.MessageEvent{Severity: output.SeverityWarning, Text: fmt.Sprintf("could not update ~/.aws/config: %v", err)})
return nil
return reportSetupErr(sink, "could not update ~/.aws/config", err, explicit)
}
}
if status.credsNeeded {
if err := writeCredsProfile(credsPath); err != nil {
sink.Emit(output.MessageEvent{Severity: output.SeverityWarning, Text: fmt.Sprintf("could not update ~/.aws/credentials: %v", err)})
return nil
return reportSetupErr(sink, "could not update ~/.aws/credentials", err, explicit)
}
}
if status.configNeeded && status.credsNeeded {
Expand All @@ -299,3 +302,20 @@ func Setup(ctx context.Context, sink output.Sink, resolvedHost string, status pr

return nil
}

// reportSetupErr surfaces a profile-setup failure according to how setup was invoked.
// For the explicitly-invoked command it emits a structured error and returns a
// SilentError so the process exits non-zero; for the best-effort post-start flow it
// warns and returns nil so an already-running emulator's start is not aborted.
func reportSetupErr(sink output.Sink, msg string, err error, explicit bool) error {
if explicit {
sink.Emit(output.ErrorEvent{
Title: "Could not set up the LocalStack AWS profile",
Summary: fmt.Sprintf("%s: %v", msg, err),
Actions: []output.ErrorAction{{Label: "Check the permissions of ~/.aws, then re-run:", Value: "lstk setup aws"}},
})
return output.NewSilentError(err)
}
sink.Emit(output.MessageEvent{Severity: output.SeverityWarning, Text: fmt.Sprintf("%s: %v", msg, err)})
return nil
}
2 changes: 1 addition & 1 deletion internal/ui/run_awsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ func RunConfigProfile(parentCtx context.Context, containers []config.ContainerCo
if err != nil {
return err
}
return awsconfig.Setup(ctx, sink, resolvedHost, status)
return awsconfig.Setup(ctx, sink, resolvedHost, status, true)
})
}
53 changes: 53 additions & 0 deletions test/integration/awsconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,59 @@ func TestSetupAWSCreatesAWSProfileWhenConfirmed(t *testing.T) {
assert.NotContains(t, out.String(), "Skipped adding LocalStack AWS profile.")
}

// TestSetupAWSExitsNonZeroWhenProfileWriteFails guards DEVX-941. Writing the
// profile is the whole purpose of `lstk setup aws`, but the command used to emit a
// warning and return nil when the write failed — exiting 0 and masking the failure
// from users, CI, and agents. We make ~/.aws read-only so CheckProfileStatus still
// sees the profile files as absent (the prompt appears) but the actual write fails,
// then confirm the prompt and assert a non-zero exit.
func TestSetupAWSExitsNonZeroWhenProfileWriteFails(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("PTY not supported on Windows")
}
if os.Geteuid() == 0 {
t.Skip("root bypasses directory permissions, so the profile write would not fail")
}
baseEnv, tmpHome := awsConfigEnv(t)

// A read-only ~/.aws keeps the profile files absent (so the prompt still appears)
// while making their creation fail inside upsertSection's SaveTo.
awsDir := filepath.Join(tmpHome, ".aws")
require.NoError(t, os.MkdirAll(awsDir, 0500))
// Restore write permission before t.TempDir cleanup so the dir can be removed.
t.Cleanup(func() { _ = os.Chmod(awsDir, 0700) })

ctx := testContext(t)
cmd := exec.CommandContext(ctx, binaryPath(), "setup", "aws")
cmd.Env = baseEnv

ptmx, err := pty.Start(cmd)
require.NoError(t, err, "failed to start command in PTY")
defer func() { _ = ptmx.Close() }()

out := &syncBuffer{}
outputCh := make(chan struct{})
go func() {
_, _ = io.Copy(out, ptmx)
close(outputCh)
}()

require.Eventually(t, func() bool {
return bytes.Contains(out.Bytes(), []byte(awsSetupPrompt))
}, 2*time.Minute, 200*time.Millisecond, "AWS profile prompt should appear")

_, err = ptmx.Write([]byte("y"))
require.NoError(t, err)

err = cmd.Wait()
<-outputCh
requireExitCode(t, 1, err)

assert.Contains(t, out.String(), "Could not set up the LocalStack AWS profile")
assert.NotContains(t, out.String(), "Created LocalStack profile")
}

func TestConfigProfileDoesNotCreateAWSProfileWhenDeclined(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
Expand Down
Loading