aws-annoying 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aws_annoying/cli/__init__.py +0 -0
- aws_annoying/cli/load_variables.py +139 -0
- aws_annoying/{main.py → cli/main.py} +4 -4
- aws_annoying/{mfa → cli/mfa}/_app.py +1 -1
- aws_annoying/{mfa → cli/mfa}/configure.py +4 -45
- aws_annoying/cli/session_manager/__init__.py +3 -0
- aws_annoying/{session_manager → cli/session_manager}/_app.py +1 -1
- aws_annoying/{session_manager → cli/session_manager}/_common.py +1 -1
- aws_annoying/mfa.py +54 -0
- aws_annoying/session_manager/__init__.py +3 -2
- aws_annoying/variables.py +133 -0
- {aws_annoying-0.3.0.dist-info → aws_annoying-0.4.0.dist-info}/METADATA +6 -6
- aws_annoying-0.4.0.dist-info/RECORD +30 -0
- aws_annoying-0.4.0.dist-info/entry_points.txt +2 -0
- aws_annoying/load_variables.py +0 -254
- aws_annoying-0.3.0.dist-info/RECORD +0 -26
- aws_annoying-0.3.0.dist-info/entry_points.txt +0 -2
- /aws_annoying/{app.py → cli/app.py} +0 -0
- /aws_annoying/{ecs_task_definition_lifecycle.py → cli/ecs_task_definition_lifecycle.py} +0 -0
- /aws_annoying/{mfa → cli/mfa}/__init__.py +0 -0
- /aws_annoying/{session_manager → cli/session_manager}/install.py +0 -0
- /aws_annoying/{session_manager → cli/session_manager}/port_forward.py +0 -0
- /aws_annoying/{session_manager → cli/session_manager}/start.py +0 -0
- /aws_annoying/{session_manager → cli/session_manager}/stop.py +0 -0
- {aws_annoying-0.3.0.dist-info → aws_annoying-0.4.0.dist-info}/WHEEL +0 -0
- {aws_annoying-0.3.0.dist-info → aws_annoying-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
File without changes
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# flake8: noqa: B008
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import NoReturn, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from aws_annoying.variables import VariableLoader
|
|
13
|
+
|
|
14
|
+
from .app import app
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command(
|
|
18
|
+
context_settings={
|
|
19
|
+
# Allow extra arguments for user provided command
|
|
20
|
+
"allow_extra_args": True,
|
|
21
|
+
"ignore_unknown_options": True,
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
def load_variables( # noqa: PLR0913
|
|
25
|
+
*,
|
|
26
|
+
ctx: typer.Context,
|
|
27
|
+
arns: list[str] = typer.Option(
|
|
28
|
+
[],
|
|
29
|
+
metavar="ARN",
|
|
30
|
+
help=(
|
|
31
|
+
"ARNs of the secret or parameter to load."
|
|
32
|
+
" The variables are loaded in the order of the ARNs,"
|
|
33
|
+
" overwriting the variables with the same name in the order of the ARNs."
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
env_prefix: Optional[str] = typer.Option(
|
|
37
|
+
None,
|
|
38
|
+
help="Prefix of the environment variables to load the ARNs from.",
|
|
39
|
+
show_default=False,
|
|
40
|
+
),
|
|
41
|
+
overwrite_env: bool = typer.Option(
|
|
42
|
+
False, # noqa: FBT003
|
|
43
|
+
help="Overwrite the existing environment variables with the same name.",
|
|
44
|
+
),
|
|
45
|
+
quiet: bool = typer.Option(
|
|
46
|
+
False, # noqa: FBT003
|
|
47
|
+
help="Suppress all outputs from this command.",
|
|
48
|
+
),
|
|
49
|
+
dry_run: bool = typer.Option(
|
|
50
|
+
False, # noqa: FBT003
|
|
51
|
+
help="Print the progress only. Neither load variables nor run the command.",
|
|
52
|
+
),
|
|
53
|
+
replace: bool = typer.Option(
|
|
54
|
+
True, # noqa: FBT003
|
|
55
|
+
help=(
|
|
56
|
+
"Replace the current process (`os.execvpe`) with the command."
|
|
57
|
+
" If disabled, run the command as a `subprocess`."
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
) -> NoReturn:
|
|
61
|
+
"""Wrapper command to run command with variables from AWS resources injected as environment variables.
|
|
62
|
+
|
|
63
|
+
This script is intended to be used in the ECS environment, where currently AWS does not support
|
|
64
|
+
injecting whole JSON dictionary of secrets or parameters as environment variables directly.
|
|
65
|
+
|
|
66
|
+
It first loads the variables from the AWS sources then runs the command with the variables injected as environment variables.
|
|
67
|
+
|
|
68
|
+
In addition to `--arns` option, you can provide ARNs as the environment variables by providing `--env-prefix`.
|
|
69
|
+
For example, if you have the following environment variables:
|
|
70
|
+
|
|
71
|
+
```shell
|
|
72
|
+
export LOAD_AWS_CONFIG__001_app_config=arn:aws:secretsmanager:...
|
|
73
|
+
export LOAD_AWS_CONFIG__002_db_config=arn:aws:ssm:...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
You can run the following command:
|
|
77
|
+
|
|
78
|
+
```shell
|
|
79
|
+
aws-annoying load-variables --env-prefix LOAD_AWS_CONFIG__ -- ...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The variables are loaded in the order of option provided, overwriting the variables with the same name in the order of the ARNs.
|
|
83
|
+
Existing environment variables are preserved by default, unless `--overwrite-env` is provided.
|
|
84
|
+
""" # noqa: E501
|
|
85
|
+
console = Console(quiet=quiet, emoji=False)
|
|
86
|
+
|
|
87
|
+
command = ctx.args
|
|
88
|
+
if not command:
|
|
89
|
+
console.print("⚠️ No command provided. Exiting...")
|
|
90
|
+
raise typer.Exit(0)
|
|
91
|
+
|
|
92
|
+
# Mapping of the ARNs by index (index used for ordering)
|
|
93
|
+
map_arns_by_index = {str(idx): arn for idx, arn in enumerate(arns)}
|
|
94
|
+
if env_prefix:
|
|
95
|
+
console.print(f"🔍 Loading ARNs from environment variables with prefix: {env_prefix!r}")
|
|
96
|
+
arns_env = {
|
|
97
|
+
key.removeprefix(env_prefix): value for key, value in os.environ.items() if key.startswith(env_prefix)
|
|
98
|
+
}
|
|
99
|
+
console.print(f"🔍 Found {len(arns_env)} sources from environment variables.")
|
|
100
|
+
map_arns_by_index = arns_env | map_arns_by_index
|
|
101
|
+
|
|
102
|
+
# Briefly show the ARNs
|
|
103
|
+
table = Table("Index", "ARN")
|
|
104
|
+
for idx, arn in sorted(map_arns_by_index.items()):
|
|
105
|
+
table.add_row(idx, arn)
|
|
106
|
+
|
|
107
|
+
console.print(table)
|
|
108
|
+
|
|
109
|
+
# Retrieve the variables
|
|
110
|
+
loader = VariableLoader(dry_run=dry_run)
|
|
111
|
+
console.print("🔍 Retrieving variables from AWS resources...")
|
|
112
|
+
if dry_run:
|
|
113
|
+
console.print("⚠️ Dry run mode enabled. Variables won't be loaded from AWS.")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
variables, load_stats = loader.load(map_arns_by_index)
|
|
117
|
+
except Exception as exc: # noqa: BLE001
|
|
118
|
+
console.print(f"❌ Failed to load the variables: {exc!s}")
|
|
119
|
+
raise typer.Exit(1) from None
|
|
120
|
+
|
|
121
|
+
console.print(f"✅ Retrieved {load_stats['secrets']} secrets and {load_stats['parameters']} parameters.")
|
|
122
|
+
|
|
123
|
+
# Prepare the environment variables
|
|
124
|
+
env = os.environ.copy()
|
|
125
|
+
if overwrite_env:
|
|
126
|
+
env.update(variables)
|
|
127
|
+
else:
|
|
128
|
+
# Update variables, preserving the existing ones
|
|
129
|
+
for key, value in variables.items():
|
|
130
|
+
env.setdefault(key, str(value))
|
|
131
|
+
|
|
132
|
+
# Run the command with the variables injected as environment variables, replacing current process
|
|
133
|
+
console.print(f"🚀 Running the command: [bold orchid]{' '.join(command)}[/bold orchid]")
|
|
134
|
+
if replace: # pragma: no cover (not coverable)
|
|
135
|
+
os.execvpe(command[0], command, env=env) # noqa: S606
|
|
136
|
+
# The above line should never return
|
|
137
|
+
|
|
138
|
+
result = subprocess.run(command, env=env, check=False) # noqa: S603
|
|
139
|
+
raise typer.Exit(result.returncode)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# flake8: noqa: F401
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import aws_annoying.ecs_task_definition_lifecycle
|
|
5
|
-
import aws_annoying.load_variables
|
|
6
|
-
import aws_annoying.mfa
|
|
7
|
-
import aws_annoying.session_manager
|
|
4
|
+
import aws_annoying.cli.ecs_task_definition_lifecycle
|
|
5
|
+
import aws_annoying.cli.load_variables
|
|
6
|
+
import aws_annoying.cli.mfa
|
|
7
|
+
import aws_annoying.cli.session_manager
|
|
8
8
|
from aws_annoying.utils.debugger import input_as_args
|
|
9
9
|
|
|
10
10
|
# App with all commands registered
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import configparser
|
|
4
3
|
from pathlib import Path # noqa: TC003
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import boto3
|
|
8
7
|
import typer
|
|
9
|
-
from pydantic import BaseModel, ConfigDict
|
|
10
8
|
from rich import print # noqa: A004
|
|
11
9
|
from rich.prompt import Prompt
|
|
12
10
|
|
|
11
|
+
from aws_annoying.mfa import MfaConfig, update_credentials
|
|
12
|
+
|
|
13
13
|
from ._app import mfa_app
|
|
14
14
|
|
|
15
15
|
_CONFIG_INI_SECTION = "aws-annoying:mfa"
|
|
@@ -55,7 +55,7 @@ def configure( # noqa: PLR0913
|
|
|
55
55
|
aws_config = aws_config.expanduser()
|
|
56
56
|
|
|
57
57
|
# Load configuration
|
|
58
|
-
mfa_config, exists =
|
|
58
|
+
mfa_config, exists = MfaConfig.from_ini_file(aws_config, _CONFIG_INI_SECTION)
|
|
59
59
|
if exists:
|
|
60
60
|
print(f"⚙️ Loaded MFA configuration from AWS config ({aws_config}).")
|
|
61
61
|
|
|
@@ -94,7 +94,7 @@ def configure( # noqa: PLR0913
|
|
|
94
94
|
|
|
95
95
|
# Update MFA profile in AWS credentials
|
|
96
96
|
print(f"✅ Updating MFA profile ([bold]{mfa_profile}[/bold]) to AWS credentials ({aws_credentials})")
|
|
97
|
-
|
|
97
|
+
update_credentials(
|
|
98
98
|
aws_credentials,
|
|
99
99
|
mfa_profile, # type: ignore[arg-type]
|
|
100
100
|
access_key=credentials["AccessKeyId"],
|
|
@@ -114,44 +114,3 @@ def configure( # noqa: PLR0913
|
|
|
114
114
|
mfa_config.save_ini_file(aws_config, _CONFIG_INI_SECTION)
|
|
115
115
|
else:
|
|
116
116
|
print("⚠️ MFA configuration not persisted.")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class _MfaConfig(BaseModel):
|
|
120
|
-
model_config = ConfigDict(extra="ignore")
|
|
121
|
-
|
|
122
|
-
mfa_profile: Optional[str] = None
|
|
123
|
-
mfa_source_profile: Optional[str] = None
|
|
124
|
-
mfa_serial_number: Optional[str] = None
|
|
125
|
-
|
|
126
|
-
def save_ini_file(self, path: Path, section_key: str) -> None:
|
|
127
|
-
"""Save configuration to an AWS config file."""
|
|
128
|
-
config_ini = configparser.ConfigParser()
|
|
129
|
-
config_ini.read(path)
|
|
130
|
-
config_ini.setdefault(section_key, {})
|
|
131
|
-
for k, v in self.model_dump(exclude_none=True).items():
|
|
132
|
-
config_ini[section_key][k] = v
|
|
133
|
-
|
|
134
|
-
with path.open("w") as f:
|
|
135
|
-
config_ini.write(f)
|
|
136
|
-
|
|
137
|
-
@classmethod
|
|
138
|
-
def from_ini_file(cls, path: Path, section_key: str) -> tuple[_MfaConfig, bool]:
|
|
139
|
-
"""Load configuration from an AWS config file, with boolean indicating if the config already exists."""
|
|
140
|
-
config_ini = configparser.ConfigParser()
|
|
141
|
-
config_ini.read(path)
|
|
142
|
-
if config_ini.has_section(section_key):
|
|
143
|
-
section = dict(config_ini.items(section_key))
|
|
144
|
-
return cls.model_validate(section), True
|
|
145
|
-
|
|
146
|
-
return cls(), False
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _update_credentials(path: Path, profile: str, *, access_key: str, secret_key: str, session_token: str) -> None:
|
|
150
|
-
credentials_ini = configparser.ConfigParser()
|
|
151
|
-
credentials_ini.read(path)
|
|
152
|
-
credentials_ini.setdefault(profile, {})
|
|
153
|
-
credentials_ini[profile]["aws_access_key_id"] = access_key
|
|
154
|
-
credentials_ini[profile]["aws_secret_access_key"] = secret_key
|
|
155
|
-
credentials_ini[profile]["aws_session_token"] = session_token
|
|
156
|
-
with path.open("w") as f:
|
|
157
|
-
credentials_ini.write(f)
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
import typer
|
|
7
7
|
from rich.prompt import Confirm
|
|
8
8
|
|
|
9
|
-
from .session_manager import SessionManager as _SessionManager
|
|
9
|
+
from aws_annoying.session_manager import SessionManager as _SessionManager
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
# Custom session manager with console interactivity
|
aws_annoying/mfa.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
from pathlib import Path # noqa: TC003
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
# TODO(lasuillard): Need some refactoring (configurator class)
|
|
10
|
+
# TODO(lasuillard): Put some logging
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MfaConfig(BaseModel):
|
|
14
|
+
"""MFA configuration for AWS profiles."""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(extra="ignore")
|
|
17
|
+
|
|
18
|
+
mfa_profile: Optional[str] = None
|
|
19
|
+
mfa_source_profile: Optional[str] = None
|
|
20
|
+
mfa_serial_number: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
def save_ini_file(self, path: Path, section_key: str) -> None:
|
|
23
|
+
"""Save configuration to an AWS config file."""
|
|
24
|
+
config_ini = configparser.ConfigParser()
|
|
25
|
+
config_ini.read(path)
|
|
26
|
+
config_ini.setdefault(section_key, {})
|
|
27
|
+
for k, v in self.model_dump(exclude_none=True).items():
|
|
28
|
+
config_ini[section_key][k] = v
|
|
29
|
+
|
|
30
|
+
with path.open("w") as f:
|
|
31
|
+
config_ini.write(f)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_ini_file(cls, path: Path, section_key: str) -> tuple[MfaConfig, bool]:
|
|
35
|
+
"""Load configuration from an AWS config file, with boolean indicating if the config already exists."""
|
|
36
|
+
config_ini = configparser.ConfigParser()
|
|
37
|
+
config_ini.read(path)
|
|
38
|
+
if config_ini.has_section(section_key):
|
|
39
|
+
section = dict(config_ini.items(section_key))
|
|
40
|
+
return cls.model_validate(section), True
|
|
41
|
+
|
|
42
|
+
return cls(), False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def update_credentials(path: Path, profile: str, *, access_key: str, secret_key: str, session_token: str) -> None:
|
|
46
|
+
"""Update AWS credentials file with the provided profile and credentials."""
|
|
47
|
+
credentials_ini = configparser.ConfigParser()
|
|
48
|
+
credentials_ini.read(path)
|
|
49
|
+
credentials_ini.setdefault(profile, {})
|
|
50
|
+
credentials_ini[profile]["aws_access_key_id"] = access_key
|
|
51
|
+
credentials_ini[profile]["aws_secret_access_key"] = secret_key
|
|
52
|
+
credentials_ini[profile]["aws_session_token"] = session_token
|
|
53
|
+
with path.open("w") as f:
|
|
54
|
+
credentials_ini.write(f)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
from . import
|
|
1
|
+
from .errors import PluginNotInstalledError, SessionManagerError, UnsupportedPlatformError
|
|
2
|
+
from .session_manager import SessionManager
|
|
2
3
|
|
|
3
|
-
__all__ = ("
|
|
4
|
+
__all__ = ("PluginNotInstalledError", "SessionManager", "SessionManagerError", "UnsupportedPlatformError")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# flake8: noqa: B008
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, TypedDict
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
|
|
9
|
+
# Type aliases for readability
|
|
10
|
+
_ARN = str
|
|
11
|
+
_Variables = dict[str, Any]
|
|
12
|
+
|
|
13
|
+
# TODO(lasuillard): Need some refactoring (with #2, #3)
|
|
14
|
+
# TODO(lasuillard): Put some logging
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _LoadStatsDict(TypedDict):
|
|
18
|
+
secrets: int
|
|
19
|
+
parameters: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VariableLoader: # noqa: D101
|
|
23
|
+
def __init__(self, *, dry_run: bool) -> None:
|
|
24
|
+
"""Initialize the VariableLoader.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
dry_run: Whether to run in dry-run mode.
|
|
28
|
+
console: Rich console instance.
|
|
29
|
+
"""
|
|
30
|
+
self.dry_run = dry_run
|
|
31
|
+
|
|
32
|
+
# TODO(lasuillard): Currently not using pagination (do we need more than 10-20 secrets or parameters each?)
|
|
33
|
+
# ; consider adding it if needed
|
|
34
|
+
def load(self, map_arns: dict[str, _ARN]) -> tuple[dict[str, Any], _LoadStatsDict]:
|
|
35
|
+
"""Load the variables from the AWS Secrets Manager and SSM Parameter Store.
|
|
36
|
+
|
|
37
|
+
Each secret or parameter should be a valid dictionary, where the keys are the variable names
|
|
38
|
+
and the values are the variable values.
|
|
39
|
+
|
|
40
|
+
The items are merged in the order of the key of provided mapping, overwriting the variables with the same name
|
|
41
|
+
in the order of the keys.
|
|
42
|
+
"""
|
|
43
|
+
# Split the ARNs by resource types
|
|
44
|
+
secrets_map, parameters_map = {}, {}
|
|
45
|
+
for idx, arn in map_arns.items():
|
|
46
|
+
if arn.startswith("arn:aws:secretsmanager:"):
|
|
47
|
+
secrets_map[idx] = arn
|
|
48
|
+
elif arn.startswith("arn:aws:ssm:"):
|
|
49
|
+
parameters_map[idx] = arn
|
|
50
|
+
else:
|
|
51
|
+
msg = f"Unsupported resource: {arn!r}"
|
|
52
|
+
raise ValueError(msg)
|
|
53
|
+
|
|
54
|
+
# Retrieve variables from AWS resources
|
|
55
|
+
secrets: dict[str, _Variables]
|
|
56
|
+
parameters: dict[str, _Variables]
|
|
57
|
+
if self.dry_run:
|
|
58
|
+
secrets = {idx: {} for idx, _ in secrets_map.items()}
|
|
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)
|
|
63
|
+
|
|
64
|
+
load_stats: _LoadStatsDict = {
|
|
65
|
+
"secrets": len(secrets),
|
|
66
|
+
"parameters": len(parameters),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Merge the variables in order
|
|
70
|
+
full_variables = secrets | parameters # Keys MUST NOT conflict
|
|
71
|
+
merged_in_order = {}
|
|
72
|
+
for _, variables in sorted(full_variables.items()):
|
|
73
|
+
merged_in_order.update(variables)
|
|
74
|
+
|
|
75
|
+
return merged_in_order, load_stats
|
|
76
|
+
|
|
77
|
+
def _retrieve_secrets(self, secrets_map: dict[str, _ARN]) -> dict[str, _Variables]:
|
|
78
|
+
"""Retrieve the secrets from AWS Secrets Manager."""
|
|
79
|
+
if not secrets_map:
|
|
80
|
+
return {}
|
|
81
|
+
|
|
82
|
+
secretsmanager = boto3.client("secretsmanager")
|
|
83
|
+
|
|
84
|
+
# Retrieve the secrets
|
|
85
|
+
arns = list(secrets_map.values())
|
|
86
|
+
response = secretsmanager.batch_get_secret_value(SecretIdList=arns)
|
|
87
|
+
if errors := response["Errors"]:
|
|
88
|
+
msg = f"Failed to retrieve secrets: {errors!r}"
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
|
|
91
|
+
# Parse the secrets
|
|
92
|
+
secrets = response["SecretValues"]
|
|
93
|
+
result = {}
|
|
94
|
+
for secret in secrets:
|
|
95
|
+
arn = secret["ARN"]
|
|
96
|
+
order_key = next(key for key, value in secrets_map.items() if value == arn)
|
|
97
|
+
data = json.loads(secret["SecretString"])
|
|
98
|
+
if not isinstance(data, dict):
|
|
99
|
+
msg = f"Secret data must be a valid dictionary, but got: {type(data)!r}"
|
|
100
|
+
raise TypeError(msg)
|
|
101
|
+
|
|
102
|
+
result[order_key] = data
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
def _retrieve_parameters(self, parameters_map: dict[str, _ARN]) -> dict[str, _Variables]:
|
|
107
|
+
"""Retrieve the parameters from AWS SSM Parameter Store."""
|
|
108
|
+
if not parameters_map:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
ssm = boto3.client("ssm")
|
|
112
|
+
|
|
113
|
+
# Retrieve the parameters
|
|
114
|
+
parameter_names = list(parameters_map.values())
|
|
115
|
+
response = ssm.get_parameters(Names=parameter_names, WithDecryption=True)
|
|
116
|
+
if errors := response["InvalidParameters"]:
|
|
117
|
+
msg = f"Failed to retrieve parameters: {errors!r}"
|
|
118
|
+
raise ValueError(msg)
|
|
119
|
+
|
|
120
|
+
# Parse the parameters
|
|
121
|
+
parameters = response["Parameters"]
|
|
122
|
+
result = {}
|
|
123
|
+
for parameter in parameters:
|
|
124
|
+
arn = parameter["ARN"]
|
|
125
|
+
order_key = next(key for key, value in parameters_map.items() if value == arn)
|
|
126
|
+
data = json.loads(parameter["Value"])
|
|
127
|
+
if not isinstance(data, dict):
|
|
128
|
+
msg = f"Parameter data must be a valid dictionary, but got: {type(data)!r}"
|
|
129
|
+
raise TypeError(msg)
|
|
130
|
+
|
|
131
|
+
result[order_key] = data
|
|
132
|
+
|
|
133
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-annoying
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -9,11 +9,11 @@ Author-email: Yuchan Lee <lasuillard@gmail.com>
|
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Python: <4.0,>=3.9
|
|
12
|
-
Requires-Dist: boto3
|
|
13
|
-
Requires-Dist: pydantic
|
|
14
|
-
Requires-Dist: requests
|
|
15
|
-
Requires-Dist: tqdm
|
|
16
|
-
Requires-Dist: typer
|
|
12
|
+
Requires-Dist: boto3<2,>=1
|
|
13
|
+
Requires-Dist: pydantic<3,>=2
|
|
14
|
+
Requires-Dist: requests<3,>=2
|
|
15
|
+
Requires-Dist: tqdm<5,>=4
|
|
16
|
+
Requires-Dist: typer<1,>=0
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: boto3-stubs[ec2,ecs,secretsmanager,ssm,sts]>=1.37.1; extra == 'dev'
|
|
19
19
|
Requires-Dist: mypy~=1.15.0; extra == 'dev'
|
|
@@ -0,0 +1,30 @@
|
|
|
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,,
|
aws_annoying/load_variables.py
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
# flake8: noqa: B008
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
from typing import Any, NoReturn, Optional
|
|
8
|
-
|
|
9
|
-
import boto3
|
|
10
|
-
import typer
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.table import Table
|
|
13
|
-
|
|
14
|
-
from .app import app
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@app.command(
|
|
18
|
-
context_settings={
|
|
19
|
-
# Allow extra arguments for user provided command
|
|
20
|
-
"allow_extra_args": True,
|
|
21
|
-
"ignore_unknown_options": True,
|
|
22
|
-
},
|
|
23
|
-
)
|
|
24
|
-
def load_variables( # noqa: PLR0913
|
|
25
|
-
*,
|
|
26
|
-
ctx: typer.Context,
|
|
27
|
-
arns: list[str] = typer.Option(
|
|
28
|
-
[],
|
|
29
|
-
metavar="ARN",
|
|
30
|
-
help=(
|
|
31
|
-
"ARNs of the secret or parameter to load."
|
|
32
|
-
" The variables are loaded in the order of the ARNs,"
|
|
33
|
-
" overwriting the variables with the same name in the order of the ARNs."
|
|
34
|
-
),
|
|
35
|
-
),
|
|
36
|
-
env_prefix: Optional[str] = typer.Option(
|
|
37
|
-
None,
|
|
38
|
-
help="Prefix of the environment variables to load the ARNs from.",
|
|
39
|
-
show_default=False,
|
|
40
|
-
),
|
|
41
|
-
overwrite_env: bool = typer.Option(
|
|
42
|
-
False, # noqa: FBT003
|
|
43
|
-
help="Overwrite the existing environment variables with the same name.",
|
|
44
|
-
),
|
|
45
|
-
quiet: bool = typer.Option(
|
|
46
|
-
False, # noqa: FBT003
|
|
47
|
-
help="Suppress all outputs from this command.",
|
|
48
|
-
),
|
|
49
|
-
dry_run: bool = typer.Option(
|
|
50
|
-
False, # noqa: FBT003
|
|
51
|
-
help="Print the progress only. Neither load variables nor run the command.",
|
|
52
|
-
),
|
|
53
|
-
replace: bool = typer.Option(
|
|
54
|
-
True, # noqa: FBT003
|
|
55
|
-
help=(
|
|
56
|
-
"Replace the current process (`os.execvpe`) with the command."
|
|
57
|
-
" If disabled, run the command as a `subprocess`."
|
|
58
|
-
),
|
|
59
|
-
),
|
|
60
|
-
) -> NoReturn:
|
|
61
|
-
"""Wrapper command to run command with variables from AWS resources injected as environment variables.
|
|
62
|
-
|
|
63
|
-
This script is intended to be used in the ECS environment, where currently AWS does not support
|
|
64
|
-
injecting whole JSON dictionary of secrets or parameters as environment variables directly.
|
|
65
|
-
|
|
66
|
-
It first loads the variables from the AWS sources then runs the command with the variables injected as environment variables.
|
|
67
|
-
|
|
68
|
-
In addition to `--arns` option, you can provide ARNs as the environment variables by providing `--env-prefix`.
|
|
69
|
-
For example, if you have the following environment variables:
|
|
70
|
-
|
|
71
|
-
```shell
|
|
72
|
-
export LOAD_AWS_CONFIG__001_app_config=arn:aws:secretsmanager:...
|
|
73
|
-
export LOAD_AWS_CONFIG__002_db_config=arn:aws:ssm:...
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
You can run the following command:
|
|
77
|
-
|
|
78
|
-
```shell
|
|
79
|
-
aws-annoying load-variables --env-prefix LOAD_AWS_CONFIG__ -- ...
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The variables are loaded in the order of option provided, overwriting the variables with the same name in the order of the ARNs.
|
|
83
|
-
Existing environment variables are preserved by default, unless `--overwrite-env` is provided.
|
|
84
|
-
""" # noqa: E501
|
|
85
|
-
console = Console(quiet=quiet, emoji=False)
|
|
86
|
-
|
|
87
|
-
command = ctx.args
|
|
88
|
-
if not command:
|
|
89
|
-
console.print("⚠️ No command provided. Exiting...")
|
|
90
|
-
raise typer.Exit(0)
|
|
91
|
-
|
|
92
|
-
# Mapping of the ARNs by index (index used for ordering)
|
|
93
|
-
map_arns_by_index = {str(idx): arn for idx, arn in enumerate(arns)}
|
|
94
|
-
if env_prefix:
|
|
95
|
-
console.print(f"🔍 Loading ARNs from environment variables with prefix: {env_prefix!r}")
|
|
96
|
-
arns_env = {
|
|
97
|
-
key.removeprefix(env_prefix): value for key, value in os.environ.items() if key.startswith(env_prefix)
|
|
98
|
-
}
|
|
99
|
-
console.print(f"🔍 Found {len(arns_env)} sources from environment variables.")
|
|
100
|
-
map_arns_by_index = arns_env | map_arns_by_index
|
|
101
|
-
|
|
102
|
-
# Briefly show the ARNs
|
|
103
|
-
table = Table("Index", "ARN")
|
|
104
|
-
for idx, arn in sorted(map_arns_by_index.items()):
|
|
105
|
-
table.add_row(idx, arn)
|
|
106
|
-
|
|
107
|
-
console.print(table)
|
|
108
|
-
|
|
109
|
-
# Retrieve the variables
|
|
110
|
-
loader = VariableLoader(dry_run=dry_run, console=console)
|
|
111
|
-
try:
|
|
112
|
-
variables = loader.load(map_arns_by_index)
|
|
113
|
-
except Exception as exc: # noqa: BLE001
|
|
114
|
-
console.print(f"❌ Failed to load the variables: {exc!s}")
|
|
115
|
-
raise typer.Exit(1) from None
|
|
116
|
-
|
|
117
|
-
# Prepare the environment variables
|
|
118
|
-
env = os.environ.copy()
|
|
119
|
-
if overwrite_env:
|
|
120
|
-
env.update(variables)
|
|
121
|
-
else:
|
|
122
|
-
# Update variables, preserving the existing ones
|
|
123
|
-
for key, value in variables.items():
|
|
124
|
-
env.setdefault(key, str(value))
|
|
125
|
-
|
|
126
|
-
# Run the command with the variables injected as environment variables, replacing current process
|
|
127
|
-
console.print(f"🚀 Running the command: [bold orchid]{' '.join(command)}[/bold orchid]")
|
|
128
|
-
if replace: # pragma: no cover (not coverable)
|
|
129
|
-
os.execvpe(command[0], command, env=env) # noqa: S606
|
|
130
|
-
# The above line should never return
|
|
131
|
-
|
|
132
|
-
result = subprocess.run(command, env=env, check=False) # noqa: S603
|
|
133
|
-
raise typer.Exit(result.returncode)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Type aliases for readability
|
|
137
|
-
_ARN = str
|
|
138
|
-
_Variables = dict[str, Any]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class VariableLoader: # noqa: D101
|
|
142
|
-
def __init__(self, *, console: Console | None = None, dry_run: bool) -> None:
|
|
143
|
-
"""Initialize the VariableLoader.
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
dry_run: Whether to run in dry-run mode.
|
|
147
|
-
console: Rich console instance.
|
|
148
|
-
"""
|
|
149
|
-
self.console = console or Console(quiet=True)
|
|
150
|
-
self.dry_run = dry_run
|
|
151
|
-
|
|
152
|
-
# TODO(lasuillard): Currently not using pagination (do we need more than 10-20 secrets or parameters each?)
|
|
153
|
-
# ; consider adding it if needed
|
|
154
|
-
def load(self, map_arns: dict[str, _ARN]) -> dict[str, Any]:
|
|
155
|
-
"""Load the variables from the AWS Secrets Manager and SSM Parameter Store.
|
|
156
|
-
|
|
157
|
-
Each secret or parameter should be a valid dictionary, where the keys are the variable names
|
|
158
|
-
and the values are the variable values.
|
|
159
|
-
|
|
160
|
-
The items are merged in the order of the key of provided mapping, overwriting the variables with the same name
|
|
161
|
-
in the order of the keys.
|
|
162
|
-
"""
|
|
163
|
-
self.console.print("🔍 Retrieving variables from AWS resources...")
|
|
164
|
-
if self.dry_run:
|
|
165
|
-
self.console.print("⚠️ Dry run mode enabled. Variables won't be loaded from AWS.")
|
|
166
|
-
|
|
167
|
-
# Split the ARNs by resource types
|
|
168
|
-
secrets_map, parameters_map = {}, {}
|
|
169
|
-
for idx, arn in map_arns.items():
|
|
170
|
-
if arn.startswith("arn:aws:secretsmanager:"):
|
|
171
|
-
secrets_map[idx] = arn
|
|
172
|
-
elif arn.startswith("arn:aws:ssm:"):
|
|
173
|
-
parameters_map[idx] = arn
|
|
174
|
-
else:
|
|
175
|
-
msg = f"Unsupported resource: {arn!r}"
|
|
176
|
-
raise ValueError(msg)
|
|
177
|
-
|
|
178
|
-
# Retrieve variables from AWS resources
|
|
179
|
-
secrets: dict[str, _Variables]
|
|
180
|
-
parameters: dict[str, _Variables]
|
|
181
|
-
if self.dry_run:
|
|
182
|
-
secrets = {idx: {} for idx, _ in secrets_map.items()}
|
|
183
|
-
parameters = {idx: {} for idx, _ in parameters_map.items()}
|
|
184
|
-
else:
|
|
185
|
-
secrets = self._retrieve_secrets(secrets_map)
|
|
186
|
-
parameters = self._retrieve_parameters(parameters_map)
|
|
187
|
-
|
|
188
|
-
self.console.print(f"✅ Retrieved {len(secrets)} secrets and {len(parameters)} parameters.")
|
|
189
|
-
|
|
190
|
-
# Merge the variables in order
|
|
191
|
-
full_variables = secrets | parameters # Keys MUST NOT conflict
|
|
192
|
-
merged_in_order = {}
|
|
193
|
-
for _, variables in sorted(full_variables.items()):
|
|
194
|
-
merged_in_order.update(variables)
|
|
195
|
-
|
|
196
|
-
return merged_in_order
|
|
197
|
-
|
|
198
|
-
def _retrieve_secrets(self, secrets_map: dict[str, _ARN]) -> dict[str, _Variables]:
|
|
199
|
-
"""Retrieve the secrets from AWS Secrets Manager."""
|
|
200
|
-
if not secrets_map:
|
|
201
|
-
return {}
|
|
202
|
-
|
|
203
|
-
secretsmanager = boto3.client("secretsmanager")
|
|
204
|
-
|
|
205
|
-
# Retrieve the secrets
|
|
206
|
-
arns = list(secrets_map.values())
|
|
207
|
-
response = secretsmanager.batch_get_secret_value(SecretIdList=arns)
|
|
208
|
-
if errors := response["Errors"]:
|
|
209
|
-
msg = f"Failed to retrieve secrets: {errors!r}"
|
|
210
|
-
raise ValueError(msg)
|
|
211
|
-
|
|
212
|
-
# Parse the secrets
|
|
213
|
-
secrets = response["SecretValues"]
|
|
214
|
-
result = {}
|
|
215
|
-
for secret in secrets:
|
|
216
|
-
arn = secret["ARN"]
|
|
217
|
-
order_key = next(key for key, value in secrets_map.items() if value == arn)
|
|
218
|
-
data = json.loads(secret["SecretString"])
|
|
219
|
-
if not isinstance(data, dict):
|
|
220
|
-
msg = f"Secret data must be a valid dictionary, but got: {type(data)!r}"
|
|
221
|
-
raise TypeError(msg)
|
|
222
|
-
|
|
223
|
-
result[order_key] = data
|
|
224
|
-
|
|
225
|
-
return result
|
|
226
|
-
|
|
227
|
-
def _retrieve_parameters(self, parameters_map: dict[str, _ARN]) -> dict[str, _Variables]:
|
|
228
|
-
"""Retrieve the parameters from AWS SSM Parameter Store."""
|
|
229
|
-
if not parameters_map:
|
|
230
|
-
return {}
|
|
231
|
-
|
|
232
|
-
ssm = boto3.client("ssm")
|
|
233
|
-
|
|
234
|
-
# Retrieve the parameters
|
|
235
|
-
parameter_names = list(parameters_map.values())
|
|
236
|
-
response = ssm.get_parameters(Names=parameter_names, WithDecryption=True)
|
|
237
|
-
if errors := response["InvalidParameters"]:
|
|
238
|
-
msg = f"Failed to retrieve parameters: {errors!r}"
|
|
239
|
-
raise ValueError(msg)
|
|
240
|
-
|
|
241
|
-
# Parse the parameters
|
|
242
|
-
parameters = response["Parameters"]
|
|
243
|
-
result = {}
|
|
244
|
-
for parameter in parameters:
|
|
245
|
-
arn = parameter["ARN"]
|
|
246
|
-
order_key = next(key for key, value in parameters_map.items() if value == arn)
|
|
247
|
-
data = json.loads(parameter["Value"])
|
|
248
|
-
if not isinstance(data, dict):
|
|
249
|
-
msg = f"Parameter data must be a valid dictionary, but got: {type(data)!r}"
|
|
250
|
-
raise TypeError(msg)
|
|
251
|
-
|
|
252
|
-
result[order_key] = data
|
|
253
|
-
|
|
254
|
-
return result
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
aws_annoying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
aws_annoying/app.py,sha256=sp50uVoAl4D6Wk3DFpzKZzSsxmSxNYejFxm62b_Kxps,201
|
|
3
|
-
aws_annoying/ecs_task_definition_lifecycle.py,sha256=O36Bf5LBnVJNyYmdlUxhtsIHNoxky1t5YacAXiL9UEI,2803
|
|
4
|
-
aws_annoying/load_variables.py,sha256=380xT1i85HWybgOIWn72xGCDgqYJ2OSa9VOKMlyHg8M,9488
|
|
5
|
-
aws_annoying/main.py,sha256=lWvPCEnm_N9Sx9LA_YDoGHmbCyYWDhkg-UHyt8i-GLY,487
|
|
6
|
-
aws_annoying/mfa/__init__.py,sha256=rbEGhw5lOQZV_XAc3nSbo56JVhsSPpeOgEtiAy9qzEA,50
|
|
7
|
-
aws_annoying/mfa/_app.py,sha256=hpa1Bfx8lRsuZfujM-RyaYU5llOuwGKgf4FwEydthU0,185
|
|
8
|
-
aws_annoying/mfa/configure.py,sha256=i5e0qZBFYafuv3D59eP_JXOMrWSRW_bZp0xgIpOlaPE,5364
|
|
9
|
-
aws_annoying/session_manager/__init__.py,sha256=FkT6jT6OXduOURN61d-U6hgd-XluQbvuVtKXXiXgSEk,105
|
|
10
|
-
aws_annoying/session_manager/_app.py,sha256=89brd-3hEe8ECpJ9Z-ANcOqvduFxbPVA-gqJ50Zr_kU,217
|
|
11
|
-
aws_annoying/session_manager/_common.py,sha256=UzCBDvxVymiZP7t2B65sNi1ZVL6HA67GAkKwIYIFZPI,780
|
|
12
|
-
aws_annoying/session_manager/errors.py,sha256=YioKlRtZ-GUP0F_ts_ebw7-HYkxe8mTes6HK821Kuiw,353
|
|
13
|
-
aws_annoying/session_manager/install.py,sha256=zX2cu-IBYlf9yl8z9P0CTQz72BP6aAMo-KnolC_PGsU,1443
|
|
14
|
-
aws_annoying/session_manager/port_forward.py,sha256=uMcsafTNAHZh-0E1yWXCliMivWyuWV2K9DdVhBZ9pG8,4327
|
|
15
|
-
aws_annoying/session_manager/session_manager.py,sha256=7tqfFPA4bWF0Me_VbUDcq5Cpl1sw3bevGyT8QWfLTOk,12454
|
|
16
|
-
aws_annoying/session_manager/start.py,sha256=1yaMuy-7IWrrDoPHjgygOP_-g_tajfnpaVPZanxsZxs,215
|
|
17
|
-
aws_annoying/session_manager/stop.py,sha256=ttU6nlbVgBkZDtY-DwUyCstv5TFtat5TljkyuY8QICU,1482
|
|
18
|
-
aws_annoying/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
aws_annoying/utils/debugger.py,sha256=UFllDCGI2gPtwo1XS5vqw0qyR6bYr7XknmBwSxalKIc,754
|
|
20
|
-
aws_annoying/utils/downloader.py,sha256=aB5RzT-LpbFX24-2HXlAkdgVowc4TR9FWT_K8WwZ1BE,1923
|
|
21
|
-
aws_annoying/utils/platform.py,sha256=h3DUWmTMM-_4TfTWNqY0uNqyVsBjAuMm2DEbG-daxe8,742
|
|
22
|
-
aws_annoying-0.3.0.dist-info/METADATA,sha256=H6ciT2fBTifIqg0yYSP5IyfK_CaSzl6f1QI_9ktZjMU,1926
|
|
23
|
-
aws_annoying-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
24
|
-
aws_annoying-0.3.0.dist-info/entry_points.txt,sha256=s0l5LK26zEUxLRkBHH9Bu5aH1bOPvYVoTMnUcFlzjm8,62
|
|
25
|
-
aws_annoying-0.3.0.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
|
|
26
|
-
aws_annoying-0.3.0.dist-info/RECORD,,
|
|
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
|