Skip to content

Commit d5273cb

Browse files
authored
Add pipeline selected groups in create project API endpoint #1426 (#1427)
* Add pipeline selected groups in create project API endpoint #1426 Signed-off-by: tdruez <tdruez@nexb.com> * Add proper pipeline validation in the OrderedMultiplePipelineChoiceField #1426 Signed-off-by: tdruez <tdruez@nexb.com> --------- Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 495997c commit d5273cb

File tree

4 files changed

+90
-26
lines changed

4 files changed

+90
-26
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
v34.9.0 (unreleased)
5+
--------------------
6+
7+
- Add ability to declared pipeline selected groups in create project REST API endpoint.
8+
https://github.com/aboutcode-org/scancode.io/issues/1426
9+
410
v34.8.3 (2024-10-30)
511
--------------------
612

docs/rest-api.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ Using cURL:
126126
To tag the ``upload_file``, you can provide the tag value using the
127127
``upload_file_tag`` field.
128128

129+
.. tip::
130+
131+
You can declare multiple pipelines to be executed at the project creation using a
132+
list of pipeline names or a comma-separated string:
133+
134+
- ``"pipeline": ["scan_single_package", "scan_for_virus"]``
135+
- ``"pipeline": "scan_single_package,scan_for_virus"``
136+
137+
.. tip::
138+
139+
Use the "pipeline_name:group1,group2" syntax to select steps groups:
140+
141+
``"pipeline": "map_deploy_to_develop:Java,JavaScript"``
142+
129143
Using Python and the **"requests"** library:
130144

131145
.. code-block:: python

scanpipe/api/serializers.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,12 @@ def __init__(self, *args, **kwargs):
6969
self.fields["pipeline"].choices = scanpipe_app.get_pipeline_choices()
7070

7171

72-
class OrderedMultipleChoiceField(serializers.MultipleChoiceField):
73-
"""Forcing outputs as list() in place of set() to keep the ordering integrity."""
72+
class OrderedMultiplePipelineChoiceField(serializers.MultipleChoiceField):
73+
"""
74+
Forcing outputs as list() in place of set() to keep the ordering integrity.
75+
The field validation is bypassed and delegated to the ``project.add_pipeline``
76+
method called in the ``ProjectSerializer.create`` method.
77+
"""
7478

7579
def to_internal_value(self, data):
7680
if isinstance(data, str):
@@ -80,15 +84,17 @@ def to_internal_value(self, data):
8084
if not self.allow_empty and len(data) == 0:
8185
self.fail("empty")
8286

83-
# Backward compatibility with old pipeline names.
84-
# This will need to be refactored in case this OrderedMultipleChoiceField
85-
# class is used for another field that is not ``pipeline`` related.
86-
data = [scanpipe_app.get_new_pipeline_name(pipeline) for pipeline in data]
87+
# Adds support for providing pipeline names as a comma-separated single string.
88+
data = [item.strip() for value in data for item in value.split(",")]
8789

88-
return [
89-
super(serializers.MultipleChoiceField, self).to_internal_value(item)
90-
for item in data
91-
]
90+
# Pipeline validation
91+
for pipeline in data:
92+
pipeline_name, _ = scanpipe_app.extract_group_from_pipeline(pipeline)
93+
pipeline_name = scanpipe_app.get_new_pipeline_name(pipeline_name)
94+
if pipeline_name not in scanpipe_app.pipelines:
95+
self.fail("invalid_choice", input=pipeline_name)
96+
97+
return data
9298

9399
def to_representation(self, value):
94100
return [self.choice_strings_to_values.get(str(item), item) for item in value]
@@ -163,7 +169,7 @@ class ProjectSerializer(
163169
TaggitSerializer,
164170
serializers.ModelSerializer,
165171
):
166-
pipeline = OrderedMultipleChoiceField(
172+
pipeline = OrderedMultiplePipelineChoiceField(
167173
choices=(),
168174
required=False,
169175
write_only=True,
@@ -302,7 +308,7 @@ def create(self, validated_data):
302308
upload_file = validated_data.pop("upload_file", None)
303309
upload_file_tag = validated_data.pop("upload_file_tag", "")
304310
input_urls = validated_data.pop("input_urls", [])
305-
pipeline = validated_data.pop("pipeline", [])
311+
pipelines = validated_data.pop("pipeline", [])
306312
execute_now = validated_data.pop("execute_now", False)
307313
webhook_url = validated_data.pop("webhook_url", None)
308314
webhooks = validated_data.pop("webhooks", [])
@@ -315,8 +321,10 @@ def create(self, validated_data):
315321
for url in input_urls:
316322
project.add_input_source(download_url=url)
317323

318-
for pipeline_name in pipeline:
319-
project.add_pipeline(pipeline_name, execute_now)
324+
for pipeline in pipelines:
325+
pipeline_name, groups = scanpipe_app.extract_group_from_pipeline(pipeline)
326+
pipeline_name = scanpipe_app.get_new_pipeline_name(pipeline_name)
327+
project.add_pipeline(pipeline_name, execute_now, selected_groups=groups)
320328

321329
if webhook_url:
322330
project.add_webhook_subscription(target_url=webhook_url)

scanpipe/tests/test_api.py

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -447,19 +447,39 @@ def test_scanpipe_api_project_create_multiple_pipelines(self):
447447
"pipeline": "analyze_docker_image,scan_single_package",
448448
}
449449
response = self.csrf_client.post(self.project_list_url, data)
450-
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
451-
expected = {
450+
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
451+
self.assertEqual(2, len(response.data["runs"]))
452+
self.assertEqual(
453+
"analyze_docker_image", response.data["runs"][0]["pipeline_name"]
454+
)
455+
self.assertEqual(
456+
"scan_single_package", response.data["runs"][1]["pipeline_name"]
457+
)
458+
459+
data = {
460+
"name": "Mix of string and list plus selected groups",
452461
"pipeline": [
453-
ErrorDetail(
454-
string=(
455-
'"analyze_docker_image,scan_single_package" '
456-
"is not a valid choice."
457-
),
458-
code="invalid_choice",
459-
)
460-
]
462+
"analyze_docker_image",
463+
"inspect_packages:StaticResolver,scan_single_package",
464+
],
461465
}
462-
self.assertEqual(expected, response.data)
466+
response = self.csrf_client.post(self.project_list_url, data)
467+
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
468+
self.assertEqual(
469+
"analyze_docker_image", response.data["runs"][0]["pipeline_name"]
470+
)
471+
self.assertEqual("inspect_packages", response.data["runs"][1]["pipeline_name"])
472+
self.assertEqual(
473+
"scan_single_package", response.data["runs"][2]["pipeline_name"]
474+
)
475+
self.assertEqual(
476+
["StaticResolver"], response.data["runs"][1]["selected_groups"]
477+
)
478+
runs = Project.objects.get(name=data["name"]).runs.all()
479+
self.assertEqual("analyze_docker_image", runs[0].pipeline_name)
480+
self.assertEqual("inspect_packages", runs[1].pipeline_name)
481+
self.assertEqual("scan_single_package", runs[2].pipeline_name)
482+
self.assertEqual(["StaticResolver"], runs[1].selected_groups)
463483

464484
def test_scanpipe_api_project_create_pipeline_old_name_compatibility(self):
465485
data = {
@@ -498,6 +518,20 @@ def test_scanpipe_api_project_create_labels(self):
498518
project = Project.objects.get(name=data["name"])
499519
self.assertEqual(data["labels"], sorted(project.labels.names()))
500520

521+
def test_scanpipe_api_project_create_pipeline_groups(self):
522+
data = {
523+
"name": "Project1",
524+
"pipeline": "inspect_packages:StaticResolver",
525+
}
526+
response = self.csrf_client.post(self.project_list_url, data)
527+
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
528+
self.assertEqual(
529+
["StaticResolver"], response.data["runs"][0]["selected_groups"]
530+
)
531+
run = Project.objects.get(name="Project1").runs.get()
532+
self.assertEqual("inspect_packages", run.pipeline_name)
533+
self.assertEqual(["StaticResolver"], run.selected_groups)
534+
501535
def test_scanpipe_api_project_create_webhooks(self):
502536
data = {
503537
"name": "Project1",
@@ -862,7 +896,9 @@ def test_scanpipe_api_project_action_add_pipeline_groups(self):
862896
}
863897
response = self.csrf_client.post(url, data=data)
864898
self.assertEqual({"status": "Pipeline added."}, response.data)
865-
self.assertEqual("analyze_docker_image", self.project1.runs.get().pipeline_name)
899+
run = self.project1.runs.get()
900+
self.assertEqual("analyze_docker_image", run.pipeline_name)
901+
self.assertEqual(["group1", "group2"], run.selected_groups)
866902

867903
def test_scanpipe_api_project_action_add_input(self):
868904
url = reverse("project-add-input", args=[self.project1.uuid])

0 commit comments

Comments
 (0)