aws-annoying 0.4.0__tar.gz → 0.5.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.
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/PKG-INFO +1 -1
- aws_annoying-0.5.0/aws_annoying/cli/session_manager/_common.py +54 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/session_manager/install.py +2 -2
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/session_manager/port_forward.py +26 -33
- aws_annoying-0.5.0/aws_annoying/cli/session_manager/start.py +47 -0
- aws_annoying-0.5.0/aws_annoying/session_manager/__init__.py +11 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/session_manager/session_manager.py +24 -35
- aws_annoying-0.5.0/aws_annoying/session_manager/shortcuts.py +72 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/console/ecs-exec/ecs-exec.user.js +3 -3
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/pyproject.toml +1 -1
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/session_manager/test_session_manager.py +2 -3
- aws_annoying-0.5.0/tests/utils/test_platform.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/uv.lock +1 -1
- aws_annoying-0.4.0/aws_annoying/cli/session_manager/_common.py +0 -24
- aws_annoying-0.4.0/aws_annoying/cli/session_manager/start.py +0 -9
- aws_annoying-0.4.0/aws_annoying/session_manager/__init__.py +0 -4
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.devcontainer/.env.example +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.devcontainer/Dockerfile +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.devcontainer/devcontainer.json +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.devcontainer/docker-compose.devcontainer.yaml +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.devcontainer/onCreateCommand.sh +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.devcontainer/postAttachCommand.sh +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.editorconfig +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.gitattributes +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.github/dependabot.yaml +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.github/workflows/ci.yaml +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.github/workflows/release.yaml +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.gitignore +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.pre-commit-config.yaml +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.python-version +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.vscode/extensions.json +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.vscode/launch.json +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/.vscode/settings.json +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/LICENSE +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/Makefile +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/README.md +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/app.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/ecs_task_definition_lifecycle.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/load_variables.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/main.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/mfa/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/mfa/_app.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/mfa/configure.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/session_manager/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/session_manager/_app.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/cli/session_manager/stop.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/mfa.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/session_manager/errors.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/utils/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/utils/debugger.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/utils/downloader.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/utils/platform.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/aws_annoying/variables.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/console/ecs-exec/README.md +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/console/ecs-exec/ecs-console.png +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/console/ecs-exec/session-manager.png +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/examples/dbeaver/README.md +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/examples/dbeaver/after-disconnect.png +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/examples/dbeaver/before-connect.png +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/examples/dbeaver/new-connection.png +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/examples/dbeaver/test-connection.png +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/_helpers/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/_helpers/command_builder.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/_helpers/invoke.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/_helpers/scripts/printenv.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/_helpers/string_.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/mfa/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/mfa/snapshots/test_configure/test_basic/persist/aws_config.ini +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/mfa/snapshots/test_configure/test_basic/persist/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/mfa/snapshots/test_configure/test_basic/skip_persist/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/mfa/snapshots/test_configure/test_load_existing_config/aws_config.ini +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/mfa/snapshots/test_configure/test_load_existing_config/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/mfa/test_configure.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/session_manager/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/session_manager/test_install.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/session_manager/test_port_forward.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/session_manager/test_start.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/session_manager/test_stop.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_ecs_task_definition_lifecycle/test_basic/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_ecs_task_definition_lifecycle/test_dry_run/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_basic/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_dry_run/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_env_prefix/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_nothing/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_overwrite_env/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_replace_quiet/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_resource_not_found/ssm/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/snapshots/test_load_variables/test_unsupported_resource/stdout.txt +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/test_app.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/test_ecs_task_definition_lifecycle.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/test_load_variables.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/cli/test_main.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/conftest.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/session_manager/__init__.py +0 -0
- {aws_annoying-0.4.0 → aws_annoying-0.5.0}/tests/session_manager/test_errors.py +0 -0
- /aws_annoying-0.4.0/tests/test_mfa.py → /aws_annoying-0.5.0/tests/session_manager/test_shortcuts.py +0 -0
- /aws_annoying-0.4.0/tests/test_variables.py → /aws_annoying-0.5.0/tests/test_mfa.py +0 -0
- /aws_annoying-0.4.0/tests/utils/__init__.py → /aws_annoying-0.5.0/tests/test_variables.py +0 -0
- /aws_annoying-0.4.0/tests/utils/test_downloader.py → /aws_annoying-0.5.0/tests/utils/__init__.py +0 -0
- /aws_annoying-0.4.0/tests/utils/test_platform.py → /aws_annoying-0.5.0/tests/utils/test_downloader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-annoying
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# TODO(lasuillard): Using this file until split CLI from library codebase
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
import typer
|
|
9
|
+
from rich.prompt import Confirm
|
|
10
|
+
|
|
11
|
+
from aws_annoying.session_manager import SessionManager as _SessionManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Custom session manager with console interactivity
|
|
15
|
+
class SessionManager(_SessionManager):
|
|
16
|
+
def before_install(self, command: list[str]) -> None:
|
|
17
|
+
if self._confirm:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
confirm = Confirm.ask(f"⚠️ Will run the following command: [bold red]{' '.join(command)}[/bold red]. Proceed?")
|
|
21
|
+
if not confirm:
|
|
22
|
+
raise typer.Abort
|
|
23
|
+
|
|
24
|
+
def install(self, *args: Any, confirm: bool = False, **kwargs: Any) -> None:
|
|
25
|
+
self._confirm = confirm
|
|
26
|
+
return super().install(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_instance_id_by_name(name_or_id: str) -> str | None:
|
|
30
|
+
"""Get the EC2 instance ID by name or ID.
|
|
31
|
+
|
|
32
|
+
Be aware that this function will only return the first instance found
|
|
33
|
+
with the given name, no matter how many instances are found.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name_or_id: The name or ID of the EC2 instance.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The instance ID if found, otherwise `None`.
|
|
40
|
+
"""
|
|
41
|
+
if re.match(r"m?i-.+", name_or_id):
|
|
42
|
+
return name_or_id
|
|
43
|
+
|
|
44
|
+
ec2 = boto3.client("ec2")
|
|
45
|
+
response = ec2.describe_instances(Filters=[{"Name": "tag:Name", "Values": [name_or_id]}])
|
|
46
|
+
reservations = response["Reservations"]
|
|
47
|
+
if not reservations:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
instances = reservations[0]["Instances"]
|
|
51
|
+
if not instances:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
return str(instances[0]["InstanceId"])
|
|
@@ -18,7 +18,7 @@ def install(
|
|
|
18
18
|
),
|
|
19
19
|
) -> None:
|
|
20
20
|
"""Install AWS Session Manager plugin."""
|
|
21
|
-
session_manager = SessionManager(
|
|
21
|
+
session_manager = SessionManager()
|
|
22
22
|
|
|
23
23
|
# Check session-manager-plugin already installed
|
|
24
24
|
is_installed, binary_path, version = session_manager.verify_installation()
|
|
@@ -28,7 +28,7 @@ def install(
|
|
|
28
28
|
|
|
29
29
|
# Install session-manager-plugin
|
|
30
30
|
print("⬇️ Installing AWS Session Manager plugin. You could be prompted for admin privileges request.")
|
|
31
|
-
session_manager.install(confirm=yes)
|
|
31
|
+
session_manager.install(confirm=yes, downloader=TQDMDownloader())
|
|
32
32
|
|
|
33
33
|
# Verify installation
|
|
34
34
|
is_installed, binary_path, version = session_manager.verify_installation()
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import re
|
|
5
4
|
import signal
|
|
5
|
+
import subprocess
|
|
6
6
|
from pathlib import Path # noqa: TC003
|
|
7
7
|
|
|
8
|
-
import boto3
|
|
9
8
|
import typer
|
|
10
9
|
from rich import print # noqa: A004
|
|
11
10
|
|
|
12
|
-
from aws_annoying.utils.downloader import TQDMDownloader
|
|
13
|
-
|
|
14
11
|
from ._app import session_manager_app
|
|
15
|
-
from ._common import SessionManager
|
|
12
|
+
from ._common import SessionManager, get_instance_id_by_name
|
|
16
13
|
|
|
17
14
|
|
|
18
15
|
# https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
|
|
@@ -57,7 +54,7 @@ def port_forward( # noqa: PLR0913
|
|
|
57
54
|
),
|
|
58
55
|
) -> None:
|
|
59
56
|
"""Start a port forwarding session using AWS Session Manager."""
|
|
60
|
-
session_manager = SessionManager(
|
|
57
|
+
session_manager = SessionManager()
|
|
61
58
|
|
|
62
59
|
# Check if the PID file already exists
|
|
63
60
|
if pid_file.exists():
|
|
@@ -80,21 +77,16 @@ def port_forward( # noqa: PLR0913
|
|
|
80
77
|
print(f"⚠️ Tried to terminate process with PID {existing_pid} but does not exist.")
|
|
81
78
|
|
|
82
79
|
# Resolve the instance name or ID
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# If the instance name is provided, get the instance ID
|
|
87
|
-
instance_id = _get_instance_id_by_name(through)
|
|
88
|
-
if instance_id:
|
|
89
|
-
print(f"❗ Instance ID resolved: [bold]{instance_id}[/bold]")
|
|
90
|
-
else:
|
|
91
|
-
print(f"🚫 Instance with name '{through}' not found.")
|
|
92
|
-
raise typer.Exit(1)
|
|
93
|
-
|
|
80
|
+
instance_id = get_instance_id_by_name(through)
|
|
81
|
+
if instance_id:
|
|
82
|
+
print(f"❗ Instance ID resolved: [bold]{instance_id}[/bold]")
|
|
94
83
|
target = instance_id
|
|
84
|
+
else:
|
|
85
|
+
print(f"🚫 Instance with name '{through}' not found.")
|
|
86
|
+
raise typer.Exit(1)
|
|
95
87
|
|
|
96
88
|
# Initiate the session
|
|
97
|
-
|
|
89
|
+
command = session_manager.build_command(
|
|
98
90
|
target=target,
|
|
99
91
|
document_name="AWS-StartPortForwardingSessionToRemoteHost",
|
|
100
92
|
parameters={
|
|
@@ -104,23 +96,24 @@ def port_forward( # noqa: PLR0913
|
|
|
104
96
|
},
|
|
105
97
|
reason=reason,
|
|
106
98
|
)
|
|
99
|
+
stdout: subprocess._FILE
|
|
100
|
+
if log_file is not None: # noqa: SIM108
|
|
101
|
+
stdout = log_file.open(mode="at+", buffering=1)
|
|
102
|
+
else:
|
|
103
|
+
stdout = subprocess.DEVNULL
|
|
104
|
+
|
|
105
|
+
print(
|
|
106
|
+
f"🚀 Starting port forwarding session through [bold]{through}[/bold] with reason: [italic]{reason!r}[/italic].",
|
|
107
|
+
)
|
|
108
|
+
proc = subprocess.Popen( # noqa: S603
|
|
109
|
+
command,
|
|
110
|
+
stdout=stdout,
|
|
111
|
+
stderr=subprocess.STDOUT,
|
|
112
|
+
text=True,
|
|
113
|
+
close_fds=False, # FD inherited from parent process
|
|
114
|
+
)
|
|
107
115
|
print(f"✅ Session Manager Plugin started with PID {proc.pid}. Outputs will be logged to {log_file.absolute()}.")
|
|
108
116
|
|
|
109
117
|
# Write the PID to the file
|
|
110
118
|
pid_file.write_text(str(proc.pid))
|
|
111
119
|
print(f"💾 PID file written to {pid_file.absolute()}.")
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _get_instance_id_by_name(name: str) -> str | None:
|
|
115
|
-
"""Get the EC2 instance ID by name."""
|
|
116
|
-
ec2 = boto3.client("ec2")
|
|
117
|
-
response = ec2.describe_instances(Filters=[{"Name": "tag:Name", "Values": [name]}])
|
|
118
|
-
reservations = response["Reservations"]
|
|
119
|
-
if not reservations:
|
|
120
|
-
return None
|
|
121
|
-
|
|
122
|
-
instances = reservations[0]["Instances"]
|
|
123
|
-
if not instances:
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
return str(instances[0]["InstanceId"])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print # noqa: A004
|
|
7
|
+
|
|
8
|
+
from ._app import session_manager_app
|
|
9
|
+
from ._common import SessionManager, get_instance_id_by_name
|
|
10
|
+
|
|
11
|
+
# TODO(lasuillard): ECS support (#24)
|
|
12
|
+
# TODO(lasuillard): Interactive instance selection
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@session_manager_app.command()
|
|
16
|
+
def start(
|
|
17
|
+
target: str = typer.Option(
|
|
18
|
+
...,
|
|
19
|
+
show_default=False,
|
|
20
|
+
help="The name or ID of the EC2 instance to connect to.",
|
|
21
|
+
),
|
|
22
|
+
reason: str = typer.Option(
|
|
23
|
+
"",
|
|
24
|
+
help="The reason for starting the session.",
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Start new session."""
|
|
28
|
+
session_manager = SessionManager()
|
|
29
|
+
|
|
30
|
+
# Resolve the instance name or ID
|
|
31
|
+
instance_id = get_instance_id_by_name(target)
|
|
32
|
+
if instance_id:
|
|
33
|
+
print(f"❗ Instance ID resolved: [bold]{instance_id}[/bold]")
|
|
34
|
+
target = instance_id
|
|
35
|
+
else:
|
|
36
|
+
print(f"🚫 Instance with name '{target}' not found.")
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
# Start the session, replacing the current process
|
|
40
|
+
print(f"🚀 Starting session to target [bold]{target}[/bold] with reason: [italic]{reason!r}[/italic].")
|
|
41
|
+
command = session_manager.build_command(
|
|
42
|
+
target=target,
|
|
43
|
+
document_name="SSM-SessionManagerRunShell",
|
|
44
|
+
parameters={},
|
|
45
|
+
reason=reason,
|
|
46
|
+
)
|
|
47
|
+
os.execvp(command[0], command) # noqa: S606
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .errors import PluginNotInstalledError, SessionManagerError, UnsupportedPlatformError
|
|
2
|
+
from .session_manager import SessionManager
|
|
3
|
+
from .shortcuts import port_forward
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"PluginNotInstalledError",
|
|
7
|
+
"SessionManager",
|
|
8
|
+
"SessionManagerError",
|
|
9
|
+
"UnsupportedPlatformError",
|
|
10
|
+
"port_forward",
|
|
11
|
+
)
|
|
@@ -28,15 +28,13 @@ logger = logging.getLogger(__name__)
|
|
|
28
28
|
class SessionManager:
|
|
29
29
|
"""AWS Session Manager plugin manager."""
|
|
30
30
|
|
|
31
|
-
def __init__(self, *, session: boto3.session.Session | None = None
|
|
31
|
+
def __init__(self, *, session: boto3.session.Session | None = None) -> None:
|
|
32
32
|
"""Initialize SessionManager.
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
35
|
session: Boto3 session to use for AWS operations.
|
|
36
|
-
downloader: File downloader to use for downloading the plugin.
|
|
37
36
|
"""
|
|
38
37
|
self.session = session or boto3.session.Session()
|
|
39
|
-
self.downloader = downloader
|
|
40
38
|
|
|
41
39
|
# ------------------------------------------------------------------------
|
|
42
40
|
# Installation
|
|
@@ -90,6 +88,7 @@ class SessionManager:
|
|
|
90
88
|
linux_distribution: _LinuxDistribution | None = None,
|
|
91
89
|
arch: str | None = None,
|
|
92
90
|
root: bool | None = None,
|
|
91
|
+
downloader: AbstractDownloader,
|
|
93
92
|
) -> None:
|
|
94
93
|
"""Install AWS Session Manager plugin.
|
|
95
94
|
|
|
@@ -99,17 +98,23 @@ class SessionManager:
|
|
|
99
98
|
If `None` and current `os` is `"Linux"`, will try to detect the distribution from current system.
|
|
100
99
|
arch: The architecture to install the plugin on. If `None`, will use the current architecture.
|
|
101
100
|
root: Whether to run the installation as root. If `None`, will check if the current user is root.
|
|
101
|
+
downloader: File downloader to use for downloading the plugin.
|
|
102
102
|
"""
|
|
103
103
|
os = os or platform.system()
|
|
104
104
|
arch = arch or platform.machine()
|
|
105
105
|
|
|
106
106
|
if os == "Windows":
|
|
107
|
-
self._install_windows()
|
|
107
|
+
self._install_windows(downloader=downloader)
|
|
108
108
|
elif os == "Darwin":
|
|
109
|
-
self._install_macos(arch=arch, root=root or is_root())
|
|
109
|
+
self._install_macos(arch=arch, root=root or is_root(), downloader=downloader)
|
|
110
110
|
elif os == "Linux":
|
|
111
111
|
linux_distribution = linux_distribution or _detect_linux_distribution()
|
|
112
|
-
self._install_linux(
|
|
112
|
+
self._install_linux(
|
|
113
|
+
linux_distribution=linux_distribution,
|
|
114
|
+
arch=arch,
|
|
115
|
+
root=root or is_root(),
|
|
116
|
+
downloader=downloader,
|
|
117
|
+
)
|
|
113
118
|
else:
|
|
114
119
|
msg = f"Unsupported operating system: {os}"
|
|
115
120
|
raise UnsupportedPlatformError(msg)
|
|
@@ -118,20 +123,20 @@ class SessionManager:
|
|
|
118
123
|
"""Hook to run before invoking plugin installation command."""
|
|
119
124
|
|
|
120
125
|
# https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-windows.html
|
|
121
|
-
def _install_windows(self) -> None:
|
|
126
|
+
def _install_windows(self, *, downloader: AbstractDownloader) -> None:
|
|
122
127
|
"""Install session-manager-plugin on Windows via EXE installer."""
|
|
123
128
|
download_url = (
|
|
124
129
|
"https://s3.amazonaws.com/session-manager-downloads/plugin/latest/windows/SessionManagerPluginSetup.exe"
|
|
125
130
|
)
|
|
126
131
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
127
132
|
p = Path(temp_dir)
|
|
128
|
-
exe_installer =
|
|
133
|
+
exe_installer = downloader.download(download_url, to=p / "SessionManagerPluginSetup.exe")
|
|
129
134
|
command = [str(exe_installer), "/quiet"]
|
|
130
135
|
self.before_install(command)
|
|
131
136
|
subprocess.call(command, cwd=p) # noqa: S603
|
|
132
137
|
|
|
133
138
|
# https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-macos-overview.html
|
|
134
|
-
def _install_macos(self, *, arch: str, root: bool) -> None:
|
|
139
|
+
def _install_macos(self, *, arch: str, root: bool, downloader: AbstractDownloader) -> None:
|
|
135
140
|
"""Install session-manager-plugin on macOS via signed installer."""
|
|
136
141
|
# ! Intel chip will not be supported
|
|
137
142
|
if arch == "x86_64":
|
|
@@ -148,7 +153,7 @@ class SessionManager:
|
|
|
148
153
|
|
|
149
154
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
150
155
|
p = Path(temp_dir)
|
|
151
|
-
pkg_installer =
|
|
156
|
+
pkg_installer = downloader.download(download_url, to=p / "session-manager-plugin.pkg")
|
|
152
157
|
|
|
153
158
|
# Run installer
|
|
154
159
|
command = command_as_root(
|
|
@@ -179,6 +184,7 @@ class SessionManager:
|
|
|
179
184
|
linux_distribution: _LinuxDistribution,
|
|
180
185
|
arch: str,
|
|
181
186
|
root: bool,
|
|
187
|
+
downloader: AbstractDownloader,
|
|
182
188
|
) -> None:
|
|
183
189
|
name = linux_distribution.name
|
|
184
190
|
version = linux_distribution.version
|
|
@@ -200,7 +206,7 @@ class SessionManager:
|
|
|
200
206
|
|
|
201
207
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
202
208
|
p = Path(temp_dir)
|
|
203
|
-
deb_installer =
|
|
209
|
+
deb_installer = downloader.download(download_url, to=p / "session-manager-plugin.deb")
|
|
204
210
|
|
|
205
211
|
# Invoke installation command
|
|
206
212
|
command = command_as_root(["dpkg", "--install", str(deb_installer)], root=root)
|
|
@@ -241,28 +247,25 @@ class SessionManager:
|
|
|
241
247
|
raise UnsupportedPlatformError(msg)
|
|
242
248
|
|
|
243
249
|
# ------------------------------------------------------------------------
|
|
244
|
-
#
|
|
250
|
+
# Command
|
|
245
251
|
# ------------------------------------------------------------------------
|
|
246
|
-
def
|
|
252
|
+
def build_command(
|
|
247
253
|
self,
|
|
248
|
-
*,
|
|
249
254
|
target: str,
|
|
250
255
|
document_name: str,
|
|
251
256
|
parameters: dict[str, Any],
|
|
252
257
|
reason: str | None = None,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"""Start new session.
|
|
258
|
+
) -> list[str]:
|
|
259
|
+
"""Build command for starting a session.
|
|
256
260
|
|
|
257
261
|
Args:
|
|
258
|
-
target: The target instance ID
|
|
262
|
+
target: The target instance ID.
|
|
259
263
|
document_name: The SSM document name to use for the session.
|
|
260
264
|
parameters: The parameters to pass to the SSM document.
|
|
261
265
|
reason: The reason for starting the session.
|
|
262
|
-
log_file: Optional file to log output to.
|
|
263
266
|
|
|
264
267
|
Returns:
|
|
265
|
-
|
|
268
|
+
The command to start the session.
|
|
266
269
|
"""
|
|
267
270
|
is_installed, binary_path, version = self.verify_installation()
|
|
268
271
|
if not is_installed:
|
|
@@ -279,7 +282,7 @@ class SessionManager:
|
|
|
279
282
|
)
|
|
280
283
|
|
|
281
284
|
region = self.session.region_name
|
|
282
|
-
|
|
285
|
+
return [
|
|
283
286
|
str(binary_path),
|
|
284
287
|
json.dumps(response),
|
|
285
288
|
region,
|
|
@@ -289,20 +292,6 @@ class SessionManager:
|
|
|
289
292
|
f"https://ssm.{region}.amazonaws.com",
|
|
290
293
|
]
|
|
291
294
|
|
|
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
295
|
|
|
307
296
|
# ? Could be moved to utils, but didn't because it's too specific to this module
|
|
308
297
|
class _LinuxDistribution(NamedTuple):
|
|
@@ -0,0 +1,72 @@
|
|
|
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 .session_manager import SessionManager
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def port_forward(
|
|
19
|
+
*,
|
|
20
|
+
through: str,
|
|
21
|
+
local_port: int,
|
|
22
|
+
remote_host: str,
|
|
23
|
+
remote_port: int,
|
|
24
|
+
reason: str | None = None,
|
|
25
|
+
) -> Iterator[subprocess.Popen[str]]:
|
|
26
|
+
"""Context manager for port forwarding sessions.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
through: The instance ID to use as port-forwarding proxy.
|
|
30
|
+
local_port: The local port to listen to.
|
|
31
|
+
remote_host: The remote host to connect to.
|
|
32
|
+
remote_port: The remote port to connect to.
|
|
33
|
+
reason: The reason for starting the session.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The command to start the session.
|
|
37
|
+
"""
|
|
38
|
+
session_manager = SessionManager()
|
|
39
|
+
command = session_manager.build_command(
|
|
40
|
+
target=through,
|
|
41
|
+
document_name="AWS-StartPortForwardingSessionToRemoteHost",
|
|
42
|
+
parameters={
|
|
43
|
+
"localPortNumber": [str(local_port)],
|
|
44
|
+
"host": [remote_host],
|
|
45
|
+
"portNumber": [str(remote_port)],
|
|
46
|
+
},
|
|
47
|
+
reason=reason,
|
|
48
|
+
)
|
|
49
|
+
try:
|
|
50
|
+
proc = subprocess.Popen( # noqa: S603
|
|
51
|
+
command,
|
|
52
|
+
stdout=subprocess.PIPE,
|
|
53
|
+
stderr=subprocess.STDOUT,
|
|
54
|
+
text=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# * Must be unreachable
|
|
58
|
+
if proc.stdout is None:
|
|
59
|
+
msg = "Standard output is not available"
|
|
60
|
+
raise RuntimeError(msg)
|
|
61
|
+
|
|
62
|
+
# Wait for the session to start
|
|
63
|
+
# ? Not sure this is trustworthy health check
|
|
64
|
+
# TODO(lasuillard): Need timeout to avoid hanging forever
|
|
65
|
+
for line in proc.stdout:
|
|
66
|
+
if "Waiting for connections..." in line:
|
|
67
|
+
logger.info("Session started successfully.")
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
yield proc
|
|
71
|
+
finally:
|
|
72
|
+
proc.terminate()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ==UserScript==
|
|
2
2
|
// @name AWS ECS Exec
|
|
3
3
|
// @namespace mailto:lasuillard@gmail.com
|
|
4
|
-
// @version 2025.
|
|
4
|
+
// @version 2025.04.16
|
|
5
5
|
// @description Add link to AWS SSM Session Manager for ECS container
|
|
6
6
|
// @author lasuillard
|
|
7
7
|
// @source https://raw.githubusercontent.com/lasuillard/aws-annoying/refs/heads/main/console/ecs-exec/ecs-exec.user.js
|
|
@@ -106,11 +106,11 @@
|
|
|
106
106
|
|
|
107
107
|
// Get task info from the detail page
|
|
108
108
|
function getTaskInfoForDetailPage() {
|
|
109
|
-
const arnNeighbor = document.evaluate(
|
|
109
|
+
const arnNeighbor = document.evaluate(`//*[text()="ARN"]`, document).iterateNext();
|
|
110
110
|
if (!arnNeighbor) {
|
|
111
111
|
return null;
|
|
112
112
|
}
|
|
113
|
-
const arn = arnNeighbor.parentNode.children[1].textContent;
|
|
113
|
+
const arn = arnNeighbor.parentNode.parentNode.children[1].textContent;
|
|
114
114
|
const [, , , region, , taskPart] = arn.split(":");
|
|
115
115
|
const [, clusterName, taskId] = taskPart.split("/");
|
|
116
116
|
|
|
@@ -5,7 +5,6 @@ from typer.testing import CliRunner
|
|
|
5
5
|
|
|
6
6
|
from aws_annoying.cli.main import app
|
|
7
7
|
from aws_annoying.session_manager import SessionManager
|
|
8
|
-
from aws_annoying.utils.downloader import DummyDownloader
|
|
9
8
|
|
|
10
9
|
runner = CliRunner()
|
|
11
10
|
|
|
@@ -15,7 +14,7 @@ sentinel = object()
|
|
|
15
14
|
@pytest.mark.macos
|
|
16
15
|
def test_macos_session_manager_install() -> None:
|
|
17
16
|
# Arrange
|
|
18
|
-
session_manager = SessionManager(
|
|
17
|
+
session_manager = SessionManager()
|
|
19
18
|
assert session_manager.verify_installation() == (False, None, None)
|
|
20
19
|
|
|
21
20
|
# Act
|
|
@@ -36,7 +35,7 @@ def test_macos_session_manager_install() -> None:
|
|
|
36
35
|
@pytest.mark.windows
|
|
37
36
|
def test_windows_session_manager_install() -> None:
|
|
38
37
|
# Arrange
|
|
39
|
-
session_manager = SessionManager(
|
|
38
|
+
session_manager = SessionManager()
|
|
40
39
|
assert session_manager.verify_installation() == (False, None, None)
|
|
41
40
|
|
|
42
41
|
# Act
|
|
File without changes
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# TODO(lasuillard): Using this file until split CLI from library codebase
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
from rich.prompt import Confirm
|
|
8
|
-
|
|
9
|
-
from aws_annoying.session_manager import SessionManager as _SessionManager
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Custom session manager with console interactivity
|
|
13
|
-
class SessionManager(_SessionManager):
|
|
14
|
-
def before_install(self, command: list[str]) -> None:
|
|
15
|
-
if self._confirm:
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
confirm = Confirm.ask(f"⚠️ Will run the following command: [bold red]{' '.join(command)}[/bold red]. Proceed?")
|
|
19
|
-
if not confirm:
|
|
20
|
-
raise typer.Abort
|
|
21
|
-
|
|
22
|
-
def install(self, *args: Any, confirm: bool = False, **kwargs: Any) -> None:
|
|
23
|
-
self._confirm = confirm
|
|
24
|
-
return super().install(*args, **kwargs)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/aws_annoying-0.4.0/tests/test_mfa.py → /aws_annoying-0.5.0/tests/session_manager/test_shortcuts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/aws_annoying-0.4.0/tests/utils/test_downloader.py → /aws_annoying-0.5.0/tests/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|