2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2019-07-07 11:18:12 +05:30
class Environment < ApplicationRecord
2019-02-15 15:39:39 +05:30
include Gitlab :: Utils :: StrongMemoize
2019-09-30 21:07:59 +05:30
include ReactiveCaching
2020-04-22 19:07:51 +05:30
include FastDestroyAll :: Helpers
2021-01-03 14:25:43 +05:30
include Presentable
2021-10-27 15:23:28 +05:30
include NullifyIfBlank
2019-09-30 21:07:59 +05:30
2019-12-26 22:10:19 +05:30
self . reactive_cache_refresh_interval = 1 . minute
self . reactive_cache_lifetime = 55 . seconds
2020-03-13 15:44:24 +05:30
self . reactive_cache_hard_limit = 10 . megabytes
2020-05-24 23:13:21 +05:30
self . reactive_cache_work_type = :external_dependency
2019-12-26 22:10:19 +05:30
2022-06-21 17:19:12 +05:30
belongs_to :project , optional : false
2016-06-16 23:09:34 +05:30
2020-04-22 19:07:51 +05:30
use_fast_destroy :all_deployments
2021-10-27 15:23:28 +05:30
nullify_if_blank :external_url
2020-04-22 19:07:51 +05:30
has_many :all_deployments , class_name : 'Deployment'
has_many :deployments , - > { visible }
2019-12-21 20:55:43 +05:30
has_many :successful_deployments , - > { success } , class_name : 'Deployment'
2020-03-13 15:44:24 +05:30
has_many :active_deployments , - > { active } , class_name : 'Deployment'
has_many :prometheus_alerts , inverse_of : :environment
2020-04-22 19:07:51 +05:30
has_many :metrics_dashboard_annotations , class_name : 'Metrics::Dashboard::Annotation' , inverse_of : :environment
has_many :self_managed_prometheus_alert_events , inverse_of : :environment
2020-07-28 23:09:34 +05:30
has_many :alert_management_alerts , class_name : 'AlertManagement::Alert' , inverse_of : :environment
2018-03-17 18:26:18 +05:30
2022-07-16 23:28:13 +05:30
has_one :last_deployment , - > { success . ordered } , class_name : 'Deployment' , inverse_of : :environment
2019-12-26 22:10:19 +05:30
has_one :last_visible_deployment , - > { visible . distinct_on_environment } , inverse_of : :environment , class_name : 'Deployment'
2021-11-18 22:05:49 +05:30
has_one :last_visible_deployable , through : :last_visible_deployment , source : 'deployable' , source_type : 'CommitStatus' , disable_joins : true
has_one :last_visible_pipeline , through : :last_visible_deployable , source : 'pipeline' , disable_joins : true
2021-11-11 11:23:49 +05:30
2022-01-26 12:08:38 +05:30
has_one :upcoming_deployment , - > { upcoming . distinct_on_environment } , class_name : 'Deployment' , inverse_of : :environment
2020-10-24 23:57:45 +05:30
has_one :latest_opened_most_severe_alert , - > { order_severity_with_open_prometheus_alert } , class_name : 'AlertManagement::Alert' , inverse_of : :environment
2016-06-16 23:09:34 +05:30
2017-08-17 22:00:37 +05:30
before_validation :generate_slug , if : - > ( env ) { env . slug . blank? }
2016-09-29 09:46:39 +05:30
before_save :set_environment_type
2021-04-17 20:07:23 +05:30
before_save :ensure_environment_tier
2019-09-30 21:07:59 +05:30
after_save :clear_reactive_cache!
2016-09-13 17:45:13 +05:30
2016-06-16 23:09:34 +05:30
validates :name ,
presence : true ,
uniqueness : { scope : :project_id } ,
2017-08-17 22:00:37 +05:30
length : { maximum : 255 } ,
2016-06-16 23:09:34 +05:30
format : { with : Gitlab :: Regex . environment_name_regex ,
message : Gitlab :: Regex . environment_name_regex_message }
2017-08-17 22:00:37 +05:30
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 }
2016-09-13 17:45:13 +05:30
validates :external_url ,
length : { maximum : 255 } ,
allow_nil : true ,
2019-07-31 22:56:46 +05:30
addressable_url : true
2016-09-13 17:45:13 +05:30
2022-07-23 23:45:48 +05:30
delegate :manual_actions , :other_manual_actions , to : :last_deployment , allow_nil : true
2021-02-22 17:27:13 +05:30
delegate :auto_rollback_enabled? , to : :project
2016-11-03 12:29:30 +05:30
scope :available , - > { with_state ( :available ) }
scope :stopped , - > { with_state ( :stopped ) }
2020-03-13 15:44:24 +05:30
2017-08-17 22:00:37 +05:30
scope :order_by_last_deployed_at , - > do
2022-06-21 17:19:12 +05:30
order ( Arel :: Nodes :: Grouping . new ( max_deployment_id_query ) . asc . nulls_first )
2017-08-17 22:00:37 +05:30
end
2020-03-13 15:44:24 +05:30
scope :order_by_last_deployed_at_desc , - > do
2022-06-21 17:19:12 +05:30
order ( Arel :: Nodes :: Grouping . new ( max_deployment_id_query ) . desc . nulls_last )
2020-03-13 15:44:24 +05:30
end
2021-01-03 14:25:43 +05:30
scope :order_by_name , - > { order ( 'environments.name ASC' ) }
2020-03-13 15:44:24 +05:30
2017-09-10 17:25:29 +05:30
scope :in_review_folder , - > { where ( environment_type : " review " ) }
2018-12-13 13:39:08 +05:30
scope :for_name , - > ( name ) { where ( name : name ) }
2019-10-12 21:52:04 +05:30
scope :preload_cluster , - > { preload ( last_deployment : :cluster ) }
2021-11-11 11:23:49 +05:30
scope :preload_project , - > { preload ( :project ) }
2020-03-13 15:44:24 +05:30
scope :auto_stoppable , - > ( limit ) { available . where ( 'auto_stop_at < ?' , Time . zone . now ) . limit ( limit ) }
2021-10-27 15:23:28 +05:30
scope :auto_deletable , - > ( limit ) { stopped . where ( 'auto_delete_at < ?' , Time . zone . now ) . limit ( limit ) }
2019-03-02 22:35:43 +05:30
##
# 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
2019-12-04 20:38:33 +05:30
where ( arel_table [ :name ] . matches ( " #{ sanitize_sql_like query } % " ) ) . limit ( limit )
2019-03-02 22:35:43 +05:30
end
2018-12-13 13:39:08 +05:30
scope :for_project , - > ( project ) { where ( project_id : project ) }
2021-04-29 21:17:54 +05:30
scope :for_tier , - > ( tier ) { where ( tier : tier ) . where . not ( tier : nil ) }
2019-12-26 22:10:19 +05:30
scope :unfoldered , - > { where ( environment_type : nil ) }
scope :with_rank , - > do
select ( 'environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)' )
end
2021-01-03 14:25:43 +05:30
scope :for_id , - > ( id ) { where ( id : id ) }
2016-11-03 12:29:30 +05:30
2022-06-21 17:19:12 +05:30
scope :with_deployment , - > ( sha , status : nil ) do
deployments = Deployment . select ( 1 ) . where ( 'deployments.environment_id = environments.id' ) . where ( sha : sha )
deployments = deployments . where ( status : status ) if status
where ( 'EXISTS (?)' , deployments )
end
2021-04-17 20:07:23 +05:30
scope :stopped_review_apps , - > ( before , limit ) do
stopped
. in_review_folder
. where ( " created_at < ? " , before )
. order ( " created_at ASC " )
. limit ( limit )
end
scope :scheduled_for_deletion , - > do
where . not ( auto_delete_at : nil )
end
scope :not_scheduled_for_deletion , - > do
where ( auto_delete_at : nil )
end
enum tier : {
production : 0 ,
staging : 1 ,
testing : 2 ,
development : 3 ,
other : 4
}
2016-11-03 12:29:30 +05:30
state_machine :state , initial : :available do
event :start do
transition stopped : :available
end
event :stop do
2022-07-23 23:45:48 +05:30
transition available : :stopping , if : :wait_for_stop?
transition available : :stopped , unless : :wait_for_stop?
end
event :stop_complete do
transition % i ( available stopping ) = > :stopped
2016-11-03 12:29:30 +05:30
end
state :available
2022-07-23 23:45:48 +05:30
state :stopping
2016-11-03 12:29:30 +05:30
state :stopped
2017-09-10 17:25:29 +05:30
2021-11-11 11:23:49 +05:30
before_transition any = > :stopped do | environment |
environment . auto_stop_at = nil
end
2017-09-10 17:25:29 +05:30
after_transition do | environment |
environment . expire_etag_cache
end
2016-11-03 12:29:30 +05:30
end
2020-04-08 14:13:33 +05:30
def self . for_id_and_slug ( id , slug )
find_by ( id : id , slug : slug )
end
2022-06-21 17:19:12 +05:30
def self . max_deployment_id_query
Arel . sql (
Deployment . select ( Deployment . arel_table [ :id ] . maximum )
. where ( Deployment . arel_table [ :environment_id ] . eq ( arel_table [ :id ] ) ) . to_sql
)
2020-03-13 15:44:24 +05:30
end
2019-03-02 22:35:43 +05:30
def self . pluck_names
pluck ( :name )
end
2021-01-03 14:25:43 +05:30
def self . pluck_unique_names
pluck ( 'DISTINCT(environments.name)' )
end
2019-12-21 20:55:43 +05:30
def self . find_or_create_by_name ( name )
find_or_create_by ( name : name )
end
2020-04-22 19:07:51 +05:30
def self . valid_states
self . state_machine . states . map ( & :name )
end
2021-04-17 20:07:23 +05:30
def self . schedule_to_delete ( at_time = 1 . week . from_now )
update_all ( auto_delete_at : at_time )
end
2020-03-13 15:44:24 +05:30
class << self
2020-05-24 23:13:21 +05:30
def count_by_state
environments_count_by_state = group ( :state ) . count
valid_states . each_with_object ( { } ) do | state , count_hash |
count_hash [ state ] = environments_count_by_state [ state . to_s ] || 0
end
end
2021-11-11 11:23:49 +05:30
end
def last_deployable
last_deployment & . deployable
end
2020-05-24 23:13:21 +05:30
2022-06-21 17:19:12 +05:30
def last_deployment_pipeline
last_deployable & . pipeline
end
# This method returns the deployment records of the last deployment pipeline, that successfully executed to this environment.
# e.g.
# A pipeline contains
# - deploy job A => production environment
# - deploy job B => production environment
# In this case, `last_deployment_group` returns both deployments, whereas `last_deployable` returns only B.
2022-07-23 23:45:48 +05:30
def legacy_last_deployment_group
2022-06-21 17:19:12 +05:30
return Deployment . none unless last_deployment_pipeline
successful_deployments . where (
deployable_id : last_deployment_pipeline . latest_builds . pluck ( :id ) )
end
2021-11-11 11:23:49 +05:30
# NOTE: Below assocation overrides is a workaround for issue https://gitlab.com/gitlab-org/gitlab/-/issues/339908
# It helps to avoid cross joins with the CI database.
# Caveat: It also overrides and losses the default AR caching mechanism.
# Read - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68870#note_677227727
2020-03-13 15:44:24 +05:30
2021-11-11 11:23:49 +05:30
# NOTE: Association Preloads does not use the overriden definitions below.
# Association Preloads when preloading uses the original definitions from the relationships above.
# https://github.com/rails/rails/blob/75ac626c4e21129d8296d4206a1960563cc3d4aa/activerecord/lib/active_record/associations/preloader.rb#L158
# But after preloading, when they are called it is using the overriden methods below.
# So we are checking for `association_cached?(:association_name)` in the overridden methods and calling `super` which inturn fetches the preloaded values.
# Overriding association
def last_visible_deployable
2021-11-18 22:05:49 +05:30
return super if association_cached? ( :last_visible_deployable )
2021-11-11 11:23:49 +05:30
last_visible_deployment & . deployable
end
# Overriding association
def last_visible_pipeline
2021-11-18 22:05:49 +05:30
return super if association_cached? ( :last_visible_pipeline )
2021-11-11 11:23:49 +05:30
last_visible_deployable & . pipeline
2020-03-13 15:44:24 +05:30
end
def clear_prometheus_reactive_cache! ( query_name )
cluster_prometheus_adapter & . clear_prometheus_reactive_cache! ( query_name , self )
end
def cluster_prometheus_adapter
@cluster_prometheus_adapter || = :: Gitlab :: Prometheus :: Adapter . new ( project , deployment_platform & . cluster ) . cluster_prometheus_adapter
end
2017-08-17 22:00:37 +05:30
def predefined_variables
2018-05-09 12:01:36 +05:30
Gitlab :: Ci :: Variables :: Collection . new
. append ( key : 'CI_ENVIRONMENT_NAME' , value : name )
. append ( key : 'CI_ENVIRONMENT_SLUG' , value : slug )
2021-09-04 01:27:46 +05:30
. append ( key : 'CI_ENVIRONMENT_TIER' , value : tier )
2017-08-17 22:00:37 +05:30
end
def recently_updated_on_branch? ( ref )
ref . to_s == last_deployment . try ( :ref )
2016-06-16 23:09:34 +05:30
end
2016-09-13 17:45:13 +05:30
2016-09-29 09:46:39 +05:30
def set_environment_type
names = name . split ( '/' )
2017-09-10 17:25:29 +05:30
self . environment_type = names . many? ? names . first : nil
2016-09-29 09:46:39 +05:30
end
2022-04-04 11:22:00 +05:30
def includes_commit? ( sha )
2016-09-13 17:45:13 +05:30
return false unless last_deployment
2022-04-04 11:22:00 +05:30
last_deployment . includes_commit? ( sha )
2016-09-13 17:45:13 +05:30
end
2016-09-29 09:46:39 +05:30
2017-08-17 22:00:37 +05:30
def last_deployed_at
last_deployment . try ( :created_at )
end
2016-11-03 12:29:30 +05:30
def ref_path
2018-03-17 18:26:18 +05:30
" refs/ #{ Repository :: REF_ENVIRONMENTS } / #{ slug } "
2016-11-03 12:29:30 +05:30
end
def formatted_external_url
2019-07-07 11:18:12 +05:30
return unless external_url
2016-11-03 12:29:30 +05:30
2018-03-17 18:26:18 +05:30
external_url . gsub ( %r{ \ A.*?:// } , '' )
2016-11-03 12:29:30 +05:30
end
2022-06-21 17:19:12 +05:30
def stop_actions_available?
available? && stop_actions . present?
2016-11-03 12:29:30 +05:30
end
2020-07-28 23:09:34 +05:30
def cancel_deployment_jobs!
2021-11-18 22:05:49 +05:30
active_deployments . builds . each do | build |
Gitlab :: OptimisticLocking . retry_lock ( build , name : 'environment_cancel_deployment_jobs' ) do | build |
build . cancel! if build & . cancelable?
2020-07-28 23:09:34 +05:30
end
2021-06-08 01:23:25 +05:30
rescue StandardError = > e
2020-07-28 23:09:34 +05:30
Gitlab :: ErrorTracking . track_exception ( e , environment_id : id , deployment_id : deployment . id )
end
end
2022-07-23 23:45:48 +05:30
def wait_for_stop?
stop_actions . present?
end
2022-06-21 17:19:12 +05:30
def stop_with_actions! ( current_user )
2017-08-17 22:00:37 +05:30
return unless available?
stop!
2022-06-21 17:19:12 +05:30
actions = [ ]
stop_actions . each do | stop_action |
Gitlab :: OptimisticLocking . retry_lock (
stop_action ,
name : 'environment_stop_with_actions'
) do | build |
actions << build . play ( current_user )
end
end
actions
end
def stop_actions
strong_memoize ( :stop_actions ) do
2022-07-16 23:28:13 +05:30
last_deployment_group . map ( & :stop_action ) . compact
2022-06-21 17:19:12 +05:30
end
2017-08-17 22:00:37 +05:30
end
2022-07-23 23:45:48 +05:30
def last_deployment_group
if :: Feature . enabled? ( :batch_load_environment_last_deployment_group , project )
Deployment . last_deployment_group_for_environment ( self )
else
legacy_last_deployment_group
end
end
2020-01-01 13:55:28 +05:30
def reset_auto_stop
update_column ( :auto_stop_at , nil )
end
2017-08-17 22:00:37 +05:30
def actions_for ( environment )
2022-07-23 23:45:48 +05:30
return [ ] unless other_manual_actions
2017-08-17 22:00:37 +05:30
2022-07-23 23:45:48 +05:30
other_manual_actions . select do | action |
2017-08-17 22:00:37 +05:30
action . expanded_environment_name == environment
end
end
def has_terminals?
2019-09-30 21:07:59 +05:30
available? && deployment_platform . present? && last_deployment . present?
2017-08-17 22:00:37 +05:30
end
def terminals
2019-09-30 21:07:59 +05:30
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
2019-10-12 21:52:04 +05:30
deployment_platform . cluster . kubernetes_namespace_for ( self ) if deployment_platform
2019-09-30 21:07:59 +05:30
end
2017-08-17 22:00:37 +05:30
end
def has_metrics?
2020-03-13 15:44:24 +05:30
available? && ( prometheus_adapter & . configured? || has_sample_metrics? )
end
def has_sample_metrics?
! ! ENV [ 'USE_SAMPLE_METRICS' ]
2017-08-17 22:00:37 +05:30
end
2020-10-24 23:57:45 +05:30
def has_opened_alert?
latest_opened_most_severe_alert . present?
end
2021-01-29 00:20:46 +05:30
def has_running_deployments?
all_deployments . running . exists?
end
2017-08-17 22:00:37 +05:30
def metrics
2020-03-13 15:44:24 +05:30
prometheus_adapter . query ( :environment , self ) if has_metrics_and_can_query?
2017-09-10 17:25:29 +05:30
end
2019-07-07 11:18:12 +05:30
def additional_metrics ( * args )
2020-03-13 15:44:24 +05:30
return unless has_metrics_and_can_query?
2019-07-07 11:18:12 +05:30
prometheus_adapter . query ( :additional_metrics_environment , self , * args . map ( & :to_f ) )
2017-09-10 17:25:29 +05:30
end
2018-03-27 19:54:05 +05:30
def prometheus_adapter
2020-03-13 15:44:24 +05:30
@prometheus_adapter || = Gitlab :: Prometheus :: Adapter . new ( project , deployment_platform & . cluster ) . prometheus_adapter
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
def slug
super . presence || generate_slug
end
2017-08-17 22:00:37 +05:30
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
2019-10-12 21:52:04 +05:30
[ external_url . delete_suffix ( '/' ) , public_path . delete_prefix ( '/' ) ] . join ( '/' )
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
def expire_etag_cache
Gitlab :: EtagCaching :: Store . new . tap do | store |
store . touch ( etag_cache_key )
end
end
def etag_cache_key
Gitlab :: Routing . url_helpers . project_environments_path (
project ,
format : :json )
end
def folder_name
self . environment_type || self . name
end
2019-07-07 11:18:12 +05:30
def name_without_type
@name_without_type || = name . delete_prefix ( " #{ environment_type } / " )
end
2018-03-27 19:54:05 +05:30
def deployment_platform
2019-02-15 15:39:39 +05:30
strong_memoize ( :deployment_platform ) do
project . deployment_platform ( environment : self . name )
end
2018-03-27 19:54:05 +05:30
end
2019-10-12 21:52:04 +05:30
def knative_services_finder
if last_deployment & . cluster
Clusters :: KnativeServicesFinder . new ( last_deployment . cluster , self )
end
end
2020-01-01 13:55:28 +05:30
def auto_stop_in
2020-06-23 00:09:42 +05:30
auto_stop_at - Time . current if auto_stop_at
2020-01-01 13:55:28 +05:30
end
def auto_stop_in = ( value )
return unless value
return unless parsed_result = ChronicDuration . parse ( value )
self . auto_stop_at = parsed_result . seconds . from_now
end
2020-04-08 14:13:33 +05:30
def elastic_stack_available?
2021-06-08 01:23:25 +05:30
! ! deployment_platform & . cluster & . elastic_stack_available?
2020-04-08 14:13:33 +05:30
end
2021-02-22 17:27:13 +05:30
def rollout_status
return unless rollout_status_available?
result = rollout_status_with_reactive_cache
result || :: Gitlab :: Kubernetes :: RolloutStatus . loading
end
def ingresses
return unless rollout_status_available?
deployment_platform . ingresses ( deployment_namespace )
end
def patch_ingress ( ingress , data )
return unless rollout_status_available?
deployment_platform . patch_ingress ( deployment_namespace , ingress , data )
end
def clear_all_caches
expire_etag_cache
clear_reactive_cache!
end
2021-12-11 22:18:48 +05:30
def should_link_to_merge_requests?
unfoldered? || production? || staging?
end
def unfoldered?
environment_type . nil?
end
2017-08-17 22:00:37 +05:30
private
2016-11-03 12:29:30 +05:30
2021-02-22 17:27:13 +05:30
def rollout_status_available?
has_terminals?
end
def rollout_status_with_reactive_cache
with_reactive_cache do | data |
deployment_platform . rollout_status ( self , data )
end
end
2020-03-13 15:44:24 +05:30
def has_metrics_and_can_query?
has_metrics? && prometheus_adapter . can_query?
end
2019-10-12 21:52:04 +05:30
def generate_slug
self . slug = Gitlab :: Slug :: Environment . new ( name ) . generate
2016-11-03 12:29:30 +05:30
end
2021-04-17 20:07:23 +05:30
def ensure_environment_tier
self . tier || = guess_tier
end
# Guessing the tier of the environment if it's not explicitly specified by users.
# See https://en.wikipedia.org/wiki/Deployment_environment for industry standard deployment environments
def guess_tier
case name
2022-05-07 20:08:51 +05:30
when / (dev|review|trunk) /i
self . class . tiers [ :development ]
when / (test|tst|int|ac(ce|)pt|qa|qc|control|quality) /i
self . class . tiers [ :testing ]
when / (st(a|)g|mod(e|)l|pre|demo) /i
self . class . tiers [ :staging ]
when / (pr(o|)d|live) /i
self . class . tiers [ :production ]
else
self . class . tiers [ :other ]
2021-04-17 20:07:23 +05:30
end
end
2016-06-16 23:09:34 +05:30
end
2019-12-04 20:38:33 +05:30
2021-06-08 01:23:25 +05:30
Environment . prepend_mod_with ( 'Environment' )