diff --git a/docs/command-line-interface.rst b/docs/command-line-interface.rst index ebacc387d3..6ea5bcc72b 100644 --- a/docs/command-line-interface.rst +++ b/docs/command-line-interface.rst @@ -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]` @@ -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,...}` @@ -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` @@ -598,6 +601,7 @@ worksheet:: $ scanpipe report --model package --search audit + .. _cli_check_compliance: `$ scanpipe check-compliance --project PROJECT` @@ -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` --------------------------------------------- @@ -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` diff --git a/scanpipe/api/serializers.py b/scanpipe/api/serializers.py index 8f8b7a59d7..587a6b411e 100644 --- a/scanpipe/api/serializers.py +++ b/scanpipe/api/serializers.py @@ -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, diff --git a/scanpipe/forms.py b/scanpipe/forms.py index cd40b48ee5..e3f93e3af4 100644 --- a/scanpipe/forms.py +++ b/scanpipe/forms.py @@ -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, @@ -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"], } diff --git a/scanpipe/management/commands/reset-project.py b/scanpipe/management/commands/reset-project.py index 47b6e28b55..19e4b83010 100644 --- a/scanpipe/management/commands/reset-project.py +++ b/scanpipe/management/commands/reset-project.py @@ -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): @@ -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) @@ -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) diff --git a/scanpipe/models.py b/scanpipe/models.py index f12d079957..5ead8c6f01 100644 --- a/scanpipe/models.py +++ b/scanpipe/models.py @@ -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. @@ -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, @@ -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 @@ -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 directory—when the `keep_input` option is True. + directories except the input directory when the `keep_input` option is True. """ self._raise_if_run_in_progress() @@ -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, diff --git a/scanpipe/templates/scanpipe/modals/project_reset_modal.html b/scanpipe/templates/scanpipe/modals/project_reset_modal.html index 43d07184e0..ade682ca12 100644 --- a/scanpipe/templates/scanpipe/modals/project_reset_modal.html +++ b/scanpipe/templates/scanpipe/modals/project_reset_modal.html @@ -21,6 +21,12 @@ {{ reset_form.keep_input }} {{ reset_form.keep_input.label }} + +