Skip to content

Commit 0318583

Browse files
authored
Merge pull request #2112 from aboutcode-org/onetime_pipeline
Support running pipeline only once
2 parents 05b26dd + 03ec81f commit 0318583

File tree

10 files changed

+197
-22
lines changed

10 files changed

+197
-22
lines changed

docs/source/contributing.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ resources to help you get started.
1717
Do Your Homework
1818
----------------
1919

20-
Before adding a contribution or create a new issue, take a look at the projects
20+
Before adding a contribution or create a new issue, take a look at the project's
2121
`README <https://github.com/aboutcode-org/vulnerablecode>`_, read through our
2222
`documentation <https://vulnerablecode.readthedocs.io/en/latest/>`_,
2323
and browse existing `issues <https://github.com/aboutcode-org/vulnerablecode/issues>`_,
@@ -73,7 +73,7 @@ overlooked. We value any suggestions to improve
7373

7474
.. tip::
7575
Our documentation is treated like code. Make sure to check our
76-
`writing guidelines <https://scancode-toolkit.readthedocs.io/en/latest/contribute/contrib_doc.html>`_
76+
`writing guidelines <https://scancode-toolkit.readthedocs.io/en/stable/contribute/contrib_doc.html>`_
7777
to help guide new users.
7878

7979
Other Ways
@@ -87,7 +87,7 @@ questions, and interact with us and other community members on
8787
Helpful Resources
8888
-----------------
8989

90-
- Review our `comprehensive guide <https://scancode-toolkit.readthedocs.io/en/latest/contribute/index.html>`_
90+
- Review our `comprehensive guide <https://scancode-toolkit.readthedocs.io/en/stable/contribute/index.html>`_
9191
for more details on how to add quality contributions to our codebase and documentation
9292
- Check this free resource on `How to contribute to an open source project on github <https://egghead.io/lessons/javascript-identifying-how-to-contribute-to-an-open-source-project-on-github>`_
9393
- Follow `this wiki page <https://aboutcode.readthedocs.io/en/latest/contributing/writing_good_commit_messages.html>`_

docs/source/tutorial_add_importer_pipeline.rst

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ At this point, an example importer will look like this:
152152
.. code-block:: python
153153
:caption: vulnerabilities/pipelines/example_importer.py
154154
:linenos:
155-
:emphasize-lines: 16-17, 20-21, 23-24
155+
:emphasize-lines: 17-18, 21-22, 24-25
156156
157157
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
158158
@@ -165,6 +165,7 @@ At this point, an example importer will look like this:
165165
license_url = "https://exmaple.org/license/"
166166
spdx_license_expression = "CC-BY-4.0"
167167
importer_name = "Example Importer"
168+
run_once = False
168169
169170
@classmethod
170171
def steps(cls):
@@ -196,7 +197,7 @@ version management from `univers <https://github.com/aboutcode-org/univers>`_.
196197
.. code-block:: python
197198
:caption: vulnerabilities/pipelines/example_importer.py
198199
:linenos:
199-
:emphasize-lines: 34-35, 37-40
200+
:emphasize-lines: 35-36, 38-41
200201
201202
from datetime import datetime
202203
from datetime import timezone
@@ -223,6 +224,7 @@ version management from `univers <https://github.com/aboutcode-org/univers>`_.
223224
license_url = "https://example.org/license/"
224225
spdx_license_expression = "CC-BY-4.0"
225226
importer_name = "Example Importer"
227+
run_once = False
226228
227229
@classmethod
228230
def steps(cls):
@@ -303,6 +305,17 @@ version management from `univers <https://github.com/aboutcode-org/univers>`_.
303305
Implement ``on_failure`` to handle cleanup in case of pipeline failure.
304306
Cleanup of downloaded archives or cloned repos is necessary to avoid potential resource leakage.
305307

308+
.. tip::
309+
310+
Set ``run_once`` to ``True`` if pipeline is meant to be run once.
311+
312+
- To rerun onetime pipeline, reset ``is_active`` to ``True`` via a migration, pipeline will
313+
run one more time and then deactivate.
314+
315+
- To convert a onetime pipeline to a regular pipeline, set the ``run_once`` class variable
316+
to ``False`` and reset ``is_active` field to ``True`` via a migration.
317+
318+
306319
.. note::
307320

308321
| Use ``make valid`` to format your new code using black and isort automatically.

vulnerabilities/management/commands/run_scheduler.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@
1717

1818

1919
def init_pipeline_scheduled():
20-
"""Initialize schedule jobs for active PipelineSchedule."""
21-
active_pipeline_qs = models.PipelineSchedule.objects.filter(is_active=True).order_by(
22-
"created_date"
23-
)
24-
for pipeline_schedule in active_pipeline_qs:
25-
if scheduled_job_exists(pipeline_schedule.schedule_work_id):
26-
continue
27-
new_id = pipeline_schedule.create_new_job()
28-
pipeline_schedule.schedule_work_id = new_id
29-
pipeline_schedule.save(update_fields=["schedule_work_id"])
20+
"""
21+
Initialize schedule jobs for active PipelineSchedule.
22+
- Create new schedule if there is no schedule for active pipeline
23+
- Create new schedule if schedule is corrupted for an active pipeline
24+
- Delete schedule for inactive pipeline
25+
"""
26+
pipeline_qs = models.PipelineSchedule.objects.order_by("created_date")
27+
for pipeline in pipeline_qs:
28+
reset_schedule = pipeline.is_active != bool(pipeline.schedule_work_id)
29+
if not scheduled_job_exists(pipeline.schedule_work_id):
30+
reset_schedule = True
31+
32+
if reset_schedule:
33+
pipeline.schedule_work_id = pipeline.create_new_job()
34+
pipeline.save(update_fields=["schedule_work_id"])
3035

3136

3237
class Command(rqscheduler.Command):
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 4.2.22 on 2026-01-08 13:41
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0109_alter_advisoryseverity_scoring_elements_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="pipelineschedule",
15+
name="is_run_once",
16+
field=models.BooleanField(
17+
db_index=True,
18+
default=False,
19+
help_text="When set to True, this Pipeline will run only once.",
20+
),
21+
),
22+
]

vulnerabilities/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,6 +2273,13 @@ class PipelineSchedule(models.Model):
22732273
),
22742274
)
22752275

2276+
is_run_once = models.BooleanField(
2277+
null=False,
2278+
db_index=True,
2279+
default=False,
2280+
help_text=("When set to True, this Pipeline will run only once."),
2281+
)
2282+
22762283
live_logging = models.BooleanField(
22772284
null=False,
22782285
db_index=True,

vulnerabilities/pipelines/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ class VulnerableCodeBaseImporterPipeline(VulnerableCodePipeline):
169169
importer_name = None
170170
advisory_confidence = MAX_CONFIDENCE
171171

172+
# When set to true pipeline is run only once.
173+
# To rerun onetime pipeline reset is_active field to True via migration.
174+
run_once = False
175+
172176
@classmethod
173177
def steps(cls):
174178
return (
@@ -262,6 +266,10 @@ class VulnerableCodeBaseImporterPipelineV2(VulnerableCodePipeline):
262266
repo_url = None
263267
ignorable_versions = []
264268

269+
# When set to true pipeline is run only once.
270+
# To rerun onetime pipeline reset is_active field to True via migration.
271+
run_once = False
272+
265273
@classmethod
266274
def steps(cls):
267275
return (

vulnerabilities/schedules.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,15 @@ def update_pipeline_schedule():
8989
from vulnerabilities.improvers import IMPROVERS_REGISTRY
9090
from vulnerabilities.models import PipelineSchedule
9191

92-
pipeline_ids = [*IMPORTERS_REGISTRY.keys(), *IMPROVERS_REGISTRY.keys()]
93-
94-
PipelineSchedule.objects.exclude(pipeline_id__in=pipeline_ids).delete()
95-
[PipelineSchedule.objects.get_or_create(pipeline_id=id) for id in pipeline_ids]
92+
pipelines = IMPORTERS_REGISTRY | IMPROVERS_REGISTRY
93+
94+
PipelineSchedule.objects.exclude(pipeline_id__in=pipelines.keys()).delete()
95+
for id, pipeline_class in pipelines.items():
96+
run_once = getattr(pipeline_class, "run_once", False)
97+
98+
PipelineSchedule.objects.get_or_create(
99+
pipeline_id=id,
100+
defaults={
101+
"is_run_once": run_once,
102+
},
103+
)

vulnerabilities/tasks.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from vulnerabilities import models
1818
from vulnerabilities.importer import Importer
1919
from vulnerabilities.improver import Improver
20-
from vulnerablecode.settings import VULNERABLECODE_PIPELINE_TIMEOUT
2120

2221
logger = logging.getLogger(__name__)
2322

@@ -48,6 +47,13 @@ def execute_pipeline(pipeline_id, run_id):
4847
exitcode = 1
4948

5049
run.set_run_ended(exitcode=exitcode, output=output)
50+
51+
# Onetime pipeline are inactive after first execution.
52+
pipeline = run.pipeline
53+
if pipeline.is_run_once:
54+
pipeline.is_active = False
55+
pipeline.save()
56+
5157
logger.info("Update Run instance with exitcode, output, and end_date")
5258

5359

vulnerabilities/templates/pipeline_dashboard.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ <h1>Pipeline Dashboard</h1>
8080
<div class="column is-one-quarter">{{ schedule.pipeline_id }}</div>
8181
<div class="column is-one-eighth has-text-grey">{{ schedule.is_active|yesno:"Yes,No" }}</div>
8282
<div class="column is-one-eighth has-text-grey">
83-
{{ schedule.run_interval }} hour{{ schedule.run_interval|pluralize }}
83+
{% if schedule.is_run_once %}
84+
Once
85+
{% else %}
86+
{{ schedule.run_interval }} hour{{ schedule.run_interval|pluralize }}
87+
{% endif %}
8488
</div>
8589
<div class="column is-one-eighth">
8690
<span class="is-flex is-align-items-center">
@@ -95,7 +99,11 @@ <h1>Pipeline Dashboard</h1>
9599
{% endif %}
96100
</div>
97101
<div class="column is-one-fifth has-text-grey">
98-
{{ schedule.next_run_date|date:"Y-m-d" }}
102+
{% if schedule.next_run_date %}
103+
{{ schedule.next_run_date|date:"Y-m-d" }}
104+
{% else %}
105+
N/A
106+
{% endif %}
99107
</div>
100108
</div>
101109
</a>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
11+
from unittest import mock
12+
13+
from django.test import TestCase
14+
15+
from vulnerabilities.models import PipelineRun
16+
from vulnerabilities.models import PipelineSchedule
17+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
18+
from vulnerabilities.tasks import execute_pipeline
19+
20+
21+
class OneTimePipeline(VulnerableCodeBaseImporterPipelineV2):
22+
pipeline_id = "one_time_pipeline_test"
23+
run_once = True
24+
25+
def collect_advisories(self):
26+
return []
27+
28+
def advisories_count(self):
29+
return 0
30+
31+
32+
class NotOneTimePipeline(VulnerableCodeBaseImporterPipelineV2):
33+
pipeline_id = "not_one_time_pipeline_test"
34+
run_once = False
35+
36+
def collect_advisories(self):
37+
return []
38+
39+
def advisories_count(self):
40+
return 0
41+
42+
43+
class TestOneTimePipelineExecution(TestCase):
44+
@mock.patch("vulnerabilities.models.PipelineSchedule.create_new_job")
45+
@mock.patch(
46+
"vulnerabilities.models.PipelineSchedule.pipeline_class",
47+
new_callable=mock.PropertyMock,
48+
)
49+
def test_onetime_pipeline_deactivation(self, mock_pipeline_class, mock_create_job):
50+
mock_create_job.return_value = True
51+
mock_pipeline_class.return_value = OneTimePipeline
52+
53+
ps, _ = PipelineSchedule.objects.get_or_create(
54+
pipeline_id=OneTimePipeline.pipeline_id,
55+
defaults={
56+
"is_run_once": OneTimePipeline.run_once,
57+
},
58+
)
59+
60+
self.assertTrue(ps.is_run_once)
61+
self.assertTrue(ps.is_active)
62+
63+
run = PipelineRun.objects.create(
64+
pipeline=ps,
65+
)
66+
execute_pipeline(ps.pipeline_id, run.run_id)
67+
68+
ps.refresh_from_db()
69+
self.assertTrue(ps.is_run_once)
70+
self.assertFalse(ps.is_active)
71+
72+
@mock.patch("vulnerabilities.models.PipelineSchedule.create_new_job")
73+
@mock.patch(
74+
"vulnerabilities.models.PipelineSchedule.pipeline_class",
75+
new_callable=mock.PropertyMock,
76+
)
77+
def test_normal_pipeline_no_deactivation(self, mock_pipeline_class, mock_create_job):
78+
mock_create_job.return_value = True
79+
mock_pipeline_class.return_value = NotOneTimePipeline
80+
81+
ps, _ = PipelineSchedule.objects.get_or_create(
82+
pipeline_id=NotOneTimePipeline.pipeline_id,
83+
defaults={
84+
"is_run_once": NotOneTimePipeline.run_once,
85+
},
86+
)
87+
88+
self.assertFalse(ps.is_run_once)
89+
self.assertTrue(ps.is_active)
90+
91+
run = PipelineRun.objects.create(
92+
pipeline=ps,
93+
)
94+
execute_pipeline(ps.pipeline_id, run.run_id)
95+
96+
ps.refresh_from_db()
97+
self.assertFalse(ps.is_run_once)
98+
self.assertTrue(ps.is_active)

0 commit comments

Comments
 (0)