Skip to content

Commit 7ede2ab

Browse files
committed
Add periodic workflow and workflow run support
Mirror pulp_workflow PR #41 in the CLI/glue: - Add --dispatch-interval to 'workflow create' for recurring workflows. - 'workflow cancel' now stops a workflow (removes schedule, cancels in-flight runs) and is idempotent. - Add PulpWorkflowRunContext and a 'workflow run' command group (list/show/cancel) for the new workflow-runs resource. - Drop the workflow-level 'state' filter (state now lives on runs).
1 parent 13fe83b commit 7ede2ab

7 files changed

Lines changed: 135 additions & 23 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added `pulp workflow create --dispatch-interval` to schedule a workflow to re-run on a recurring
2+
interval, and a new `pulp workflow run` command group (`list`, `show`, `cancel`) to inspect and
3+
cancel the individual runs of a workflow. `pulp workflow cancel` now stops a workflow by removing
4+
its schedule and canceling any in-flight runs.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added `PulpWorkflowRunContext` for the new `workflow-runs` resource and a `dispatch_interval` field
2+
on workflow creation to support periodic (recurring) workflows.

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ A pulp-cli plugin for managing [Pulp workflows](https://github.com/daviddavis/pu
88
pulp workflow list
99
pulp workflow show --name <name>
1010
pulp workflow create --name <name> --task '<json>'
11+
pulp workflow create --name <name> --task '<json>' --dispatch-interval '1 00:00:00'
1112
pulp workflow cancel --name <name>
1213
pulp workflow label set --name <name> --key <key> --value <value>
14+
15+
# Inspect the individual runs (executions) of workflows
16+
pulp workflow run list --workflow <name>
17+
pulp workflow run show --href <href>
18+
pulp workflow run cancel --href <href>
1319
```
20+
21+
A workflow is a definition plus a schedule. Each time its schedule fires, a
22+
`WorkflowRun` records that execution. Set `--dispatch-interval` to re-run a
23+
workflow on a recurring schedule; otherwise it runs once at `--start-time`.
24+
`pulp workflow cancel` stops a workflow (removes its schedule and cancels any
25+
in-flight runs), while `pulp workflow run cancel` cancels a single run.

pulp-glue-workflow/src/pulp_glue/workflow/context.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class PulpWorkflowContext(PulpEntityContext):
1515
ENTITY = _("workflow")
1616
ENTITIES = _("workflows")
1717
HREF = "workflow_workflow_href"
18+
HREF_PATTERN = r"workflow/workflows/[^/]+/"
1819
ID_PREFIX = "workflow_workflows"
1920
NEEDS_PLUGINS = [PluginRequirement("workflow")]
2021
NULLABLES: t.ClassVar[set[str]] = set()
@@ -31,3 +32,22 @@ def cancel(self) -> t.Any:
3132
def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> EntityDefinition:
3233
body = super().preprocess_entity(body, partial=partial)
3334
return body
35+
36+
37+
class PulpWorkflowRunContext(PulpEntityContext):
38+
ENTITY = _("workflow run")
39+
ENTITIES = _("workflow runs")
40+
HREF = "workflow_workflow_run_href"
41+
HREF_PATTERN = r"workflow/workflow-runs/[^/]+/"
42+
ID_PREFIX = "workflow_workflow_runs"
43+
NEEDS_PLUGINS = [PluginRequirement("workflow")]
44+
NULLABLES: t.ClassVar[set[str]] = set()
45+
46+
CANCEL_ID = "workflow_runs_cancel"
47+
48+
def cancel(self) -> t.Any:
49+
return self.call(
50+
"cancel",
51+
parameters={self.HREF: self.pulp_href},
52+
body={"state": "canceled"},
53+
)

src/pulpcore/cli/workflow/__init__.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
name_option,
1111
pass_pulp_context,
1212
pulp_group,
13+
resource_option,
1314
show_command,
1415
)
1516

1617
from pulp_glue.common.i18n import get_translation
17-
from pulp_glue.workflow.context import PulpWorkflowContext
18+
from pulp_glue.workflow.context import PulpWorkflowContext, PulpWorkflowRunContext
1819

19-
from pulpcore.cli.workflow.workflow import cancel, create
20+
from pulpcore.cli.workflow.workflow import cancel, create, run_cancel
2021

2122
translation = get_translation(__package__)
2223
_ = translation.gettext
@@ -26,16 +27,28 @@
2627
lookup_options = [href_option, name_option]
2728
filter_options = [
2829
click.option("--name"),
29-
click.option(
30-
"--state",
31-
type=click.Choice(
32-
["waiting", "skipped", "running", "completed", "failed", "canceled"],
33-
case_sensitive=False,
34-
),
35-
),
3630
label_select_option,
3731
]
3832

33+
state_choice = click.Choice(
34+
["waiting", "skipped", "running", "completed", "failed", "canceled"],
35+
case_sensitive=False,
36+
)
37+
38+
workflow_option = resource_option(
39+
"--workflow",
40+
default_plugin="workflow",
41+
default_type="workflow",
42+
context_table={"workflow:workflow": PulpWorkflowContext},
43+
href_pattern=PulpWorkflowContext.HREF_PATTERN,
44+
help=_("Workflow to filter runs by, in the form <name> or by href."),
45+
)
46+
run_filter_options = [
47+
workflow_option,
48+
click.option("--state", type=state_choice),
49+
]
50+
run_lookup_options = [href_option]
51+
3952

4053
@pulp_group(name="workflow")
4154
@pass_pulp_context
@@ -44,10 +57,23 @@ def workflow_group(ctx: click.Context, pulp_ctx: PulpCLIContext, /) -> None:
4457
ctx.obj = PulpWorkflowContext(pulp_ctx)
4558

4659

60+
@pulp_group(name="run")
61+
@pass_pulp_context
62+
@click.pass_context
63+
def run_group(ctx: click.Context, pulp_ctx: PulpCLIContext, /) -> None:
64+
ctx.obj = PulpWorkflowRunContext(pulp_ctx)
65+
66+
67+
run_group.add_command(list_command(decorators=run_filter_options))
68+
run_group.add_command(show_command(decorators=run_lookup_options))
69+
run_group.add_command(run_cancel)
70+
71+
4772
def mount(main: click.Group, **kwargs: t.Any) -> None:
4873
workflow_group.add_command(list_command(decorators=filter_options))
4974
workflow_group.add_command(show_command(decorators=lookup_options))
5075
workflow_group.add_command(label_command(decorators=lookup_options))
5176
workflow_group.add_command(create)
5277
workflow_group.add_command(cancel)
78+
workflow_group.add_command(run_group)
5379
main.add_command(workflow_group)

src/pulpcore/cli/workflow/workflow.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from pulp_glue.common.context import DATETIME_FORMATS, PulpEntityContext
1616
from pulp_glue.common.i18n import get_translation
17-
from pulp_glue.workflow.context import PulpWorkflowContext
17+
from pulp_glue.workflow.context import PulpWorkflowContext, PulpWorkflowRunContext
1818

1919
translation = get_translation(__name__)
2020
_ = translation.gettext
@@ -27,7 +27,18 @@
2727
"start_time",
2828
default=None,
2929
type=click.DateTime(formats=DATETIME_FORMATS),
30-
help=_("ISO 8601 datetime for when the workflow should start. Defaults to now."),
30+
help=_("ISO 8601 datetime for when the workflow should first run. Defaults to now."),
31+
)
32+
@click.option(
33+
"--dispatch-interval",
34+
"dispatch_interval",
35+
default=None,
36+
type=click.STRING,
37+
help=_(
38+
"If set, the interval on which the workflow re-runs, creating a new run each time "
39+
"(e.g. '1 00:00:00' for daily or '01:00:00' for hourly). If omitted, the workflow "
40+
"runs exactly once at start-time."
41+
),
3142
)
3243
@click.option(
3344
"--task",
@@ -55,6 +66,7 @@ def create(
5566
/,
5667
name: str,
5768
start_time: t.Optional[datetime],
69+
dispatch_interval: t.Optional[str],
5870
tasks: tuple[str, ...],
5971
pulp_labels: tuple[str, ...],
6072
) -> None:
@@ -66,6 +78,9 @@ def create(
6678
if start_time is not None:
6779
body["start_time"] = start_time
6880

81+
if dispatch_interval is not None:
82+
body["dispatch_interval"] = dispatch_interval
83+
6984
if tasks:
7085
parsed_tasks = []
7186
for task_json in tasks:
@@ -100,14 +115,34 @@ def cancel(
100115
entity_ctx: PulpEntityContext,
101116
/,
102117
) -> None:
103-
"""Cancel a waiting or running workflow."""
118+
"""Stop a workflow.
119+
120+
Removes the workflow's schedule so no further runs are created and cancels any of its
121+
runs that are still in progress. This is idempotent.
122+
"""
104123
assert isinstance(entity_ctx, PulpWorkflowContext)
105124

125+
result = entity_ctx.cancel()
126+
pulp_ctx.output_result(result)
127+
128+
129+
@pulp_command(name="cancel")
130+
@href_option
131+
@pass_entity_context
132+
@pass_pulp_context
133+
def run_cancel(
134+
pulp_ctx: PulpCLIContext,
135+
entity_ctx: PulpEntityContext,
136+
/,
137+
) -> None:
138+
"""Cancel a waiting or running workflow run."""
139+
assert isinstance(entity_ctx, PulpWorkflowRunContext)
140+
106141
entity = entity_ctx.entity
107142
if entity["state"] not in ("waiting", "running"):
108143
raise click.ClickException(
109-
_("Workflow '{name}' is in state '{state}' and cannot be canceled.").format(
110-
name=entity["name"], state=entity["state"]
144+
_("Workflow run '{href}' is in state '{state}' and cannot be canceled.").format(
145+
href=entity["pulp_href"], state=entity["state"]
111146
)
112147
)
113148
result = entity_ctx.cancel()

tests/scripts/pulp_workflow/test_workflow.sh

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ set -eu
88
WORKFLOW_NAME="test_cli_workflow_$$"
99
CANCEL_NAME="test_cli_workflow_cancel_$$"
1010
FUTURE_NAME="test_cli_workflow_future_$$"
11+
PERIODIC_NAME="test_cli_workflow_periodic_$$"
1112

1213
cleanup() {
1314
pulp workflow cancel --name "${WORKFLOW_NAME}" 2>/dev/null || true
1415
pulp workflow cancel --name "${CANCEL_NAME}" 2>/dev/null || true
1516
pulp workflow cancel --name "${FUTURE_NAME}" 2>/dev/null || true
17+
pulp workflow cancel --name "${PERIODIC_NAME}" 2>/dev/null || true
1618
}
1719
trap cleanup EXIT
1820

@@ -36,9 +38,6 @@ assert "$(echo "$OUTPUT" | jq -r '.pulp_labels.test_key')" = "test_value"
3638
expect_succ pulp workflow list --name "${WORKFLOW_NAME}"
3739
assert "$(echo "$OUTPUT" | jq -r '.[0].name')" = "${WORKFLOW_NAME}"
3840

39-
# Test: list with state filter
40-
expect_succ pulp workflow list --state waiting
41-
4241
# Test: label set
4342
expect_succ pulp workflow label set --name "${WORKFLOW_NAME}" --key "env" --value "ci"
4443
expect_succ pulp workflow show --name "${WORKFLOW_NAME}"
@@ -49,17 +48,31 @@ expect_succ pulp workflow label unset --name "${WORKFLOW_NAME}" --key "env"
4948
expect_succ pulp workflow show --name "${WORKFLOW_NAME}"
5049
assert "$(echo "$OUTPUT" | jq -r '.pulp_labels | has("env")')" = "false"
5150

52-
# Test: cancel a waiting workflow
51+
# Test: list the runs of a workflow (may be empty, but must be valid JSON)
52+
expect_succ pulp workflow run list --workflow "${WORKFLOW_NAME}"
53+
assert "$OUTPUT" != "null"
54+
55+
# Test: create a periodic workflow with --dispatch-interval
56+
expect_succ pulp workflow create \
57+
--name "${PERIODIC_NAME}" \
58+
--dispatch-interval "01:00:00" \
59+
--task '{"task_name": "pulpcore.app.tasks.base.general_create", "task_args": [], "task_kwargs": []}'
60+
expect_succ pulp workflow show --name "${PERIODIC_NAME}"
61+
assert "$(echo "$OUTPUT" | jq -r '.name')" = "${PERIODIC_NAME}"
62+
# Stopping a periodic workflow halts its schedule.
63+
expect_succ pulp workflow cancel --name "${PERIODIC_NAME}"
64+
65+
# Test: stop a workflow scheduled in the future
5366
expect_succ pulp workflow create \
5467
--name "${CANCEL_NAME}" \
5568
--task '{"task_name": "pulpcore.app.tasks.base.general_create", "task_args": [], "task_kwargs": []}' \
5669
--start-time "2099-01-01T00:00:00"
5770
expect_succ pulp workflow cancel --name "${CANCEL_NAME}"
71+
# Stopping is idempotent: a second stop still succeeds.
72+
expect_succ pulp workflow cancel --name "${CANCEL_NAME}"
73+
# The workflow definition is still readable after being stopped.
5874
expect_succ pulp workflow show --name "${CANCEL_NAME}"
59-
assert "$(echo "$OUTPUT" | jq -r '.state')" = "canceled"
60-
61-
# Test: canceling an already-canceled workflow should fail
62-
expect_fail pulp workflow cancel --name "${CANCEL_NAME}"
75+
assert "$(echo "$OUTPUT" | jq -r '.name')" = "${CANCEL_NAME}"
6376

6477
# Test: create with --start-time in the future
6578
FUTURE_NAME="test_cli_workflow_future_$$"
@@ -68,7 +81,7 @@ expect_succ pulp workflow create \
6881
--task '{"task_name": "pulpcore.app.tasks.base.general_create", "task_args": [], "task_kwargs": []}' \
6982
--start-time "2099-01-01T00:00:00"
7083
expect_succ pulp workflow show --name "${FUTURE_NAME}"
71-
assert "$(echo "$OUTPUT" | jq -r '.state')" = "waiting"
84+
assert "$(echo "$OUTPUT" | jq -r '.name')" = "${FUTURE_NAME}"
7285

7386
# Clean up the future workflow
7487
expect_succ pulp workflow cancel --name "${FUTURE_NAME}"

0 commit comments

Comments
 (0)