|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "regexp" |
| 8 | + "strings" |
| 9 | + |
| 10 | + "github.com/localstack/lstk/internal/config" |
| 11 | + "github.com/localstack/lstk/internal/container" |
| 12 | + tfcli "github.com/localstack/lstk/internal/iac/terraform/cli" |
| 13 | + "github.com/localstack/lstk/internal/output" |
| 14 | + "github.com/localstack/lstk/internal/runtime" |
| 15 | +) |
| 16 | + |
| 17 | +// Shared command-boundary helpers for the IaC proxy commands (terraform, cdk). |
| 18 | +// These live here rather than in any one command's file because both commands |
| 19 | +// depend on them equally; keeping them in cmd/ (not a domain package) is |
| 20 | +// deliberate — they touch config.Get(), the output.Sink, and the raw CLI args, |
| 21 | +// all of which are command-boundary concerns. |
| 22 | + |
| 23 | +var accountIDRe = regexp.MustCompile(`^\d{12}$`) |
| 24 | + |
| 25 | +// requireRunningAWSEmulator verifies the AWS emulator is running before an IaC |
| 26 | +// proxy command (terraform/cdk) that contacts AWS proceeds. When it is not |
| 27 | +// running it emits an actionable error through the sink — an AWS-specific |
| 28 | +// message naming the other emulator when a non-AWS one is up, otherwise the |
| 29 | +// generic "not running" error — and returns a silent error. cmdLabel is the |
| 30 | +// lstk command name used in the message (e.g. "terraform"/"cdk"). It returns nil |
| 31 | +// when the AWS emulator is running. |
| 32 | +func requireRunningAWSEmulator(ctx context.Context, rt runtime.Runtime, sink output.Sink, awsContainer config.ContainerConfig, cmdLabel string) error { |
| 33 | + runningName, err := container.ResolveRunningContainerName(ctx, rt, awsContainer) |
| 34 | + if err != nil { |
| 35 | + return fmt.Errorf("checking emulator status: %w", err) |
| 36 | + } |
| 37 | + if runningName != "" { |
| 38 | + return nil |
| 39 | + } |
| 40 | + // These commands only work with the AWS emulator. If a different emulator |
| 41 | + // is running, say so specifically rather than reporting a misleading |
| 42 | + // "AWS not running". |
| 43 | + if other := runningNonAWSEmulator(ctx, rt); other != "" { |
| 44 | + sink.Emit(output.ErrorEvent{ |
| 45 | + Title: fmt.Sprintf("lstk %s requires the %s, but the %s is running", cmdLabel, awsContainer.DisplayName(), other), |
| 46 | + Actions: []output.ErrorAction{ |
| 47 | + {Label: "Start the AWS emulator:", Value: "lstk"}, |
| 48 | + }, |
| 49 | + }) |
| 50 | + return output.NewSilentError(fmt.Errorf("lstk %s requires the AWS emulator, but the %s is running", cmdLabel, other)) |
| 51 | + } |
| 52 | + sink.Emit(output.ErrorEvent{ |
| 53 | + Title: fmt.Sprintf("%s is not running", awsContainer.DisplayName()), |
| 54 | + Actions: []output.ErrorAction{ |
| 55 | + {Label: "Start LocalStack:", Value: "lstk"}, |
| 56 | + {Label: "See help:", Value: "lstk -h"}, |
| 57 | + }, |
| 58 | + }) |
| 59 | + return output.NewSilentError(fmt.Errorf("%s is not running", awsContainer.Name())) |
| 60 | +} |
| 61 | + |
| 62 | +// runningNonAWSEmulator returns the display name of a running non-AWS emulator |
| 63 | +// (e.g. Snowflake or Azure), or "" if none is running. The IaC proxy commands |
| 64 | +// support only the AWS emulator, so this lets them give a specific error when a |
| 65 | +// different emulator is running instead of a misleading "AWS not running". |
| 66 | +func runningNonAWSEmulator(ctx context.Context, rt runtime.Runtime) string { |
| 67 | + var others []config.ContainerConfig |
| 68 | + for _, t := range config.SelectableEmulatorTypes { |
| 69 | + if t == config.EmulatorAWS { |
| 70 | + continue |
| 71 | + } |
| 72 | + others = append(others, config.ContainerConfig{Type: t, Port: config.DefaultPort}) |
| 73 | + } |
| 74 | + running, err := container.RunningEmulators(ctx, rt, others) |
| 75 | + if err != nil || len(running) == 0 { |
| 76 | + return "" |
| 77 | + } |
| 78 | + return running[0].DisplayName() |
| 79 | +} |
| 80 | + |
| 81 | +// resolveAWSContainer returns the configured AWS emulator container, falling |
| 82 | +// back to defaults when no matching entry exists (mirrors cmd/aws.go). |
| 83 | +func resolveAWSContainer() config.ContainerConfig { |
| 84 | + awsContainer := config.ContainerConfig{Type: config.EmulatorAWS, Port: config.DefaultPort} |
| 85 | + appCfg, err := config.Get() |
| 86 | + if err != nil { |
| 87 | + return awsContainer |
| 88 | + } |
| 89 | + for _, c := range appCfg.Containers { |
| 90 | + if c.Type == config.EmulatorAWS { |
| 91 | + return c |
| 92 | + } |
| 93 | + } |
| 94 | + return awsContainer |
| 95 | +} |
| 96 | + |
| 97 | +// emitValidationError renders a command-boundary validation failure through the |
| 98 | +// sink (consistent with the other IaC proxy error events) and returns a silent |
| 99 | +// error so the top-level handler does not print it a second time. |
| 100 | +func emitValidationError(sink output.Sink, err error) error { |
| 101 | + sink.Emit(output.ErrorEvent{Title: err.Error()}) |
| 102 | + return output.NewSilentError(err) |
| 103 | +} |
| 104 | + |
| 105 | +// rejectPreSubcommandFlags returns an error if --region or --account appears in |
| 106 | +// the raw command line before the subcommand token. Such flags are consumed by |
| 107 | +// Cobra during command resolution and would otherwise be silently dropped; |
| 108 | +// calledAs is the name the command was invoked as (e.g. "terraform"/"tf"/"cdk"). |
| 109 | +func rejectPreSubcommandFlags(calledAs string) error { |
| 110 | + cmdIdx := -1 |
| 111 | + for i, a := range os.Args { |
| 112 | + if a == calledAs { |
| 113 | + cmdIdx = i |
| 114 | + break |
| 115 | + } |
| 116 | + } |
| 117 | + if cmdIdx <= 0 { |
| 118 | + return nil |
| 119 | + } |
| 120 | + for _, a := range os.Args[1:cmdIdx] { |
| 121 | + if a == "--region" || a == "--account" || |
| 122 | + strings.HasPrefix(a, "--region=") || strings.HasPrefix(a, "--account=") { |
| 123 | + return fmt.Errorf("--region and --account must appear after the %s subcommand (e.g. `lstk %s --region us-west-2 ...`)", calledAs, calledAs) |
| 124 | + } |
| 125 | + } |
| 126 | + return nil |
| 127 | +} |
| 128 | + |
| 129 | +// stripLeadingIaCFlags extracts the lstk-specific --region/--account flags, and |
| 130 | +// (when recognizeChdir is set) reads terraform's global -chdir, but only in |
| 131 | +// leading position (between the subcommand alias and the action). It accepts |
| 132 | +// both --flag value and --flag=value forms for the lstk flags and -chdir=DIR for |
| 133 | +// chdir, stops at the first token that is none of these (forwarding the rest |
| 134 | +// verbatim), and errors if a leading lstk flag is missing its value. |
| 135 | +// |
| 136 | +// --region/--account are consumed and removed from the returned args; -chdir is |
| 137 | +// read for lstk's own working-directory resolution but kept in the returned |
| 138 | +// args, because terraform itself must also see it to switch directories. Only |
| 139 | +// the -chdir=DIR form is recognized (terraform does not accept a space-separated |
| 140 | +// -chdir DIR); any other spelling falls through and is forwarded verbatim. CDK |
| 141 | +// has no -chdir equivalent, so it calls this with recognizeChdir=false and |
| 142 | +// ignores the returned chdir. |
| 143 | +func stripLeadingIaCFlags(args []string, recognizeChdir bool) (remaining []string, region, account, chdir string, err error) { |
| 144 | + i := 0 |
| 145 | + for i < len(args) { |
| 146 | + arg := args[i] |
| 147 | + switch { |
| 148 | + case arg == "--region": |
| 149 | + if i+1 >= len(args) { |
| 150 | + return nil, "", "", "", fmt.Errorf("--region requires a value") |
| 151 | + } |
| 152 | + region = args[i+1] |
| 153 | + i += 2 |
| 154 | + case strings.HasPrefix(arg, "--region="): |
| 155 | + region = strings.TrimPrefix(arg, "--region=") |
| 156 | + i++ |
| 157 | + case arg == "--account": |
| 158 | + if i+1 >= len(args) { |
| 159 | + return nil, "", "", "", fmt.Errorf("--account requires a value") |
| 160 | + } |
| 161 | + account = args[i+1] |
| 162 | + i += 2 |
| 163 | + case strings.HasPrefix(arg, "--account="): |
| 164 | + account = strings.TrimPrefix(arg, "--account=") |
| 165 | + i++ |
| 166 | + case recognizeChdir && strings.HasPrefix(arg, "-chdir="): |
| 167 | + // Read the value but keep -chdir in the forwarded args so terraform |
| 168 | + // also switches into it; continue scanning so leading --region/--account |
| 169 | + // positioned after -chdir are still consumed. |
| 170 | + chdir = strings.TrimPrefix(arg, "-chdir=") |
| 171 | + remaining = append(remaining, arg) |
| 172 | + i++ |
| 173 | + default: |
| 174 | + return append(remaining, args[i:]...), region, account, chdir, nil |
| 175 | + } |
| 176 | + } |
| 177 | + return remaining, region, account, chdir, nil |
| 178 | +} |
| 179 | + |
| 180 | +// resolveRegion applies the precedence --region flag → AWS_REGION → us-east-1. |
| 181 | +// The deprecated AWS_DEFAULT_REGION is intentionally not consulted. |
| 182 | +func resolveRegion(flag string) string { |
| 183 | + if flag != "" { |
| 184 | + return flag |
| 185 | + } |
| 186 | + if v := os.Getenv("AWS_REGION"); v != "" { |
| 187 | + return v |
| 188 | + } |
| 189 | + return "us-east-1" |
| 190 | +} |
| 191 | + |
| 192 | +// resolveAccount applies the precedence --account flag → AWS_ACCESS_KEY_ID → |
| 193 | +// test. A flag value must be exactly 12 digits. An AWS_ACCESS_KEY_ID value is |
| 194 | +// run through DeactivateAccessKey so a real key (AKIA…/ASIA…) accidentally |
| 195 | +// present in the environment is never written into the override or sent to |
| 196 | +// LocalStack; the validated 12-digit flag is used as-is. |
| 197 | +func resolveAccount(flag string) (string, error) { |
| 198 | + if flag != "" { |
| 199 | + if !accountIDRe.MatchString(flag) { |
| 200 | + return "", fmt.Errorf("--account must be a 12-digit AWS account id, got %q", flag) |
| 201 | + } |
| 202 | + return flag, nil |
| 203 | + } |
| 204 | + if v := os.Getenv("AWS_ACCESS_KEY_ID"); v != "" { |
| 205 | + return tfcli.DeactivateAccessKey(v), nil |
| 206 | + } |
| 207 | + return "test", nil |
| 208 | +} |
0 commit comments