Skip to content

Commit a86bbca

Browse files
authored
Allow --profile to be specified anywhere on the CLI command line (#12500)
Prior to this commit, --profile must be specified as a top-level option, e.g. localstack --profile test start. Now it can be specified at any point, e.g. localstack start --profile test.
1 parent 22e82dc commit a86bbca

File tree

3 files changed

+181
-24
lines changed

3 files changed

+181
-24
lines changed

localstack-core/localstack/cli/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ def main():
66
os.environ["LOCALSTACK_CLI"] = "1"
77

88
# config profiles are the first thing that need to be loaded (especially before localstack.config!)
9-
from .profiles import set_profile_from_sys_argv
9+
from .profiles import set_and_remove_profile_from_sys_argv
1010

11-
set_profile_from_sys_argv()
11+
# WARNING: This function modifies sys.argv to remove the profile argument.
12+
set_and_remove_profile_from_sys_argv()
1213

1314
# initialize CLI plugins
1415
from .localstack import create_with_plugins
Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,66 @@
1+
import argparse
12
import os
23
import sys
34
from typing import Optional
45

56
# important: this needs to be free of localstack imports
67

78

8-
def set_profile_from_sys_argv():
9+
def set_and_remove_profile_from_sys_argv():
910
"""
10-
Reads the --profile flag from sys.argv and then sets the 'CONFIG_PROFILE' os variable accordingly. This is later
11-
picked up by ``localstack.config``.
11+
Performs the following steps:
12+
13+
1. Use argparse to parse the command line arguments for the --profile flag.
14+
All occurrences are removed from the sys.argv list, and the value from
15+
the last occurrence is used. This allows the user to specify a profile
16+
at any point on the command line.
17+
18+
2. If a --profile flag is not found, check for the -p flag. The first
19+
occurrence of the -p flag is used and it is not removed from sys.argv.
20+
The reasoning for this is that at least one of the CLI subcommands has
21+
a -p flag, and we want to keep it in sys.argv for that command to
22+
pick up. An existing bug means that if a -p flag is used with a
23+
subcommand, it could erroneously be used as the profile value as well.
24+
This behaviour is undesired, but we must maintain back-compatibility of
25+
allowing the profile to be specified using -p.
26+
27+
3. If a profile is found, the 'CONFIG_PROFILE' os variable is set
28+
accordingly. This is later picked up by ``localstack.config``.
29+
30+
WARNING: Any --profile options are REMOVED from sys.argv, so that they are
31+
not passed to the localstack CLI. This allows the profile option
32+
to be set at any point on the command line.
1233
"""
13-
profile = parse_profile_argument(sys.argv)
34+
parser = argparse.ArgumentParser()
35+
parser.add_argument("--profile")
36+
namespace, sys.argv = parser.parse_known_args(sys.argv)
37+
profile = namespace.profile
38+
39+
if not profile:
40+
# if no profile is given, check for the -p argument
41+
profile = parse_p_argument(sys.argv)
42+
1443
if profile:
1544
os.environ["CONFIG_PROFILE"] = profile.strip()
1645

1746

18-
def parse_profile_argument(args) -> Optional[str]:
47+
def parse_p_argument(args) -> Optional[str]:
1948
"""
20-
Lightweight arg parsing to find ``--profile <config>``, or ``--profile=<config>`` and return the value of
49+
Lightweight arg parsing to find the first occurrence of ``-p <config>``, or ``-p=<config>`` and return the value of
2150
``<config>`` from the given arguments.
2251
2352
:param args: list of CLI arguments
24-
:returns: the value of ``--profile``.
53+
:returns: the value of ``-p``.
2554
"""
2655
for i, current_arg in enumerate(args):
27-
if current_arg.startswith("--profile="):
28-
# if using the "<arg>=<value>" notation, we remove the "--profile=" prefix to get the value
29-
return current_arg[10:]
30-
elif current_arg.startswith("-p="):
56+
if current_arg.startswith("-p="):
3157
# if using the "<arg>=<value>" notation, we remove the "-p=" prefix to get the value
3258
return current_arg[3:]
33-
if current_arg in ["--profile", "-p"]:
59+
if current_arg == "-p":
3460
# otherwise use the next arg in the args list as value
3561
try:
3662
return args[i + 1]
37-
except KeyError:
63+
except IndexError:
3864
return None
3965

4066
return None

tests/unit/cli/test_profiles.py

Lines changed: 139 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,148 @@
11
import os
22
import sys
33

4-
from localstack.cli.profiles import set_profile_from_sys_argv
4+
from localstack.cli.profiles import set_and_remove_profile_from_sys_argv
55

66

7-
def test_profiles_equals_notation(monkeypatch):
8-
monkeypatch.setattr(sys, "argv", ["--profile=non-existing-test-profile"])
7+
def profile_test(monkeypatch, input_args, expected_profile, expected_argv):
8+
monkeypatch.setattr(sys, "argv", input_args)
99
monkeypatch.setenv("CONFIG_PROFILE", "")
10-
set_profile_from_sys_argv()
11-
assert os.environ["CONFIG_PROFILE"] == "non-existing-test-profile"
10+
set_and_remove_profile_from_sys_argv()
11+
assert os.environ["CONFIG_PROFILE"] == expected_profile
12+
assert sys.argv == expected_argv
13+
14+
15+
def test_profiles_equals_notation(monkeypatch):
16+
profile_test(
17+
monkeypatch,
18+
input_args=["--profile=non-existing-test-profile"],
19+
expected_profile="non-existing-test-profile",
20+
expected_argv=[],
21+
)
1222

1323

1424
def test_profiles_separate_args_notation(monkeypatch):
15-
monkeypatch.setattr(sys, "argv", ["--profile", "non-existing-test-profile"])
16-
monkeypatch.setenv("CONFIG_PROFILE", "")
17-
set_profile_from_sys_argv()
18-
assert os.environ["CONFIG_PROFILE"] == "non-existing-test-profile"
25+
profile_test(
26+
monkeypatch,
27+
input_args=["--profile", "non-existing-test-profile"],
28+
expected_profile="non-existing-test-profile",
29+
expected_argv=[],
30+
)
31+
32+
33+
def test_p_equals_notation(monkeypatch):
34+
profile_test(
35+
monkeypatch,
36+
input_args=["-p=non-existing-test-profile"],
37+
expected_profile="non-existing-test-profile",
38+
expected_argv=["-p=non-existing-test-profile"],
39+
)
40+
41+
42+
def test_p_separate_args_notation(monkeypatch):
43+
profile_test(
44+
monkeypatch,
45+
input_args=["-p", "non-existing-test-profile"],
46+
expected_profile="non-existing-test-profile",
47+
expected_argv=["-p", "non-existing-test-profile"],
48+
)
49+
50+
51+
def test_profiles_args_before_and_after(monkeypatch):
52+
profile_test(
53+
monkeypatch,
54+
input_args=["cli", "-D", "--profile=non-existing-test-profile", "start"],
55+
expected_profile="non-existing-test-profile",
56+
expected_argv=["cli", "-D", "start"],
57+
)
58+
59+
60+
def test_profiles_args_before_and_after_separate(monkeypatch):
61+
profile_test(
62+
monkeypatch,
63+
input_args=["cli", "-D", "--profile", "non-existing-test-profile", "start"],
64+
expected_profile="non-existing-test-profile",
65+
expected_argv=["cli", "-D", "start"],
66+
)
67+
68+
69+
def test_p_args_before_and_after_separate(monkeypatch):
70+
profile_test(
71+
monkeypatch,
72+
input_args=["cli", "-D", "-p", "non-existing-test-profile", "start"],
73+
expected_profile="non-existing-test-profile",
74+
expected_argv=["cli", "-D", "-p", "non-existing-test-profile", "start"],
75+
)
76+
77+
78+
def test_profiles_args_multiple(monkeypatch):
79+
profile_test(
80+
monkeypatch,
81+
input_args=[
82+
"cli",
83+
"--profile",
84+
"non-existing-test-profile",
85+
"start",
86+
"--profile",
87+
"another-profile",
88+
],
89+
expected_profile="another-profile",
90+
expected_argv=["cli", "start"],
91+
)
92+
93+
94+
def test_p_args_multiple(monkeypatch):
95+
profile_test(
96+
monkeypatch,
97+
input_args=[
98+
"cli",
99+
"-p",
100+
"non-existing-test-profile",
101+
"start",
102+
"-p",
103+
"another-profile",
104+
],
105+
expected_profile="non-existing-test-profile",
106+
expected_argv=[
107+
"cli",
108+
"-p",
109+
"non-existing-test-profile",
110+
"start",
111+
"-p",
112+
"another-profile",
113+
],
114+
)
115+
116+
117+
def test_p_and_profile_args(monkeypatch):
118+
profile_test(
119+
monkeypatch,
120+
input_args=[
121+
"cli",
122+
"-p",
123+
"non-existing-test-profile",
124+
"start",
125+
"--profile",
126+
"the_profile",
127+
"-p",
128+
"another-profile",
129+
],
130+
expected_profile="the_profile",
131+
expected_argv=[
132+
"cli",
133+
"-p",
134+
"non-existing-test-profile",
135+
"start",
136+
"-p",
137+
"another-profile",
138+
],
139+
)
140+
141+
142+
def test_trailing_p_argument(monkeypatch):
143+
profile_test(
144+
monkeypatch,
145+
input_args=["cli", "start", "-p"],
146+
expected_profile="",
147+
expected_argv=["cli", "start", "-p"],
148+
)

0 commit comments

Comments
 (0)