# frozen_string_literal: true module Environments class ScheduleToDeleteReviewAppsService < ::BaseService include ::Gitlab::ExclusiveLeaseHelpers EXCLUSIVE_LOCK_KEY_BASE = 'environments:delete_review_apps:lock' LOCK_TIMEOUT = 2.minutes def execute if validation_error = validate return validation_error end mark_deletable_environments end private def key "#{EXCLUSIVE_LOCK_KEY_BASE}:#{project.id}" end def dry_run? return true if params[:dry_run].nil? params[:dry_run] end def validate return if can?(current_user, :destroy_environment, project) Result.new(error_message: "You do not have permission to destroy environments in this project", status: :unauthorized) end def mark_deletable_environments in_lock(key, ttl: LOCK_TIMEOUT, retries: 1) do unsafe_mark_deletable_environments end rescue FailedToObtainLockError Result.new(error_message: "Another process is already processing a delete request. Please retry later.", status: :conflict) end def unsafe_mark_deletable_environments result = Result.new environments = project.environments .not_scheduled_for_deletion .stopped_review_apps(params[:before], params[:limit]) # Check if the actor has write permission to a potentially-protected environment. deletable, failed = *environments.partition { |env| current_user.can?(:destroy_environment, env) } if deletable.any? && failed.empty? mark_for_deletion(deletable) unless dry_run? result.set_status(:ok) result.set_scheduled_entries(deletable) else result.set_status( :bad_request, error_message: "No environments found for scheduled deletion. Either your query did not match any environments (default parameters match environments that are 30 days or older), or you have insufficient permissions to delete matching environments." ) result.set_unprocessable_entries(failed) end result end def mark_for_deletion(deletable_environments) Environment.id_in(deletable_environments).schedule_to_delete end class Result attr_accessor :scheduled_entries, :unprocessable_entries, :error_message, :status def initialize(scheduled_entries: [], unprocessable_entries: [], error_message: nil, status: nil) self.scheduled_entries = scheduled_entries self.unprocessable_entries = unprocessable_entries self.error_message = error_message self.status = status end def success? status == :ok end def set_status(status, error_message: nil) self.status = status self.error_message = error_message end def set_scheduled_entries(entries) self.scheduled_entries = entries end def set_unprocessable_entries(entries) self.unprocessable_entries = entries end end end end