aws-annoying 0.2.1__py3-none-any.whl → 0.4.0__py3-none-any.whl

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 (30) hide show
  1. aws_annoying/cli/__init__.py +0 -0
  2. aws_annoying/{ecs_task_definition_lifecycle.py → cli/ecs_task_definition_lifecycle.py} +27 -0
  3. aws_annoying/cli/load_variables.py +139 -0
  4. aws_annoying/{main.py → cli/main.py} +4 -3
  5. aws_annoying/{mfa → cli/mfa}/_app.py +1 -1
  6. aws_annoying/{mfa → cli/mfa}/configure.py +4 -45
  7. aws_annoying/cli/session_manager/__init__.py +3 -0
  8. aws_annoying/cli/session_manager/_app.py +9 -0
  9. aws_annoying/cli/session_manager/_common.py +24 -0
  10. aws_annoying/cli/session_manager/install.py +39 -0
  11. aws_annoying/cli/session_manager/port_forward.py +126 -0
  12. aws_annoying/cli/session_manager/start.py +9 -0
  13. aws_annoying/cli/session_manager/stop.py +50 -0
  14. aws_annoying/mfa.py +54 -0
  15. aws_annoying/session_manager/__init__.py +4 -0
  16. aws_annoying/session_manager/errors.py +10 -0
  17. aws_annoying/session_manager/session_manager.py +318 -0
  18. aws_annoying/utils/downloader.py +58 -0
  19. aws_annoying/utils/platform.py +27 -0
  20. aws_annoying/variables.py +133 -0
  21. {aws_annoying-0.2.1.dist-info → aws_annoying-0.4.0.dist-info}/METADATA +8 -5
  22. aws_annoying-0.4.0.dist-info/RECORD +30 -0
  23. aws_annoying-0.4.0.dist-info/entry_points.txt +2 -0
  24. aws_annoying/load_variables.py +0 -254
  25. aws_annoying-0.2.1.dist-info/RECORD +0 -15
  26. aws_annoying-0.2.1.dist-info/entry_points.txt +0 -2
  27. /aws_annoying/{app.py → cli/app.py} +0 -0
  28. /aws_annoying/{mfa → cli/mfa}/__init__.py +0 -0
  29. {aws_annoying-0.2.1.dist-info → aws_annoying-0.4.0.dist-info}/WHEEL +0 -0
  30. {aws_annoying-0.2.1.dist-info → aws_annoying-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,254 +0,0 @@
1
- # flake8: noqa: B008
2
- from __future__ import annotations
3
-
4
- import json
5
- import os
6
- import subprocess
7
- from typing import Any, NoReturn, Optional
8
-
9
- import boto3
10
- import typer
11
- from rich.console import Console
12
- from rich.table import Table
13
-
14
- from .app import app
15
-
16
-
17
- @app.command(
18
- context_settings={
19
- # Allow extra arguments for user provided command
20
- "allow_extra_args": True,
21
- "ignore_unknown_options": True,
22
- },
23
- )
24
- def load_variables( # noqa: PLR0913
25
- *,
26
- ctx: typer.Context,
27
- arns: list[str] = typer.Option(
28
- [],
29
- metavar="ARN",
30
- help=(
31
- "ARNs of the secret or parameter to load."
32
- " The variables are loaded in the order of the ARNs,"
33
- " overwriting the variables with the same name in the order of the ARNs."
34
- ),
35
- ),
36
- env_prefix: Optional[str] = typer.Option(
37
- None,
38
- help="Prefix of the environment variables to load the ARNs from.",
39
- show_default=False,
40
- ),
41
- overwrite_env: bool = typer.Option(
42
- False, # noqa: FBT003
43
- help="Overwrite the existing environment variables with the same name.",
44
- ),
45
- quiet: bool = typer.Option(
46
- False, # noqa: FBT003
47
- help="Suppress all outputs from this command.",
48
- ),
49
- dry_run: bool = typer.Option(
50
- False, # noqa: FBT003
51
- help="Print the progress only. Neither load variables nor run the command.",
52
- ),
53
- replace: bool = typer.Option(
54
- True, # noqa: FBT003
55
- help=(
56
- "Replace the current process (`os.execvpe`) with the command."
57
- " If disabled, run the command as a `subprocess`."
58
- ),
59
- ),
60
- ) -> NoReturn:
61
- """Wrapper command to run command with variables from AWS resources injected as environment variables.
62
-
63
- This script is intended to be used in the ECS environment, where currently AWS does not support
64
- injecting whole JSON dictionary of secrets or parameters as environment variables directly.
65
-
66
- It first loads the variables from the AWS sources then runs the command with the variables injected as environment variables.
67
-
68
- In addition to `--arns` option, you can provide ARNs as the environment variables by providing `--env-prefix`.
69
- For example, if you have the following environment variables:
70
-
71
- ```shell
72
- export LOAD_AWS_CONFIG__001_app_config=arn:aws:secretsmanager:...
73
- export LOAD_AWS_CONFIG__002_db_config=arn:aws:ssm:...
74
- ```
75
-
76
- You can run the following command:
77
-
78
- ```shell
79
- aws-annoying load-variables --env-prefix LOAD_AWS_CONFIG__ -- ...
80
- ```
81
-
82
- The variables are loaded in the order of option provided, overwriting the variables with the same name in the order of the ARNs.
83
- Existing environment variables are preserved by default, unless `--overwrite-env` is provided.
84
- """ # noqa: E501
85
- console = Console(quiet=quiet, emoji=False)
86
-
87
- command = ctx.args
88
- if not command:
89
- console.print("⚠️ No command provided. Exiting...")
90
- raise typer.Exit(0)
91
-
92
- # Mapping of the ARNs by index (index used for ordering)
93
- map_arns_by_index = {str(idx): arn for idx, arn in enumerate(arns)}
94
- if env_prefix:
95
- console.print(f"🔍 Loading ARNs from environment variables with prefix: {env_prefix!r}")
96
- arns_env = {
97
- key.removeprefix(env_prefix): value for key, value in os.environ.items() if key.startswith(env_prefix)
98
- }
99
- console.print(f"🔍 Found {len(arns_env)} sources from environment variables.")
100
- map_arns_by_index = arns_env | map_arns_by_index
101
-
102
- # Briefly show the ARNs
103
- table = Table("Index", "ARN")
104
- for idx, arn in sorted(map_arns_by_index.items()):
105
- table.add_row(idx, arn)
106
-
107
- console.print(table)
108
-
109
- # Retrieve the variables
110
- loader = VariableLoader(dry_run=dry_run, console=console)
111
- try:
112
- variables = loader.load(map_arns_by_index)
113
- except Exception as exc: # noqa: BLE001
114
- console.print(f"❌ Failed to load the variables: {exc!s}")
115
- raise typer.Exit(1) from None
116
-
117
- # Prepare the environment variables
118
- env = os.environ.copy()
119
- if overwrite_env:
120
- env.update(variables)
121
- else:
122
- # Update variables, preserving the existing ones
123
- for key, value in variables.items():
124
- env.setdefault(key, str(value))
125
-
126
- # Run the command with the variables injected as environment variables, replacing current process
127
- console.print(f"🚀 Running the command: [bold orchid]{' '.join(command)}[/bold orchid]")
128
- if replace: # pragma: no cover (not coverable)
129
- os.execvpe(command[0], command, env=env) # noqa: S606
130
- # The above line should never return
131
-
132
- result = subprocess.run(command, env=env, check=False) # noqa: S603
133
- raise typer.Exit(result.returncode)
134
-
135
-
136
- # Type aliases for readability
137
- _ARN = str
138
- _Variables = dict[str, Any]
139
-
140
-
141
- class VariableLoader: # noqa: D101
142
- def __init__(self, *, console: Console | None = None, dry_run: bool) -> None:
143
- """Initialize the VariableLoader.
144
-
145
- Args:
146
- dry_run: Whether to run in dry-run mode.
147
- console: Rich console instance.
148
- """
149
- self.console = console or Console(quiet=True)
150
- self.dry_run = dry_run
151
-
152
- # TODO(lasuillard): Currently not using pagination (do we need more than 10-20 secrets or parameters each?)
153
- # ; consider adding it if needed
154
- def load(self, map_arns: dict[str, _ARN]) -> dict[str, Any]:
155
- """Load the variables from the AWS Secrets Manager and SSM Parameter Store.
156
-
157
- Each secret or parameter should be a valid dictionary, where the keys are the variable names
158
- and the values are the variable values.
159
-
160
- The items are merged in the order of the key of provided mapping, overwriting the variables with the same name
161
- in the order of the keys.
162
- """
163
- self.console.print("🔍 Retrieving variables from AWS resources...")
164
- if self.dry_run:
165
- self.console.print("⚠️ Dry run mode enabled. Variables won't be loaded from AWS.")
166
-
167
- # Split the ARNs by resource types
168
- secrets_map, parameters_map = {}, {}
169
- for idx, arn in map_arns.items():
170
- if arn.startswith("arn:aws:secretsmanager:"):
171
- secrets_map[idx] = arn
172
- elif arn.startswith("arn:aws:ssm:"):
173
- parameters_map[idx] = arn
174
- else:
175
- msg = f"Unsupported resource: {arn!r}"
176
- raise ValueError(msg)
177
-
178
- # Retrieve variables from AWS resources
179
- secrets: dict[str, _Variables]
180
- parameters: dict[str, _Variables]
181
- if self.dry_run:
182
- secrets = {idx: {} for idx, _ in secrets_map.items()}
183
- parameters = {idx: {} for idx, _ in parameters_map.items()}
184
- else:
185
- secrets = self._retrieve_secrets(secrets_map)
186
- parameters = self._retrieve_parameters(parameters_map)
187
-
188
- self.console.print(f"✅ Retrieved {len(secrets)} secrets and {len(parameters)} parameters.")
189
-
190
- # Merge the variables in order
191
- full_variables = secrets | parameters # Keys MUST NOT conflict
192
- merged_in_order = {}
193
- for _, variables in sorted(full_variables.items()):
194
- merged_in_order.update(variables)
195
-
196
- return merged_in_order
197
-
198
- def _retrieve_secrets(self, secrets_map: dict[str, _ARN]) -> dict[str, _Variables]:
199
- """Retrieve the secrets from AWS Secrets Manager."""
200
- if not secrets_map:
201
- return {}
202
-
203
- secretsmanager = boto3.client("secretsmanager")
204
-
205
- # Retrieve the secrets
206
- arns = list(secrets_map.values())
207
- response = secretsmanager.batch_get_secret_value(SecretIdList=arns)
208
- if errors := response["Errors"]:
209
- msg = f"Failed to retrieve secrets: {errors!r}"
210
- raise ValueError(msg)
211
-
212
- # Parse the secrets
213
- secrets = response["SecretValues"]
214
- result = {}
215
- for secret in secrets:
216
- arn = secret["ARN"]
217
- order_key = next(key for key, value in secrets_map.items() if value == arn)
218
- data = json.loads(secret["SecretString"])
219
- if not isinstance(data, dict):
220
- msg = f"Secret data must be a valid dictionary, but got: {type(data)!r}"
221
- raise TypeError(msg)
222
-
223
- result[order_key] = data
224
-
225
- return result
226
-
227
- def _retrieve_parameters(self, parameters_map: dict[str, _ARN]) -> dict[str, _Variables]:
228
- """Retrieve the parameters from AWS SSM Parameter Store."""
229
- if not parameters_map:
230
- return {}
231
-
232
- ssm = boto3.client("ssm")
233
-
234
- # Retrieve the parameters
235
- parameter_names = list(parameters_map.values())
236
- response = ssm.get_parameters(Names=parameter_names, WithDecryption=True)
237
- if errors := response["InvalidParameters"]:
238
- msg = f"Failed to retrieve parameters: {errors!r}"
239
- raise ValueError(msg)
240
-
241
- # Parse the parameters
242
- parameters = response["Parameters"]
243
- result = {}
244
- for parameter in parameters:
245
- arn = parameter["ARN"]
246
- order_key = next(key for key, value in parameters_map.items() if value == arn)
247
- data = json.loads(parameter["Value"])
248
- if not isinstance(data, dict):
249
- msg = f"Parameter data must be a valid dictionary, but got: {type(data)!r}"
250
- raise TypeError(msg)
251
-
252
- result[order_key] = data
253
-
254
- return result
@@ -1,15 +0,0 @@
1
- aws_annoying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- aws_annoying/app.py,sha256=sp50uVoAl4D6Wk3DFpzKZzSsxmSxNYejFxm62b_Kxps,201
3
- aws_annoying/ecs_task_definition_lifecycle.py,sha256=hiypU5RD5moo0KYHn8YRAyGDSMKh-zPnfxfFYWm6w78,1744
4
- aws_annoying/load_variables.py,sha256=380xT1i85HWybgOIWn72xGCDgqYJ2OSa9VOKMlyHg8M,9488
5
- aws_annoying/main.py,sha256=jngo15w50Jf6nr63N-yV6AEYsFKYzObJc0wI364zS0s,451
6
- aws_annoying/mfa/__init__.py,sha256=rbEGhw5lOQZV_XAc3nSbo56JVhsSPpeOgEtiAy9qzEA,50
7
- aws_annoying/mfa/_app.py,sha256=hpa1Bfx8lRsuZfujM-RyaYU5llOuwGKgf4FwEydthU0,185
8
- aws_annoying/mfa/configure.py,sha256=i5e0qZBFYafuv3D59eP_JXOMrWSRW_bZp0xgIpOlaPE,5364
9
- aws_annoying/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- aws_annoying/utils/debugger.py,sha256=UFllDCGI2gPtwo1XS5vqw0qyR6bYr7XknmBwSxalKIc,754
11
- aws_annoying-0.2.1.dist-info/METADATA,sha256=j7HsUgzImczAdXw4L16y6zl0pvwRQEb1sc3dCUClpUk,1806
12
- aws_annoying-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- aws_annoying-0.2.1.dist-info/entry_points.txt,sha256=s0l5LK26zEUxLRkBHH9Bu5aH1bOPvYVoTMnUcFlzjm8,62
14
- aws_annoying-0.2.1.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
15
- aws_annoying-0.2.1.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- aws-annoying = aws_annoying.main:entrypoint
File without changes
File without changes