1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
# frozen_string_literal: true
module Clusters
class ClustersHierarchy
DEPTH_COLUMN = :depth
def initialize(clusterable)
@clusterable = clusterable
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
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
cte << same_namespace_management_clusters_query if clusterable.is_a?(::Project)
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)
order = <<~SQL
(CASE clusters.management_project_id
WHEN :project_id THEN 0
ELSE #{DEPTH_COLUMN}
END) ASC
SQL
values = {
project_id: clusterable.id
}
Arel.sql(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
|