leverage 3.0.0__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {leverage-3.0.0 → leverage-3.1.0}/PKG-INFO +1 -1
- {leverage-3.0.0 → leverage-3.1.0}/leverage/__init__.py +1 -1
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/auth.py +238 -55
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/aws.py +24 -2
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/tf.py +3 -5
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/utils.py +18 -9
- {leverage-3.0.0 → leverage-3.1.0}/pyproject.toml +1 -1
- {leverage-3.0.0 → leverage-3.1.0}/LICENSE.txt +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/README.md +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/_backend_config.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/_internals.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/_parsing.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/_utils.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/conf.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/leverage.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/logger.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/__init__.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/credentials.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/kubectl.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/project.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/run.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/runner.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/tfautomv.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/modules/tfrunner.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/path.py +0 -0
- {leverage-3.0.0 → leverage-3.1.0}/leverage/tasks.py +0 -0
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
5
|
from functools import wraps
|
|
6
6
|
from configparser import NoSectionError, NoOptionError
|
|
7
|
+
from typing import Tuple
|
|
7
8
|
|
|
8
9
|
import boto3
|
|
9
10
|
import click
|
|
@@ -57,6 +58,23 @@ def get_layer_profile(raw_profile: str, config_updater: ConfigUpdater, tf_profil
|
|
|
57
58
|
return account_id, account_name, sso_role, layer_profile
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def _get_sso_profile_from_section(
|
|
62
|
+
config_updater: ConfigUpdater, profile_section: str, project: str
|
|
63
|
+
) -> Tuple[str, str, str, str]:
|
|
64
|
+
"""
|
|
65
|
+
Read account_id, account_name, sso_role and build layer_profile from an SSO config section.
|
|
66
|
+
|
|
67
|
+
Section name format: "profile <project>-sso-<account_name>".
|
|
68
|
+
Used by refresh_all_accounts_credentials, where iteration starts from already-known
|
|
69
|
+
section names rather than profile references parsed from .tf files.
|
|
70
|
+
"""
|
|
71
|
+
account_name = profile_section.replace(f"profile {project}-sso-", "")
|
|
72
|
+
account_id = config_updater.get(profile_section, "account_id").value
|
|
73
|
+
sso_role = config_updater.get(profile_section, "role_name").value
|
|
74
|
+
layer_profile = f"{project}-{account_name}-{sso_role.lower()}"
|
|
75
|
+
return account_id, account_name, sso_role, layer_profile
|
|
76
|
+
|
|
77
|
+
|
|
60
78
|
def update_config_section(updater: ConfigUpdater, layer_profile: str, data: dict):
|
|
61
79
|
"""
|
|
62
80
|
Update the <layer_profile> section with the values given on <data>.
|
|
@@ -174,13 +192,16 @@ def check_sso_token(paths: PathsHandler):
|
|
|
174
192
|
)
|
|
175
193
|
|
|
176
194
|
|
|
177
|
-
def refresh_layer_credentials(paths: PathsHandler):
|
|
195
|
+
def refresh_layer_credentials(paths: PathsHandler) -> None:
|
|
178
196
|
tf_profile, raw_profiles = get_profiles(paths)
|
|
179
197
|
config_updater = ConfigUpdater()
|
|
180
198
|
config_updater.read(paths.aws_config_file)
|
|
181
199
|
|
|
182
200
|
region = config_updater.get(f"profile {paths.project}-sso", "sso_region").value
|
|
183
201
|
client = boto3.client("sso", region_name=region)
|
|
202
|
+
access_token = _get_sso_token_or_raise(paths)
|
|
203
|
+
credentials_updater = None # created lazily on the first profile that needs to be written
|
|
204
|
+
|
|
184
205
|
for raw in raw_profiles:
|
|
185
206
|
try:
|
|
186
207
|
account_id, account_name, sso_role, layer_profile = get_layer_profile(
|
|
@@ -192,65 +213,227 @@ def refresh_layer_credentials(paths: PathsHandler):
|
|
|
192
213
|
except SkipProfile:
|
|
193
214
|
continue
|
|
194
215
|
|
|
195
|
-
|
|
216
|
+
if _check_credentials_expiration(config_updater, layer_profile):
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if credentials_updater is None:
|
|
220
|
+
credentials_updater = _get_credentials_updater(paths)
|
|
221
|
+
|
|
222
|
+
_retrieve_and_update_credentials(
|
|
223
|
+
paths=paths,
|
|
224
|
+
client=client,
|
|
225
|
+
access_token=access_token,
|
|
226
|
+
account_id=account_id,
|
|
227
|
+
sso_role=sso_role,
|
|
228
|
+
layer_profile=layer_profile,
|
|
229
|
+
account_name=account_name,
|
|
230
|
+
config_updater=config_updater,
|
|
231
|
+
credentials_updater=credentials_updater,
|
|
232
|
+
raise_on_permission_error=True,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _check_credentials_expiration(
|
|
237
|
+
config_updater: ConfigUpdater, layer_profile: str, force_refresh: bool = False
|
|
238
|
+
) -> bool:
|
|
239
|
+
"""
|
|
240
|
+
Check if credentials need to be renewed based on expiration time.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
config_updater: ConfigUpdater instance with the config file loaded.
|
|
244
|
+
layer_profile: The layer profile name to check.
|
|
245
|
+
force_refresh: If True, skip expiration check and return False (needs refresh).
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
bool: True if credentials are still valid and should be skipped, False if they need refresh.
|
|
249
|
+
"""
|
|
250
|
+
if force_refresh:
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
expiration = int(config_updater.get(f"profile {layer_profile}", "expiration").value) / 1000
|
|
255
|
+
except (NoSectionError, NoOptionError):
|
|
256
|
+
# first time using this profile, skip into the credential's retrieval step
|
|
257
|
+
logger.debug("No cached credentials found.")
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# we reduce the validity 30 minutes, to avoid expiration over long-standing tasks
|
|
261
|
+
renewal = time.time() + (30 * 60)
|
|
262
|
+
logger.debug(f"Token expiration time: {expiration}")
|
|
263
|
+
logger.debug(f"Token renewal time: {renewal}")
|
|
264
|
+
if renewal < expiration:
|
|
265
|
+
# still valid, nothing to do with this profile!
|
|
266
|
+
logger.info("Using already configured temporary credentials.")
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _get_credentials_updater(paths: PathsHandler) -> ConfigUpdater:
|
|
273
|
+
"""
|
|
274
|
+
Prepare and return a ConfigUpdater for the credentials file.
|
|
275
|
+
"""
|
|
276
|
+
paths.aws_credentials_file.touch(exist_ok=True)
|
|
277
|
+
credentials_updater = ConfigUpdater()
|
|
278
|
+
credentials_updater.read(paths.aws_credentials_file)
|
|
279
|
+
return credentials_updater
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _retrieve_and_update_credentials(
|
|
283
|
+
paths: PathsHandler,
|
|
284
|
+
client,
|
|
285
|
+
access_token: str,
|
|
286
|
+
account_id: str,
|
|
287
|
+
sso_role: str,
|
|
288
|
+
layer_profile: str,
|
|
289
|
+
account_name: str,
|
|
290
|
+
config_updater: ConfigUpdater,
|
|
291
|
+
credentials_updater: ConfigUpdater,
|
|
292
|
+
raise_on_permission_error: bool = True,
|
|
293
|
+
) -> bool:
|
|
294
|
+
"""
|
|
295
|
+
Retrieve credentials from SSO and update both config and credentials files.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
paths: PathsHandler instance.
|
|
299
|
+
client: boto3 SSO client.
|
|
300
|
+
access_token: SSO access token.
|
|
301
|
+
account_id: AWS account ID.
|
|
302
|
+
sso_role: SSO role name.
|
|
303
|
+
layer_profile: Layer profile name.
|
|
304
|
+
account_name: Account name for logging.
|
|
305
|
+
config_updater: ConfigUpdater instance for config file.
|
|
306
|
+
credentials_updater: ConfigUpdater instance for credentials file.
|
|
307
|
+
raise_on_permission_error: If True, raise ExitError on permission errors.
|
|
308
|
+
If False, log warning and return False.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
bool: True if successful, False if permission error occurred (only when raise_on_permission_error=False).
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
ExitError: If raise_on_permission_error=True and permission error occurs, or for any other ClientError.
|
|
315
|
+
"""
|
|
316
|
+
logger.debug(f"Retrieving role credentials for {sso_role}...")
|
|
317
|
+
try:
|
|
318
|
+
credentials = client.get_role_credentials(
|
|
319
|
+
roleName=sso_role,
|
|
320
|
+
accountId=account_id,
|
|
321
|
+
accessToken=access_token,
|
|
322
|
+
)["roleCredentials"]
|
|
323
|
+
except ClientError as error:
|
|
324
|
+
if error.response["Error"]["Code"] in ("AccessDeniedException", "ForbiddenException"):
|
|
325
|
+
error_msg = (
|
|
326
|
+
f"User does not have permission to assume role [bold]{sso_role}[/bold]"
|
|
327
|
+
" in this account.\nPlease check with your administrator or try"
|
|
328
|
+
" running [bold]leverage aws configure sso[/bold]."
|
|
329
|
+
)
|
|
330
|
+
if raise_on_permission_error:
|
|
331
|
+
raise ExitError(40, error_msg) from error
|
|
332
|
+
logger.warning(f"No permission to assume role [bold]{sso_role}[/bold] in {account_name} account. Skipping.")
|
|
333
|
+
return False
|
|
334
|
+
raise ExitError(50, f"Error retrieving role credentials: {error}") from error
|
|
335
|
+
|
|
336
|
+
# Update expiration on aws/<project>/config
|
|
337
|
+
logger.info(f"Writing {layer_profile} profile")
|
|
338
|
+
update_config_section(
|
|
339
|
+
config_updater,
|
|
340
|
+
f"profile {layer_profile}",
|
|
341
|
+
data={
|
|
342
|
+
"expiration": credentials["expiration"],
|
|
343
|
+
},
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Write credentials on aws/<project>/credentials
|
|
347
|
+
update_config_section(
|
|
348
|
+
credentials_updater,
|
|
349
|
+
layer_profile,
|
|
350
|
+
data={
|
|
351
|
+
"aws_access_key_id": credentials["accessKeyId"],
|
|
352
|
+
"aws_secret_access_key": credentials["secretAccessKey"],
|
|
353
|
+
"aws_session_token": credentials["sessionToken"],
|
|
354
|
+
},
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
logger.info(f"Credentials for {account_name} account written successfully.")
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _get_sso_token_or_raise(paths: PathsHandler) -> str:
|
|
362
|
+
"""Get the SSO access token, or raise a friendly ExitError on failure.
|
|
363
|
+
|
|
364
|
+
Wraps file/JSON/key errors so callers (e.g. `leverage aws sso refresh` invoked without
|
|
365
|
+
a prior `check_sso_token`) get a clear message instead of a raw stacktrace.
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
return get_sso_access_token(paths.sso_token_file)
|
|
369
|
+
except (OSError, ValueError, KeyError) as e:
|
|
370
|
+
raise ExitError(1, f"Failed to get SSO access token: {e}") from e
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def refresh_all_accounts_credentials(paths: PathsHandler, force_refresh: bool = False) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Refresh credentials for all configured SSO accounts.
|
|
376
|
+
|
|
377
|
+
Iterates through every `profile <project>-sso-<account>` section in the AWS config
|
|
378
|
+
file and refreshes their credentials using the current SSO access token.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
paths: PathsHandler instance with access to project paths and configuration.
|
|
382
|
+
force_refresh: If True, refresh all credentials regardless of expiration.
|
|
383
|
+
If False, only refresh expired credentials.
|
|
384
|
+
"""
|
|
385
|
+
logger.info("Refreshing credentials for all accounts...")
|
|
386
|
+
|
|
387
|
+
config_updater = ConfigUpdater()
|
|
388
|
+
config_updater.read(paths.aws_config_file)
|
|
389
|
+
# SSO account profiles use the section name "profile <project>-sso-<account>"; the trailing
|
|
390
|
+
# dash naturally excludes the parent "profile <project>-sso" section.
|
|
391
|
+
sso_prefix = f"profile {paths.project}-sso-"
|
|
392
|
+
sso_profiles = [s for s in config_updater.sections() if s.startswith(sso_prefix)]
|
|
393
|
+
|
|
394
|
+
if not sso_profiles:
|
|
395
|
+
logger.warning("No SSO account profiles found. Run 'leverage aws configure sso' first.")
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
logger.info(f"Found {len(sso_profiles)} account(s) to refresh.")
|
|
399
|
+
region = config_updater.get(f"profile {paths.project}-sso", "sso_region").value
|
|
400
|
+
client = boto3.client("sso", region_name=region)
|
|
401
|
+
access_token = _get_sso_token_or_raise(paths)
|
|
402
|
+
credentials_updater = _get_credentials_updater(paths)
|
|
403
|
+
|
|
404
|
+
refreshed = skipped = failed = 0
|
|
405
|
+
for profile_section in sso_profiles:
|
|
196
406
|
try:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
renewal = time.time() + (30 * 60)
|
|
204
|
-
logger.debug(f"Token expiration time: {expiration}")
|
|
205
|
-
logger.debug(f"Token renewal time: {renewal}")
|
|
206
|
-
if renewal < expiration:
|
|
207
|
-
# still valid, nothing to do with these profile!
|
|
208
|
-
logger.info("Using already configured temporary credentials.")
|
|
407
|
+
account_id, account_name, sso_role, layer_profile = _get_sso_profile_from_section(
|
|
408
|
+
config_updater, profile_section, paths.project
|
|
409
|
+
)
|
|
410
|
+
if _check_credentials_expiration(config_updater, layer_profile, force_refresh=force_refresh):
|
|
411
|
+
logger.info(f"Credentials for {account_name} account are still valid, skipping.")
|
|
412
|
+
skipped += 1
|
|
209
413
|
continue
|
|
210
414
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
415
|
+
logger.info(f"Retrieving credentials for {account_name} account...")
|
|
416
|
+
if _retrieve_and_update_credentials(
|
|
417
|
+
paths=paths,
|
|
418
|
+
client=client,
|
|
419
|
+
access_token=access_token,
|
|
420
|
+
account_id=account_id,
|
|
421
|
+
sso_role=sso_role,
|
|
422
|
+
layer_profile=layer_profile,
|
|
423
|
+
account_name=account_name,
|
|
424
|
+
config_updater=config_updater,
|
|
425
|
+
credentials_updater=credentials_updater,
|
|
426
|
+
raise_on_permission_error=False,
|
|
427
|
+
):
|
|
428
|
+
logger.info(f"Credentials for {account_name} account refreshed successfully.")
|
|
429
|
+
refreshed += 1
|
|
227
430
|
else:
|
|
228
|
-
|
|
431
|
+
failed += 1
|
|
432
|
+
except Exception as e:
|
|
433
|
+
logger.exception(f"Failed to refresh credentials for {profile_section}: {e}")
|
|
434
|
+
failed += 1
|
|
229
435
|
|
|
230
|
-
|
|
231
|
-
logger.info(f"Writing {layer_profile} profile")
|
|
232
|
-
update_config_section(
|
|
233
|
-
config_updater,
|
|
234
|
-
f"profile {layer_profile}",
|
|
235
|
-
data={
|
|
236
|
-
"expiration": credentials["expiration"],
|
|
237
|
-
},
|
|
238
|
-
)
|
|
239
|
-
# write credentials on aws/<project>/credentials (create the file if it doesn't exist first)
|
|
240
|
-
paths.aws_credentials_file.touch(exist_ok=True)
|
|
241
|
-
credentials_updater = ConfigUpdater()
|
|
242
|
-
credentials_updater.read(paths.aws_credentials_file)
|
|
243
|
-
|
|
244
|
-
update_config_section(
|
|
245
|
-
credentials_updater,
|
|
246
|
-
layer_profile,
|
|
247
|
-
data={
|
|
248
|
-
"aws_access_key_id": credentials["accessKeyId"],
|
|
249
|
-
"aws_secret_access_key": credentials["secretAccessKey"],
|
|
250
|
-
"aws_session_token": credentials["sessionToken"],
|
|
251
|
-
},
|
|
252
|
-
)
|
|
253
|
-
logger.info(f"Credentials for {account_name} account written successfully.")
|
|
436
|
+
logger.info(f"Credential refresh complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.")
|
|
254
437
|
|
|
255
438
|
|
|
256
439
|
def refresh_layer_credentials_mfa(paths: PathsHandler):
|
|
@@ -15,7 +15,7 @@ from leverage.modules.runner import Runner
|
|
|
15
15
|
from leverage.modules.utils import _handle_subcommand
|
|
16
16
|
from leverage._utils import get_or_create_section, ExitError
|
|
17
17
|
from leverage._internals import pass_state, pass_runner, pass_paths
|
|
18
|
-
from leverage.modules.auth import get_sso_access_token
|
|
18
|
+
from leverage.modules.auth import get_sso_access_token, refresh_all_accounts_credentials
|
|
19
19
|
from leverage.modules.auth import _perform_authentication as perform_authentication
|
|
20
20
|
|
|
21
21
|
|
|
@@ -217,9 +217,15 @@ def sso(context: click.Context, awscli: Runner, args: Tuple[str, ...]) -> None:
|
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
@sso.command()
|
|
220
|
+
@click.option(
|
|
221
|
+
"--refresh-all",
|
|
222
|
+
is_flag=True,
|
|
223
|
+
default=False,
|
|
224
|
+
help="Automatically refresh credentials for all configured accounts after login.",
|
|
225
|
+
)
|
|
220
226
|
@pass_paths
|
|
221
227
|
@pass_runner
|
|
222
|
-
def login(awscli: Runner, paths: PathsHandler) -> None:
|
|
228
|
+
def login(awscli: Runner, paths: PathsHandler, refresh_all: bool) -> None:
|
|
223
229
|
"""Login"""
|
|
224
230
|
exit_code, region, _ = awscli.exec("configure", "get", "sso_region", "--profile", f"{paths.project}-sso")
|
|
225
231
|
if exit_code:
|
|
@@ -299,6 +305,22 @@ def login(awscli: Runner, paths: PathsHandler) -> None:
|
|
|
299
305
|
|
|
300
306
|
logger.info(f"Successfully logged in!.")
|
|
301
307
|
|
|
308
|
+
if refresh_all:
|
|
309
|
+
refresh_all_accounts_credentials(paths, force_refresh=False)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@sso.command()
|
|
313
|
+
@click.option(
|
|
314
|
+
"--force",
|
|
315
|
+
is_flag=True,
|
|
316
|
+
default=False,
|
|
317
|
+
help="Force refresh all credentials, even if they haven't expired.",
|
|
318
|
+
)
|
|
319
|
+
@pass_paths
|
|
320
|
+
def refresh(paths: PathsHandler, force: bool) -> None:
|
|
321
|
+
"""Refresh credentials for all configured accounts"""
|
|
322
|
+
refresh_all_accounts_credentials(paths, force_refresh=force)
|
|
323
|
+
|
|
302
324
|
|
|
303
325
|
@sso.command()
|
|
304
326
|
@pass_paths
|
|
@@ -22,8 +22,7 @@ REGION = r"(global|([a-z]{2}(-gov)?)-(central|(north|south)?(east|west)?)-\d)"
|
|
|
22
22
|
@click.group()
|
|
23
23
|
@pass_state
|
|
24
24
|
def tofu(state):
|
|
25
|
-
"""Run OpenTofu commands in
|
|
26
|
-
with your cloud provider such as handling multi factor authentication for you.
|
|
25
|
+
"""Run OpenTofu commands in the context of the current project.
|
|
27
26
|
All tofu subcommands that receive extra args will pass the given strings as is to their corresponding OpenTofu
|
|
28
27
|
counterparts in the container. For example as in `leverage tofu apply -auto-approve` or
|
|
29
28
|
`leverage tofu init -reconfigure`
|
|
@@ -34,8 +33,7 @@ def tofu(state):
|
|
|
34
33
|
@click.group()
|
|
35
34
|
@pass_state
|
|
36
35
|
def terraform(state):
|
|
37
|
-
"""Run Terraform commands in
|
|
38
|
-
with your cloud provider such as handling multi factor authentication for you.
|
|
36
|
+
"""Run Terraform commands in the context of the current project.
|
|
39
37
|
All terraform subcommands that receive extra args will pass the given strings as is to their corresponding Terraform
|
|
40
38
|
counterparts in the container. For example as in `leverage terraform apply -auto-approve` or
|
|
41
39
|
`leverage terraform init -reconfigure`
|
|
@@ -272,7 +270,7 @@ def invoke_for_all_commands(paths, layers, command, *args: Sequence[str], skip_v
|
|
|
272
270
|
validate_for_all_commands(layer, skip_validation=skip_validation)
|
|
273
271
|
|
|
274
272
|
# set the s3 key
|
|
275
|
-
if not get_backend_key(layer / "config.tf"):
|
|
273
|
+
if not skip_validation and not get_backend_key(layer / "config.tf"):
|
|
276
274
|
backend_key_base = f"{paths.cwd.relative_to(paths.root_dir).as_posix()}/terraform.tfstate"
|
|
277
275
|
backend_key = backend_key_base.replace("/base-", "/").replace("/tools-", "/")
|
|
278
276
|
set_backend_key(layer / "config.tf", backend_key)
|
|
@@ -24,7 +24,13 @@ def _handle_subcommand(
|
|
|
24
24
|
Raises:
|
|
25
25
|
Exit: Whenever runner execution returns a non-zero exit code
|
|
26
26
|
"""
|
|
27
|
-
|
|
27
|
+
# When this function is called from a click subcommand callback (e.g. the `sso` group),
|
|
28
|
+
# `args` is the subcontext's args slice and no longer contains the caller's own name. Treat
|
|
29
|
+
# that as "search from the start" rather than crashing on ValueError.
|
|
30
|
+
try:
|
|
31
|
+
caller_pos = args.index(caller_name) if caller_name is not None else 0
|
|
32
|
+
except ValueError:
|
|
33
|
+
caller_pos = 0
|
|
28
34
|
|
|
29
35
|
# Find if one of the wrapped subcommand was invoked
|
|
30
36
|
wrapped_subcommands = context.command.commands.keys()
|
|
@@ -38,12 +44,15 @@ def _handle_subcommand(
|
|
|
38
44
|
raise Exit(exit_code)
|
|
39
45
|
|
|
40
46
|
subcommand = context.command.commands.get(subcommand)
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
# Build the subcommand's own context from its slice of the CLI args and invoke it through that
|
|
48
|
+
# context. We can't use context.invoke / context.forward here: invoke ignores user-supplied
|
|
49
|
+
# options (everything falls back to defaults) and forward copies the parent group's params
|
|
50
|
+
# (notably `args`) into the child callback's kwargs, which raises TypeError as soon as the
|
|
51
|
+
# child has its own click options (e.g. `sso login --refresh-all`, `sso refresh --force`).
|
|
52
|
+
sub_ctx = subcommand.make_context(
|
|
53
|
+
info_name=subcommand.name,
|
|
54
|
+
args=list(args)[args.index(subcommand.name) + 1 :],
|
|
55
|
+
parent=context,
|
|
44
56
|
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
context.invoke(subcommand)
|
|
48
|
-
else:
|
|
49
|
-
context.forward(subcommand)
|
|
57
|
+
with sub_ctx:
|
|
58
|
+
sub_ctx.command.invoke(sub_ctx)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|