cloudsnake 0.6.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.
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/PKG-INFO +40 -7
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/README.md +35 -4
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/pyproject.toml +5 -3
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/cli.py +11 -13
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/dto.py +13 -0
- cloudsnake-0.8.0/src/cloudsnake/cli/sso.py +155 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/logger.py +2 -1
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/session.py +1 -1
- cloudsnake-0.8.0/src/cloudsnake/sdk/sso.py +71 -0
- cloudsnake-0.8.0/src/cloudsnake/sdk/sso_oidc.py +65 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/tui.py +1 -0
- cloudsnake-0.8.0/src/cloudsnake/utils.py +63 -0
- cloudsnake-0.8.0/tests/test_cli.py +12 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/test_helpers.py +0 -7
- cloudsnake-0.6.0/tests/test_cli.py +0 -12
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/CHANGELOG.md +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/LICENSE +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/__init__.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/__main__.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/rds.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/ssm.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/helpers.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/aws.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/ec2.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/rds_session.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/ssm_parameters.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/ssm_session.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/README.md +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/__init__.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/conftest.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/test_logger.py +0 -0
- {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/test_rds.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudsnake
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Some AWS CLI commands with a beautiful TUI
|
|
5
5
|
License: GPL-3.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -22,12 +22,14 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
23
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
24
24
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
25
|
-
Requires-Dist: boto3 (>=1.
|
|
25
|
+
Requires-Dist: boto3 (>=1.42.10,<2.0.0)
|
|
26
|
+
Requires-Dist: boto3-stubs (>=1.42.8,<1.43.0)
|
|
26
27
|
Requires-Dist: click (<8.4.0)
|
|
28
|
+
Requires-Dist: configparser (>=7.2.0,<8.0.0)
|
|
27
29
|
Requires-Dist: dacite (>=1.8.1,<2.0.0)
|
|
28
30
|
Requires-Dist: jmespath (>=1.0.1,<2.0.0)
|
|
29
31
|
Requires-Dist: moto (>=5.0.9,<6.0.0)
|
|
30
|
-
Requires-Dist: requests (>=2.32.
|
|
32
|
+
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
31
33
|
Requires-Dist: rich (>=14.2.0,<15.0.0)
|
|
32
34
|
Requires-Dist: simple-term-menu (>=1.6.4,<2.0.0)
|
|
33
35
|
Requires-Dist: typer (>=0.20.0,<0.21.0)
|
|
@@ -59,14 +61,23 @@ Description-Content-Type: text/markdown
|
|
|
59
61
|
|
|
60
62
|
---
|
|
61
63
|
|
|
64
|
+
In your terminal, set the corresponding `AWS_PROFILE=MyProfile` if not using the default. (`~/.aws/credentials`). Copy [this helper function](./aws-profile.sh) called `aws-profile` into your favourite shell (`.bashrc`, `.zshrc`, `~/.config/fish/function`) to easily switch between AWS profiles. In case of using `fish` shell, use [this other function](./aws-profile.fish).
|
|
65
|
+
|
|
66
|
+
<br><br>
|
|
67
|
+
<p align="center">
|
|
68
|
+
<img align="center" alt="SSM session" src="docs/img/aws-profile.gif">
|
|
69
|
+
<h3 align="center">aws-profile</h3>
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
<br><br>
|
|
62
74
|
<p align="center">
|
|
63
75
|
<img align="center" alt="SSM session" src="docs/img//cloudsnake-ssm-session.gif">
|
|
64
76
|
<h3 align="center">SSM session</h3>
|
|
65
77
|
</p>
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
Install the [REQUIRED plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) to use SSM sessions.
|
|
68
80
|
|
|
69
|
-
Then run:
|
|
70
81
|
|
|
71
82
|
```shell
|
|
72
83
|
cloudsnake ssm start-session -is # will print all your instances in a terminal menu
|
|
@@ -75,15 +86,31 @@ cloudsnake ssm start-session --target i-XXXXXX # connect to the instance specif
|
|
|
75
86
|
|
|
76
87
|
---
|
|
77
88
|
|
|
89
|
+
<br><br>
|
|
78
90
|
<p align="center">
|
|
79
91
|
<img align="center" alt="SSM get parameter" src="docs/img/cloudsnake-ssm-parameter.gif">
|
|
80
|
-
<h3 align="center">SSM
|
|
92
|
+
<h3 align="center">SSM parameter</h3>
|
|
81
93
|
</p>
|
|
82
94
|
|
|
83
95
|
```shell
|
|
84
96
|
cloudsnake ssm get-parameter # default region eu-west-1
|
|
85
97
|
cloudsnake --region us-east-1 ssm get-parameters # specify region
|
|
86
98
|
```
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
<br><br>
|
|
102
|
+
<p align="center">
|
|
103
|
+
<img align="center" alt="SSO get-credentials" src="docs/img/cloudsnake-sso-get-credentials.png">
|
|
104
|
+
<h3 align="center">SSO get-credentials</h3>
|
|
105
|
+
</p>
|
|
106
|
+
|
|
107
|
+
```shell
|
|
108
|
+
cloudsnake --region eu-west-1 sso get-credentials --start-url https://myapp.awsapps.com/start
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> [!NOTE]
|
|
112
|
+
> This command will open your default browser. You will need to approve manually the authentication.
|
|
113
|
+
> More use cases and examples for `cloudsnake sso get-credentials` can be found in [`docs/sso-get-credentials.md`](./docs/sso-get-credentials.md).
|
|
87
114
|
|
|
88
115
|
# Installation
|
|
89
116
|
|
|
@@ -134,12 +161,18 @@ hint: See PEP 668 for the detailed specification.
|
|
|
134
161
|
|
|
135
162
|
# Uninstall
|
|
136
163
|
|
|
137
|
-
```
|
|
164
|
+
```bash
|
|
138
165
|
pipx uninstall cloudsnake
|
|
139
166
|
# or
|
|
140
167
|
pip3 uninstall cloudsnake
|
|
141
168
|
```
|
|
142
169
|
|
|
170
|
+
## Debug AWS SDK API calls
|
|
171
|
+
|
|
172
|
+
```shell
|
|
173
|
+
cloudsnake --log-level debug command subcommand [options]
|
|
174
|
+
```
|
|
175
|
+
|
|
143
176
|
# License
|
|
144
177
|
|
|
145
178
|
`cloudsnake` is distributed under the terms of the [GPL3](https://spdx.org/licenses/GPL-3.0-or-later.html) license.
|
|
@@ -21,14 +21,23 @@
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
In your terminal, set the corresponding `AWS_PROFILE=MyProfile` if not using the default. (`~/.aws/credentials`). Copy [this helper function](./aws-profile.sh) called `aws-profile` into your favourite shell (`.bashrc`, `.zshrc`, `~/.config/fish/function`) to easily switch between AWS profiles. In case of using `fish` shell, use [this other function](./aws-profile.fish).
|
|
25
|
+
|
|
26
|
+
<br><br>
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img align="center" alt="SSM session" src="docs/img/aws-profile.gif">
|
|
29
|
+
<h3 align="center">aws-profile</h3>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
<br><br>
|
|
24
34
|
<p align="center">
|
|
25
35
|
<img align="center" alt="SSM session" src="docs/img//cloudsnake-ssm-session.gif">
|
|
26
36
|
<h3 align="center">SSM session</h3>
|
|
27
37
|
</p>
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
Install the [REQUIRED plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) to use SSM sessions.
|
|
30
40
|
|
|
31
|
-
Then run:
|
|
32
41
|
|
|
33
42
|
```shell
|
|
34
43
|
cloudsnake ssm start-session -is # will print all your instances in a terminal menu
|
|
@@ -37,15 +46,31 @@ cloudsnake ssm start-session --target i-XXXXXX # connect to the instance specif
|
|
|
37
46
|
|
|
38
47
|
---
|
|
39
48
|
|
|
49
|
+
<br><br>
|
|
40
50
|
<p align="center">
|
|
41
51
|
<img align="center" alt="SSM get parameter" src="docs/img/cloudsnake-ssm-parameter.gif">
|
|
42
|
-
<h3 align="center">SSM
|
|
52
|
+
<h3 align="center">SSM parameter</h3>
|
|
43
53
|
</p>
|
|
44
54
|
|
|
45
55
|
```shell
|
|
46
56
|
cloudsnake ssm get-parameter # default region eu-west-1
|
|
47
57
|
cloudsnake --region us-east-1 ssm get-parameters # specify region
|
|
48
58
|
```
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
<br><br>
|
|
62
|
+
<p align="center">
|
|
63
|
+
<img align="center" alt="SSO get-credentials" src="docs/img/cloudsnake-sso-get-credentials.png">
|
|
64
|
+
<h3 align="center">SSO get-credentials</h3>
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
```shell
|
|
68
|
+
cloudsnake --region eu-west-1 sso get-credentials --start-url https://myapp.awsapps.com/start
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> [!NOTE]
|
|
72
|
+
> This command will open your default browser. You will need to approve manually the authentication.
|
|
73
|
+
> More use cases and examples for `cloudsnake sso get-credentials` can be found in [`docs/sso-get-credentials.md`](./docs/sso-get-credentials.md).
|
|
49
74
|
|
|
50
75
|
# Installation
|
|
51
76
|
|
|
@@ -96,12 +121,18 @@ hint: See PEP 668 for the detailed specification.
|
|
|
96
121
|
|
|
97
122
|
# Uninstall
|
|
98
123
|
|
|
99
|
-
```
|
|
124
|
+
```bash
|
|
100
125
|
pipx uninstall cloudsnake
|
|
101
126
|
# or
|
|
102
127
|
pip3 uninstall cloudsnake
|
|
103
128
|
```
|
|
104
129
|
|
|
130
|
+
## Debug AWS SDK API calls
|
|
131
|
+
|
|
132
|
+
```shell
|
|
133
|
+
cloudsnake --log-level debug command subcommand [options]
|
|
134
|
+
```
|
|
135
|
+
|
|
105
136
|
# License
|
|
106
137
|
|
|
107
138
|
`cloudsnake` is distributed under the terms of the [GPL3](https://spdx.org/licenses/GPL-3.0-or-later.html) license.
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "cloudsnake"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = 'Some AWS CLI commands with a beautiful TUI'
|
|
9
9
|
authors = ["containerscrew <info@containerscrew.com>"]
|
|
10
10
|
repository = "https://github.com/containerscrew/cloudsnake"
|
|
@@ -41,7 +41,7 @@ pytest-cov = "^7.0.0"
|
|
|
41
41
|
|
|
42
42
|
[tool.poetry.dependencies]
|
|
43
43
|
python = "^3.12"
|
|
44
|
-
boto3 = "^1.
|
|
44
|
+
boto3 = "^1.42.10"
|
|
45
45
|
click = "<8.4.0"
|
|
46
46
|
dacite = "^1.8.1"
|
|
47
47
|
simple-term-menu = "^1.6.4"
|
|
@@ -50,4 +50,6 @@ typing-extensions = "^4.12.0"
|
|
|
50
50
|
rich = "^14.2.0"
|
|
51
51
|
jmespath = "^1.0.1"
|
|
52
52
|
moto = "^5.0.9"
|
|
53
|
-
requests = "^2.32.
|
|
53
|
+
requests = "^2.32.5"
|
|
54
|
+
boto3-stubs = "~=1.42.8"
|
|
55
|
+
configparser = "^7.2.0"
|
|
@@ -4,6 +4,7 @@ from typing import Optional
|
|
|
4
4
|
from importlib.metadata import version
|
|
5
5
|
from cloudsnake.cli.dto import Common, LoggingLevel
|
|
6
6
|
from cloudsnake.cli.ssm import ssm
|
|
7
|
+
from cloudsnake.cli.sso import sso
|
|
7
8
|
from cloudsnake.sdk.session import SessionWrapper
|
|
8
9
|
from cloudsnake.logger import init_logger
|
|
9
10
|
from rich import traceback
|
|
@@ -16,7 +17,7 @@ APP_VERSION = version("cloudsnake")
|
|
|
16
17
|
# Declare app and add subcommands
|
|
17
18
|
app = typer.Typer(
|
|
18
19
|
name="cloudsnake",
|
|
19
|
-
help="
|
|
20
|
+
help=f"🐍 A modern CLI to interact with AWS resources. (c) 2025 containerscrew - version {APP_VERSION}",
|
|
20
21
|
no_args_is_help=True,
|
|
21
22
|
pretty_exceptions_short=True,
|
|
22
23
|
pretty_exceptions_show_locals=False,
|
|
@@ -24,17 +25,7 @@ app = typer.Typer(
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
app.add_typer(ssm, name="ssm", help="Manage SSM operations")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@app.command("version", help="Show cloudsnake app version")
|
|
30
|
-
def version_cmd():
|
|
31
|
-
typer.echo(
|
|
32
|
-
typer.style(
|
|
33
|
-
f"cloudsnake version: {APP_VERSION}",
|
|
34
|
-
fg=typer.colors.GREEN,
|
|
35
|
-
bold=True,
|
|
36
|
-
)
|
|
37
|
-
)
|
|
28
|
+
app.add_typer(sso, name="sso", help="Manage SSO operations")
|
|
38
29
|
|
|
39
30
|
|
|
40
31
|
@app.callback()
|
|
@@ -63,7 +54,14 @@ def entrypoint(
|
|
|
63
54
|
Entry point for the cloudsnake CLI.
|
|
64
55
|
"""
|
|
65
56
|
logger = init_logger(log_level.value)
|
|
66
|
-
|
|
57
|
+
|
|
58
|
+
typer.echo(
|
|
59
|
+
typer.style(
|
|
60
|
+
f"~> cloudsnake 🐍 - version {APP_VERSION}",
|
|
61
|
+
fg=typer.colors.CYAN,
|
|
62
|
+
bold=True,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
67
65
|
|
|
68
66
|
# Create resources
|
|
69
67
|
session = SessionWrapper(profile, region).with_local_session()
|
|
@@ -10,6 +10,19 @@ class Common:
|
|
|
10
10
|
region: str
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
@dataclass
|
|
14
|
+
class DeviceRegistration:
|
|
15
|
+
client_id: str
|
|
16
|
+
client_secret: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DeviceCode:
|
|
21
|
+
device_code: str
|
|
22
|
+
user_code: str
|
|
23
|
+
verification_uri_complete: str
|
|
24
|
+
|
|
25
|
+
|
|
13
26
|
class OutputMode(str, Enum):
|
|
14
27
|
json = "json"
|
|
15
28
|
table = "table"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import sys
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.progress import (
|
|
10
|
+
Progress,
|
|
11
|
+
SpinnerColumn,
|
|
12
|
+
BarColumn,
|
|
13
|
+
TextColumn,
|
|
14
|
+
TimeElapsedColumn,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from cloudsnake import utils
|
|
18
|
+
from cloudsnake.sdk.sso import SSOWrapper
|
|
19
|
+
from cloudsnake.sdk.sso_oidc import SSOOIDCWrapper
|
|
20
|
+
from cloudsnake.utils import open_browser_url, parse_key_val_list
|
|
21
|
+
|
|
22
|
+
AWS_CREDENTIALS_FILE_PATH = os.path.expanduser("~/.aws/credentials")
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("cloudsnake.sso")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def signal_handler(sig, frame):
|
|
28
|
+
typer.secho("\n ~> You pressed Ctrl+C! Exiting gracefully. Bye!", fg="bright_red")
|
|
29
|
+
sys.exit(0)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
sso = typer.Typer(
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
pretty_exceptions_short=True,
|
|
35
|
+
pretty_exceptions_show_locals=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@sso.command("get-credentials", help="Get SSO credentials", no_args_is_help=True)
|
|
40
|
+
def get_credentials(
|
|
41
|
+
ctx: typer.Context,
|
|
42
|
+
start_url: str = typer.Option(..., help="SSO Start URL"),
|
|
43
|
+
role_overrides: Optional[List[str]] = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--role-overrides",
|
|
46
|
+
"-ro",
|
|
47
|
+
help="Override the role name to filter",
|
|
48
|
+
),
|
|
49
|
+
account_overrides: Optional[List[str]] = typer.Option(
|
|
50
|
+
None,
|
|
51
|
+
"--account-overrides",
|
|
52
|
+
"-ao",
|
|
53
|
+
help="Override the account id to filter",
|
|
54
|
+
),
|
|
55
|
+
workers: int = typer.Option(
|
|
56
|
+
4,
|
|
57
|
+
"--workers",
|
|
58
|
+
"-w",
|
|
59
|
+
help="Number of concurrent workers to fetch credentials",
|
|
60
|
+
),
|
|
61
|
+
):
|
|
62
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
63
|
+
sso_oidc = SSOOIDCWrapper(
|
|
64
|
+
session=ctx.obj.session,
|
|
65
|
+
profile=ctx.obj.profile,
|
|
66
|
+
region=ctx.obj.region,
|
|
67
|
+
)
|
|
68
|
+
device_registration = sso_oidc.register_device_code("cloudsnake", "public")
|
|
69
|
+
device_auth = sso_oidc.create_device_code(
|
|
70
|
+
device_registration.client_id, device_registration.client_secret, start_url
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
open_browser_url(device_auth.verification_uri_complete)
|
|
74
|
+
|
|
75
|
+
typer.echo(
|
|
76
|
+
typer.style(
|
|
77
|
+
f"~> Press Enter after you have authorized the device in the opened browser: {device_auth.verification_uri_complete}",
|
|
78
|
+
fg=typer.colors.CYAN,
|
|
79
|
+
bold=True,
|
|
80
|
+
),
|
|
81
|
+
nl=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
sys.stdin.readline()
|
|
85
|
+
|
|
86
|
+
token = sso_oidc.create_token(
|
|
87
|
+
device_registration.client_id,
|
|
88
|
+
device_registration.client_secret,
|
|
89
|
+
device_auth.device_code,
|
|
90
|
+
"urn:ietf:params:oauth:grant-type:device_code",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
sso = SSOWrapper(
|
|
94
|
+
session=ctx.obj.session,
|
|
95
|
+
profile=ctx.obj.profile,
|
|
96
|
+
region=ctx.obj.region,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
accounts = sso.list_accounts(token)
|
|
100
|
+
all_credentials = []
|
|
101
|
+
account_list = accounts.get("accountList", [])
|
|
102
|
+
|
|
103
|
+
with Progress(
|
|
104
|
+
SpinnerColumn(style="cyan"),
|
|
105
|
+
TextColumn("[bold cyan]{task.description}"),
|
|
106
|
+
BarColumn(bar_width=40),
|
|
107
|
+
TimeElapsedColumn(),
|
|
108
|
+
) as progress:
|
|
109
|
+
task = progress.add_task(
|
|
110
|
+
"Fetching AWS credentials",
|
|
111
|
+
total=len(account_list),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
115
|
+
future_to_account = {
|
|
116
|
+
executor.submit(
|
|
117
|
+
sso.get_credentials,
|
|
118
|
+
account["accountId"],
|
|
119
|
+
account.get("accountName", ""),
|
|
120
|
+
token,
|
|
121
|
+
): account.get("accountName", "")
|
|
122
|
+
for account in account_list
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for future in as_completed(future_to_account):
|
|
126
|
+
account_name = future_to_account[future]
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
result = future.result()
|
|
130
|
+
all_credentials.extend(result)
|
|
131
|
+
progress.update(
|
|
132
|
+
task, advance=1, description=f"[cyan]{account_name}"
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"Error fetching credentials for {account_name}: {e}")
|
|
136
|
+
progress.update(
|
|
137
|
+
task,
|
|
138
|
+
advance=1,
|
|
139
|
+
description=f"[red]ERROR {account_name}",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
role_overrides_map = parse_key_val_list(role_overrides)
|
|
143
|
+
account_overrides_map = parse_key_val_list(account_overrides)
|
|
144
|
+
utils.write_config_file(
|
|
145
|
+
AWS_CREDENTIALS_FILE_PATH,
|
|
146
|
+
all_credentials,
|
|
147
|
+
ctx.obj.region,
|
|
148
|
+
account_overrides_map,
|
|
149
|
+
role_overrides_map,
|
|
150
|
+
)
|
|
151
|
+
typer.secho(
|
|
152
|
+
f"~> AWS credentials have been written to {AWS_CREDENTIALS_FILE_PATH}",
|
|
153
|
+
fg=typer.colors.GREEN,
|
|
154
|
+
bold=True,
|
|
155
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# https://docs.python.org/3/howto/logging.html
|
|
2
2
|
import logging
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class CustomFormatter(logging.Formatter):
|
|
@@ -36,7 +37,7 @@ def configure_boto3_logger(handler, log_level):
|
|
|
36
37
|
botocore_logger.addHandler(handler)
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def init_logger(log_level:
|
|
40
|
+
def init_logger(log_level: Any) -> logging.Logger:
|
|
40
41
|
logger = logging.getLogger("cloudsnake")
|
|
41
42
|
logger.setLevel(log_level)
|
|
42
43
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from cloudsnake.sdk.aws import App
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SSOWrapper(App):
|
|
8
|
+
def __init__(self, **kwargs):
|
|
9
|
+
super().__init__(**kwargs)
|
|
10
|
+
self.log = logging.getLogger("cloudsnake.sso")
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def client_name(self) -> str:
|
|
14
|
+
return "sso"
|
|
15
|
+
|
|
16
|
+
def list_accounts(self, token: str) -> dict:
|
|
17
|
+
try:
|
|
18
|
+
response = self.client.list_accounts(maxResults=123, accessToken=token)
|
|
19
|
+
return response
|
|
20
|
+
except Exception as e:
|
|
21
|
+
self.log.error(f"Couldn't list accounts: {str(e)}")
|
|
22
|
+
raise
|
|
23
|
+
|
|
24
|
+
def list_account_roles(self, account_id: str, token: str) -> dict:
|
|
25
|
+
try:
|
|
26
|
+
response = self.client.list_account_roles(
|
|
27
|
+
accountId=account_id, accessToken=token
|
|
28
|
+
)
|
|
29
|
+
return response
|
|
30
|
+
except Exception as e:
|
|
31
|
+
self.log.error(f"Couldn't list account roles: {str(e)}")
|
|
32
|
+
raise
|
|
33
|
+
|
|
34
|
+
def get_role_credentials(self, account_id: str, role_name: str, token: str) -> dict:
|
|
35
|
+
try:
|
|
36
|
+
response = self.client.get_role_credentials(
|
|
37
|
+
accountId=account_id, roleName=role_name, accessToken=token
|
|
38
|
+
)
|
|
39
|
+
return response["roleCredentials"]
|
|
40
|
+
except Exception as e:
|
|
41
|
+
self.log.error(f"Couldn't get role credentials: {str(e)}")
|
|
42
|
+
raise
|
|
43
|
+
|
|
44
|
+
def get_credentials_by_role(
|
|
45
|
+
self, account_id: str, role_name: str, token: str
|
|
46
|
+
) -> dict:
|
|
47
|
+
role_credentials = self.get_role_credentials(account_id, role_name, token)
|
|
48
|
+
credentials = {
|
|
49
|
+
"AccessKeyId": role_credentials["accessKeyId"],
|
|
50
|
+
"SecretAccessKey": role_credentials["secretAccessKey"],
|
|
51
|
+
"SessionToken": role_credentials["sessionToken"],
|
|
52
|
+
"Expiration": role_credentials["expiration"],
|
|
53
|
+
}
|
|
54
|
+
return credentials
|
|
55
|
+
|
|
56
|
+
def get_credentials(
|
|
57
|
+
self, account_id: str, account_name: str, token: str
|
|
58
|
+
) -> List[dict]:
|
|
59
|
+
roles = self.list_account_roles(account_id, token)
|
|
60
|
+
all_credentials = []
|
|
61
|
+
for role in roles.get("roleList", []):
|
|
62
|
+
role_name = role["roleName"]
|
|
63
|
+
credentials = self.get_credentials_by_role(account_id, role_name, token)
|
|
64
|
+
credentials_info = {
|
|
65
|
+
"AccountId": account_id,
|
|
66
|
+
"AccountName": account_name,
|
|
67
|
+
"RoleName": role_name,
|
|
68
|
+
"Credentials": credentials,
|
|
69
|
+
}
|
|
70
|
+
all_credentials.append(credentials_info)
|
|
71
|
+
return all_credentials
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from cloudsnake.cli.dto import DeviceRegistration, DeviceCode
|
|
4
|
+
from cloudsnake.sdk.aws import App
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SSOOIDCWrapper(App):
|
|
8
|
+
def __init__(self, **kwargs):
|
|
9
|
+
super().__init__(**kwargs)
|
|
10
|
+
self.log = logging.getLogger("cloudsnake.sso")
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def client_name(self) -> str:
|
|
14
|
+
return "sso-oidc"
|
|
15
|
+
|
|
16
|
+
def register_device_code(
|
|
17
|
+
self, client_name: str, client_type: str
|
|
18
|
+
) -> DeviceRegistration:
|
|
19
|
+
try:
|
|
20
|
+
response_client_registration = self.client.register_client(
|
|
21
|
+
clientName=client_name,
|
|
22
|
+
clientType=client_type,
|
|
23
|
+
)
|
|
24
|
+
return DeviceRegistration(
|
|
25
|
+
client_id=response_client_registration["clientId"],
|
|
26
|
+
client_secret=response_client_registration["clientSecret"],
|
|
27
|
+
)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
self.log.error(f"Couldn't register device: {str(e)}")
|
|
30
|
+
raise
|
|
31
|
+
|
|
32
|
+
def create_device_code(
|
|
33
|
+
self, client_id: str, client_secret: str, start_url: str
|
|
34
|
+
) -> DeviceCode:
|
|
35
|
+
try:
|
|
36
|
+
response_device_code = self.client.start_device_authorization(
|
|
37
|
+
clientId=client_id,
|
|
38
|
+
clientSecret=client_secret,
|
|
39
|
+
startUrl=start_url,
|
|
40
|
+
)
|
|
41
|
+
return DeviceCode(
|
|
42
|
+
device_code=response_device_code["deviceCode"],
|
|
43
|
+
user_code=response_device_code["userCode"],
|
|
44
|
+
verification_uri_complete=response_device_code[
|
|
45
|
+
"verificationUriComplete"
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
self.log.error(f"Couldn't create device code: {str(e)}")
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
def create_token(
|
|
53
|
+
self, client_id: str, client_secret: str, device_code: str, grant_type: str
|
|
54
|
+
) -> str:
|
|
55
|
+
try:
|
|
56
|
+
response_token = self.client.create_token(
|
|
57
|
+
clientId=client_id,
|
|
58
|
+
clientSecret=client_secret,
|
|
59
|
+
deviceCode=device_code,
|
|
60
|
+
grantType=grant_type,
|
|
61
|
+
)
|
|
62
|
+
return response_token["accessToken"]
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self.log.error(f"Couldn't create token: {str(e)}")
|
|
65
|
+
raise
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import webbrowser
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def open_browser_url(url: str) -> str | None:
|
|
7
|
+
"""Open a URL in the default web browser."""
|
|
8
|
+
try:
|
|
9
|
+
webbrowser.open(url)
|
|
10
|
+
except Exception as e:
|
|
11
|
+
return f"Failed to open browser: {str(e)}. Open the URL manually {url}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_key_val_list(values: Optional[List[str]]) -> Dict[str, str]:
|
|
15
|
+
parsed: Dict[str, str] = {}
|
|
16
|
+
if not values:
|
|
17
|
+
return parsed
|
|
18
|
+
|
|
19
|
+
for item in values:
|
|
20
|
+
for part in item.split(","):
|
|
21
|
+
if "=" not in part:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"Invalid override format '{part}', expected key=value"
|
|
24
|
+
)
|
|
25
|
+
key, value = part.split("=", 1)
|
|
26
|
+
parsed[key] = value
|
|
27
|
+
|
|
28
|
+
return parsed
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_config_file(
|
|
32
|
+
path: str,
|
|
33
|
+
credentials: List[dict],
|
|
34
|
+
region: str,
|
|
35
|
+
account_overrides: Dict[str, str],
|
|
36
|
+
role_overrides: Dict[str, str],
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Write content to a configuration file."""
|
|
39
|
+
config = configparser.ConfigParser()
|
|
40
|
+
|
|
41
|
+
for cred in credentials:
|
|
42
|
+
account_name = cred["AccountName"].replace(" ", "")
|
|
43
|
+
role_name = cred["RoleName"]
|
|
44
|
+
|
|
45
|
+
account_part = account_overrides.get(account_name, account_name)
|
|
46
|
+
|
|
47
|
+
override_val = role_overrides.get(role_name)
|
|
48
|
+
if override_val is not None and override_val == "":
|
|
49
|
+
profile_name = account_part
|
|
50
|
+
elif override_val is not None:
|
|
51
|
+
profile_name = override_val
|
|
52
|
+
else:
|
|
53
|
+
profile_name = f"{account_part}@{role_name}"
|
|
54
|
+
|
|
55
|
+
config[profile_name] = {
|
|
56
|
+
"aws_access_key_id": cred["Credentials"]["AccessKeyId"],
|
|
57
|
+
"aws_secret_access_key": cred["Credentials"]["SecretAccessKey"],
|
|
58
|
+
"aws_session_token": cred["Credentials"]["SessionToken"],
|
|
59
|
+
"region": region,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
with open(path, "w") as config_file:
|
|
63
|
+
config.write(config_file)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# from typer.testing import CliRunner
|
|
2
|
+
# from cloudsnake.cli.cli import app
|
|
3
|
+
# from importlib.metadata import version
|
|
4
|
+
#
|
|
5
|
+
# runner = CliRunner()
|
|
6
|
+
# app_version = version("cloudsnake")
|
|
7
|
+
#
|
|
8
|
+
#
|
|
9
|
+
# def test_app():
|
|
10
|
+
# result = runner.invoke(app, ["version"])
|
|
11
|
+
# assert result.exit_code == 0
|
|
12
|
+
# assert f"cloudsnake version: {app_version}" in result.stdout
|
|
@@ -83,10 +83,3 @@ def test_ensure_directory_exists(mock_directory):
|
|
|
83
83
|
filepath = os.path.join(mock_directory, "test_file.txt")
|
|
84
84
|
with pytest.raises(FileNotFoundError):
|
|
85
85
|
ensure_directory_exists(filepath)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# def test_ensure_directory_exists(mock_directory):
|
|
89
|
-
# """Test that ensure_directory_exists creates the directory if it doesn't exist."""
|
|
90
|
-
# filepath = os.path.join(mock_directory, "test_file.txt")
|
|
91
|
-
# ensure_directory_exists(filepath)
|
|
92
|
-
# assert os.path.exists(os.path.dirname(filepath))
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from typer.testing import CliRunner
|
|
2
|
-
from cloudsnake.cli.cli import app
|
|
3
|
-
from importlib.metadata import version
|
|
4
|
-
|
|
5
|
-
runner = CliRunner()
|
|
6
|
-
app_version = version("cloudsnake")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_app():
|
|
10
|
-
result = runner.invoke(app, ["version"])
|
|
11
|
-
assert result.exit_code == 0
|
|
12
|
-
assert f"cloudsnake version: {app_version}" in result.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|