Skip to content

Commit f013bb6

Browse files
authored
fix: prevent deletion of plans that are still used in composite plans (#2993)
1 parent 493f4c5 commit f013bb6

File tree

4 files changed

+55
-2
lines changed

4 files changed

+55
-2
lines changed

renku/core/workflow/plan.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,19 @@ def remove_plan(name_or_id: str, force: bool, plan_gateway: IPlanGateway, when:
151151
if latest_version.deleted:
152152
raise errors.ParameterError(f"The specified workflow '{name_or_id}' is already deleted.")
153153

154+
composites_containing_child = get_composite_plans_by_child(plan)
155+
156+
if composites_containing_child:
157+
composite_names = "\n\t".join([c.name for c in composites_containing_child])
158+
159+
if not force:
160+
raise errors.ParameterError(
161+
f"The specified workflow '{name_or_id}' is part of the following composite workflows and won't be "
162+
f"removed (use '--force' to remove anyways):\n\t{composite_names}"
163+
)
164+
else:
165+
communication.warn(f"Removing '{name_or_id}', which is still used in these workflows:\n\t{composite_names}")
166+
154167
if not force:
155168
prompt_text = f"You are about to remove the following workflow '{name_or_id}'.\n\nDo you wish to continue?"
156169
communication.confirm(prompt_text, abort=True, warning=True)
@@ -632,3 +645,16 @@ def is_plan_removed(plan: AbstractPlan) -> bool:
632645
return True
633646

634647
return False
648+
649+
650+
@inject.autoparams()
651+
def get_composite_plans_by_child(plan: AbstractPlan, plan_gateway: IPlanGateway) -> List[CompositePlan]:
652+
"""Return all composite plans that contain a child plan."""
653+
654+
derivatives = {p.id for p in get_derivative_chain(plan=plan)}
655+
656+
composites = (p for p in plan_gateway.get_newest_plans_by_names().values() if isinstance(p, CompositePlan))
657+
658+
composites_containing_child = [c for c in composites if {p.id for p in c.plans}.intersection(derivatives)]
659+
660+
return composites_containing_child

renku/ui/cli/workflow.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,9 @@ def remove(name, force):
806806
"""Remove a workflow named <name>."""
807807
from renku.command.workflow import remove_plan_command
808808

809-
remove_plan_command().build().execute(name_or_id=name, force=force)
809+
communicator = ClickCallback()
810+
811+
remove_plan_command().with_communicator(communicator).build().execute(name_or_id=name, force=force)
810812

811813

812814
@workflow.command()

tests/api/test_plan.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_list_plans(client_with_runs):
3636

3737
def test_list_deleted_plans(client_with_runs, runner):
3838
"""Test listing deleted plans."""
39-
result = runner.invoke(cli, ["workflow", "remove", "plan-1"])
39+
result = runner.invoke(cli, ["workflow", "remove", "--force", "plan-1"])
4040
assert 0 == result.exit_code, format_result_exception(result)
4141

4242
plans = Plan.list()

tests/cli/test_workflow.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,31 @@ def test_workflow_remove_command(runner, project):
369369
assert 2 == result.exit_code, format_result_exception(result)
370370

371371

372+
def test_workflow_remove_with_composite_command(runner, project):
373+
"""Test workflow remove with builder."""
374+
workflow_name = "test_workflow"
375+
376+
result = runner.invoke(cli, ["workflow", "remove", workflow_name])
377+
assert 2 == result.exit_code
378+
379+
result = runner.invoke(cli, ["run", "--success-code", "0", "--no-output", "--name", workflow_name, "echo", "foo"])
380+
assert 0 == result.exit_code, format_result_exception(result)
381+
382+
result = runner.invoke(cli, ["workflow", "compose", "composed-workflow", workflow_name])
383+
assert 0 == result.exit_code, format_result_exception(result)
384+
385+
result = runner.invoke(cli, ["workflow", "remove", workflow_name])
386+
assert 2 == result.exit_code, format_result_exception(result)
387+
assert (
388+
"The specified workflow 'test_workflow' is part of the following composite workflows and won't be removed"
389+
in result.stderr
390+
)
391+
392+
result = runner.invoke(cli, ["workflow", "remove", "--force", workflow_name])
393+
assert 0 == result.exit_code, format_result_exception(result)
394+
assert "Removing 'test_workflow', which is still used in these workflows" in result.output
395+
396+
372397
def test_workflow_export_command(runner, project):
373398
"""Test workflow export with builder."""
374399
result = runner.invoke(cli, ["run", "--success-code", "0", "--no-output", "--name", "run1", "touch", "data.csv"])

0 commit comments

Comments
 (0)