aws-annoying 0.2.0__tar.gz → 0.3.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.
Files changed (84) hide show
  1. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.github/workflows/ci.yaml +33 -4
  2. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.vscode/settings.json +2 -0
  3. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/PKG-INFO +11 -5
  4. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/README.md +1 -1
  5. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/ecs_task_definition_lifecycle.py +27 -0
  6. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/main.py +1 -0
  7. aws_annoying-0.3.0/aws_annoying/session_manager/__init__.py +3 -0
  8. aws_annoying-0.3.0/aws_annoying/session_manager/_app.py +9 -0
  9. aws_annoying-0.3.0/aws_annoying/session_manager/_common.py +24 -0
  10. aws_annoying-0.3.0/aws_annoying/session_manager/errors.py +10 -0
  11. aws_annoying-0.3.0/aws_annoying/session_manager/install.py +39 -0
  12. aws_annoying-0.3.0/aws_annoying/session_manager/port_forward.py +126 -0
  13. aws_annoying-0.3.0/aws_annoying/session_manager/session_manager.py +318 -0
  14. aws_annoying-0.3.0/aws_annoying/session_manager/start.py +9 -0
  15. aws_annoying-0.3.0/aws_annoying/session_manager/stop.py +50 -0
  16. aws_annoying-0.3.0/aws_annoying/utils/downloader.py +58 -0
  17. aws_annoying-0.3.0/aws_annoying/utils/platform.py +27 -0
  18. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/pyproject.toml +24 -8
  19. aws_annoying-0.3.0/tests/session_manager/test_errors.py +0 -0
  20. aws_annoying-0.3.0/tests/session_manager/test_install.py +0 -0
  21. aws_annoying-0.3.0/tests/session_manager/test_port_forward.py +0 -0
  22. aws_annoying-0.3.0/tests/session_manager/test_session_manager.py +51 -0
  23. aws_annoying-0.3.0/tests/session_manager/test_start.py +0 -0
  24. aws_annoying-0.3.0/tests/session_manager/test_stop.py +0 -0
  25. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_ecs_task_definition_lifecycle/test_basic/stdout.txt +1 -0
  26. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_ecs_task_definition_lifecycle/test_dry_run/stdout.txt +1 -0
  27. aws_annoying-0.3.0/tests/test_app.py +0 -0
  28. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/test_load_variables.py +1 -1
  29. aws_annoying-0.3.0/tests/utils/__init__.py +0 -0
  30. aws_annoying-0.3.0/tests/utils/test_downloader.py +0 -0
  31. aws_annoying-0.3.0/tests/utils/test_platform.py +0 -0
  32. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/uv.lock +282 -184
  33. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.devcontainer/.env.example +0 -0
  34. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.devcontainer/Dockerfile +0 -0
  35. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.devcontainer/devcontainer.json +0 -0
  36. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.devcontainer/docker-compose.devcontainer.yaml +0 -0
  37. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.devcontainer/onCreateCommand.sh +0 -0
  38. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.devcontainer/postAttachCommand.sh +0 -0
  39. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.editorconfig +0 -0
  40. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.gitattributes +0 -0
  41. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.github/dependabot.yaml +0 -0
  42. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.github/workflows/release.yaml +0 -0
  43. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.gitignore +0 -0
  44. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.pre-commit-config.yaml +0 -0
  45. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.python-version +0 -0
  46. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.vscode/extensions.json +0 -0
  47. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/.vscode/launch.json +0 -0
  48. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/LICENSE +0 -0
  49. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/Makefile +0 -0
  50. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/__init__.py +0 -0
  51. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/app.py +0 -0
  52. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/load_variables.py +0 -0
  53. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/mfa/__init__.py +0 -0
  54. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/mfa/_app.py +0 -0
  55. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/mfa/configure.py +0 -0
  56. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/utils/__init__.py +0 -0
  57. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/aws_annoying/utils/debugger.py +0 -0
  58. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/console/ecs-exec/README.md +0 -0
  59. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/console/ecs-exec/ecs-console.png +0 -0
  60. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/console/ecs-exec/ecs-exec.user.js +0 -0
  61. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/console/ecs-exec/session-manager.png +0 -0
  62. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/__init__.py +0 -0
  63. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/_helpers/__init__.py +0 -0
  64. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/_helpers/command_builder.py +0 -0
  65. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/_helpers/scripts/printenv.py +0 -0
  66. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/_helpers/string_.py +0 -0
  67. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/conftest.py +0 -0
  68. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/mfa/__init__.py +0 -0
  69. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/mfa/snapshots/test_configure/test_basic/persist/aws_config.ini +0 -0
  70. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/mfa/snapshots/test_configure/test_basic/persist/stdout.txt +0 -0
  71. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/mfa/snapshots/test_configure/test_basic/skip_persist/stdout.txt +0 -0
  72. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/mfa/snapshots/test_configure/test_load_existing_config/aws_config.ini +0 -0
  73. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/mfa/snapshots/test_configure/test_load_existing_config/stdout.txt +0 -0
  74. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/mfa/test_configure.py +0 -0
  75. /aws_annoying-0.2.0/tests/test_app.py → /aws_annoying-0.3.0/tests/session_manager/__init__.py +0 -0
  76. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_basic/stdout.txt +0 -0
  77. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_dry_run/stdout.txt +0 -0
  78. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_env_prefix/stdout.txt +0 -0
  79. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_nothing/stdout.txt +0 -0
  80. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_overwrite_env/stdout.txt +0 -0
  81. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_replace_quiet/stdout.txt +0 -0
  82. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_resource_not_found/ssm/stdout.txt +0 -0
  83. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/snapshots/test_load_variables/test_unsupported_resource/stdout.txt +0 -0
  84. {aws_annoying-0.2.0 → aws_annoying-0.3.0}/tests/test_ecs_task_definition_lifecycle.py +0 -0
@@ -36,16 +36,24 @@ jobs:
36
36
  if: always()
37
37
 
38
38
  test:
39
- name: Test (Python ${{ matrix.python-version }})
40
- runs-on: ubuntu-latest
39
+ name: Test (${{ matrix.os }}, Python ${{ matrix.python-version }})
40
+ runs-on: ${{ matrix.os }}
41
41
  permissions:
42
42
  contents: read
43
43
  id-token: write
44
44
  strategy:
45
45
  fail-fast: false
46
46
  matrix:
47
+ os: [ubuntu-latest]
47
48
  python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
48
49
 
50
+ # TODO(lasuillard): Simplified matrix for macOS and Windows for testing
51
+ include:
52
+ - os: macos-latest
53
+ python-version: "3.9"
54
+ - os: windows-latest
55
+ python-version: "3.9"
56
+
49
57
  steps:
50
58
  - name: Checkout
51
59
  uses: actions/checkout@v4
@@ -62,8 +70,27 @@ jobs:
62
70
  - name: Install deps
63
71
  run: uv sync --all-extras
64
72
 
65
- - name: Run tests
66
- run: uv run pytest
73
+ - name: Run tests (Ubuntu)
74
+ if: ${{ matrix.os == 'ubuntu-latest' }}
75
+ run: |
76
+ sudo apt-get remove --yes session-manager-plugin
77
+ uv run pytest
78
+
79
+ - name: Run tests (macOS)
80
+ if: ${{ matrix.os == 'macos-latest' }}
81
+ run: |
82
+ brew uninstall session-manager-plugin
83
+ uv run pytest -m macos
84
+
85
+ - name: Run tests (Windows)
86
+ if: ${{ matrix.os == 'windows-latest' }}
87
+ run: |
88
+ Invoke-WebRequest `
89
+ -Uri "https://raw.githubusercontent.com/aws/session-manager-plugin/refs/heads/mainline/Tools/src/update/windows/uninstall.bat" `
90
+ -OutFile "uninstall.bat"
91
+
92
+ Start-Process "cmd.exe" -ArgumentList "/c uninstall.bat" -Wait
93
+ uv run pytest -m windows
67
94
 
68
95
  - name: Upload test results to Codecov
69
96
  uses: codecov/codecov-action@v5
@@ -73,6 +100,7 @@ jobs:
73
100
  report_type: test_results
74
101
  files: junit.xml
75
102
  flags: >-
103
+ ${{ matrix.os }}
76
104
  python-${{ matrix.python-version }}
77
105
 
78
106
  - name: Upload coverage report
@@ -82,4 +110,5 @@ jobs:
82
110
  fail_ci_if_error: false
83
111
  files: coverage.xml
84
112
  flags: >-
113
+ ${{ matrix.os }}
85
114
  python-${{ matrix.python-version }}
@@ -8,7 +8,9 @@
8
8
  },
9
9
  "autoDocstring.docstringFormat": "google-notypes",
10
10
  "cSpell.words": [
11
+ "Deregistering",
11
12
  "localstack",
13
+ "sessionmanagerplugin",
12
14
  "Tampermonkey"
13
15
  ],
14
16
  "editor.formatOnSave": true,
@@ -1,20 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-annoying
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Utils to handle some annoying AWS tasks.
5
+ Project-URL: Homepage, https://github.com/lasuillard/aws-annoying
6
+ Project-URL: Repository, https://github.com/lasuillard/aws-annoying.git
7
+ Project-URL: Issues, https://github.com/lasuillard/aws-annoying/issues
5
8
  Author-email: Yuchan Lee <lasuillard@gmail.com>
6
9
  License-Expression: MIT
7
10
  License-File: LICENSE
8
11
  Requires-Python: <4.0,>=3.9
9
12
  Requires-Dist: boto3>=1.37.1
10
13
  Requires-Dist: pydantic>=2.10.6
14
+ Requires-Dist: requests>=2.32.3
15
+ Requires-Dist: tqdm>=4.67.1
11
16
  Requires-Dist: typer>=0.15.1
12
17
  Provides-Extra: dev
13
- Requires-Dist: boto3-stubs[ecs,secretsmanager,ssm,sts]>=1.37.1; extra == 'dev'
18
+ Requires-Dist: boto3-stubs[ec2,ecs,secretsmanager,ssm,sts]>=1.37.1; extra == 'dev'
14
19
  Requires-Dist: mypy~=1.15.0; extra == 'dev'
15
- Requires-Dist: ruff~=0.9.9; extra == 'dev'
20
+ Requires-Dist: ruff<0.12.0,>=0.9.9; extra == 'dev'
21
+ Requires-Dist: types-requests>=2.31.0.6; extra == 'dev'
16
22
  Provides-Extra: test
17
- Requires-Dist: coverage~=7.6.0; extra == 'test'
23
+ Requires-Dist: coverage<7.9,>=7.6; extra == 'test'
18
24
  Requires-Dist: moto[ecs,secretsmanager,server,ssm]~=5.1.1; extra == 'test'
19
25
  Requires-Dist: pytest-cov~=6.0.0; extra == 'test'
20
26
  Requires-Dist: pytest-env~=1.1.1; extra == 'test'
@@ -30,6 +36,6 @@ Description-Content-Type: text/markdown
30
36
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
37
  [![CI](https://github.com/lasuillard/aws-annoying/actions/workflows/ci.yaml/badge.svg)](https://github.com/lasuillard/aws-annoying/actions/workflows/ci.yaml)
32
38
  [![codecov](https://codecov.io/gh/lasuillard/aws-annoying/graph/badge.svg?token=gbcHMVVz2k)](https://codecov.io/gh/lasuillard/aws-annoying)
33
- ![GitHub Release](https://img.shields.io/github/v/release/lasuillard/aws-annoying)
39
+ ![PyPI - Version](https://img.shields.io/pypi/v/aws-annoying)
34
40
 
35
41
  Utils to handle some annoying AWS tasks.
@@ -3,6 +3,6 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![CI](https://github.com/lasuillard/aws-annoying/actions/workflows/ci.yaml/badge.svg)](https://github.com/lasuillard/aws-annoying/actions/workflows/ci.yaml)
5
5
  [![codecov](https://codecov.io/gh/lasuillard/aws-annoying/graph/badge.svg?token=gbcHMVVz2k)](https://codecov.io/gh/lasuillard/aws-annoying)
6
- ![GitHub Release](https://img.shields.io/github/v/release/lasuillard/aws-annoying)
6
+ ![PyPI - Version](https://img.shields.io/pypi/v/aws-annoying)
7
7
 
8
8
  Utils to handle some annoying AWS tasks.
@@ -1,11 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  import boto3
4
6
  import typer
5
7
  from rich import print # noqa: A004
6
8
 
7
9
  from .app import app
8
10
 
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Iterator
13
+
14
+ _DELETE_CHUNK_SIZE = 10
15
+
9
16
 
10
17
  @app.command()
11
18
  def ecs_task_definition_lifecycle(
@@ -22,6 +29,10 @@ def ecs_task_definition_lifecycle(
22
29
  min=1,
23
30
  max=100,
24
31
  ),
32
+ delete: bool = typer.Option(
33
+ False, # noqa: FBT003
34
+ help="Delete the task definition after deregistering it.",
35
+ ),
25
36
  dry_run: bool = typer.Option(
26
37
  False, # noqa: FBT003
27
38
  help="Do not perform any changes, only show what would be done.",
@@ -48,6 +59,7 @@ def ecs_task_definition_lifecycle(
48
59
 
49
60
  # Keep the latest N task definitions
50
61
  expired_taskdef_arns = task_definition_arns[:-keep_latest]
62
+ print(f"⚠️ Deregistering {len(expired_taskdef_arns)} task definitions...")
51
63
  for arn in expired_taskdef_arns:
52
64
  if not dry_run:
53
65
  ecs.deregister_task_definition(taskDefinition=arn)
@@ -55,3 +67,18 @@ def ecs_task_definition_lifecycle(
55
67
  # ARN like: "arn:aws:ecs:<region>:<account-id>:task-definition/<family>:<revision>"
56
68
  _, family_revision = arn.split(":task-definition/")
57
69
  print(f"✅ Deregistered task definition [yellow]{family_revision!r}[/yellow]")
70
+
71
+ if delete and expired_taskdef_arns:
72
+ # Delete the expired task definitions in chunks due to API limitation
73
+ print(f"⚠️ Deleting {len(expired_taskdef_arns)} task definitions in chunks of size {_DELETE_CHUNK_SIZE}...")
74
+ for idx, chunk in enumerate(_chunker(expired_taskdef_arns, _DELETE_CHUNK_SIZE)):
75
+ if not dry_run:
76
+ ecs.delete_task_definitions(taskDefinitions=chunk)
77
+
78
+ print(f"✅ Deleted {len(chunk)} task definitions in {idx}-th batch.")
79
+
80
+
81
+ def _chunker(sequence: list, size: int) -> Iterator[list]:
82
+ """Yield successive chunks of a given size from the sequence."""
83
+ for i in range(0, len(sequence), size):
84
+ yield sequence[i : i + size]
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  import aws_annoying.ecs_task_definition_lifecycle
5
5
  import aws_annoying.load_variables
6
6
  import aws_annoying.mfa
7
+ import aws_annoying.session_manager
7
8
  from aws_annoying.utils.debugger import input_as_args
8
9
 
9
10
  # App with all commands registered
@@ -0,0 +1,3 @@
1
+ from . import install, port_forward, start, stop
2
+
3
+ __all__ = ("install", "port_forward", "start", "stop")
@@ -0,0 +1,9 @@
1
+ import typer
2
+
3
+ from aws_annoying.app import app
4
+
5
+ session_manager_app = typer.Typer(
6
+ no_args_is_help=True,
7
+ help="AWS Session Manager CLI utilities.",
8
+ )
9
+ app.add_typer(session_manager_app, name="session-manager")
@@ -0,0 +1,24 @@
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 .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)
@@ -0,0 +1,10 @@
1
+ class SessionManagerError(Exception):
2
+ """Base exception for all errors related to Session Manager."""
3
+
4
+
5
+ class UnsupportedPlatformError(SessionManagerError):
6
+ """Exception raised when the platform is not supported."""
7
+
8
+
9
+ class PluginNotInstalledError(SessionManagerError):
10
+ """Trying to use the Session Manager plugin before it is installed."""
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+ from rich import print # noqa: A004
5
+
6
+ from aws_annoying.utils.downloader import TQDMDownloader
7
+
8
+ from ._app import session_manager_app
9
+ from ._common import SessionManager
10
+
11
+
12
+ # https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
13
+ @session_manager_app.command()
14
+ def install(
15
+ yes: bool = typer.Option( # noqa: FBT001
16
+ False, # noqa: FBT003
17
+ help="Do not ask confirmation for installation.",
18
+ ),
19
+ ) -> None:
20
+ """Install AWS Session Manager plugin."""
21
+ session_manager = SessionManager(downloader=TQDMDownloader())
22
+
23
+ # Check session-manager-plugin already installed
24
+ is_installed, binary_path, version = session_manager.verify_installation()
25
+ if is_installed:
26
+ print(f"✅ Session Manager plugin is already installed at {binary_path} (version: {version})")
27
+ return
28
+
29
+ # Install session-manager-plugin
30
+ print("⬇️ Installing AWS Session Manager plugin. You could be prompted for admin privileges request.")
31
+ session_manager.install(confirm=yes)
32
+
33
+ # Verify installation
34
+ is_installed, binary_path, version = session_manager.verify_installation()
35
+ if not is_installed:
36
+ print("❌ Installation failed. Session Manager plugin not found.")
37
+ raise typer.Exit(1)
38
+
39
+ print(f"✅ Session Manager plugin successfully installed at {binary_path} (version: {version})")
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import signal
6
+ from pathlib import Path # noqa: TC003
7
+
8
+ import boto3
9
+ import typer
10
+ from rich import print # noqa: A004
11
+
12
+ from aws_annoying.utils.downloader import TQDMDownloader
13
+
14
+ from ._app import session_manager_app
15
+ from ._common import SessionManager
16
+
17
+
18
+ # https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
19
+ @session_manager_app.command()
20
+ def port_forward( # noqa: PLR0913
21
+ # TODO(lasuillard): Add `--local-host` option, redirect the traffic to non-localhost bind (unsupported by AWS)
22
+ local_port: int = typer.Option(
23
+ ...,
24
+ show_default=False,
25
+ help="The local port to use for port forwarding.",
26
+ ),
27
+ through: str = typer.Option(
28
+ ...,
29
+ show_default=False,
30
+ help="The name or ID of the EC2 instance to use as a proxy for port forwarding.",
31
+ ),
32
+ remote_host: str = typer.Option(
33
+ ...,
34
+ show_default=False,
35
+ help="The remote host to connect to.",
36
+ ),
37
+ remote_port: int = typer.Option(
38
+ ...,
39
+ show_default=False,
40
+ help="The remote port to connect to.",
41
+ ),
42
+ reason: str = typer.Option(
43
+ "",
44
+ help="The reason for starting the port forwarding session.",
45
+ ),
46
+ pid_file: Path = typer.Option( # noqa: B008
47
+ "./session-manager-plugin.pid",
48
+ help="The path to the PID file to store the process ID of the session manager plugin.",
49
+ ),
50
+ terminate_running_process: bool = typer.Option( # noqa: FBT001
51
+ False, # noqa: FBT003
52
+ help="Terminate the process in the PID file if it already exists.",
53
+ ),
54
+ log_file: Path = typer.Option( # noqa: B008
55
+ "./session-manager-plugin.log",
56
+ help="The path to the log file to store the output of the session manager plugin.",
57
+ ),
58
+ ) -> None:
59
+ """Start a port forwarding session using AWS Session Manager."""
60
+ session_manager = SessionManager(downloader=TQDMDownloader())
61
+
62
+ # Check if the PID file already exists
63
+ if pid_file.exists():
64
+ if not terminate_running_process:
65
+ print("🚫 PID file already exists.")
66
+ raise typer.Exit(1)
67
+
68
+ pid_content = pid_file.read_text()
69
+ try:
70
+ existing_pid = int(pid_content)
71
+ except ValueError:
72
+ print(f"🚫 PID file content is invalid; expected integer, but got: {type(pid_content)}")
73
+ raise typer.Exit(1) from None
74
+
75
+ try:
76
+ print(f"⚠️ Terminating running process with PID {existing_pid}.")
77
+ os.kill(existing_pid, signal.SIGTERM)
78
+ pid_file.write_text("") # Clear the PID file
79
+ except ProcessLookupError:
80
+ print(f"⚠️ Tried to terminate process with PID {existing_pid} but does not exist.")
81
+
82
+ # Resolve the instance name or ID
83
+ if re.match(r"m?i-.+", through):
84
+ target = through
85
+ else:
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
+
94
+ target = instance_id
95
+
96
+ # Initiate the session
97
+ proc = session_manager.start(
98
+ target=target,
99
+ document_name="AWS-StartPortForwardingSessionToRemoteHost",
100
+ parameters={
101
+ "host": [remote_host],
102
+ "portNumber": [str(remote_port)],
103
+ "localPortNumber": [str(local_port)],
104
+ },
105
+ reason=reason,
106
+ )
107
+ print(f"✅ Session Manager Plugin started with PID {proc.pid}. Outputs will be logged to {log_file.absolute()}.")
108
+
109
+ # Write the PID to the file
110
+ pid_file.write_text(str(proc.pid))
111
+ 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"])