Skip to content

Commit 1c65c2d

Browse files
committed
Add gcp auto connect
1 parent 6fedb4a commit 1c65c2d

File tree

1 file changed

+192
-0
lines changed

1 file changed

+192
-0
lines changed

cirun/cloud.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import subprocess
3+
import time
34

45
import typer
56
from rich.console import Console
@@ -241,6 +242,197 @@ def create_azure(
241242
_connect_cloud(name="azure", credentials=credentials)
242243

243244

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+
244436
def _connect_cloud(name, credentials):
245437
cirun = Cirun()
246438
response_json = cirun.cloud_connect(

0 commit comments

Comments
 (0)