|
1 | 1 | import json |
2 | 2 | import subprocess |
| 3 | +import time |
3 | 4 |
|
4 | 5 | import typer |
5 | 6 | from rich.console import Console |
@@ -241,6 +242,197 @@ def create_azure( |
241 | 242 | _connect_cloud(name="azure", credentials=credentials) |
242 | 243 |
|
243 | 244 |
|
| 245 | +@cloud_create.command(name="gcp") |
| 246 | +def create_gcp( |
| 247 | + name: str = typer.Option( |
| 248 | + None, |
| 249 | + "--name", |
| 250 | + help="Name for the service account (optional, auto-generated if not provided)" |
| 251 | + ), |
| 252 | + role: str = typer.Option( |
| 253 | + "roles/compute.admin", |
| 254 | + "--role", |
| 255 | + help="IAM role to grant the service account" |
| 256 | + ), |
| 257 | + auto_connect: bool = typer.Option( |
| 258 | + False, |
| 259 | + "--auto-connect", |
| 260 | + help="Automatically connect the created credentials to Cirun" |
| 261 | + ), |
| 262 | +): |
| 263 | + """Create GCP Service Account credentials for Cirun""" |
| 264 | + import os |
| 265 | + import tempfile |
| 266 | + |
| 267 | + console = Console() |
| 268 | + error_console = Console(stderr=True, style="bold red") |
| 269 | + |
| 270 | + if auto_connect and not os.environ.get("CIRUN_API_KEY"): |
| 271 | + error_console.print("Error: CIRUN_API_KEY environment variable is required for --auto-connect") |
| 272 | + raise typer.Exit(code=1) |
| 273 | + |
| 274 | + # Check if gcloud CLI is installed |
| 275 | + try: |
| 276 | + subprocess.run( |
| 277 | + ["gcloud", "--version"], |
| 278 | + capture_output=True, |
| 279 | + check=True |
| 280 | + ) |
| 281 | + except (subprocess.CalledProcessError, FileNotFoundError): |
| 282 | + error_console.print("Error: gcloud CLI is not installed or not found in PATH") |
| 283 | + error_console.print("Install it from: https://cloud.google.com/sdk/docs/install") |
| 284 | + raise typer.Exit(code=1) |
| 285 | + |
| 286 | + # Get current project |
| 287 | + console.print("[bold blue]Checking gcloud CLI configuration...[/bold blue]") |
| 288 | + try: |
| 289 | + result = subprocess.run( |
| 290 | + ["gcloud", "config", "get-value", "project"], |
| 291 | + capture_output=True, |
| 292 | + check=True, |
| 293 | + text=True |
| 294 | + ) |
| 295 | + project_id = result.stdout.strip() |
| 296 | + except subprocess.CalledProcessError: |
| 297 | + error_console.print("Error: No active GCP project configured") |
| 298 | + error_console.print("Please run: gcloud config set project PROJECT_ID") |
| 299 | + raise typer.Exit(code=1) |
| 300 | + |
| 301 | + if not project_id or project_id == "(unset)": |
| 302 | + error_console.print("Error: No active GCP project configured") |
| 303 | + error_console.print("Please run: gcloud config set project PROJECT_ID") |
| 304 | + raise typer.Exit(code=1) |
| 305 | + |
| 306 | + # Check authentication |
| 307 | + try: |
| 308 | + result = subprocess.run( |
| 309 | + ["gcloud", "auth", "list", "--filter=status:ACTIVE", "--format=value(account)"], |
| 310 | + capture_output=True, |
| 311 | + check=True, |
| 312 | + text=True |
| 313 | + ) |
| 314 | + active_account = result.stdout.strip() |
| 315 | + except subprocess.CalledProcessError: |
| 316 | + active_account = None |
| 317 | + |
| 318 | + if not active_account: |
| 319 | + error_console.print("Error: Not logged in to gcloud CLI") |
| 320 | + error_console.print("Please run: gcloud auth login") |
| 321 | + raise typer.Exit(code=1) |
| 322 | + |
| 323 | + # Display account details |
| 324 | + console.print("\n[bold green]GCP Account Details:[/bold green]") |
| 325 | + console.print(f" Account: [bold]{active_account}[/bold]") |
| 326 | + console.print(f" Project ID: [bold]{project_id}[/bold]") |
| 327 | + console.print("") |
| 328 | + |
| 329 | + # Generate service account name if not provided |
| 330 | + if not name: |
| 331 | + from datetime import datetime, timezone |
| 332 | + name = f"cirun-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}" |
| 333 | + |
| 334 | + sa_email = f"{name}@{project_id}.iam.gserviceaccount.com" |
| 335 | + |
| 336 | + # Confirm before creating |
| 337 | + typer.confirm( |
| 338 | + f"Create service account '{name}' with {role} on project '{project_id}'?", |
| 339 | + abort=True, |
| 340 | + ) |
| 341 | + |
| 342 | + # Create service account |
| 343 | + console.print(f"[bold blue]Creating service account '[bold green]{name}[/bold green]'...[/bold blue]") |
| 344 | + try: |
| 345 | + subprocess.run( |
| 346 | + [ |
| 347 | + "gcloud", "iam", "service-accounts", "create", name, |
| 348 | + "--display-name", f"Cirun service account ({name})", |
| 349 | + "--project", project_id, |
| 350 | + ], |
| 351 | + capture_output=True, |
| 352 | + check=True, |
| 353 | + text=True |
| 354 | + ) |
| 355 | + except subprocess.CalledProcessError as e: |
| 356 | + error_console.print(f"Error creating service account: {e.stderr}") |
| 357 | + raise typer.Exit(code=1) |
| 358 | + |
| 359 | + # Wait for service account to be available |
| 360 | + for _ in range(10): |
| 361 | + result = subprocess.run( |
| 362 | + ["gcloud", "iam", "service-accounts", "describe", sa_email, |
| 363 | + "--project", project_id], |
| 364 | + capture_output=True, text=True |
| 365 | + ) |
| 366 | + if result.returncode == 0: |
| 367 | + break |
| 368 | + time.sleep(2) |
| 369 | + else: |
| 370 | + error_console.print("Service account was not ready in time.") |
| 371 | + raise typer.Exit(code=1) |
| 372 | + |
| 373 | + # Grant IAM role |
| 374 | + console.print(f"[bold blue]Granting [bold green]{role}[/bold green] role...[/bold blue]") |
| 375 | + try: |
| 376 | + subprocess.run( |
| 377 | + [ |
| 378 | + "gcloud", "projects", "add-iam-policy-binding", project_id, |
| 379 | + "--member", f"serviceAccount:{sa_email}", |
| 380 | + "--role", role, |
| 381 | + "--format", "json", |
| 382 | + ], |
| 383 | + capture_output=True, |
| 384 | + check=True, |
| 385 | + text=True |
| 386 | + ) |
| 387 | + except subprocess.CalledProcessError as e: |
| 388 | + error_console.print(f"Error granting IAM role: {e.stderr}") |
| 389 | + raise typer.Exit(code=1) |
| 390 | + |
| 391 | + # Create key file |
| 392 | + console.print("[bold blue]Creating service account key...[/bold blue]") |
| 393 | + try: |
| 394 | + with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp: |
| 395 | + key_file_path = tmp.name |
| 396 | + |
| 397 | + subprocess.run( |
| 398 | + [ |
| 399 | + "gcloud", "iam", "service-accounts", "keys", "create", key_file_path, |
| 400 | + "--iam-account", sa_email, |
| 401 | + ], |
| 402 | + capture_output=True, |
| 403 | + check=True, |
| 404 | + text=True |
| 405 | + ) |
| 406 | + |
| 407 | + with open(key_file_path, "r") as f: |
| 408 | + credentials = json.loads(f.read()) |
| 409 | + except subprocess.CalledProcessError as e: |
| 410 | + error_console.print(f"Error creating service account key: {e.stderr}") |
| 411 | + raise typer.Exit(code=1) |
| 412 | + finally: |
| 413 | + os.unlink(key_file_path) |
| 414 | + |
| 415 | + # Display credentials |
| 416 | + success_console = Console(style="bold green") |
| 417 | + success_console.rule("[bold green]") |
| 418 | + success_console.print("[bold green]✓[/bold green] Service account created successfully!") |
| 419 | + success_console.print("") |
| 420 | + success_console.print("[bold yellow]GCP Credentials for Cirun:[/bold yellow]") |
| 421 | + success_console.print("") |
| 422 | + success_console.print(f" Project ID: [bold]{credentials.get('project_id')}[/bold]") |
| 423 | + success_console.print(f" Client Email: [bold]{credentials.get('client_email')}[/bold]") |
| 424 | + success_console.print(f" Client ID: [bold]{credentials.get('client_id')}[/bold]") |
| 425 | + success_console.print(f" Private Key ID: [bold]{credentials.get('private_key_id')}[/bold]") |
| 426 | + success_console.print("") |
| 427 | + success_console.print("[bold red]⚠️ The private key cannot be recovered if lost![/bold red]") |
| 428 | + success_console.rule("[bold green]") |
| 429 | + |
| 430 | + # Auto-connect if requested |
| 431 | + if auto_connect: |
| 432 | + console.print("\n[bold blue]Connecting credentials to Cirun...[/bold blue]") |
| 433 | + _connect_cloud(name="gcp", credentials=credentials) |
| 434 | + |
| 435 | + |
244 | 436 | def _connect_cloud(name, credentials): |
245 | 437 | cirun = Cirun() |
246 | 438 | response_json = cirun.cloud_connect( |
|
0 commit comments