Skip to content

Commit 39189e8

Browse files
author
Dylan Huang
committed
Add confirmation and management for existing secrets in upload process
- Implemented functions to check for existing secrets on Fireworks and confirm overwriting them with user prompts. - Updated the secret selection process to deselect existing secrets by default, enhancing user experience and preventing accidental overwrites. - Refactored the `upload_secrets_to_fireworks` function to incorporate these changes, ensuring a safer and more intuitive secret management workflow.
1 parent bce68c8 commit 39189e8

1 file changed

Lines changed: 170 additions & 15 deletions

File tree

eval_protocol/cli_commands/upload.py

Lines changed: 170 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import re
55
import sys
66
from pathlib import Path
7-
from typing import Any, Dict
7+
from typing import Any, Dict, Set
88

99
from eval_protocol.auth import get_fireworks_api_key
10-
from eval_protocol.platform_api import create_or_update_fireworks_secret
10+
from eval_protocol.platform_api import create_or_update_fireworks_secret, get_fireworks_secret
1111

1212
from eval_protocol.evaluation import create_evaluation
1313
from .utils import (
@@ -167,13 +167,119 @@ def _mask_secret_value(value: str) -> str:
167167
return "<masked>"
168168

169169

170+
def _check_existing_secrets(
171+
account_id: str,
172+
secret_keys: list[str],
173+
) -> Set[str]:
174+
"""
175+
Check which secrets already exist on Fireworks.
176+
Returns a set of key names that already exist.
177+
"""
178+
existing: Set[str] = set()
179+
for key in secret_keys:
180+
try:
181+
secret = get_fireworks_secret(account_id=account_id, key_name=key)
182+
if secret is not None:
183+
existing.add(key)
184+
except Exception:
185+
# If we can't check, assume it doesn't exist
186+
pass
187+
return existing
188+
189+
190+
def _confirm_overwrite_secrets(
191+
secrets_to_overwrite: list[str],
192+
non_interactive: bool,
193+
) -> bool:
194+
"""
195+
Prompt user to confirm overwriting existing secrets with double confirmation.
196+
Returns True if user confirms both prompts, False otherwise.
197+
"""
198+
if not secrets_to_overwrite:
199+
return True
200+
201+
if non_interactive:
202+
return True
203+
204+
if not sys.stdin.isatty():
205+
return True
206+
207+
print(
208+
f"\n⚠️ The following {len(secrets_to_overwrite)} secret(s) already exist on Fireworks and will be overwritten:"
209+
)
210+
for key in secrets_to_overwrite:
211+
print(f" • {key}")
212+
213+
print("\n" + "=" * 70)
214+
print("⚠️ WARNING: Overwriting secrets may affect running jobs!")
215+
print("=" * 70)
216+
print("Make sure any new or existing evaluator jobs will work with the new")
217+
print("secret values before proceeding. Existing RFT jobs and evaluators that")
218+
print("depend on these secrets may fail if the new values are incompatible.")
219+
print("=" * 70)
220+
221+
try:
222+
import questionary
223+
224+
custom_style = _get_questionary_style()
225+
226+
# First confirmation
227+
confirm1 = questionary.confirm(
228+
"Do you want to overwrite these existing secrets?",
229+
default=False,
230+
style=custom_style,
231+
).ask()
232+
233+
if confirm1 is None or not confirm1:
234+
print("\nSecret overwrite cancelled.")
235+
return False
236+
237+
# Second confirmation
238+
confirm2 = questionary.confirm(
239+
"Are you SURE? This may break existing jobs using these secrets.",
240+
default=False,
241+
style=custom_style,
242+
).ask()
243+
244+
if confirm2 is None or not confirm2:
245+
print("\nSecret overwrite cancelled.")
246+
return False
247+
248+
return True
249+
250+
except ImportError:
251+
# Fallback to simple text-based confirmation
252+
try:
253+
print("\nFirst confirmation:")
254+
response1 = input("Type 'yes' to confirm overwrite: ").strip().lower()
255+
if response1 != "yes":
256+
print("Secret overwrite cancelled.")
257+
return False
258+
259+
print("\nSecond confirmation:")
260+
response2 = input("Type 'yes' again to confirm (this may break existing jobs): ").strip().lower()
261+
if response2 != "yes":
262+
print("Secret overwrite cancelled.")
263+
return False
264+
265+
return True
266+
except KeyboardInterrupt:
267+
print("\nSecret upload cancelled.")
268+
return False
269+
except KeyboardInterrupt:
270+
print("\n\nSecret upload cancelled.")
271+
return False
272+
273+
170274
def _prompt_select_secrets(
171275
secrets: Dict[str, str],
172276
secrets_from_env_file: Dict[str, str],
277+
existing_secrets: Set[str],
173278
non_interactive: bool,
174279
) -> Dict[str, str]:
175280
"""
176281
Prompt user to select which environment variables to upload as secrets.
282+
Existing secrets are shown but deselected by default.
177283
Returns the selected secrets.
178284
"""
179285
if not secrets:
@@ -192,17 +298,23 @@ def _prompt_select_secrets(
192298
custom_style = _get_questionary_style()
193299

194300
# Build choices with source info and masked values
301+
# Existing secrets are unchecked by default
195302
choices = []
196303
for key, value in secrets.items():
197304
source = ".env" if key in secrets_from_env_file else "env"
198305
masked = _mask_secret_value(value)
199-
label = f"{key} ({source}: {masked})"
200-
choices.append(questionary.Choice(title=label, value=key, checked=True))
306+
is_existing = key in existing_secrets
307+
status = " [exists]" if is_existing else ""
308+
label = f"{key}{status} ({source}: {masked})"
309+
# Existing secrets are unchecked by default
310+
choices.append(questionary.Choice(title=label, value=key, checked=not is_existing))
201311

202312
if len(choices) == 0:
203313
return {}
204314

205315
print("\nFound environment variables to upload as Fireworks secrets:")
316+
if existing_secrets:
317+
print("(Secrets marked [exists] are deselected by default to avoid overwriting)")
206318
selected_keys = questionary.checkbox(
207319
"Select secrets to upload:",
208320
choices=choices,
@@ -220,7 +332,7 @@ def _prompt_select_secrets(
220332

221333
except ImportError:
222334
# Fallback to simple text-based selection
223-
return _prompt_select_secrets_fallback(secrets, secrets_from_env_file)
335+
return _prompt_select_secrets_fallback(secrets, secrets_from_env_file, existing_secrets)
224336
except KeyboardInterrupt:
225337
print("\n\nSecret upload cancelled.")
226338
return {}
@@ -229,6 +341,7 @@ def _prompt_select_secrets(
229341
def _prompt_select_secrets_fallback(
230342
secrets: Dict[str, str],
231343
secrets_from_env_file: Dict[str, str],
344+
existing_secrets: Set[str],
232345
) -> Dict[str, str]:
233346
"""Fallback prompt selection for when questionary is not available."""
234347
print("\n" + "=" * 60)
@@ -237,12 +350,20 @@ def _prompt_select_secrets_fallback(
237350
print("\nTip: Install questionary for better UX: pip install questionary\n")
238351

239352
secret_list = list(secrets.items())
353+
new_secret_indices = []
240354
for idx, (key, value) in enumerate(secret_list, 1):
241355
source = ".env" if key in secrets_from_env_file else "env"
242356
masked = _mask_secret_value(value)
243-
print(f" [{idx}] {key} ({source}: {masked})")
357+
is_existing = key in existing_secrets
358+
status = " [exists]" if is_existing else ""
359+
print(f" [{idx}] {key}{status} ({source}: {masked})")
360+
if not is_existing:
361+
new_secret_indices.append(idx)
244362

245363
print("\n" + "=" * 60)
364+
if existing_secrets:
365+
print("Secrets marked [exists] already exist on Fireworks.")
366+
print(f"Default selection (new secrets only): {','.join(str(i) for i in new_secret_indices) or 'none'}")
246367
print("Enter numbers to select (comma-separated), 'all' for all, or 'none' to skip:")
247368

248369
try:
@@ -251,7 +372,15 @@ def _prompt_select_secrets_fallback(
251372
print("\nSecret upload cancelled.")
252373
return {}
253374

254-
if not choice or choice == "none":
375+
if not choice:
376+
# Default: select only new secrets
377+
selected = {}
378+
for idx in new_secret_indices:
379+
key, value = secret_list[idx - 1]
380+
selected[key] = value
381+
return selected
382+
383+
if choice == "none":
255384
return {}
256385

257386
if choice == "all":
@@ -280,8 +409,10 @@ def upload_secrets_to_fireworks(
280409
281410
This function:
282411
1. Loads secrets from the specified .env file (or default .env in root)
283-
2. Prompts user to select which secrets to upload (unless non_interactive)
284-
3. Creates/updates the selected secrets on Fireworks
412+
2. Checks which secrets already exist on Fireworks
413+
3. Prompts user to select which secrets to upload (existing secrets are deselected by default)
414+
4. Confirms before overwriting any existing secrets
415+
5. Creates/updates the selected secrets on Fireworks
285416
286417
Args:
287418
root: The project root directory
@@ -307,32 +438,56 @@ def upload_secrets_to_fireworks(
307438
secrets_from_file["FIREWORKS_API_KEY"] = fw_api_key_value
308439

309440
if fw_account_id and secrets_from_file:
441+
print(f"\n🔐 Managing secrets for Fireworks account: {fw_account_id}")
310442
if secrets_from_env_file and os.path.exists(env_file_path):
311443
print(f"Loading secrets from: {env_file_path}")
312444

445+
# Check which secrets already exist on Fireworks
446+
print("Checking existing secrets on Fireworks...")
447+
existing_secrets = _check_existing_secrets(
448+
account_id=fw_account_id,
449+
secret_keys=list(secrets_from_file.keys()),
450+
)
451+
if existing_secrets:
452+
print(f"Found {len(existing_secrets)} existing secret(s): {', '.join(sorted(existing_secrets))}")
453+
313454
# Prompt user to select which secrets to upload
455+
# Existing secrets are deselected by default
314456
selected_secrets = _prompt_select_secrets(
315457
secrets_from_file,
316458
secrets_from_env_file,
459+
existing_secrets,
317460
non_interactive,
318461
)
319462

320463
if selected_secrets:
464+
# Check if any selected secrets already exist and need confirmation
465+
secrets_to_overwrite = [k for k in selected_secrets.keys() if k in existing_secrets]
466+
if secrets_to_overwrite:
467+
if not _confirm_overwrite_secrets(secrets_to_overwrite, non_interactive):
468+
# User declined to overwrite - remove existing secrets from selection
469+
selected_secrets = {k: v for k, v in selected_secrets.items() if k not in existing_secrets}
470+
if not selected_secrets:
471+
print("No new secrets to upload.")
472+
return
473+
print(f"\nProceeding with {len(selected_secrets)} new secret(s) only...")
474+
321475
print(f"\nUploading {len(selected_secrets)} selected secret(s) to Fireworks...")
322476
for secret_name, secret_value in selected_secrets.items():
323477
source = ".env" if secret_name in secrets_from_env_file else "environment"
324-
print(
325-
f"Ensuring {secret_name} is registered as a secret on Fireworks for rollout... "
326-
f"({source}: {_mask_secret_value(secret_value)})"
327-
)
478+
is_overwrite = secret_name in existing_secrets
479+
action = "Overwriting" if is_overwrite else "Creating"
480+
print(f"{action} {secret_name} on Fireworks... ({source}: {_mask_secret_value(secret_value)})")
328481
if create_or_update_fireworks_secret(
329482
account_id=fw_account_id,
330483
key_name=secret_name,
331484
secret_value=secret_value,
332485
):
333-
print(f"✓ {secret_name} secret created/updated on Fireworks.")
486+
print(f"✓ {secret_name} secret {'updated' if is_overwrite else 'created'} on Fireworks.")
334487
else:
335-
print(f"Warning: Failed to create/update {secret_name} secret on Fireworks.")
488+
print(
489+
f"Warning: Failed to {'update' if is_overwrite else 'create'} {secret_name} secret on Fireworks."
490+
)
336491
else:
337492
print("No secrets selected for upload.")
338493
else:

0 commit comments

Comments
 (0)