Skip to content

Commit 25e6cb6

Browse files
author
Travis Wrightsman
authored
feat(grzctl): confirm before updating submission from error state (#357)
Resolves #318 fix(grz-db): allow empty author private key passphrases
1 parent 6c3b62a commit 25e6cb6

3 files changed

Lines changed: 56 additions & 3 deletions

File tree

packages/grz-db/src/grz_db/models/author.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def private_key(self) -> Ed25519PrivateKey:
2121
from cryptography.hazmat.primitives.serialization import load_ssh_private_key
2222

2323
passphrase = self.private_key_passphrase
24-
if passphrase:
24+
if passphrase is not None:
2525
passphrase_callback = lambda: passphrase
2626
else:
2727
passphrase_callback = partial(getpass, prompt=f"Passphrase for GRZ DB author ({self.name}'s) private key: ")

packages/grzctl/src/grzctl/commands/db.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,9 @@ def add(ctx: click.Context, submission_id: str):
378378
@click.argument("submission_id", type=str)
379379
@click.argument("state_str", metavar="STATE", type=click.Choice(SubmissionStateEnum.list(), case_sensitive=False))
380380
@click.option("--data", "data_json", type=str, default=None, help='Additional JSON data (e.g., \'{"k":"v"}\').')
381+
@click.option("--ignore-error-state/--confirm-error-state")
381382
@click.pass_context
382-
def update(ctx: click.Context, submission_id: str, state_str: str, data_json: str | None):
383+
def update(ctx: click.Context, submission_id: str, state_str: str, data_json: str | None, ignore_error_state: bool): # noqa: C901
383384
"""Update a submission to the given state. Optionally accepts additional JSON data to associate with the log entry."""
384385
db = ctx.obj["db_url"]
385386
db_service = get_submission_db_instance(db, author=ctx.obj["author"])
@@ -397,6 +398,23 @@ def update(ctx: click.Context, submission_id: str, state_str: str, data_json: st
397398
console_err.print(f"[red]Error: Invalid JSON string for --data: {data_json}[/red]")
398399
raise click.Abort() from e
399400
try:
401+
submission = db_service.get_submission(submission_id)
402+
if not submission:
403+
raise SubmissionNotFoundError(submission_id)
404+
latest_state = submission.get_latest_state()
405+
latest_state_is_error = latest_state is not None and latest_state.state == SubmissionStateEnum.ERROR
406+
if (
407+
latest_state_is_error
408+
and not ignore_error_state
409+
and not click.confirm(
410+
f"Submission is currently in an 'Error' state. Are you sure you want to set it to '{state_enum}'?",
411+
default=False,
412+
show_default=True,
413+
)
414+
):
415+
console_err.print(f"[yellow]Not modifying state of errored submission '{submission_id}'.[/yellow]")
416+
ctx.exit()
417+
400418
new_state_log = db_service.update_submission_state(submission_id, state_enum, parsed_data)
401419
console_err.print(
402420
f"[green]Submission '{submission_id}' updated to state '{new_state_log.state.value}'. Log ID: {new_state_log.id}[/green]"
@@ -408,6 +426,9 @@ def update(ctx: click.Context, submission_id: str, state_str: str, data_json: st
408426
console_err.print(f"[red]Error: {e}[/red]")
409427
console_err.print(f"You might need to add it first: grz-cli db submission add {submission_id}")
410428
raise click.Abort() from e
429+
except click.exceptions.Exit as e:
430+
if e.exit_code != 0:
431+
raise e
411432
except Exception as e:
412433
console_err.print(f"[red]An unexpected error occurred: {e}[/red]")
413434
traceback.print_exc()

packages/grzctl/tests/cli/test_db.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ def blank_database_config(tmp_path: Path) -> DbConfig:
4444
return DbConfig(
4545
db={
4646
"database_url": "sqlite:///" + str((tmp_path / "submission.db.sqlite").resolve()),
47-
"author": {"name": "alice", "private_key_path": str(private_key_path.resolve())},
47+
"author": {
48+
"name": "alice",
49+
"private_key_path": str(private_key_path.resolve()),
50+
"private_key_passphrase": "",
51+
},
4852
"known_public_keys": str(public_key_path.resolve()),
4953
}
5054
)
@@ -157,3 +161,31 @@ def test_populate_redacted(tmp_path: Path, blank_database_config_path: Path):
157161
[*args_common, "submission", "populate", submission_id, str(metadata_path), "--no-confirm"],
158162
catch_exceptions=False,
159163
)
164+
165+
166+
def test_update_error_confirm(blank_database_config_path: Path):
167+
"""Database should confirm before updating a submission from an Error state."""
168+
args_common = ["db", "--config-file", blank_database_config_path]
169+
metadata = GrzSubmissionMetadata.model_validate_json(
170+
(importlib.resources.files(test_resources) / "metadata.json").read_text()
171+
)
172+
173+
runner = click.testing.CliRunner()
174+
cli = grzctl.cli.build_cli()
175+
result_add = runner.invoke(cli, [*args_common, "submission", "add", metadata.submission_id])
176+
assert result_add.exit_code == 0, result_add.stderr
177+
178+
result_update1 = runner.invoke(cli, [*args_common, "submission", "update", metadata.submission_id, "Error"])
179+
assert result_update1.exit_code == 0, result_update1.output
180+
181+
result_update2 = runner.invoke(cli, [*args_common, "submission", "update", metadata.submission_id, "Validated"])
182+
assert result_update2.exit_code != 0, result_update2.output
183+
assert (
184+
"Submission is currently in an 'Error' state. Are you sure you want to set it to 'Validated'?"
185+
in result_update2.output
186+
)
187+
188+
result_update3 = runner.invoke(
189+
cli, [*args_common, "submission", "update", "--ignore-error-state", metadata.submission_id, "Validated"]
190+
)
191+
assert result_update3.exit_code == 0, result_update3.output

0 commit comments

Comments
 (0)