cluster.rb 9.74 KB
Newer Older
1 2
# frozen_string_literal: true

3
module Clusters
4
  class Cluster < ApplicationRecord
5
    include Presentable
6
    include Gitlab::Utils::StrongMemoize
7
    include FromUnion
Jacques Erasmus's avatar
Jacques Erasmus committed
8
    include ReactiveCaching
9

Shinya Maeda's avatar
Shinya Maeda committed
10
    self.table_name = 'clusters'
Jacques Erasmus's avatar
Jacques Erasmus committed
11
    self.reactive_cache_key = -> (cluster) { [cluster.class.model_name.singular, cluster.id] }
Shinya Maeda's avatar
Shinya Maeda committed
12

13 14
    PROJECT_ONLY_APPLICATIONS = {
      Applications::Jupyter.application_name => Applications::Jupyter,
15
      Applications::Knative.application_name => Applications::Knative
16
    }.freeze
17
    APPLICATIONS = {
Kamil Trzcinski's avatar
Kamil Trzcinski committed
18
      Applications::Helm.application_name => Applications::Helm,
19
      Applications::Ingress.application_name => Applications::Ingress,
Amit Rathi's avatar
Amit Rathi committed
20
      Applications::CertManager.application_name => Applications::CertManager,
21 22
      Applications::Runner.application_name => Applications::Runner,
      Applications::Prometheus.application_name => Applications::Prometheus
23
    }.merge(PROJECT_ONLY_APPLICATIONS).freeze
24
    DEFAULT_ENVIRONMENT = '*'.freeze
25
    KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze
26

27 28
    belongs_to :user

Shinya Maeda's avatar
Shinya Maeda committed
29
    has_many :cluster_projects, class_name: 'Clusters::Project'
30
    has_many :projects, through: :cluster_projects, class_name: '::Project'
31
    has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
32

33 34 35
    has_many :cluster_groups, class_name: 'Clusters::Group'
    has_many :groups, through: :cluster_groups, class_name: '::Group'

36 37 38
    # we force autosave to happen when we save `Cluster` model
    has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true

39
    has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
40

41
    has_one :application_helm, class_name: 'Clusters::Applications::Helm'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
42
    has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
Amit Rathi's avatar
Amit Rathi committed
43
    has_one :application_cert_manager, class_name: 'Clusters::Applications::CertManager'
44
    has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
45
    has_one :application_runner, class_name: 'Clusters::Applications::Runner'
46
    has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
Chris Baumbauer's avatar
Chris Baumbauer committed
47
    has_one :application_knative, class_name: 'Clusters::Applications::Knative'
48

49 50
    has_many :kubernetes_namespaces

51
    accepts_nested_attributes_for :provider_gcp, update_only: true
52
    accepts_nested_attributes_for :platform_kubernetes, update_only: true
53

Shinya Maeda's avatar
Shinya Maeda committed
54
    validates :name, cluster_name: true
55
    validates :cluster_type, presence: true
56
    validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
57

58
    validate :restrict_modification, on: :update
59 60 61
    validate :no_groups, unless: :group_type?
    validate :no_projects, unless: :project_type?

Jacques Erasmus's avatar
Jacques Erasmus committed
62 63
    after_save :clear_reactive_cache!

Kamil Trzcinski's avatar
Kamil Trzcinski committed
64
    delegate :status, to: :provider, allow_nil: true
65
    delegate :status_reason, to: :provider, allow_nil: true
Shinya Maeda's avatar
Shinya Maeda committed
66
    delegate :on_creation?, to: :provider, allow_nil: true
67

68
    delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
69
    delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
70 71 72
    delegate :available?, to: :application_helm, prefix: true, allow_nil: true
    delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
    delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
73
    delegate :available?, to: :application_knative, prefix: true, allow_nil: true
74
    delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
75
    delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true
76

77
    alias_attribute :base_domain, :domain
78
    alias_attribute :provided_by_user?, :user?
79

80 81 82 83 84 85
    enum cluster_type: {
      instance_type: 1,
      group_type: 2,
      project_type: 3
    }

86 87 88 89 90 91 92 93 94 95 96
    enum platform_type: {
      kubernetes: 1
    }

    enum provider_type: {
      user: 0,
      gcp: 1
    }

    scope :enabled, -> { where(enabled: true) }
    scope :disabled, -> { where(enabled: false) }
97 98
    scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
    scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
99
    scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
100
    scope :managed, -> { where(managed: true) }
101

102
    scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
103

104 105 106 107 108 109
    scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do
      subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.cluster_id = clusters.id')

      where('NOT EXISTS (?)', subquery)
    end

110
    scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.available) }
111 112 113

    scope :preload_knative, -> {
      preload(
114
        :kubernetes_namespaces,
115 116 117 118 119
        :platform_kubernetes,
        :application_knative
      )
    }

120
    def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
121 122
      return [] if clusterable.is_a?(Instance)

123 124
      hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters)
      hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope
125

126
      hierarchy_groups.flat_map(&:clusters) + Instance.new.clusters
127 128
    end

129
    def status_name
Jacques Erasmus's avatar
Jacques Erasmus committed
130 131 132 133 134 135
      provider&.status_name || connection_status.presence || :created
    end

    def connection_status
      with_reactive_cache do |data|
        data[:connection_status]
136 137 138
      end
    end

Jacques Erasmus's avatar
Jacques Erasmus committed
139 140 141 142
    def calculate_reactive_cache
      return unless enabled?

      { connection_status: retrieve_connection_status }
143 144
    end

145
    def applications
146
      [
Kamil Trzcinski's avatar
Kamil Trzcinski committed
147
        application_helm || build_application_helm,
148
        application_ingress || build_application_ingress,
Amit Rathi's avatar
Amit Rathi committed
149
        application_cert_manager || build_application_cert_manager,
150
        application_prometheus || build_application_prometheus,
151
        application_runner || build_application_runner,
Chris Baumbauer's avatar
Chris Baumbauer committed
152 153
        application_jupyter || build_application_jupyter,
        application_knative || build_application_knative
154 155 156
      ]
    end

157
    def provider
Shinya Maeda's avatar
Shinya Maeda committed
158
      return provider_gcp if gcp?
159 160 161
    end

    def platform
Shinya Maeda's avatar
Shinya Maeda committed
162 163 164
      return platform_kubernetes if kubernetes?
    end

165 166 167 168 169 170 171 172 173 174
    def all_projects
      if project_type?
        projects
      elsif group_type?
        first_group.all_projects
      else
        Project.none
      end
    end

175
    def first_project
176 177 178
      strong_memoize(:first_project) do
        projects.first
      end
179
    end
180
    alias_method :project, :first_project
Shinya Maeda's avatar
Shinya Maeda committed
181

182 183 184 185 186 187 188
    def first_group
      strong_memoize(:first_group) do
        groups.first
      end
    end
    alias_method :group, :first_group

189 190 191 192
    def instance
      Instance.new if instance_type?
    end

193 194 195 196
    def kubeclient
      platform_kubernetes.kubeclient if kubernetes?
    end

197 198 199 200
    def kubernetes_namespace_for(project)
      find_or_initialize_kubernetes_namespace_for_project(project).namespace
    end

201
    def find_or_initialize_kubernetes_namespace_for_project(project)
202 203 204 205 206
      attributes = { project: project }
      attributes[:cluster_project] = cluster_project if project_type?

      kubernetes_namespaces.find_or_initialize_by(attributes).tap do |namespace|
        namespace.set_defaults
207
      end
208 209
    end

210 211 212 213
    def allow_user_defined_namespace?
      project_type?
    end

214
    def kube_ingress_domain
215
      @kube_ingress_domain ||= domain.presence || instance_domain
216 217 218 219
    end

    def predefined_variables
      Gitlab::Ci::Variables::Collection.new.tap do |variables|
220
        break variables unless kube_ingress_domain
221

222
        variables.append(key: KUBE_INGRESS_BASE_DOMAIN, value: kube_ingress_domain)
223 224 225
      end
    end

Shinya Maeda's avatar
Shinya Maeda committed
226 227
    private

228
    def instance_domain
229 230 231
      @instance_domain ||= Gitlab::CurrentSettings.auto_devops_domain
    end

Jacques Erasmus's avatar
Jacques Erasmus committed
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
    def retrieve_connection_status
      kubeclient.core_client.discover
    rescue *Gitlab::Kubernetes::Errors::CONNECTION
      :unreachable
    rescue *Gitlab::Kubernetes::Errors::AUTHENTICATION
      :authentication_failure
    rescue Kubeclient::HttpError => e
      kubeclient_error_status(e.message)
    rescue => e
      Gitlab::Sentry.track_acceptable_exception(e, extra: { cluster_id: id })

      :unknown_failure
    else
      :connected
    end

    # KubeClient uses the same error class
    # For connection errors (eg. timeout) and
    # for Kubernetes errors.
    def kubeclient_error_status(message)
      if message&.match?(/timed out|timeout/i)
        :unreachable
      else
        :authentication_failure
      end
    end

    # To keep backward compatibility with AUTO_DEVOPS_DOMAIN
    # environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN
    # is set if AUTO_DEVOPS_DOMAIN is set on any of the following options:
    # ProjectAutoDevops#Domain, project variables or group variables,
    # as the AUTO_DEVOPS_DOMAIN is needed for CI_ENVIRONMENT_URL
    #
    # This method should is scheduled to be removed on
    # https://gitlab.com/gitlab-org/gitlab-ce/issues/56959
    def legacy_auto_devops_domain
      if project_type?
        project&.auto_devops&.domain.presence ||
          project.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence ||
          project.group&.variables&.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence
      elsif group_type?
        group.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence
      end
    end

Shinya Maeda's avatar
Shinya Maeda committed
277 278 279 280 281 282 283 284
    def restrict_modification
      if provider&.on_creation?
        errors.add(:base, "cannot modify during creation")
        return false
      end

      true
    end
285 286 287 288 289 290 291 292 293 294 295 296

    def no_groups
      if groups.any?
        errors.add(:cluster, 'cannot have groups assigned')
      end
    end

    def no_projects
      if projects.any?
        errors.add(:cluster, 'cannot have projects assigned')
      end
    end
297 298
  end
end