Skip to content

Commit bcef1d0

Browse files
authored
Add filtering by label and pipeline in the flush-projects management command (#1690)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 6a397f3 commit bcef1d0

File tree

6 files changed

+141
-25
lines changed

6 files changed

+141
-25
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changelog
44
v34.12.0 (unreleased)
55
---------------------
66

7+
- Add filtering by label and pipeline in the ``flush-projects`` management command.
8+
Also, a new ``--dry-run`` option is available to test the filters before applying
9+
the deletion.
10+
711
- Add support for using Package URL (purl) as project input.
812
This implementation is based on ``purl2url.get_download_url``.
913
https://github.com/aboutcode-org/scancode.io/issues/1383

docs/command-line-interface.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,14 @@ Optional arguments:
549549

550550
scanpipe flush-projects --retain-days 7
551551

552+
- ``--dry-run`` Do not delete any projects; just print the ones that would be flushed.
553+
554+
- ``--label LABELS`` Filter projects by the provided label.
555+
Multiple labels can be provided by using this argument multiple times.
556+
557+
- ``--pipeline PIPELINES`` Filter projects by the provided pipeline name.
558+
Multiple pipeline name can be provided by using this argument multiple times.
559+
552560
- ``--no-input`` Does not prompt the user for input of any kind.
553561

554562

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ max-complexity = 10
3636
# Allow the usage of assert in the test_spdx file.
3737
"**/test_spdx.py*" = ["S101"]
3838
"scanpipe/pipes/spdx.py" = ["UP006", "UP035"]
39+
# Allow complexity in management commands
40+
"scanpipe/management/commands/*" = ["C901"]

scanpipe/management/commands/flush-projects.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,72 @@ def add_arguments(self, parser):
4848
),
4949
default=0,
5050
)
51+
parser.add_argument(
52+
"--label",
53+
action="append",
54+
dest="labels",
55+
default=list(),
56+
help=(
57+
"Filter projects by the provided label. "
58+
"Multiple labels can be provided by using this argument multiple times."
59+
),
60+
)
61+
parser.add_argument(
62+
"--pipeline",
63+
action="append",
64+
dest="pipelines",
65+
default=list(),
66+
help=(
67+
"Filter projects by the provided pipeline name. "
68+
"Multiple pipeline name can be provided by using this argument multiple"
69+
" times."
70+
),
71+
)
5172
parser.add_argument(
5273
"--no-input",
5374
action="store_false",
5475
dest="interactive",
5576
help="Do not prompt the user for input of any kind.",
5677
)
78+
parser.add_argument(
79+
"--dry-run",
80+
action="store_true",
81+
help=(
82+
"Do not delete any projects; just print the ones that would be flushed."
83+
),
84+
)
5785

5886
def handle(self, *inputs, **options):
5987
verbosity = options["verbosity"]
6088
retain_days = options["retain_days"]
61-
projects = Project.objects.all()
89+
labels = options["labels"]
90+
pipelines = options["pipelines"]
91+
dry_run = options["dry_run"]
92+
projects = Project.objects.order_by("-created_date").distinct()
6293

6394
if retain_days:
6495
cutoff_date = timezone.now() - datetime.timedelta(days=retain_days)
6596
projects = projects.filter(created_date__lt=cutoff_date)
6697

98+
if labels:
99+
projects = projects.filter(labels__name__in=labels)
100+
101+
if pipelines:
102+
projects = projects.filter(runs__pipeline_name__in=pipelines)
103+
67104
projects_count = projects.count()
68105
if projects_count == 0:
69106
if verbosity > 0:
70-
self.stdout.write("No projects to remove.")
107+
self.stdout.write("No projects to delete.")
71108
sys.exit(0)
72109

110+
if dry_run:
111+
msg = self.style.WARNING(f"{projects_count} projects would be deleted:")
112+
self.stdout.write(msg)
113+
self.stdout.write("\n".join([f"- {project.name}" for project in projects]))
114+
return
115+
# sys.exit(0)
116+
73117
if options["interactive"]:
74118
confirm = input(
75119
f"You have requested the deletion of {projects_count} "

scanpipe/tests/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,23 @@
4949

5050

5151
def make_project(name=None, **data):
52+
"""
53+
Create and return a Project instance.
54+
Labels can be provided using the labels=["labels1", "labels2"] argument.
55+
"""
5256
name = name or str(uuid.uuid4())[:8]
53-
return Project.objects.create(name=name, **data)
57+
pipelines = data.pop("pipelines", [])
58+
labels = data.pop("labels", [])
59+
60+
project = Project.objects.create(name=name, **data)
61+
62+
for pipeline in pipelines:
63+
project.add_pipeline(pipeline)
64+
65+
if labels:
66+
project.labels.add(*labels)
67+
68+
return project
5469

5570

5671
def make_resource_file(project, path, **data):

scanpipe/tests/test_commands.py

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ def test_scanpipe_management_command_batch_create_global_webhook(
319319
def test_scanpipe_management_command_add_input_file(self):
320320
out = StringIO()
321321

322-
project = Project.objects.create(name="my_project")
322+
project = make_project(name="my_project")
323323
parent_path = Path(__file__).parent
324324
options = [
325325
"--input-file",
@@ -346,7 +346,7 @@ def test_scanpipe_management_command_add_input_file(self):
346346
call_command("add-input", *options, stdout=out)
347347

348348
def test_scanpipe_management_command_add_input_url(self):
349-
project = Project.objects.create(name="my_project")
349+
project = make_project(name="my_project")
350350
options = [
351351
"--input-url",
352352
"https://example.com/archive.zip",
@@ -368,7 +368,7 @@ def test_scanpipe_management_command_add_input_url(self):
368368
def test_scanpipe_management_command_add_input_copy_codebase(self):
369369
out = StringIO()
370370

371-
project = Project.objects.create(name="my_project")
371+
project = make_project(name="my_project")
372372

373373
options = ["--copy-codebase", "non-existing", "--project", project.name]
374374
expected = "non-existing not found"
@@ -394,7 +394,7 @@ def test_scanpipe_management_command_add_input_copy_codebase(self):
394394
def test_scanpipe_management_command_add_pipeline(self):
395395
out = StringIO()
396396

397-
project = Project.objects.create(name="my_project")
397+
project = make_project(name="my_project")
398398

399399
pipelines = [
400400
self.pipeline_name,
@@ -431,7 +431,7 @@ def test_scanpipe_management_command_add_pipeline(self):
431431
def test_scanpipe_management_command_add_webhook(self):
432432
out = StringIO()
433433

434-
project = Project.objects.create(name="my_project")
434+
project = make_project(name="my_project")
435435

436436
options = ["https://example.com/webhook"]
437437
expected = "the following arguments are required: --project"
@@ -486,7 +486,7 @@ def test_scanpipe_management_command_show_pipeline(self):
486486
"analyze_root_filesystem_or_vm_image",
487487
]
488488

489-
project = Project.objects.create(name="my_project")
489+
project = make_project(name="my_project")
490490
for pipeline_name in pipeline_names:
491491
project.add_pipeline(pipeline_name)
492492

@@ -513,7 +513,7 @@ def test_scanpipe_management_command_show_pipeline(self):
513513
self.assertEqual(expected, out.getvalue())
514514

515515
def test_scanpipe_management_command_execute(self):
516-
project = Project.objects.create(name="my_project")
516+
project = make_project(name="my_project")
517517
options = ["--project", project.name]
518518

519519
out = StringIO()
@@ -557,7 +557,7 @@ def test_scanpipe_management_command_execute(self):
557557
self.assertEqual("", run3.task_output)
558558

559559
def test_scanpipe_management_command_execute_project_function(self):
560-
project = Project.objects.create(name="my_project")
560+
project = make_project(name="my_project")
561561

562562
expected = "No pipelines to run on project my_project"
563563
with self.assertRaisesMessage(CommandError, expected):
@@ -584,7 +584,7 @@ def test_scanpipe_management_command_execute_project_function(self):
584584
self.assertIsNone(returned_value)
585585

586586
def test_scanpipe_management_command_status(self):
587-
project = Project.objects.create(name="my_project")
587+
project = make_project(name="my_project")
588588
run = project.add_pipeline(self.pipeline_name)
589589

590590
options = ["--project", project.name, "--no-color"]
@@ -657,9 +657,9 @@ def test_scanpipe_management_command_list_pipelines(self):
657657
self.assertIn("(addon)", output)
658658

659659
def test_scanpipe_management_command_list_project(self):
660-
project1 = Project.objects.create(name="project1")
661-
project2 = Project.objects.create(name="project2")
662-
project3 = Project.objects.create(name="archived", is_archived=True)
660+
project1 = make_project(name="project1")
661+
project2 = make_project(name="project2")
662+
project3 = make_project(name="archived", is_archived=True)
663663

664664
options = []
665665
out = StringIO()
@@ -686,7 +686,7 @@ def test_scanpipe_management_command_list_project(self):
686686
self.assertIn(project3.name, output)
687687

688688
def test_scanpipe_management_command_output(self):
689-
project = Project.objects.create(name="my_project")
689+
project = make_project(name="my_project")
690690
make_package(project, package_url="pkg:generic/name@1.0")
691691

692692
out = StringIO()
@@ -754,7 +754,7 @@ def test_scanpipe_management_command_output(self):
754754
self.assertIn('"specVersion": "1.5",', out_value)
755755

756756
def test_scanpipe_management_command_delete_project(self):
757-
project = Project.objects.create(name="my_project")
757+
project = make_project(name="my_project")
758758
work_path = project.work_path
759759
self.assertTrue(work_path.exists())
760760

@@ -770,7 +770,7 @@ def test_scanpipe_management_command_delete_project(self):
770770
self.assertFalse(work_path.exists())
771771

772772
def test_scanpipe_management_command_archive_project(self):
773-
project = Project.objects.create(name="my_project")
773+
project = make_project(name="my_project")
774774
(project.input_path / "input_file").touch()
775775
(project.codebase_path / "codebase_file").touch()
776776
self.assertEqual(1, len(Project.get_root_content(project.input_path)))
@@ -797,7 +797,7 @@ def test_scanpipe_management_command_archive_project(self):
797797
self.assertEqual(0, len(Project.get_root_content(project.codebase_path)))
798798

799799
def test_scanpipe_management_command_reset_project(self):
800-
project = Project.objects.create(name="my_project")
800+
project = make_project(name="my_project")
801801
project.add_pipeline("analyze_docker_image")
802802
CodebaseResource.objects.create(project=project, path="filename.ext")
803803
DiscoveredPackage.objects.create(project=project)
@@ -833,8 +833,8 @@ def test_scanpipe_management_command_reset_project(self):
833833
self.assertEqual(0, len(Project.get_root_content(project.codebase_path)))
834834

835835
def test_scanpipe_management_command_flush_projects(self):
836-
project1 = Project.objects.create(name="project1")
837-
project2 = Project.objects.create(name="project2")
836+
project1 = make_project("project1")
837+
project2 = make_project("project2")
838838
ten_days_ago = timezone.now() - datetime.timedelta(days=10)
839839
project2.update(created_date=ten_days_ago)
840840

@@ -846,14 +846,58 @@ def test_scanpipe_management_command_flush_projects(self):
846846
self.assertEqual(expected, out_value)
847847
self.assertEqual(project1, Project.objects.get())
848848

849-
Project.objects.create(name="project2")
849+
make_project("project2")
850+
out = StringIO()
851+
options = ["--no-color", "--no-input", "--dry-run"]
852+
call_command("flush-projects", *options, stdout=out)
853+
out_value = out.getvalue().strip()
854+
expected = "2 projects would be deleted:\n- project2\n- project1"
855+
self.assertEqual(expected, out_value)
856+
850857
out = StringIO()
851858
options = ["--no-color", "--no-input"]
852859
call_command("flush-projects", *options, stdout=out)
853860
out_value = out.getvalue().strip()
854861
expected = "2 projects and their related data have been removed."
855862
self.assertEqual(expected, out_value)
856863

864+
def test_scanpipe_management_command_flush_projects_filters(self):
865+
label1 = "label1"
866+
label2 = "label2"
867+
make_project("project1", labels=[label1])
868+
make_project("project2", labels=[label1, label2])
869+
make_project("project3", pipelines=["scan_single_package"])
870+
871+
base_options = ["--no-color", "--no-input", "--dry-run"]
872+
873+
out = StringIO()
874+
options = base_options + ["--label", label1]
875+
call_command("flush-projects", *options, stdout=out)
876+
out_value = out.getvalue().strip()
877+
expected = "2 projects would be deleted:\n- project2\n- project1"
878+
self.assertEqual(expected, out_value)
879+
880+
out = StringIO()
881+
options = base_options + ["--label", label2]
882+
call_command("flush-projects", *options, stdout=out)
883+
out_value = out.getvalue().strip()
884+
expected = "1 projects would be deleted:\n- project2"
885+
self.assertEqual(expected, out_value)
886+
887+
out = StringIO()
888+
options = base_options + ["--label", label1, "--label", label2]
889+
call_command("flush-projects", *options, stdout=out)
890+
out_value = out.getvalue().strip()
891+
expected = "2 projects would be deleted:\n- project2\n- project1"
892+
self.assertEqual(expected, out_value)
893+
894+
out = StringIO()
895+
options = base_options + ["--pipeline", "scan_single_package"]
896+
call_command("flush-projects", *options, stdout=out)
897+
out_value = out.getvalue().strip()
898+
expected = "1 projects would be deleted:\n- project3"
899+
self.assertEqual(expected, out_value)
900+
857901
def test_scanpipe_management_command_create_user(self):
858902
out = StringIO()
859903

@@ -1123,7 +1167,7 @@ def test_scanpipe_management_command_purldb_scan_queue_worker_continue_after_fai
11231167
)
11241168

11251169
def test_scanpipe_management_command_check_compliance(self):
1126-
project = Project.objects.create(name="my_project")
1170+
project = make_project(name="my_project")
11271171

11281172
out = StringIO()
11291173
options = ["--project", project.name]
@@ -1169,9 +1213,8 @@ def test_scanpipe_management_command_check_compliance(self):
11691213
self.assertEqual(expected, out_value)
11701214

11711215
def test_scanpipe_management_command_report(self):
1172-
project1 = make_project("project1")
11731216
label1 = "label1"
1174-
project1.labels.add(label1)
1217+
project1 = make_project("project1", labels=[label1])
11751218
make_resource_file(project1, path="file.ext", status=flag.REQUIRES_REVIEW)
11761219
make_project("project2")
11771220

0 commit comments

Comments
 (0)