Read about our upcoming Code of Conduct on this issue

environment.rb 7.48 KB
Newer Older
1
2
# frozen_string_literal: true

3
class Environment < ApplicationRecord
4
  include Gitlab::Utils::StrongMemoize
5
6
  include ReactiveCaching

7
8
9
  self.reactive_cache_refresh_interval = 1.minute
  self.reactive_cache_lifetime = 55.seconds

10
  belongs_to :project, required: true
11

12
13
  has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :successful_deployments, -> { success }, class_name: 'Deployment'
14

15
  has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
16
17
  has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
  has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
18
  has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
19
20
  has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
  has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
21

Z.J. van de Weg's avatar
Z.J. van de Weg committed
22
  before_validation :nullify_external_url
Nick Thomas's avatar
Nick Thomas committed
23
24
  before_validation :generate_slug, if: ->(env) { env.slug.blank? }

25
  before_save :set_environment_type
26
  after_save :clear_reactive_cache!
Z.J. van de Weg's avatar
Z.J. van de Weg committed
27

28
29
  validates :name,
            presence: true,
Kamil Trzcinski's avatar
Kamil Trzcinski committed
30
            uniqueness: { scope: :project_id },
31
            length: { maximum: 255 },
32
33
            format: { with: Gitlab::Regex.environment_name_regex,
                      message: Gitlab::Regex.environment_name_regex_message }
34

Nick Thomas's avatar
Nick Thomas committed
35
36
37
38
39
40
41
  validates :slug,
            presence: true,
            uniqueness: { scope: :project_id },
            length: { maximum: 24 },
            format: { with: Gitlab::Regex.environment_slug_regex,
                      message: Gitlab::Regex.environment_slug_regex_message }

42
  validates :external_url,
Z.J. van de Weg's avatar
Z.J. van de Weg committed
43
44
            length: { maximum: 255 },
            allow_nil: true,
45
            addressable_url: true
46

47
  delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
48

Kamil Trzcinski's avatar
Kamil Trzcinski committed
49
50
  scope :available, -> { with_state(:available) }
  scope :stopped, -> { with_state(:stopped) }
Douwe Maan's avatar
Douwe Maan committed
51
52
  scope :order_by_last_deployed_at, -> do
    max_deployment_id_sql =
53
54
55
      Deployment.select(Deployment.arel_table[:id].maximum)
      .where(Deployment.arel_table[:environment_id].eq(arel_table[:id]))
      .to_sql
Douwe Maan's avatar
Douwe Maan committed
56
57
    order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
  end
58
  scope :in_review_folder, -> { where(environment_type: "review") }
59
  scope :for_name, -> (name) { where(name: name) }
60
  scope :preload_cluster, -> { preload(last_deployment: :cluster) }
61
62
63
64
65

  ##
  # Search environments which have names like the given query.
  # Do not set a large limit unless you've confirmed that it works on gitlab.com scale.
  scope :for_name_like, -> (query, limit: 5) do
66
    where(arel_table[:name].matches("#{sanitize_sql_like query}%")).limit(limit)
67
68
  end

69
  scope :for_project, -> (project) { where(project_id: project) }
70
  scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
71
  scope :unfoldered, -> { where(environment_type: nil) }
72
73
74
  scope :with_rank, -> do
    select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
75

76
77
78
  state_machine :state, initial: :available do
    event :start do
      transition stopped: :available
79
80
    end

81
82
    event :stop do
      transition available: :stopped
83
84
    end

85
86
    state :available
    state :stopped
87
88
89
90

    after_transition do |environment|
      environment.expire_etag_cache
    end
91
92
  end

93
94
95
96
  def self.pluck_names
    pluck(:name)
  end

97
98
99
100
  def self.find_or_create_by_name(name)
    find_or_create_by(name: name)
  end

101
  def predefined_variables
102
103
104
    Gitlab::Ci::Variables::Collection.new
      .append(key: 'CI_ENVIRONMENT_NAME', value: name)
      .append(key: 'CI_ENVIRONMENT_SLUG', value: slug)
105
106
  end

107
  def recently_updated_on_branch?(ref)
108
    ref.to_s == last_deployment.try(:ref)
109
110
  end

Z.J. van de Weg's avatar
Z.J. van de Weg committed
111
112
113
  def nullify_external_url
    self.external_url = nil if self.external_url.blank?
  end
114

115
116
117
  def set_environment_type
    names = name.split('/')

118
    self.environment_type = names.many? ? names.first : nil
119
120
  end

121
  def includes_commit?(commit)
Z.J. van de Weg's avatar
Z.J. van de Weg committed
122
    return false unless last_deployment
123

124
    last_deployment.includes_commit?(commit)
125
  end
126

127
  def last_deployed_at
Shinya Maeda's avatar
Shinya Maeda committed
128
    last_deployment.try(:created_at)
129
130
  end

131
  def update_merge_request_metrics?
132
    folder_name == "production"
133
  end
134

135
136
137
  def first_deployment_for(commit_sha)
    ref = project.repository.ref_name_for_sha(ref_path, commit_sha)

138
    return unless ref
139
140
141
142
143

    deployment_iid = ref.split('/').last
    deployments.find_by(iid: deployment_iid)
  end

144
  def ref_path
145
    "refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
146
  end
Z.J. van de Weg's avatar
Z.J. van de Weg committed
147
148

  def formatted_external_url
149
    return unless external_url
Z.J. van de Weg's avatar
Z.J. van de Weg committed
150

151
    external_url.gsub(%r{\A.*?://}, '')
Z.J. van de Weg's avatar
Z.J. van de Weg committed
152
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
153

154
  def stop_action_available?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
155
156
    available? && stop_action.present?
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
157

Kamil Trzcinski's avatar
Kamil Trzcinski committed
158
  def stop_with_action!(current_user)
159
    return unless available?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
160

Kamil Trzcinski's avatar
Kamil Trzcinski committed
161
    stop!
162
    stop_action&.play(current_user)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
163
  end
164
165
166
167

  def actions_for(environment)
    return [] unless manual_actions

168
169
    manual_actions.select do |action|
      action.expanded_environment_name == environment
170
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
171
  end
Nick Thomas's avatar
Nick Thomas committed
172

173
  def has_terminals?
174
    available? && deployment_platform.present? && last_deployment.present?
175
176
177
  end

  def terminals
178
179
180
181
182
183
184
185
186
187
188
189
190
    with_reactive_cache do |data|
      deployment_platform.terminals(self, data)
    end
  end

  def calculate_reactive_cache
    return unless has_terminals? && !project.pending_delete?

    deployment_platform.calculate_reactive_cache_for(self)
  end

  def deployment_namespace
    strong_memoize(:kubernetes_namespace) do
191
      deployment_platform.cluster.kubernetes_namespace_for(self) if deployment_platform
192
    end
193
194
  end

195
  def has_metrics?
196
    available? && prometheus_adapter&.configured?
197
198
199
  end

  def metrics
200
    prometheus_adapter.query(:environment, self) if has_metrics? && prometheus_adapter.can_query?
201
202
  end

203
204
205
206
  def prometheus_status
    deployment_platform&.cluster&.application_prometheus&.status_name
  end

207
208
209
210
  def additional_metrics(*args)
    return unless has_metrics?

    prometheus_adapter.query(:additional_metrics_environment, self, *args.map(&:to_f))
211
212
  end

213
  # rubocop: disable CodeReuse/ServiceClass
214
215
216
  def prometheus_adapter
    @prometheus_adapter ||= Prometheus::AdapterService.new(project, deployment_platform).prometheus_adapter
  end
217
  # rubocop: enable CodeReuse/ServiceClass
218

219
220
221
222
  def slug
    super.presence || generate_slug
  end

223
224
225
226
227
228
  def external_url_for(path, commit_sha)
    return unless self.external_url

    public_path = project.public_path_for_source_path(path, commit_sha)
    return unless public_path

229
    [external_url.delete_suffix('/'), public_path.delete_prefix('/')].join('/')
230
231
  end

232
233
  def expire_etag_cache
    Gitlab::EtagCaching::Store.new.tap do |store|
234
      store.touch(etag_cache_key)
235
236
237
    end
  end

238
  def etag_cache_key
239
    Gitlab::Routing.url_helpers.project_environments_path(
Z.J. van de Weg's avatar
Z.J. van de Weg committed
240
241
      project,
      format: :json)
242
243
  end

244
245
246
247
  def folder_name
    self.environment_type || self.name
  end

248
249
250
251
  def name_without_type
    @name_without_type ||= name.delete_prefix("#{environment_type}/")
  end

252
  def deployment_platform
253
254
255
    strong_memoize(:deployment_platform) do
      project.deployment_platform(environment: self.name)
    end
256
257
  end

258
259
260
261
262
263
  def knative_services_finder
    if last_deployment&.cluster
      Clusters::KnativeServicesFinder.new(last_deployment.cluster, self)
    end
  end

Nick Thomas's avatar
Nick Thomas committed
264
265
  private

266
267
  def generate_slug
    self.slug = Gitlab::Slug::Environment.new(name).generate
Nick Thomas's avatar
Nick Thomas committed
268
  end
269
end
270
271

Environment.prepend_if_ee('EE::Environment')