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.
Files changed (32) hide show
  1. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/PKG-INFO +40 -7
  2. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/README.md +35 -4
  3. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/pyproject.toml +5 -3
  4. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/cli.py +11 -13
  5. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/dto.py +13 -0
  6. cloudsnake-0.8.0/src/cloudsnake/cli/sso.py +155 -0
  7. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/logger.py +2 -1
  8. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/session.py +1 -1
  9. cloudsnake-0.8.0/src/cloudsnake/sdk/sso.py +71 -0
  10. cloudsnake-0.8.0/src/cloudsnake/sdk/sso_oidc.py +65 -0
  11. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/tui.py +1 -0
  12. cloudsnake-0.8.0/src/cloudsnake/utils.py +63 -0
  13. cloudsnake-0.8.0/tests/test_cli.py +12 -0
  14. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/test_helpers.py +0 -7
  15. cloudsnake-0.6.0/tests/test_cli.py +0 -12
  16. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/CHANGELOG.md +0 -0
  17. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/LICENSE +0 -0
  18. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/__init__.py +0 -0
  19. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/__main__.py +0 -0
  20. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/rds.py +0 -0
  21. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/cli/ssm.py +0 -0
  22. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/helpers.py +0 -0
  23. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/aws.py +0 -0
  24. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/ec2.py +0 -0
  25. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/rds_session.py +0 -0
  26. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/ssm_parameters.py +0 -0
  27. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/src/cloudsnake/sdk/ssm_session.py +0 -0
  28. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/README.md +0 -0
  29. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/__init__.py +0 -0
  30. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/conftest.py +0 -0
  31. {cloudsnake-0.6.0 → cloudsnake-0.8.0}/tests/test_logger.py +0 -0
  32. {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.6.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.41.5,<2.0.0)
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.3,<3.0.0)
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
- In your terminal, set the corresponding `AWS_PROFILE=MyProfile` if not using the default. (`~/.aws/credentials`). Also install the [REQUIRED plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) to use SSM sessions.
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 get parameter</h3>
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
- ```console
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
- In your terminal, set the corresponding `AWS_PROFILE=MyProfile` if not using the default. (`~/.aws/credentials`). Also install the [REQUIRED plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) to use SSM sessions.
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 get parameter</h3>
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
- ```console
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.6.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.41.5"
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.3"
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="🐍☁ A modern CLI to interact with AWS resources (EC2, SSM, RDS). By github.com/containerscrew",
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
- logger.info("Initializing cloudsnake 🐍☁")
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: str = "INFO") -> logging.Logger:
40
+ def init_logger(log_level: Any) -> logging.Logger:
40
41
  logger = logging.getLogger("cloudsnake")
41
42
  logger.setLevel(log_level)
42
43
 
@@ -13,7 +13,7 @@ class SessionWrapper:
13
13
  self.region = region
14
14
 
15
15
  if not self.profile:
16
- self.log.warning(
16
+ self.log.debug(
17
17
  "No AWS profile provided, falling back to environment defaults"
18
18
  )
19
19
 
@@ -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
@@ -86,4 +86,5 @@ class EC2Tui:
86
86
  clear_screen=True,
87
87
  )
88
88
  idx = terminal_menu.show()
89
+
89
90
  return instance_names[idx]
@@ -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