leverage 2.2.0rc2__tar.gz → 2.2.0rc3__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.0rc2 → leverage-2.2.0rc3}/PKG-INFO +3 -3
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/__init__.py +1 -7
- leverage-2.2.0rc3/leverage/_backend_config.py +172 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/_internals.py +18 -4
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/_utils.py +11 -71
- leverage-2.2.0rc3/leverage/leverage.py +43 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/modules/__init__.py +2 -1
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/modules/auth.py +63 -15
- leverage-2.2.0rc3/leverage/modules/aws.py +353 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/modules/credentials.py +82 -68
- leverage-2.2.0rc3/leverage/modules/kubectl.py +188 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/modules/project.py +14 -12
- leverage-2.2.0rc3/leverage/modules/runner.py +124 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/modules/tf.py +177 -180
- leverage-2.2.0rc3/leverage/modules/tfautomv.py +43 -0
- leverage-2.2.0rc3/leverage/modules/tfrunner.py +86 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/modules/utils.py +19 -16
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/path.py +24 -61
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/pyproject.toml +4 -4
- leverage-2.2.0rc2/leverage/container.py +0 -716
- leverage-2.2.0rc2/leverage/containers/kubectl.py +0 -145
- leverage-2.2.0rc2/leverage/leverage.py +0 -68
- leverage-2.2.0rc2/leverage/modules/aws.py +0 -225
- leverage-2.2.0rc2/leverage/modules/kubectl.py +0 -43
- leverage-2.2.0rc2/leverage/modules/shell.py +0 -44
- leverage-2.2.0rc2/leverage/modules/tfautomv.py +0 -39
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/LICENSE.txt +0 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/README.md +0 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/_parsing.py +0 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/conf.py +0 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/logger.py +0 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/leverage/modules/run.py +0 -0
- {leverage-2.2.0rc2 → leverage-2.2.0rc3}/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.0rc3
|
|
4
4
|
Summary: Binbash Leverage Command-Line tool.
|
|
5
5
|
Home-page: https://github.com/binbashar/leverage
|
|
6
6
|
License: MIT
|
|
@@ -21,10 +21,10 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
21
21
|
Requires-Dist: boto3 (==1.33.2)
|
|
22
22
|
Requires-Dist: click (==8.0.1)
|
|
23
23
|
Requires-Dist: configupdater (==3.2)
|
|
24
|
-
Requires-Dist: docker (==6.1.0)
|
|
25
|
-
Requires-Dist: dockerpty (==0.4.1)
|
|
26
24
|
Requires-Dist: docutils (==0.17.1)
|
|
27
25
|
Requires-Dist: jinja2 (==3.0.1)
|
|
26
|
+
Requires-Dist: prompt-toolkit (==3.0.51)
|
|
27
|
+
Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
|
|
28
28
|
Requires-Dist: python-hcl2 (==7.3.1)
|
|
29
29
|
Requires-Dist: questionary (==1.10.0)
|
|
30
30
|
Requires-Dist: requests (==2.31)
|
|
@@ -4,13 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# pylint: disable=wrong-import-position
|
|
6
6
|
|
|
7
|
-
__version__ = "2.2.
|
|
8
|
-
__toolbox_version__ = "1.3.5-0.2.0"
|
|
9
|
-
|
|
10
|
-
MINIMUM_VERSIONS = {
|
|
11
|
-
"TERRAFORM": "1.3.5",
|
|
12
|
-
"TOOLBOX": "0.2.4",
|
|
13
|
-
}
|
|
7
|
+
__version__ = "2.2.0rc3"
|
|
14
8
|
|
|
15
9
|
import sys
|
|
16
10
|
from shutil import which
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Utilities for modifying Terraform backend configuration."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Union, Optional
|
|
6
|
+
|
|
7
|
+
import hcl2
|
|
8
|
+
import lark
|
|
9
|
+
|
|
10
|
+
from leverage._utils import ExitError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_backend_key(config_file_path: Union[str, Path], key: str) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Set or update the backend key in a Terraform config.tf file.
|
|
16
|
+
|
|
17
|
+
This function modifies the Terraform backend configuration to set the S3 state key.
|
|
18
|
+
It preserves all comments, formatting, and other HCL code in the file by using
|
|
19
|
+
string manipulation to surgically modify only the key attribute.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
config_file_path: Path to the config.tf file
|
|
23
|
+
key: The backend key value to set (e.g., "apps-devstg/notifications/terraform.tfstate")
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ExitError: If the file cannot be found, cannot be parsed, or does not contain a
|
|
27
|
+
terraform block with an S3 backend
|
|
28
|
+
"""
|
|
29
|
+
config_file_path = Path(config_file_path)
|
|
30
|
+
|
|
31
|
+
# Validate file exists
|
|
32
|
+
if not config_file_path.exists():
|
|
33
|
+
raise ExitError(1, f"Config file not found: {config_file_path}")
|
|
34
|
+
|
|
35
|
+
# Read the file content
|
|
36
|
+
content = config_file_path.read_text()
|
|
37
|
+
|
|
38
|
+
# Parse the config file to validate structure
|
|
39
|
+
try:
|
|
40
|
+
config_tf = hcl2.loads(content)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise ExitError(1, f"Failed to parse config.tf: {e}")
|
|
43
|
+
|
|
44
|
+
# Validate that the file contains a terraform backend block with S3
|
|
45
|
+
if not (
|
|
46
|
+
"terraform" in config_tf
|
|
47
|
+
and config_tf["terraform"]
|
|
48
|
+
and isinstance(config_tf["terraform"], list)
|
|
49
|
+
and "backend" in config_tf["terraform"][0]
|
|
50
|
+
and config_tf["terraform"][0]["backend"]
|
|
51
|
+
and isinstance(config_tf["terraform"][0]["backend"], list)
|
|
52
|
+
and "s3" in config_tf["terraform"][0]["backend"][0]
|
|
53
|
+
):
|
|
54
|
+
raise ExitError(
|
|
55
|
+
1,
|
|
56
|
+
f"Malformed config.tf: File must contain a terraform block with an S3 backend. "
|
|
57
|
+
f"Expected structure:\n"
|
|
58
|
+
f"terraform {{\n"
|
|
59
|
+
f' backend "s3" {{\n'
|
|
60
|
+
f" # configuration\n"
|
|
61
|
+
f" }}\n"
|
|
62
|
+
f"}}",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Check if key already exists
|
|
66
|
+
backend_config = config_tf["terraform"][0]["backend"][0]["s3"]
|
|
67
|
+
key_exists = "key" in backend_config
|
|
68
|
+
|
|
69
|
+
# Modify the file content
|
|
70
|
+
modified_content = _modify_backend_key(content, key, key_exists)
|
|
71
|
+
|
|
72
|
+
# Write back to file
|
|
73
|
+
config_file_path.write_text(modified_content)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _modify_backend_key(content: str, key: str, key_exists: bool) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Modify the backend key in the HCL content.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
content: The original file content
|
|
82
|
+
key: The new key value
|
|
83
|
+
key_exists: Whether the key attribute already exists
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Modified content with the key set
|
|
87
|
+
"""
|
|
88
|
+
# Pattern to find the backend "s3" block
|
|
89
|
+
# This matches: backend "s3" { ... }
|
|
90
|
+
backend_pattern = r'(backend\s+"s3"\s*\{)'
|
|
91
|
+
|
|
92
|
+
if key_exists:
|
|
93
|
+
# Update existing key
|
|
94
|
+
# Match: key = "anything" or key = "anything" with various whitespace/quotes
|
|
95
|
+
key_pattern = r'(\s*key\s*=\s*)"[^"]*"'
|
|
96
|
+
replacement = r'\1"' + key + '"'
|
|
97
|
+
|
|
98
|
+
modified = re.sub(key_pattern, replacement, content)
|
|
99
|
+
|
|
100
|
+
return modified
|
|
101
|
+
else:
|
|
102
|
+
# Add new key attribute
|
|
103
|
+
# Find the backend "s3" block and add the key after the opening brace
|
|
104
|
+
def add_key(match):
|
|
105
|
+
# Get the matched backend opening
|
|
106
|
+
backend_opening = match.group(1)
|
|
107
|
+
|
|
108
|
+
# Find the indentation by looking at the next line
|
|
109
|
+
remaining = content[match.end() :]
|
|
110
|
+
next_line_match = re.search(r"\n(\s*)", remaining)
|
|
111
|
+
if next_line_match:
|
|
112
|
+
indent = next_line_match.group(1)
|
|
113
|
+
else:
|
|
114
|
+
indent = " " # Default to 4 spaces
|
|
115
|
+
|
|
116
|
+
# Add the key attribute with proper indentation
|
|
117
|
+
return f'{backend_opening}\n{indent}key = "{key}"'
|
|
118
|
+
|
|
119
|
+
modified = re.sub(backend_pattern, add_key, content, count=1)
|
|
120
|
+
|
|
121
|
+
return modified
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_backend_key(config_file: Union[str, Path]) -> Optional[str]:
|
|
125
|
+
"""
|
|
126
|
+
Get the current backend key from a Terraform config.tf file.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
config_file: Path to the config.tf file
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The backend key if it exists, None otherwise
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> get_backend_key("/path/to/config.tf")
|
|
136
|
+
'apps-devstg/layer/terraform.tfstate'
|
|
137
|
+
"""
|
|
138
|
+
config_file = Path(config_file)
|
|
139
|
+
|
|
140
|
+
if not config_file.exists():
|
|
141
|
+
raise ExitError(1, f"Config file not found: {config_file}")
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
config_content = config_file.read_text()
|
|
145
|
+
config_tf = hcl2.loads(config_content)
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
"terraform" in config_tf
|
|
149
|
+
and config_tf["terraform"]
|
|
150
|
+
and isinstance(config_tf["terraform"], list)
|
|
151
|
+
and "backend" in config_tf["terraform"][0]
|
|
152
|
+
and config_tf["terraform"][0]["backend"]
|
|
153
|
+
and isinstance(config_tf["terraform"][0]["backend"], list)
|
|
154
|
+
and "s3" in config_tf["terraform"][0]["backend"][0]
|
|
155
|
+
):
|
|
156
|
+
return config_tf["terraform"][0]["backend"][0]["s3"].get("key")
|
|
157
|
+
else:
|
|
158
|
+
raise ExitError(
|
|
159
|
+
1,
|
|
160
|
+
f"Malformed [bold]config.tf[/bold] file. Missing backend block.\n"
|
|
161
|
+
f"In some cases you may want to skip this check by using the --skip-validation flag, "
|
|
162
|
+
f"e.g. the first time you initialize a tf-backend layer.",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
except lark.exceptions.UnexpectedInput as error:
|
|
166
|
+
raise ExitError(
|
|
167
|
+
1,
|
|
168
|
+
f"Possible invalid expression in [bold]config.tf[/bold] near line {error.line}, column {error.column}\n"
|
|
169
|
+
f"{error.get_context(config_content)}",
|
|
170
|
+
)
|
|
171
|
+
except Exception:
|
|
172
|
+
raise ExitError(1, f"Malformed [bold]config.tf[/bold] file. Unable to parse.")
|
|
@@ -27,7 +27,9 @@ class State:
|
|
|
27
27
|
def __init__(self):
|
|
28
28
|
self._verbosity = None
|
|
29
29
|
self.module = Module()
|
|
30
|
-
self.
|
|
30
|
+
self.config = None
|
|
31
|
+
self.paths = None
|
|
32
|
+
self.runner = None
|
|
31
33
|
|
|
32
34
|
@property
|
|
33
35
|
def verbosity(self):
|
|
@@ -41,13 +43,25 @@ class State:
|
|
|
41
43
|
pass_state = click.make_pass_decorator(State, ensure=True)
|
|
42
44
|
|
|
43
45
|
|
|
44
|
-
def
|
|
45
|
-
"""Decorator to pass the current
|
|
46
|
+
def pass_runner(command):
|
|
47
|
+
"""Decorator to pass the current runner (Terraform/OpenTofu runner) to the command."""
|
|
46
48
|
|
|
47
49
|
@wraps(command)
|
|
48
50
|
def new_command(*args, **kwargs):
|
|
49
51
|
ctx = click.get_current_context()
|
|
50
52
|
|
|
51
|
-
return command(ctx.obj.
|
|
53
|
+
return command(ctx.obj.runner, *args, **kwargs)
|
|
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)
|
|
52
66
|
|
|
53
67
|
return new_command
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from subprocess import run
|
|
7
|
-
from
|
|
6
|
+
from subprocess import PIPE, run
|
|
7
|
+
from typing import List, Optional
|
|
8
8
|
|
|
9
9
|
import hcl2
|
|
10
10
|
import lark
|
|
11
|
-
from click.exceptions import
|
|
11
|
+
from click.exceptions import ClickException
|
|
12
12
|
from configupdater import ConfigUpdater
|
|
13
|
-
from docker import DockerClient
|
|
14
|
-
from docker.models.containers import Container
|
|
15
13
|
|
|
16
14
|
from leverage import logger
|
|
17
15
|
|
|
@@ -64,56 +62,17 @@ def git(command):
|
|
|
64
62
|
run(command, stdout=PIPE, stderr=PIPE, check=True)
|
|
65
63
|
|
|
66
64
|
|
|
67
|
-
class
|
|
68
|
-
"""
|
|
69
|
-
Set a custom entrypoint on the container while entering the context.
|
|
70
|
-
Once outside, return it to its original value.
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
def __init__(self, container, entrypoint):
|
|
74
|
-
self.container = container
|
|
75
|
-
self.old_entrypoint = container.entrypoint
|
|
76
|
-
self.new_entrypoint = entrypoint
|
|
77
|
-
|
|
78
|
-
def __enter__(self):
|
|
79
|
-
self.container.entrypoint = self.new_entrypoint
|
|
80
|
-
|
|
81
|
-
def __exit__(self, *args, **kwargs):
|
|
82
|
-
self.container.entrypoint = self.old_entrypoint
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class AwsCredsEntryPoint(CustomEntryPoint):
|
|
86
|
-
"""
|
|
87
|
-
Fetching AWS credentials by setting the SSO/MFA entrypoints.
|
|
88
|
-
"""
|
|
89
|
-
|
|
90
|
-
def __init__(self, container, override_entrypoint=None):
|
|
91
|
-
auth_method = container.auth_method()
|
|
92
|
-
|
|
93
|
-
new_entrypoint = f"{auth_method}{container.entrypoint if override_entrypoint is None else override_entrypoint}"
|
|
94
|
-
super(AwsCredsEntryPoint, self).__init__(container, entrypoint=new_entrypoint)
|
|
95
|
-
|
|
96
|
-
def __exit__(self, *args, **kwargs):
|
|
97
|
-
super(AwsCredsEntryPoint, self).__exit__(*args, **kwargs)
|
|
98
|
-
if self.container.mfa_enabled:
|
|
99
|
-
self.container.environment.update(
|
|
100
|
-
{
|
|
101
|
-
"AWS_SHARED_CREDENTIALS_FILE": self.container.environment["AWS_SHARED_CREDENTIALS_FILE"].replace(
|
|
102
|
-
".aws", "tmp"
|
|
103
|
-
),
|
|
104
|
-
"AWS_CONFIG_FILE": self.container.environment["AWS_CONFIG_FILE"].replace(".aws", "tmp"),
|
|
105
|
-
}
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class ExitError(Exit):
|
|
65
|
+
class ExitError(ClickException):
|
|
110
66
|
"""
|
|
111
67
|
Raise an Exit exception but also print an error description.
|
|
112
68
|
"""
|
|
113
69
|
|
|
114
70
|
def __init__(self, exit_code: int, error_description: str):
|
|
115
|
-
|
|
116
|
-
super(ExitError, self).__init__(
|
|
71
|
+
self.exit_code = exit_code
|
|
72
|
+
super(ExitError, self).__init__(message=error_description)
|
|
73
|
+
|
|
74
|
+
def show(self):
|
|
75
|
+
logger.error(self.message)
|
|
117
76
|
|
|
118
77
|
|
|
119
78
|
def parse_tf_file(file: Path):
|
|
@@ -134,31 +93,12 @@ def parse_tf_file(file: Path):
|
|
|
134
93
|
return parsed
|
|
135
94
|
|
|
136
95
|
|
|
137
|
-
|
|
138
|
-
"""
|
|
139
|
-
Handle the start/stop cycle of a container.
|
|
140
|
-
Useful when you need to keep your container alive to share context between multiple commands.
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
def __init__(self, docker_client: DockerClient, container_data):
|
|
144
|
-
self.docker_client = docker_client
|
|
145
|
-
self.container_data = container_data
|
|
146
|
-
|
|
147
|
-
def __enter__(self) -> Container:
|
|
148
|
-
self.docker_client.api.start(self.container_data)
|
|
149
|
-
return self.docker_client.containers.get(self.container_data["Id"])
|
|
150
|
-
|
|
151
|
-
def __exit__(self, exc_type, exc_value, exc_tb):
|
|
152
|
-
self.docker_client.api.stop(self.container_data)
|
|
153
|
-
self.docker_client.api.remove_container(self.container_data)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def key_finder(d: dict, target: str, avoid: str = None):
|
|
96
|
+
def key_finder(d: dict, target: str, avoid: Optional[str] = None) -> List[str]:
|
|
157
97
|
"""
|
|
158
98
|
Iterate over a dict of dicts and/or lists of dicts, looking for a key with value "target".
|
|
159
99
|
Collect and return all the values that matches "target" as key.
|
|
160
100
|
"""
|
|
161
|
-
values = []
|
|
101
|
+
values: List[str] = []
|
|
162
102
|
|
|
163
103
|
for key, value in d.items():
|
|
164
104
|
if isinstance(value, dict):
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Binbash Leverage Command-line tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from leverage import __version__, conf
|
|
8
|
+
from leverage._internals import pass_state
|
|
9
|
+
from leverage.path import NotARepositoryError, PathsHandler
|
|
10
|
+
from leverage.modules import aws, credentials, run, project, tofu, terraform, tfautomv, kubectl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(invoke_without_command=True)
|
|
14
|
+
@click.option("-v", "--verbose", is_flag=True, help="Increase output verbosity.")
|
|
15
|
+
@click.version_option(version=__version__)
|
|
16
|
+
@pass_state
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def leverage(context, state, verbose):
|
|
19
|
+
"""Leverage Reference Architecture projects command-line tool."""
|
|
20
|
+
# --verbose | -v
|
|
21
|
+
state.verbosity = verbose
|
|
22
|
+
if context.invoked_subcommand is None:
|
|
23
|
+
# leverage called with no subcommand
|
|
24
|
+
click.echo(context.get_help())
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
state.config = conf.load()
|
|
28
|
+
except NotARepositoryError:
|
|
29
|
+
return
|
|
30
|
+
state.paths = PathsHandler(state.config)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Add modules to leverage
|
|
34
|
+
leverage.add_command(run)
|
|
35
|
+
leverage.add_command(project)
|
|
36
|
+
leverage.add_command(tofu)
|
|
37
|
+
leverage.add_command(tofu, name="tf")
|
|
38
|
+
leverage.add_command(terraform)
|
|
39
|
+
leverage.add_command(credentials)
|
|
40
|
+
leverage.add_command(aws)
|
|
41
|
+
leverage.add_command(tfautomv)
|
|
42
|
+
leverage.add_command(kubectl)
|
|
43
|
+
leverage.add_command(kubectl, name="kc")
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import json
|
|
2
3
|
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
3
5
|
from configparser import NoSectionError, NoOptionError
|
|
4
6
|
|
|
5
7
|
import boto3
|
|
@@ -7,6 +9,7 @@ from botocore.exceptions import ClientError
|
|
|
7
9
|
from configupdater import ConfigUpdater
|
|
8
10
|
|
|
9
11
|
from leverage import logger
|
|
12
|
+
from leverage.path import PathsHandler
|
|
10
13
|
from leverage._utils import key_finder, ExitError, get_or_create_section, parse_tf_file
|
|
11
14
|
|
|
12
15
|
|
|
@@ -21,10 +24,14 @@ def get_layer_profile(raw_profile: str, config_updater: ConfigUpdater, tf_profil
|
|
|
21
24
|
raise SkipProfile
|
|
22
25
|
|
|
23
26
|
# if it is exactly that variable, we already know the layer profile is tf_profile
|
|
24
|
-
layer_profile = tf_profile if raw_profile
|
|
27
|
+
layer_profile = tf_profile if raw_profile in ("${var.profile}", "each.value.profile") else None
|
|
25
28
|
|
|
26
29
|
# replace variables with their corresponding values
|
|
27
|
-
raw =
|
|
30
|
+
raw = (
|
|
31
|
+
raw_profile.replace("${var.profile}", tf_profile)
|
|
32
|
+
.replace("${var.project}", project)
|
|
33
|
+
.replace("each.value.profile", tf_profile)
|
|
34
|
+
)
|
|
28
35
|
|
|
29
36
|
# the project and the role are at the beginning and end of the string
|
|
30
37
|
_, *account_name, _ = raw.split("-")
|
|
@@ -57,15 +64,15 @@ def update_config_section(updater: ConfigUpdater, layer_profile: str, data: dict
|
|
|
57
64
|
updater.update_file()
|
|
58
65
|
|
|
59
66
|
|
|
60
|
-
def get_profiles(
|
|
67
|
+
def get_profiles(paths: PathsHandler):
|
|
61
68
|
"""
|
|
62
69
|
Get the AWS profiles present on the layer by parsing some tf files.
|
|
63
70
|
"""
|
|
64
71
|
raw_profiles = set()
|
|
65
72
|
# these are files from the layer we are currently on
|
|
66
|
-
for name in ("config.tf", "locals.tf"):
|
|
73
|
+
for name in ("config.tf", "locals.tf", "runtime.tf"):
|
|
67
74
|
try:
|
|
68
|
-
tf_config = parse_tf_file(Path(
|
|
75
|
+
tf_config = parse_tf_file(Path(paths.cwd / name))
|
|
69
76
|
except FileNotFoundError:
|
|
70
77
|
continue
|
|
71
78
|
|
|
@@ -74,25 +81,65 @@ def get_profiles(cli):
|
|
|
74
81
|
raw_profiles.update(set(key_finder(tf_config, "profile", "lookup")))
|
|
75
82
|
|
|
76
83
|
# the profile value from <layer>/config/backend.tfvars
|
|
77
|
-
backend_config = parse_tf_file(
|
|
84
|
+
backend_config = parse_tf_file(paths.backend_tfvars)
|
|
78
85
|
tf_profile = backend_config["profile"]
|
|
79
86
|
|
|
80
87
|
return tf_profile, raw_profiles
|
|
81
88
|
|
|
82
89
|
|
|
83
|
-
def
|
|
84
|
-
|
|
90
|
+
def get_sso_access_token(sso_token_file: Path) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Get the SSO access token from the token file.
|
|
93
|
+
"""
|
|
94
|
+
return json.loads(sso_token_file.read_text())["accessToken"]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def check_sso_token(paths: PathsHandler):
|
|
98
|
+
"""Check for the existence and validity of the SSO token to be used to get credentials."""
|
|
99
|
+
|
|
100
|
+
# Adding `token` file name to this function in order to
|
|
101
|
+
# meet the requirement regarding to have just one
|
|
102
|
+
# token file in the sso/cache
|
|
103
|
+
sso_role = paths.account_conf.get("sso_role")
|
|
104
|
+
token_file = paths.sso_cache / sso_role
|
|
105
|
+
|
|
106
|
+
token_files = list(paths.sso_cache.glob("*"))
|
|
107
|
+
if not token_files:
|
|
108
|
+
raise ExitError(1, "No AWS SSO token found. Please log in or configure SSO.")
|
|
109
|
+
|
|
110
|
+
if token_file not in token_files and paths.sso_token_file not in token_files:
|
|
111
|
+
raise ExitError(
|
|
112
|
+
1,
|
|
113
|
+
"No valid AWS SSO token found for current account.\n"
|
|
114
|
+
"Please log out and reconfigure SSO before proceeding.",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
token = json.loads(paths.sso_token_file.read_text())
|
|
118
|
+
expiry = datetime.strptime(token.get("expiresAt"), "%Y-%m-%dT%H:%M:%SZ")
|
|
119
|
+
renewal = datetime.now()
|
|
120
|
+
|
|
121
|
+
if expiry < renewal:
|
|
122
|
+
raise ExitError(
|
|
123
|
+
1,
|
|
124
|
+
"AWS SSO token has expired, please log back in by running [bold]leverage aws sso login[/bold]"
|
|
125
|
+
" to refresh your credentials before re-running the last command.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def refresh_layer_credentials(paths: PathsHandler):
|
|
130
|
+
tf_profile, raw_profiles = get_profiles(paths)
|
|
85
131
|
config_updater = ConfigUpdater()
|
|
86
|
-
config_updater.read(
|
|
132
|
+
config_updater.read(paths.aws_config_file)
|
|
87
133
|
|
|
88
|
-
|
|
134
|
+
region = config_updater.get(f"profile {paths.project}-sso", "sso_region").value
|
|
135
|
+
client = boto3.client("sso", region_name=region)
|
|
89
136
|
for raw in raw_profiles:
|
|
90
137
|
try:
|
|
91
138
|
account_id, account_name, sso_role, layer_profile = get_layer_profile(
|
|
92
139
|
raw,
|
|
93
140
|
config_updater,
|
|
94
141
|
tf_profile,
|
|
95
|
-
|
|
142
|
+
paths.project,
|
|
96
143
|
)
|
|
97
144
|
except SkipProfile:
|
|
98
145
|
continue
|
|
@@ -119,7 +166,7 @@ def refresh_layer_credentials(cli):
|
|
|
119
166
|
credentials = client.get_role_credentials(
|
|
120
167
|
roleName=sso_role,
|
|
121
168
|
accountId=account_id,
|
|
122
|
-
accessToken=
|
|
169
|
+
accessToken=get_sso_access_token(paths.sso_token_file),
|
|
123
170
|
)["roleCredentials"]
|
|
124
171
|
except ClientError as error:
|
|
125
172
|
if error.response["Error"]["Code"] in ("AccessDeniedException", "ForbiddenException"):
|
|
@@ -129,6 +176,8 @@ def refresh_layer_credentials(cli):
|
|
|
129
176
|
" in this account.\nPlease check with your administrator or try"
|
|
130
177
|
" running [bold]leverage aws configure sso[/bold].",
|
|
131
178
|
)
|
|
179
|
+
else:
|
|
180
|
+
raise ExitError(50, f"Error retrieving role credentials: {error}")
|
|
132
181
|
|
|
133
182
|
# update expiration on aws/<project>/config
|
|
134
183
|
logger.info(f"Writing {layer_profile} profile")
|
|
@@ -140,10 +189,9 @@ def refresh_layer_credentials(cli):
|
|
|
140
189
|
},
|
|
141
190
|
)
|
|
142
191
|
# write credentials on aws/<project>/credentials (create the file if it doesn't exist first)
|
|
143
|
-
|
|
144
|
-
creds_path.touch(exist_ok=True)
|
|
192
|
+
paths.aws_credentials_file.touch(exist_ok=True)
|
|
145
193
|
credentials_updater = ConfigUpdater()
|
|
146
|
-
credentials_updater.read(
|
|
194
|
+
credentials_updater.read(paths.aws_credentials_file)
|
|
147
195
|
|
|
148
196
|
update_config_section(
|
|
149
197
|
credentials_updater,
|