Skip to content

Commit 64aca0c

Browse files
ritwik-gclaude
andcommitted
Implement Task 3.6: Add confirmation for existing values
Enhanced values set and set-secret commands with overwrite protection: Features: - Check for existing values before setting new ones - Show current value (masked for secrets) when confirming overwrite - Prompt "Value already exists. Overwrite? [y/N]" with safe default - Special handling for secrets showing "env:VAR_NAME" format - Warning when overwriting regular values with secrets - --force flag to bypass confirmation prompts on both commands User Experience: - Prevents accidental overwrites with confirmation prompts - Clear display of current values for informed decisions - Graceful cancellation with proper exit handling - Consistent behavior across set and set-secret commands Safety: - Default "No" on confirmation prompts - Force flag available for automation/scripting - Proper masking of sensitive information - Comprehensive test coverage for all scenarios 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 47940f8 commit 64aca0c

File tree

3 files changed

+249
-1
lines changed

3 files changed

+249
-1
lines changed

guide/prompt_plan.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,23 @@ Update `values set-secret` command to:
137137
```
138138
**Status**: Completed - Extensible menu with validation framework
139139

140+
#### Task 3.6: Add Confirmation for Existing Values ✅
141+
```prompt
142+
Update both `values set` and `values set-secret` commands to:
143+
1. Check if the key already exists in the specified environment
144+
2. If exists:
145+
a. Show current value (masked for secrets)
146+
b. Prompt: "Value already exists. Overwrite? [y/N]"
147+
c. Abort if not confirmed
148+
3. For `values set-secret`:
149+
a. Show both current type and name if available
150+
b. Example prompt:
151+
"Key 'database-password' already set as env:PROD_DB_PASSWORD
152+
Overwrite? [y/N]"
153+
4. Add --force flag to bypass confirmation
154+
```
155+
**Status**: Completed - Confirmation prompts with --force flag bypass
156+
140157
### Phase 4: Core Engine
141158
#### Task 4.1: Validator
142159
```prompt

helm_values_manager/commands/values.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def set_command(
3333
env: str = typer.Option(..., "--env", "-e", help="Environment name"),
3434
schema_path: str = typer.Option("schema.json", "--schema", help="Path to schema file"),
3535
values_path: Optional[str] = typer.Option(None, "--values", help="Path to values file"),
36+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation for existing values"),
3637
):
3738
"""Set a value for a specific environment."""
3839
# Load schema
@@ -62,6 +63,26 @@ def set_command(
6263
# Load existing values
6364
values = load_values(env, values_path)
6465

66+
# Check if key already exists and confirm overwrite
67+
if key in values and not force:
68+
current_value = values[key]
69+
70+
# Display current value
71+
if is_secret_reference(current_value):
72+
console.print(f"Key '{key}' already set as [red][SECRET - {current_value['name']}][/red]")
73+
else:
74+
if isinstance(current_value, (dict, list)):
75+
display_value = json.dumps(current_value)
76+
if len(display_value) > 50:
77+
display_value = display_value[:47] + "..."
78+
else:
79+
display_value = str(current_value)
80+
console.print(f"Key '{key}' already set to: {display_value}")
81+
82+
if not Confirm.ask("Value already exists. Overwrite?", default=False):
83+
console.print("Cancelled")
84+
raise typer.Exit(0)
85+
6586
# Set the value
6687
values[key] = parsed_value
6788

@@ -77,6 +98,7 @@ def set_secret_command(
7798
env: str = typer.Option(..., "--env", "-e", help="Environment name"),
7899
schema_path: str = typer.Option("schema.json", "--schema", help="Path to schema file"),
79100
values_path: Optional[str] = typer.Option(None, "--values", help="Path to values file"),
101+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation for existing values"),
80102
):
81103
"""Set a secret value for a specific environment."""
82104
# Load schema
@@ -117,6 +139,28 @@ def set_secret_command(
117139
# Load existing values
118140
values = load_values(env, values_path)
119141

142+
# Check if key already exists and confirm overwrite
143+
if key in values and not force:
144+
current_value = values[key]
145+
146+
# Display current value
147+
if is_secret_reference(current_value):
148+
console.print(f"Key '{key}' already set as [red]{current_value['type']}:{current_value['name']}[/red]")
149+
else:
150+
# Show non-secret value (shouldn't happen for set-secret, but handle gracefully)
151+
if isinstance(current_value, (dict, list)):
152+
display_value = json.dumps(current_value)
153+
if len(display_value) > 50:
154+
display_value = display_value[:47] + "..."
155+
else:
156+
display_value = str(current_value)
157+
console.print(f"Key '{key}' currently set to: {display_value}")
158+
console.print("[yellow]Warning:[/yellow] This will overwrite a non-secret value with a secret")
159+
160+
if not Confirm.ask("Overwrite?", default=False):
161+
console.print("Cancelled")
162+
raise typer.Exit(0)
163+
120164
# Set the secret reference
121165
values[key] = {"type": "env", "name": env_var_name}
122166
else:

tests/test_values.py

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,4 +398,191 @@ def test_validate_secret_reference():
398398
not_secret = "plain-value"
399399
is_valid, error = validate_secret_reference(not_secret)
400400
assert not is_valid
401-
assert "Not a valid secret reference" in error
401+
assert "Not a valid secret reference" in error
402+
403+
404+
def test_values_set_existing_value_confirmation(tmp_path):
405+
"""Test confirmation when overwriting existing value."""
406+
with runner.isolated_filesystem(temp_dir=tmp_path):
407+
schema = create_test_schema()
408+
with open("schema.json", "w") as f:
409+
json.dump(schema.model_dump(), f)
410+
411+
# Set initial value
412+
result = runner.invoke(app, ["values", "set", "app-name", "initial-app", "--env", "dev"])
413+
assert result.exit_code == 0
414+
415+
# Try to overwrite without force, but cancel
416+
result = runner.invoke(app, ["values", "set", "app-name", "new-app", "--env", "dev"], input="n\n")
417+
418+
assert result.exit_code == 0
419+
assert "already set to: initial-app" in result.output
420+
assert "Value already exists. Overwrite?" in result.output
421+
assert "Cancelled" in result.output
422+
423+
# Verify value wasn't changed
424+
with open("values-dev.json") as f:
425+
values = json.load(f)
426+
assert values["app-name"] == "initial-app"
427+
428+
429+
def test_values_set_existing_value_overwrite(tmp_path):
430+
"""Test overwriting existing value with confirmation."""
431+
with runner.isolated_filesystem(temp_dir=tmp_path):
432+
schema = create_test_schema()
433+
with open("schema.json", "w") as f:
434+
json.dump(schema.model_dump(), f)
435+
436+
# Set initial value
437+
result = runner.invoke(app, ["values", "set", "app-name", "initial-app", "--env", "dev"])
438+
assert result.exit_code == 0
439+
440+
# Overwrite with confirmation
441+
result = runner.invoke(app, ["values", "set", "app-name", "new-app", "--env", "dev"], input="y\n")
442+
443+
assert result.exit_code == 0
444+
assert "already set to: initial-app" in result.output
445+
assert "Set 'app-name' = new-app" in result.output
446+
447+
# Verify value was changed
448+
with open("values-dev.json") as f:
449+
values = json.load(f)
450+
assert values["app-name"] == "new-app"
451+
452+
453+
def test_values_set_existing_value_force(tmp_path):
454+
"""Test overwriting existing value with --force flag."""
455+
with runner.isolated_filesystem(temp_dir=tmp_path):
456+
schema = create_test_schema()
457+
with open("schema.json", "w") as f:
458+
json.dump(schema.model_dump(), f)
459+
460+
# Set initial value
461+
result = runner.invoke(app, ["values", "set", "app-name", "initial-app", "--env", "dev"])
462+
assert result.exit_code == 0
463+
464+
# Overwrite with force (no confirmation)
465+
result = runner.invoke(app, ["values", "set", "app-name", "forced-app", "--env", "dev", "--force"])
466+
467+
assert result.exit_code == 0
468+
assert "already set" not in result.output # No confirmation shown
469+
assert "Set 'app-name' = forced-app" in result.output
470+
471+
# Verify value was changed
472+
with open("values-dev.json") as f:
473+
values = json.load(f)
474+
assert values["app-name"] == "forced-app"
475+
476+
477+
def test_values_set_secret_existing_confirmation(tmp_path, monkeypatch):
478+
"""Test confirmation when overwriting existing secret."""
479+
with runner.isolated_filesystem(temp_dir=tmp_path):
480+
schema = create_test_schema()
481+
with open("schema.json", "w") as f:
482+
json.dump(schema.model_dump(), f)
483+
484+
# Set environment variables
485+
monkeypatch.setenv("OLD_PASSWORD", "old_secret")
486+
monkeypatch.setenv("NEW_PASSWORD", "new_secret")
487+
488+
# Set initial secret
489+
result = runner.invoke(app, ["values", "set-secret", "db-password", "--env", "dev"], input="1\nOLD_PASSWORD\n")
490+
assert result.exit_code == 0
491+
492+
# Try to overwrite but cancel
493+
result = runner.invoke(app, ["values", "set-secret", "db-password", "--env", "dev"], input="1\nNEW_PASSWORD\nn\n")
494+
495+
assert result.exit_code == 0
496+
assert "already set as env:OLD_PASSWORD" in result.output
497+
assert "Overwrite?" in result.output
498+
assert "Cancelled" in result.output
499+
500+
# Verify secret wasn't changed
501+
with open("values-dev.json") as f:
502+
values = json.load(f)
503+
assert values["db-password"]["name"] == "OLD_PASSWORD"
504+
505+
506+
def test_values_set_secret_existing_overwrite(tmp_path, monkeypatch):
507+
"""Test overwriting existing secret with confirmation."""
508+
with runner.isolated_filesystem(temp_dir=tmp_path):
509+
schema = create_test_schema()
510+
with open("schema.json", "w") as f:
511+
json.dump(schema.model_dump(), f)
512+
513+
# Set environment variables
514+
monkeypatch.setenv("OLD_PASSWORD", "old_secret")
515+
monkeypatch.setenv("NEW_PASSWORD", "new_secret")
516+
517+
# Set initial secret
518+
result = runner.invoke(app, ["values", "set-secret", "db-password", "--env", "dev"], input="1\nOLD_PASSWORD\n")
519+
assert result.exit_code == 0
520+
521+
# Overwrite with confirmation
522+
result = runner.invoke(app, ["values", "set-secret", "db-password", "--env", "dev"], input="1\nNEW_PASSWORD\ny\n")
523+
524+
assert result.exit_code == 0
525+
assert "already set as env:OLD_PASSWORD" in result.output
526+
assert "Set secret 'db-password'" in result.output
527+
528+
# Verify secret was changed
529+
with open("values-dev.json") as f:
530+
values = json.load(f)
531+
assert values["db-password"]["name"] == "NEW_PASSWORD"
532+
533+
534+
def test_values_set_secret_existing_force(tmp_path, monkeypatch):
535+
"""Test overwriting existing secret with --force flag."""
536+
with runner.isolated_filesystem(temp_dir=tmp_path):
537+
schema = create_test_schema()
538+
with open("schema.json", "w") as f:
539+
json.dump(schema.model_dump(), f)
540+
541+
# Set environment variables
542+
monkeypatch.setenv("OLD_PASSWORD", "old_secret")
543+
monkeypatch.setenv("NEW_PASSWORD", "new_secret")
544+
545+
# Set initial secret
546+
result = runner.invoke(app, ["values", "set-secret", "db-password", "--env", "dev"], input="1\nOLD_PASSWORD\n")
547+
assert result.exit_code == 0
548+
549+
# Overwrite with force (no confirmation)
550+
result = runner.invoke(app, ["values", "set-secret", "db-password", "--env", "dev", "--force"], input="1\nNEW_PASSWORD\n")
551+
552+
assert result.exit_code == 0
553+
assert "already set" not in result.output # No confirmation shown
554+
assert "Set secret 'db-password'" in result.output
555+
556+
# Verify secret was changed
557+
with open("values-dev.json") as f:
558+
values = json.load(f)
559+
assert values["db-password"]["name"] == "NEW_PASSWORD"
560+
561+
562+
def test_values_set_secret_overwrite_regular_value_warning(tmp_path, monkeypatch):
563+
"""Test warning when overwriting regular value with secret."""
564+
with runner.isolated_filesystem(temp_dir=tmp_path):
565+
schema = create_test_schema()
566+
with open("schema.json", "w") as f:
567+
json.dump(schema.model_dump(), f)
568+
569+
# Set environment variable
570+
monkeypatch.setenv("SECRET_PASSWORD", "secret")
571+
572+
# Set initial regular value (even though it should be secret)
573+
values = {"db-password": "plain-text-password"}
574+
with open("values-dev.json", "w") as f:
575+
json.dump(values, f)
576+
577+
# Try to overwrite with secret
578+
result = runner.invoke(app, ["values", "set-secret", "db-password", "--env", "dev"], input="1\nSECRET_PASSWORD\ny\n")
579+
580+
assert result.exit_code == 0
581+
assert "currently set to: plain-text-password" in result.output
582+
assert "This will overwrite a non-secret value with a secret" in result.output
583+
584+
# Verify it was changed to secret
585+
with open("values-dev.json") as f:
586+
values = json.load(f)
587+
assert values["db-password"]["type"] == "env"
588+
assert values["db-password"]["name"] == "SECRET_PASSWORD"

0 commit comments

Comments
 (0)