leverage 2.2.0rc2__tar.gz → 2.2.0rc4__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 (35) hide show
  1. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/PKG-INFO +3 -3
  2. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/__init__.py +1 -7
  3. leverage-2.2.0rc4/leverage/_backend_config.py +172 -0
  4. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/_internals.py +31 -4
  5. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/_utils.py +11 -71
  6. leverage-2.2.0rc4/leverage/leverage.py +47 -0
  7. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/modules/__init__.py +2 -1
  8. leverage-2.2.0rc4/leverage/modules/auth.py +379 -0
  9. leverage-2.2.0rc4/leverage/modules/aws.py +335 -0
  10. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/modules/credentials.py +80 -69
  11. leverage-2.2.0rc4/leverage/modules/kubectl.py +168 -0
  12. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/modules/project.py +14 -12
  13. leverage-2.2.0rc4/leverage/modules/runner.py +133 -0
  14. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/modules/tf.py +157 -189
  15. leverage-2.2.0rc4/leverage/modules/tfautomv.py +38 -0
  16. leverage-2.2.0rc4/leverage/modules/tfrunner.py +94 -0
  17. leverage-2.2.0rc4/leverage/modules/utils.py +49 -0
  18. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/path.py +34 -69
  19. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/pyproject.toml +4 -4
  20. leverage-2.2.0rc2/leverage/container.py +0 -716
  21. leverage-2.2.0rc2/leverage/containers/kubectl.py +0 -145
  22. leverage-2.2.0rc2/leverage/leverage.py +0 -68
  23. leverage-2.2.0rc2/leverage/modules/auth.py +0 -157
  24. leverage-2.2.0rc2/leverage/modules/aws.py +0 -225
  25. leverage-2.2.0rc2/leverage/modules/kubectl.py +0 -43
  26. leverage-2.2.0rc2/leverage/modules/shell.py +0 -44
  27. leverage-2.2.0rc2/leverage/modules/tfautomv.py +0 -39
  28. leverage-2.2.0rc2/leverage/modules/utils.py +0 -43
  29. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/LICENSE.txt +0 -0
  30. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/README.md +0 -0
  31. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/_parsing.py +0 -0
  32. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/conf.py +0 -0
  33. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/logger.py +0 -0
  34. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/modules/run.py +0 -0
  35. {leverage-2.2.0rc2 → leverage-2.2.0rc4}/leverage/tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: leverage
3
- Version: 2.2.0rc2
3
+ Version: 2.2.0rc4
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.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
- }
7
+ __version__ = "2.2.0rc4"
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,10 @@ class State:
27
27
  def __init__(self):
28
28
  self._verbosity = None
29
29
  self.module = Module()
30
- self.container = None
30
+ self.config = None
31
+ self.paths = None
32
+ self.runner = None
33
+ self.environment = None
31
34
 
32
35
  @property
33
36
  def verbosity(self):
@@ -41,13 +44,37 @@ class State:
41
44
  pass_state = click.make_pass_decorator(State, ensure=True)
42
45
 
43
46
 
44
- def pass_container(command):
45
- """Decorator to pass the current container to the command."""
47
+ def pass_runner(command):
48
+ """Decorator to pass the current runner (Terraform/OpenTofu runner) to the command."""
46
49
 
47
50
  @wraps(command)
48
51
  def new_command(*args, **kwargs):
49
52
  ctx = click.get_current_context()
50
53
 
51
- return command(ctx.obj.container, *args, **kwargs)
54
+ return command(ctx.obj.runner, *args, **kwargs)
55
+
56
+ return new_command
57
+
58
+
59
+ def pass_paths(command):
60
+ """Decorator to pass the current project paths to the command."""
61
+
62
+ @wraps(command)
63
+ def new_command(*args, **kwargs):
64
+ ctx = click.get_current_context()
65
+
66
+ return command(ctx.obj.paths, *args, **kwargs)
67
+
68
+ return new_command
69
+
70
+
71
+ def pass_environment(command):
72
+ """Decorator to pass the current environment to the command."""
73
+
74
+ @wraps(command)
75
+ def new_command(*args, **kwargs):
76
+ ctx = click.get_current_context()
77
+
78
+ return command(ctx.obj.environment, *args, **kwargs)
52
79
 
53
80
  return new_command
@@ -3,15 +3,13 @@
3
3
  """
4
4
 
5
5
  from pathlib import Path
6
- from subprocess import run
7
- from subprocess import PIPE
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 Exit
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 CustomEntryPoint:
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
- logger.error(error_description)
116
- super(ExitError, self).__init__(exit_code)
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
- class ContainerSession:
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,47 @@
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
+ state.environment = {
32
+ "AWS_SHARED_CREDENTIALS_FILE": str(state.paths.aws_credentials_file),
33
+ "AWS_CONFIG_FILE": str(state.paths.aws_config_file),
34
+ }
35
+
36
+
37
+ # Add modules to leverage
38
+ leverage.add_command(run)
39
+ leverage.add_command(project)
40
+ leverage.add_command(tofu)
41
+ leverage.add_command(tofu, name="tf")
42
+ leverage.add_command(terraform)
43
+ leverage.add_command(credentials)
44
+ leverage.add_command(aws)
45
+ leverage.add_command(tfautomv)
46
+ leverage.add_command(kubectl)
47
+ leverage.add_command(kubectl, name="kc")
@@ -1,6 +1,7 @@
1
1
  from .run import run
2
2
  from .project import project
3
+ from .aws import aws
3
4
  from .tf import tofu, terraform
5
+ from .credentials import credentials
4
6
  from .tfautomv import tfautomv
5
7
  from .kubectl import kubectl
6
- from .shell import shell