Skip to content

Commit 184174d

Browse files
committed
feat: add support for using API keys in addition to tokens [ENG-6324]
[ENG-6324](https://stacklet.atlassian.net/browse/ENG-6324) ### what - support using API keys in place of tokens (via `STACKLET_API_KEY` env var) - 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 and called for each group in addition to handlers (so multiple times for a single CLI call) ### why - provide support for API keys - make the CLI setup more predictable by accessing all configured details via StackletContext, rather than sticking entries in a plain dict. ### testing local testing, updated tests ### docs n/a
1 parent 2e5c83f commit 184174d

26 files changed

Lines changed: 599 additions & 585 deletions

stacklet/client/platform/cli.py

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,26 @@
1010
import jwt
1111
import requests
1212

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
13+
from .cognito import CognitoUserManager
14+
from .commands import commands
15+
from .config import StackletConfig
16+
from .context import StackletContext
17+
from .utils import default_options, setup_logging
1918

2019

2120
@click.group()
2221
@default_options()
2322
@click.pass_context
24-
def cli(*args, **kwargs):
23+
def cli(
24+
ctx,
25+
config,
26+
output,
27+
cognito_user_pool_id,
28+
cognito_client_id,
29+
cognito_region,
30+
api,
31+
v,
32+
):
2533
"""
2634
Stacklet CLI
2735
@@ -77,7 +85,25 @@ def cli(*args, **kwargs):
7785
--after $after_token \\
7886
list
7987
"""
80-
click_group_entry(*args, **kwargs)
88+
setup_logging(v)
89+
90+
config_items = [cognito_user_pool_id, cognito_client_id, cognito_region, api]
91+
if any(config_items) and not all(config_items):
92+
raise Exception(
93+
"All options must be set for config items: --cognito-user-pool-id, "
94+
+ "--cognito-client-id, --cognito-region, and --api"
95+
)
96+
97+
raw_config = None
98+
if all(config_items):
99+
raw_config = {
100+
"cognito_user_pool_id": cognito_user_pool_id,
101+
"cognito_client_id": cognito_client_id,
102+
"region": cognito_region,
103+
"api": api,
104+
}
105+
106+
ctx.obj = StackletContext(raw_config=raw_config, config_file=config, output_format=output)
81107

82108

83109
@cli.command(short_help="Configure stacklet-admin cli")
@@ -89,9 +115,7 @@ def cli(*args, **kwargs):
89115
@click.option("--auth-url", prompt="(SSO) Auth Url", default="")
90116
@click.option("--cubejs", prompt="Stacklet cube.js endpoint", default="")
91117
@click.option("--location", prompt="Config File Location", default="~/.stacklet/config.json") # noqa
92-
@click.pass_context
93118
def configure(
94-
ctx,
95119
api,
96120
region,
97121
cognito_client_id,
@@ -263,26 +287,24 @@ def auto_configure(url, prefix, idp, location):
263287

264288

265289
@cli.command()
266-
@click.pass_context
267-
def show(ctx):
290+
@click.pass_obj
291+
def show(obj):
268292
"""
269293
Show your config
270294
"""
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})
295+
fmt = obj.formatter()
296+
if id_token := obj.credentials.id_token():
297+
id_details = jwt.decode(id_token, options={"verify_signature": False})
276298
click.echo(fmt(id_details))
277299
click.echo()
278-
click.echo(fmt(context.config.to_json()))
300+
click.echo(fmt(obj.config.to_json()))
279301

280302

281303
@cli.command(short_help="Login to Stacklet")
282304
@click.option("--username", required=False)
283305
@click.option("--password", hide_input=True, required=False)
284-
@click.pass_context
285-
def login(ctx, username, password):
306+
@click.pass_obj
307+
def login(obj, username, password):
286308
"""
287309
Login to Stacklet
288310
@@ -295,18 +317,19 @@ def login(ctx, username, password):
295317
296318
If password is not passed in, your password will be prompted
297319
"""
298-
context = StackletContext(ctx.obj["config"], ctx.obj["raw_config"])
320+
config = obj.config
321+
can_sso_login = obj.can_sso_login()
299322
# sso login
300-
if context.can_sso_login() and not any([username, password]):
323+
if can_sso_login and not any([username, password]):
301324
from stacklet.client.platform.vendored.auth import BrowserAuthenticator
302325

303326
BrowserAuthenticator(
304-
authority_url=context.config.auth_url,
305-
client_id=context.config.cognito_client_id,
306-
idp_id=context.config.idp_id,
327+
authority_url=config.auth_url,
328+
client_id=config.cognito_client_id,
329+
idp_id=config.idp_id,
307330
)()
308331
return
309-
elif not context.can_sso_login() and not any([username, password]):
332+
elif not can_sso_login and not any([username, password]):
310333
click.echo(
311334
"To login with SSO ensure that your configuration includes "
312335
+ "auth_url, and cognito_client_id values."
@@ -318,15 +341,12 @@ def login(ctx, username, password):
318341
username = click.prompt("Username")
319342
if not password:
320343
password = click.prompt("Password", hide_input=True)
321-
manager = CognitoUserManager.from_context(context)
322-
res = manager.login(
344+
manager = CognitoUserManager.from_context(obj)
345+
id_token, access_token = manager.login(
323346
user=username,
324347
password=password,
325348
)
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)
349+
obj.credentials.write(id_token, access_token)
330350

331351

332352
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(raw_config={})
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}"

stacklet/client/platform/cognito.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,15 @@ def create_user(self, user, password, email, phone_number, permanent=True):
7171
self.log.debug(res)
7272
return True
7373

74-
def login(self, user, password):
74+
def login(self, user, password) -> tuple[str, str]:
7575
res = self.client.initiate_auth(
7676
ClientId=self.user_pool_client_id,
7777
AuthFlow="USER_PASSWORD_AUTH",
7878
AuthParameters={"USERNAME": user, "PASSWORD": password},
7979
)
8080
self.log.debug("Authentication Success")
81-
return res["AuthenticationResult"]["AccessToken"]
81+
auth = res["AuthenticationResult"]
82+
return auth["IdToken"], auth["AccessToken"]
8283

8384
def ensure_group(self, user, group) -> bool:
8485
try:

stacklet/client/platform/commands/account.py

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from stacklet.client.platform.executor import StackletGraphqlExecutor, _run_graphql, snippet_options
99
from stacklet.client.platform.graphql import StackletGraphqlSnippet
10-
from stacklet.client.platform.utils import click_group_entry, default_options
10+
from stacklet.client.platform.utils import default_options
1111

1212

1313
@StackletGraphqlExecutor.registry.register("list-accounts")
@@ -273,7 +273,6 @@ class ValidateAccountSnippet(StackletGraphqlSnippet):
273273

274274
@click.group(short_help="Run account queries/mutations")
275275
@default_options()
276-
@click.pass_context
277276
def account(*args, **kwargs):
278277
"""
279278
Query against and Run mutations against Account objects in Stacklet.
@@ -287,86 +286,85 @@ def account(*args, **kwargs):
287286
$ stacklet account --output json list
288287
289288
"""
290-
click_group_entry(*args, **kwargs)
291289

292290

293291
@account.command()
294292
@snippet_options("list-accounts")
295-
@click.pass_context
296-
def list(ctx, **kwargs):
293+
@click.pass_obj
294+
def list(obj, **kwargs):
297295
"""
298296
List cloud accounts in Stacklet
299297
"""
300-
click.echo(_run_graphql(ctx=ctx, name="list-accounts", variables=kwargs))
298+
click.echo(_run_graphql(obj, name="list-accounts", variables=kwargs))
301299

302300

303301
@account.command()
304302
@snippet_options("add-account")
305-
@click.pass_context
306-
def add(ctx, **kwargs):
303+
@click.pass_obj
304+
def add(obj, **kwargs):
307305
"""
308306
Add an account to Stacklet
309307
"""
310-
click.echo(_run_graphql(ctx=ctx, name="add-account", variables=kwargs))
308+
click.echo(_run_graphql(obj, name="add-account", variables=kwargs))
311309

312310

313311
@account.command()
314312
@snippet_options("remove-account")
315-
@click.pass_context
316-
def remove(ctx, **kwargs):
313+
@click.pass_obj
314+
def remove(obj, **kwargs):
317315
"""
318316
Remove an account from Stacklet
319317
"""
320-
click.echo(_run_graphql(ctx=ctx, name="remove-account", variables=kwargs))
318+
click.echo(_run_graphql(obj, name="remove-account", variables=kwargs))
321319

322320

323321
@account.command()
324322
@snippet_options("update-account")
325-
@click.pass_context
326-
def update(ctx, **kwargs):
323+
@click.pass_obj
324+
def update(obj, **kwargs):
327325
"""
328326
Update an account in platform
329327
"""
330-
click.echo(_run_graphql(ctx=ctx, name="update-account", variables=kwargs))
328+
click.echo(_run_graphql(obj, name="update-account", variables=kwargs))
331329

332330

333331
@account.command()
334332
@snippet_options("show-account")
335-
@click.pass_context
336-
def show(ctx, **kwargs):
333+
@click.pass_obj
334+
def show(obj, **kwargs):
337335
"""
338336
Show an account in Stacklet
339337
"""
340-
click.echo(_run_graphql(ctx=ctx, name="show-account", variables=kwargs))
338+
click.echo(_run_graphql(obj, name="show-account", variables=kwargs))
341339

342340

343341
@account.command()
344342
@snippet_options("validate-account")
345-
@click.pass_context
346-
def validate(ctx, **kwargs):
343+
@click.pass_obj
344+
def validate(obj, **kwargs):
347345
"""
348346
Validate an account in Stacklet
349347
"""
350-
click.echo(_run_graphql(ctx=ctx, name="validate-account", variables=kwargs))
348+
click.echo(_run_graphql(obj, name="validate-account", variables=kwargs))
351349

352350

353351
@account.command()
354352
@snippet_options("list-accounts")
355-
@click.pass_context
356-
def validate_all(ctx, **kwargs):
353+
@click.pass_obj
354+
def validate_all(obj, **kwargs):
357355
"""
358356
Validate all accounts in Stacklet
359357
"""
360-
result = _run_graphql(ctx=ctx, name="list-accounts", variables=kwargs, raw=True)
358+
result = _run_graphql(obj, name="list-accounts", variables=kwargs, raw=True)
361359

362360
# get all the accounts
363361
count = result["data"]["accounts"]["pageInfo"]["total"]
364362
kwargs["last"] = count
365363

366-
result = _run_graphql(ctx=ctx, name="list-accounts", variables=kwargs, raw=True)
364+
result = _run_graphql(obj, name="list-accounts", variables=kwargs, raw=True)
367365
account_provider_pairs = [
368366
{"provider": r["node"]["provider"], "key": r["node"]["key"]}
369367
for r in result["data"]["accounts"]["edges"]
370368
]
371369
for pair in account_provider_pairs:
372-
click.echo(_run_graphql(ctx=ctx, name="validate-account", variables=pair))
370+
click.echo(_run_graphql(obj, name="validate-account", variables=pair))

0 commit comments

Comments
 (0)