dayhoff-tools 1.0.1__tar.gz → 1.0.2__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.
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/PKG-INFO +1 -1
- dayhoff_tools-1.0.2/dayhoff_tools/cli/cloud_commands.py +597 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/cli/main.py +5 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/pyproject.toml +1 -1
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/README.md +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/__init__.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/chemistry/standardizer.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/chemistry/utils.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/cli/__init__.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/cli/swarm_commands.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/cli/utility_commands.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/deployment/base.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/deployment/deploy_aws.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/deployment/deploy_gcp.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/deployment/deploy_utils.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/deployment/job_runner.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/deployment/processors.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/deployment/swarm.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/embedders.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/fasta.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/file_ops.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/gcp.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/h5.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/kegg.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/logs.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/mmseqs.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/sqlite.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/structure.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/uniprot.py +0 -0
- {dayhoff_tools-1.0.1 → dayhoff_tools-1.0.2}/dayhoff_tools/warehouse.py +0 -0
@@ -0,0 +1,597 @@
|
|
1
|
+
"""CLI commands for cloud provider authentication and management.
|
2
|
+
|
3
|
+
This module provides commands for authenticating with GCP and AWS from within
|
4
|
+
development containers. It handles both immediate shell environment configuration
|
5
|
+
via the --export flag and persistent configuration via shell RC files.
|
6
|
+
|
7
|
+
The implementation focuses on:
|
8
|
+
1. Unifying cloud authentication with the `dh` CLI tool
|
9
|
+
2. Maintaining persistence across shell sessions via RC file modifications
|
10
|
+
3. Providing similar capabilities to the shell scripts it replaces
|
11
|
+
"""
|
12
|
+
|
13
|
+
import os
|
14
|
+
import re
|
15
|
+
import shutil
|
16
|
+
import subprocess
|
17
|
+
import sys
|
18
|
+
from pathlib import Path
|
19
|
+
from typing import List, Optional, Tuple
|
20
|
+
|
21
|
+
import questionary
|
22
|
+
import typer
|
23
|
+
|
24
|
+
# --- Configuration ---
|
25
|
+
GCP_DEVCON_SA = "devcon@enzyme-discovery.iam.gserviceaccount.com"
|
26
|
+
GCP_PROJECT_ID = "enzyme-discovery"
|
27
|
+
AWS_DEFAULT_PROFILE = "dev-devaccess"
|
28
|
+
AWS_CONFIG_FILE = Path.home() / ".aws" / "config"
|
29
|
+
SHELL_RC_FILES = [
|
30
|
+
Path.home() / ".bashrc",
|
31
|
+
Path.home() / ".bash_profile",
|
32
|
+
Path.home() / ".profile",
|
33
|
+
]
|
34
|
+
|
35
|
+
# --- Color constants for formatted output ---
|
36
|
+
RED = "\033[0;31m"
|
37
|
+
GREEN = "\033[0;32m"
|
38
|
+
YELLOW = "\033[0;33m"
|
39
|
+
BLUE = "\033[0;36m"
|
40
|
+
NC = "\033[0m" # No Color
|
41
|
+
|
42
|
+
|
43
|
+
# --- Common Helper Functions ---
|
44
|
+
def _find_executable(name: str) -> str:
|
45
|
+
"""Find the full path to an executable in PATH."""
|
46
|
+
path = shutil.which(name)
|
47
|
+
if not path:
|
48
|
+
raise FileNotFoundError(
|
49
|
+
f"{name} command not found. Please ensure it's installed."
|
50
|
+
)
|
51
|
+
return path
|
52
|
+
|
53
|
+
|
54
|
+
def _run_command(
|
55
|
+
cmd_list: List[str],
|
56
|
+
capture: bool = False,
|
57
|
+
check: bool = True,
|
58
|
+
suppress_output: bool = False,
|
59
|
+
) -> Tuple[int, str, str]:
|
60
|
+
"""Run a command and return its result.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
cmd_list: List of command arguments
|
64
|
+
capture: Whether to capture output
|
65
|
+
check: Whether to raise on non-zero exit code
|
66
|
+
suppress_output: Whether to hide output even if not captured
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Tuple of (return_code, stdout_str, stderr_str)
|
70
|
+
"""
|
71
|
+
stdout_opt = (
|
72
|
+
subprocess.PIPE if capture else subprocess.DEVNULL if suppress_output else None
|
73
|
+
)
|
74
|
+
stderr_opt = (
|
75
|
+
subprocess.PIPE if capture else subprocess.DEVNULL if suppress_output else None
|
76
|
+
)
|
77
|
+
|
78
|
+
try:
|
79
|
+
result = subprocess.run(
|
80
|
+
cmd_list, stdout=stdout_opt, stderr=stderr_opt, check=check, text=True
|
81
|
+
)
|
82
|
+
return (
|
83
|
+
result.returncode,
|
84
|
+
result.stdout if capture else "",
|
85
|
+
result.stderr if capture else "",
|
86
|
+
)
|
87
|
+
except subprocess.CalledProcessError as e:
|
88
|
+
if capture:
|
89
|
+
return (e.returncode, e.stdout or "", e.stderr or "")
|
90
|
+
return (e.returncode, "", "")
|
91
|
+
|
92
|
+
|
93
|
+
def _modify_rc_files(variable: str, value: Optional[str]) -> None:
|
94
|
+
"""Add or remove an export line from RC files.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
variable: Environment variable name
|
98
|
+
value: Value to set, or None to remove
|
99
|
+
"""
|
100
|
+
for rc_file in SHELL_RC_FILES:
|
101
|
+
if not rc_file.exists():
|
102
|
+
continue
|
103
|
+
|
104
|
+
try:
|
105
|
+
# Read existing content
|
106
|
+
with open(rc_file, "r") as f:
|
107
|
+
lines = f.readlines()
|
108
|
+
|
109
|
+
# Filter out existing exports for this variable
|
110
|
+
pattern = re.compile(f"^export {variable}=")
|
111
|
+
new_lines = [line for line in lines if not pattern.match(line.strip())]
|
112
|
+
|
113
|
+
# Add new export if value is provided
|
114
|
+
if value is not None:
|
115
|
+
new_lines.append(f"export {variable}={value}\n")
|
116
|
+
|
117
|
+
# Write back to file
|
118
|
+
with open(rc_file, "w") as f:
|
119
|
+
f.writelines(new_lines)
|
120
|
+
|
121
|
+
except (IOError, PermissionError) as e:
|
122
|
+
print(f"Warning: Could not update {rc_file}: {e}", file=sys.stderr)
|
123
|
+
|
124
|
+
|
125
|
+
def _get_env_var(variable: str) -> Optional[str]:
|
126
|
+
"""Safely get an environment variable."""
|
127
|
+
return os.environ.get(variable)
|
128
|
+
|
129
|
+
|
130
|
+
# --- GCP Functions ---
|
131
|
+
def _is_gcp_user_authenticated() -> bool:
|
132
|
+
"""Check if a user is authenticated with GCP (not a compute service account)."""
|
133
|
+
gcloud_path = _find_executable("gcloud")
|
134
|
+
cmd = [
|
135
|
+
gcloud_path,
|
136
|
+
"auth",
|
137
|
+
"list",
|
138
|
+
"--filter=status:ACTIVE",
|
139
|
+
"--format=value(account)",
|
140
|
+
]
|
141
|
+
_, stdout, _ = _run_command(cmd, capture=True, check=False)
|
142
|
+
|
143
|
+
account = stdout.strip()
|
144
|
+
return bool(account) and "compute@developer.gserviceaccount.com" not in account
|
145
|
+
|
146
|
+
|
147
|
+
def _get_current_gcp_user() -> str:
|
148
|
+
"""Get the currently authenticated GCP user."""
|
149
|
+
gcloud_path = _find_executable("gcloud")
|
150
|
+
cmd = [
|
151
|
+
gcloud_path,
|
152
|
+
"auth",
|
153
|
+
"list",
|
154
|
+
"--filter=status:ACTIVE",
|
155
|
+
"--format=value(account)",
|
156
|
+
]
|
157
|
+
_, stdout, _ = _run_command(cmd, capture=True, check=False)
|
158
|
+
|
159
|
+
account = stdout.strip()
|
160
|
+
if account:
|
161
|
+
if "compute@developer.gserviceaccount.com" in account:
|
162
|
+
return "Not authenticated (using VM service account)"
|
163
|
+
return account
|
164
|
+
return "Not authenticated"
|
165
|
+
|
166
|
+
|
167
|
+
def _get_current_gcp_impersonation() -> str:
|
168
|
+
"""Get the current impersonated service account, if any."""
|
169
|
+
sa = _get_env_var("CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT")
|
170
|
+
return sa if sa else "None"
|
171
|
+
|
172
|
+
|
173
|
+
def _run_gcloud_login() -> None:
|
174
|
+
"""Run the gcloud auth login command."""
|
175
|
+
gcloud_path = _find_executable("gcloud")
|
176
|
+
print(f"{BLUE}Authenticating with Google Cloud...{NC}")
|
177
|
+
_run_command([gcloud_path, "auth", "login"])
|
178
|
+
print(f"{GREEN}Authentication complete.{NC}")
|
179
|
+
|
180
|
+
|
181
|
+
def _test_gcp_credentials(user: str, impersonation_sa: str) -> None:
|
182
|
+
"""Test GCP credentials with and without impersonation."""
|
183
|
+
gcloud_path = _find_executable("gcloud")
|
184
|
+
|
185
|
+
print(f"\n{BLUE}Testing credentials...{NC}")
|
186
|
+
|
187
|
+
if user != "Not authenticated" and "Not authenticated" not in user:
|
188
|
+
if impersonation_sa != "None":
|
189
|
+
# Test user account first by temporarily unsetting impersonation
|
190
|
+
orig_impersonation = _get_env_var(
|
191
|
+
"CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT"
|
192
|
+
)
|
193
|
+
if orig_impersonation:
|
194
|
+
del os.environ["CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT"]
|
195
|
+
|
196
|
+
print(f"First with user account {user}:")
|
197
|
+
cmd = [gcloud_path, "compute", "zones", "list", "--limit=1"]
|
198
|
+
returncode, _, _ = _run_command(cmd, suppress_output=True, check=False)
|
199
|
+
|
200
|
+
if returncode == 0:
|
201
|
+
print(f"{GREEN}✓ User has direct GCP access{NC}")
|
202
|
+
else:
|
203
|
+
print(f"{YELLOW}✗ User lacks direct GCP access{NC}")
|
204
|
+
|
205
|
+
# Restore impersonation and test with it
|
206
|
+
if orig_impersonation:
|
207
|
+
os.environ["CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT"] = (
|
208
|
+
orig_impersonation
|
209
|
+
)
|
210
|
+
|
211
|
+
print(f"Then impersonating {impersonation_sa}:")
|
212
|
+
returncode, _, _ = _run_command(cmd, suppress_output=True, check=False)
|
213
|
+
|
214
|
+
if returncode == 0:
|
215
|
+
print(f"{GREEN}✓ Successfully using devcon service account{NC}")
|
216
|
+
else:
|
217
|
+
print(
|
218
|
+
f"{RED}Failed to access GCP resources with impersonation. Check permissions.{NC}"
|
219
|
+
)
|
220
|
+
else:
|
221
|
+
# Test user account directly (no impersonation)
|
222
|
+
print(f"Using user account {user} (no impersonation):")
|
223
|
+
cmd = [gcloud_path, "compute", "zones", "list", "--limit=1"]
|
224
|
+
returncode, _, _ = _run_command(cmd, suppress_output=True, check=False)
|
225
|
+
|
226
|
+
if returncode == 0:
|
227
|
+
print(f"{GREEN}✓ Successfully using personal account{NC}")
|
228
|
+
else:
|
229
|
+
print(f"{RED}Failed to access GCP resources. Check permissions.{NC}")
|
230
|
+
|
231
|
+
|
232
|
+
# --- AWS Functions ---
|
233
|
+
def _unset_aws_static_creds() -> None:
|
234
|
+
"""Unset static AWS credential environment variables."""
|
235
|
+
_modify_rc_files("AWS_ACCESS_KEY_ID", None)
|
236
|
+
_modify_rc_files("AWS_SECRET_ACCESS_KEY", None)
|
237
|
+
_modify_rc_files("AWS_SESSION_TOKEN", None)
|
238
|
+
|
239
|
+
|
240
|
+
def _set_aws_profile(profile: str) -> None:
|
241
|
+
"""Set and persist AWS profile in environment and RC files."""
|
242
|
+
_modify_rc_files("AWS_PROFILE", profile)
|
243
|
+
_unset_aws_static_creds()
|
244
|
+
|
245
|
+
|
246
|
+
def _get_current_aws_profile() -> str:
|
247
|
+
"""Get the current AWS profile."""
|
248
|
+
# Check environment variable first
|
249
|
+
profile = _get_env_var("AWS_PROFILE")
|
250
|
+
if profile:
|
251
|
+
return profile
|
252
|
+
|
253
|
+
# Try using aws command to check
|
254
|
+
aws_path = _find_executable("aws")
|
255
|
+
try:
|
256
|
+
cmd = [aws_path, "configure", "list", "--no-cli-pager"]
|
257
|
+
_, stdout, _ = _run_command(cmd, capture=True, check=False)
|
258
|
+
|
259
|
+
# Extract profile from output
|
260
|
+
profile_match = re.search(r"profile\s+(\S+)", stdout)
|
261
|
+
if profile_match and profile_match.group(1) not in ("<not", "not"):
|
262
|
+
return profile_match.group(1)
|
263
|
+
except:
|
264
|
+
pass
|
265
|
+
|
266
|
+
# Default if nothing else works
|
267
|
+
return AWS_DEFAULT_PROFILE
|
268
|
+
|
269
|
+
|
270
|
+
def _is_aws_profile_authenticated(profile: str) -> bool:
|
271
|
+
"""Check if an AWS profile has valid credentials."""
|
272
|
+
aws_path = _find_executable("aws")
|
273
|
+
cmd = [
|
274
|
+
aws_path,
|
275
|
+
"sts",
|
276
|
+
"get-caller-identity",
|
277
|
+
"--profile",
|
278
|
+
profile,
|
279
|
+
"--no-cli-pager",
|
280
|
+
]
|
281
|
+
returncode, _, _ = _run_command(cmd, suppress_output=True, check=False)
|
282
|
+
return returncode == 0
|
283
|
+
|
284
|
+
|
285
|
+
def _run_aws_sso_login(profile: str) -> None:
|
286
|
+
"""Run the AWS SSO login command for a specific profile."""
|
287
|
+
aws_path = _find_executable("aws")
|
288
|
+
print(f"{BLUE}Running 'aws sso login --profile {profile}'...{NC}")
|
289
|
+
_run_command([aws_path, "sso", "login", "--profile", profile])
|
290
|
+
print(f"{GREEN}Authentication complete.{NC}")
|
291
|
+
|
292
|
+
|
293
|
+
def _get_available_aws_profiles() -> List[str]:
|
294
|
+
"""Get list of available AWS profiles from config file."""
|
295
|
+
profiles = []
|
296
|
+
|
297
|
+
if not AWS_CONFIG_FILE.exists():
|
298
|
+
return profiles
|
299
|
+
|
300
|
+
try:
|
301
|
+
with open(AWS_CONFIG_FILE, "r") as f:
|
302
|
+
lines = f.readlines()
|
303
|
+
|
304
|
+
for line in lines:
|
305
|
+
# Match [profile name] or [name] if default profile
|
306
|
+
match = re.match(r"^\[(?:profile\s+)?([^\]]+)\]", line.strip())
|
307
|
+
if match:
|
308
|
+
profiles.append(match.group(1))
|
309
|
+
except:
|
310
|
+
pass
|
311
|
+
|
312
|
+
return profiles
|
313
|
+
|
314
|
+
|
315
|
+
# --- Typer Applications ---
|
316
|
+
gcp_app = typer.Typer(help="Manage GCP authentication and impersonation.")
|
317
|
+
aws_app = typer.Typer(help="Manage AWS SSO authentication.")
|
318
|
+
|
319
|
+
|
320
|
+
# --- GCP Commands ---
|
321
|
+
@gcp_app.command("status")
|
322
|
+
def gcp_status():
|
323
|
+
"""Show current GCP authentication and impersonation status."""
|
324
|
+
user_account = _get_current_gcp_user()
|
325
|
+
impersonated_sa = _get_current_gcp_impersonation()
|
326
|
+
|
327
|
+
print(f"{BLUE}GCP Status:{NC}")
|
328
|
+
print(f"User account: {GREEN}{user_account}{NC}")
|
329
|
+
print(f"Service account: {GREEN}{impersonated_sa}{NC}")
|
330
|
+
print(f"Project: {GREEN}{GCP_PROJECT_ID}{NC}")
|
331
|
+
print(
|
332
|
+
f"Mode: {GREEN}{'Service account impersonation' if impersonated_sa != 'None' else 'Personal account'}{NC}"
|
333
|
+
)
|
334
|
+
|
335
|
+
_test_gcp_credentials(user_account, impersonated_sa)
|
336
|
+
|
337
|
+
|
338
|
+
@gcp_app.command("login")
|
339
|
+
def gcp_login():
|
340
|
+
"""Authenticate with GCP using your Google account."""
|
341
|
+
_run_gcloud_login()
|
342
|
+
print("\nTo activate devcon service account impersonation, run:")
|
343
|
+
print(f' {YELLOW}eval "$(dh gcp use-devcon --export)"{NC}')
|
344
|
+
print("To use your personal account permissions, run:")
|
345
|
+
print(f' {YELLOW}eval "$(dh gcp use-user --export)"{NC}')
|
346
|
+
|
347
|
+
|
348
|
+
@gcp_app.command("use-devcon")
|
349
|
+
def gcp_use_devcon(
|
350
|
+
export: bool = typer.Option(
|
351
|
+
False, "--export", "-x", help="Print export commands for the current shell."
|
352
|
+
),
|
353
|
+
auth_first: bool = typer.Option(
|
354
|
+
False, "--auth", "-a", help="Authenticate user first if needed."
|
355
|
+
),
|
356
|
+
):
|
357
|
+
"""Switch to devcon service account impersonation mode."""
|
358
|
+
if not _is_gcp_user_authenticated():
|
359
|
+
if auth_first:
|
360
|
+
print(
|
361
|
+
f"{YELLOW}You need to authenticate first. Running authentication...{NC}",
|
362
|
+
file=sys.stderr,
|
363
|
+
)
|
364
|
+
_run_gcloud_login()
|
365
|
+
else:
|
366
|
+
print(
|
367
|
+
f"{RED}Error: Not authenticated with GCP. Run 'dh gcp login' first or use --auth flag.{NC}",
|
368
|
+
file=sys.stderr,
|
369
|
+
)
|
370
|
+
sys.exit(1)
|
371
|
+
|
372
|
+
# Modify RC files to persist across sessions
|
373
|
+
_modify_rc_files("CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT", f"'{GCP_DEVCON_SA}'")
|
374
|
+
_modify_rc_files("GOOGLE_CLOUD_PROJECT", f"'{GCP_PROJECT_ID}'")
|
375
|
+
|
376
|
+
if export:
|
377
|
+
# Print export commands for the current shell to stdout
|
378
|
+
print(f"export CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT='{GCP_DEVCON_SA}'")
|
379
|
+
print(f"export GOOGLE_CLOUD_PROJECT='{GCP_PROJECT_ID}'")
|
380
|
+
|
381
|
+
# Print confirmation to stderr so it doesn't affect eval
|
382
|
+
print(
|
383
|
+
f"{GREEN}GCP service account impersonation for '{GCP_DEVCON_SA}' set up successfully.{NC}",
|
384
|
+
file=sys.stderr,
|
385
|
+
)
|
386
|
+
print(f"{GREEN}You now have standard devcon permissions.{NC}", file=sys.stderr)
|
387
|
+
else:
|
388
|
+
# Just print confirmation
|
389
|
+
print(
|
390
|
+
f"{GREEN}Switched to devcon service account impersonation. You now have standard devcon permissions.{NC}"
|
391
|
+
)
|
392
|
+
print(
|
393
|
+
f"Changes will take effect in new shell sessions. To apply in current shell, run:"
|
394
|
+
)
|
395
|
+
print(f' {YELLOW}eval "$(dh gcp use-devcon --export)"{NC}')
|
396
|
+
|
397
|
+
|
398
|
+
@gcp_app.command("use-user")
|
399
|
+
def gcp_use_user(
|
400
|
+
export: bool = typer.Option(
|
401
|
+
False, "--export", "-x", help="Print export commands for the current shell."
|
402
|
+
),
|
403
|
+
auth_first: bool = typer.Option(
|
404
|
+
False, "--auth", "-a", help="Authenticate user first if needed."
|
405
|
+
),
|
406
|
+
):
|
407
|
+
"""Switch to personal account mode (no impersonation)."""
|
408
|
+
if not _is_gcp_user_authenticated():
|
409
|
+
if auth_first:
|
410
|
+
print(
|
411
|
+
f"{YELLOW}You need to authenticate first. Running authentication...{NC}",
|
412
|
+
file=sys.stderr,
|
413
|
+
)
|
414
|
+
_run_gcloud_login()
|
415
|
+
else:
|
416
|
+
print(
|
417
|
+
f"{RED}Error: Not authenticated with GCP. Run 'dh gcp login' first or use --auth flag.{NC}",
|
418
|
+
file=sys.stderr,
|
419
|
+
)
|
420
|
+
sys.exit(1)
|
421
|
+
|
422
|
+
# Modify RC files to persist across sessions
|
423
|
+
_modify_rc_files("CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT", None)
|
424
|
+
_modify_rc_files("GOOGLE_CLOUD_PROJECT", f"'{GCP_PROJECT_ID}'")
|
425
|
+
|
426
|
+
if export:
|
427
|
+
# Print export commands for the current shell to stdout
|
428
|
+
print(f"unset CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT")
|
429
|
+
print(f"export GOOGLE_CLOUD_PROJECT='{GCP_PROJECT_ID}'")
|
430
|
+
|
431
|
+
# Print confirmation to stderr so it doesn't affect eval
|
432
|
+
print(
|
433
|
+
f"{GREEN}Switched to personal account mode. You are now using your own permissions.{NC}",
|
434
|
+
file=sys.stderr,
|
435
|
+
)
|
436
|
+
else:
|
437
|
+
# Just print confirmation
|
438
|
+
print(
|
439
|
+
f"{GREEN}Switched to personal account mode. You are now using your own permissions.{NC}"
|
440
|
+
)
|
441
|
+
print(
|
442
|
+
f"Changes will take effect in new shell sessions. To apply in current shell, run:"
|
443
|
+
)
|
444
|
+
print(f' {YELLOW}eval "$(dh gcp use-user --export)"{NC}')
|
445
|
+
|
446
|
+
|
447
|
+
# --- AWS Commands ---
|
448
|
+
@aws_app.command("status")
|
449
|
+
def aws_status(
|
450
|
+
profile: Optional[str] = typer.Option(
|
451
|
+
None, "--profile", "-p", help="Check specific profile instead of current."
|
452
|
+
)
|
453
|
+
):
|
454
|
+
"""Show current AWS authentication status."""
|
455
|
+
target_profile = profile or _get_current_aws_profile()
|
456
|
+
print(f"{BLUE}AWS profile:{NC} {GREEN}{target_profile}{NC}")
|
457
|
+
|
458
|
+
if _is_aws_profile_authenticated(target_profile):
|
459
|
+
print(f"Credential status: {GREEN}valid{NC}")
|
460
|
+
# Get detailed identity information
|
461
|
+
aws_path = _find_executable("aws")
|
462
|
+
_run_command(
|
463
|
+
[aws_path, "sts", "get-caller-identity", "--profile", target_profile]
|
464
|
+
)
|
465
|
+
else:
|
466
|
+
print(f"Credential status: {RED}not authenticated{NC}")
|
467
|
+
print(f"\nTo authenticate, run:")
|
468
|
+
print(f" {YELLOW}dh aws login --profile {target_profile}{NC}")
|
469
|
+
|
470
|
+
|
471
|
+
@aws_app.command("login")
|
472
|
+
def aws_login(
|
473
|
+
profile: Optional[str] = typer.Option(
|
474
|
+
None, "--profile", "-p", help="Login to specific profile instead of current."
|
475
|
+
)
|
476
|
+
):
|
477
|
+
"""Login to AWS SSO with the specified or current profile."""
|
478
|
+
target_profile = profile or _get_current_aws_profile()
|
479
|
+
_run_aws_sso_login(target_profile)
|
480
|
+
print(f"\nTo activate profile {target_profile} in your current shell, run:")
|
481
|
+
print(f' {YELLOW}eval "$(dh aws use-profile {target_profile} --export)"{NC}')
|
482
|
+
|
483
|
+
|
484
|
+
@aws_app.command("use-profile")
|
485
|
+
def aws_use_profile(
|
486
|
+
profile: str = typer.Argument(..., help="AWS profile name to activate."),
|
487
|
+
export: bool = typer.Option(
|
488
|
+
False, "--export", "-x", help="Print export commands for the current shell."
|
489
|
+
),
|
490
|
+
auto_login: bool = typer.Option(
|
491
|
+
False, "--auto-login", "-a", help="Run 'aws sso login' if needed."
|
492
|
+
),
|
493
|
+
):
|
494
|
+
"""Switch to a specific AWS profile."""
|
495
|
+
# Modify RC files to persist across sessions
|
496
|
+
_set_aws_profile(profile)
|
497
|
+
|
498
|
+
if auto_login and not _is_aws_profile_authenticated(profile):
|
499
|
+
print(
|
500
|
+
f"{YELLOW}Profile '{profile}' not authenticated. Running 'aws sso login'...{NC}",
|
501
|
+
file=sys.stderr,
|
502
|
+
)
|
503
|
+
_run_aws_sso_login(profile)
|
504
|
+
|
505
|
+
if export:
|
506
|
+
# Print export commands for the current shell to stdout
|
507
|
+
print(f"export AWS_PROFILE='{profile}'")
|
508
|
+
print("unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN")
|
509
|
+
|
510
|
+
# Print confirmation to stderr so it doesn't affect eval
|
511
|
+
print(
|
512
|
+
f"{GREEN}AWS profile '{profile}' exported successfully.{NC}",
|
513
|
+
file=sys.stderr,
|
514
|
+
)
|
515
|
+
else:
|
516
|
+
# Just print confirmation
|
517
|
+
print(f"{GREEN}AWS profile set to '{profile}' and persisted to RC files.{NC}")
|
518
|
+
print(
|
519
|
+
f"Changes will take effect in new shell sessions. To apply in current shell, run:"
|
520
|
+
)
|
521
|
+
print(f' {YELLOW}eval "$(dh aws use-profile {profile} --export)"{NC}')
|
522
|
+
|
523
|
+
|
524
|
+
@aws_app.command("interactive")
|
525
|
+
def aws_interactive():
|
526
|
+
"""Launch interactive AWS profile management menu."""
|
527
|
+
current_profile = _get_current_aws_profile()
|
528
|
+
|
529
|
+
print(f"{BLUE}AWS SSO helper – current profile: {GREEN}{current_profile}{NC}")
|
530
|
+
|
531
|
+
while True:
|
532
|
+
choice = questionary.select(
|
533
|
+
"Choose an option:",
|
534
|
+
choices=[
|
535
|
+
f"Authenticate current profile ({current_profile})",
|
536
|
+
"Switch profile",
|
537
|
+
"Show status",
|
538
|
+
"Exit",
|
539
|
+
],
|
540
|
+
).ask()
|
541
|
+
|
542
|
+
if choice == f"Authenticate current profile ({current_profile})":
|
543
|
+
_run_aws_sso_login(current_profile)
|
544
|
+
print(f"{GREEN}Authentication complete.{NC}")
|
545
|
+
print(f"To activate in your current shell, run:")
|
546
|
+
print(
|
547
|
+
f' {YELLOW}eval "$(dh aws use-profile {current_profile} --export)"{NC}'
|
548
|
+
)
|
549
|
+
|
550
|
+
elif choice == "Switch profile":
|
551
|
+
available_profiles = _get_available_aws_profiles()
|
552
|
+
|
553
|
+
if not available_profiles:
|
554
|
+
print(f"{RED}No AWS profiles found. Check your ~/.aws/config file.{NC}")
|
555
|
+
continue
|
556
|
+
|
557
|
+
for i, prof in enumerate(available_profiles, 1):
|
558
|
+
print(f"{i}) {prof}")
|
559
|
+
|
560
|
+
# Get profile selection by number or name
|
561
|
+
sel = questionary.text("Select profile number or name:").ask()
|
562
|
+
|
563
|
+
if sel.isdigit() and 1 <= int(sel) <= len(available_profiles):
|
564
|
+
new_profile = available_profiles[int(sel) - 1]
|
565
|
+
elif sel in available_profiles:
|
566
|
+
new_profile = sel
|
567
|
+
else:
|
568
|
+
print(f"{RED}Invalid selection{NC}")
|
569
|
+
continue
|
570
|
+
|
571
|
+
_set_aws_profile(new_profile)
|
572
|
+
print(f"{GREEN}Switched to profile {new_profile}{NC}")
|
573
|
+
print(f"To activate in your current shell, run:")
|
574
|
+
print(f' {YELLOW}eval "$(dh aws use-profile {new_profile} --export)"{NC}')
|
575
|
+
|
576
|
+
# Ask if they want to authenticate now
|
577
|
+
if questionary.confirm(
|
578
|
+
"Authenticate this profile now?", default=False
|
579
|
+
).ask():
|
580
|
+
_run_aws_sso_login(new_profile)
|
581
|
+
print(f"{GREEN}Authentication complete.{NC}")
|
582
|
+
print(f"To activate in your current shell, run:")
|
583
|
+
print(
|
584
|
+
f' {YELLOW}eval "$(dh aws use-profile {new_profile} --export)"{NC}'
|
585
|
+
)
|
586
|
+
|
587
|
+
elif choice == "Show status":
|
588
|
+
aws_status()
|
589
|
+
|
590
|
+
elif choice == "Exit":
|
591
|
+
print(f"To activate profile {current_profile} in your current shell, run:")
|
592
|
+
print(
|
593
|
+
f' {YELLOW}eval "$(dh aws use-profile {current_profile} --export)"{NC}'
|
594
|
+
)
|
595
|
+
break
|
596
|
+
|
597
|
+
print() # Add newline between iterations
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Entry file for the CLI, which aggregates and aliases all commands."""
|
2
2
|
|
3
3
|
import typer
|
4
|
+
from dayhoff_tools.cli.cloud_commands import aws_app, gcp_app
|
4
5
|
from dayhoff_tools.cli.utility_commands import (
|
5
6
|
add_to_warehouse_typer,
|
6
7
|
build_and_upload_wheel,
|
@@ -21,6 +22,10 @@ app.command("wadd")(add_to_warehouse_typer)
|
|
21
22
|
app.command("wancestry")(get_ancestry)
|
22
23
|
app.command("wimport")(import_from_warehouse_typer)
|
23
24
|
|
25
|
+
# Cloud commands
|
26
|
+
app.add_typer(gcp_app, name="gcp", help="Manage GCP authentication and impersonation.")
|
27
|
+
app.add_typer(aws_app, name="aws", help="Manage AWS SSO authentication.")
|
28
|
+
|
24
29
|
|
25
30
|
@app.command("wheel")
|
26
31
|
def build_and_upload_wheel_command(
|
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
|
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
|