gitlab-org--gitlab-foss/app/models/environment.rb
Shinya Maeda e8d9df83a6 Inroduce Internal API for searching environment names
Add changelog

Rename word to query

User hash for limit

Do not allow control limit

Rename pluck names and add more specs
2019-02-06 18:14:18 +09:00

261 lines
7.1 KiB
Ruby

# frozen_string_literal: true
class Environment < ActiveRecord::Base
include Gitlab::Utils::StrongMemoize
# Used to generate random suffixes for the slug
LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
belongs_to :project, required: true
has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
before_save :set_environment_type
validates :name,
presence: true,
uniqueness: { scope: :project_id },
length: { maximum: 255 },
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
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 }
validates :external_url,
length: { maximum: 255 },
allow_nil: true,
url: true
delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
scope :order_by_last_deployed_at, -> do
max_deployment_id_sql =
Deployment.select(Deployment.arel_table[:id].maximum)
.where(Deployment.arel_table[:environment_id].eq(arel_table[:id]))
.to_sql
order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
end
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
##
# 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
where('name LIKE ?', "#{sanitize_sql_like(query)}%").limit(limit)
end
scope :for_project, -> (project) { where(project_id: project) }
scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
state_machine :state, initial: :available do
event :start do
transition stopped: :available
end
event :stop do
transition available: :stopped
end
state :available
state :stopped
after_transition do |environment|
environment.expire_etag_cache
end
end
def self.pluck_names
pluck(:name)
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name)
.append(key: 'CI_ENVIRONMENT_SLUG', value: slug)
end
def recently_updated_on_branch?(ref)
ref.to_s == last_deployment.try(:ref)
end
def nullify_external_url
self.external_url = nil if self.external_url.blank?
end
def set_environment_type
names = name.split('/')
self.environment_type = names.many? ? names.first : nil
end
def includes_commit?(commit)
return false unless last_deployment
last_deployment.includes_commit?(commit)
end
def last_deployed_at
last_deployment.try(:created_at)
end
def update_merge_request_metrics?
folder_name == "production"
end
def first_deployment_for(commit_sha)
ref = project.repository.ref_name_for_sha(ref_path, commit_sha)
return nil unless ref
deployment_iid = ref.split('/').last
deployments.find_by(iid: deployment_iid)
end
def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
def formatted_external_url
return nil unless external_url
external_url.gsub(%r{\A.*?://}, '')
end
def stop_action_available?
available? && stop_action.present?
end
def stop_with_action!(current_user)
return unless available?
stop!
stop_action&.play(current_user)
end
def actions_for(environment)
return [] unless manual_actions
manual_actions.select do |action|
action.expanded_environment_name == environment
end
end
def has_terminals?
project.deployment_platform.present? && available? && last_deployment.present?
end
def terminals
project.deployment_platform.terminals(self) if has_terminals?
end
def has_metrics?
prometheus_adapter&.can_query? && available?
end
def metrics
prometheus_adapter.query(:environment, self) if has_metrics?
end
def additional_metrics
prometheus_adapter.query(:additional_metrics_environment, self) if has_metrics?
end
# rubocop: disable CodeReuse/ServiceClass
def prometheus_adapter
@prometheus_adapter ||= Prometheus::AdapterService.new(project, deployment_platform).prometheus_adapter
end
# rubocop: enable CodeReuse/ServiceClass
def slug
super.presence || generate_slug
end
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
# * contains only lowercase letters (a-z), numbers (0-9), and '-'
# * begins with a letter
# * has a maximum length of 24 bytes (OpenShift limitation)
# * cannot end with `-`
def generate_slug
# Lowercase letters and numbers only
slugified = +name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
# Must start with a letter
slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0])
# Repeated dashes are invalid (OpenShift limitation)
slugified.gsub!(/\-+/, '-')
# Maximum length: 24 characters (OpenShift limitation)
slugified = slugified[0..23]
# Cannot end with a dash (Kubernetes label limitation)
slugified.chop! if slugified.end_with?('-')
# Add a random suffix, shortening the current string if necessary, if it
# has been slugified. This ensures uniqueness.
if slugified != name
slugified = slugified[0..16]
slugified << '-' unless slugified.end_with?('-')
slugified << random_suffix
end
self.slug = slugified
end
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
[external_url, public_path].join('/')
end
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
def deployment_platform
strong_memoize(:deployment_platform) do
project.deployment_platform(environment: self.name)
end
end
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
# based on name (which must be unique). To compensate, we add a random
# 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness,
# but the chance of collisions is vanishingly small
def random_suffix
(0..5).map { SUFFIX_CHARS.sample }.join
end
end