leverage 2.2.0rc1__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.
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/PKG-INFO +1 -2
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/__init__.py +7 -1
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/_internals.py +4 -18
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/_utils.py +4 -4
- leverage-2.2.0rc2/leverage/leverage.py +68 -0
- leverage-2.2.0rc2/leverage/modules/__init__.py +6 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/auth.py +15 -64
- leverage-2.2.0rc2/leverage/modules/aws.py +225 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/credentials.py +3 -0
- leverage-2.2.0rc2/leverage/modules/shell.py +44 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/tf.py +181 -178
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/utils.py +15 -12
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/path.py +59 -14
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/pyproject.toml +1 -3
- leverage-2.2.0rc1/leverage/_backend_config.py +0 -172
- leverage-2.2.0rc1/leverage/leverage.py +0 -50
- leverage-2.2.0rc1/leverage/modules/__init__.py +0 -9
- leverage-2.2.0rc1/leverage/modules/aws.py +0 -338
- leverage-2.2.0rc1/leverage/modules/runner.py +0 -124
- leverage-2.2.0rc1/leverage/modules/tfrunner.py +0 -72
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/LICENSE.txt +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/README.md +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/_parsing.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/conf.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/container.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/containers/kubectl.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/logger.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/kubectl.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/project.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/run.py +0 -0
- {leverage-2.2.0rc1 → leverage-2.2.0rc2}/leverage/modules/tfautomv.py +0 -0
- {leverage-2.2.0rc1 → 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.
|
|
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.
|
|
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.
|
|
31
|
-
self.paths = None
|
|
32
|
-
self.runner = None
|
|
30
|
+
self.container = None
|
|
33
31
|
|
|
34
32
|
@property
|
|
35
33
|
def verbosity(self):
|
|
@@ -43,25 +41,13 @@ class State:
|
|
|
43
41
|
pass_state = click.make_pass_decorator(State, ensure=True)
|
|
44
42
|
|
|
45
43
|
|
|
46
|
-
def
|
|
47
|
-
"""Decorator to pass the current
|
|
44
|
+
def pass_container(command):
|
|
45
|
+
"""Decorator to pass the current container to the command."""
|
|
48
46
|
|
|
49
47
|
@wraps(command)
|
|
50
48
|
def new_command(*args, **kwargs):
|
|
51
49
|
ctx = click.get_current_context()
|
|
52
50
|
|
|
53
|
-
return command(ctx.obj.
|
|
54
|
-
|
|
55
|
-
return new_command
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def pass_paths(command):
|
|
59
|
-
"""Decorator to pass the current project paths to the command."""
|
|
60
|
-
|
|
61
|
-
@wraps(command)
|
|
62
|
-
def new_command(*args, **kwargs):
|
|
63
|
-
ctx = click.get_current_context()
|
|
64
|
-
|
|
65
|
-
return command(ctx.obj.paths, *args, **kwargs)
|
|
51
|
+
return command(ctx.obj.container, *args, **kwargs)
|
|
66
52
|
|
|
67
53
|
return new_command
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from subprocess import
|
|
7
|
-
from
|
|
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:
|
|
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
|
|
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)
|
|
@@ -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
|
|
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(
|
|
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"
|
|
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.
|
|
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
|
|
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.
|
|
86
|
+
config_updater.read(cli.paths.host_aws_profiles_file)
|
|
134
87
|
|
|
135
|
-
|
|
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
|
-
|
|
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(
|
|
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"):
|
|
@@ -177,8 +129,6 @@ def refresh_layer_credentials(paths: PathsHandler):
|
|
|
177
129
|
" in this account.\nPlease check with your administrator or try"
|
|
178
130
|
" running [bold]leverage aws configure sso[/bold].",
|
|
179
131
|
)
|
|
180
|
-
else:
|
|
181
|
-
raise ExitError(50, f"Error retrieving role credentials: {error}")
|
|
182
132
|
|
|
183
133
|
# update expiration on aws/<project>/config
|
|
184
134
|
logger.info(f"Writing {layer_profile} profile")
|
|
@@ -190,9 +140,10 @@ def refresh_layer_credentials(paths: PathsHandler):
|
|
|
190
140
|
},
|
|
191
141
|
)
|
|
192
142
|
# write credentials on aws/<project>/credentials (create the file if it doesn't exist first)
|
|
193
|
-
paths.
|
|
143
|
+
creds_path = Path(cli.paths.host_aws_credentials_file)
|
|
144
|
+
creds_path.touch(exist_ok=True)
|
|
194
145
|
credentials_updater = ConfigUpdater()
|
|
195
|
-
credentials_updater.read(paths.
|
|
146
|
+
credentials_updater.read(cli.paths.host_aws_credentials_file)
|
|
196
147
|
|
|
197
148
|
update_config_section(
|
|
198
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)
|