diff options
Diffstat (limited to 'db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb')
-rw-r--r-- | db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb b/db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb new file mode 100644 index 00000000000..b351940092a --- /dev/null +++ b/db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +# This migration adds or updates the routes for all the entities affected by +# post-migration '20200511083541_cleanup_projects_with_missing_namespace' +# - A route is added for the 'lost-and-found' group +# - A route is added for the Ghost user (if not already defined) +# - The routes for all the orphaned projects that were moved under the 'lost-and-found' +# group are updated to reflect the new path +class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migration[6.0] + DOWNTIME = false + + class User < ActiveRecord::Base + self.table_name = 'users' + + LOST_AND_FOUND_GROUP = 'lost-and-found' + USER_TYPE_GHOST = 5 + ACCESS_LEVEL_OWNER = 50 + + has_one :namespace, -> { where(type: nil) }, + foreign_key: :owner_id, inverse_of: :owner, autosave: true, + class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace' + + def lost_and_found_group + # Find the 'lost-and-found' group + # There should only be one Group owned by the Ghost user starting with 'lost-and-found' + Group + .joins('INNER JOIN members ON namespaces.id = members.source_id') + .where('namespaces.type = ?', 'Group') + .where('members.type = ?', 'GroupMember') + .where('members.source_type = ?', 'Namespace') + .where('members.user_id = ?', self.id) + .where('members.access_level = ?', ACCESS_LEVEL_OWNER) + .find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%")) + end + + class << self + # Return the ghost user + def ghost + User.find_by(user_type: USER_TYPE_GHOST) + end + end + end + + # Temporary Concern to not repeat the same methods twice + module HasPath + extend ActiveSupport::Concern + + def full_path + if parent && path + parent.full_path + '/' + path + else + path + end + end + + def full_name + if parent && name + parent.full_name + ' / ' + name + else + name + end + end + end + + class Namespace < ActiveRecord::Base + include HasPath + + self.table_name = 'namespaces' + + belongs_to :owner, class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::User' + belongs_to :parent, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace" + has_many :children, foreign_key: :parent_id, + class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace" + has_many :projects, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Project" + + def ensure_route! + unless Route.for_source('Namespace', self.id) + Route.create!( + source_id: self.id, + source_type: 'Namespace', + path: self.full_path, + name: self.full_name + ) + end + end + end + + class Group < Namespace + # Disable STI to allow us to manually set "type = 'Group'" + # Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::Group" + self.inheritance_column = :_type_disabled + end + + class Route < ActiveRecord::Base + self.table_name = 'routes' + + def self.for_source(source_type, source_id) + Route.find_by(source_type: source_type, source_id: source_id) + end + end + + class Project < ActiveRecord::Base + include HasPath + + self.table_name = 'projects' + + belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id', + class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Group" + belongs_to :namespace, + class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace" + + alias_method :parent, :namespace + alias_attribute :parent_id, :namespace_id + + def ensure_route! + Route.find_or_initialize_by(source_type: 'Project', source_id: self.id).tap do |record| + record.path = self.full_path + record.name = self.full_name + record.save! + end + end + end + + def up + # Reset the column information of all the models that update the database + # to ensure the Active Record's knowledge of the table structure is current + Namespace.reset_column_information + Route.reset_column_information + + # Find the ghost user, its namespace and the "lost and found" group + ghost_user = User.ghost + return unless ghost_user # No reason to continue if there is no Ghost user + + ghost_namespace = ghost_user.namespace + lost_and_found_group = ghost_user.lost_and_found_group + + # No reason to continue if there is no 'lost-and-found' group + # 1. No orphaned projects were found in this instance, or + # 2. The 'lost-and-found' group and the orphaned projects have been already deleted + return unless lost_and_found_group + + # Update the 'lost-and-found' group description to be more self-explanatory + lost_and_found_group.description = + 'Group for storing projects that were not properly deleted. '\ + 'It should be considered as a system level Group with non-working '\ + 'projects inside it. The contents may be deleted with a future update. '\ + 'More info: gitlab.com/gitlab-org/gitlab/-/issues/198603' + lost_and_found_group.save! + + # Update the routes for the Ghost user, the "lost and found" group + # and all the orphaned projects + ghost_namespace.ensure_route! + lost_and_found_group.ensure_route! + + # The following does a fast index scan by namespace_id + # No reason to process in batches: + # - 66 projects in GitLab.com, less than 1ms execution time to fetch them + # with a constant update time for each + lost_and_found_group.projects.each do |project| + project.ensure_route! + end + end + + def down + # no-op + end +end |