summaryrefslogtreecommitdiff
path: root/db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb
diff options
context:
space:
mode:
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.rb167
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