Skip to content

Commit 752e0fa

Browse files
chore: refactor command line handling and context object (#92)
### what - rework `StackletContext` and `StackletConfig` and add `StackletCredentials` - cleanup click-related setup to use the `StackletContext` for common data - remove `click_group_entry` which was defined for each group in addition to handlers (so it got called multiple times for a single CLI call) - rework tests to make more use of fixture, and remove uses of TestCase This also drops the global configuration override options (which did not work in all cases), requiring a configure, authenticate, execute flow. Overriding required passing all configuration options, which is cumbersome, and would only work if saved authentication matched credentials for the passed configuration. It also makes command line options for individual commands clearer since they now only contain command-specific options. ### why - make the CLI setup more predictable by accessing all configured details via StackletContext, rather than sticking entries in a plain dict. - handle configuration and credentials in one place ### testing local testing, updated existing tests and added more ### docs udpated the help output for the CLI to match command line changes
1 parent 9bd2d51 commit 752e0fa

33 files changed

Lines changed: 1154 additions & 1109 deletions

‎README.md‎

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ Commands are grouped into command groups, for example, all the commands relating
6868
found by running the following command:
6969

7070
```
71-
stacklet-admin account --help
7271
Usage: stacklet-admin account [OPTIONS] COMMAND [ARGS]...
7372
7473
Query against and Run mutations against Account objects in Stacklet.
@@ -82,34 +81,16 @@ Usage: stacklet-admin account [OPTIONS] COMMAND [ARGS]...
8281
$ stacklet account --output json list
8382
8483
Options:
85-
-v Verbosity level, increase verbosity by
86-
appending v, e.g. -vvv
87-
88-
--api TEXT If set, --cognito-user-pool-id, --cognito-
89-
client-id, --cognito-region, and --api must
90-
also be set.
91-
92-
--cognito-region TEXT If set, --cognito-user-pool-id, --cognito-
93-
client-id, --cognito-region, and --api must
94-
also be set.
95-
96-
--cognito-client-id TEXT If set, --cognito-user-pool-id, --cognito-
97-
client-id, --cognito-region, and --api must
98-
also be set.
99-
100-
--cognito-user-pool-id TEXT If set, --cognito-user-pool-id, --cognito-
101-
client-id, --cognito-region, and --api must
102-
also be set.
103-
104-
--output [|plain|json|yaml] Ouput type
105-
--config TEXT
106-
--help Show this message and exit.
84+
--help Show this message and exit.
10785
10886
Commands:
109-
add Add an account to Stacklet
110-
list List cloud accounts in Stacklet
111-
remove Remove an account from Stacklet
112-
show Show an account in Stacklet
87+
add Add an account to Stacklet
88+
list List cloud accounts in Stacklet
89+
remove Remove an account from Stacklet
90+
show Show an account in Stacklet
91+
update Update an account in platform
92+
validate Validate an account in Stacklet
93+
validate-all Validate all accounts in Stacklet
11394
```
11495

11596
Then run the command:

‎stacklet/client/platform/cli.py‎

Lines changed: 106 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
11
# Copyright Stacklet, Inc.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
import json
5-
import os
64
import pathlib
75
from urllib.parse import urlsplit, urlunsplit
86

97
import click
108
import jwt
119
import requests
1210

13-
from stacklet.client.platform.cognito import CognitoUserManager
14-
from stacklet.client.platform.commands import commands
15-
from stacklet.client.platform.config import StackletConfig
16-
from stacklet.client.platform.context import StackletContext
17-
from stacklet.client.platform.formatter import Formatter
18-
from stacklet.client.platform.utils import click_group_entry, default_options
11+
from .cognito import CognitoUserManager
12+
from .commands import commands
13+
from .config import DEFAULT_CONFIG_FILE, DEFAULT_OUTPUT_FORMAT, StackletConfig
14+
from .context import StackletContext
15+
from .formatter import Formatter
16+
from .utils import setup_logging
1917

2018

2119
@click.group()
22-
@default_options()
20+
@click.option(
21+
"--config",
22+
type=click.Path(path_type=pathlib.Path, dir_okay=False),
23+
default=DEFAULT_CONFIG_FILE,
24+
envvar="STACKLET_CONFIG",
25+
show_envvar=True,
26+
help="Configuration file",
27+
)
28+
@click.option(
29+
"--output",
30+
type=click.Choice(list(Formatter.registry.keys()), case_sensitive=False),
31+
default=DEFAULT_OUTPUT_FORMAT,
32+
envvar="STACKLET_OUTPUT",
33+
show_envvar=True,
34+
help="Output type",
35+
)
36+
@click.option(
37+
"-v",
38+
count=True,
39+
default=0,
40+
help="Verbosity level, increase verbosity by appending v, e.g. -vvv",
41+
)
2342
@click.pass_context
24-
def cli(*args, **kwargs):
43+
def cli(
44+
ctx,
45+
config,
46+
output,
47+
v,
48+
):
2549
"""
2650
Stacklet CLI
2751
@@ -30,6 +54,21 @@ def cli(*args, **kwargs):
3054
3155
$ stacklet-admin configure
3256
57+
or specify the configuration parameters directly:
58+
59+
\b
60+
$ stacklet-admin \\
61+
configure \\
62+
--api $stacklet_api \\
63+
--cognito-client-id $cognito_client_id \\
64+
--cognito-user-pool-id $cognito_user_pool_id \\
65+
--region $region
66+
\b
67+
68+
In alternative, autoconfiguration can be used by specifying the deployment URL:
69+
70+
$ stacklet-admin auto-configure --url https://console.myorg.stacklet.io
71+
3372
If this is your first time using Stacklet, create a user:
3473
3574
$ stacklet-admin user add
@@ -48,24 +87,15 @@ def cli(*args, **kwargs):
4887
4988
To specify a different configuration file or different API endpoint/Cognito Configuration:
5089
51-
\b
52-
$ stacklet-admin \\
53-
--api $stacklet_api \\
54-
--cognito-client-id $cognito_client_id \\
55-
--cognito-user-pool-id $cognito_user_pool_id \\
56-
--region $region \\
57-
login
58-
59-
or:
60-
6190
\b
6291
$ stacklet-admin \\
6392
--config $config_file_location \\
6493
login
94+
\b
6595
6696
Specify different output types:
6797
68-
$ stacklet-admin account --output json list --provider AWS
98+
$ stacklet-admin --output json account list --provider AWS
6999
70100
Stacklet queries default to 20 entries per page. To use pagiantion:
71101
@@ -76,22 +106,30 @@ def cli(*args, **kwargs):
76106
--before $before_token \\
77107
--after $after_token \\
78108
list
109+
\b
79110
"""
80-
click_group_entry(*args, **kwargs)
111+
setup_logging(v)
112+
ctx.obj = StackletContext(config_file=config, output_format=output)
81113

82114

83115
@cli.command(short_help="Configure stacklet-admin cli")
84-
@click.option("--api", prompt="Stacklet API endpoint")
85-
@click.option("--region", prompt="Cognito Region")
86-
@click.option("--cognito-client-id", prompt="Cognito User Pool Client ID")
87-
@click.option("--cognito-user-pool-id", prompt="Cognito User Pool ID")
88-
@click.option("--idp-id", prompt="(SSO) IDP ID", default="")
89-
@click.option("--auth-url", prompt="(SSO) Auth Url", default="")
90-
@click.option("--cubejs", prompt="Stacklet cube.js endpoint", default="")
91-
@click.option("--location", prompt="Config File Location", default="~/.stacklet/config.json") # noqa
92-
@click.pass_context
116+
@click.option("--api", prompt="Stacklet API endpoint", help="Stacklet API endpoint")
117+
@click.option("--region", prompt="Cognito Region", help="Cognito Region")
118+
@click.option(
119+
"--cognito-client-id", prompt="Cognito User Pool Client ID", help="Cognito User Pool Client ID"
120+
)
121+
@click.option("--cognito-user-pool-id", prompt="Cognito User Pool ID", help="Cognito User Pool ID")
122+
@click.option("--cubejs", prompt="Stacklet cube.js endpoint", help="Stacklet cube.js endpoint")
123+
@click.option("--idp-id", prompt="(SSO) IDP ID", default="", help="(SSO) IDP ID")
124+
@click.option("--auth-url", prompt="(SSO) Auth Url", default="", help="(SSO) Auth Url")
125+
@click.option(
126+
"--location",
127+
type=click.Path(path_type=pathlib.Path, dir_okay=False),
128+
default=DEFAULT_CONFIG_FILE,
129+
prompt="Config File Location",
130+
help="Config File Location",
131+
)
93132
def configure(
94-
ctx,
95133
api,
96134
region,
97135
cognito_client_id,
@@ -104,24 +142,18 @@ def configure(
104142
"""
105143
Interactively save a Stacklet Config file
106144
"""
107-
config = {
108-
"api": api,
109-
"region": region,
110-
"cognito_client_id": cognito_client_id,
111-
"cognito_user_pool_id": cognito_user_pool_id,
112-
"idp_id": idp_id,
113-
"auth_url": auth_url,
114-
"cubejs": cubejs,
115-
}
116-
117-
StackletConfig.validate(config)
118-
119-
if not os.path.exists(location):
120-
dirs = location.rsplit("/", 1)[0]
121-
os.makedirs(os.path.expanduser(dirs), exist_ok=True)
122-
123-
with open(os.path.expanduser(location), "w+") as f:
124-
f.write(json.dumps(config))
145+
config = StackletConfig.from_dict(
146+
{
147+
"api": api,
148+
"region": region,
149+
"cognito_client_id": cognito_client_id,
150+
"cognito_user_pool_id": cognito_user_pool_id,
151+
"idp_id": idp_id,
152+
"auth_url": auth_url,
153+
"cubejs": cubejs,
154+
}
155+
)
156+
config.to_file(location)
125157
click.echo(f"Saved config to {location}")
126158

127159

@@ -140,7 +172,7 @@ def configure(
140172
)
141173
@click.option(
142174
"--location",
143-
default="~/.stacklet/config.json",
175+
default=str(DEFAULT_CONFIG_FILE),
144176
type=click.Path(path_type=pathlib.Path, dir_okay=False),
145177
show_default=True,
146178
)
@@ -253,34 +285,28 @@ def auto_configure(url, prefix, idp, location):
253285
except KeyError as err:
254286
raise click.ClickException(f"The configuration details are missing a required key: {err}")
255287

256-
config_file = location.expanduser()
257-
if not config_file.exists():
258-
config_file.parent.mkdir(parents=True, exist_ok=True)
259-
260-
config_file.write_text(json.dumps(formatted_config, indent=4, sort_keys=True))
261-
262-
click.echo(f"Saved config to {config_file}")
288+
config = StackletConfig.from_dict(formatted_config)
289+
config.to_file(location)
290+
click.echo(f"Saved config to {location}")
263291

264292

265293
@cli.command()
266-
@click.pass_context
267-
def show(ctx):
294+
@click.pass_obj
295+
def show(obj):
268296
"""
269297
Show your config
270298
"""
271-
context = StackletContext(ctx.obj["config"], ctx.obj["raw_config"])
272-
fmt = Formatter.registry.get(ctx.obj["output"])()
273-
if os.path.exists(os.path.expanduser(StackletContext.DEFAULT_ID)):
274-
with open(os.path.expanduser(StackletContext.DEFAULT_ID), "r") as f:
275-
id_details = jwt.decode(f.read(), options={"verify_signature": False})
299+
fmt = obj.formatter()
300+
if id_token := obj.credentials.id_token():
301+
id_details = jwt.decode(id_token, options={"verify_signature": False})
276302
click.echo(fmt(id_details))
277303
click.echo()
278-
click.echo(fmt(context.config.to_json()))
304+
click.echo(fmt(obj.config.to_dict()))
279305

280306

281307
@cli.command(short_help="Login to Stacklet")
282-
@click.option("--username", required=False)
283-
@click.option("--password", hide_input=True, required=False)
308+
@click.option("--username", help="Login username", required=False)
309+
@click.option("--password", help="Login password", hide_input=True, required=False)
284310
@click.pass_context
285311
def login(ctx, username, password):
286312
"""
@@ -295,38 +321,37 @@ def login(ctx, username, password):
295321
296322
If password is not passed in, your password will be prompted
297323
"""
298-
context = StackletContext(ctx.obj["config"], ctx.obj["raw_config"])
324+
context = ctx.obj
325+
config = context.config
326+
can_sso_login = bool(config.auth_url)
299327
# sso login
300-
if context.can_sso_login() and not any([username, password]):
301-
from stacklet.client.platform.vendored.auth import BrowserAuthenticator
328+
if can_sso_login and not any([username, password]):
329+
from .vendored.auth import BrowserAuthenticator
302330

303331
BrowserAuthenticator(
304-
authority_url=context.config.auth_url,
305-
client_id=context.config.cognito_client_id,
306-
idp_id=context.config.idp_id,
332+
authority_url=config.auth_url,
333+
client_id=config.cognito_client_id,
334+
idp_id=config.idp_id,
307335
)()
308336
return
309-
elif not context.can_sso_login() and not any([username, password]):
337+
elif not can_sso_login and not any([username, password]):
310338
click.echo(
311339
"To login with SSO ensure that your configuration includes "
312340
+ "auth_url, and cognito_client_id values."
313341
)
314-
raise Exception()
342+
ctx.exit(1)
315343

316344
# handle login with username/password
317345
if not username:
318346
username = click.prompt("Username")
319347
if not password:
320348
password = click.prompt("Password", hide_input=True)
321349
manager = CognitoUserManager.from_context(context)
322-
res = manager.login(
350+
id_token, access_token = manager.login(
323351
user=username,
324352
password=password,
325353
)
326-
if not os.path.exists(os.path.dirname(os.path.expanduser(StackletContext.DEFAULT_CREDENTIALS))):
327-
os.makedirs(os.path.dirname(os.path.expanduser(StackletContext.DEFAULT_CREDENTIALS)))
328-
with open(os.path.expanduser(StackletContext.DEFAULT_CREDENTIALS), "w+") as f: # noqa
329-
f.write(res)
354+
context.credentials.write(id_token, access_token)
330355

331356

332357
for c in commands:

‎stacklet/client/platform/client.py‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,27 @@
33

44
# platform client using cli
55

6-
from pathlib import Path
76

87
import jmespath
98

109
# import the commands so the registry is populated when the client is instantiated
11-
import stacklet.client.platform.commands # noqa
12-
from stacklet.client.platform.context import StackletContext
13-
from stacklet.client.platform.executor import StackletGraphqlExecutor
14-
from stacklet.client.platform.utils import _PAGINATION_OPTIONS, get_token
10+
from . import commands # noqa
11+
from .config import DEFAULT_CONFIG_FILE
12+
from .context import StackletContext
13+
from .executor import StackletGraphqlExecutor
14+
from .utils import _PAGINATION_OPTIONS
1515

1616

1717
def platform_client(pager=False, expr=False):
1818
# for more pythonic experience, pass expr=True to de-graphqlize the result
1919
# for automatic pagination handling, pass pager=True
20-
if (
21-
not Path(StackletContext.DEFAULT_CONFIG).expanduser().exists()
22-
or not Path(StackletContext.DEFAULT_CREDENTIALS).expanduser().exists()
23-
):
20+
context = StackletContext(config_file=DEFAULT_CONFIG_FILE)
21+
if not DEFAULT_CONFIG_FILE.exists() or not context.credentials.api_token():
2422
raise ValueError("Please configure and authenticate on stacklet-admin cli")
2523

2624
d = {}
27-
ctx = StackletContext(raw_config={})
28-
token = get_token()
29-
executor = StackletGraphqlExecutor(ctx, token)
25+
26+
executor = StackletGraphqlExecutor(context)
3027

3128
for k, snippet in StackletGraphqlExecutor.registry.items():
3229
# assert k == snippet.name, f"{k} mismatch {snippet.name}"

0 commit comments

Comments
 (0)