2018-07-25 05:30:33 -04:00
# frozen_string_literal: true
2019-03-28 09:17:42 -04:00
class Environment < ApplicationRecord
2018-12-20 04:39:09 -05:00
include Gitlab :: Utils :: StrongMemoize
2019-05-22 14:55:15 -04:00
include ReactiveCaching
2020-03-25 02:07:58 -04:00
include FastDestroyAll :: Helpers
2020-10-13 02:09:09 -04:00
include Presentable
2021-07-23 02:08:47 -04:00
include NullifyIfBlank
2019-05-22 14:55:15 -04:00
2019-11-18 07:06:03 -05:00
self . reactive_cache_refresh_interval = 1 . minute
self . reactive_cache_lifetime = 55 . seconds
2020-02-10 10:08:54 -05:00
self . reactive_cache_hard_limit = 10 . megabytes
2020-04-24 17:09:48 -04:00
self . reactive_cache_work_type = :external_dependency
2019-11-18 07:06:03 -05:00
2022-03-28 08:07:26 -04:00
belongs_to :project , optional : false
2016-06-10 17:36:54 -04:00
2020-03-25 02:07:58 -04:00
use_fast_destroy :all_deployments
2021-07-23 02:08:47 -04:00
nullify_if_blank :external_url
2020-03-25 02:07:58 -04:00
has_many :all_deployments , class_name : 'Deployment'
has_many :deployments , - > { visible }
2019-10-16 14:08:01 -04:00
has_many :successful_deployments , - > { success } , class_name : 'Deployment'
2020-02-17 04:08:52 -05:00
has_many :active_deployments , - > { active } , class_name : 'Deployment'
2020-02-06 13:08:54 -05:00
has_many :prometheus_alerts , inverse_of : :environment
2020-04-07 14:09:19 -04:00
has_many :metrics_dashboard_annotations , class_name : 'Metrics::Dashboard::Annotation' , inverse_of : :environment
2020-03-23 08:09:47 -04:00
has_many :self_managed_prometheus_alert_events , inverse_of : :environment
2020-06-26 14:09:03 -04:00
has_many :alert_management_alerts , class_name : 'AlertManagement::Alert' , inverse_of : :environment
2017-09-13 08:43:03 -04:00
2022-04-27 14:10:15 -04:00
has_one :last_deployment , - > { success . ordered } , class_name : 'Deployment' , inverse_of : :environment
2019-11-13 22:06:25 -05:00
has_one :last_visible_deployment , - > { visible . distinct_on_environment } , inverse_of : :environment , class_name : 'Deployment'
2021-10-12 08:09:36 -04:00
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-09-15 05:09:47 -04:00
2021-12-08 01:13:27 -05:00
has_one :upcoming_deployment , - > { upcoming . distinct_on_environment } , class_name : 'Deployment' , inverse_of : :environment
2020-08-13 14:10:36 -04:00
has_one :latest_opened_most_severe_alert , - > { order_severity_with_open_prometheus_alert } , class_name : 'AlertManagement::Alert' , inverse_of : :environment
2016-06-10 17:36:54 -04:00
2016-12-07 20:09:18 -05:00
before_validation :generate_slug , if : - > ( env ) { env . slug . blank? }
2016-09-13 08:14:55 -04:00
before_save :set_environment_type
2021-03-04 19:09:24 -05:00
before_save :ensure_environment_tier
2019-05-22 14:55:15 -04:00
after_save :clear_reactive_cache!
2016-07-26 08:19:37 -04:00
2016-06-14 07:04:21 -04:00
validates :name ,
presence : true ,
2016-06-14 12:34:48 -04:00
uniqueness : { scope : :project_id } ,
2016-12-02 07:54:57 -05:00
length : { maximum : 255 } ,
2016-06-14 07:04:21 -04:00
format : { with : Gitlab :: Regex . environment_name_regex ,
message : Gitlab :: Regex . environment_name_regex_message }
2016-06-10 17:36:54 -04:00
2016-12-07 20:09:18 -05:00
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-07-26 03:35:47 -04:00
validates :external_url ,
2016-07-26 08:19:37 -04:00
length : { maximum : 255 } ,
allow_nil : true ,
2019-04-11 02:29:07 -04:00
addressable_url : true
2016-07-26 03:35:47 -04:00
2022-05-20 11:09:10 -04:00
delegate :manual_actions , :other_manual_actions , to : :last_deployment , allow_nil : true
2020-11-20 01:09:10 -05:00
delegate :auto_rollback_enabled? , to : :project
2016-10-06 07:10:50 -04:00
2016-10-17 15:06:10 -04:00
scope :available , - > { with_state ( :available ) }
scope :stopped , - > { with_state ( :stopped ) }
2019-12-18 13:08:04 -05:00
2017-02-06 19:06:46 -05:00
scope :order_by_last_deployed_at , - > do
2022-04-19 05:08:55 -04:00
order ( Arel :: Nodes :: Grouping . new ( max_deployment_id_query ) . asc . nulls_first )
2017-02-06 19:06:46 -05:00
end
2019-12-18 13:08:04 -05:00
scope :order_by_last_deployed_at_desc , - > do
2022-04-19 05:08:55 -04:00
order ( Arel :: Nodes :: Grouping . new ( max_deployment_id_query ) . desc . nulls_last )
2019-12-18 13:08:04 -05:00
end
2020-10-19 02:09:08 -04:00
scope :order_by_name , - > { order ( 'environments.name ASC' ) }
2019-12-18 13:08:04 -05:00
2017-06-15 06:50:45 -04:00
scope :in_review_folder , - > { where ( environment_type : " review " ) }
2018-10-12 10:10:34 -04:00
scope :for_name , - > ( name ) { where ( name : name ) }
2019-08-07 00:40:29 -04:00
scope :preload_cluster , - > { preload ( last_deployment : :cluster ) }
2021-09-01 02:09:00 -04:00
scope :preload_project , - > { preload ( :project ) }
2020-02-13 19:09:07 -05:00
scope :auto_stoppable , - > ( limit ) { available . where ( 'auto_stop_at < ?' , Time . zone . now ) . limit ( limit ) }
2021-07-29 11:09:48 -04:00
scope :auto_deletable , - > ( limit ) { stopped . where ( 'auto_delete_at < ?' , Time . zone . now ) . limit ( limit ) }
2019-02-05 02:14:30 -05:00
##
# 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-09-18 10:02:45 -04:00
where ( arel_table [ :name ] . matches ( " #{ sanitize_sql_like query } % " ) ) . limit ( limit )
2019-02-05 02:14:30 -05:00
end
2018-10-12 10:10:34 -04:00
scope :for_project , - > ( project ) { where ( project_id : project ) }
2021-04-01 17:09:22 -04:00
scope :for_tier , - > ( tier ) { where ( tier : tier ) . where . not ( tier : nil ) }
2019-11-07 19:05:58 -05:00
scope :unfoldered , - > { where ( environment_type : nil ) }
2019-11-13 22:06:25 -05:00
scope :with_rank , - > do
select ( 'environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)' )
end
2020-10-02 14:08:56 -04:00
scope :for_id , - > ( id ) { where ( id : id ) }
2016-10-17 15:06:10 -04:00
2022-04-14 02:08:29 -04:00
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-03-11 22:08:56 -05:00
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
2021-03-04 19:09:24 -05:00
enum tier : {
production : 0 ,
staging : 1 ,
testing : 2 ,
development : 3 ,
other : 4
}
2016-10-17 06:45:31 -04:00
state_machine :state , initial : :available do
event :start do
transition stopped : :available
2016-10-06 07:10:50 -04:00
end
2016-10-17 06:45:31 -04:00
event :stop do
2022-05-24 08:09:04 -04:00
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-10-06 07:10:50 -04:00
end
2016-10-17 06:45:31 -04:00
state :available
2022-05-24 08:09:04 -04:00
state :stopping
2016-10-17 06:45:31 -04:00
state :stopped
2017-05-12 09:19:27 -04:00
2021-09-01 02:09:00 -04:00
before_transition any = > :stopped do | environment |
environment . auto_stop_at = nil
end
2017-05-12 09:19:27 -04:00
after_transition do | environment |
environment . expire_etag_cache
end
2016-10-06 07:10:50 -04:00
end
2020-02-20 10:08:44 -05:00
def self . for_id_and_slug ( id , slug )
find_by ( id : id , slug : slug )
end
2022-04-19 05:08:55 -04:00
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
)
2019-12-18 13:08:04 -05:00
end
2019-02-05 02:14:30 -05:00
def self . pluck_names
pluck ( :name )
end
2020-10-19 02:09:08 -04:00
def self . pluck_unique_names
pluck ( 'DISTINCT(environments.name)' )
end
2019-10-16 14:08:01 -04:00
def self . find_or_create_by_name ( name )
find_or_create_by ( name : name )
end
2020-04-07 20:09:30 -04:00
def self . valid_states
self . state_machine . states . map ( & :name )
end
2021-03-11 22:08:56 -05:00
def self . schedule_to_delete ( at_time = 1 . week . from_now )
update_all ( auto_delete_at : at_time )
end
2020-02-13 19:09:07 -05:00
class << self
2020-04-28 05:09:34 -04:00
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
2020-02-13 19:09:07 -05:00
end
2021-09-15 05:09:47 -04:00
def last_deployable
last_deployment & . deployable
end
2022-04-14 02:08:29 -04:00
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-05-30 11:08:03 -04:00
def legacy_last_deployment_group
2022-04-14 02:08:29 -04:00
return Deployment . none unless last_deployment_pipeline
successful_deployments . where (
deployable_id : last_deployment_pipeline . latest_builds . pluck ( :id ) )
end
2021-09-15 05:09:47 -04:00
# 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
# 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-10-12 08:09:36 -04:00
return super if association_cached? ( :last_visible_deployable )
2021-09-15 05:09:47 -04:00
last_visible_deployment & . deployable
end
# Overriding association
def last_visible_pipeline
2021-10-12 08:09:36 -04:00
return super if association_cached? ( :last_visible_pipeline )
2021-09-15 05:09:47 -04:00
last_visible_deployable & . pipeline
end
2020-02-06 13:08:54 -05:00
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
2016-12-08 11:21:16 -05:00
def predefined_variables
2018-03-14 06:15:18 -04:00
Gitlab :: Ci :: Variables :: Collection . new
. append ( key : 'CI_ENVIRONMENT_NAME' , value : name )
. append ( key : 'CI_ENVIRONMENT_SLUG' , value : slug )
2021-06-09 17:10:34 -04:00
. append ( key : 'CI_ENVIRONMENT_TIER' , value : tier )
2016-12-08 11:21:16 -05:00
end
2016-11-16 04:54:20 -05:00
def recently_updated_on_branch? ( ref )
2016-11-16 05:26:36 -05:00
ref . to_s == last_deployment . try ( :ref )
2016-11-15 08:48:14 -05:00
end
2016-09-13 08:14:55 -04:00
def set_environment_type
names = name . split ( '/' )
2017-08-21 11:46:45 -04:00
self . environment_type = names . many? ? names . first : nil
2016-09-13 08:14:55 -04:00
end
2022-01-27 22:15:57 -05:00
def includes_commit? ( sha )
2016-08-03 07:37:39 -04:00
return false unless last_deployment
2016-08-02 08:01:22 -04:00
2022-01-27 22:15:57 -05:00
last_deployment . includes_commit? ( sha )
2016-08-02 08:01:22 -04:00
end
2016-09-20 05:36:54 -04:00
2017-01-30 19:26:40 -05:00
def last_deployed_at
2018-11-07 00:36:42 -05:00
last_deployment . try ( :created_at )
2017-01-30 19:26:40 -05:00
end
2016-09-30 09:45:27 -04:00
def ref_path
2017-11-02 18:32:22 -04:00
" refs/ #{ Repository :: REF_ENVIRONMENTS } / #{ slug } "
2016-09-30 09:45:27 -04:00
end
2016-10-12 09:21:01 -04:00
def formatted_external_url
2019-02-08 07:19:53 -05:00
return unless external_url
2016-10-12 09:21:01 -04:00
2018-01-27 00:35:53 -05:00
external_url . gsub ( %r{ \ A.*?:// } , '' )
2016-10-12 09:21:01 -04:00
end
2016-10-17 10:13:19 -04:00
2022-04-14 02:08:29 -04:00
def stop_actions_available?
available? && stop_actions . present?
2016-10-17 10:13:19 -04:00
end
2016-10-18 06:02:50 -04:00
2020-06-26 08:08:51 -04:00
def cancel_deployment_jobs!
2021-10-12 11:12:08 -04:00
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-08 20:09:11 -04:00
end
2021-04-26 08:09:44 -04:00
rescue StandardError = > e
2020-07-08 20:09:11 -04:00
Gitlab :: ErrorTracking . track_exception ( e , environment_id : id , deployment_id : deployment . id )
end
2020-06-26 08:08:51 -04:00
end
2022-05-24 08:09:04 -04:00
def wait_for_stop?
2022-06-08 08:08:46 -04:00
stop_actions . present?
2022-05-24 08:09:04 -04:00
end
2022-04-14 02:08:29 -04:00
def stop_with_actions! ( current_user )
2016-11-10 07:59:26 -05:00
return unless available?
2016-10-18 06:02:50 -04:00
2017-02-06 10:50:03 -05:00
stop!
2022-04-07 08:10:21 -04:00
2022-04-14 02:08:29 -04:00
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
2022-04-07 08:10:21 -04:00
2022-04-14 02:08:29 -04:00
actions
end
def stop_actions
strong_memoize ( :stop_actions ) do
2022-05-06 23:08:03 -04:00
last_deployment_group . map ( & :stop_action ) . compact
2022-04-07 08:10:21 -04:00
end
2016-10-18 06:02:50 -04:00
end
2016-11-21 07:47:18 -05:00
2022-05-30 11:08:03 -04:00
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
2019-12-10 02:53:40 -05:00
def reset_auto_stop
update_column ( :auto_stop_at , nil )
end
2016-11-21 07:47:18 -05:00
def actions_for ( environment )
2022-05-20 11:09:10 -04:00
return [ ] unless other_manual_actions
2016-11-21 07:47:18 -05:00
2022-05-20 11:09:10 -04:00
other_manual_actions . select do | action |
2016-11-21 11:26:35 -05:00
action . expanded_environment_name == environment
2016-11-10 07:59:26 -05:00
end
2016-10-18 06:02:50 -04:00
end
2016-12-07 20:09:18 -05:00
2016-11-22 14:55:56 -05:00
def has_terminals?
2019-06-19 07:59:47 -04:00
available? && deployment_platform . present? && last_deployment . present?
2016-11-22 14:55:56 -05:00
end
def terminals
2019-05-22 14:55:15 -04:00
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-08-07 00:40:29 -04:00
deployment_platform . cluster . kubernetes_namespace_for ( self ) if deployment_platform
2019-05-22 14:55:15 -04:00
end
2016-11-22 14:55:56 -05:00
end
2017-03-07 11:57:42 -05:00
def has_metrics?
2019-12-23 22:07:52 -05:00
available? && ( prometheus_adapter & . configured? || has_sample_metrics? )
end
def has_sample_metrics?
! ! ENV [ 'USE_SAMPLE_METRICS' ]
2017-03-07 11:57:42 -05:00
end
2020-08-13 14:10:36 -04:00
def has_opened_alert?
latest_opened_most_severe_alert . present?
end
2020-11-08 22:09:03 -05:00
def has_running_deployments?
all_deployments . running . exists?
end
2017-03-07 11:57:42 -05:00
def metrics
2019-12-20 07:07:40 -05:00
prometheus_adapter . query ( :environment , self ) if has_metrics_and_can_query?
2017-06-06 20:36:59 -04:00
end
2019-03-15 07:20:59 -04:00
def additional_metrics ( * args )
2019-12-20 07:07:40 -05:00
return unless has_metrics_and_can_query?
2019-03-15 07:20:59 -04:00
prometheus_adapter . query ( :additional_metrics_environment , self , * args . map ( & :to_f ) )
2017-05-10 05:25:30 -04:00
end
2018-03-05 13:34:59 -05:00
def prometheus_adapter
2020-01-08 04:07:53 -05:00
@prometheus_adapter || = Gitlab :: Prometheus :: Adapter . new ( project , deployment_platform & . cluster ) . prometheus_adapter
2018-03-05 13:34:59 -05:00
end
2017-11-02 18:32:22 -04:00
def slug
super . presence || generate_slug
end
2017-01-29 14:38:00 -05:00
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-08-01 05:56:33 -04:00
[ external_url . delete_suffix ( '/' ) , public_path . delete_prefix ( '/' ) ] . join ( '/' )
2017-01-29 14:38:00 -05:00
end
2017-05-12 09:19:27 -04:00
def expire_etag_cache
Gitlab :: EtagCaching :: Store . new . tap do | store |
2017-05-30 17:56:26 -04:00
store . touch ( etag_cache_key )
2017-05-12 09:19:27 -04:00
end
end
2017-05-30 17:56:26 -04:00
def etag_cache_key
2017-06-29 13:06:35 -04:00
Gitlab :: Routing . url_helpers . project_environments_path (
2017-06-12 03:09:51 -04:00
project ,
format : :json )
2017-05-30 17:56:26 -04:00
end
2017-08-21 11:46:45 -04:00
def folder_name
self . environment_type || self . name
end
2019-02-26 14:13:09 -05:00
def name_without_type
@name_without_type || = name . delete_prefix ( " #{ environment_type } / " )
end
2018-02-26 13:57:11 -05:00
def deployment_platform
2018-12-20 04:39:09 -05:00
strong_memoize ( :deployment_platform ) do
project . deployment_platform ( environment : self . name )
end
2018-02-26 13:57:11 -05:00
end
2019-08-07 00:40:29 -04:00
def knative_services_finder
if last_deployment & . cluster
Clusters :: KnativeServicesFinder . new ( last_deployment . cluster , self )
end
end
2019-12-10 02:53:40 -05:00
def auto_stop_in
2020-05-22 05:08:09 -04:00
auto_stop_at - Time . current if auto_stop_at
2019-12-10 02:53:40 -05:00
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-11-30 13:09:46 -05:00
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
2021-01-05 07:10:36 -05:00
def clear_all_caches
expire_etag_cache
clear_reactive_cache!
end
2021-10-25 08:10:19 -04:00
def should_link_to_merge_requests?
unfoldered? || production? || staging?
end
def unfoldered?
environment_type . nil?
end
2016-12-07 20:09:18 -05:00
private
2020-11-30 13:09:46 -05:00
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
2019-12-20 07:07:40 -05:00
def has_metrics_and_can_query?
has_metrics? && prometheus_adapter . can_query?
end
2019-07-09 23:55:32 -04:00
def generate_slug
self . slug = Gitlab :: Slug :: Environment . new ( name ) . generate
2016-12-07 20:09:18 -05:00
end
2021-03-04 19:09:24 -05:00
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-03-07 10:22:51 -05:00
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-03-04 19:09:24 -05:00
end
end
2016-06-10 17:36:54 -04:00
end
2019-09-13 09:26:31 -04:00
2021-05-11 17:10:21 -04:00
Environment . prepend_mod_with ( 'Environment' )