summaryrefslogtreecommitdiff
path: root/app/models/clusters/clusters_hierarchy.rb
blob: 9435d258d67cbdcabd03cd7bfd52ec3c443482f1 (plain)
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
145
146
# 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

      if clusterable.is_a?(::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)

      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