leverage 2.2.0rc0__tar.gz → 2.2.0rc2__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.
Files changed (32) hide show
  1. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/PKG-INFO +1 -2
  2. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/__init__.py +7 -1
  3. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/_internals.py +1 -26
  4. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/_utils.py +4 -4
  5. leverage-2.2.0rc2/leverage/leverage.py +68 -0
  6. leverage-2.2.0rc2/leverage/modules/__init__.py +6 -0
  7. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/auth.py +15 -62
  8. leverage-2.2.0rc2/leverage/modules/aws.py +225 -0
  9. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/credentials.py +3 -0
  10. leverage-2.2.0rc2/leverage/modules/shell.py +44 -0
  11. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/tf.py +181 -178
  12. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/utils.py +15 -12
  13. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/path.py +59 -14
  14. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/pyproject.toml +1 -3
  15. leverage-2.2.0rc0/leverage/_backend_config.py +0 -172
  16. leverage-2.2.0rc0/leverage/leverage.py +0 -50
  17. leverage-2.2.0rc0/leverage/modules/__init__.py +0 -9
  18. leverage-2.2.0rc0/leverage/modules/aws.py +0 -338
  19. leverage-2.2.0rc0/leverage/modules/runner.py +0 -124
  20. leverage-2.2.0rc0/leverage/modules/tfrunner.py +0 -72
  21. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/LICENSE.txt +0 -0
  22. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/README.md +0 -0
  23. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/_parsing.py +0 -0
  24. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/conf.py +0 -0
  25. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/container.py +0 -0
  26. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/containers/kubectl.py +0 -0
  27. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/logger.py +0 -0
  28. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/kubectl.py +0 -0
  29. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/project.py +0 -0
  30. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/run.py +0 -0
  31. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/modules/tfautomv.py +0 -0
  32. {leverage-2.2.0rc0 → leverage-2.2.0rc2}/leverage/tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: leverage
3
- Version: 2.2.0rc0
3
+ Version: 2.2.0rc2
4
4
  Summary: Binbash Leverage Command-Line tool.
5
5
  Home-page: https://github.com/binbashar/leverage
6
6
  License: MIT
@@ -25,7 +25,6 @@ Requires-Dist: docker (==6.1.0)
25
25
  Requires-Dist: dockerpty (==0.4.1)
26
26
  Requires-Dist: docutils (==0.17.1)
27
27
  Requires-Dist: jinja2 (==3.0.1)
28
- Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
29
28
  Requires-Dist: python-hcl2 (==7.3.1)
30
29
  Requires-Dist: questionary (==1.10.0)
31
30
  Requires-Dist: requests (==2.31)
@@ -4,7 +4,13 @@
4
4
 
5
5
  # pylint: disable=wrong-import-position
6
6
 
7
- __version__ = "2.2.0rc0"
7
+ __version__ = "2.2.0rc2"
8
+ __toolbox_version__ = "1.3.5-0.2.0"
9
+
10
+ MINIMUM_VERSIONS = {
11
+ "TERRAFORM": "1.3.5",
12
+ "TOOLBOX": "0.2.4",
13
+ }
8
14
 
9
15
  import sys
10
16
  from shutil import which
@@ -27,9 +27,7 @@ class State:
27
27
  def __init__(self):
28
28
  self._verbosity = None
29
29
  self.module = Module()
30
- self.config = None
31
- self.paths = None
32
- self.runner = None
30
+ self.container = None
33
31
 
34
32
  @property
35
33
  def verbosity(self):
@@ -53,26 +51,3 @@ def pass_container(command):
53
51
  return command(ctx.obj.container, *args, **kwargs)
54
52
 
55
53
  return new_command
56
-
57
- def pass_runner(command):
58
- """Decorator to pass the current runner (Terraform/OpenTofu runner) to the command."""
59
-
60
- @wraps(command)
61
- def new_command(*args, **kwargs):
62
- ctx = click.get_current_context()
63
-
64
- return command(ctx.obj.runner, *args, **kwargs)
65
-
66
- return new_command
67
-
68
-
69
- def pass_paths(command):
70
- """Decorator to pass the current project paths to the command."""
71
-
72
- @wraps(command)
73
- def new_command(*args, **kwargs):
74
- ctx = click.get_current_context()
75
-
76
- return command(ctx.obj.paths, *args, **kwargs)
77
-
78
- return new_command
@@ -3,8 +3,8 @@
3
3
  """
4
4
 
5
5
  from pathlib import Path
6
- from subprocess import PIPE, run
7
- from typing import List, Optional
6
+ from subprocess import run
7
+ from subprocess import PIPE
8
8
 
9
9
  import hcl2
10
10
  import lark
@@ -153,12 +153,12 @@ class ContainerSession:
153
153
  self.docker_client.api.remove_container(self.container_data)
154
154
 
155
155
 
156
- def key_finder(d: dict, target: str, avoid: Optional[str] = None) -> List[str]:
156
+ def key_finder(d: dict, target: str, avoid: str = None):
157
157
  """
158
158
  Iterate over a dict of dicts and/or lists of dicts, looking for a key with value "target".
159
159
  Collect and return all the values that matches "target" as key.
160
160
  """
161
- values: List[str] = []
161
+ values = []
162
162
 
163
163
  for key, value in d.items():
164
164
  if isinstance(value, dict):
@@ -0,0 +1,68 @@
1
+ """
2
+ Binbash Leverage Command-line tool.
3
+ """
4
+
5
+ import rich
6
+ from packaging.version import Version
7
+
8
+ import click
9
+
10
+ from leverage import __version__, conf, MINIMUM_VERSIONS
11
+ from leverage._internals import pass_state
12
+ from leverage.modules.aws import aws
13
+ from leverage.modules.credentials import credentials
14
+ from leverage.modules import run, project, tofu, terraform, tfautomv, kubectl, shell
15
+ from leverage.path import NotARepositoryError
16
+
17
+
18
+ @click.group(invoke_without_command=True)
19
+ @click.option("-v", "--verbose", is_flag=True, help="Increase output verbosity.")
20
+ @click.version_option(version=__version__)
21
+ @pass_state
22
+ @click.pass_context
23
+ def leverage(context, state, verbose):
24
+ """Leverage Reference Architecture projects command-line tool."""
25
+ # --verbose | -v
26
+ state.verbosity = verbose
27
+ if context.invoked_subcommand is None:
28
+ # leverage called with no subcommand
29
+ click.echo(context.get_help())
30
+
31
+ # if there is a version restriction set, make sure we satisfy it
32
+ try:
33
+ config = conf.load()
34
+ except NotARepositoryError:
35
+ # restrictions are only verified within a leverage project
36
+ return
37
+
38
+ # check if the current versions are lower than the minimum required
39
+ if not (image_tag := config.get("TF_IMAGE_TAG", config.get("TERRAFORM_IMAGE_TAG"))):
40
+ # at some points of the project (the init), the config file is not created yet
41
+ return
42
+
43
+ # validate both TOOLBOX and TF versions
44
+ image_versions = image_tag.split("-")
45
+ if "tofu" not in image_versions:
46
+ versions = zip(MINIMUM_VERSIONS, image_versions)
47
+ else:
48
+ versions = {"TOOLBOX": image_versions[-1]}.items()
49
+
50
+ for key, current in versions:
51
+ if Version(current) < Version(MINIMUM_VERSIONS[key]):
52
+ rich.print(
53
+ f"[red]WARNING[/red]\tYour current {key} version ({current}) is lower than the required minimum ({MINIMUM_VERSIONS[key]})."
54
+ )
55
+
56
+
57
+ # Add modules to leverage
58
+ leverage.add_command(run)
59
+ leverage.add_command(project)
60
+ leverage.add_command(tofu)
61
+ leverage.add_command(tofu, name="tf")
62
+ leverage.add_command(terraform)
63
+ leverage.add_command(credentials)
64
+ leverage.add_command(aws)
65
+ leverage.add_command(tfautomv)
66
+ leverage.add_command(kubectl)
67
+ leverage.add_command(kubectl, name="kc")
68
+ leverage.add_command(shell)
@@ -0,0 +1,6 @@
1
+ from .run import run
2
+ from .project import project
3
+ from .tf import tofu, terraform
4
+ from .tfautomv import tfautomv
5
+ from .kubectl import kubectl
6
+ from .shell import shell
@@ -1,8 +1,5 @@
1
1
  import time
2
- import json
3
2
  from pathlib import Path
4
- from datetime import datetime
5
- from dateutil.tz import tzutc
6
3
  from configparser import NoSectionError, NoOptionError
7
4
 
8
5
  import boto3
@@ -10,7 +7,6 @@ from botocore.exceptions import ClientError
10
7
  from configupdater import ConfigUpdater
11
8
 
12
9
  from leverage import logger
13
- from leverage.path import PathsHandler
14
10
  from leverage._utils import key_finder, ExitError, get_or_create_section, parse_tf_file
15
11
 
16
12
 
@@ -25,14 +21,10 @@ def get_layer_profile(raw_profile: str, config_updater: ConfigUpdater, tf_profil
25
21
  raise SkipProfile
26
22
 
27
23
  # if it is exactly that variable, we already know the layer profile is tf_profile
28
- layer_profile = tf_profile if raw_profile in ("${var.profile}", "each.value.profile") else None
24
+ layer_profile = tf_profile if raw_profile == "${var.profile}" else None
29
25
 
30
26
  # replace variables with their corresponding values
31
- raw = (
32
- raw_profile.replace("${var.profile}", tf_profile)
33
- .replace("${var.project}", project)
34
- .replace("each.value.profile", tf_profile)
35
- )
27
+ raw = raw_profile.replace("${var.profile}", tf_profile).replace("${var.project}", project)
36
28
 
37
29
  # the project and the role are at the beginning and end of the string
38
30
  _, *account_name, _ = raw.split("-")
@@ -65,15 +57,15 @@ def update_config_section(updater: ConfigUpdater, layer_profile: str, data: dict
65
57
  updater.update_file()
66
58
 
67
59
 
68
- def get_profiles(paths: PathsHandler):
60
+ def get_profiles(cli):
69
61
  """
70
62
  Get the AWS profiles present on the layer by parsing some tf files.
71
63
  """
72
64
  raw_profiles = set()
73
65
  # these are files from the layer we are currently on
74
- for name in ("config.tf", "locals.tf", "runtime.tf"):
66
+ for name in ("config.tf", "locals.tf"):
75
67
  try:
76
- tf_config = parse_tf_file(Path(paths.cwd / name))
68
+ tf_config = parse_tf_file(Path(cli.paths.cwd / name))
77
69
  except FileNotFoundError:
78
70
  continue
79
71
 
@@ -82,65 +74,25 @@ def get_profiles(paths: PathsHandler):
82
74
  raw_profiles.update(set(key_finder(tf_config, "profile", "lookup")))
83
75
 
84
76
  # the profile value from <layer>/config/backend.tfvars
85
- backend_config = parse_tf_file(paths.backend_tfvars)
77
+ backend_config = parse_tf_file(cli.paths.local_backend_tfvars)
86
78
  tf_profile = backend_config["profile"]
87
79
 
88
80
  return tf_profile, raw_profiles
89
81
 
90
82
 
91
- def get_sso_access_token(sso_token_file: Path) -> str:
92
- """
93
- Get the SSO access token from the token file.
94
- """
95
- return json.loads(sso_token_file.read_text())["accessToken"]
96
-
97
-
98
- def check_sso_token(paths: PathsHandler):
99
- """Check for the existence and validity of the SSO token to be used to get credentials."""
100
-
101
- # Adding `token` file name to this function in order to
102
- # meet the requirement regarding to have just one
103
- # token file in the sso/cache
104
- sso_role = paths.account_conf.get("sso_role")
105
- token_file = paths.sso_cache / sso_role
106
-
107
- token_files = list(paths.sso_cache.glob("*"))
108
- if not token_files:
109
- raise ExitError(1, "No AWS SSO token found. Please log in or configure SSO.")
110
-
111
- if token_file not in token_files and paths.sso_token_file not in token_files:
112
- raise ExitError(
113
- 1,
114
- "No valid AWS SSO token found for current account.\n"
115
- "Please log out and reconfigure SSO before proceeding.",
116
- )
117
-
118
- token = json.loads(paths.sso_token_file.read_text())
119
- expiry = datetime.strptime(token.get("expiresAt"), "%Y-%m-%dT%H:%M:%SZ")
120
- renewal = datetime.now()
121
-
122
- if expiry < renewal:
123
- raise ExitError(
124
- 1,
125
- "AWS SSO token has expired, please log back in by running [bold]leverage aws sso login[/bold]"
126
- " to refresh your credentials before re-running the last command.",
127
- )
128
-
129
-
130
- def refresh_layer_credentials(paths: PathsHandler):
131
- tf_profile, raw_profiles = get_profiles(paths)
83
+ def refresh_layer_credentials(cli):
84
+ tf_profile, raw_profiles = get_profiles(cli)
132
85
  config_updater = ConfigUpdater()
133
- config_updater.read(paths.aws_config_file)
86
+ config_updater.read(cli.paths.host_aws_profiles_file)
134
87
 
135
- region = config_updater.get(f"profile {paths.project}-sso", "sso_region").value
136
- client = boto3.client("sso", region_name=region)
88
+ client = boto3.client("sso", region_name=cli.sso_region_from_main_profile)
137
89
  for raw in raw_profiles:
138
90
  try:
139
91
  account_id, account_name, sso_role, layer_profile = get_layer_profile(
140
92
  raw,
141
93
  config_updater,
142
94
  tf_profile,
143
- paths.project,
95
+ cli.project,
144
96
  )
145
97
  except SkipProfile:
146
98
  continue
@@ -167,7 +119,7 @@ def refresh_layer_credentials(paths: PathsHandler):
167
119
  credentials = client.get_role_credentials(
168
120
  roleName=sso_role,
169
121
  accountId=account_id,
170
- accessToken=get_sso_access_token(paths.sso_token_file),
122
+ accessToken=cli.get_sso_access_token(),
171
123
  )["roleCredentials"]
172
124
  except ClientError as error:
173
125
  if error.response["Error"]["Code"] in ("AccessDeniedException", "ForbiddenException"):
@@ -188,9 +140,10 @@ def refresh_layer_credentials(paths: PathsHandler):
188
140
  },
189
141
  )
190
142
  # write credentials on aws/<project>/credentials (create the file if it doesn't exist first)
191
- paths.aws_credentials_file.touch(exist_ok=True)
143
+ creds_path = Path(cli.paths.host_aws_credentials_file)
144
+ creds_path.touch(exist_ok=True)
192
145
  credentials_updater = ConfigUpdater()
193
- credentials_updater.read(paths.aws_credentials_file)
146
+ credentials_updater.read(cli.paths.host_aws_credentials_file)
194
147
 
195
148
  update_config_section(
196
149
  credentials_updater,
@@ -0,0 +1,225 @@
1
+ import boto3
2
+ import click
3
+ from click.exceptions import Exit
4
+ from configupdater import ConfigUpdater
5
+
6
+ from leverage import logger
7
+ from leverage._internals import pass_state
8
+ from leverage._internals import pass_container
9
+ from leverage._utils import get_or_create_section
10
+ from leverage.container import get_docker_client, SSOContainer
11
+ from leverage.container import AWSCLIContainer
12
+ from leverage.modules.utils import _handle_subcommand
13
+
14
+ CONTEXT_SETTINGS = {"ignore_unknown_options": True}
15
+
16
+
17
+ def get_account_roles(sso_client, access_token: str) -> dict:
18
+ """
19
+ Fetch the accounts and roles from the user.
20
+ """
21
+ account_roles = {}
22
+
23
+ accounts = sso_client.list_accounts(accessToken=access_token)
24
+ for account in accounts["accountList"]:
25
+ acc_role = sso_client.list_account_roles(
26
+ accessToken=access_token,
27
+ accountId=account["accountId"],
28
+ maxResults=1, # assume the first role is always the correct one
29
+ )["roleList"][0]
30
+
31
+ account_roles[account["accountName"]] = {
32
+ "account_id": account["accountId"],
33
+ "role_name": acc_role["roleName"],
34
+ }
35
+
36
+ return account_roles
37
+
38
+
39
+ def add_sso_profile(
40
+ config_updater: ConfigUpdater, section_name: str, role_name: str, account_id: str, region: str, start_url: str
41
+ ):
42
+ """
43
+ Add a profile to the config file.
44
+ """
45
+ section = get_or_create_section(config_updater, section_name)
46
+
47
+ data = {
48
+ "role_name": role_name,
49
+ "account_id": account_id,
50
+ "sso_region": region,
51
+ "sso_start_url": start_url,
52
+ }
53
+ for k, v in data.items():
54
+ # can't set a dict directly, so we need to go field by field
55
+ section[k] = v
56
+
57
+
58
+ def configure_sso_profiles(cli: SSOContainer):
59
+ """
60
+ Populate the ~./aws/<project>/config file with the sso profiles from the accounts.
61
+ """
62
+ updater = ConfigUpdater()
63
+ updater.read(cli.paths.host_aws_profiles_file)
64
+
65
+ # get values from the default profile first
66
+ default_sso_profile_name = f"profile {cli.project}-sso"
67
+ default_profile = updater[default_sso_profile_name]
68
+ region = default_profile["sso_region"].value
69
+ start_url = default_profile["sso_start_url"].value
70
+
71
+ # then set a profile for each account
72
+ access_token = cli.get_sso_access_token()
73
+ logger.info(f"Fetching accounts and roles...")
74
+ client = boto3.client("sso", region_name=region)
75
+ account_roles = get_account_roles(client, access_token)
76
+ for acc_name, values in account_roles.items():
77
+ # account names comes in the form of: {long project name}-{account name}
78
+ short_acc_name = acc_name.replace(cli.paths.project_long + "-", "")
79
+ section_name = f"profile {cli.project}-sso-{short_acc_name}"
80
+ logger.info(f"Adding {section_name}")
81
+ add_sso_profile(updater, section_name, values["role_name"], values["account_id"], region, start_url)
82
+
83
+ # save/update the profile file
84
+ updater.update_file()
85
+
86
+
87
+ @click.group(invoke_without_command=True, add_help_option=False, context_settings=CONTEXT_SETTINGS)
88
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
89
+ @pass_state
90
+ @click.pass_context
91
+ def aws(context, state, args):
92
+ """Run AWS CLI commands in a custom containerized environment."""
93
+ cli = AWSCLIContainer(get_docker_client())
94
+ state.container = cli
95
+ state.container.ensure_image()
96
+
97
+ _handle_subcommand(context=context, cli_container=cli, args=args)
98
+
99
+
100
+ @aws.group(invoke_without_command=True, add_help_option=False, context_settings=CONTEXT_SETTINGS)
101
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
102
+ @pass_container
103
+ @click.pass_context
104
+ def configure(context, cli, args):
105
+ """configure"""
106
+ _handle_subcommand(context=context, cli_container=cli, args=args, caller_name="configure")
107
+
108
+
109
+ @configure.command("sso")
110
+ @pass_container
111
+ @click.pass_context
112
+ def _sso(context, cli):
113
+ """configure sso"""
114
+ cli.paths.check_for_layer_location()
115
+
116
+ # region_primary was added in refarch v1
117
+ # for v2 it was replaced by region at project level
118
+ region_primary = "region_primary"
119
+ if "region_primary" not in cli.paths.common_conf:
120
+ region_primary = "region"
121
+ default_region = cli.paths.common_conf.get(region_primary, cli.paths.common_conf.get("sso_region"))
122
+ if default_region is None:
123
+ logger.error("No primary region configured in global config file.")
124
+ raise Exit(1)
125
+
126
+ logger.info("Configuring default profile.")
127
+ default_profile = {"region": default_region, "output": "json"}
128
+ for key, value in default_profile.items():
129
+ cli.exec(f"configure set {key} {value}", profile="default")
130
+
131
+ if not all(sso_key in cli.paths.common_conf for sso_key in ("sso_start_url", "sso_region")):
132
+ logger.error("Missing configuration values for SSO in global config file.")
133
+ raise Exit(1)
134
+
135
+ sso_role = cli.paths.account_conf.get("sso_role")
136
+ if not sso_role:
137
+ logger.error("Missing SSO role in account config file.")
138
+ raise Exit(1)
139
+
140
+ current_account = cli.paths.account_conf.get("environment")
141
+ try:
142
+ # this is for refarch v1
143
+ account_id = cli.paths.common_conf.get("accounts").get(current_account).get("id")
144
+ except AttributeError:
145
+ # this is for refarch v2
146
+ try:
147
+ # this is for accounts with no org unit on top of it
148
+ account_id = cli.paths.common_conf.get("organization").get("accounts").get(current_account).get("id")
149
+ except AttributeError:
150
+ try:
151
+ # this is for accounts with no org unit on top of it
152
+ found = False
153
+ for ou in cli.paths.common_conf.get("organization").get("organizational_units"):
154
+ if current_account in cli.paths.common_conf.get("organization").get("organizational_units").get(
155
+ ou
156
+ ).get("accounts"):
157
+ account_id = (
158
+ cli.paths.common_conf.get("organization")
159
+ .get("organizational_units")
160
+ .get(ou)
161
+ .get("accounts")
162
+ .get(current_account)
163
+ .get("id")
164
+ )
165
+ found = True
166
+ break
167
+ if not found:
168
+ raise AttributeError
169
+ except AttributeError:
170
+ logger.error(f"Missing account configuration for [bold]{current_account}[/bold] in global config file.")
171
+ raise Exit(1)
172
+ if not account_id:
173
+ logger.error(f"Missing id for account [bold]{current_account}[/bold].")
174
+ raise Exit(1)
175
+
176
+ logger.info(f"Configuring [bold]{cli.project}-sso[/bold] profile.")
177
+ sso_profile = {
178
+ "sso_start_url": cli.paths.common_conf.get("sso_start_url"),
179
+ "sso_region": cli.paths.common_conf.get("sso_region", cli.paths.common_conf.get(region_primary)),
180
+ "sso_account_id": account_id,
181
+ "sso_role_name": sso_role,
182
+ }
183
+ for key, value in sso_profile.items():
184
+ cli.exec(f"configure set {key} {value}", profile=f"{cli.project}-sso")
185
+
186
+ context.invoke(login)
187
+
188
+ logger.info("Storing account information.")
189
+ configure_sso_profiles(cli)
190
+
191
+
192
+ @aws.group(invoke_without_command=True, add_help_option=False, context_settings=CONTEXT_SETTINGS)
193
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
194
+ @pass_container
195
+ @click.pass_context
196
+ def sso(context, cli, args):
197
+ """sso"""
198
+ _handle_subcommand(context=context, cli_container=cli, args=args, caller_name="sso")
199
+
200
+
201
+ @sso.command()
202
+ @pass_container
203
+ def login(cli):
204
+ """Login"""
205
+ exit_code, region = cli.exec(f"configure get sso_region --profile {cli.project}-sso")
206
+ if exit_code:
207
+ logger.error(f"Region configuration for [bold]{cli.project}-sso[/bold] profile not found.")
208
+ raise Exit(1)
209
+
210
+ if exit_code := cli.sso_login():
211
+ raise Exit(exit_code)
212
+
213
+
214
+ @sso.command()
215
+ @pass_container
216
+ def logout(cli):
217
+ """Logout"""
218
+ exit_code = cli.system_start(cli.AWS_SSO_LOGOUT_SCRIPT)
219
+ if exit_code:
220
+ raise Exit(exit_code)
221
+
222
+ logger.info(
223
+ f"Don't forget to log out of your [bold]AWS SSO[/bold] start page {cli.paths.common_conf.get('sso_start_url')}"
224
+ " and your external identity provider portal."
225
+ )
@@ -14,6 +14,7 @@ from click.exceptions import Exit
14
14
  from questionary import Choice
15
15
  from ruamel.yaml import YAML
16
16
 
17
+ from leverage import __toolbox_version__
17
18
  from leverage import logger
18
19
  from leverage._internals import pass_state
19
20
  from leverage._utils import ExitError
@@ -264,6 +265,8 @@ def credentials(state):
264
265
  if short_name is None or not re.match("^[a-z]{2,4}$", short_name):
265
266
  logger.error("Invalid or missing project short name in project.yaml file.")
266
267
  raise Exit(1)
268
+ if not build_env.exists():
269
+ build_env.write_text(f"PROJECT={short_name}\nTF_IMAGE_TAG={__toolbox_version__}")
267
270
  elif not build_env.exists():
268
271
  # project_config is not empty
269
272
  # and build.env does not exist
@@ -0,0 +1,44 @@
1
+ import click
2
+
3
+ from leverage._utils import CustomEntryPoint
4
+ from leverage.container import get_docker_client, TFContainer
5
+ from leverage.modules.utils import env_var_option, mount_option, auth_sso, auth_mfa
6
+
7
+
8
+ @click.command()
9
+ @mount_option
10
+ @env_var_option
11
+ @auth_mfa
12
+ @auth_sso
13
+ def shell(mount, env_var, mfa, sso):
14
+ """
15
+ Run a shell in a generic container. It supports mounting local paths and injecting arbitrary environment variables.
16
+ It also supports AWS credentials injection via mfa/sso.
17
+
18
+ Syntax:
19
+ leverage shell --mount <local-path> <container-path> --env-var <name> <value>
20
+
21
+ Example:
22
+ leverage shell --mount /home/user/bin/ /usr/bin/ --env-var env dev
23
+
24
+ Both mount and env-var parameters can be provided multiple times.
25
+
26
+ Example:
27
+ leverage shell --mount /home/user/bin/ /usr/bin/ --mount /etc/config.ini /etc/config.ini --env-var init 5 --env-var env dev
28
+ """
29
+ if env_var:
30
+ env_var = dict(env_var)
31
+ # TODO: TFContainer is the only class supporting sso/mfa auth automagically
32
+ # Move this capacity into a mixin later
33
+ container = TFContainer(get_docker_client(), mounts=mount, env_vars=env_var)
34
+ container.ensure_image()
35
+
36
+ # auth
37
+ container.disable_authentication()
38
+ if sso:
39
+ container.enable_sso()
40
+ if mfa:
41
+ container.enable_mfa()
42
+
43
+ with CustomEntryPoint(container, entrypoint=""):
44
+ container._start(container.SHELL)