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
6 changes: 6 additions & 0 deletions cmd/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ Examples:
return output.NewSilentError(err)
}

// --help/-h never contacts LocalStack, so it runs directly without
// requiring Docker or a running emulator (DEVX-1002).
if awscli.IsHelp(passthrough) {
return awscli.Exec(cmd.Context(), "", false, os.Stdout, os.Stderr, passthrough)
}

rt, err := runtime.NewDockerRuntime(cfg.DockerHost)
if err != nil {
return err
Expand Down
20 changes: 19 additions & 1 deletion internal/awscli/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ func CheckInstalled() error {
return nil
}

// helpFlags are the flags/tokens the aws CLI recognizes as a help request.
// "help" is a bare pseudo-subcommand the aws CLI accepts at any command level
// (`aws help`, `aws s3 help`), equivalent to -h/--help.
var helpFlags = map[string]bool{"-h": true, "--help": true, "help": true}

// IsHelp reports whether args requests the aws CLI's help output. The aws CLI
// answers this without needing a running emulator or resolved endpoint.
func IsHelp(args []string) bool {
for _, a := range args {
if helpFlags[a] {
return true
}
}
return false
}

func Exec(ctx context.Context, endpointURL string, useProfile bool, stdout, stderr io.Writer, args []string) error {
ctx, span := otel.Tracer("github.com/localstack/lstk/internal/awscli").Start(ctx, "aws cli")
defer span.End()
Expand All @@ -43,7 +59,9 @@ func Exec(ctx context.Context, endpointURL string, useProfile bool, stdout, stde
capacity += 2
}
cmdArgs := make([]string, 0, capacity)
cmdArgs = append(cmdArgs, "--endpoint-url", endpointURL)
if endpointURL != "" {
cmdArgs = append(cmdArgs, "--endpoint-url", endpointURL)
}
if useProfile {
cmdArgs = append(cmdArgs, "--profile", awsconfig.ProfileName)
}
Expand Down
12 changes: 12 additions & 0 deletions internal/awscli/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import (
"github.com/stretchr/testify/assert"
)

func TestIsHelp(t *testing.T) {
for _, args := range [][]string{
{"--help"}, {"-h"}, {"s3", "--help"}, {"s3", "ls", "-h"},
{"help"}, {"s3", "help"},
} {
assert.Truef(t, IsHelp(args), "%v", args)
}
for _, args := range [][]string{{"s3", "ls"}, {}} {
assert.Falsef(t, IsHelp(args), "%v", args)
}
}

func TestBuildEnvSetsDefaultsWhenAbsent(t *testing.T) {
base := []string{"PATH=/usr/bin", "HOME=/home/user"}
env := BuildEnv(base)
Expand Down
20 changes: 18 additions & 2 deletions internal/iac/cdk/cli/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,25 @@ var valueFlags = map[string]bool{
}

// IsOffline reports whether the CDK invocation described by args is one of the
// offline subcommands that need no running emulator.
// offline subcommands that need no running emulator, or a help request.
func IsOffline(args []string) bool {
return offlineCommands[subcommand(args)]
return IsHelp(args) || offlineCommands[subcommand(args)]
}

// helpFlags are the flags/tokens cdk recognizes as a help request. "help" is a
// bare pseudo-subcommand cdk accepts at any command level (`cdk help`,
// `cdk deploy help`), equivalent to -h/--help.
var helpFlags = map[string]bool{"-h": true, "--help": true, "help": true}

// IsHelp reports whether args requests cdk's help output. cdk answers this
// without needing a running emulator, same as the other offline commands.
func IsHelp(args []string) bool {
for _, a := range args {
if helpFlags[a] {
return true
}
}
return false
}

// subcommand returns the first non-flag token in args that is not consumed as a
Expand Down
5 changes: 5 additions & 0 deletions internal/iac/cdk/cli/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ func TestIsOffline(t *testing.T) {
{"doctor"},
{"acknowledge", "12345"},
{"context"},
{"--help"},
{"-h"},
{"deploy", "--help"},
{"help"},
{"deploy", "help"},
}
for _, args := range offline {
assert.Truef(t, IsOffline(args), "expected %v offline", args)
Expand Down
18 changes: 16 additions & 2 deletions internal/iac/sam/cli/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,23 @@ var offlineCommands = map[string]bool{
var valueFlags = map[string]bool{}

// IsOffline reports whether the SAM invocation described by args is one of the
// offline subcommands that need no running emulator.
// offline subcommands that need no running emulator, or a help request.
func IsOffline(args []string) bool {
return offlineCommands[subcommand(args)]
return IsHelp(args) || offlineCommands[subcommand(args)]
}

// helpFlags are the flags sam recognizes as a help request.
var helpFlags = map[string]bool{"-h": true, "--help": true}

// IsHelp reports whether args requests sam's help output. sam answers this
// without needing a running emulator, same as the other offline commands.
func IsHelp(args []string) bool {
for _, a := range args {
if helpFlags[a] {
return true
}
}
return false
}

// subcommand returns the first non-flag token in args that is not consumed as a
Expand Down
3 changes: 3 additions & 0 deletions internal/iac/sam/cli/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ func TestIsOffline(t *testing.T) {
{"local", "generate-event", "s3", "put"},
{"local", "invoke"},
{"pipeline", "init"},
{"--help"},
{"-h"},
{"deploy", "--help"},
}
for _, args := range offline {
assert.Truef(t, IsOffline(args), "expected %v offline", args)
Expand Down
27 changes: 23 additions & 4 deletions internal/iac/terraform/cli/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,30 @@ var unproxiedCommands = map[string]bool{
}

// IsUnproxied reports whether the terraform invocation described by args is one
// of the unconditionally unproxied subcommands (fmt/validate/version). The
// subcommand is the first argument that is not a flag (terraform's global
// options, e.g. -chdir, precede the subcommand).
// of the unconditionally unproxied subcommands (fmt/validate/version), or a
// help request. The subcommand is the first argument that is not a flag
// (terraform's global options, e.g. -chdir, precede the subcommand).
func IsUnproxied(args []string) bool {
return unproxiedCommands[subcommand(args)]
return IsHelp(args) || unproxiedCommands[subcommand(args)]
}

// helpFlags are the flags terraform recognizes as a help request, in any
// position (e.g. `terraform --help`, `terraform plan -h`).
var helpFlags = map[string]bool{
"-h": true,
"-help": true,
"--help": true,
}

// IsHelp reports whether args requests terraform's help output via a help flag
// anywhere in args (e.g. `terraform --help`, `terraform plan -h`).
func IsHelp(args []string) bool {
for _, a := range args {
if helpFlags[a] {
return true
}
}
return false
}

// RequiresEmulator reports whether the invocation needs a running LocalStack
Expand Down
24 changes: 24 additions & 0 deletions internal/iac/terraform/cli/routing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ func TestIsUnproxiedSet(t *testing.T) {
}
}

// DEVX-1002 — help flags (in any position) are unproxied and never require
// the emulator, even in an uninitialized project. Unlike aws/cdk, terraform
// has no bare `help` pseudo-subcommand (`terraform help` is an error), so it
// is not recognized.
func TestIsHelp(t *testing.T) {
trueCases := [][]string{
{"--help"}, {"-h"}, {"-help"},
{"plan", "--help"}, {"plan", "-h"},
}
for _, args := range trueCases {
assert.Truef(t, IsHelp(args), "%v", args)
assert.Truef(t, IsUnproxied(args), "%v", args)
}

falseCases := [][]string{{"plan"}, {"apply", "-auto-approve"}, {"init"}, {"help"}, {"help", "plan"}}
for _, args := range falseCases {
assert.Falsef(t, IsHelp(args), "%v", args)
}
}

func TestRequiresEmulatorRouting(t *testing.T) {
noBackend := t.TempDir()
writeTF(t, noBackend, "main.tf", `provider "aws" {}`)
Expand All @@ -41,6 +61,10 @@ func TestRequiresEmulatorRouting(t *testing.T) {
// plan/apply always require the emulator.
assert.True(t, RequiresEmulator([]string{"plan"}, noBackend, log.Nop()))
assert.True(t, RequiresEmulator([]string{"apply", "-auto-approve"}, noBackend, log.Nop()))

// help requests never require the emulator, even with an S3 backend present.
assert.False(t, RequiresEmulator([]string{"--help"}, withBackend, log.Nop()))
assert.False(t, RequiresEmulator([]string{"plan", "-h"}, withBackend, log.Nop()))
}

// 9.2 — adding a backend/remote-state section still refuses to clobber a
Expand Down
33 changes: 33 additions & 0 deletions test/integration/aws_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/moby/moby/client"
Expand Down Expand Up @@ -251,6 +252,38 @@ func TestAWSCommandPropagatesExitCode(t *testing.T) {
requireExitCode(t, 42, err)
}

// DEVX-1002 — --help/-h, and the bare "help" pseudo-subcommand, never contact
// LocalStack, so they run without Docker/an emulator: DOCKER_HOST points at an
// unreachable address (mirroring TestAWSCommandFailsWhenDockerNotRunning) yet
// the command still succeeds and forwards the help request untouched, with no
// --endpoint-url injected.
func TestAWSCommandHelpSkipsDockerAndEmulator(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows Docker error tested separately via windowsDockerErrorEnv")
}

dir := t.TempDir()
script := "#!/bin/sh\necho \"ARGS:$*\"\n"
require.NoError(t, os.WriteFile(filepath.Join(dir, "aws"), []byte(script), 0755))

for _, args := range [][]string{{"--help"}, {"-h"}, {"s3", "--help"}, {"help"}, {"s3", "help"}} {
args := args
t.Run(strings.Join(args, "_"), func(t *testing.T) {
e := env.With(env.DisableEvents, "1").
With("PATH", dir).
With(env.Home, t.TempDir()).
With(env.Key("DOCKER_HOST"), "tcp://localhost:1")

cmdArgs := append([]string{"aws"}, args...)
stdout, stderr, err := runLstk(t, testContext(t), t.TempDir(), e, cmdArgs...)
require.NoError(t, err, "stderr: %s", stderr)

assert.Contains(t, stdout, "ARGS:"+strings.Join(args, " "))
assert.NotContains(t, stdout, "--endpoint-url")
})
}
}

func TestAWSCommandFailsWhenDockerNotRunning(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows Docker error tested separately via windowsDockerErrorEnv")
Expand Down
21 changes: 21 additions & 0 deletions test/integration/cdk_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/localstack/lstk/test/integration/env"
Expand Down Expand Up @@ -131,6 +132,26 @@ func TestCDKOfflineCommandsNoEmulator(t *testing.T) {
}
}

// DEVX-1002 — --help/-h, and the bare "help" pseudo-subcommand, never require
// the emulator, even for an AWS-contacting subcommand, and are forwarded to
// cdk untouched.
func TestCDKHelpNoEmulator(t *testing.T) {
t.Parallel()
for _, args := range [][]string{{"--help"}, {"-h"}, {"deploy", "--help"}, {"help"}, {"deploy", "help"}} {
args := args
t.Run(strings.Join(args, "_"), func(t *testing.T) {
t.Parallel()
fakeDir := writeFakeCDK(t, "2.177.0")
e := env.With(env.DisableEvents, "1").With("PATH", fakeDir).With(env.Home, t.TempDir())

cmdArgs := append([]string{"cdk"}, args...)
stdout, stderr, err := runLstk(t, testContext(t), t.TempDir(), e, cmdArgs...)
require.NoError(t, err, "stderr: %s", stderr)
assert.Contains(t, stdout, "ARGS:"+strings.Join(args, " "))
})
}
}

// 7.5 — a too-old cdk fails before the command runs.
func TestCDKVersionTooOld(t *testing.T) {
t.Parallel()
Expand Down
20 changes: 20 additions & 0 deletions test/integration/sam_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/localstack/lstk/test/integration/env"
Expand Down Expand Up @@ -133,6 +134,25 @@ func TestSAMOfflineCommandsNoEmulator(t *testing.T) {
}
}

// DEVX-1002 — --help (and -h) never require the emulator, even for an
// AWS-contacting subcommand, and are forwarded to sam untouched.
func TestSAMHelpNoEmulator(t *testing.T) {
t.Parallel()
for _, args := range [][]string{{"--help"}, {"-h"}, {"deploy", "--help"}} {
args := args
t.Run(strings.Join(args, "_"), func(t *testing.T) {
t.Parallel()
fakeDir := writeFakeSAM(t, "1.95.0")
e := env.With(env.DisableEvents, "1").With("PATH", fakeDir).With(env.Home, t.TempDir())

cmdArgs := append([]string{"sam"}, args...)
stdout, stderr, err := runLstk(t, testContext(t), t.TempDir(), e, cmdArgs...)
require.NoError(t, err, "stderr: %s", stderr)
assert.Contains(t, stdout, "ARGS:"+strings.Join(args, " "))
})
}
}

// a too-old sam fails before the command runs.
func TestSAMVersionTooOld(t *testing.T) {
t.Parallel()
Expand Down
24 changes: 24 additions & 0 deletions test/integration/terraform_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/localstack/lstk/test/integration/env"
Expand Down Expand Up @@ -167,6 +168,29 @@ func TestTerraformUnproxiedSkipsOverride(t *testing.T) {
}
}

// DEVX-1002 — --help (and -h) never require the emulator/provider schema, even
// in an uninitialized project with no running emulator, and are forwarded to
// terraform untouched.
func TestTerraformHelpSkipsOverride(t *testing.T) {
t.Parallel()
for _, args := range [][]string{{"--help"}, {"-h"}, {"-help"}, {"plan", "--help"}} {
args := args
t.Run(strings.Join(args, "_"), func(t *testing.T) {
t.Parallel()
fakeDir := writeFakeTerraform(t)
workDir := t.TempDir()
e := env.With(env.DisableEvents, "1").With("PATH", fakeDir).With(env.Home, t.TempDir())

cmdArgs := append([]string{"terraform"}, args...)
stdout, stderr, err := runLstk(t, testContext(t), workDir, e, cmdArgs...)
require.NoError(t, err, "stderr: %s", stderr)

assert.Contains(t, stdout, "ARGS:"+strings.Join(args, " "))
assert.NoFileExists(t, filepath.Join(workDir, tfOverrideFile))
})
}
}

// 7.6 (validation) — invalid --account fails before terraform is invoked. No
// emulator needed because validation happens at the command boundary.
func TestTerraformInvalidAccountRejected(t *testing.T) {
Expand Down
Loading