Skip to content
Open
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
1 change: 0 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ Use `lstk setup <emulator>` to set up CLI integration for an emulator type:
- `lstk az start-interception` / `lstk az stop-interception` — Opt-in second mode: instead of the isolated dir, these mutate the user's **global** `~/.azure` so plain `az` (any terminal/script) targets LocalStack, then switch back. `start-interception` runs the same register → activate → `instance_discovery=false` → dummy-login flow against the global config (but does not touch global telemetry/survey prefs) and is independent of `lstk setup azure`. `stop-interception` switches the active cloud back to `AzureCloud` (override with `--cloud <name>`, validated against the live `az cloud list`) and re-enables instance discovery — but only if `LocalStack` is still the active cloud, to avoid clobbering an unrelated selection.

This naming avoids AWS-specific "profile" terminology and uses a clear verb for mutation operations.
The deprecated `lstk config profile` command still works but points users to `lstk setup aws`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do you think it's a good idea to add in CLAUDE.md the decision around not using Cobra's deprecated field, as explained in the PR description already?


The default `lstk az <args>` mode mirrors `lstk aws`: the Azure CLI has no `--endpoint-url`/`--profile`, so the only isolation knob is `AZURE_CONFIG_DIR`. Inside that isolated dir we register a custom cloud whose endpoints point at `https://azure.localhost.localstack.cloud:4566`, so `az` makes direct calls to LocalStack for Azure services (no HTTP(S) forward proxy in front of `az`). `core.instance_discovery=false` is required because `az` does not recognise the LocalStack host as a real Azure cloud. Adding a new Azure service that needs its own endpoint in `az`'s cloud config means extending the map in `internal/azureconfig/azureconfig.go::BuildCloudConfig`.

Expand Down
26 changes: 1 addition & 25 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,19 @@ import (
"fmt"

"github.com/localstack/lstk/internal/config"
"github.com/localstack/lstk/internal/env"
"github.com/localstack/lstk/internal/ui"
"github.com/spf13/cobra"
)

func newConfigCmd(cfg *env.Env) *cobra.Command {
func newConfigCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage configuration",
}
requireSubcommand(cmd)
cmd.AddCommand(newConfigProfileCmd(cfg))
cmd.AddCommand(newConfigPathCmd())
return cmd
}

func newConfigProfileCmd(cfg *env.Env) *cobra.Command {
return &cobra.Command{
Use: "profile",
Short: "Deprecated: use 'lstk setup aws' instead",
PreRunE: initConfig(nil),
RunE: func(cmd *cobra.Command, args []string) error {
appConfig, err := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}

if !isInteractiveMode(cfg) {
return fmt.Errorf("config profile requires an interactive terminal")
}

// Delegate to the same handler as "lstk setup aws"
return ui.RunConfigProfile(cmd.Context(), appConfig.Containers, cfg.LocalStackHost, false)
},
}
}

func newConfigPathCmd() *cobra.Command {
return &cobra.Command{
Use: "path",
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func NewRootCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra.C
newStatusCmd(cfg),
newLogsCmd(cfg),
newSetupCmd(cfg),
newConfigCmd(cfg),
newConfigCmd(),
newVolumeCmd(cfg),
newUpdateCmd(cfg),
newDocsCmd(),
Expand Down
2 changes: 1 addition & 1 deletion cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func newSetupAWSCmd(cfg *env.Env) *cobra.Command {
}

if isInteractiveMode(cfg) {
return ui.RunConfigProfile(cmd.Context(), appConfig.Containers, cfg.LocalStackHost, force)
return ui.RunSetupAWS(cmd.Context(), appConfig.Containers, cfg.LocalStackHost, force)
}

resolvedHost, dnsOK, err := awsconfig.ResolveProfileHost(cmd.Context(), appConfig.Containers, cfg.LocalStackHost)
Expand Down
4 changes: 2 additions & 2 deletions internal/awsconfig/awsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ func applyProfile(sink output.Sink, resolvedHost, configPath, credsPath string,
//
// When skipConfirm is true the confirmation prompt is bypassed and the profile is written directly.
//
// 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
// explicit is true for the user-invoked `lstk setup aws` command, 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.
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/run_awsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
"github.com/localstack/lstk/internal/output"
)

// RunConfigProfile runs the AWS profile setup flow with TUI output.
// RunSetupAWS runs the AWS profile setup flow with TUI output.
// It resolves the host from the AWS container config and runs the setup.
// When force is true, the confirmation prompt is skipped.
func RunConfigProfile(parentCtx context.Context, containers []config.ContainerConfig, localStackHost string, force bool) error {
func RunSetupAWS(parentCtx context.Context, containers []config.ContainerConfig, localStackHost string, force bool) error {
resolvedHost, dnsOK, err := awsconfig.ResolveProfileHost(parentCtx, containers, localStackHost)
if err != nil {
return err
Expand Down
55 changes: 2 additions & 53 deletions test/integration/awsconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,57 +208,6 @@ func TestStartEmitsNoteWhenAWSProfileIsPartial(t *testing.T) {
"profile prompt should not appear for a partial setup")
}

func TestConfigProfileCreatesAWSProfileWhenConfirmed(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("PTY not supported on Windows")
}
baseEnv, tmpHome := awsConfigEnv(t)

ctx := testContext(t)
cmd := exec.CommandContext(ctx, binaryPath(), "config", "profile")
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)
}()

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

// Press Y to confirm.
_, err = ptmx.Write([]byte("y"))
require.NoError(t, err)

err = cmd.Wait()
<-outputCh
require.NoError(t, err)

configContent, err := os.ReadFile(filepath.Join(tmpHome, ".aws", "config"))
require.NoError(t, err, "~/.aws/config should have been created")
assert.Contains(t, string(configContent), "[profile localstack]")
assert.Contains(t, string(configContent), "endpoint_url")

credsContent, err := os.ReadFile(filepath.Join(tmpHome, ".aws", "credentials"))
require.NoError(t, err, "~/.aws/credentials should have been created")
normalizedCreds := strings.Join(strings.Fields(string(credsContent)), " ")
assert.Contains(t, normalizedCreds, "[localstack]")
assert.Contains(t, normalizedCreds, "aws_access_key_id = test")
assert.Contains(t, normalizedCreds, "aws_secret_access_key = test")

assert.Contains(t, out.String(), "Created LocalStack profile in ~/.aws")
assert.NotContains(t, out.String(), "Skipped adding LocalStack AWS profile.")
}

func TestSetupAWSCreatesAWSProfileWhenConfirmed(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
Expand Down Expand Up @@ -363,15 +312,15 @@ func TestSetupAWSExitsNonZeroWhenProfileWriteFails(t *testing.T) {
assert.NotContains(t, out.String(), "Created LocalStack profile")
}

func TestConfigProfileDoesNotCreateAWSProfileWhenDeclined(t *testing.T) {
func TestSetupAWSDoesNotCreateAWSProfileWhenDeclined(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("PTY not supported on Windows")
}
baseEnv, tmpHome := awsConfigEnv(t)

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

ptmx, err := pty.Start(cmd)
Expand Down
10 changes: 8 additions & 2 deletions test/integration/az_interception_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,21 @@ func TestAzStopInterceptionNoOpWhenNotIntercepting(t *testing.T) {
workDir := azureWorkDir(t)
home := t.TempDir() // fresh ~/.azure: active cloud is the default AzureCloud

// Disable az telemetry: interception deliberately leaves the global config's telemetry
// prefs untouched, so without this the CLI spawns a detached background upload process
// that lingers past `az` and holds a handle in `home`, breaking t.TempDir() cleanup on
// Windows ("The process cannot access the file because it is being used by another process").
azEnv := env.With(env.Home, home).With(env.AzureCollectTelemetry, "false")

stdout, _, err := runLstk(t, testContext(t), workDir,
env.With(env.Home, home),
azEnv,
"az", "stop-interception",
)
require.NoError(t, err, "stop-interception must not fail when LocalStack is not active")
assert.Contains(t, stdout, "not the active Azure cloud")

// It must not have switched the active cloud to anything.
active, azErr := runAzRaw(t, testContext(t), env.With(env.Home, home), "cloud", "show", "--query", "name", "-o", "tsv")
active, azErr := runAzRaw(t, testContext(t), azEnv, "cloud", "show", "--query", "name", "-o", "tsv")
require.NoError(t, azErr)
assert.Equal(t, "AzureCloud", active)
}
Expand Down
5 changes: 5 additions & 0 deletions test/integration/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const (
OtelEndpoint Key = "OTEL_EXPORTER_OTLP_ENDPOINT"
AWSAccessKeyID Key = "AWS_ACCESS_KEY_ID"
AWSSecretAccessKey Key = "AWS_SECRET_ACCESS_KEY"
// AzureCollectTelemetry is the env form of the Azure CLI's `core.collect_telemetry`
// config key. Tests that run `az` against a temp HOME set it to "false" so the CLI
// does not spawn its detached background telemetry-upload process, which on Windows
// outlives `az` and keeps a handle inside the temp dir — breaking t.TempDir() cleanup.
AzureCollectTelemetry Key = "AZURE_CORE_COLLECT_TELEMETRY"

@anisaoshafi anisaoshafi Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what was the reason for the az related code changes in this PR, like this. seems like outside the scope?

updated: actually entire commit 28774fa seems outside the scope of this PR. Isn't this already handled in this newer PR? in that case can we remove this from here to keep things clean? 🙌🏼

)

// UnreachableAnalyticsEndpoint is a closed local port used as the default
Expand Down
1 change: 1 addition & 0 deletions test/integration/exit_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestInvalidUsageExitsNonZero(t *testing.T) {
{"unknown flag on root", []string{"--bogus-flag-xyz"}, "unknown flag"},
{"unknown command", []string{"bogus-command"}, "unknown command"},
{"unknown config subcommand", []string{"config", "bogus"}, `unknown command "bogus"`},
{"removed config profile subcommand", []string{"config", "profile"}, `unknown command "profile"`},
{"unknown setup subcommand", []string{"setup", "bogus"}, `unknown command "bogus"`},
{"unknown volume subcommand", []string{"volume", "bogus"}, `unknown command "bogus"`},
{"unknown snapshot subcommand", []string{"snapshot", "bogus"}, `unknown command "bogus"`},
Expand Down
Loading