Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions docs/command-line-interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ Optional arguments:
- ``--no-global-webhook`` Skip the creation of the global webhook. This option is
only useful if a global webhook is defined in the settings.


.. _cli_batch_create:

`$ scanpipe batch-create [--input-directory INPUT_DIRECTORY] [--input-list FILENAME.csv]`
Expand Down Expand Up @@ -532,6 +533,7 @@ Displays status information about the ``PROJECT`` project.
The full logs of each pipeline execution are displayed by default.
This can be disabled providing the ``--verbosity 0`` option.


.. _cli_output:

`$ scanpipe output --project PROJECT --format {json,csv,xlsx,spdx,cyclonedx,attribution,...}`
Expand All @@ -557,6 +559,7 @@ your outputs on the host machine when running with Docker.
.. tip:: To specify a CycloneDX spec version (default to latest), use the syntax
``cyclonedx:VERSION`` as format value. For example: ``--format cyclonedx:1.5``.


.. _cli_report:

`$ scanpipe report --model MODEL`
Expand Down Expand Up @@ -598,6 +601,7 @@ worksheet::

$ scanpipe report --model package --search audit


.. _cli_check_compliance:

`$ scanpipe check-compliance --project PROJECT`
Expand Down Expand Up @@ -630,16 +634,41 @@ Optional arguments:
- ``--no-input`` Does not prompt the user for input of any kind.


.. _cli_reset_project:

`$ scanpipe reset-project --project PROJECT`
--------------------------------------------

Resets a project removing all database entrie and all data on disks except for
the input/ directory.
Resets a project removing all database entries and all data on disks except for
the :guilabel:`input/` directory.

Optional arguments:

- ``--remove-input`` Remove the :guilabel:`input/` directory and input sources when
resetting the project.
- ``--remove-webhook`` Remove webhook subscriptions when resetting the project.
- ``--restore-pipelines`` Restore all pipelines that were previously existing on the
project.
- ``--execute-now`` Execute the restored pipelines immediately after restoration.
Applies only when ``--restore-pipelines`` is provided.
- ``--no-input`` Does not prompt the user for input of any kind.

Example usage:

1. Reset a project while preserving input files and webhooks (default behavior)::

$ scanpipe reset-project --project foo

2. Reset a project and remove all data including input files::

$ scanpipe reset-project --project foo --remove-input

3. Reset a project and restore its original pipelines for re-execution::

$ scanpipe reset-project --project foo --restore-pipelines --execute-now


.. _cli_delete_project:

`$ scanpipe delete-project --project PROJECT`
---------------------------------------------
Expand Down Expand Up @@ -715,6 +744,7 @@ Optional arguments:
- ``--admin`` Specifies that the user should be created as an admin user.
- ``--super`` Specifies that the user should be created as a superuser.


.. _cli_run:

`$ run PIPELINE_NAME [PIPELINE_NAME ...] input_location`
Expand Down
6 changes: 6 additions & 0 deletions scanpipe/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,12 @@ class ProjectResetSerializer(serializers.Serializer):
initial=True,
help_text="Keep the input directory and input sources when resetting.",
)
keep_webhook = serializers.BooleanField(
required=False,
default=True,
initial=True,
help_text="Keep webhook subscriptions when resetting.",
)
restore_pipelines = serializers.BooleanField(
required=False,
default=False,
Expand Down
6 changes: 6 additions & 0 deletions scanpipe/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ class ProjectResetForm(BaseProjectActionForm):
initial=True,
required=False,
)
keep_webhook = forms.BooleanField(
label="Keep webhook subscriptions",
initial=True,
required=False,
)
restore_pipelines = forms.BooleanField(
label="Restore existing pipelines",
initial=False,
Expand All @@ -301,6 +306,7 @@ class ProjectResetForm(BaseProjectActionForm):
def get_action_kwargs(self):
return {
"keep_input": self.cleaned_data["keep_input"],
"keep_webhook": self.cleaned_data["keep_webhook"],
"restore_pipelines": self.cleaned_data["restore_pipelines"],
"execute_now": self.cleaned_data["execute_now"],
}
Expand Down
41 changes: 36 additions & 5 deletions scanpipe/management/commands/reset-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@

import sys

from django.core.management import CommandError

from scanpipe.management.commands import ProjectCommand
from scanpipe.models import RunInProgressError


class Command(ProjectCommand):
Expand All @@ -39,6 +42,26 @@ def add_arguments(self, parser):
dest="interactive",
help="Do not prompt the user for input of any kind.",
)
parser.add_argument(
"--remove-input",
action="store_true",
help="Remove the input directory and input sources when resetting.",
)
parser.add_argument(
"--remove-webhook",
action="store_true",
help="Remove webhook subscriptions when resetting.",
)
parser.add_argument(
"--restore-pipelines",
action="store_true",
help="Restore all pipelines that were previously existing on the project.",
)
parser.add_argument(
"--execute-now",
action="store_true",
help="Execute the restored pipelines immediately after restoration.",
)

def handle(self, *inputs, **options):
super().handle(*inputs, **options)
Expand All @@ -56,11 +79,19 @@ def handle(self, *inputs, **options):
self.stdout.write("Reset cancelled.")
sys.exit(0)

self.project.reset(keep_input=True)
keep_input = not options["remove_input"]
keep_webhook = not options["remove_webhook"]

if self.verbosity > 0:
msg = (
f"All data, except inputs, for the {self.project} project have been "
f"removed."
try:
self.project.reset(
keep_input=keep_input,
keep_webhook=keep_webhook,
restore_pipelines=options["restore_pipelines"],
execute_now=options["execute_now"],
)
except RunInProgressError as error:
raise CommandError(error)

if self.verbosity > 0:
msg = f"The {self.project} project has been reset."
self.stdout.write(msg, self.style.SUCCESS)
22 changes: 17 additions & 5 deletions scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,9 @@ def archive(self, remove_input=False, remove_codebase=False, remove_output=False

self.update(is_archived=True)

def delete_related_objects(self, keep_input=False, keep_labels=False):
def delete_related_objects(
self, keep_input=False, keep_labels=False, keep_webhook=False
):
"""
Delete all related object instances using the private `_raw_delete` model API.
This bypass the objects collection, cascade deletions, and signals.
Expand All @@ -700,7 +702,6 @@ def delete_related_objects(self, keep_input=False, keep_labels=False):

relationships = [
self.webhookdeliveries,
self.webhooksubscriptions,
self.projectmessages,
self.codebaserelations,
self.discovereddependencies,
Expand All @@ -712,6 +713,9 @@ def delete_related_objects(self, keep_input=False, keep_labels=False):
if not keep_input:
relationships.append(self.inputsources)

if not keep_webhook:
relationships.append(self.webhooksubscriptions)

for qs in relationships:
count = qs.all()._raw_delete(qs.db)
deleted_counter[qs.model._meta.label] = count
Expand All @@ -730,10 +734,16 @@ def delete(self, *args, **kwargs):

return super().delete(*args, **kwargs)

def reset(self, keep_input=True, restore_pipelines=False, execute_now=False):
def reset(
self,
keep_input=True,
keep_webhook=True,
restore_pipelines=False,
execute_now=False,
):
"""
Reset the project by deleting all related database objects and all work
directories except the input directorywhen the `keep_input` option is True.
directories except the input directory when the `keep_input` option is True.
"""
self._raise_if_run_in_progress()

Expand All @@ -748,7 +758,9 @@ def reset(self, keep_input=True, restore_pipelines=False, execute_now=False):
for run in self.runs.all()
]

self.delete_related_objects(keep_input=keep_input, keep_labels=True)
self.delete_related_objects(
keep_input=keep_input, keep_labels=True, keep_webhook=keep_webhook
)

work_directories = [
self.codebase_path,
Expand Down
6 changes: 6 additions & 0 deletions scanpipe/templates/scanpipe/modals/project_reset_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
{{ reset_form.keep_input }}
{{ reset_form.keep_input.label }}
</label>
</div>
<div class="field">
<label class="label">
{{ reset_form.keep_webhook }}
{{ reset_form.keep_webhook.label }}
</label>
</div>
<div class="field">
<label class="label">
Expand Down
28 changes: 23 additions & 5 deletions scanpipe/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,10 +804,12 @@ def test_scanpipe_management_command_reset_project(self):
project.add_pipeline("analyze_docker_image")
CodebaseResource.objects.create(project=project, path="filename.ext")
DiscoveredPackage.objects.create(project=project)
project.add_webhook_subscription(target_url="https://localhost")

self.assertEqual(1, project.runs.count())
self.assertEqual(1, project.codebaseresources.count())
self.assertEqual(1, project.discoveredpackages.count())
self.assertEqual(1, project.webhooksubscriptions.count())

(project.input_path / "input_file").touch()
(project.codebase_path / "codebase_file").touch()
Expand All @@ -823,17 +825,33 @@ def test_scanpipe_management_command_reset_project(self):
]
call_command("reset-project", *options, stdout=out)
out_value = out.getvalue().strip()

expected = (
"All data, except inputs, for the my_project project have been removed."
)
self.assertEqual(expected, out_value)
self.assertEqual("The my_project project has been reset.", out_value)

self.assertEqual(0, project.runs.count())
self.assertEqual(0, project.codebaseresources.count())
self.assertEqual(0, project.discoveredpackages.count())
self.assertEqual(1, len(Project.get_root_content(project.input_path)))
self.assertEqual(0, len(Project.get_root_content(project.codebase_path)))
self.assertEqual(1, project.webhooksubscriptions.count())

project.add_pipeline("analyze_docker_image")
self.assertEqual(1, project.runs.count())
out = StringIO()
options += [
"--remove-input",
"--remove-webhook",
"--restore-pipelines",
]
call_command("reset-project", *options, stdout=out)
out_value = out.getvalue().strip()
self.assertEqual("The my_project project has been reset.", out_value)

self.assertEqual(1, project.runs.count())
self.assertEqual(0, project.codebaseresources.count())
self.assertEqual(0, project.discoveredpackages.count())
self.assertEqual(0, len(Project.get_root_content(project.input_path)))
self.assertEqual(0, len(Project.get_root_content(project.codebase_path)))
self.assertEqual(0, project.webhooksubscriptions.count())

def test_scanpipe_management_command_flush_projects(self):
project1 = make_project("project1")
Expand Down
6 changes: 5 additions & 1 deletion scanpipe/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,25 +227,29 @@ def test_scanpipe_project_model_reset(self):
package = DiscoveredPackage.objects.create(project=self.project1)
resource.discovered_packages.add(package)
make_message(self.project1, description="Error")
self.project1.add_webhook_subscription(target_url="https://localhost")

self.assertEqual(1, self.project1.projectmessages.count())
self.assertEqual(1, self.project1.runs.count())
self.assertEqual(1, self.project1.discoveredpackages.count())
self.assertEqual(1, self.project1.codebaseresources.count())
self.assertEqual(1, self.project1.inputsources.count())
self.assertEqual(1, self.project1.webhooksubscriptions.count())

self.project1.reset(restore_pipelines=True, execute_now=False)
self.assertEqual(0, self.project1.projectmessages.count())
self.assertEqual(1, self.project1.runs.count())
self.assertEqual(0, self.project1.discoveredpackages.count())
self.assertEqual(0, self.project1.codebaseresources.count())
self.assertEqual(1, self.project1.webhooksubscriptions.count())

self.project1.reset()
self.project1.reset(keep_webhook=False)
self.assertTrue(Project.objects.filter(name=self.project1.name).exists())
self.assertEqual(0, self.project1.projectmessages.count())
self.assertEqual(0, self.project1.runs.count())
self.assertEqual(0, self.project1.discoveredpackages.count())
self.assertEqual(0, self.project1.codebaseresources.count())
self.assertEqual(0, self.project1.webhooksubscriptions.count())

# The InputSource objects are kept
self.assertEqual(1, self.project1.inputsources.count())
Expand Down