147 lines
4.3 KiB
Ruby
147 lines
4.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Clusters
|
|
class ClustersHierarchy
|
|
DEPTH_COLUMN = :depth
|
|
|
|
def initialize(clusterable, include_management_project: true)
|
|
@clusterable = clusterable
|
|
@include_management_project = include_management_project
|
|
end
|
|
|
|
# Returns clusters in order from deepest to highest group
|
|
def base_and_ancestors
|
|
cte = recursive_cte
|
|
cte_alias = cte.table.alias(model.table_name)
|
|
|
|
model
|
|
.unscoped
|
|
.where.not('clusters.id' => nil)
|
|
.with
|
|
.recursive(cte.to_arel)
|
|
.from(cte_alias)
|
|
.order(depth_order_clause)
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :clusterable, :include_management_project
|
|
|
|
def recursive_cte
|
|
cte = Gitlab::SQL::RecursiveCTE.new(:clusters_cte)
|
|
|
|
base_query = case clusterable
|
|
when ::Group
|
|
group_clusters_base_query
|
|
when ::Project
|
|
project_clusters_base_query
|
|
else
|
|
raise ArgumentError, "unknown type for #{clusterable}"
|
|
end
|
|
|
|
if clusterable.is_a?(::Project) && include_management_project
|
|
cte << same_namespace_management_clusters_query
|
|
end
|
|
|
|
cte << base_query
|
|
cte << parent_query(cte)
|
|
|
|
cte
|
|
end
|
|
|
|
# Returns project-level clusters where the project is the management project
|
|
# for the cluster. The management project has to be in the same namespace /
|
|
# group as the cluster's project.
|
|
#
|
|
# Support for management project in sub-groups is planned in
|
|
# https://gitlab.com/gitlab-org/gitlab/issues/34650
|
|
#
|
|
# NB: group_parent_id is un-used but we still need to match the same number of
|
|
# columns as other queries in the CTE.
|
|
def same_namespace_management_clusters_query
|
|
clusterable.management_clusters
|
|
.project_type
|
|
.select([clusters_star, 'NULL AS group_parent_id', "0 AS #{DEPTH_COLUMN}"])
|
|
.for_project_namespace(clusterable.namespace_id)
|
|
end
|
|
|
|
# Management clusters should be first in the hierarchy so we use 0 for the
|
|
# depth column.
|
|
#
|
|
# Only applicable if the clusterable is a project (most especially when
|
|
# requesting project.deployment_platform).
|
|
def depth_order_clause
|
|
return { DEPTH_COLUMN => :asc } unless clusterable.is_a?(::Project) && include_management_project
|
|
|
|
order = <<~SQL
|
|
(CASE clusters.management_project_id
|
|
WHEN :project_id THEN 0
|
|
ELSE #{DEPTH_COLUMN}
|
|
END) ASC
|
|
SQL
|
|
|
|
values = {
|
|
project_id: clusterable.id
|
|
}
|
|
|
|
model.sanitize_sql_array([Arel.sql(order), values])
|
|
end
|
|
|
|
def group_clusters_base_query
|
|
group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id')
|
|
join_sources = ::Group.left_joins(:clusters).arel.join_sources
|
|
|
|
model
|
|
.unscoped
|
|
.select([clusters_star, group_parent_id_alias, "1 AS #{DEPTH_COLUMN}"])
|
|
.where(groups[:id].eq(clusterable.id))
|
|
.from(groups)
|
|
.joins(join_sources)
|
|
end
|
|
|
|
def project_clusters_base_query
|
|
projects = ::Project.arel_table
|
|
project_parent_id_alias = alias_as_column(projects[:namespace_id], 'group_parent_id')
|
|
join_sources = ::Project.left_joins(:clusters).arel.join_sources
|
|
|
|
model
|
|
.unscoped
|
|
.select([clusters_star, project_parent_id_alias, "1 AS #{DEPTH_COLUMN}"])
|
|
.where(projects[:id].eq(clusterable.id))
|
|
.from(projects)
|
|
.joins(join_sources)
|
|
end
|
|
|
|
def parent_query(cte)
|
|
group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id')
|
|
|
|
model
|
|
.unscoped
|
|
.select([clusters_star, group_parent_id_alias, cte.table[DEPTH_COLUMN] + 1])
|
|
.from([cte.table, groups])
|
|
.joins('LEFT OUTER JOIN cluster_groups ON cluster_groups.group_id = namespaces.id')
|
|
.joins('LEFT OUTER JOIN clusters ON cluster_groups.cluster_id = clusters.id')
|
|
.where(groups[:id].eq(cte.table[:group_parent_id]))
|
|
end
|
|
|
|
def model
|
|
Clusters::Cluster
|
|
end
|
|
|
|
def clusters
|
|
@clusters ||= model.arel_table
|
|
end
|
|
|
|
def groups
|
|
@groups ||= ::Group.arel_table
|
|
end
|
|
|
|
def clusters_star
|
|
@clusters_star ||= clusters[Arel.star]
|
|
end
|
|
|
|
def alias_as_column(value, alias_to)
|
|
Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to))
|
|
end
|
|
end
|
|
end
|