Move project restore from EE to CE in projects API

What does this MR do and why?

Due to a recent data-loss incident, the decision was made to down-tier delayed deletion. This MR moves delayed deletion logic from the EE code for the projects restore API into CE. The down-tier is gated by the downtier_delayed_deletion feature flag and the API functions as normal unless that feature flag is enabled.

References

Related to #534189 (closed)

How to set up and validate locally

  1. Run GDK in SaaS mode.
    export GITLAB_SIMULATE_SAAS=1
  2. Enable the feature flag
    Feature.enable(:downtier_delayed_deletion)
  3. Delete and restore a project in a group with the API. Both actions should be successful.
    ▶ curl --header 'PRIVATE-TOKEN: TOKEN' -X DELETE 'https://gdk.test:3443/api/v4/projects/72'
    {"message":"202 Accepted"}
    
    ▶ curl --header 'PRIVATE-TOKEN: TOKEN' -X POST 'https://gdk.test:3443/api/v4/projects/72/restore'
    {"id":72,"description":null,"name":"disabled","name_with_namespace":"test-delete / disabled","path":"disabled","path_with_namespace":"test-delete/disabled","created_at":"2025-04-07T19:54:18.184Z","default_branch":"main","tag_list":[],"topics":[],"ssh_url_to_repo":"ssh://git@gdk.test:2222/test-delete/disabled.git","http_url_to_repo":"https://gdk.test:3443/test-delete/disabled.git","web_url":"https://gdk.test:3443/test-delete/disabled","readme_url":"https://gdk.test:3443/test-delete/disabled/-/blob/main/README.md","forks_count":0,"avatar_url":null,"star_count":0,"last_activity_at":"2025-04-07T19:55:23.002Z","namespace":{"id":143,"name":"test-delete","path":"test-delete","kind":"group","full_path":"test-delete","parent_id":null,"avatar_url":null,"web_url":"https://gdk.test:3443/groups/test-delete"},"repository_storage":"default","_links":{"self":"https://gdk.test:3443/api/v4/projects/72","issues":"https://gdk.test:3443/api/v4/projects/72/issues","merge_requests":"https://gdk.test:3443/api/v4/projects/72/merge_requests","repo_branches":"https://gdk.test:3443/api/v4/projects/72/repository/branches","labels":"https://gdk.test:3443/api/v4/projects/72/labels","events":"https://gdk.test:3443/api/v4/projects/72/events","members":"https://gdk.test:3443/api/v4/projects/72/members","cluster_agents":"https://gdk.test:3443/api/v4/projects/72/cluster_agents"},"packages_enabled":true,"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_expiration_policy":{"cadence":"1d","enabled":false,"keep_n":10,"older_than":"90d","name_regex":".*","name_regex_keep":null,"next_run_at":"2025-04-08T19:54:18.210Z"},"repository_object_format":"sha1","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":false,"service_desk_address":null,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"private","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","model_registry_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":1,"import_url":null,"import_type":null,"import_status":"none","open_issues_count":0,"description_html":"","updated_at":"2025-04-07T19:55:23.057Z","ci_default_git_depth":20,"ci_delete_pipelines_in_seconds":null,"ci_forward_deployment_enabled":true,"ci_forward_deployment_rollback_allowed":true,"ci_job_token_scope_enabled":false,"ci_separated_caches":true,"ci_allow_fork_pipelines_to_run_in_parent_project":true,"ci_id_token_sub_claim_components":["project_path","ref_type","ref"],"build_git_strategy":"fetch","keep_latest_artifact":true,"restrict_user_defined_variables":false,"ci_pipeline_variables_minimum_override_role":"developer","runners_token":"GR1348941SZ7Fs2QJWvjdhbagN-y5","runner_token_expiration_interval":null,"group_runners_enabled":true,"auto_cancel_pending_pipelines":"enabled","build_timeout":3600,"auto_devops_enabled":true,"auto_devops_deploy_strategy":"continuous","ci_push_repository_for_job_token_allowed":false,"ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"merge","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"warn_about_potentially_unwanted_characters":true,"autoclose_referenced_issues":true,"max_artifacts_size":null,"external_authorization_classification_label":null,"requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":true,"compliance_frameworks":[]}
  4. Disable the feature flag
    Feature.disable(:downtier_delayed_deletion)
  5. Delete and restore a project in a group with the API. The restore should fail with a 404 error.
    ▶ curl --header 'PRIVATE-TOKEN: TOKEN' -X DELETE 'https://gdk.test:3443/api/v4/projects/72'
    {"message":"202 Accepted"}
    
    ▶ curl --header 'PRIVATE-TOKEN: TOKEN' -X POST 'https://gdk.test:3443/api/v4/projects/72/restore'
    {"message":"404 Group Not Found"}

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Ian Anderson

Merge request reports

Loading