aws-annoying 0.7.0__tar.gz → 0.8.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 (128) hide show
  1. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.devcontainer/devcontainer.json +1 -3
  2. aws_annoying-0.8.0/.devcontainer/onCreateCommand.sh +3 -0
  3. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/PKG-INFO +1 -1
  4. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/app.py +30 -1
  5. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/ecs/task_definition_lifecycle.py +0 -3
  6. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/logging_handler.py +3 -3
  7. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/mfa/configure.py +13 -8
  8. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/session_manager/install.py +6 -2
  9. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/session_manager/port_forward.py +21 -11
  10. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/session_manager/start.py +5 -1
  11. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/session_manager/stop.py +9 -3
  12. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/pyproject.toml +1 -1
  13. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/_helpers/string_.py +8 -1
  14. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/ecs/snapshots/test_task_definition_lifecycle/test_dry_run/stdout.txt +1 -1
  15. aws_annoying-0.8.0/tests/cli/mfa/snapshots/test_configure/test_basic/persist/stdout.txt +3 -0
  16. aws_annoying-0.8.0/tests/cli/mfa/snapshots/test_configure/test_basic/skip_persist/stdout.txt +3 -0
  17. aws_annoying-0.8.0/tests/cli/mfa/snapshots/test_configure/test_dry_run/stdout.txt +4 -0
  18. aws_annoying-0.8.0/tests/cli/mfa/snapshots/test_configure/test_load_existing_config/stdout.txt +4 -0
  19. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/mfa/test_configure.py +50 -4
  20. aws_annoying-0.7.0/tests/session_manager/test_session_manager.py → aws_annoying-0.8.0/tests/cli/session_manager/test_install.py +3 -3
  21. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/snapshots/test_load_variables/test_basic/stdout.txt +4 -4
  22. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/snapshots/test_load_variables/test_env_prefix/stdout.txt +6 -6
  23. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/snapshots/test_load_variables/test_overwrite_env/stdout.txt +6 -6
  24. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/snapshots/test_load_variables/test_resource_not_found/ssm/stdout.txt +3 -3
  25. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/snapshots/test_load_variables/test_unsupported_resource/stdout.txt +3 -3
  26. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/uv.lock +1 -1
  27. aws_annoying-0.7.0/.devcontainer/onCreateCommand.sh +0 -1
  28. aws_annoying-0.7.0/tests/cli/mfa/snapshots/test_configure/test_basic/persist/stdout.txt +0 -3
  29. aws_annoying-0.7.0/tests/cli/mfa/snapshots/test_configure/test_basic/skip_persist/stdout.txt +0 -3
  30. aws_annoying-0.7.0/tests/cli/mfa/snapshots/test_configure/test_load_existing_config/stdout.txt +0 -4
  31. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.devcontainer/.env.example +0 -0
  32. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.devcontainer/Dockerfile +0 -0
  33. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.devcontainer/docker-compose.devcontainer.yaml +0 -0
  34. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.devcontainer/postAttachCommand.sh +0 -0
  35. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.editorconfig +0 -0
  36. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.gitattributes +0 -0
  37. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.github/dependabot.yaml +0 -0
  38. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.github/workflows/ci.yaml +0 -0
  39. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.github/workflows/release.yaml +0 -0
  40. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.gitignore +0 -0
  41. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.pre-commit-config.yaml +0 -0
  42. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.python-version +0 -0
  43. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.vscode/extensions.json +0 -0
  44. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.vscode/launch.json +0 -0
  45. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/.vscode/settings.json +0 -0
  46. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/LICENSE +0 -0
  47. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/Makefile +0 -0
  48. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/README.md +0 -0
  49. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/__init__.py +0 -0
  50. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/__init__.py +0 -0
  51. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/ecs/__init__.py +0 -0
  52. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/ecs/_app.py +0 -0
  53. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/ecs/wait_for_deployment.py +0 -0
  54. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/load_variables.py +0 -0
  55. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/main.py +0 -0
  56. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/mfa/__init__.py +0 -0
  57. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/mfa/_app.py +0 -0
  58. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/session_manager/__init__.py +0 -0
  59. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/session_manager/_app.py +0 -0
  60. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/cli/session_manager/_common.py +0 -0
  61. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/ecs/__init__.py +0 -0
  62. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/ecs/check.py +0 -0
  63. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/ecs/common.py +0 -0
  64. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/ecs/errors.py +0 -0
  65. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/ecs/wait_for.py +0 -0
  66. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/mfa_config.py +0 -0
  67. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/session_manager/__init__.py +0 -0
  68. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/session_manager/errors.py +0 -0
  69. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/session_manager/session_manager.py +0 -0
  70. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/session_manager/shortcuts.py +0 -0
  71. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/utils/__init__.py +0 -0
  72. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/utils/debugger.py +0 -0
  73. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/utils/downloader.py +0 -0
  74. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/utils/ec2.py +0 -0
  75. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/utils/platform.py +0 -0
  76. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/utils/timeout.py +0 -0
  77. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/aws_annoying/variable_loader.py +0 -0
  78. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/console/ecs-exec/README.md +0 -0
  79. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/console/ecs-exec/ecs-console.png +0 -0
  80. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/console/ecs-exec/ecs-exec.user.js +0 -0
  81. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/console/ecs-exec/session-manager.png +0 -0
  82. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/examples/dbeaver/README.md +0 -0
  83. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/examples/dbeaver/after-disconnect.png +0 -0
  84. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/examples/dbeaver/before-connect.png +0 -0
  85. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/examples/dbeaver/new-connection.png +0 -0
  86. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/examples/dbeaver/test-connection.png +0 -0
  87. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/__init__.py +0 -0
  88. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/_helpers.py +0 -0
  89. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/__init__.py +0 -0
  90. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/_helpers/__init__.py +0 -0
  91. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/_helpers/boto3.py +0 -0
  92. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/_helpers/command_builder.py +0 -0
  93. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/_helpers/invoke.py +0 -0
  94. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/_helpers/scripts/printenv.py +0 -0
  95. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/ecs/__init__.py +0 -0
  96. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/ecs/snapshots/test_task_definition_lifecycle/test_basic/stdout.txt +0 -0
  97. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/ecs/test_task_definition_lifecycle.py +0 -0
  98. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/ecs/test_wait_for_deployment.py +0 -0
  99. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/mfa/__init__.py +0 -0
  100. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/mfa/snapshots/test_configure/test_basic/persist/aws_config.ini +0 -0
  101. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/mfa/snapshots/test_configure/test_load_existing_config/aws_config.ini +0 -0
  102. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/session_manager/__init__.py +0 -0
  103. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/session_manager/test_port_forward.py +0 -0
  104. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/session_manager/test_start.py +0 -0
  105. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/session_manager/test_stop.py +0 -0
  106. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/snapshots/test_load_variables/test_nothing/stdout.txt +0 -0
  107. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/snapshots/test_load_variables/test_replace_quiet/stdout.txt +0 -0
  108. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/test_app.py +0 -0
  109. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/test_load_variables.py +0 -0
  110. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/test_logging_handler.py +0 -0
  111. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/cli/test_main.py +0 -0
  112. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/conftest.py +0 -0
  113. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/ecs/__init__.py +0 -0
  114. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/ecs/test_check.py +0 -0
  115. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/ecs/test_common.py +0 -0
  116. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/ecs/test_errors.py +0 -0
  117. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/ecs/test_wait_for.py +0 -0
  118. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/session_manager/__init__.py +0 -0
  119. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/session_manager/test_errors.py +0 -0
  120. /aws_annoying-0.7.0/tests/cli/session_manager/test_install.py → /aws_annoying-0.8.0/tests/session_manager/test_session_manager.py +0 -0
  121. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/session_manager/test_shortcuts.py +0 -0
  122. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/test_mfa_config.py +0 -0
  123. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/test_variable_loader.py +0 -0
  124. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/utils/__init__.py +0 -0
  125. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/utils/test_downloader.py +0 -0
  126. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/utils/test_ec2.py +0 -0
  127. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/utils/test_platform.py +0 -0
  128. {aws_annoying-0.7.0 → aws_annoying-0.8.0}/tests/utils/test_timeout.py +0 -0
@@ -7,10 +7,8 @@
7
7
  "overrideCommand": true,
8
8
  "workspaceFolder": "/workspaces/aws-annoying",
9
9
  "features": {
10
- "ghcr.io/devcontainers-contrib/features/pre-commit:2": {},
11
10
  "ghcr.io/devcontainers/features/docker-in-docker:2": {},
12
- "ghcr.io/devcontainers/features/aws-cli:1": {},
13
- "ghcr.io/lasuillard/devcontainer-features/aws-ssm-session-manager-plugin:0.1": {}
11
+ "ghcr.io/devcontainers/features/aws-cli:1": {}
14
12
  },
15
13
  "onCreateCommand": "./.devcontainer/onCreateCommand.sh",
16
14
  "postAttachCommand": "./.devcontainer/postAttachCommand.sh",
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ pipx install pre-commit
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-annoying
3
- Version: 0.7.0
3
+ Version: 0.8.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
@@ -8,6 +8,10 @@ from typing import Optional
8
8
  import typer
9
9
  from rich import print # noqa: A004
10
10
  from rich.console import Console
11
+ from rich.highlighter import ReprHighlighter
12
+ from rich.theme import Theme
13
+
14
+ logger = logging.getLogger(__name__)
11
15
 
12
16
  app = typer.Typer(
13
17
  pretty_exceptions_short=True,
@@ -51,7 +55,7 @@ def main( # noqa: D103
51
55
  ),
52
56
  ) -> None:
53
57
  log_level = logging.DEBUG if verbose else logging.INFO
54
- console = Console(soft_wrap=True, emoji=False)
58
+ console = _get_console()
55
59
  logging_config: logging.config._DictConfigArgs = {
56
60
  "version": 1,
57
61
  "disable_existing_loggers": False,
@@ -89,3 +93,28 @@ def main( # noqa: D103
89
93
 
90
94
  # Global flags
91
95
  ctx.meta["dry_run"] = dry_run
96
+ if dry_run:
97
+ logger.warning("Dry run mode enabled. Some operation may behave differently to avoid making changes.")
98
+
99
+
100
+ def _get_console() -> Console:
101
+ theme = Theme(
102
+ {
103
+ "repr.arn": "bold orange3",
104
+ "repr.constant": "bold blue",
105
+ },
106
+ )
107
+ return Console(soft_wrap=True, emoji=False, highlighter=CustomHighlighter(), theme=theme)
108
+
109
+
110
+ class CustomHighlighter(ReprHighlighter):
111
+ """Custom highlighter to handle additional patterns."""
112
+
113
+ highlights = [ # noqa: RUF012
114
+ *ReprHighlighter.highlights,
115
+ # AWS Resource Name; https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
116
+ # NOTE: Quite simplified regex, may not cover all cases.
117
+ r"(?P<arn>\barn:[0-9a-zA-Z/+=,\.@_\-:]+\b)",
118
+ # Constants
119
+ r"(?P<constant>\b[A-Z_]+\b)",
120
+ ]
@@ -40,9 +40,6 @@ def task_definition_lifecycle(
40
40
  ) -> None:
41
41
  """Execute ECS task definition lifecycle."""
42
42
  dry_run = ctx.meta["dry_run"]
43
- if dry_run:
44
- logger.info("Dry run mode enabled. Will not perform any actual changes.")
45
-
46
43
  ecs = boto3.client("ecs")
47
44
 
48
45
  # Get all task definitions for the family
@@ -15,10 +15,10 @@ class RichLogHandler(logging.Handler):
15
15
 
16
16
  _level_emojis: Final[dict[str, str]] = {
17
17
  "DEBUG": "🔍",
18
- "INFO": "ℹ️", # noqa: RUF001
18
+ "INFO": "🔔",
19
19
  "WARNING": "⚠️",
20
- "ERROR": "",
21
- "CRITICAL": "🚨",
20
+ "ERROR": "🚨",
21
+ "CRITICAL": "🔥",
22
22
  }
23
23
 
24
24
  def __init__(self, console: Console, *args: Any, **kwargs: Any) -> None:
@@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  @mfa_app.command()
19
19
  def configure( # noqa: PLR0913
20
+ ctx: typer.Context,
20
21
  *,
21
22
  mfa_profile: Optional[str] = typer.Option(
22
23
  None,
@@ -54,6 +55,8 @@ def configure( # noqa: PLR0913
54
55
  ),
55
56
  ) -> None:
56
57
  """Configure AWS profile for MFA."""
58
+ dry_run = ctx.meta["dry_run"]
59
+
57
60
  # Expand user home directory
58
61
  aws_credentials = aws_credentials.expanduser()
59
62
  aws_config = aws_config.expanduser()
@@ -102,13 +105,14 @@ def configure( # noqa: PLR0913
102
105
  mfa_profile,
103
106
  aws_credentials,
104
107
  )
105
- update_credentials(
106
- aws_credentials,
107
- mfa_profile, # type: ignore[arg-type]
108
- access_key=credentials["AccessKeyId"],
109
- secret_key=credentials["SecretAccessKey"],
110
- session_token=credentials["SessionToken"],
111
- )
108
+ if not dry_run:
109
+ update_credentials(
110
+ aws_credentials,
111
+ mfa_profile, # type: ignore[arg-type]
112
+ access_key=credentials["AccessKeyId"],
113
+ secret_key=credentials["SecretAccessKey"],
114
+ session_token=credentials["SessionToken"],
115
+ )
112
116
 
113
117
  # Persist MFA configuration
114
118
  if persist:
@@ -120,6 +124,7 @@ def configure( # noqa: PLR0913
120
124
  mfa_config.mfa_profile = mfa_profile
121
125
  mfa_config.mfa_source_profile = mfa_source_profile
122
126
  mfa_config.mfa_serial_number = mfa_serial_number
123
- mfa_config.save_ini_file(aws_config, aws_config_section)
127
+ if not dry_run:
128
+ mfa_config.save_ini_file(aws_config, aws_config_section)
124
129
  else:
125
130
  logger.warning("MFA configuration not persisted.")
@@ -15,12 +15,15 @@ logger = logging.getLogger(__name__)
15
15
  # https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
16
16
  @session_manager_app.command()
17
17
  def install(
18
- yes: bool = typer.Option( # noqa: FBT001
18
+ ctx: typer.Context,
19
+ *,
20
+ yes: bool = typer.Option(
19
21
  False, # noqa: FBT003
20
22
  help="Do not ask confirmation for installation.",
21
23
  ),
22
24
  ) -> None:
23
25
  """Install AWS Session Manager plugin."""
26
+ dry_run = ctx.meta["dry_run"]
24
27
  session_manager = SessionManager()
25
28
 
26
29
  # Check session-manager-plugin already installed
@@ -31,7 +34,8 @@ def install(
31
34
 
32
35
  # Install session-manager-plugin
33
36
  logger.warning("Installing AWS Session Manager plugin. You could be prompted for admin privileges request.")
34
- session_manager.install(confirm=yes, downloader=TQDMDownloader())
37
+ if not dry_run:
38
+ session_manager.install(confirm=yes, downloader=TQDMDownloader())
35
39
 
36
40
  # Verify installation
37
41
  is_installed, binary_path, version = session_manager.verify_installation()
@@ -19,6 +19,8 @@ logger = logging.getLogger(__name__)
19
19
  # https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
20
20
  @session_manager_app.command()
21
21
  def port_forward( # noqa: PLR0913
22
+ ctx: typer.Context,
23
+ *,
22
24
  # TODO(lasuillard): Add `--local-host` option, redirect the traffic to non-localhost bind (unsupported by AWS)
23
25
  local_port: int = typer.Option(
24
26
  ...,
@@ -48,7 +50,7 @@ def port_forward( # noqa: PLR0913
48
50
  "./session-manager-plugin.pid",
49
51
  help="The path to the PID file to store the process ID of the session manager plugin.",
50
52
  ),
51
- terminate_running_process: bool = typer.Option( # noqa: FBT001
53
+ terminate_running_process: bool = typer.Option(
52
54
  False, # noqa: FBT003
53
55
  help="Terminate the process in the PID file if it already exists.",
54
56
  ),
@@ -58,6 +60,7 @@ def port_forward( # noqa: PLR0913
58
60
  ),
59
61
  ) -> None:
60
62
  """Start a port forwarding session using AWS Session Manager."""
63
+ dry_run = ctx.meta["dry_run"]
61
64
  session_manager = SessionManager()
62
65
 
63
66
  # Check if the PID file already exists
@@ -86,7 +89,7 @@ def port_forward( # noqa: PLR0913
86
89
  logger.info("Instance ID resolved: [bold]%s[/bold]", instance_id)
87
90
  target = instance_id
88
91
  else:
89
- logger.info("Instance with name '%s' not found.", through)
92
+ logger.error("Instance with name '%s' not found.", through)
90
93
  raise typer.Exit(1)
91
94
 
92
95
  # Initiate the session
@@ -111,19 +114,26 @@ def port_forward( # noqa: PLR0913
111
114
  through,
112
115
  reason,
113
116
  )
114
- proc = subprocess.Popen( # noqa: S603
115
- command,
116
- stdout=stdout,
117
- stderr=subprocess.STDOUT,
118
- text=True,
119
- close_fds=False, # FD inherited from parent process
120
- )
117
+ if not dry_run:
118
+ proc = subprocess.Popen( # noqa: S603
119
+ command,
120
+ stdout=stdout,
121
+ stderr=subprocess.STDOUT,
122
+ text=True,
123
+ close_fds=False, # FD inherited from parent process
124
+ )
125
+ pid = proc.pid
126
+ else:
127
+ pid = -1
128
+
121
129
  logger.info(
122
130
  "Session Manager Plugin started with PID %d. Outputs will be logged to %s.",
123
- proc.pid,
131
+ pid,
124
132
  log_file.absolute(),
125
133
  )
126
134
 
127
135
  # Write the PID to the file
128
- pid_file.write_text(str(proc.pid))
136
+ if not dry_run:
137
+ pid_file.write_text(str(pid))
138
+
129
139
  logger.info("PID file written to %s.", pid_file.absolute())
@@ -18,6 +18,8 @@ logger = logging.getLogger(__name__)
18
18
 
19
19
  @session_manager_app.command()
20
20
  def start(
21
+ ctx: typer.Context,
22
+ *,
21
23
  target: str = typer.Option(
22
24
  ...,
23
25
  show_default=False,
@@ -29,6 +31,7 @@ def start(
29
31
  ),
30
32
  ) -> None:
31
33
  """Start new session."""
34
+ dry_run = ctx.meta["dry_run"]
32
35
  session_manager = SessionManager()
33
36
 
34
37
  # Resolve the instance name or ID
@@ -52,4 +55,5 @@ def start(
52
55
  parameters={},
53
56
  reason=reason,
54
57
  )
55
- os.execvp(command[0], command) # noqa: S606
58
+ if not dry_run:
59
+ os.execvp(command[0], command) # noqa: S606
@@ -14,16 +14,20 @@ logger = logging.getLogger(__name__)
14
14
 
15
15
  @session_manager_app.command()
16
16
  def stop(
17
+ ctx: typer.Context,
18
+ *,
17
19
  pid_file: Path = typer.Option( # noqa: B008
18
20
  "./session-manager-plugin.pid",
19
21
  help="The path to the PID file to store the process ID of the session manager plugin.",
20
22
  ),
21
- remove: bool = typer.Option( # noqa: FBT001
23
+ remove: bool = typer.Option(
22
24
  True, # noqa: FBT003
23
25
  help="Remove the PID file after stopping the session.",
24
26
  ),
25
27
  ) -> None:
26
28
  """Stop running session for PID file."""
29
+ dry_run = ctx.meta["dry_run"]
30
+
27
31
  # Check if PID file exists
28
32
  if not pid_file.is_file():
29
33
  logger.error("PID file not found: %s", pid_file)
@@ -40,13 +44,15 @@ def stop(
40
44
  # Send SIGTERM to the process
41
45
  try:
42
46
  logger.warning("Terminating running process with PID %d.", pid)
43
- os.kill(pid, signal.SIGTERM)
47
+ if not dry_run:
48
+ os.kill(pid, signal.SIGTERM)
44
49
  except ProcessLookupError:
45
50
  logger.warning("Tried to terminate process with PID %d but does not exist.", pid)
46
51
 
47
52
  # Remove the PID file
48
53
  if remove:
49
54
  logger.info("Removed the PID file %s.", pid_file)
50
- pid_file.unlink()
55
+ if not dry_run:
56
+ pid_file.unlink()
51
57
 
52
58
  logger.info("Terminated the session successfully.")
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "aws-annoying"
3
3
  description = "Utils to handle some annoying AWS tasks."
4
- version = "0.7.0"
4
+ version = "0.8.0"
5
5
  authors = [{ name = "Yuchan Lee", email = "lasuillard@gmail.com" }]
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
 
4
- def normalize_console_output(output: str) -> str:
6
+ def normalize_console_output(output: str, *, replace: dict[str, str] | None = None) -> str:
5
7
  """Normalize the console output for easier comparison."""
6
8
  # Remove leading and trailing spaces
7
9
  output = output.strip()
@@ -9,6 +11,11 @@ def normalize_console_output(output: str) -> str:
9
11
  # Unwrap each line
10
12
  output = re.sub(r"[ ]+\n", " ", output)
11
13
 
14
+ # Extra replacements; e.g. temporary paths that may vary
15
+ if replace:
16
+ for old, new in replace.items():
17
+ output = output.replace(old, new)
18
+
12
19
  # Handle Windows path separator
13
20
  output = output.replace("\\", "/")
14
21
 
@@ -1,4 +1,4 @@
1
- ℹ️ Dry run mode enabled. Will not perform any actual changes.
1
+ ⚠️ Dry run mode enabled. Some operation may behave differently to avoid making changes.
2
2
  ⚠️ Deregistering 15 task definitions...
3
3
  ⚠️ Deregistered task definition 'my-task:1'
4
4
  ⚠️ Deregistered task definition 'my-task:2'
@@ -0,0 +1,3 @@
1
+ 🔔 Retrieving MFA credentials using profile default
2
+ ⚠️ Updating MFA profile (mfa) to AWS credentials (<tmp_path>/credentials)
3
+ 🔔 Persisting MFA configuration in AWS config (<tmp_path>/config), in aws-annoying:mfa section.
@@ -0,0 +1,3 @@
1
+ 🔔 Retrieving MFA credentials using profile default
2
+ ⚠️ Updating MFA profile (mfa) to AWS credentials (<tmp_path>/credentials)
3
+ ⚠️ MFA configuration not persisted.
@@ -0,0 +1,4 @@
1
+ ⚠️ Dry run mode enabled. Some operation may behave differently to avoid making changes.
2
+ 🔔 Retrieving MFA credentials using profile default
3
+ ⚠️ Updating MFA profile (mfa) to AWS credentials (<tmp_path>/credentials)
4
+ 🔔 Persisting MFA configuration in AWS config (<tmp_path>/config), in aws-annoying:mfa section.
@@ -0,0 +1,4 @@
1
+ 🔔 Loaded MFA configuration from AWS config (<tmp_path>/config).
2
+ 🔔 Retrieving MFA credentials using profile default
3
+ ⚠️ Updating MFA profile (mfa) to AWS credentials (<tmp_path>/credentials)
4
+ 🔔 Persisting MFA configuration in AWS config (<tmp_path>/config), in aws-annoying:mfa section.
@@ -56,8 +56,10 @@ def test_basic(snapshot: Snapshot, tmp_path: Path, skip_persist: bool) -> None:
56
56
 
57
57
  # Assert
58
58
  assert result.exit_code == 0
59
- stdout = result.stdout.replace(str(tmp_path), "<tmp_path>")
60
- snapshot.assert_match(normalize_console_output(stdout), "stdout.txt")
59
+ snapshot.assert_match(
60
+ normalize_console_output(result.stdout, replace={str(tmp_path): "<tmp_path>"}),
61
+ "stdout.txt",
62
+ )
61
63
 
62
64
  ini = ConfigParser()
63
65
  ini.read(aws_credentials)
@@ -102,8 +104,10 @@ def test_load_existing_config(snapshot: Snapshot, tmp_path: Path) -> None:
102
104
 
103
105
  # Assert
104
106
  assert result.exit_code == 0
105
- stdout = result.stdout.replace(str(tmp_path), "<tmp_path>")
106
- snapshot.assert_match(normalize_console_output(stdout), "stdout.txt")
107
+ snapshot.assert_match(
108
+ normalize_console_output(result.stdout, replace={str(tmp_path): "<tmp_path>"}),
109
+ "stdout.txt",
110
+ )
107
111
 
108
112
  ini = ConfigParser()
109
113
  ini.read(aws_credentials)
@@ -114,3 +118,45 @@ def test_load_existing_config(snapshot: Snapshot, tmp_path: Path) -> None:
114
118
  }
115
119
 
116
120
  snapshot.assert_match(aws_config.read_text(), "aws_config.ini")
121
+
122
+
123
+ def test_dry_run(snapshot: Snapshot, tmp_path: Path) -> None:
124
+ """If dry-run mode enabled, configuration shouldn't updated."""
125
+ # Arrange
126
+ mfa_profile = "mfa"
127
+ aws_credentials = tmp_path / "credentials"
128
+ aws_config = tmp_path / "config"
129
+
130
+ # Act
131
+ result = runner.invoke(
132
+ app,
133
+ [
134
+ "--dry-run",
135
+ "mfa",
136
+ "configure",
137
+ "--mfa-profile",
138
+ mfa_profile,
139
+ "--mfa-source-profile",
140
+ "default",
141
+ "--mfa-serial-number",
142
+ "1234567890",
143
+ "--mfa-token-code",
144
+ "123456",
145
+ "--aws-credentials",
146
+ str(aws_credentials),
147
+ "--aws-config",
148
+ str(aws_config),
149
+ ],
150
+ )
151
+
152
+ # Assert
153
+ assert result.exit_code == 0
154
+ snapshot.assert_match(
155
+ normalize_console_output(result.stdout, replace={str(tmp_path): "<tmp_path>"}),
156
+ "stdout.txt",
157
+ )
158
+
159
+ ini = ConfigParser()
160
+ ini.read(aws_credentials)
161
+ assert mfa_profile not in ini
162
+ assert not aws_config.exists()
@@ -17,7 +17,7 @@ pytestmark = [
17
17
 
18
18
  @skip_if_macos
19
19
  @skip_if_windows
20
- def test_linux_session_manager_install() -> None:
20
+ def test_install_linux() -> None:
21
21
  # Arrange
22
22
  session_manager = SessionManager()
23
23
  assert session_manager.verify_installation() == (False, None, None)
@@ -35,7 +35,7 @@ def test_linux_session_manager_install() -> None:
35
35
 
36
36
 
37
37
  @run_if_macos
38
- def test_macos_session_manager_install() -> None:
38
+ def test_install_macos() -> None:
39
39
  # Arrange
40
40
  session_manager = SessionManager()
41
41
  assert session_manager.verify_installation() == (False, None, None)
@@ -53,7 +53,7 @@ def test_macos_session_manager_install() -> None:
53
53
 
54
54
 
55
55
  @run_if_windows
56
- def test_windows_session_manager_install() -> None:
56
+ def test_install_windows() -> None:
57
57
  # Arrange
58
58
  session_manager = SessionManager()
59
59
  assert session_manager.verify_installation() == (False, None, None)
@@ -1,13 +1,13 @@
1
- ℹ️ Summary:
1
+ 🔔 Summary:
2
2
  ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
3
3
  ┃ Index ┃ ARN ┃
4
4
  ┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
5
5
  │ 0 │ arn:aws:secretsmanager:us-east-1:000000000000:secret:my-app/django-… │
6
6
  │ 1 │ arn:aws:ssm:us-east-1:000000000000:parameter/my-app/django-settings │
7
7
  └───────┴──────────────────────────────────────────────────────────────────────┘
8
- ℹ️ Retrieving variables from AWS resources...
9
- ℹ️ Retrieved 1 secrets and 1 parameters.
10
- ℹ️ Running the command: tests/cli/_helpers/scripts/printenv.py DJANGO_SETTINGS_MODULE DJANGO_SECRET_KEY DJANGO_DEBUG DJANGO_ALLOWED_HOSTS
8
+ 🔔 Retrieving variables from AWS resources...
9
+ 🔔 Retrieved 1 secrets and 1 parameters.
10
+ 🔔 Running the command: tests/cli/_helpers/scripts/printenv.py DJANGO_SETTINGS_MODULE DJANGO_SECRET_KEY DJANGO_DEBUG DJANGO_ALLOWED_HOSTS
11
11
  DJANGO_SETTINGS_MODULE=config.settings.development
12
12
  DJANGO_SECRET_KEY=my-secret-key
13
13
  DJANGO_DEBUG=False
@@ -1,6 +1,6 @@
1
- ℹ️ Loading ARNs from environment variables with prefix: 'LOAD_AWS_CONFIG__'
2
- ℹ️ Found 1 sources from environment variables.
3
- ℹ️ Summary:
1
+ 🔔 Loading ARNs from environment variables with prefix: 'LOAD_AWS_CONFIG__'
2
+ 🔔 Found 1 sources from environment variables.
3
+ 🔔 Summary:
4
4
  ┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
5
5
  ┃ Index ┃ ARN ┃
6
6
  ┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
@@ -8,9 +8,9 @@
8
8
  │ 1 │ arn:aws:ssm:us-east-1:000000000000:parameter/my-app/django-s… │
9
9
  │ 900_override │ arn:aws:ssm:us-east-1:000000000000:parameter/my-app/override │
10
10
  └──────────────┴───────────────────────────────────────────────────────────────┘
11
- ℹ️ Retrieving variables from AWS resources...
12
- ℹ️ Retrieved 1 secrets and 2 parameters.
13
- ℹ️ Running the command: tests/cli/_helpers/scripts/printenv.py DJANGO_SETTINGS_MODULE DJANGO_SECRET_KEY DJANGO_DEBUG DJANGO_ALLOWED_HOSTS
11
+ 🔔 Retrieving variables from AWS resources...
12
+ 🔔 Retrieved 1 secrets and 2 parameters.
13
+ 🔔 Running the command: tests/cli/_helpers/scripts/printenv.py DJANGO_SETTINGS_MODULE DJANGO_SECRET_KEY DJANGO_DEBUG DJANGO_ALLOWED_HOSTS
14
14
  DJANGO_SETTINGS_MODULE=config.settings.development
15
15
  DJANGO_SECRET_KEY=my-secret-key
16
16
  DJANGO_DEBUG=False
@@ -1,6 +1,6 @@
1
- ℹ️ Loading ARNs from environment variables with prefix: 'LOAD_AWS_CONFIG__'
2
- ℹ️ Found 1 sources from environment variables.
3
- ℹ️ Summary:
1
+ 🔔 Loading ARNs from environment variables with prefix: 'LOAD_AWS_CONFIG__'
2
+ 🔔 Found 1 sources from environment variables.
3
+ 🔔 Summary:
4
4
  ┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
5
5
  ┃ Index ┃ ARN ┃
6
6
  ┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
@@ -8,9 +8,9 @@
8
8
  │ 1 │ arn:aws:ssm:us-east-1:000000000000:parameter/my-app/django-s… │
9
9
  │ 900_override │ arn:aws:ssm:us-east-1:000000000000:parameter/my-app/override │
10
10
  └──────────────┴───────────────────────────────────────────────────────────────┘
11
- ℹ️ Retrieving variables from AWS resources...
12
- ℹ️ Retrieved 1 secrets and 2 parameters.
13
- ℹ️ Running the command: tests/cli/_helpers/scripts/printenv.py DJANGO_SETTINGS_MODULE DJANGO_SECRET_KEY DJANGO_DEBUG DJANGO_ALLOWED_HOSTS
11
+ 🔔 Retrieving variables from AWS resources...
12
+ 🔔 Retrieved 1 secrets and 2 parameters.
13
+ 🔔 Running the command: tests/cli/_helpers/scripts/printenv.py DJANGO_SETTINGS_MODULE DJANGO_SECRET_KEY DJANGO_DEBUG DJANGO_ALLOWED_HOSTS
14
14
  DJANGO_SETTINGS_MODULE=config.settings.local
15
15
  DJANGO_SECRET_KEY=my-secret-key
16
16
  DJANGO_DEBUG=False
@@ -1,4 +1,4 @@
1
- ℹ️ Summary:
1
+ 🔔 Summary:
2
2
  ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
3
3
  ┃ Index ┃ ARN ┃
4
4
  ┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
@@ -6,5 +6,5 @@
6
6
  │ 1 │ arn:aws:ssm:us-east-1:000000000000:parameter/my-app/django-settings │
7
7
  │ 2 │ arn:aws:ssm:us-east-1:123456789012:parameter/unknown-parameter │
8
8
  └───────┴──────────────────────────────────────────────────────────────────────┘
9
- ℹ️ Retrieving variables from AWS resources...
10
- Failed to load the variables: Failed to retrieve parameters: ['unknown-parameter']
9
+ 🔔 Retrieving variables from AWS resources...
10
+ 🚨 Failed to load the variables: Failed to retrieve parameters: ['unknown-parameter']
@@ -1,8 +1,8 @@
1
- ℹ️ Summary:
1
+ 🔔 Summary:
2
2
  ┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
3
3
  ┃ Index ┃ ARN ┃
4
4
  ┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
5
5
  │ 0 │ arn:aws:s3:::my-bucket/my-object │
6
6
  └───────┴──────────────────────────────────┘
7
- ℹ️ Retrieving variables from AWS resources...
8
- Failed to load the variables: Unsupported resource: 'arn:aws:s3:::my-bucket/my-object'
7
+ 🔔 Retrieving variables from AWS resources...
8
+ 🚨 Failed to load the variables: Unsupported resource: 'arn:aws:s3:::my-bucket/my-object'
@@ -35,7 +35,7 @@ wheels = [
35
35
 
36
36
  [[package]]
37
37
  name = "aws-annoying"
38
- version = "0.7.0"
38
+ version = "0.8.0"
39
39
  source = { editable = "." }
40
40
  dependencies = [
41
41
  { name = "boto3" },
@@ -1 +0,0 @@
1
- #!/usr/bin/env bash
@@ -1,3 +0,0 @@
1
- ℹ️ Retrieving MFA credentials using profile default
2
- ⚠️ Updating MFA profile (mfa) to AWS credentials (<tmp_path>/credentials)
3
- ℹ️ Persisting MFA configuration in AWS config (<tmp_path>/config), in aws-annoying:mfa section.
@@ -1,3 +0,0 @@
1
- ℹ️ Retrieving MFA credentials using profile default
2
- ⚠️ Updating MFA profile (mfa) to AWS credentials (<tmp_path>/credentials)
3
- ⚠️ MFA configuration not persisted.
@@ -1,4 +0,0 @@
1
- ℹ️ Loaded MFA configuration from AWS config (<tmp_path>/config).
2
- ℹ️ Retrieving MFA credentials using profile default
3
- ⚠️ Updating MFA profile (mfa) to AWS credentials (<tmp_path>/credentials)
4
- ℹ️ Persisting MFA configuration in AWS config (<tmp_path>/config), in aws-annoying:mfa section.
File without changes
File without changes
File without changes
File without changes
File without changes