gitlab-org--gitlab-foss/app/models/clusters/clusters_hierarchy.rb

148 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('clusters.id IS NOT NULL')
.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