geronimo.cli.keys_cmd
CLI commands for API key management.
1"""CLI commands for API key management.""" 2 3import typer 4from rich.table import Table 5 6from geronimo.serving.auth.keys import APIKeyManager 7from geronimo.cli.utils import console, success, error, warning, dim 8 9keys_app = typer.Typer( 10 name="keys", 11 help="Manage Service to Service API keys for endpoint authentication", 12 no_args_is_help=True, 13) 14 15 16@keys_app.command("create") 17def create_key( 18 name: str = typer.Option(..., "--name", "-n", help="Name for the API key"), 19 scopes: str = typer.Option( 20 "predict", 21 "--scopes", 22 "-s", 23 help="Comma-separated list of scopes", 24 ), 25 keys_file: str = typer.Option( 26 ".geronimo/keys.json", 27 "--keys-file", 28 "-f", 29 help="Path to keys file", 30 ), 31) -> None: 32 """Create a new API key. 33 34 The raw key is only displayed once - save it securely! 35 """ 36 manager = APIKeyManager(keys_file) 37 scope_list = [s.strip() for s in scopes.split(",")] 38 39 raw_key, api_key = manager.create_key(name=name, scopes=scope_list) 40 41 console.print("\n[bold green]✓ API key created successfully![/bold green]\n") 42 console.print(f" Name: [cyan]{api_key.name}[/cyan]") 43 console.print(f" ID: [dim]{api_key.key_id}[/dim]") 44 console.print(f" Scopes: [yellow]{', '.join(api_key.scopes)}[/yellow]") 45 console.print() 46 console.print("[bold yellow]⚠ Save this key - it won't be shown again:[/bold yellow]") 47 console.print(f"\n [bold]{raw_key}[/bold]\n") 48 49 50@keys_app.command("list") 51def list_keys( 52 keys_file: str = typer.Option( 53 ".geronimo/keys.json", 54 "--keys-file", 55 "-f", 56 help="Path to keys file", 57 ), 58) -> None: 59 """List all API keys.""" 60 manager = APIKeyManager(keys_file) 61 keys = manager.list_keys() 62 63 if not keys: 64 dim("No API keys found.") 65 return 66 67 table = Table(title="API Keys") 68 table.add_column("ID", style="dim") 69 table.add_column("Name", style="cyan") 70 table.add_column("Scopes", style="yellow") 71 table.add_column("Created", style="dim") 72 table.add_column("Status") 73 74 for key in keys: 75 status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]" 76 if key.expires_at: 77 status += f" [dim](expires {key.expires_at.date()})[/dim]" 78 79 table.add_row( 80 key.key_id, 81 key.name, 82 ", ".join(key.scopes), 83 key.created_at.strftime("%Y-%m-%d"), 84 status, 85 ) 86 87 console.print(table) 88 89 90@keys_app.command("revoke") 91def revoke_key( 92 key_id: str = typer.Argument(..., help="ID of the key to revoke"), 93 keys_file: str = typer.Option( 94 ".geronimo/keys.json", 95 "--keys-file", 96 "-f", 97 help="Path to keys file", 98 ), 99) -> None: 100 """Revoke an API key (disable but keep record).""" 101 manager = APIKeyManager(keys_file) 102 103 if manager.revoke(key_id): 104 success(f"Key {key_id} revoked") 105 else: 106 error(f"Key {key_id} not found", exit_code=1) 107 108 109@keys_app.command("delete") 110def delete_key( 111 key_id: str = typer.Argument(..., help="ID of the key to delete"), 112 keys_file: str = typer.Option( 113 ".geronimo/keys.json", 114 "--keys-file", 115 "-f", 116 help="Path to keys file", 117 ), 118 force: bool = typer.Option( 119 False, 120 "--force", 121 "-y", # Changed from -f to avoid conflict with --keys-file 122 help="Skip confirmation", 123 ), 124) -> None: 125 """Permanently delete an API key.""" 126 manager = APIKeyManager(keys_file) 127 128 key = manager.get_key(key_id) 129 if not key: 130 error(f"Key {key_id} not found", exit_code=1) 131 132 if not force: 133 confirm = typer.confirm(f"Permanently delete key '{key.name}' ({key_id})?") 134 if not confirm: 135 raise typer.Abort() 136 137 manager.delete(key_id) 138 success(f"Key {key_id} deleted") 139 140 141@keys_app.command("sync") 142def sync_keys( 143 keys_file: str = typer.Option( 144 ".geronimo/keys.json", 145 "--keys-file", 146 "-f", 147 help="Path to keys file", 148 ), 149 key_ids: str = typer.Option( 150 None, 151 "--key-ids", 152 "-k", 153 help="Comma-separated key IDs to sync (default: all)", 154 ), 155 interactive: bool = typer.Option( 156 False, 157 "--interactive", 158 "-i", 159 help="Interactively select keys to sync", 160 ), 161) -> None: 162 """Sync local API keys to Geronimo Cloud. 163 164 Uploads your local API keys to Geronimo Cloud so they can be used 165 for authenticating requests to cloud-deployed endpoints. 166 167 Cloud-managed keys (created via dashboard) take precedence and 168 won't be overwritten by synced keys. 169 """ 170 from geronimo.deploy_cloud.client import GeronimoCloudClient 171 172 manager = APIKeyManager(keys_file) 173 all_keys = manager.list_keys() 174 175 if not all_keys: 176 dim("No local API keys found.") 177 return 178 179 # Filter keys based on options 180 keys_to_sync = all_keys 181 182 if key_ids: 183 # Filter to specified key IDs 184 requested_ids = {k.strip() for k in key_ids.split(",")} 185 keys_to_sync = [k for k in all_keys if k.key_id in requested_ids] 186 187 # Warn about missing keys 188 found_ids = {k.key_id for k in keys_to_sync} 189 missing_ids = requested_ids - found_ids 190 if missing_ids: 191 warning(f"Keys not found: {', '.join(missing_ids)}") 192 193 if not keys_to_sync: 194 error("No matching keys found", exit_code=1) 195 196 elif interactive: 197 # Interactive selection 198 console.print("\n[bold]Select keys to sync:[/bold]\n") 199 keys_to_sync = [] 200 201 for key in all_keys: 202 status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]" 203 console.print(f" [dim]{key.key_id}[/dim] - [cyan]{key.name}[/cyan] ({status})") 204 205 if typer.confirm(" Sync this key?", default=True): 206 keys_to_sync.append(key) 207 208 console.print() 209 210 if not keys_to_sync: 211 dim("No keys selected.") 212 return 213 214 # Convert to dicts for API 215 keys_data = [key.to_dict() for key in keys_to_sync] 216 217 # Sync to cloud 218 try: 219 client = GeronimoCloudClient() 220 result = client.sync_keys(keys_data) 221 222 synced = result.get("synced", 0) 223 skipped = result.get("skipped", 0) 224 225 console.print(f"\n[bold green]✓ Keys synced to Geronimo Cloud[/bold green]") 226 console.print(f" Synced: [green]{synced}[/green]") 227 if skipped: 228 console.print(f" Skipped: [yellow]{skipped}[/yellow] (cloud-managed keys take precedence)") 229 console.print() 230 231 except RuntimeError as e: 232 error(str(e), exit_code=1) 233 except Exception as e: 234 error(f"Failed to sync keys: {e}", exit_code=1)
keys_app =
<typer.main.Typer object>
@keys_app.command('create')
def
create_key( name: str = <typer.models.OptionInfo object>, scopes: str = <typer.models.OptionInfo object>, keys_file: str = <typer.models.OptionInfo object>) -> None:
17@keys_app.command("create") 18def create_key( 19 name: str = typer.Option(..., "--name", "-n", help="Name for the API key"), 20 scopes: str = typer.Option( 21 "predict", 22 "--scopes", 23 "-s", 24 help="Comma-separated list of scopes", 25 ), 26 keys_file: str = typer.Option( 27 ".geronimo/keys.json", 28 "--keys-file", 29 "-f", 30 help="Path to keys file", 31 ), 32) -> None: 33 """Create a new API key. 34 35 The raw key is only displayed once - save it securely! 36 """ 37 manager = APIKeyManager(keys_file) 38 scope_list = [s.strip() for s in scopes.split(",")] 39 40 raw_key, api_key = manager.create_key(name=name, scopes=scope_list) 41 42 console.print("\n[bold green]✓ API key created successfully![/bold green]\n") 43 console.print(f" Name: [cyan]{api_key.name}[/cyan]") 44 console.print(f" ID: [dim]{api_key.key_id}[/dim]") 45 console.print(f" Scopes: [yellow]{', '.join(api_key.scopes)}[/yellow]") 46 console.print() 47 console.print("[bold yellow]⚠ Save this key - it won't be shown again:[/bold yellow]") 48 console.print(f"\n [bold]{raw_key}[/bold]\n")
Create a new API key.
The raw key is only displayed once - save it securely!
@keys_app.command('list')
def
list_keys(keys_file: str = <typer.models.OptionInfo object>) -> None:
51@keys_app.command("list") 52def list_keys( 53 keys_file: str = typer.Option( 54 ".geronimo/keys.json", 55 "--keys-file", 56 "-f", 57 help="Path to keys file", 58 ), 59) -> None: 60 """List all API keys.""" 61 manager = APIKeyManager(keys_file) 62 keys = manager.list_keys() 63 64 if not keys: 65 dim("No API keys found.") 66 return 67 68 table = Table(title="API Keys") 69 table.add_column("ID", style="dim") 70 table.add_column("Name", style="cyan") 71 table.add_column("Scopes", style="yellow") 72 table.add_column("Created", style="dim") 73 table.add_column("Status") 74 75 for key in keys: 76 status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]" 77 if key.expires_at: 78 status += f" [dim](expires {key.expires_at.date()})[/dim]" 79 80 table.add_row( 81 key.key_id, 82 key.name, 83 ", ".join(key.scopes), 84 key.created_at.strftime("%Y-%m-%d"), 85 status, 86 ) 87 88 console.print(table)
List all API keys.
@keys_app.command('revoke')
def
revoke_key( key_id: str = <typer.models.ArgumentInfo object>, keys_file: str = <typer.models.OptionInfo object>) -> None:
91@keys_app.command("revoke") 92def revoke_key( 93 key_id: str = typer.Argument(..., help="ID of the key to revoke"), 94 keys_file: str = typer.Option( 95 ".geronimo/keys.json", 96 "--keys-file", 97 "-f", 98 help="Path to keys file", 99 ), 100) -> None: 101 """Revoke an API key (disable but keep record).""" 102 manager = APIKeyManager(keys_file) 103 104 if manager.revoke(key_id): 105 success(f"Key {key_id} revoked") 106 else: 107 error(f"Key {key_id} not found", exit_code=1)
Revoke an API key (disable but keep record).
@keys_app.command('delete')
def
delete_key( key_id: str = <typer.models.ArgumentInfo object>, keys_file: str = <typer.models.OptionInfo object>, force: bool = <typer.models.OptionInfo object>) -> None:
110@keys_app.command("delete") 111def delete_key( 112 key_id: str = typer.Argument(..., help="ID of the key to delete"), 113 keys_file: str = typer.Option( 114 ".geronimo/keys.json", 115 "--keys-file", 116 "-f", 117 help="Path to keys file", 118 ), 119 force: bool = typer.Option( 120 False, 121 "--force", 122 "-y", # Changed from -f to avoid conflict with --keys-file 123 help="Skip confirmation", 124 ), 125) -> None: 126 """Permanently delete an API key.""" 127 manager = APIKeyManager(keys_file) 128 129 key = manager.get_key(key_id) 130 if not key: 131 error(f"Key {key_id} not found", exit_code=1) 132 133 if not force: 134 confirm = typer.confirm(f"Permanently delete key '{key.name}' ({key_id})?") 135 if not confirm: 136 raise typer.Abort() 137 138 manager.delete(key_id) 139 success(f"Key {key_id} deleted")
Permanently delete an API key.
@keys_app.command('sync')
def
sync_keys( keys_file: str = <typer.models.OptionInfo object>, key_ids: str = <typer.models.OptionInfo object>, interactive: bool = <typer.models.OptionInfo object>) -> None:
142@keys_app.command("sync") 143def sync_keys( 144 keys_file: str = typer.Option( 145 ".geronimo/keys.json", 146 "--keys-file", 147 "-f", 148 help="Path to keys file", 149 ), 150 key_ids: str = typer.Option( 151 None, 152 "--key-ids", 153 "-k", 154 help="Comma-separated key IDs to sync (default: all)", 155 ), 156 interactive: bool = typer.Option( 157 False, 158 "--interactive", 159 "-i", 160 help="Interactively select keys to sync", 161 ), 162) -> None: 163 """Sync local API keys to Geronimo Cloud. 164 165 Uploads your local API keys to Geronimo Cloud so they can be used 166 for authenticating requests to cloud-deployed endpoints. 167 168 Cloud-managed keys (created via dashboard) take precedence and 169 won't be overwritten by synced keys. 170 """ 171 from geronimo.deploy_cloud.client import GeronimoCloudClient 172 173 manager = APIKeyManager(keys_file) 174 all_keys = manager.list_keys() 175 176 if not all_keys: 177 dim("No local API keys found.") 178 return 179 180 # Filter keys based on options 181 keys_to_sync = all_keys 182 183 if key_ids: 184 # Filter to specified key IDs 185 requested_ids = {k.strip() for k in key_ids.split(",")} 186 keys_to_sync = [k for k in all_keys if k.key_id in requested_ids] 187 188 # Warn about missing keys 189 found_ids = {k.key_id for k in keys_to_sync} 190 missing_ids = requested_ids - found_ids 191 if missing_ids: 192 warning(f"Keys not found: {', '.join(missing_ids)}") 193 194 if not keys_to_sync: 195 error("No matching keys found", exit_code=1) 196 197 elif interactive: 198 # Interactive selection 199 console.print("\n[bold]Select keys to sync:[/bold]\n") 200 keys_to_sync = [] 201 202 for key in all_keys: 203 status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]" 204 console.print(f" [dim]{key.key_id}[/dim] - [cyan]{key.name}[/cyan] ({status})") 205 206 if typer.confirm(" Sync this key?", default=True): 207 keys_to_sync.append(key) 208 209 console.print() 210 211 if not keys_to_sync: 212 dim("No keys selected.") 213 return 214 215 # Convert to dicts for API 216 keys_data = [key.to_dict() for key in keys_to_sync] 217 218 # Sync to cloud 219 try: 220 client = GeronimoCloudClient() 221 result = client.sync_keys(keys_data) 222 223 synced = result.get("synced", 0) 224 skipped = result.get("skipped", 0) 225 226 console.print(f"\n[bold green]✓ Keys synced to Geronimo Cloud[/bold green]") 227 console.print(f" Synced: [green]{synced}[/green]") 228 if skipped: 229 console.print(f" Skipped: [yellow]{skipped}[/yellow] (cloud-managed keys take precedence)") 230 console.print() 231 232 except RuntimeError as e: 233 error(str(e), exit_code=1) 234 except Exception as e: 235 error(f"Failed to sync keys: {e}", exit_code=1)
Sync local API keys to Geronimo Cloud.
Uploads your local API keys to Geronimo Cloud so they can be used for authenticating requests to cloud-deployed endpoints.
Cloud-managed keys (created via dashboard) take precedence and won't be overwritten by synced keys.