aws-annoying 0.4.0__py3-none-any.whl → 0.6.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.
- aws_annoying/cli/app.py +81 -0
- aws_annoying/cli/ecs/__init__.py +3 -0
- aws_annoying/cli/ecs/_app.py +9 -0
- aws_annoying/cli/{ecs_task_definition_lifecycle.py → ecs/task_definition_lifecycle.py} +18 -13
- aws_annoying/cli/ecs/wait_for_deployment.py +94 -0
- aws_annoying/cli/load_variables.py +22 -22
- aws_annoying/cli/logging_handler.py +52 -0
- aws_annoying/cli/main.py +1 -1
- aws_annoying/cli/mfa/configure.py +21 -12
- aws_annoying/cli/session_manager/_common.py +1 -2
- aws_annoying/cli/session_manager/install.py +10 -7
- aws_annoying/cli/session_manager/port_forward.py +41 -38
- aws_annoying/cli/session_manager/start.py +48 -2
- aws_annoying/cli/session_manager/stop.py +9 -7
- aws_annoying/ecs/__init__.py +17 -0
- aws_annoying/ecs/common.py +8 -0
- aws_annoying/ecs/deployment_waiter.py +274 -0
- aws_annoying/ecs/errors.py +14 -0
- aws_annoying/{mfa.py → mfa_config.py} +7 -2
- aws_annoying/session_manager/__init__.py +8 -1
- aws_annoying/session_manager/session_manager.py +26 -39
- aws_annoying/session_manager/shortcuts.py +76 -0
- aws_annoying/utils/ec2.py +36 -0
- aws_annoying/utils/platform.py +11 -0
- aws_annoying/utils/timeout.py +88 -0
- aws_annoying/{variables.py → variable_loader.py} +11 -16
- {aws_annoying-0.4.0.dist-info → aws_annoying-0.6.0.dist-info}/METADATA +47 -2
- aws_annoying-0.6.0.dist-info/RECORD +41 -0
- aws_annoying-0.4.0.dist-info/RECORD +0 -30
- {aws_annoying-0.4.0.dist-info → aws_annoying-0.6.0.dist-info}/WHEEL +0 -0
- {aws_annoying-0.4.0.dist-info → aws_annoying-0.6.0.dist-info}/entry_points.txt +0 -0
- {aws_annoying-0.4.0.dist-info → aws_annoying-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple
|
|
|
13
13
|
|
|
14
14
|
import boto3
|
|
15
15
|
|
|
16
|
-
from aws_annoying.utils.platform import command_as_root, is_root, os_release
|
|
16
|
+
from aws_annoying.utils.platform import command_as_root, is_root, is_windows, os_release
|
|
17
17
|
|
|
18
18
|
from .errors import PluginNotInstalledError, UnsupportedPlatformError
|
|
19
19
|
|
|
@@ -22,21 +22,17 @@ if TYPE_CHECKING:
|
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
25
|
-
# TODO(lasuillard): Platform checking is spread everywhere, should be moved to a single place
|
|
26
|
-
|
|
27
25
|
|
|
28
26
|
class SessionManager:
|
|
29
27
|
"""AWS Session Manager plugin manager."""
|
|
30
28
|
|
|
31
|
-
def __init__(self, *, session: boto3.session.Session | None = None
|
|
29
|
+
def __init__(self, *, session: boto3.session.Session | None = None) -> None:
|
|
32
30
|
"""Initialize SessionManager.
|
|
33
31
|
|
|
34
32
|
Args:
|
|
35
33
|
session: Boto3 session to use for AWS operations.
|
|
36
|
-
downloader: File downloader to use for downloading the plugin.
|
|
37
34
|
"""
|
|
38
35
|
self.session = session or boto3.session.Session()
|
|
39
|
-
self.downloader = downloader
|
|
40
36
|
|
|
41
37
|
# ------------------------------------------------------------------------
|
|
42
38
|
# Installation
|
|
@@ -67,7 +63,7 @@ class SessionManager:
|
|
|
67
63
|
"""Get the path to the session-manager-plugin binary."""
|
|
68
64
|
binary_path_str = shutil.which("session-manager-plugin")
|
|
69
65
|
if not binary_path_str:
|
|
70
|
-
if
|
|
66
|
+
if is_windows():
|
|
71
67
|
# Windows: use the default installation path
|
|
72
68
|
binary_path = (
|
|
73
69
|
Path(os.environ["ProgramFiles"]) # noqa: SIM112
|
|
@@ -90,6 +86,7 @@ class SessionManager:
|
|
|
90
86
|
linux_distribution: _LinuxDistribution | None = None,
|
|
91
87
|
arch: str | None = None,
|
|
92
88
|
root: bool | None = None,
|
|
89
|
+
downloader: AbstractDownloader,
|
|
93
90
|
) -> None:
|
|
94
91
|
"""Install AWS Session Manager plugin.
|
|
95
92
|
|
|
@@ -99,17 +96,23 @@ class SessionManager:
|
|
|
99
96
|
If `None` and current `os` is `"Linux"`, will try to detect the distribution from current system.
|
|
100
97
|
arch: The architecture to install the plugin on. If `None`, will use the current architecture.
|
|
101
98
|
root: Whether to run the installation as root. If `None`, will check if the current user is root.
|
|
99
|
+
downloader: File downloader to use for downloading the plugin.
|
|
102
100
|
"""
|
|
103
101
|
os = os or platform.system()
|
|
104
102
|
arch = arch or platform.machine()
|
|
105
103
|
|
|
106
104
|
if os == "Windows":
|
|
107
|
-
self._install_windows()
|
|
105
|
+
self._install_windows(downloader=downloader)
|
|
108
106
|
elif os == "Darwin":
|
|
109
|
-
self._install_macos(arch=arch, root=root or is_root())
|
|
107
|
+
self._install_macos(arch=arch, root=root or is_root(), downloader=downloader)
|
|
110
108
|
elif os == "Linux":
|
|
111
109
|
linux_distribution = linux_distribution or _detect_linux_distribution()
|
|
112
|
-
self._install_linux(
|
|
110
|
+
self._install_linux(
|
|
111
|
+
linux_distribution=linux_distribution,
|
|
112
|
+
arch=arch,
|
|
113
|
+
root=root or is_root(),
|
|
114
|
+
downloader=downloader,
|
|
115
|
+
)
|
|
113
116
|
else:
|
|
114
117
|
msg = f"Unsupported operating system: {os}"
|
|
115
118
|
raise UnsupportedPlatformError(msg)
|
|
@@ -118,20 +121,20 @@ class SessionManager:
|
|
|
118
121
|
"""Hook to run before invoking plugin installation command."""
|
|
119
122
|
|
|
120
123
|
# https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-windows.html
|
|
121
|
-
def _install_windows(self) -> None:
|
|
124
|
+
def _install_windows(self, *, downloader: AbstractDownloader) -> None:
|
|
122
125
|
"""Install session-manager-plugin on Windows via EXE installer."""
|
|
123
126
|
download_url = (
|
|
124
127
|
"https://s3.amazonaws.com/session-manager-downloads/plugin/latest/windows/SessionManagerPluginSetup.exe"
|
|
125
128
|
)
|
|
126
129
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
127
130
|
p = Path(temp_dir)
|
|
128
|
-
exe_installer =
|
|
131
|
+
exe_installer = downloader.download(download_url, to=p / "SessionManagerPluginSetup.exe")
|
|
129
132
|
command = [str(exe_installer), "/quiet"]
|
|
130
133
|
self.before_install(command)
|
|
131
134
|
subprocess.call(command, cwd=p) # noqa: S603
|
|
132
135
|
|
|
133
136
|
# https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-macos-overview.html
|
|
134
|
-
def _install_macos(self, *, arch: str, root: bool) -> None:
|
|
137
|
+
def _install_macos(self, *, arch: str, root: bool, downloader: AbstractDownloader) -> None:
|
|
135
138
|
"""Install session-manager-plugin on macOS via signed installer."""
|
|
136
139
|
# ! Intel chip will not be supported
|
|
137
140
|
if arch == "x86_64":
|
|
@@ -148,7 +151,7 @@ class SessionManager:
|
|
|
148
151
|
|
|
149
152
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
150
153
|
p = Path(temp_dir)
|
|
151
|
-
pkg_installer =
|
|
154
|
+
pkg_installer = downloader.download(download_url, to=p / "session-manager-plugin.pkg")
|
|
152
155
|
|
|
153
156
|
# Run installer
|
|
154
157
|
command = command_as_root(
|
|
@@ -179,6 +182,7 @@ class SessionManager:
|
|
|
179
182
|
linux_distribution: _LinuxDistribution,
|
|
180
183
|
arch: str,
|
|
181
184
|
root: bool,
|
|
185
|
+
downloader: AbstractDownloader,
|
|
182
186
|
) -> None:
|
|
183
187
|
name = linux_distribution.name
|
|
184
188
|
version = linux_distribution.version
|
|
@@ -200,7 +204,7 @@ class SessionManager:
|
|
|
200
204
|
|
|
201
205
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
202
206
|
p = Path(temp_dir)
|
|
203
|
-
deb_installer =
|
|
207
|
+
deb_installer = downloader.download(download_url, to=p / "session-manager-plugin.deb")
|
|
204
208
|
|
|
205
209
|
# Invoke installation command
|
|
206
210
|
command = command_as_root(["dpkg", "--install", str(deb_installer)], root=root)
|
|
@@ -241,28 +245,25 @@ class SessionManager:
|
|
|
241
245
|
raise UnsupportedPlatformError(msg)
|
|
242
246
|
|
|
243
247
|
# ------------------------------------------------------------------------
|
|
244
|
-
#
|
|
248
|
+
# Command
|
|
245
249
|
# ------------------------------------------------------------------------
|
|
246
|
-
def
|
|
250
|
+
def build_command(
|
|
247
251
|
self,
|
|
248
|
-
*,
|
|
249
252
|
target: str,
|
|
250
253
|
document_name: str,
|
|
251
254
|
parameters: dict[str, Any],
|
|
252
255
|
reason: str | None = None,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"""Start new session.
|
|
256
|
+
) -> list[str]:
|
|
257
|
+
"""Build command for starting a session.
|
|
256
258
|
|
|
257
259
|
Args:
|
|
258
|
-
target: The target instance ID
|
|
260
|
+
target: The target instance ID.
|
|
259
261
|
document_name: The SSM document name to use for the session.
|
|
260
262
|
parameters: The parameters to pass to the SSM document.
|
|
261
263
|
reason: The reason for starting the session.
|
|
262
|
-
log_file: Optional file to log output to.
|
|
263
264
|
|
|
264
265
|
Returns:
|
|
265
|
-
|
|
266
|
+
The command to start the session.
|
|
266
267
|
"""
|
|
267
268
|
is_installed, binary_path, version = self.verify_installation()
|
|
268
269
|
if not is_installed:
|
|
@@ -279,7 +280,7 @@ class SessionManager:
|
|
|
279
280
|
)
|
|
280
281
|
|
|
281
282
|
region = self.session.region_name
|
|
282
|
-
|
|
283
|
+
return [
|
|
283
284
|
str(binary_path),
|
|
284
285
|
json.dumps(response),
|
|
285
286
|
region,
|
|
@@ -289,20 +290,6 @@ class SessionManager:
|
|
|
289
290
|
f"https://ssm.{region}.amazonaws.com",
|
|
290
291
|
]
|
|
291
292
|
|
|
292
|
-
stdout: subprocess._FILE
|
|
293
|
-
if log_file is not None: # noqa: SIM108
|
|
294
|
-
stdout = log_file.open(mode="at+", buffering=1)
|
|
295
|
-
else:
|
|
296
|
-
stdout = subprocess.DEVNULL
|
|
297
|
-
|
|
298
|
-
return subprocess.Popen( # noqa: S603
|
|
299
|
-
command,
|
|
300
|
-
stdout=stdout,
|
|
301
|
-
stderr=subprocess.STDOUT,
|
|
302
|
-
text=True,
|
|
303
|
-
close_fds=False, # FD inherited from parent process
|
|
304
|
-
)
|
|
305
|
-
|
|
306
293
|
|
|
307
294
|
# ? Could be moved to utils, but didn't because it's too specific to this module
|
|
308
295
|
class _LinuxDistribution(NamedTuple):
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from aws_annoying.utils.timeout import Timeout
|
|
9
|
+
|
|
10
|
+
from .session_manager import SessionManager
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextmanager
|
|
20
|
+
def port_forward( # noqa: PLR0913
|
|
21
|
+
*,
|
|
22
|
+
through: str,
|
|
23
|
+
local_port: int,
|
|
24
|
+
remote_host: str,
|
|
25
|
+
remote_port: int,
|
|
26
|
+
reason: str | None = None,
|
|
27
|
+
start_timeout: int | None = None,
|
|
28
|
+
) -> Iterator[subprocess.Popen[str]]:
|
|
29
|
+
"""Context manager for port forwarding sessions.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
through: The instance ID to use as port-forwarding proxy.
|
|
33
|
+
local_port: The local port to listen to.
|
|
34
|
+
remote_host: The remote host to connect to.
|
|
35
|
+
remote_port: The remote port to connect to.
|
|
36
|
+
reason: The reason for starting the session.
|
|
37
|
+
start_timeout: The timeout in seconds to wait for the session to start.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The command to start the session.
|
|
41
|
+
"""
|
|
42
|
+
session_manager = SessionManager()
|
|
43
|
+
command = session_manager.build_command(
|
|
44
|
+
target=through,
|
|
45
|
+
document_name="AWS-StartPortForwardingSessionToRemoteHost",
|
|
46
|
+
parameters={
|
|
47
|
+
"localPortNumber": [str(local_port)],
|
|
48
|
+
"host": [remote_host],
|
|
49
|
+
"portNumber": [str(remote_port)],
|
|
50
|
+
},
|
|
51
|
+
reason=reason,
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
proc = subprocess.Popen( # noqa: S603
|
|
55
|
+
command,
|
|
56
|
+
stdout=subprocess.PIPE,
|
|
57
|
+
stderr=subprocess.STDOUT,
|
|
58
|
+
text=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# * Must be unreachable
|
|
62
|
+
if proc.stdout is None:
|
|
63
|
+
msg = "Standard output is not available"
|
|
64
|
+
raise RuntimeError(msg)
|
|
65
|
+
|
|
66
|
+
# Wait for the session to start
|
|
67
|
+
# ? Not sure this is trustworthy health check
|
|
68
|
+
with Timeout(start_timeout):
|
|
69
|
+
for line in proc.stdout:
|
|
70
|
+
if "Waiting for connections..." in line:
|
|
71
|
+
logger.info("Session started successfully.")
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
yield proc
|
|
75
|
+
finally:
|
|
76
|
+
proc.terminate()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_instance_id_by_name(name_or_id: str, *, session: boto3.session.Session | None = None) -> str | None:
|
|
9
|
+
"""Get the EC2 instance ID by name or ID.
|
|
10
|
+
|
|
11
|
+
Be aware that this function will only return the first instance found
|
|
12
|
+
with the given name, no matter how many instances are found.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
name_or_id: The name or ID of the EC2 instance.
|
|
16
|
+
session: The boto3 session to use. If not provided, a new session will be created.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The instance ID if found, otherwise `None`.
|
|
20
|
+
"""
|
|
21
|
+
if re.match(r"^m?i-[0-9a-f]+$", name_or_id):
|
|
22
|
+
return name_or_id
|
|
23
|
+
|
|
24
|
+
session = session or boto3.session.Session()
|
|
25
|
+
ec2 = session.client("ec2")
|
|
26
|
+
|
|
27
|
+
response = ec2.describe_instances(Filters=[{"Name": "tag:Name", "Values": [name_or_id]}])
|
|
28
|
+
reservations = response["Reservations"]
|
|
29
|
+
if not reservations:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
instances = reservations[0]["Instances"]
|
|
33
|
+
if not instances:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
return str(instances[0]["InstanceId"])
|
aws_annoying/utils/platform.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import platform
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
|
|
@@ -25,3 +26,13 @@ def os_release() -> dict[str, str]:
|
|
|
25
26
|
key.strip('"'): value.strip('"')
|
|
26
27
|
for key, value in (line.split("=", 1) for line in content.splitlines() if "=" in line)
|
|
27
28
|
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_macos() -> bool:
|
|
32
|
+
"""Check if the current OS is macOS."""
|
|
33
|
+
return platform.system() == "Darwin"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_windows() -> bool:
|
|
37
|
+
"""Check if the current OS is Windows."""
|
|
38
|
+
return platform.system() == "Windows"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import PositiveInt, validate_call
|
|
8
|
+
|
|
9
|
+
from aws_annoying.utils.platform import is_windows
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from types import FrameType
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OperationTimeoutError(Exception):
|
|
17
|
+
"""Operation timed out."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_F = TypeVar("_F", bound=Callable)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Timeout:
|
|
24
|
+
"""Timeout handler utilizing signals.
|
|
25
|
+
|
|
26
|
+
This utility relies on Unix signals (`signal.SIGALRM`). The behavior will be dummied
|
|
27
|
+
to do nothing on Windows OS.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@validate_call
|
|
31
|
+
def __init__(self, seconds: Optional[PositiveInt]) -> None:
|
|
32
|
+
"""Initialize timeout handler.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
seconds: The timeout in seconds. `None` means no timeout,
|
|
36
|
+
allowing the function to run normally.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
self.timeout_seconds = seconds
|
|
40
|
+
|
|
41
|
+
self._signal_handler_registered = False
|
|
42
|
+
|
|
43
|
+
def _set_signal_handler(self) -> None:
|
|
44
|
+
if is_windows() or self.timeout_seconds is None:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
signal.signal(signal.SIGALRM, self._handler)
|
|
48
|
+
signal.alarm(self.timeout_seconds)
|
|
49
|
+
self._signal_handler_registered = True
|
|
50
|
+
|
|
51
|
+
def _handler(self, signum: int, frame: FrameType | None) -> Any: # noqa: ARG002
|
|
52
|
+
msg = "Timeout reached"
|
|
53
|
+
raise OperationTimeoutError(msg)
|
|
54
|
+
|
|
55
|
+
def _reset_signal_handler(self) -> None:
|
|
56
|
+
if is_windows() or not self._signal_handler_registered:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
signal.signal(signal.SIGALRM, signal.SIG_IGN)
|
|
60
|
+
signal.alarm(0)
|
|
61
|
+
self._signal_handler_registered = False
|
|
62
|
+
|
|
63
|
+
def __call__(self, func: _F) -> _F:
|
|
64
|
+
"""Decorator to set a timeout for a function.
|
|
65
|
+
|
|
66
|
+
Please note, using this decorator in nested functions may not work properly as
|
|
67
|
+
the signal handler for outer functions may not be resumed correctly.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
OperationTimeoutError: When timeout is reached.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@wraps(func)
|
|
74
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
75
|
+
self._set_signal_handler()
|
|
76
|
+
try:
|
|
77
|
+
return func(*args, **kwargs)
|
|
78
|
+
finally:
|
|
79
|
+
self._reset_signal_handler()
|
|
80
|
+
|
|
81
|
+
return cast("_F", wrapper)
|
|
82
|
+
|
|
83
|
+
def __enter__(self) -> None:
|
|
84
|
+
self._set_signal_handler()
|
|
85
|
+
|
|
86
|
+
def __exit__(self, *args: object) -> Any:
|
|
87
|
+
self._reset_signal_handler()
|
|
88
|
+
return False # Re-raise
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
5
6
|
from typing import Any, TypedDict
|
|
6
7
|
|
|
7
8
|
import boto3
|
|
8
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
9
12
|
# Type aliases for readability
|
|
10
13
|
_ARN = str
|
|
11
14
|
_Variables = dict[str, Any]
|
|
12
15
|
|
|
13
|
-
# TODO(lasuillard): Need some refactoring (with #2, #3)
|
|
14
|
-
# TODO(lasuillard): Put some logging
|
|
15
|
-
|
|
16
16
|
|
|
17
17
|
class _LoadStatsDict(TypedDict):
|
|
18
18
|
secrets: int
|
|
@@ -20,14 +20,13 @@ class _LoadStatsDict(TypedDict):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class VariableLoader: # noqa: D101
|
|
23
|
-
def __init__(self, *,
|
|
24
|
-
"""Initialize
|
|
23
|
+
def __init__(self, *, session: boto3.session.Session | None = None) -> None:
|
|
24
|
+
"""Initialize variable loader.
|
|
25
25
|
|
|
26
26
|
Args:
|
|
27
|
-
|
|
28
|
-
console: Rich console instance.
|
|
27
|
+
session: Boto3 session to use for AWS operations.
|
|
29
28
|
"""
|
|
30
|
-
self.
|
|
29
|
+
self.session = session or boto3.session.Session()
|
|
31
30
|
|
|
32
31
|
# TODO(lasuillard): Currently not using pagination (do we need more than 10-20 secrets or parameters each?)
|
|
33
32
|
# ; consider adding it if needed
|
|
@@ -54,12 +53,8 @@ class VariableLoader: # noqa: D101
|
|
|
54
53
|
# Retrieve variables from AWS resources
|
|
55
54
|
secrets: dict[str, _Variables]
|
|
56
55
|
parameters: dict[str, _Variables]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
parameters = {idx: {} for idx, _ in parameters_map.items()}
|
|
60
|
-
else:
|
|
61
|
-
secrets = self._retrieve_secrets(secrets_map)
|
|
62
|
-
parameters = self._retrieve_parameters(parameters_map)
|
|
56
|
+
secrets = self._retrieve_secrets(secrets_map)
|
|
57
|
+
parameters = self._retrieve_parameters(parameters_map)
|
|
63
58
|
|
|
64
59
|
load_stats: _LoadStatsDict = {
|
|
65
60
|
"secrets": len(secrets),
|
|
@@ -79,7 +74,7 @@ class VariableLoader: # noqa: D101
|
|
|
79
74
|
if not secrets_map:
|
|
80
75
|
return {}
|
|
81
76
|
|
|
82
|
-
secretsmanager =
|
|
77
|
+
secretsmanager = self.session.client("secretsmanager")
|
|
83
78
|
|
|
84
79
|
# Retrieve the secrets
|
|
85
80
|
arns = list(secrets_map.values())
|
|
@@ -108,7 +103,7 @@ class VariableLoader: # noqa: D101
|
|
|
108
103
|
if not parameters_map:
|
|
109
104
|
return {}
|
|
110
105
|
|
|
111
|
-
ssm =
|
|
106
|
+
ssm = self.session.client("ssm")
|
|
112
107
|
|
|
113
108
|
# Retrieve the parameters
|
|
114
109
|
parameter_names = list(parameters_map.values())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-annoying
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Utils to handle some annoying AWS tasks.
|
|
5
5
|
Project-URL: Homepage, https://github.com/lasuillard/aws-annoying
|
|
6
6
|
Project-URL: Repository, https://github.com/lasuillard/aws-annoying.git
|
|
@@ -22,13 +22,15 @@ Requires-Dist: types-requests>=2.31.0.6; extra == 'dev'
|
|
|
22
22
|
Provides-Extra: test
|
|
23
23
|
Requires-Dist: coverage<7.9,>=7.6; extra == 'test'
|
|
24
24
|
Requires-Dist: moto[ecs,secretsmanager,server,ssm]~=5.1.1; extra == 'test'
|
|
25
|
-
Requires-Dist: pytest-cov
|
|
25
|
+
Requires-Dist: pytest-cov<6.2,>=6.0; extra == 'test'
|
|
26
26
|
Requires-Dist: pytest-env~=1.1.1; extra == 'test'
|
|
27
27
|
Requires-Dist: pytest-snapshot>=0.9.0; extra == 'test'
|
|
28
28
|
Requires-Dist: pytest-sugar~=1.0.0; extra == 'test'
|
|
29
29
|
Requires-Dist: pytest-xdist>=3.6.1; extra == 'test'
|
|
30
30
|
Requires-Dist: pytest~=8.3.2; extra == 'test'
|
|
31
31
|
Requires-Dist: testcontainers[localstack]>=4.9.2; extra == 'test'
|
|
32
|
+
Requires-Dist: toml>=0.10.2; extra == 'test'
|
|
33
|
+
Requires-Dist: types-toml>=0.10.8.20240310; extra == 'test'
|
|
32
34
|
Description-Content-Type: text/markdown
|
|
33
35
|
|
|
34
36
|
# aws-annoying
|
|
@@ -39,3 +41,46 @@ Description-Content-Type: text/markdown
|
|
|
39
41
|

|
|
40
42
|
|
|
41
43
|
Utils to handle some annoying AWS tasks.
|
|
44
|
+
|
|
45
|
+
## ❓ About
|
|
46
|
+
|
|
47
|
+
This project aims to provide a set of utilities and examples to help with some annoying tasks when working with AWS.
|
|
48
|
+
|
|
49
|
+
Major directories of the project:
|
|
50
|
+
|
|
51
|
+
- **aws_annoying** Python package containing CLI and utility functions.
|
|
52
|
+
- **console** Utilities to help working with AWS Console.
|
|
53
|
+
- **examples** Examples of how to use the package.
|
|
54
|
+
|
|
55
|
+
## 🚀 Installation
|
|
56
|
+
|
|
57
|
+
It is recommended to use [pipx](https://pipx.pypa.io/stable/) to install `aws-annoying`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ pipx install aws-annoying
|
|
61
|
+
$ aws-annoying --help
|
|
62
|
+
|
|
63
|
+
Usage: aws-annoying [OPTIONS] COMMAND [ARGS]...
|
|
64
|
+
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Available commands:
|
|
69
|
+
|
|
70
|
+
- **ecs** ECS utilities.
|
|
71
|
+
- **task-definition-lifecycle** Help to manage ECS task definitions lifecycle.
|
|
72
|
+
- **wait-for-deployment** Wait for ECS deployment to complete.
|
|
73
|
+
- **load-variables** Wrapper command to run command with variables from AWS resources injected as environment variables.
|
|
74
|
+
- **mfa** Commands to manage MFA authentication.
|
|
75
|
+
- **configure** Configure AWS profile for MFA.
|
|
76
|
+
- **session-manager** AWS Session Manager CLI utilities.
|
|
77
|
+
- **install** Install AWS Session Manager plugin.
|
|
78
|
+
- **port-forward** Start a port forwarding session using AWS Session Manager.
|
|
79
|
+
- **start** Start new session.
|
|
80
|
+
- **stop** Stop running session for PID file.
|
|
81
|
+
|
|
82
|
+
Please refer to the CLI help for more information about the available commands and options.
|
|
83
|
+
|
|
84
|
+
## 💖 Contributing
|
|
85
|
+
|
|
86
|
+
Any feedback, suggestions or contributions are welcome! Feel free to open an issue or a pull request.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
aws_annoying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
aws_annoying/mfa_config.py,sha256=z0GpRhLHEWaaXbECV4Ei4oNM1WCFoEZAxCIPbpY4Ymc,2200
|
|
3
|
+
aws_annoying/variable_loader.py,sha256=N9qPPHG6mzSIIHrWJnJ_FV5kKZxssOaTHoAQEwiDE3s,4569
|
|
4
|
+
aws_annoying/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
aws_annoying/cli/app.py,sha256=6opsAfIFGPnUSK68s7sEnFzLhipnevfP2cDJ0yj7Lh0,2366
|
|
6
|
+
aws_annoying/cli/load_variables.py,sha256=83RGt5eA_TJtza3kyLXoSM_A3lfQs6TNC2TCrWYia_I,5032
|
|
7
|
+
aws_annoying/cli/logging_handler.py,sha256=JPsZePV3YH9e1nLv0Q3fqSeXSEfZ5cGJo6eLK2F4oxE,1426
|
|
8
|
+
aws_annoying/cli/main.py,sha256=bU4Gxic5_3qrrd8l9eN709-D4o_OHgrdH91FS9Xs8zI,477
|
|
9
|
+
aws_annoying/cli/ecs/__init__.py,sha256=IxfaMXcGU6WTHE_RXj-aitXtSg25j5m3HGTG9O02GjI,125
|
|
10
|
+
aws_annoying/cli/ecs/_app.py,sha256=izD0VL55i7oG-2CtWCV21bSoAeE-DZbxyJ5pi6VXhjU,200
|
|
11
|
+
aws_annoying/cli/ecs/task_definition_lifecycle.py,sha256=W7zr8zEPjjHgss9Tnb3QMcJ_45Yb9SJ5D1B315vb9kc,2803
|
|
12
|
+
aws_annoying/cli/ecs/wait_for_deployment.py,sha256=0lzl5t0TOvJJeMxIA6if2TbG7bJFf6JulhPHZqNXjFM,3169
|
|
13
|
+
aws_annoying/cli/mfa/__init__.py,sha256=rbEGhw5lOQZV_XAc3nSbo56JVhsSPpeOgEtiAy9qzEA,50
|
|
14
|
+
aws_annoying/cli/mfa/_app.py,sha256=Ub7gxb6kGF3Ve1ucQSOjHmc4jAu8mxgegcXsIbOzLLQ,189
|
|
15
|
+
aws_annoying/cli/mfa/configure.py,sha256=s2SCz7gzvIE9mJM7w95JPwoGhuQrgkFhjUd6qjD169k,3957
|
|
16
|
+
aws_annoying/cli/session_manager/__init__.py,sha256=FkT6jT6OXduOURN61d-U6hgd-XluQbvuVtKXXiXgSEk,105
|
|
17
|
+
aws_annoying/cli/session_manager/_app.py,sha256=OVOHW0iyKzunvaqLhjoseHw1-WxJ1gGb7QmiyAEezyY,221
|
|
18
|
+
aws_annoying/cli/session_manager/_common.py,sha256=Uj-MF7z8Qntd24Z03xxE-jSKcgrsd8xl41E6db4qCtY,711
|
|
19
|
+
aws_annoying/cli/session_manager/install.py,sha256=VIk6313jUf6THwvs06AOUQwhTjzCGCcvDXMxyz0BcgE,1477
|
|
20
|
+
aws_annoying/cli/session_manager/port_forward.py,sha256=upsUsd7MkUfaAP3o0ZK6N-SkOBnr1ogwqYozG4TDTzY,4331
|
|
21
|
+
aws_annoying/cli/session_manager/start.py,sha256=1Q-WFvbQkMgVntwIrM2HfpUKmVL38aFrjJjG3aA0fzU,1447
|
|
22
|
+
aws_annoying/cli/session_manager/stop.py,sha256=QjjOmmhZr_0IInhOyHvSlodk7Nx4qu9EtrRw4kl8Hks,1535
|
|
23
|
+
aws_annoying/ecs/__init__.py,sha256=Bohwe4-jxF1cxQmErCXl0l5ma08HpWmV2PqdAS-gf4w,432
|
|
24
|
+
aws_annoying/ecs/common.py,sha256=TvP27SEvdIBnA92Oude-oDCy1SuaYNdtpokkbpZmdzo,139
|
|
25
|
+
aws_annoying/ecs/deployment_waiter.py,sha256=e3xWShvr9piraoEMJdZun2GuffkyvTI76-riECfoe9A,9936
|
|
26
|
+
aws_annoying/ecs/errors.py,sha256=n9j_h1MDUV6IVabKgwbCVAiPZQNJDJ5rVRHA82Je5QQ,429
|
|
27
|
+
aws_annoying/session_manager/__init__.py,sha256=IENviL3ux2LF7o9xFGYEiqaGw03hxnyNX2btbB1xyEU,318
|
|
28
|
+
aws_annoying/session_manager/errors.py,sha256=YioKlRtZ-GUP0F_ts_ebw7-HYkxe8mTes6HK821Kuiw,353
|
|
29
|
+
aws_annoying/session_manager/session_manager.py,sha256=pUsyJ_w9UzdIfHA2Z8kU6UcZxOqypFXH1rl6pDytdqo,12046
|
|
30
|
+
aws_annoying/session_manager/shortcuts.py,sha256=Yn4wCl43lfttrZ7GbzfGua2jZHe5Fe6ClEy4ikg-Q_s,2143
|
|
31
|
+
aws_annoying/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
aws_annoying/utils/debugger.py,sha256=UFllDCGI2gPtwo1XS5vqw0qyR6bYr7XknmBwSxalKIc,754
|
|
33
|
+
aws_annoying/utils/downloader.py,sha256=aB5RzT-LpbFX24-2HXlAkdgVowc4TR9FWT_K8WwZ1BE,1923
|
|
34
|
+
aws_annoying/utils/ec2.py,sha256=RjEA5eO53c-H6VE6cZBeB1RJo1rxpDQFhbZTYdANbs8,1062
|
|
35
|
+
aws_annoying/utils/platform.py,sha256=TBIzCzYiFj36HmndZedegvFlxPSNtBQyAxzuwelvxNg,985
|
|
36
|
+
aws_annoying/utils/timeout.py,sha256=PjfFtiLALh7lQvchtMYOfjTNrfuZwCaeaPKW16EpM5c,2483
|
|
37
|
+
aws_annoying-0.6.0.dist-info/METADATA,sha256=68sKE-Atya0ibgT1ljJsaKHpnKXHDq-x7EdUy0yRIqI,3503
|
|
38
|
+
aws_annoying-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
39
|
+
aws_annoying-0.6.0.dist-info/entry_points.txt,sha256=DcKE5V0WvVJ8wUOHxyUz1yLAJOuuJUgRPlMcQ4O7jEs,66
|
|
40
|
+
aws_annoying-0.6.0.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
|
|
41
|
+
aws_annoying-0.6.0.dist-info/RECORD,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
aws_annoying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
aws_annoying/mfa.py,sha256=m6-V1bWeUWsAmRddl-lv13mPCMnftoPzJoNnZ0kiaWQ,2007
|
|
3
|
-
aws_annoying/variables.py,sha256=a9cMS9JU-XA2h1tztO7ofixoDEpqtS_eVEiWrQ75mTo,4761
|
|
4
|
-
aws_annoying/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
aws_annoying/cli/app.py,sha256=sp50uVoAl4D6Wk3DFpzKZzSsxmSxNYejFxm62b_Kxps,201
|
|
6
|
-
aws_annoying/cli/ecs_task_definition_lifecycle.py,sha256=O36Bf5LBnVJNyYmdlUxhtsIHNoxky1t5YacAXiL9UEI,2803
|
|
7
|
-
aws_annoying/cli/load_variables.py,sha256=eWNByUEc1ijF8uCe_egdAnjWxfMNCZeVr0vtTtQLe3Y,5086
|
|
8
|
-
aws_annoying/cli/main.py,sha256=TSzPeMkgIgKFf3bom_vDkFYK0bHF1r5K9ADreZUV3k4,503
|
|
9
|
-
aws_annoying/cli/mfa/__init__.py,sha256=rbEGhw5lOQZV_XAc3nSbo56JVhsSPpeOgEtiAy9qzEA,50
|
|
10
|
-
aws_annoying/cli/mfa/_app.py,sha256=Ub7gxb6kGF3Ve1ucQSOjHmc4jAu8mxgegcXsIbOzLLQ,189
|
|
11
|
-
aws_annoying/cli/mfa/configure.py,sha256=vsoHfTVFF2dPgiYsp2L-EkMwtAA0_-tVwFd6Wv6DscU,3746
|
|
12
|
-
aws_annoying/cli/session_manager/__init__.py,sha256=FkT6jT6OXduOURN61d-U6hgd-XluQbvuVtKXXiXgSEk,105
|
|
13
|
-
aws_annoying/cli/session_manager/_app.py,sha256=OVOHW0iyKzunvaqLhjoseHw1-WxJ1gGb7QmiyAEezyY,221
|
|
14
|
-
aws_annoying/cli/session_manager/_common.py,sha256=0fCm6Zx6P1NcyyiHlSoB9PgIdrxzUXLVPqKJWQnJ8I4,792
|
|
15
|
-
aws_annoying/cli/session_manager/install.py,sha256=zX2cu-IBYlf9yl8z9P0CTQz72BP6aAMo-KnolC_PGsU,1443
|
|
16
|
-
aws_annoying/cli/session_manager/port_forward.py,sha256=uMcsafTNAHZh-0E1yWXCliMivWyuWV2K9DdVhBZ9pG8,4327
|
|
17
|
-
aws_annoying/cli/session_manager/start.py,sha256=1yaMuy-7IWrrDoPHjgygOP_-g_tajfnpaVPZanxsZxs,215
|
|
18
|
-
aws_annoying/cli/session_manager/stop.py,sha256=ttU6nlbVgBkZDtY-DwUyCstv5TFtat5TljkyuY8QICU,1482
|
|
19
|
-
aws_annoying/session_manager/__init__.py,sha256=swbRdFh9CdO6tzyBjSxN0KC7MMASvptIpbsijUPIZgI,243
|
|
20
|
-
aws_annoying/session_manager/errors.py,sha256=YioKlRtZ-GUP0F_ts_ebw7-HYkxe8mTes6HK821Kuiw,353
|
|
21
|
-
aws_annoying/session_manager/session_manager.py,sha256=7tqfFPA4bWF0Me_VbUDcq5Cpl1sw3bevGyT8QWfLTOk,12454
|
|
22
|
-
aws_annoying/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
aws_annoying/utils/debugger.py,sha256=UFllDCGI2gPtwo1XS5vqw0qyR6bYr7XknmBwSxalKIc,754
|
|
24
|
-
aws_annoying/utils/downloader.py,sha256=aB5RzT-LpbFX24-2HXlAkdgVowc4TR9FWT_K8WwZ1BE,1923
|
|
25
|
-
aws_annoying/utils/platform.py,sha256=h3DUWmTMM-_4TfTWNqY0uNqyVsBjAuMm2DEbG-daxe8,742
|
|
26
|
-
aws_annoying-0.4.0.dist-info/METADATA,sha256=9u5wofuLZXXPrAj1YcOhbqPlPH46jtub8AUvs2KNZ9s,1916
|
|
27
|
-
aws_annoying-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
28
|
-
aws_annoying-0.4.0.dist-info/entry_points.txt,sha256=DcKE5V0WvVJ8wUOHxyUz1yLAJOuuJUgRPlMcQ4O7jEs,66
|
|
29
|
-
aws_annoying-0.4.0.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
|
|
30
|
-
aws_annoying-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|