2020-06-16 12:09:00 +00:00
|
|
|
# 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
|
|
|
|
|
|
|
|
def generate_unique_path
|
|
|
|
# Generate a unique path if there is no route for the namespace
|
|
|
|
# (an existing route guarantees that the path is already unique)
|
|
|
|
unless Route.for_source('Namespace', self.id)
|
|
|
|
self.path = Uniquify.new.string(self.path) do |str|
|
|
|
|
Route.where(path: str).exists?
|
|
|
|
end
|
|
|
|
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
|
2020-07-07 12:09:16 +00:00
|
|
|
User.reset_column_information
|
2020-06-16 12:09:00 +00:00
|
|
|
|
|
|
|
# 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.generate_unique_path
|
|
|
|
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!
|
|
|
|
|
2020-07-07 12:09:16 +00:00
|
|
|
# make sure that the ghost namespace has a unique path
|
|
|
|
ghost_namespace.generate_unique_path
|
|
|
|
|
|
|
|
if ghost_namespace.path_changed?
|
|
|
|
ghost_namespace.save!
|
|
|
|
# If the path changed, also update the Ghost User's username to match the new path.
|
|
|
|
ghost_user.update!(username: ghost_namespace.path)
|
|
|
|
end
|
|
|
|
|
2020-06-16 12:09:00 +00:00
|
|
|
# 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
|