leverage 3.1.0__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {leverage-3.1.0 → leverage-3.2.0}/PKG-INFO +31 -28
- {leverage-3.1.0 → leverage-3.2.0}/README.md +30 -27
- {leverage-3.1.0 → leverage-3.2.0}/leverage/__init__.py +1 -1
- {leverage-3.1.0 → leverage-3.2.0}/leverage/leverage.py +16 -6
- {leverage-3.1.0 → leverage-3.2.0}/leverage/logger.py +1 -1
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/credentials.py +95 -14
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/tf.py +27 -17
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/tfautomv.py +1 -1
- {leverage-3.1.0 → leverage-3.2.0}/leverage/path.py +27 -3
- {leverage-3.1.0 → leverage-3.2.0}/pyproject.toml +1 -1
- {leverage-3.1.0 → leverage-3.2.0}/LICENSE.txt +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/_backend_config.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/_internals.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/_parsing.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/_utils.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/conf.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/__init__.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/auth.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/aws.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/kubectl.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/project.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/run.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/runner.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/tfrunner.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/modules/utils.py +0 -0
- {leverage-3.1.0 → leverage-3.2.0}/leverage/tasks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: leverage
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Binbash Leverage Command-Line tool.
|
|
5
5
|
Home-page: https://github.com/binbashar/leverage
|
|
6
6
|
License: MIT
|
|
@@ -45,11 +45,9 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
|
|
46
46
|
Leverage CLI is the tool used to manage and interact with any Leverage project.
|
|
47
47
|
|
|
48
|
-
It transparently handles the most complex and error
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
management of said tools.
|
|
52
|
-
Provides the means to interact with your Leverage project and allows you to define custom tasks to run.
|
|
48
|
+
It transparently handles the most complex and error-prone tasks that arise from working with a state-of-the-art infrastructure definition like our Leverage Reference Architecture. It understands the structure of the Leverage Reference Architecture and assists the user in their day-to-day operation.
|
|
49
|
+
|
|
50
|
+
Leverage CLI orchestrates a set of tools used to operate in a Leverage project. Usually this means OpenTofu/Terraform, but also aws-cli, kubectl and more.
|
|
53
51
|
|
|
54
52
|
Reviewing and implementing the [Binbash Leverage Landing Zone for AWS](https://leverage.binbash.co/try-leverage/) would
|
|
55
53
|
be a very good place to start!
|
|
@@ -61,10 +59,13 @@ to [this page](https://leverage.binbash.co/user-guide/leverage-cli/installation/
|
|
|
61
59
|
|
|
62
60
|
### Note for migration from previous versions
|
|
63
61
|
|
|
64
|
-
If you come from Leverage CLI version <
|
|
62
|
+
If you come from Leverage CLI version <3.0.0 and want to use Leverage CLI version >= 3.0.0 keep into account the
|
|
65
63
|
following.
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
Leverage CLI no longer runs tools inside a Docker container; it executes the OpenTofu/Terraform binaries installed on
|
|
66
|
+
your host. Because of this, the `TERRAFORM_IMAGE_NAME`/`TF_IMAGE_NAME` and `TERRAFORM_IMAGE_TAG`/`TF_IMAGE_TAG`
|
|
67
|
+
parameters in the `build.env` file no longer exist. In their place, the optional `TF_BINARY` parameter specifies the path
|
|
68
|
+
to the OpenTofu/Terraform binary to use. As an example, this is the old format:
|
|
68
69
|
|
|
69
70
|
```
|
|
70
71
|
# Project settings
|
|
@@ -74,8 +75,7 @@ PROJECT=bb
|
|
|
74
75
|
MFA_ENABLED=false
|
|
75
76
|
|
|
76
77
|
# Terraform
|
|
77
|
-
|
|
78
|
-
TERRAFORM_IMAGE_TAG=1.1.9
|
|
78
|
+
TF_IMAGE_TAG=1.5.0-0.2.0
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
New version example:
|
|
@@ -87,16 +87,19 @@ PROJECT=bb
|
|
|
87
87
|
# General
|
|
88
88
|
MFA_ENABLED=false
|
|
89
89
|
|
|
90
|
-
# Terraform
|
|
91
|
-
|
|
90
|
+
# OpenTofu/Terraform binary
|
|
91
|
+
TF_BINARY=/usr/local/bin/tofu
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
So, if you have created a project with version <
|
|
95
|
-
|
|
96
|
-
- remove TERRAFORM_IMAGE_NAME line
|
|
97
|
-
- update TF_IMAGE_TAG from this form '9.9.9' to this one '9.9.9-9.9.9'.
|
|
94
|
+
So, if you have created a project with version <3.0.0 and want to use it with version >=3.0.0 you should:
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
- remove the `TERRAFORM_IMAGE_NAME`/`TF_IMAGE_NAME` and `TERRAFORM_IMAGE_TAG`/`TF_IMAGE_TAG` lines
|
|
97
|
+
- install [OpenTofu](https://opentofu.org/docs/intro/install/) or [Terraform](https://developer.hashicorp.com/terraform/install)
|
|
98
|
+
on your host
|
|
99
|
+
- optionally, set `TF_BINARY` to the path of the binary. This is useful when you have multiple versions of
|
|
100
|
+
OpenTofu/Terraform installed, or when the binary lives in an unconventional location. It can be an absolute path, a path starting with `~`, or a path
|
|
101
|
+
relative to the project root. If not set, the `tofu` or `terraform` binary is looked up in your `PATH`. Keep in mind
|
|
102
|
+
the binary must match the command you use: `leverage tofu` expects OpenTofu and `leverage terraform` expects Terraform.
|
|
100
103
|
|
|
101
104
|
## System requirements
|
|
102
105
|
|
|
@@ -221,12 +224,8 @@ poetry run pre-commit install
|
|
|
221
224
|
To run unit tests, pytest is the tool of choice, and the required dependencies are available in the
|
|
222
225
|
corresponding `dev-requirements.txt`.
|
|
223
226
|
|
|
224
|
-
Integration tests are implemented using [bats](https://github.com/bats-core/bats-core/).
|
|
225
|
-
|
|
226
|
-
of packages, and the cleanup may not be completely thorough. As such, is highly recommended to run these tests using the
|
|
227
|
-
docker image.
|
|
228
|
-
|
|
229
|
-
### Manually
|
|
227
|
+
Integration tests are implemented using [bats](https://github.com/bats-core/bats-core/). They drive the installed
|
|
228
|
+
`leverage` command, and work on temporary directories of their own, so they can be run directly on your machine.
|
|
230
229
|
|
|
231
230
|
1. Unit tests:
|
|
232
231
|
|
|
@@ -245,17 +244,21 @@ brew install bats-support
|
|
|
245
244
|
brew install bats-assert
|
|
246
245
|
```
|
|
247
246
|
|
|
247
|
+
The cli runs the infrastructure binaries directly, so `terraform` and `tofu` need to be installed as well. See
|
|
248
|
+
[System requirements](#system-requirements).
|
|
249
|
+
|
|
248
250
|
```bash
|
|
249
251
|
bats -r tests/bats
|
|
250
252
|
```
|
|
251
253
|
|
|
252
|
-
|
|
254
|
+
If `bats-support` and `bats-assert` are not installed in a location bats searches by default, point `BATS_LIB_PATH` at
|
|
255
|
+
the directory holding them:
|
|
253
256
|
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
```bash
|
|
258
|
+
BATS_LIB_PATH=/opt/homebrew/lib bats -r tests/bats
|
|
259
|
+
```
|
|
256
260
|
|
|
257
|
-
|
|
258
|
-
respectively.
|
|
261
|
+
Alternatively, `make tests` runs both suites, and `make test-unit` or `make test-int` runs one of them.
|
|
259
262
|
|
|
260
263
|
## Release Process
|
|
261
264
|
|
|
@@ -7,11 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
Leverage CLI is the tool used to manage and interact with any Leverage project.
|
|
9
9
|
|
|
10
|
-
It transparently handles the most complex and error
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
management of said tools.
|
|
14
|
-
Provides the means to interact with your Leverage project and allows you to define custom tasks to run.
|
|
10
|
+
It transparently handles the most complex and error-prone tasks that arise from working with a state-of-the-art infrastructure definition like our Leverage Reference Architecture. It understands the structure of the Leverage Reference Architecture and assists the user in their day-to-day operation.
|
|
11
|
+
|
|
12
|
+
Leverage CLI orchestrates a set of tools used to operate in a Leverage project. Usually this means OpenTofu/Terraform, but also aws-cli, kubectl and more.
|
|
15
13
|
|
|
16
14
|
Reviewing and implementing the [Binbash Leverage Landing Zone for AWS](https://leverage.binbash.co/try-leverage/) would
|
|
17
15
|
be a very good place to start!
|
|
@@ -23,10 +21,13 @@ to [this page](https://leverage.binbash.co/user-guide/leverage-cli/installation/
|
|
|
23
21
|
|
|
24
22
|
### Note for migration from previous versions
|
|
25
23
|
|
|
26
|
-
If you come from Leverage CLI version <
|
|
24
|
+
If you come from Leverage CLI version <3.0.0 and want to use Leverage CLI version >= 3.0.0 keep into account the
|
|
27
25
|
following.
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
Leverage CLI no longer runs tools inside a Docker container; it executes the OpenTofu/Terraform binaries installed on
|
|
28
|
+
your host. Because of this, the `TERRAFORM_IMAGE_NAME`/`TF_IMAGE_NAME` and `TERRAFORM_IMAGE_TAG`/`TF_IMAGE_TAG`
|
|
29
|
+
parameters in the `build.env` file no longer exist. In their place, the optional `TF_BINARY` parameter specifies the path
|
|
30
|
+
to the OpenTofu/Terraform binary to use. As an example, this is the old format:
|
|
30
31
|
|
|
31
32
|
```
|
|
32
33
|
# Project settings
|
|
@@ -36,8 +37,7 @@ PROJECT=bb
|
|
|
36
37
|
MFA_ENABLED=false
|
|
37
38
|
|
|
38
39
|
# Terraform
|
|
39
|
-
|
|
40
|
-
TERRAFORM_IMAGE_TAG=1.1.9
|
|
40
|
+
TF_IMAGE_TAG=1.5.0-0.2.0
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
New version example:
|
|
@@ -49,16 +49,19 @@ PROJECT=bb
|
|
|
49
49
|
# General
|
|
50
50
|
MFA_ENABLED=false
|
|
51
51
|
|
|
52
|
-
# Terraform
|
|
53
|
-
|
|
52
|
+
# OpenTofu/Terraform binary
|
|
53
|
+
TF_BINARY=/usr/local/bin/tofu
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
So, if you have created a project with version <
|
|
57
|
-
|
|
58
|
-
- remove TERRAFORM_IMAGE_NAME line
|
|
59
|
-
- update TF_IMAGE_TAG from this form '9.9.9' to this one '9.9.9-9.9.9'.
|
|
56
|
+
So, if you have created a project with version <3.0.0 and want to use it with version >=3.0.0 you should:
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
- remove the `TERRAFORM_IMAGE_NAME`/`TF_IMAGE_NAME` and `TERRAFORM_IMAGE_TAG`/`TF_IMAGE_TAG` lines
|
|
59
|
+
- install [OpenTofu](https://opentofu.org/docs/intro/install/) or [Terraform](https://developer.hashicorp.com/terraform/install)
|
|
60
|
+
on your host
|
|
61
|
+
- optionally, set `TF_BINARY` to the path of the binary. This is useful when you have multiple versions of
|
|
62
|
+
OpenTofu/Terraform installed, or when the binary lives in an unconventional location. It can be an absolute path, a path starting with `~`, or a path
|
|
63
|
+
relative to the project root. If not set, the `tofu` or `terraform` binary is looked up in your `PATH`. Keep in mind
|
|
64
|
+
the binary must match the command you use: `leverage tofu` expects OpenTofu and `leverage terraform` expects Terraform.
|
|
62
65
|
|
|
63
66
|
## System requirements
|
|
64
67
|
|
|
@@ -183,12 +186,8 @@ poetry run pre-commit install
|
|
|
183
186
|
To run unit tests, pytest is the tool of choice, and the required dependencies are available in the
|
|
184
187
|
corresponding `dev-requirements.txt`.
|
|
185
188
|
|
|
186
|
-
Integration tests are implemented using [bats](https://github.com/bats-core/bats-core/).
|
|
187
|
-
|
|
188
|
-
of packages, and the cleanup may not be completely thorough. As such, is highly recommended to run these tests using the
|
|
189
|
-
docker image.
|
|
190
|
-
|
|
191
|
-
### Manually
|
|
189
|
+
Integration tests are implemented using [bats](https://github.com/bats-core/bats-core/). They drive the installed
|
|
190
|
+
`leverage` command, and work on temporary directories of their own, so they can be run directly on your machine.
|
|
192
191
|
|
|
193
192
|
1. Unit tests:
|
|
194
193
|
|
|
@@ -207,17 +206,21 @@ brew install bats-support
|
|
|
207
206
|
brew install bats-assert
|
|
208
207
|
```
|
|
209
208
|
|
|
209
|
+
The cli runs the infrastructure binaries directly, so `terraform` and `tofu` need to be installed as well. See
|
|
210
|
+
[System requirements](#system-requirements).
|
|
211
|
+
|
|
210
212
|
```bash
|
|
211
213
|
bats -r tests/bats
|
|
212
214
|
```
|
|
213
215
|
|
|
214
|
-
|
|
216
|
+
If `bats-support` and `bats-assert` are not installed in a location bats searches by default, point `BATS_LIB_PATH` at
|
|
217
|
+
the directory holding them:
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
```bash
|
|
220
|
+
BATS_LIB_PATH=/opt/homebrew/lib bats -r tests/bats
|
|
221
|
+
```
|
|
218
222
|
|
|
219
|
-
|
|
220
|
-
respectively.
|
|
223
|
+
Alternatively, `make tests` runs both suites, and `make test-unit` or `make test-int` runs one of them.
|
|
221
224
|
|
|
222
225
|
## Release Process
|
|
223
226
|
|
|
@@ -6,7 +6,7 @@ import click
|
|
|
6
6
|
|
|
7
7
|
from leverage import __version__, conf
|
|
8
8
|
from leverage._internals import pass_state
|
|
9
|
-
from leverage.path import NotARepositoryError,
|
|
9
|
+
from leverage.path import NotARepositoryError, build_paths_and_environment, is_project_yaml_only_bootstrap
|
|
10
10
|
from leverage.modules import aws, credentials, run, project, tofu, terraform, tfautomv, kubectl
|
|
11
11
|
|
|
12
12
|
|
|
@@ -27,11 +27,21 @@ def leverage(context, state, verbose):
|
|
|
27
27
|
state.config = conf.load()
|
|
28
28
|
except NotARepositoryError:
|
|
29
29
|
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
|
|
31
|
+
# The `project` commands bootstrap a project, so they run before its configuration exists.
|
|
32
|
+
# `project init` creates the git repository, so from that point on the config loads fine but
|
|
33
|
+
# still holds no project name, and building the paths would fail on a legitimate invocation.
|
|
34
|
+
if context.invoked_subcommand == project.name:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# `credentials configure` can legitimately run right after `project init` and before
|
|
38
|
+
# `project create`: only project.yaml exists, so no project name can be resolved yet and
|
|
39
|
+
# PathsHandler would abort. The `credentials` group callback derives the project name from
|
|
40
|
+
# project.yaml and builds state.paths itself in that case.
|
|
41
|
+
if context.invoked_subcommand == credentials.name and is_project_yaml_only_bootstrap():
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
state.paths, state.environment = build_paths_and_environment(state.config)
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
# Add modules to leverage
|
|
@@ -174,7 +174,7 @@ def get_tasks_logger():
|
|
|
174
174
|
|
|
175
175
|
def _raw_logger():
|
|
176
176
|
"""
|
|
177
|
-
Provide a raw logger, in case we need to print stuff that already comes formatted
|
|
177
|
+
Provide a raw logger, in case we need to print stuff that already comes formatted.
|
|
178
178
|
"""
|
|
179
179
|
logger = logging.getLogger("raw")
|
|
180
180
|
logger.setLevel(logging.INFO)
|
|
@@ -16,13 +16,14 @@ from ruamel.yaml import YAML
|
|
|
16
16
|
from questionary import Choice
|
|
17
17
|
from click.exceptions import Exit
|
|
18
18
|
|
|
19
|
-
from leverage import logger
|
|
19
|
+
from leverage import conf, logger
|
|
20
20
|
from leverage._utils import ExitError
|
|
21
21
|
from leverage.modules.runner import Runner
|
|
22
22
|
from leverage._internals import State, pass_runner, pass_paths, pass_state
|
|
23
23
|
from leverage.path import (
|
|
24
24
|
NotARepositoryError,
|
|
25
25
|
PathsHandler,
|
|
26
|
+
build_paths_and_environment,
|
|
26
27
|
get_global_config_path,
|
|
27
28
|
get_project_root_or_current_dir_path,
|
|
28
29
|
)
|
|
@@ -253,7 +254,6 @@ def credentials(state):
|
|
|
253
254
|
raise an exception
|
|
254
255
|
|
|
255
256
|
If we reached the only common.tfvars scenario, we have no project name nor TF_IMAGE_TAG.
|
|
256
|
-
So the best chance is to read the common.tfvars directly without a container, e.g. with sed or grep
|
|
257
257
|
"""
|
|
258
258
|
project_config = _load_project_yaml()
|
|
259
259
|
build_env = Path(f"{PROJECT_ROOT}/build.env")
|
|
@@ -265,6 +265,20 @@ def credentials(state):
|
|
|
265
265
|
if short_name is None or not re.match("^[a-z]{2,4}$", short_name):
|
|
266
266
|
logger.error("Invalid or missing project short name in project.yaml file.")
|
|
267
267
|
raise Exit(1)
|
|
268
|
+
|
|
269
|
+
if not build_env.exists():
|
|
270
|
+
# Completes this branch's own docstring promise, mirroring the common.tfvars branch
|
|
271
|
+
# below. Guarded so a mature project that still has project.yaml on disk never has
|
|
272
|
+
# its real build.env clobbered (project create never deletes project.yaml).
|
|
273
|
+
logger.info("Writing project short name to build.env.")
|
|
274
|
+
build_env.write_text(f"PROJECT={short_name}\nTF_IMAGE_TAG=1.1.9")
|
|
275
|
+
|
|
276
|
+
if state.paths is None:
|
|
277
|
+
# Upstream (leverage.py) skipped PathsHandler because only project.yaml existed.
|
|
278
|
+
# build.env now has a project name (or already did) - reload config from disk and
|
|
279
|
+
# build real, project-scoped paths here instead of leaving state.paths/environment None.
|
|
280
|
+
state.config = conf.load()
|
|
281
|
+
state.paths, state.environment = build_paths_and_environment(state.config)
|
|
268
282
|
elif not build_env.exists():
|
|
269
283
|
# project_config is not empty
|
|
270
284
|
# and build.env does not exist
|
|
@@ -372,7 +386,7 @@ def _profile_is_configured(awscli: Runner, profile: str):
|
|
|
372
386
|
Returns:
|
|
373
387
|
bool: Whether the profile was already configured or not.
|
|
374
388
|
"""
|
|
375
|
-
exit_code, _, _ = awscli.exec("configure", "list", "--profile", profile)
|
|
389
|
+
exit_code, _, _ = awscli.exec("configure", "list", "--profile", profile, raises=False)
|
|
376
390
|
|
|
377
391
|
return not exit_code
|
|
378
392
|
|
|
@@ -446,7 +460,7 @@ def configure_credentials(
|
|
|
446
460
|
values = {"aws_access_key_id": key_id, "aws_secret_access_key": secret_key}
|
|
447
461
|
|
|
448
462
|
for key, value in values.items():
|
|
449
|
-
exit_code, output, _ = awscli.exec("configure", "set", key, value, "--profile", profile)
|
|
463
|
+
exit_code, output, _ = awscli.exec("configure", "set", key, value, "--profile", profile, raises=False)
|
|
450
464
|
if exit_code:
|
|
451
465
|
raise ExitError(exit_code, f"AWS CLI error: {output}")
|
|
452
466
|
|
|
@@ -467,7 +481,7 @@ def _credentials_are_valid(awscli: Runner, profile: str):
|
|
|
467
481
|
Returns:
|
|
468
482
|
bool: Whether the credentials are valid.
|
|
469
483
|
"""
|
|
470
|
-
error_code, output, _ = awscli.exec("sts", "get-caller-identity", "--profile", profile)
|
|
484
|
+
error_code, output, _ = awscli.exec("sts", "get-caller-identity", "--profile", profile, raises=False)
|
|
471
485
|
|
|
472
486
|
return error_code != 255 and "InvalidClientTokenId" not in output
|
|
473
487
|
|
|
@@ -482,7 +496,9 @@ def _get_management_account_id(awscli: Runner, profile: str):
|
|
|
482
496
|
Returns:
|
|
483
497
|
str: Management account id.
|
|
484
498
|
"""
|
|
485
|
-
exit_code, caller_identity, _ = awscli.exec(
|
|
499
|
+
exit_code, caller_identity, _ = awscli.exec(
|
|
500
|
+
"sts", "get-caller-identity", "--output", "json", "--profile", profile, raises=False
|
|
501
|
+
)
|
|
486
502
|
if exit_code:
|
|
487
503
|
raise ExitError(exit_code, f"AWS CLI error: {caller_identity}")
|
|
488
504
|
|
|
@@ -502,7 +518,7 @@ def _get_organization_accounts(awscli: Runner, profile: str, project_name: str):
|
|
|
502
518
|
dict: Mapping of organization accounts names to ids.
|
|
503
519
|
"""
|
|
504
520
|
exit_code, organization_accounts, _ = awscli.exec(
|
|
505
|
-
"organizations", "list-accounts", "--output", "json", "--profile", profile
|
|
521
|
+
"organizations", "list-accounts", "--output", "json", "--profile", profile, raises=False
|
|
506
522
|
)
|
|
507
523
|
|
|
508
524
|
if exit_code:
|
|
@@ -530,7 +546,9 @@ def _get_mfa_serial(awscli: Runner, profile: str):
|
|
|
530
546
|
Returns:
|
|
531
547
|
str: MFA device serial.
|
|
532
548
|
"""
|
|
533
|
-
exit_code, mfa_devices, _ = awscli.exec(
|
|
549
|
+
exit_code, mfa_devices, _ = awscli.exec(
|
|
550
|
+
"iam", "list-mfa-devices", "--output", "json", "--profile", profile, raises=False
|
|
551
|
+
)
|
|
534
552
|
if exit_code:
|
|
535
553
|
raise ExitError(exit_code, f"AWS CLI error: {mfa_devices}")
|
|
536
554
|
mfa_devices = json.loads(mfa_devices)
|
|
@@ -558,7 +576,7 @@ def configure_profile(awscli: Runner, profile: str, values: dict):
|
|
|
558
576
|
"""
|
|
559
577
|
logger.info(f"\tConfiguring profile [bold]{profile}[/bold]")
|
|
560
578
|
for key, value in values.items():
|
|
561
|
-
exit_code, output, _ = awscli.exec("configure", "set", key, value, "--profile", profile)
|
|
579
|
+
exit_code, output, _ = awscli.exec("configure", "set", key, value, "--profile", profile, raises=False)
|
|
562
580
|
if exit_code:
|
|
563
581
|
raise ExitError(exit_code, f"AWS CLI error: {output}")
|
|
564
582
|
|
|
@@ -613,13 +631,78 @@ def configure_accounts_profiles(
|
|
|
613
631
|
# A profile identifier looks like `le-security-oaar`
|
|
614
632
|
account_profiles[f"{short_name}-{account_name}-{PROFILES[_type]['profile_role']}-mfa"] = account_profile
|
|
615
633
|
|
|
616
|
-
|
|
617
|
-
|
|
634
|
+
if paths.aws_config_file.exists():
|
|
635
|
+
logger.info("Backing up account profiles file.")
|
|
636
|
+
shutil.copy(paths.aws_config_file, paths.aws_config_file.with_suffix(".bkp"))
|
|
618
637
|
|
|
619
638
|
for profile_identifier, profile_values in account_profiles.items():
|
|
620
639
|
configure_profile(profile_identifier, profile_values)
|
|
621
640
|
|
|
622
641
|
|
|
642
|
+
def _find_matching_brace(content: str, start: int):
|
|
643
|
+
"""Find the position of the brace closing the one opened at `start`.
|
|
644
|
+
|
|
645
|
+
Braces appearing inside double quoted strings are ignored.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
content (str): Text to scan.
|
|
649
|
+
start (int): Position of the opening brace.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
int: Position of the matching closing brace, or None if it is unbalanced.
|
|
653
|
+
"""
|
|
654
|
+
depth = 0
|
|
655
|
+
in_string = False
|
|
656
|
+
position = start
|
|
657
|
+
|
|
658
|
+
while position < len(content):
|
|
659
|
+
char = content[position]
|
|
660
|
+
|
|
661
|
+
if in_string:
|
|
662
|
+
if char == "\\":
|
|
663
|
+
position += 1
|
|
664
|
+
elif char == '"':
|
|
665
|
+
in_string = False
|
|
666
|
+
elif char == '"':
|
|
667
|
+
in_string = True
|
|
668
|
+
elif char == "{":
|
|
669
|
+
depth += 1
|
|
670
|
+
elif char == "}":
|
|
671
|
+
depth -= 1
|
|
672
|
+
if not depth:
|
|
673
|
+
return position
|
|
674
|
+
|
|
675
|
+
position += 1
|
|
676
|
+
|
|
677
|
+
return None
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _replace_hcl_attribute(content: str, attribute: str, value: str):
|
|
681
|
+
"""Replace the value of a brace delimited HCL attribute, honoring nested blocks.
|
|
682
|
+
|
|
683
|
+
A non greedy regex cannot be used for this: it stops at the first closing brace, which for a
|
|
684
|
+
nested block leaves the remaining entries orphaned and the file with unbalanced braces.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
content (str): Full text of the HCL file.
|
|
688
|
+
attribute (str): Name of the attribute to replace, e.g. `accounts`.
|
|
689
|
+
value (str): New value for the attribute, enclosing braces included.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
str: Content with the attribute replaced, unchanged if the attribute was not found.
|
|
693
|
+
"""
|
|
694
|
+
attribute_definition = re.search(rf"^{re.escape(attribute)}\s*=\s*\{{", content, flags=re.MULTILINE)
|
|
695
|
+
if attribute_definition is None:
|
|
696
|
+
return content
|
|
697
|
+
|
|
698
|
+
opening_brace = attribute_definition.end() - 1
|
|
699
|
+
closing_brace = _find_matching_brace(content, opening_brace)
|
|
700
|
+
if closing_brace is None:
|
|
701
|
+
return content
|
|
702
|
+
|
|
703
|
+
return f"{content[:attribute_definition.start()]}{attribute} = {value}{content[closing_brace + 1:]}"
|
|
704
|
+
|
|
705
|
+
|
|
623
706
|
@pass_paths
|
|
624
707
|
def _update_account_ids(paths: PathsHandler, config: dict):
|
|
625
708
|
"""Update accounts ids in global configuration file.
|
|
@@ -654,9 +737,7 @@ def _update_account_ids(paths: PathsHandler, config: dict):
|
|
|
654
737
|
accs = f"{{{accs}\n}}"
|
|
655
738
|
|
|
656
739
|
common_tfvars = paths.common_tfvars.read_text()
|
|
657
|
-
common_tfvars =
|
|
658
|
-
r"accounts\s*=\s*\{.*?\}(?=\s*(?:\n|$))", f"accounts = {accs}", common_tfvars, flags=re.DOTALL
|
|
659
|
-
)
|
|
740
|
+
common_tfvars = _replace_hcl_attribute(common_tfvars, "accounts", accs)
|
|
660
741
|
paths.common_tfvars.write_text(common_tfvars)
|
|
661
742
|
|
|
662
743
|
|
|
@@ -23,8 +23,8 @@ REGION = r"(global|([a-z]{2}(-gov)?)-(central|(north|south)?(east|west)?)-\d)"
|
|
|
23
23
|
@pass_state
|
|
24
24
|
def tofu(state):
|
|
25
25
|
"""Run OpenTofu commands in the context of the current project.
|
|
26
|
-
All tofu subcommands that receive extra args will pass the given strings as is to
|
|
27
|
-
|
|
26
|
+
All tofu subcommands that receive extra args will pass the given strings as is to the OpenTofu binary.
|
|
27
|
+
For example as in `leverage tofu apply -auto-approve` or
|
|
28
28
|
`leverage tofu init -reconfigure`
|
|
29
29
|
"""
|
|
30
30
|
state.runner = TFRunner(binary=state.paths.tf_binary, env_vars=state.environment)
|
|
@@ -34,8 +34,8 @@ def tofu(state):
|
|
|
34
34
|
@pass_state
|
|
35
35
|
def terraform(state):
|
|
36
36
|
"""Run Terraform commands in the context of the current project.
|
|
37
|
-
All terraform subcommands that receive extra args will pass the given strings as is to
|
|
38
|
-
|
|
37
|
+
All terraform subcommands that receive extra args will pass the given strings as is to the Terraform binary.
|
|
38
|
+
For example as in `leverage terraform apply -auto-approve` or
|
|
39
39
|
`leverage terraform init -reconfigure`
|
|
40
40
|
"""
|
|
41
41
|
state.runner = TFRunner(binary=state.paths.tf_binary, terraform=True, env_vars=state.environment)
|
|
@@ -146,7 +146,8 @@ def tf_default_args(paths: PathsHandler) -> tuple:
|
|
|
146
146
|
@pass_runner
|
|
147
147
|
def version(tf):
|
|
148
148
|
"""Print version."""
|
|
149
|
-
tf.run("version")
|
|
149
|
+
if exit_code := tf.run("version"):
|
|
150
|
+
raise Exit(exit_code)
|
|
150
151
|
|
|
151
152
|
|
|
152
153
|
@click.command("format", context_settings=CONTEXT_SETTINGS)
|
|
@@ -155,7 +156,8 @@ def version(tf):
|
|
|
155
156
|
def _format(tf, args):
|
|
156
157
|
"""Check if all files meet the canonical format and rewrite them accordingly."""
|
|
157
158
|
args = args if "-recursive" in args else (*args, "-recursive")
|
|
158
|
-
tf.run("fmt", *args)
|
|
159
|
+
if exit_code := tf.run("fmt", *args):
|
|
160
|
+
raise Exit(exit_code)
|
|
159
161
|
|
|
160
162
|
|
|
161
163
|
@click.command("force-unlock")
|
|
@@ -165,7 +167,8 @@ def _format(tf, args):
|
|
|
165
167
|
@pass_runner
|
|
166
168
|
def force_unlock(tf, paths: PathsHandler, lock_id):
|
|
167
169
|
"""Force unlock the state file."""
|
|
168
|
-
tf.run("force-unlock", lock_id)
|
|
170
|
+
if exit_code := tf.run("force-unlock", lock_id):
|
|
171
|
+
raise Exit(exit_code)
|
|
169
172
|
|
|
170
173
|
|
|
171
174
|
@click.command()
|
|
@@ -174,7 +177,8 @@ def force_unlock(tf, paths: PathsHandler, lock_id):
|
|
|
174
177
|
@pass_runner
|
|
175
178
|
def validate(tf, paths: PathsHandler):
|
|
176
179
|
"""Validate code of the current directory. Previous initialization might be needed."""
|
|
177
|
-
tf.run("validate", *tf_default_args())
|
|
180
|
+
if exit_code := tf.run("validate", *tf_default_args()):
|
|
181
|
+
raise Exit(exit_code)
|
|
178
182
|
|
|
179
183
|
|
|
180
184
|
@click.command("validate-layout")
|
|
@@ -190,7 +194,8 @@ def validate_layout(paths):
|
|
|
190
194
|
@pass_runner
|
|
191
195
|
def _import(tf, address, _id):
|
|
192
196
|
"""Import a resource."""
|
|
193
|
-
tf.run("import", *tf_default_args(), address, _id)
|
|
197
|
+
if exit_code := tf.run("import", *tf_default_args(), address, _id):
|
|
198
|
+
raise Exit(exit_code)
|
|
194
199
|
|
|
195
200
|
|
|
196
201
|
@click.command("refresh-credentials")
|
|
@@ -275,12 +280,11 @@ def invoke_for_all_commands(paths, layers, command, *args: Sequence[str], skip_v
|
|
|
275
280
|
backend_key = backend_key_base.replace("/base-", "/").replace("/tools-", "/")
|
|
276
281
|
set_backend_key(layer / "config.tf", backend_key)
|
|
277
282
|
|
|
278
|
-
# check layers existence
|
|
279
283
|
for layer in layers:
|
|
280
284
|
if len(layers) > 1:
|
|
281
285
|
logger.info(f"Invoking command for layer {layer}...")
|
|
282
286
|
|
|
283
|
-
# execute the actual command
|
|
287
|
+
# execute the actual command; a failing layer raises Exit and stops the remaining ones
|
|
284
288
|
command(args, working_dir=layer)
|
|
285
289
|
|
|
286
290
|
return layers
|
|
@@ -320,7 +324,8 @@ def _init(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir: P
|
|
|
320
324
|
)
|
|
321
325
|
init_args = (*filtered_args, f"-backend-config={paths.backend_tfvars}")
|
|
322
326
|
|
|
323
|
-
tf.run("init", *tf_default_args(), *init_args, working_dir=working_dir)
|
|
327
|
+
if exit_code := tf.run("init", *tf_default_args(), *init_args, working_dir=working_dir):
|
|
328
|
+
raise Exit(exit_code)
|
|
324
329
|
|
|
325
330
|
|
|
326
331
|
@authenticate
|
|
@@ -328,7 +333,8 @@ def _init(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir: P
|
|
|
328
333
|
@pass_runner
|
|
329
334
|
def _plan(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir: Path):
|
|
330
335
|
"""Generate an execution plan for this layer."""
|
|
331
|
-
tf.run("plan", *tf_default_args(), *args, working_dir=working_dir)
|
|
336
|
+
if exit_code := tf.run("plan", *tf_default_args(), *args, working_dir=working_dir):
|
|
337
|
+
raise Exit(exit_code)
|
|
332
338
|
|
|
333
339
|
|
|
334
340
|
def has_a_plan_file(args: Sequence[str]) -> bool:
|
|
@@ -393,7 +399,9 @@ def _apply(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir:
|
|
|
393
399
|
default_args = () if has_a_plan_file(args) else tf_default_args()
|
|
394
400
|
logger.debug(f"Default args passed to apply command: {default_args}")
|
|
395
401
|
|
|
396
|
-
tf.run("apply", *default_args, *args, working_dir=working_dir)
|
|
402
|
+
if exit_code := tf.run("apply", *default_args, *args, working_dir=working_dir):
|
|
403
|
+
logger.error(f"Command execution failed with exit code: {exit_code}")
|
|
404
|
+
raise Exit(exit_code)
|
|
397
405
|
|
|
398
406
|
|
|
399
407
|
@authenticate
|
|
@@ -401,7 +409,8 @@ def _apply(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir:
|
|
|
401
409
|
@pass_runner
|
|
402
410
|
def _output(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir: Path):
|
|
403
411
|
"""Show all output variables of this layer."""
|
|
404
|
-
tf.run("output", *args, working_dir=working_dir)
|
|
412
|
+
if exit_code := tf.run("output", *args, working_dir=working_dir):
|
|
413
|
+
raise Exit(exit_code)
|
|
405
414
|
|
|
406
415
|
|
|
407
416
|
@authenticate
|
|
@@ -409,7 +418,8 @@ def _output(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir:
|
|
|
409
418
|
@pass_runner
|
|
410
419
|
def _destroy(tf: TFRunner, paths: PathsHandler, args: Sequence[str], working_dir: Path):
|
|
411
420
|
"""Destroy infrastructure in this layer."""
|
|
412
|
-
tf.run("destroy", *tf_default_args(), *args, working_dir=working_dir)
|
|
421
|
+
if exit_code := tf.run("destroy", *tf_default_args(), *args, working_dir=working_dir):
|
|
422
|
+
raise Exit(exit_code)
|
|
413
423
|
|
|
414
424
|
|
|
415
425
|
# ###########################################################################
|
|
@@ -485,7 +495,7 @@ def _make_layer_backend_key(cwd, account_dir, account_name):
|
|
|
485
495
|
|
|
486
496
|
@pass_paths
|
|
487
497
|
def _validate_layout(paths, layer: str):
|
|
488
|
-
paths.check_for_layer_location()
|
|
498
|
+
paths.check_for_layer_location(Path(layer))
|
|
489
499
|
|
|
490
500
|
# Check for `environment = <account name>` in account.tfvars
|
|
491
501
|
account_name = paths.account_conf.get("environment")
|
|
@@ -12,7 +12,7 @@ from leverage.modules.auth import authenticate
|
|
|
12
12
|
@authenticate
|
|
13
13
|
@pass_state
|
|
14
14
|
def tfautomv(state, args):
|
|
15
|
-
"""Run TFAutomv commands in the context of the current project
|
|
15
|
+
"""Run TFAutomv commands in the context of the current project."""
|
|
16
16
|
tf_default_args_string = " ".join(tf_default_args())
|
|
17
17
|
state.environment["TF_CLI_ARGS_init"] = tf_default_args_string
|
|
18
18
|
state.environment["TF_CLI_ARGS_plan"] = tf_default_args_string
|
|
@@ -157,7 +157,7 @@ class PathsHandler: # TODO: Turn this class into a something that represents a
|
|
|
157
157
|
self.backend_conf = hcl2.loads(backend_config.read_text()) if backend_config.exists() else {}
|
|
158
158
|
|
|
159
159
|
# Get MFA enabled status
|
|
160
|
-
self.mfa_enabled = env_conf.get("MFA_ENABLED", "false")
|
|
160
|
+
self.mfa_enabled = str(env_conf.get("MFA_ENABLED", "false")).strip().lower() == "true"
|
|
161
161
|
|
|
162
162
|
# Get project name
|
|
163
163
|
self.project = self.common_conf.get("project", env_conf.get("PROJECT", False))
|
|
@@ -183,11 +183,13 @@ class PathsHandler: # TODO: Turn this class into a something that represents a
|
|
|
183
183
|
|
|
184
184
|
@property
|
|
185
185
|
def common_tfvars(self):
|
|
186
|
-
return f"{self.root_dir}/config/{self.COMMON_TF_VARS}"
|
|
186
|
+
# return f"{self.root_dir}/config/{self.COMMON_TF_VARS}"
|
|
187
|
+
return self.root_dir / "config" / self.COMMON_TF_VARS
|
|
187
188
|
|
|
188
189
|
@property
|
|
189
190
|
def account_tfvars(self):
|
|
190
|
-
return f"{self.account_dir}/config/{self.ACCOUNT_TF_VARS}"
|
|
191
|
+
# return f"{self.account_dir}/config/{self.ACCOUNT_TF_VARS}"
|
|
192
|
+
return self.account_dir / "config" / self.ACCOUNT_TF_VARS
|
|
191
193
|
|
|
192
194
|
@property
|
|
193
195
|
def backend_tfvars(self):
|
|
@@ -267,3 +269,25 @@ def get_project_root_or_current_dir_path() -> Path:
|
|
|
267
269
|
root = Path.cwd()
|
|
268
270
|
|
|
269
271
|
return root
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def is_project_yaml_only_bootstrap() -> bool:
|
|
275
|
+
"""Whether only `project.yaml` exists yet: `project init` has run but `project create`
|
|
276
|
+
hasn't, so neither `build.env` nor `config/common.tfvars` exist. PathsHandler can't resolve
|
|
277
|
+
a project name in this state; callers should skip it and derive the name from project.yaml.
|
|
278
|
+
"""
|
|
279
|
+
root = get_project_root_or_current_dir_path()
|
|
280
|
+
build_env = root / "build.env"
|
|
281
|
+
common_tfvars = Path(get_global_config_path()) / "common.tfvars"
|
|
282
|
+
return (root / "project.yaml").exists() and not build_env.exists() and not common_tfvars.exists()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def build_paths_and_environment(config: dict) -> tuple[PathsHandler, dict]:
|
|
286
|
+
"""Build a PathsHandler and the AWS cli environment variables derived from it. Shared by any
|
|
287
|
+
group callback that needs to resolve project paths from `state.config`."""
|
|
288
|
+
paths = PathsHandler(config)
|
|
289
|
+
environment = {
|
|
290
|
+
"AWS_SHARED_CREDENTIALS_FILE": str(paths.aws_credentials_file),
|
|
291
|
+
"AWS_CONFIG_FILE": str(paths.aws_config_file),
|
|
292
|
+
}
|
|
293
|
+
return paths, environment
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|