summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-19 22:11:55 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-19 22:11:55 +0000
commit5a8431feceba47fd8e1804d9aa1b1730606b71d5 (patch)
treee5df8e0ceee60f4af8093f5c4c2f934b8abced05 /app/models
parent4d477238500c347c6553d335d920bedfc5a46869 (diff)
downloadgitlab-ce-5a8431feceba47fd8e1804d9aa1b1730606b71d5.tar.gz
Add latest changes from gitlab-org/gitlab@12-5-stable-ee
Diffstat (limited to 'app/models')
-rw-r--r--app/models/abuse_report.rb4
-rw-r--r--app/models/analytics/cycle_analytics/project_stage.rb19
-rw-r--r--app/models/application_setting.rb59
-rw-r--r--app/models/application_setting_implementation.rb14
-rw-r--r--app/models/award_emoji.rb5
-rw-r--r--app/models/aws/role.rb6
-rw-r--r--app/models/ci/build.rb57
-rw-r--r--app/models/ci/build_metadata.rb1
-rw-r--r--app/models/ci/pipeline.rb91
-rw-r--r--app/models/clusters/applications/cert_manager.rb2
-rw-r--r--app/models/clusters/applications/crossplane.rb60
-rw-r--r--app/models/clusters/applications/elastic_stack.rb108
-rw-r--r--app/models/clusters/applications/ingress.rb73
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb67
-rw-r--r--app/models/clusters/clusters_hierarchy.rb41
-rw-r--r--app/models/clusters/concerns/application_core.rb18
-rw-r--r--app/models/clusters/instance.rb4
-rw-r--r--app/models/clusters/providers/aws.rb26
-rw-r--r--app/models/clusters/providers/gcp.rb4
-rw-r--r--app/models/commit_status_enums.rb4
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb66
-rw-r--r--app/models/concerns/ci/metadatable.rb4
-rw-r--r--app/models/concerns/ci/processable.rb8
-rw-r--r--app/models/concerns/deployment_platform.rb2
-rw-r--r--app/models/concerns/issuable.rb24
-rw-r--r--app/models/concerns/milestoneish.rb4
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/protected_ref.rb4
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb40
-rw-r--r--app/models/concerns/subscribable.rb8
-rw-r--r--app/models/concerns/worker_attributes.rb46
-rw-r--r--app/models/container_repository.rb5
-rw-r--r--app/models/dashboard_group_milestone.rb4
-rw-r--r--app/models/deployment.rb33
-rw-r--r--app/models/deployment_merge_request.rb6
-rw-r--r--app/models/description_version.rb4
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb23
-rw-r--r--app/models/global_milestone.rb6
-rw-r--r--app/models/grafana_integration.rb6
-rw-r--r--app/models/group.rb51
-rw-r--r--app/models/group_group_link.rb23
-rw-r--r--app/models/import_export_upload.rb1
-rw-r--r--app/models/issue.rb24
-rw-r--r--app/models/lfs_object.rb5
-rw-r--r--app/models/merge_request.rb114
-rw-r--r--app/models/merge_request_diff.rb17
-rw-r--r--app/models/milestone.rb3
-rw-r--r--app/models/notification_reason.rb4
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/project.rb43
-rw-r--r--app/models/project_ci_cd_setting.rb3
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb30
-rw-r--r--app/models/project_services/chat_message/push_message.rb16
-rw-r--r--app/models/project_services/data_fields.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb25
-rw-r--r--app/models/project_snippet.rb7
-rw-r--r--app/models/project_wiki.rb6
-rw-r--r--app/models/prometheus_metric.rb6
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/release.rb13
-rw-r--r--app/models/releases/source.rb4
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/todo.rb7
-rw-r--r--app/models/user.rb12
-rw-r--r--app/models/wiki_page.rb5
-rw-r--r--app/models/zoom_meeting.rb26
68 files changed, 1140 insertions, 290 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index a3a1748142f..7cfebf0473f 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -2,6 +2,7 @@
class AbuseReport < ApplicationRecord
include CacheMarkdownField
+ include Sortable
cache_markdown_field :message, pipeline: :single_line
@@ -13,6 +14,9 @@ class AbuseReport < ApplicationRecord
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
+ scope :by_user, -> (user) { where(user_id: user) }
+ scope :with_users, -> { includes(:reporter, :user) }
+
# For CacheMarkdownField
alias_method :author, :reporter
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb
index 23f0db0829b..b2c16444a2a 100644
--- a/app/models/analytics/cycle_analytics/project_stage.rb
+++ b/app/models/analytics/cycle_analytics/project_stage.rb
@@ -10,6 +10,25 @@ module Analytics
alias_attribute :parent, :project
alias_attribute :parent_id, :project_id
+
+ delegate :group, to: :project
+
+ validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? }
+
+ def self.relative_positioning_query_base(stage)
+ where(project_id: stage.project_id)
+ end
+
+ def self.relative_positioning_parent_column
+ :project_id
+ end
+
+ private
+
+ # Project should belong to a group when the stage has Label based events since only GroupLabels are allowed.
+ def validate_project_group_for_label_events
+ errors.add(:project, s_('CycleAnalyticsStage|should be under a group')) unless project.group
+ end
end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a07933d4975..4028d711fd1 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -6,6 +6,12 @@ class ApplicationSetting < ApplicationRecord
include TokenAuthenticatable
include ChronicDurationAttribute
+ # Only remove this >= %12.6 and >= 2019-12-01
+ self.ignored_columns += %i[
+ pendo_enabled
+ pendo_url
+ ]
+
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token
@@ -18,12 +24,6 @@ class ApplicationSetting < ApplicationRecord
# fix a lot of tests using allow_any_instance_of
include ApplicationSettingImplementation
- attr_encrypted :asset_proxy_secret_key,
- mode: :per_attribute_iv,
- insecure_mode: true,
- key: Settings.attr_encrypted_db_key_base_truncated,
- algorithm: 'aes-256-cbc'
-
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
@@ -99,11 +99,20 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :plantuml_enabled
+ validates :sourcegraph_url,
+ presence: true,
+ if: :sourcegraph_enabled
+
validates :snowplow_collector_hostname,
presence: true,
hostname: true,
if: :snowplow_enabled
+ validates :snowplow_iglu_registry_url,
+ addressable_url: true,
+ allow_blank: true,
+ if: :snowplow_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -270,12 +279,40 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :lets_encrypt_terms_of_service_accepted?
+ validates :eks_integration_enabled,
+ inclusion: { in: [true, false] }
+
+ validates :eks_account_id,
+ format: { with: Gitlab::Regex.aws_account_id_regex,
+ message: Gitlab::Regex.aws_account_id_message },
+ if: :eks_integration_enabled?
+
+ validates :eks_access_key_id,
+ length: { in: 16..128 },
+ if: :eks_integration_enabled?
+
+ validates :eks_secret_access_key,
+ presence: true,
+ if: :eks_integration_enabled?
+
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
pass: :external_auth_client_key_pass,
if: -> (setting) { setting.external_auth_client_cert.present? }
+ validates :default_ci_config_path,
+ format: { without: %r{(\.{2}|\A/)},
+ message: N_('cannot include leading slash or directory traversal.') },
+ length: { maximum: 255 },
+ allow_blank: true
+
+ attr_encrypted :asset_proxy_secret_key,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-cbc',
+ insecure_mode: true
+
attr_encrypted :external_auth_client_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -294,6 +331,12 @@ class ApplicationSetting < ApplicationRecord
algorithm: 'aes-256-gcm',
encode: true
+ attr_encrypted :eks_secret_access_key,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: true
+
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
@@ -304,6 +347,10 @@ class ApplicationSetting < ApplicationRecord
end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
+ def sourcegraph_url_is_com?
+ !!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/)
+ end
+
def self.create_from_defaults
transaction(requires_new: true) do
super
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 0c0ffb67c9a..7bb89f0d1e2 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -42,6 +42,7 @@ module ApplicationSettingImplementation
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_ci_config_path: nil,
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_creation: Settings.gitlab['default_project_creation'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
@@ -54,6 +55,10 @@ module ApplicationSettingImplementation
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
ed25519_key_restriction: 0,
+ eks_integration_enabled: false,
+ eks_account_id: nil,
+ eks_access_key_id: nil,
+ eks_secret_access_key: nil,
first_day_of_week: 0,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
@@ -97,6 +102,9 @@ module ApplicationSettingImplementation
shared_runners_text: nil,
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
+ sourcegraph_enabled: false,
+ sourcegraph_url: nil,
+ sourcegraph_public_only: true,
terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
@@ -128,8 +136,10 @@ module ApplicationSettingImplementation
snowplow_collector_hostname: nil,
snowplow_cookie_domain: nil,
snowplow_enabled: false,
- snowplow_site_id: nil,
- custom_http_clone_url_root: nil
+ snowplow_app_id: nil,
+ snowplow_iglu_registry_url: nil,
+ custom_http_clone_url_root: nil,
+ productivity_analytics_start_date: Time.now
}
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 24fcb97db6e..5a33a8f89df 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -6,11 +6,14 @@ class AwardEmoji < ApplicationRecord
include Participable
include GhostUser
+ include Importable
belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
- validates :awardable, :user, presence: true
+ validates :user, presence: true
+ validates :awardable, presence: true, unless: :importing?
+
validates :name, presence: true, inclusion: { in: Gitlab::Emoji.emojis_names }
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }, unless: :ghost_user?
diff --git a/app/models/aws/role.rb b/app/models/aws/role.rb
index 836107435ad..54132be749d 100644
--- a/app/models/aws/role.rb
+++ b/app/models/aws/role.rb
@@ -13,5 +13,11 @@ module Aws
with: Gitlab::Regex.aws_arn_regex,
message: Gitlab::Regex.aws_arn_regex_message
}
+
+ before_validation :ensure_role_external_id!, on: :create
+
+ def ensure_role_external_id!
+ self.role_external_id ||= SecureRandom.hex(20)
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index c48ab28ce73..59a2c09bd28 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -35,10 +35,13 @@ module Ci
refspecs: -> (build) { build.merge_request_ref? }
}.freeze
+ DEFAULT_RETRIES = {
+ scheduler_failure: 2
+ }.freeze
+
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
- has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
@@ -52,7 +55,6 @@ module Ci
accepts_nested_attributes_for :runner_session
accepts_nested_attributes_for :job_variables
- accepts_nested_attributes_for :needs
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
@@ -118,6 +120,11 @@ module Ci
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
+ scope :with_exposed_artifacts, -> do
+ joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
+ .includes(:metadata, :job_artifacts_metadata)
+ end
+
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
@@ -367,18 +374,25 @@ module Ci
pipeline.builds.retried.where(name: self.name).count
end
- def retries_max
- normalized_retry.fetch(:max, 0)
+ def retry_failure?
+ max_allowed_retries = nil
+ max_allowed_retries ||= options_retry_max if retry_on_reason_or_always?
+ max_allowed_retries ||= DEFAULT_RETRIES.fetch(failure_reason.to_sym, 0)
+
+ max_allowed_retries > 0 && retries_count < max_allowed_retries
end
- def retry_when
- normalized_retry.fetch(:when, ['always'])
+ def options_retry_max
+ options_retry[:max]
end
- def retry_failure?
- return false if retries_max.zero? || retries_count >= retries_max
+ def options_retry_when
+ options_retry.fetch(:when, ['always'])
+ end
- retry_when.include?('always') || retry_when.include?(failure_reason.to_s)
+ def retry_on_reason_or_always?
+ options_retry_when.include?(failure_reason.to_s) ||
+ options_retry_when.include?('always')
end
def latest?
@@ -595,6 +609,14 @@ module Ci
update_column(:trace, nil)
end
+ def artifacts_expose_as
+ options.dig(:artifacts, :expose_as)
+ end
+
+ def artifacts_paths
+ options.dig(:artifacts, :paths)
+ end
+
def needs_touch?
Time.now - updated_at > 15.minutes.to_i
end
@@ -818,6 +840,13 @@ module Ci
:creating
end
+ # Consider this object to have a structural integrity problems
+ def doom!
+ update_columns(
+ status: :failed,
+ failure_reason: :data_integrity_failure)
+ end
+
private
def successful_deployment_status
@@ -862,19 +891,13 @@ module Ci
# format, but builds created before GitLab 11.5 and saved in database still
# have the old integer only format. This method returns the retry option
# normalized as a hash in 11.5+ format.
- def normalized_retry
- strong_memoize(:normalized_retry) do
+ def options_retry
+ strong_memoize(:options_retry) do
value = options&.dig(:retry)
value = value.is_a?(Integer) ? { max: value } : value.to_h
value.with_indifferent_access
end
end
-
- def build_attributes_from_config
- return {} unless pipeline.config_processor
-
- pipeline.config_processor.build_attributes(name)
- end
end
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 3097e40dd3b..0df5ebfe843 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -27,6 +27,7 @@ module Ci
scope :scoped_build, -> { where('ci_builds_metadata.build_id = ci_builds.id') }
scope :with_interruptible, -> { where(interruptible: true) }
+ scope :with_exposed_artifacts, -> { where(has_exposed_artifacts: true) }
enum timeout_source: {
unknown_timeout_source: 1,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3bf19399cec..f730b949ee9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -405,7 +405,7 @@ module Ci
.where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg)
- .pluck('sg.stage', status_sql, "(#{warnings_sql})")
+ .pluck('sg.stage', Arel.sql(status_sql), Arel.sql("(#{warnings_sql})"))
stages_with_statuses.map do |stage|
Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
@@ -551,23 +551,6 @@ module Ci
end
end
- def stage_seeds
- return [] unless config_processor
-
- strong_memoize(:stage_seeds) do
- seeds = config_processor.stages_attributes.inject([]) do |previous_stages, attributes|
- seed = Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes, previous_stages)
- previous_stages + [seed]
- end
-
- seeds.select(&:included?)
- end
- end
-
- def seeds_size
- stage_seeds.sum(&:size)
- end
-
def has_kubernetes_active?
project.deployment_platform&.active?
end
@@ -587,56 +570,14 @@ module Ci
end
end
- def set_config_source
- if ci_yaml_from_repo
- self.config_source = :repository_source
- elsif implied_ci_yaml_file
- self.config_source = :auto_devops_source
- end
- end
-
- ##
- # TODO, setting yaml_errors should be moved to the pipeline creation chain.
- #
- def config_processor
- return unless ci_yaml_file
- return @config_processor if defined?(@config_processor)
-
- @config_processor ||= begin
- ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha, user: user })
- rescue Gitlab::Ci::YamlProcessor::ValidationError => e
- self.yaml_errors = e.message
- nil
- rescue
- self.yaml_errors = 'Undefined error'
- nil
- end
- end
-
- def ci_yaml_file_path
+ # TODO: this logic is duplicate with Pipeline::Chain::Config::Content
+ # we should persist this is `ci_pipelines.config_path`
+ def config_path
return unless repository_source? || unknown_source?
project.ci_config_path.presence || '.gitlab-ci.yml'
end
- def ci_yaml_file
- return @ci_yaml_file if defined?(@ci_yaml_file)
-
- @ci_yaml_file =
- if auto_devops_source?
- implied_ci_yaml_file
- else
- ci_yaml_from_repo
- end
-
- if @ci_yaml_file
- @ci_yaml_file
- else
- self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
- nil
- end
- end
-
def has_yaml_errors?
yaml_errors.present?
end
@@ -705,7 +646,7 @@ module Ci
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
- variables.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
+ variables.append(key: 'CI_CONFIG_PATH', value: config_path)
variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
@@ -783,6 +724,10 @@ module Ci
end
end
+ def has_exposed_artifacts?
+ complete? && builds.latest.with_exposed_artifacts.exists?
+ end
+
def branch_updated?
strong_memoize(:branch_updated) do
push_details.branch_updated?
@@ -896,24 +841,6 @@ module Ci
private
- def ci_yaml_from_repo
- return unless project
- return unless sha
- return unless ci_yaml_file_path
-
- project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
- rescue GRPC::NotFound, GRPC::Internal
- nil
- end
-
- def implied_ci_yaml_file
- return unless project
-
- if project.auto_devops_enabled?
- Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
- end
- end
-
def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self)
end
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 18cbf827a67..7ba04d1a2de 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -65,7 +65,7 @@ module Clusters
end
def retry_command(command)
- "for i in $(seq 1 30); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
+ "for i in $(seq 1 90); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
end
def post_delete_script
diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb
new file mode 100644
index 00000000000..36246b26066
--- /dev/null
+++ b/app/models/clusters/applications/crossplane.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class Crossplane < ApplicationRecord
+ VERSION = '0.4.1'
+
+ self.table_name = 'clusters_applications_crossplane'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+
+ default_value_for :version, VERSION
+
+ default_value_for :stack do |crossplane|
+ ''
+ end
+
+ validates :stack, presence: true
+
+ def chart
+ 'crossplane/crossplane'
+ end
+
+ def repository
+ 'https://charts.crossplane.io/alpha'
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: 'crossplane',
+ repository: repository,
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files
+ )
+ end
+
+ def values
+ crossplane_values.to_yaml
+ end
+
+ private
+
+ def crossplane_values
+ {
+ "clusterStacks" => {
+ self.stack => {
+ "deploy" => true,
+ "version" => "alpha"
+ }
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
new file mode 100644
index 00000000000..8589f8c00cb
--- /dev/null
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class ElasticStack < ApplicationRecord
+ VERSION = '1.8.0'
+
+ ELASTICSEARCH_PORT = 9200
+
+ self.table_name = 'clusters_applications_elastic_stacks'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+ include ::Gitlab::Utils::StrongMemoize
+
+ default_value_for :version, VERSION
+
+ def set_initial_status
+ return unless not_installable?
+ return unless cluster&.application_ingress_available?
+
+ ingress = cluster.application_ingress
+ self.status = status_states[:installable] if ingress.external_ip_or_hostname?
+ end
+
+ def chart
+ 'stable/elastic-stack'
+ end
+
+ def values
+ content_values.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: 'elastic-stack',
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files
+ )
+ end
+
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: 'elastic-stack',
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files,
+ postdelete: post_delete_script
+ )
+ end
+
+ def elasticsearch_client
+ strong_memoize(:elasticsearch_client) do
+ next unless kube_client
+
+ proxy_url = kube_client.proxy_url('service', 'elastic-stack-elasticsearch-client', ::Clusters::Applications::ElasticStack::ELASTICSEARCH_PORT, Gitlab::Kubernetes::Helm::NAMESPACE)
+
+ Elasticsearch::Client.new(url: proxy_url) do |faraday|
+ # ensures headers containing auth data are appended to original client options
+ faraday.headers.merge!(kube_client.headers)
+ # ensure TLS certs are properly verified
+ faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
+ faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
+ end
+
+ rescue Kubeclient::HttpError => error
+ # If users have mistakenly set parameters or removed the depended clusters,
+ # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
+ # We check for a nil client in downstream use and behaviour is equivalent to an empty state
+ log_exception(error, :failed_to_create_elasticsearch_client)
+ end
+ end
+
+ private
+
+ def specification
+ {
+ "kibana" => {
+ "ingress" => {
+ "hosts" => [kibana_hostname],
+ "tls" => [{
+ "hosts" => [kibana_hostname],
+ "secretName" => "kibana-cert"
+ }]
+ }
+ }
+ }
+ end
+
+ def content_values
+ YAML.load_file(chart_values_file).deep_merge!(specification)
+ end
+
+ def post_delete_script
+ [
+ Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack")
+ ].compact
+ end
+
+ def kube_client
+ cluster&.kubeclient&.core_client
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 885e4ff7197..d140649af3c 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -21,6 +21,7 @@ module Clusters
}
FETCH_IP_ADDRESS_DELAY = 30.seconds
+ MODSEC_SIDECAR_INITIAL_DELAY_SECONDS = 10
state_machine :status do
after_transition any => [:installed] do |application|
@@ -40,7 +41,7 @@ module Clusters
end
def allowed_to_uninstall?
- external_ip_or_hostname? && application_jupyter_nil_or_installable?
+ external_ip_or_hostname? && application_jupyter_nil_or_installable? && application_elastic_stack_nil_or_installable?
end
def install_command
@@ -78,12 +79,74 @@ module Clusters
"controller" => {
"config" => {
"enable-modsecurity" => "true",
- "enable-owasp-modsecurity-crs" => "true"
- }
+ "enable-owasp-modsecurity-crs" => "true",
+ "modsecurity.conf" => modsecurity_config_content
+ },
+ "extraContainers" => [
+ {
+ "name" => "modsecurity-log",
+ "image" => "busybox",
+ "args" => [
+ "/bin/sh",
+ "-c",
+ "tail -f /var/log/modsec/audit.log"
+ ],
+ "volumeMounts" => [
+ {
+ "name" => "modsecurity-log-volume",
+ "mountPath" => "/var/log/modsec",
+ "readOnly" => true
+ }
+ ],
+ "startupProbe" => {
+ "exec" => {
+ "command" => ["ls", "/var/log/modsec"]
+ },
+ "initialDelaySeconds" => MODSEC_SIDECAR_INITIAL_DELAY_SECONDS
+ }
+ }
+ ],
+ "extraVolumeMounts" => [
+ {
+ "name" => "modsecurity-template-volume",
+ "mountPath" => "/etc/nginx/modsecurity/modsecurity.conf",
+ "subPath" => "modsecurity.conf"
+ },
+ {
+ "name" => "modsecurity-log-volume",
+ "mountPath" => "/var/log/modsec"
+ }
+ ],
+ "extraVolumes" => [
+ {
+ "name" => "modsecurity-template-volume",
+ "configMap" => {
+ "name" => "ingress-nginx-ingress-controller",
+ "items" => [
+ {
+ "key" => "modsecurity.conf",
+ "path" => "modsecurity.conf"
+ }
+ ]
+ }
+ },
+ {
+ "name" => "modsecurity-log-volume",
+ "emptyDir" => {}
+ }
+ ]
}
}
end
+ def modsecurity_config_content
+ File.read(modsecurity_config_file_path)
+ end
+
+ def modsecurity_config_file_path
+ Rails.root.join('vendor', 'ingress', 'modsecurity.conf')
+ end
+
def content_values
YAML.load_file(chart_values_file).deep_merge!(specification)
end
@@ -91,6 +154,10 @@ module Clusters
def application_jupyter_nil_or_installable?
cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
end
+
+ def application_elastic_stack_nil_or_installable?
+ cluster.application_elastic_stack.nil? || cluster.application_elastic_stack&.installable?
+ end
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 954046c143b..37ba8a7c97e 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.9.0'
+ VERSION = '0.10.1'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index d6f5d7c3f93..f522f3f2fdb 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -6,20 +6,21 @@ module Clusters
include Gitlab::Utils::StrongMemoize
include FromUnion
include ReactiveCaching
+ include AfterCommitQueue
self.table_name = 'clusters'
- PROJECT_ONLY_APPLICATIONS = {
- }.freeze
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
Applications::CertManager.application_name => Applications::CertManager,
+ Applications::Crossplane.application_name => Applications::Crossplane,
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
Applications::Jupyter.application_name => Applications::Jupyter,
- Applications::Knative.application_name => Applications::Knative
- }.merge(PROJECT_ONLY_APPLICATIONS).freeze
+ Applications::Knative.application_name => Applications::Knative,
+ Applications::ElasticStack.application_name => Applications::ElasticStack
+ }.freeze
DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
@@ -47,14 +48,17 @@ module Clusters
has_one_cluster_application :helm
has_one_cluster_application :ingress
has_one_cluster_application :cert_manager
+ has_one_cluster_application :crossplane
has_one_cluster_application :prometheus
has_one_cluster_application :runner
has_one_cluster_application :jupyter
has_one_cluster_application :knative
+ has_one_cluster_application :elastic_stack
has_many :kubernetes_namespaces
accepts_nested_attributes_for :provider_gcp, update_only: true
+ accepts_nested_attributes_for :provider_aws, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
@@ -72,6 +76,7 @@ module Clusters
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
+ delegate :knative_pre_installed?, to: :provider, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
@@ -115,6 +120,8 @@ module Clusters
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
+ scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
+
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
return [] if clusterable.is_a?(Instance)
@@ -124,7 +131,55 @@ module Clusters
hierarchy_groups.flat_map(&:clusters) + Instance.new.clusters
end
+ state_machine :cleanup_status, initial: :cleanup_not_started do
+ state :cleanup_not_started, value: 1
+ state :cleanup_uninstalling_applications, value: 2
+ state :cleanup_removing_project_namespaces, value: 3
+ state :cleanup_removing_service_account, value: 4
+ state :cleanup_errored, value: 5
+
+ event :start_cleanup do |cluster|
+ transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications
+ end
+
+ event :continue_cleanup do
+ transition(
+ cleanup_uninstalling_applications: :cleanup_removing_project_namespaces,
+ cleanup_removing_project_namespaces: :cleanup_removing_service_account)
+ end
+
+ event :make_cleanup_errored do
+ transition any => :cleanup_errored
+ end
+
+ before_transition any => [:cleanup_errored] do |cluster, transition|
+ status_reason = transition.args.first
+ cluster.cleanup_status_reason = status_reason if status_reason
+ end
+
+ after_transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications do |cluster|
+ cluster.run_after_commit do
+ Clusters::Cleanup::AppWorker.perform_async(cluster.id)
+ end
+ end
+
+ after_transition cleanup_uninstalling_applications: :cleanup_removing_project_namespaces do |cluster|
+ cluster.run_after_commit do
+ Clusters::Cleanup::ProjectNamespaceWorker.perform_async(cluster.id)
+ end
+ end
+
+ after_transition cleanup_removing_project_namespaces: :cleanup_removing_service_account do |cluster|
+ cluster.run_after_commit do
+ Clusters::Cleanup::ServiceAccountWorker.perform_async(cluster.id)
+ end
+ end
+ end
+
def status_name
+ return cleanup_status_name if cleanup_errored?
+ return :cleanup_ongoing unless cleanup_not_started?
+
provider&.status_name || connection_status.presence || :created
end
@@ -207,10 +262,6 @@ module Clusters
end
end
- def knative_pre_installed?
- provider&.knative_pre_installed?
- end
-
private
def unique_management_project_environment_scope
diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb
index a906eb2888b..c9c18d8c96a 100644
--- a/app/models/clusters/clusters_hierarchy.rb
+++ b/app/models/clusters/clusters_hierarchy.rb
@@ -20,7 +20,7 @@ module Clusters
.with
.recursive(cte.to_arel)
.from(cte_alias)
- .order(DEPTH_COLUMN => :asc)
+ .order(depth_order_clause)
end
private
@@ -40,7 +40,7 @@ module Clusters
end
if clusterable.is_a?(::Project) && include_management_project
- cte << management_clusters_query
+ cte << same_namespace_management_clusters_query
end
cte << base_query
@@ -49,13 +49,42 @@ module Clusters
cte
end
+ # Returns project-level clusters where the project is the management project
+ # for the cluster. The management project has to be in the same namespace /
+ # group as the cluster's project.
+ #
+ # Support for management project in sub-groups is planned in
+ # https://gitlab.com/gitlab-org/gitlab/issues/34650
+ #
+ # NB: group_parent_id is un-used but we still need to match the same number of
+ # columns as other queries in the CTE.
+ def same_namespace_management_clusters_query
+ clusterable.management_clusters
+ .project_type
+ .select([clusters_star, 'NULL AS group_parent_id', "0 AS #{DEPTH_COLUMN}"])
+ .for_project_namespace(clusterable.namespace_id)
+ end
+
# Management clusters should be first in the hierarchy so we use 0 for the
# depth column.
#
- # group_parent_id is un-used but we still need to match the same number of
- # columns as other queries in the CTE.
- def management_clusters_query
- clusterable.management_clusters.select([clusters_star, 'NULL AS group_parent_id', "0 AS #{DEPTH_COLUMN}"])
+ # Only applicable if the clusterable is a project (most especially when
+ # requesting project.deployment_platform).
+ def depth_order_clause
+ return { DEPTH_COLUMN => :asc } unless clusterable.is_a?(::Project) && include_management_project
+
+ order = <<~SQL
+ (CASE clusters.management_project_id
+ WHEN :project_id THEN 0
+ ELSE #{DEPTH_COLUMN}
+ END) ASC
+ SQL
+
+ values = {
+ project_id: clusterable.id
+ }
+
+ model.sanitize_sql_array([Arel.sql(order), values])
end
def group_clusters_base_query
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index 979cf0645f5..21b98534808 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -60,6 +60,24 @@ module Clusters
# Override if your application needs any action after
# being uninstalled by Helm
end
+
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
+
+ def log_exception(error, event)
+ logger.error({
+ exception: error.class.name,
+ status_code: error.error_code,
+ cluster_id: cluster&.id,
+ application_id: id,
+ class_name: self.class.name,
+ event: event,
+ message: error.message
+ })
+
+ Gitlab::Sentry.track_acceptable_exception(error, extra: { cluster_id: cluster&.id, application_id: id })
+ end
end
end
end
diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb
index f21dbdf7f26..8c9d9ab9ab1 100644
--- a/app/models/clusters/instance.rb
+++ b/app/models/clusters/instance.rb
@@ -9,5 +9,9 @@ module Clusters
def feature_available?(feature)
::Feature.enabled?(feature, default_enabled: true)
end
+
+ def flipper_id
+ self.class.to_s
+ end
end
end
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index ae4156896bc..78eb75ddcc0 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -3,12 +3,12 @@
module Clusters
module Providers
class Aws < ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
include Clusters::Concerns::ProviderStatus
self.table_name = 'cluster_providers_aws'
belongs_to :cluster, inverse_of: :provider_aws, class_name: 'Clusters::Cluster'
- belongs_to :created_by_user, class_name: 'User'
default_value_for :region, 'us-east-1'
default_value_for :num_nodes, 3
@@ -42,6 +42,30 @@ module Clusters
session_token: nil
)
end
+
+ def api_client
+ strong_memoize(:api_client) do
+ ::Aws::CloudFormation::Client.new(credentials: credentials, region: region)
+ end
+ end
+
+ def credentials
+ strong_memoize(:credentials) do
+ ::Aws::Credentials.new(access_key_id, secret_access_key, session_token)
+ end
+ end
+
+ def has_rbac_enabled?
+ true
+ end
+
+ def knative_pre_installed?
+ false
+ end
+
+ def created_by_user
+ cluster.user
+ end
end
end
end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index f871674676f..2ca7d0249dc 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -54,6 +54,10 @@ module Clusters
assign_attributes(operation_id: operation_id)
end
+ def has_rbac_enabled?
+ !legacy_abac
+ end
+
def knative_pre_installed?
cloud_run?
end
diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb
index a540e291990..2ca6d15e642 100644
--- a/app/models/commit_status_enums.rb
+++ b/app/models/commit_status_enums.rb
@@ -15,7 +15,9 @@ module CommitStatusEnums
stale_schedule: 7,
job_execution_timeout: 8,
archived_failure: 9,
- unmet_prerequisites: 10
+ unmet_prerequisites: 10,
+ scheduler_failure: 11,
+ data_integrity_failure: 12
}
end
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 54e9a13d1ea..0e07806dd6f 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -4,19 +4,28 @@ module Analytics
module CycleAnalytics
module Stage
extend ActiveSupport::Concern
+ include RelativePositioning
+ include Gitlab::Utils::StrongMemoize
included do
+ belongs_to :start_event_label, class_name: 'GroupLabel', optional: true
+ belongs_to :end_event_label, class_name: 'GroupLabel', optional: true
+
validates :name, presence: true
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
validates :start_event_identifier, presence: true
validates :end_event_identifier, presence: true
+ validates :start_event_label, presence: true, if: :start_event_label_based?
+ validates :end_event_label, presence: true, if: :end_event_label_based?
validate :validate_stage_event_pairs
+ validate :validate_labels
enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :start_event_identifier
enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier
alias_attribute :custom_stage?, :custom
scope :default_stages, -> { where(custom: false) }
+ scope :ordered, -> { order(:relative_position, :id) }
end
def parent=(_)
@@ -28,19 +37,41 @@ module Analytics
end
def start_event
- Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event)
+ strong_memoize(:start_event) do
+ Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event)
+ end
end
def end_event
- Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event)
+ strong_memoize(:end_event) do
+ Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event)
+ end
+ end
+
+ def start_event_label_based?
+ start_event_identifier && start_event.label_based?
+ end
+
+ def end_event_label_based?
+ end_event_identifier && end_event.label_based?
+ end
+
+ def start_event_identifier=(identifier)
+ clear_memoization(:start_event)
+ super
+ end
+
+ def end_event_identifier=(identifier)
+ clear_memoization(:end_event)
+ super
end
def params_for_start_event
- {}
+ start_event_label.present? ? { label: start_event_label } : {}
end
def params_for_end_event
- {}
+ end_event_label.present? ? { label: end_event_label } : {}
end
def default_stage?
@@ -58,19 +89,44 @@ module Analytics
end_event_identifier.to_s.eql?(stage_params[:end_event_identifier].to_s)
end
+ def find_with_same_parent!(id)
+ parent.cycle_analytics_stages.find(id)
+ end
+
private
def validate_stage_event_pairs
return if start_event_identifier.nil? || end_event_identifier.nil?
unless pairing_rules.fetch(start_event.class, []).include?(end_event.class)
- errors.add(:end_event, :not_allowed_for_the_given_start_event)
+ errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event'))
end
end
def pairing_rules
Gitlab::Analytics::CycleAnalytics::StageEvents.pairing_rules
end
+
+ def validate_labels
+ validate_label_within_group(:start_event_label, start_event_label_id) if start_event_label_id_changed?
+ validate_label_within_group(:end_event_label, end_event_label_id) if end_event_label_id_changed?
+ end
+
+ def validate_label_within_group(association_name, label_id)
+ return unless label_id
+ return unless group
+
+ unless label_available_for_group?(label_id)
+ errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group'))
+ end
+ end
+
+ def label_available_for_group?(label_id)
+ LabelsFinder.new(nil, { group_id: group.id, include_ancestor_groups: true, only_group_labels: true })
+ .execute(skip_authorization: true)
+ .by_ids(label_id)
+ .exists?
+ end
end
end
end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index a0ca8a34c6d..17d431bacf2 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -16,6 +16,7 @@ module Ci
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
+ delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
before_create :ensure_metadata
end
@@ -45,6 +46,9 @@ module Ci
def options=(value)
write_metadata_attribute(:options, :config_options, value)
+
+ # Store presence of exposed artifacts in build metadata to make it easier to query
+ ensure_metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present?
end
def yaml_variables=(value)
diff --git a/app/models/concerns/ci/processable.rb b/app/models/concerns/ci/processable.rb
index 268fa8ec692..ed0087f34d4 100644
--- a/app/models/concerns/ci/processable.rb
+++ b/app/models/concerns/ci/processable.rb
@@ -8,6 +8,14 @@ module Ci
#
#
module Processable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
+
+ accepts_nested_attributes_for :needs
+ end
+
def schedulable?
raise NotImplementedError
end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index fe8e9609820..3b893a56bd6 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -12,7 +12,7 @@ module DeploymentPlatform
private
def cluster_management_project_enabled?
- Feature.enabled?(:cluster_management_project, default_enabled: true)
+ Feature.enabled?(:cluster_management_project, self, default_enabled: true)
end
def find_deployment_platform(environment)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 852576dbbc2..01cd1e0224b 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -118,8 +118,8 @@ module Issuable
# rubocop:enable GitlabSecurity/SqlInjection
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
- scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
- scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') }
+ scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
+ scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :any_label, -> { joins(:label_links).group(:id) }
@@ -137,6 +137,26 @@ module Issuable
strip_attributes :title
+ # The state_machine gem will reset the value of state_id unless it
+ # is a raw attribute passed in here:
+ # https://gitlab.com/gitlab-org/gitlab/issues/35746#note_241148787
+ #
+ # This assumes another initialize isn't defined. Otherwise this
+ # method may need to be prepended.
+ def initialize(attributes = nil)
+ if attributes.is_a?(Hash)
+ attr = attributes.symbolize_keys
+
+ if attr.key?(:state) && !attr.key?(:state_id)
+ value = attr.delete(:state)
+ state_id = self.class.available_states[value]
+ attributes[:state_id] = state_id if state_id
+ end
+ end
+
+ super(attributes)
+ end
+
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled?
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 42b370990ac..b1a7d7ec819 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -101,6 +101,10 @@ module Milestoneish
false
end
+ def global_milestone?
+ false
+ end
+
def total_issue_time_spent
@total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 3065e0ba6c5..19f2daa1b01 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -108,10 +108,6 @@ module Noteable
discussions_resolvable? && resolvable_discussions.none?(&:to_be_resolved?)
end
- def discussions_to_be_resolved?
- discussions_resolvable? && !discussions_resolved?
- end
-
def discussions_to_be_resolved
@discussions_to_be_resolved ||= resolvable_discussions.select(&:to_be_resolved?)
end
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index ebacc459cb5..d9a7f0a96dc 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -39,8 +39,8 @@ module ProtectedRef
end
end
- def developers_can?(action, ref)
- access_levels_for_ref(ref, action: action).any? do |access_level|
+ def developers_can?(action, ref, protected_refs: nil)
+ access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.access_level == Gitlab::Access::DEVELOPER
end
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 78544405c49..9c2b0372d54 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -55,20 +55,22 @@ module Storage
def move_repositories
# Move the namespace directory in all storages used by member projects
- repository_storages.each do |repository_storage|
+ repository_storages(legacy_only: true).each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)
+ Gitlab::GitalyClient::NamespaceService.allow do
+ gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)
- # Ensure new directory exists before moving it (if there's a parent)
- gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
+ # Ensure new directory exists before moving it (if there's a parent)
+ gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
- unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
+ unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
- Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger
+ Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger
- # if we cannot move namespace directory we should rollback
- # db changes in order to prevent out of sync between db and fs
- raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
+ end
end
end
end
@@ -77,12 +79,14 @@ module Storage
@old_repository_storage_paths ||= repository_storages
end
- def repository_storages
+ def repository_storages(legacy_only: false)
# We need to get the storage paths for all the projects, even the ones that are
# pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT.
Project.unscoped do
- all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage)
+ namespace_projects = all_projects
+ namespace_projects = namespace_projects.without_storage_feature(:repository) if legacy_only
+ namespace_projects.pluck(Arel.sql('distinct(repository_storage)'))
end
end
@@ -93,13 +97,15 @@ module Storage
# We will remove it later async
new_path = "#{full_path}+#{id}+deleted"
- if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
- Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
+ Gitlab::GitalyClient::NamespaceService.allow do
+ if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
+ Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
- # Remove namespace directory async with delay so
- # GitLab has time to remove all projects first
- run_after_commit do
- GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
+ # Remove namespace directory async with delay so
+ # GitLab has time to remove all projects first
+ run_after_commit do
+ GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
+ end
end
end
end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 92a5c1112af..33e9e0e38fb 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -59,6 +59,14 @@ module Subscribable
.update(subscribed: false)
end
+ def set_subscription(user, desired_state, project = nil)
+ if desired_state
+ subscribe(user, project)
+ else
+ unsubscribe(user, project)
+ end
+ end
+
private
def unsubscribe_from_other_levels(user, project)
diff --git a/app/models/concerns/worker_attributes.rb b/app/models/concerns/worker_attributes.rb
index af40e9e3b19..506215ca9ed 100644
--- a/app/models/concerns/worker_attributes.rb
+++ b/app/models/concerns/worker_attributes.rb
@@ -3,6 +3,10 @@
module WorkerAttributes
extend ActiveSupport::Concern
+ # Resource boundaries that workers can declare through the
+ # `worker_resource_boundary` attribute
+ VALID_RESOURCE_BOUNDARIES = [:memory, :cpu, :unknown].freeze
+
class_methods do
def feature_category(value)
raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
@@ -24,6 +28,48 @@ module WorkerAttributes
get_worker_attribute(:feature_category) == :not_owned
end
+ # This should be set for jobs that need to be run immediately, or, if
+ # they are delayed, risk creating inconsistencies in the application
+ # that could being perceived by the user as incorrect behavior
+ # (ie, a bug)
+ # See doc/development/sidekiq_style_guide.md#Latency-Sensitive-Jobs
+ # for details
+ def latency_sensitive_worker!
+ worker_attributes[:latency_sensitive] = true
+ end
+
+ # Returns a truthy value if the worker is latency sensitive.
+ # See doc/development/sidekiq_style_guide.md#Latency-Sensitive-Jobs
+ # for details
+ def latency_sensitive_worker?
+ worker_attributes[:latency_sensitive]
+ end
+
+ # Set this attribute on a job when it will call to services outside of the
+ # application, such as 3rd party applications, other k8s clusters etc See
+ # doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
+ # details
+ def worker_has_external_dependencies!
+ worker_attributes[:external_dependencies] = true
+ end
+
+ # Returns a truthy value if the worker has external dependencies.
+ # See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
+ # for details
+ def worker_has_external_dependencies?
+ worker_attributes[:external_dependencies]
+ end
+
+ def worker_resource_boundary(boundary)
+ raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
+
+ worker_attributes[:resource_boundary] = boundary
+ end
+
+ def get_worker_resource_boundary
+ worker_attributes[:resource_boundary] || :unknown
+ end
+
protected
# Returns a worker attribute declared on this class or its parent class.
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 27bb76835c7..152aa7b3218 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -11,7 +11,10 @@ class ContainerRepository < ApplicationRecord
delegate :client, to: :registry
scope :ordered, -> { order(:name) }
- scope :with_api_entity_associations, -> { preload(:project) }
+ scope :with_api_entity_associations, -> { preload(project: [:route, { namespace: :route }]) }
+ scope :for_group_and_its_subgroups, ->(group) do
+ where(project_id: Project.for_group_and_its_subgroups(group).with_container_registry.select(:id))
+ end
# rubocop: disable CodeReuse/ServiceClass
def registry
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
index ec52f1ed370..cf6094682f3 100644
--- a/app/models/dashboard_group_milestone.rb
+++ b/app/models/dashboard_group_milestone.rb
@@ -18,4 +18,8 @@ class DashboardGroupMilestone < GlobalMilestone
milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
Milestone.filter_by_state(milestones, params[:state]).map { |m| new(m) }
end
+
+ def dashboard_milestone?
+ true
+ end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 7ccd5e98360..4a38912db9b 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -10,6 +10,10 @@ class Deployment < ApplicationRecord
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user
belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
+ has_many :deployment_merge_requests
+
+ has_many :merge_requests,
+ through: :deployment_merge_requests
has_internal_id :iid, scope: :project, init: ->(s) do
Deployment.where(project: s.project).maximum(:iid) if s&.project
@@ -75,6 +79,11 @@ class Deployment < ApplicationRecord
find(ids)
end
+ def self.distinct_on_environment
+ order('environment_id, deployments.id DESC')
+ .select('DISTINCT ON (environment_id) deployments.*')
+ end
+
def self.find_successful_deployment!(iid)
success.find_by!(iid: iid)
end
@@ -144,6 +153,18 @@ class Deployment < ApplicationRecord
project.deployments.joins(:environment)
.where(environments: { name: self.environment.name }, ref: self.ref)
.where.not(id: self.id)
+ .order(id: :desc)
+ .take
+ end
+
+ def previous_environment_deployment
+ project
+ .deployments
+ .success
+ .joins(:environment)
+ .where(environments: { name: environment.name })
+ .where.not(id: self.id)
+ .order(id: :desc)
.take
end
@@ -176,6 +197,18 @@ class Deployment < ApplicationRecord
deployable&.user || user
end
+ def link_merge_requests(relation)
+ select = relation.select(['merge_requests.id', id]).to_sql
+
+ # We don't use `Gitlab::Database.bulk_insert` here so that we don't need to
+ # first pluck lots of IDs into memory.
+ DeploymentMergeRequest.connection.execute(<<~SQL)
+ INSERT INTO #{DeploymentMergeRequest.table_name}
+ (merge_request_id, deployment_id)
+ #{select}
+ SQL
+ end
+
private
def ref_path
diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb
new file mode 100644
index 00000000000..ff4d9f66202
--- /dev/null
+++ b/app/models/deployment_merge_request.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class DeploymentMergeRequest < ApplicationRecord
+ belongs_to :deployment, optional: false
+ belongs_to :merge_request, optional: false
+end
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
index abab7f94212..05362a2f90b 100644
--- a/app/models/description_version.rb
+++ b/app/models/description_version.rb
@@ -10,6 +10,10 @@ class DescriptionVersion < ApplicationRecord
%i(issue merge_request).freeze
end
+ def issuable
+ issue || merge_request
+ end
+
private
def exactly_one_issuable
diff --git a/app/models/environment.rb b/app/models/environment.rb
index af0c219d9a0..327b1e594d7 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -4,12 +4,20 @@ class Environment < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include ReactiveCaching
+ self.reactive_cache_refresh_interval = 1.minute
+ self.reactive_cache_lifetime = 55.seconds
+
belongs_to :project, required: true
has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
+ has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
+ has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
+ has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
+ 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'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -60,6 +68,10 @@ class Environment < ApplicationRecord
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)) }
+ scope :unfoldered, -> { where(environment_type: nil) }
+ scope :with_rank, -> do
+ select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
+ end
state_machine :state, initial: :available do
event :start do
@@ -188,6 +200,10 @@ class Environment < ApplicationRecord
prometheus_adapter.query(:environment, self) if has_metrics?
end
+ def prometheus_status
+ deployment_platform&.cluster&.application_prometheus&.status_name
+ end
+
def additional_metrics(*args)
return unless has_metrics?
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 0b4fef5eac1..2aa058a243f 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -7,6 +7,7 @@ module ErrorTracking
SENTRY_API_ERROR_TYPE_MISSING_KEYS = 'missing_keys_in_sentry_response'
SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE = 'non_20x_response_from_sentry'
+ SENTRY_API_ERROR_INVALID_SIZE = 'invalid_size_of_sentry_response'
API_URL_PATH_REGEXP = %r{
\A
@@ -87,15 +88,37 @@ module ErrorTracking
{ projects: sentry_client.list_projects }
end
+ def issue_details(opts = {})
+ with_reactive_cache('issue_details', opts.stringify_keys) do |result|
+ result
+ end
+ end
+
+ def issue_latest_event(opts = {})
+ with_reactive_cache('issue_latest_event', opts.stringify_keys) do |result|
+ result
+ end
+ end
+
def calculate_reactive_cache(request, opts)
case request
when 'list_issues'
{ issues: sentry_client.list_issues(**opts.symbolize_keys) }
+ when 'issue_details'
+ {
+ issue: sentry_client.issue_details(**opts.symbolize_keys)
+ }
+ when 'issue_latest_event'
+ {
+ latest_event: sentry_client.issue_latest_event(**opts.symbolize_keys)
+ }
end
rescue Sentry::Client::Error => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE }
rescue Sentry::Client::MissingKeysError => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_MISSING_KEYS }
+ rescue Sentry::Client::ResponseInvalidSizeError => e
+ { error: e.message, error_type: SENTRY_API_ERROR_INVALID_SIZE }
end
# http://HOST/api/0/projects/ORG/PROJECT
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 7d766e1f25c..65fd5c1b35a 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -11,7 +11,7 @@ class GlobalMilestone
delegate :title, :state, :due_date, :start_date, :participants, :project,
:group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
- :milestoneish_id, :resource_parent, to: :milestone
+ :milestoneish_id, :resource_parent, :releases, to: :milestone
def to_hash
{
@@ -100,4 +100,8 @@ class GlobalMilestone
def labels
@labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
end
+
+ def global_milestone?
+ true
+ end
end
diff --git a/app/models/grafana_integration.rb b/app/models/grafana_integration.rb
index 51cc398394d..ed4c279965a 100644
--- a/app/models/grafana_integration.rb
+++ b/app/models/grafana_integration.rb
@@ -14,7 +14,13 @@ class GrafanaIntegration < ApplicationRecord
validates :token, :project, presence: true
+ validates :enabled, inclusion: { in: [true, false] }
+
+ scope :enabled, -> { where(enabled: true) }
+
def client
+ return unless enabled?
+
@client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token)
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 042201ffa14..8289d4f099c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -30,6 +30,10 @@ class Group < Namespace
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
+ has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
+ has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
+ has_many :shared_groups, through: :shared_group_links, source: :shared_group
+ has_many :shared_with_groups, through: :shared_with_group_links, source: :shared_with_group
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
@@ -51,6 +55,8 @@ class Group < Namespace
has_many :todos
+ has_one :import_export_upload
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
@@ -120,7 +126,7 @@ class Group < Namespace
def visible_to_user_arel(user)
groups_table = self.arel_table
- authorized_groups = user.authorized_groups.as('authorized')
+ authorized_groups = user.authorized_groups.arel.as('authorized')
groups_table.project(1)
.from(authorized_groups)
@@ -259,8 +265,8 @@ class Group < Namespace
members_with_parents.maintainers.exists?(user_id: user)
end
- def has_container_repositories?
- container_repositories.exists?
+ def has_container_repository_including_subgroups?
+ ::ContainerRepository.for_group_and_its_subgroups(self).exists?
end
# @deprecated
@@ -376,11 +382,12 @@ class Group < Namespace
return GroupMember::OWNER if user.admin?
- members_with_parents
- .where(user_id: user)
- .reorder(access_level: :desc)
- .first&.
- access_level || GroupMember::NO_ACCESS
+ max_member_access = members_with_parents.where(user_id: user)
+ .reorder(access_level: :desc)
+ .first
+ &.access_level
+
+ max_member_access || max_member_access_for_user_from_shared_groups(user) || GroupMember::NO_ACCESS
end
def mattermost_team_params
@@ -444,6 +451,14 @@ class Group < Namespace
false
end
+ def export_file_exists?
+ export_file&.file
+ end
+
+ def export_file
+ import_export_upload&.export_file
+ end
+
private
def update_two_factor_requirement
@@ -474,6 +489,26 @@ class Group < Namespace
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
end
+ def max_member_access_for_user_from_shared_groups(user)
+ return unless Feature.enabled?(:share_group_with_group)
+
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids)
+ cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
+
+ link = GroupGroupLink
+ .with(cte.to_arel)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:user_id].eq(user.id))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access]))
+ .first
+
+ link&.group_access
+ end
+
def self.groups_including_descendants_by(group_ids)
Gitlab::ObjectHierarchy
.new(Group.where(id: group_ids))
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
new file mode 100644
index 00000000000..4b279b7af5b
--- /dev/null
+++ b/app/models/group_group_link.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class GroupGroupLink < ApplicationRecord
+ include Expirable
+
+ belongs_to :shared_group, class_name: 'Group', foreign_key: :shared_group_id
+ belongs_to :shared_with_group, class_name: 'Group', foreign_key: :shared_with_group_id
+
+ validates :shared_group, presence: true
+ validates :shared_group_id, uniqueness: { scope: [:shared_with_group_id],
+ message: _('The group has already been shared with this group') }
+ validates :shared_with_group, presence: true
+ validates :group_access, inclusion: { in: Gitlab::Access.values },
+ presence: true
+
+ def self.access_options
+ Gitlab::Access.options
+ end
+
+ def self.default_access
+ Gitlab::Access::DEVELOPER
+ end
+end
diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb
index 60f5491849a..7d73fd281f1 100644
--- a/app/models/import_export_upload.rb
+++ b/app/models/import_export_upload.rb
@@ -5,6 +5,7 @@ class ImportExportUpload < ApplicationRecord
include ObjectStorage::BackgroundMove
belongs_to :project
+ belongs_to :group
# These hold the project Import/Export archives (.tar.gz files)
mount_uploader :import_file, ImportExportUploader
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b9b481ac29b..948cadc34e5 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -40,6 +40,7 @@ class Issue < ApplicationRecord
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
+ has_many :zoom_meetings
validates :project, presence: true
@@ -54,9 +55,9 @@ class Issue < ApplicationRecord
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
- scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
- scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
- scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') }
+ scope :order_due_date_asc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
+ scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
+ scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :preload_associations, -> { preload(:labels, project: :namespace) }
@@ -65,6 +66,8 @@ class Issue < ApplicationRecord
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
+ scope :counts_by_state, -> { reorder(nil).group(:state).count }
+
after_commit :expire_etag_cache
after_save :ensure_metrics, unless: :imported?
@@ -137,8 +140,8 @@ class Issue < ApplicationRecord
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
- when 'due_date', 'due_date_asc' then order_due_date_asc
- when 'due_date_desc' then order_due_date_desc
+ when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
+ when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
else
super
@@ -206,7 +209,16 @@ class Issue < ApplicationRecord
if self.confidential?
"#{iid}-confidential-issue"
else
- "#{iid}-#{title.parameterize}"
+ branch_name = "#{iid}-#{title.parameterize}"
+
+ if branch_name.length > 100
+ truncated_string = branch_name[0, 100]
+ # Delete everything dangling after the last hyphen so as not to risk
+ # existence of unintended words in the branch name due to mid-word split.
+ branch_name = truncated_string[0, truncated_string.rindex("-")]
+ end
+
+ branch_name
end
end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 535c3cf2ba1..48c971194c6 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -18,6 +18,11 @@ class LfsObject < ApplicationRecord
after_save :update_file_store, if: :saved_change_to_file?
+ def self.not_linked_to_project(project)
+ where('NOT EXISTS (?)',
+ project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
+ end
+
def update_file_store
# The file.object_store is set during `uploader.store!`
# which happens after object is inserted/updated
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 32741046f39..7e1898e7142 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -16,6 +16,9 @@ class MergeRequest < ApplicationRecord
include ReactiveCaching
include FromUnion
include DeprecatedAssignee
+ include ShaAttribute
+
+ sha_attribute :squash_commit_sha
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
@@ -65,6 +68,7 @@ class MergeRequest < ApplicationRecord
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
has_many :pipelines_for_merge_request, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
has_many :suggestions, through: :notes
+ has_many :unresolved_notes, -> { unresolved }, as: :noteable, class_name: 'Note'
has_many :merge_request_assignees
has_many :assignees, class_name: "User", through: :merge_request_assignees
@@ -202,11 +206,14 @@ class MergeRequest < ApplicationRecord
scope :by_commit_sha, ->(sha) do
where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
end
+ scope :by_merge_commit_sha, -> (sha) do
+ where(merge_commit_sha: sha)
+ end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
scope :with_api_entity_associations, -> {
- preload(:assignees, :author, :notes, :labels, :milestone, :timelogs,
- latest_merge_request_diff: [:merge_request_diff_commits],
+ preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
+ :timelogs, :latest_merge_request_diff,
metrics: [:latest_closed_by, :merged_by],
target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
@@ -217,17 +224,27 @@ class MergeRequest < ApplicationRecord
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
scope :preload_source_project, -> { preload(:source_project) }
- scope :with_open_merge_when_pipeline_succeeds, -> do
- with_state(:opened).where(merge_when_pipeline_succeeds: true)
+ scope :with_auto_merge_enabled, -> do
+ with_state(:opened).where(auto_merge_enabled: true)
end
after_save :keep_around_commit
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
+
+ # Currently, `merge_when_pipeline_succeeds` column is used as a flag
+ # to check if _any_ auto merge strategy is activated on the merge request.
+ # Today, we have multiple strategies and MWPS is one of them.
+ # we'd eventually rename the column for avoiding confusions, but in the mean time
+ # please use `auto_merge_enabled` alias instead of `merge_when_pipeline_succeeds`.
alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
alias_method :issuing_parent, :target_project
+ RebaseLockTimeout = Class.new(StandardError)
+
+ REBASE_LOCK_MESSAGE = _("Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later.")
+
def self.reference_prefix
'!'
end
@@ -357,11 +374,12 @@ class MergeRequest < ApplicationRecord
"#{project.to_reference(from, full: full)}#{reference}"
end
- def commits
- return merge_request_diff.commits if persisted?
+ def commits(limit: nil)
+ return merge_request_diff.commits(limit: limit) if persisted?
commits_arr = if compare_commits
- compare_commits.reverse
+ reversed_commits = compare_commits.reverse
+ limit ? reversed_commits.take(limit) : reversed_commits
else
[]
end
@@ -369,6 +387,10 @@ class MergeRequest < ApplicationRecord
CommitCollection.new(source_project, commits_arr, source_branch)
end
+ def recent_commits
+ commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE)
+ end
+
def commits_count
if persisted?
merge_request_diff.commits_count
@@ -379,14 +401,17 @@ class MergeRequest < ApplicationRecord
end
end
- def commit_shas
- if persisted?
- merge_request_diff.commit_shas
- elsif compare_commits
- compare_commits.to_a.reverse.map(&:sha)
- else
- Array(diff_head_sha)
- end
+ def commit_shas(limit: nil)
+ return merge_request_diff.commit_shas(limit: limit) if persisted?
+
+ shas =
+ if compare_commits
+ compare_commits.to_a.reverse.map(&:sha)
+ else
+ Array(diff_head_sha)
+ end
+
+ limit ? shas.take(limit) : shas
end
# Returns true if there are commits that match at least one commit SHA.
@@ -417,9 +442,7 @@ class MergeRequest < ApplicationRecord
# Set off a rebase asynchronously, atomically updating the `rebase_jid` of
# the MR so that the status of the operation can be tracked.
def rebase_async(user_id)
- transaction do
- lock!
-
+ with_rebase_lock do
raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress?
# Although there is a race between setting rebase_jid here and clearing it
@@ -782,6 +805,8 @@ class MergeRequest < ApplicationRecord
end
def check_mergeability
+ return if Feature.enabled?(:merge_requests_conditional_mergeability_check, default_enabled: true) && !recheck_merge_status?
+
MergeRequests::MergeabilityCheckService.new(self).execute(retry_lease: false)
end
# rubocop: enable CodeReuse/ServiceClass
@@ -896,7 +921,7 @@ class MergeRequest < ApplicationRecord
def commit_notes
# Fetch comments only from last 100 commits
- commit_ids = commit_shas.take(100)
+ commit_ids = commit_shas(limit: 100)
Note
.user
@@ -907,7 +932,7 @@ class MergeRequest < ApplicationRecord
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
- !discussions_to_be_resolved?
+ unresolved_notes.none?(&:to_be_resolved?)
end
def for_fork?
@@ -1087,7 +1112,7 @@ class MergeRequest < ApplicationRecord
return true unless project.only_allow_merge_if_pipeline_succeeds?
return false unless actual_head_pipeline
- actual_head_pipeline.success? || actual_head_pipeline.skipped?
+ actual_head_pipeline.success?
end
def environments_for(current_user)
@@ -1263,6 +1288,27 @@ class MergeRequest < ApplicationRecord
compare_reports(Ci::CompareTestReportsService)
end
+ def has_exposed_artifacts?
+ return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
+
+ actual_head_pipeline&.has_exposed_artifacts?
+ end
+
+ # TODO: this method and compare_test_reports use the same
+ # result type, which is handled by the controller's #reports_response.
+ # we should minimize mistakes by isolating the common parts.
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
+ def find_exposed_artifacts
+ unless has_exposed_artifacts?
+ return { status: :error, status_reason: 'This merge request does not have exposed artifacts' }
+ end
+
+ compare_reports(Ci::GenerateExposedArtifactsReportService)
+ end
+
+ # TODO: consider renaming this as with exposed artifacts we generate reports,
+ # not always compare
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
def compare_reports(service_class, current_user = nil)
with_reactive_cache(service_class.name, current_user&.id) do |data|
unless service_class.new(project, current_user)
@@ -1277,6 +1323,8 @@ class MergeRequest < ApplicationRecord
def calculate_reactive_cache(identifier, current_user_id = nil, *args)
service_class = identifier.constantize
+ # TODO: the type check should change to something that includes exposed artifacts service
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
raise NameError, service_class unless service_class < Ci::CompareReportsBaseService
current_user = User.find_by(id: current_user_id)
@@ -1453,6 +1501,30 @@ class MergeRequest < ApplicationRecord
private
+ def with_rebase_lock
+ if Feature.enabled?(:merge_request_rebase_nowait_lock, default_enabled: true)
+ with_retried_nowait_lock { yield }
+ else
+ with_lock(true) { yield }
+ end
+ end
+
+ # If the merge request is idle in transaction or has a SELECT FOR
+ # UPDATE, we don't want to block indefinitely or this could cause a
+ # queue of SELECT FOR UPDATE calls. Instead, try to get the lock for
+ # 5 s before raising an error to the user.
+ def with_retried_nowait_lock
+ # Try at most 0.25 + (1.5 * .25) + (1.5^2 * .25) ... (1.5^5 * .25) = 5.2 s to get the lock
+ Retriable.retriable(on: ActiveRecord::LockWaitTimeout, tries: 6, base_interval: 0.25) do
+ with_lock('FOR UPDATE NOWAIT') do
+ yield
+ end
+ end
+ rescue ActiveRecord::LockWaitTimeout => e
+ Gitlab::Sentry.track_acceptable_exception(e)
+ raise RebaseLockTimeout, REBASE_LOCK_MESSAGE
+ end
+
def source_project_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless source_project
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 735ad046f22..70ce4df5678 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -213,12 +213,14 @@ class MergeRequestDiff < ApplicationRecord
end
end
- def commits
- @commits ||= load_commits
+ def commits(limit: nil)
+ strong_memoize(:"commits_#{limit || 'all'}") do
+ load_commits(limit: limit)
+ end
end
def last_commit_sha
- commit_shas.first
+ commit_shas(limit: 1).first
end
def first_commit
@@ -247,8 +249,8 @@ class MergeRequestDiff < ApplicationRecord
project.commit_by(oid: head_commit_sha)
end
- def commit_shas
- merge_request_diff_commits.map(&:sha)
+ def commit_shas(limit: nil)
+ merge_request_diff_commits.limit(limit).pluck(:sha)
end
def commits_by_shas(shas)
@@ -529,8 +531,9 @@ class MergeRequestDiff < ApplicationRecord
end
end
- def load_commits
- commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
+ def load_commits(limit: nil)
+ commits = merge_request_diff_commits.limit(limit)
+ .map { |commit| Commit.from_hash(commit.to_hash, project) }
CommitCollection
.new(merge_request.source_project, commits, merge_request.source_branch)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index a9f4cdec901..d0be54eed02 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -60,6 +60,7 @@ class Milestone < ApplicationRecord
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
+ validates :title, presence: true
validate :uniqueness_of_title, if: :title_changed?
validate :milestone_type_check
@@ -330,6 +331,6 @@ class Milestone < ApplicationRecord
end
def issues_finder_params
- { project_id: project_id }
+ { project_id: project_id, group_id: group_id }.compact
end
end
diff --git a/app/models/notification_reason.rb b/app/models/notification_reason.rb
index 6856d397413..a7967239417 100644
--- a/app/models/notification_reason.rb
+++ b/app/models/notification_reason.rb
@@ -6,12 +6,14 @@ class NotificationReason
OWN_ACTIVITY = 'own_activity'
ASSIGNED = 'assigned'
MENTIONED = 'mentioned'
+ SUBSCRIBED = 'subscribed'
# Priority list for selecting which reason to return in the notification
REASON_PRIORITY = [
OWN_ACTIVITY,
ASSIGNED,
- MENTIONED
+ MENTIONED,
+ SUBSCRIBED
].freeze
# returns the priority of a reason as an integer
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 7903a2182dd..3869d86b667 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -24,6 +24,8 @@ class PagesDomain < ApplicationRecord
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
+ default_value_for(:auto_ssl_enabled, allow_nil: false) { ::Gitlab::LetsEncrypt.enabled? }
+
attr_encrypted :key,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
diff --git a/app/models/project.rb b/app/models/project.rb
index 74da042d5a5..f4aa336fbcd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -76,6 +76,10 @@ class Project < ApplicationRecord
delegate :no_import?, to: :import_state, allow_nil: true
+ # TODO: remove once GitLab 12.5 is released
+ # https://gitlab.com/gitlab-org/gitlab/issues/34638
+ self.ignored_columns += %i[merge_requests_require_code_owner_approval]
+
default_value_for :archived, false
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
@@ -87,6 +91,8 @@ class Project < ApplicationRecord
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
+ default_value_for :remove_source_branch_after_merge, true
+ default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
@@ -281,6 +287,7 @@ class Project < ApplicationRecord
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
+ has_many :environments_for_dashboard, -> { from(with_rank.unfoldered.available, :environments).where('rank <= 3') }, class_name: 'Environment'
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
@@ -390,6 +397,7 @@ class Project < ApplicationRecord
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
+ scope :with_container_registry, -> { where(container_registry_enabled: true) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
# includes(:route) which we use in ProjectsFinder.
@@ -456,13 +464,6 @@ class Project < ApplicationRecord
# Used by Projects::CleanupService to hold a map of rewritten object IDs
mount_uploader :bfg_object_map, AttachmentUploader
- # Returns a project, if it is not about to be removed.
- #
- # id - The ID of the project to retrieve.
- def self.find_without_deleted(id)
- without_deleted.find_by_id(id)
- end
-
def self.eager_load_namespace_and_owner
includes(namespace: :owner)
end
@@ -656,6 +657,11 @@ class Project < ApplicationRecord
end
end
+ def preload_protected_branches
+ preloader = ActiveRecord::Associations::Preloader.new
+ preloader.preload(self, protected_branches: [:push_access_levels, :merge_access_levels])
+ end
+
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
@@ -1906,7 +1912,7 @@ class Project < ApplicationRecord
end
def default_environment
- production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
+ production_first = Arel.sql("(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC")
environments
.with_state(:available)
@@ -1961,27 +1967,6 @@ class Project < ApplicationRecord
(auto_devops || build_auto_devops)&.predefined_variables
end
- def append_or_update_attribute(name, value)
- if Project.reflect_on_association(name).try(:macro) == :has_many
- # if this is 1-to-N relation, update the parent object
- value.each do |item|
- item.update!(
- Project.reflect_on_association(name).foreign_key => id)
- end
-
- # force to drop relation cache
- public_send(name).reset # rubocop:disable GitlabSecurity/PublicSend
-
- # succeeded
- true
- else
- # if this is another relation or attribute, update just object
- update_attribute(name, value)
- end
- rescue ActiveRecord::RecordInvalid => e
- raise e, "Failed to set #{name}: #{e.message}"
- end
-
# Tries to set repository as read_only, checking for existing Git transfers in progress beforehand
#
# @return [Boolean] true when set to read_only or false when an existing git transfer is in progress
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index a495d34c07c..d089a004d3d 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class ProjectCiCdSetting < ApplicationRecord
+ # TODO: remove once GitLab 12.7 is released
+ # https://gitlab.com/gitlab-org/gitlab/issues/36651
+ self.ignored_columns += %i[merge_trains_enabled]
belongs_to :project, inverse_of: :ci_cd_settings
# The version of the schema that first introduced this model/table.
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index a3793d9937b..46fe894cfc3 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -75,11 +75,11 @@ module ChatMessage
def activity
{
- title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status}") %
+ title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
{
pipeline_link: pipeline_link,
ref_type: ref_type,
- branch_link: branch_link,
+ ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status
},
@@ -123,7 +123,7 @@ module ChatMessage
fields = [
{
title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
- value: Slack::Notifier::LinkFormatter.format(ref_name_link),
+ value: Slack::Notifier::LinkFormatter.format(ref_link),
short: true
},
{
@@ -141,12 +141,12 @@ module ChatMessage
end
def message
- s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
+ s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
{
project_link: project_link,
pipeline_link: pipeline_link,
ref_type: ref_type,
- branch_link: branch_link,
+ ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status,
duration: pretty_duration(duration)
@@ -193,12 +193,16 @@ module ChatMessage
end
end
- def branch_url
- "#{project_url}/commits/#{ref}"
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/commits/#{ref}"
+ end
end
- def branch_link
- "[#{ref}](#{branch_url})"
+ def ref_link
+ "[#{ref}](#{ref_url})"
end
def project_url
@@ -266,14 +270,6 @@ module ChatMessage
"[#{commit.title}](#{commit_url})"
end
- def commits_page_url
- "#{project_url}/commits/#{ref}"
- end
-
- def ref_name_link
- "[#{ref}](#{commits_page_url})"
- end
-
def author_url
return unless user && committer
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index 8163fca33a2..07622f570c2 100644
--- a/app/models/project_services/chat_message/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -82,16 +82,20 @@ module ChatMessage
Gitlab::Git.blank_ref?(after)
end
- def branch_url
- "#{project_url}/commits/#{ref}"
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/commits/#{ref}"
+ end
end
def compare_url
"#{project_url}/compare/#{before}...#{after}"
end
- def branch_link
- "[#{ref}](#{branch_url})"
+ def ref_link
+ "[#{ref}](#{ref_url})"
end
def project_link
@@ -104,11 +108,11 @@ module ChatMessage
def compose_action_details
if new_branch?
- ['pushed new', branch_link, "to #{project_link}"]
+ ['pushed new', ref_link, "to #{project_link}"]
elsif removed_branch?
['removed', ref, "from #{project_link}"]
else
- ['pushed to', branch_link, "of #{project_link} (#{compare_link})"]
+ ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
end
end
diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb
index cffb493d569..cf406a784ce 100644
--- a/app/models/project_services/data_fields.rb
+++ b/app/models/project_services/data_fields.rb
@@ -50,7 +50,7 @@ module DataFields
end
def data_fields_present?
- data_fields.persisted?
+ data_fields.present?
rescue NotImplementedError
false
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 6eff2ea2e3a..a0273fe0e5a 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -7,8 +7,15 @@ class PrometheusService < MonitoringService
prop_accessor :api_url
boolean_accessor :manual_configuration
+ # We need to allow the self-monitoring project to connect to the internal
+ # Prometheus instance.
+ # Since the internal Prometheus instance is usually a localhost URL, we need
+ # to allow localhost URLs when the following conditions are true:
+ # 1. project is the self-monitoring project.
+ # 2. api_url is the internal Prometheus URL.
with_options presence: true, if: :manual_configuration? do
- validates :api_url, public_url: true
+ validates :api_url, public_url: true, unless: proc { |object| object.allow_local_api_url? }
+ validates :api_url, url: true, if: proc { |object| object.allow_local_api_url? }
end
before_save :synchronize_service_state
@@ -82,12 +89,28 @@ class PrometheusService < MonitoringService
project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
end
+ def allow_local_api_url?
+ self_monitoring_project? && internal_prometheus_url?
+ end
+
private
+ def self_monitoring_project?
+ project && project.id == current_settings.instance_administration_project_id
+ end
+
+ def internal_prometheus_url?
+ api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri
+ end
+
def should_return_client?
api_url.present? && manual_configuration? && active? && valid?
end
+ def current_settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
def synchronize_service_state
self.active = prometheus_available? || manual_configuration?
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index b3585c4cf4c..e732c1bd86f 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -2,13 +2,6 @@
class ProjectSnippet < Snippet
belongs_to :project
- belongs_to :author, class_name: "User"
validates :project, presence: true
-
- # Scopes
- scope :fresh, -> { order("created_at DESC") }
-
- participant :author
- participant :notes_with_associations
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index bb222ac7629..f02ccd9e55e 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -160,12 +160,6 @@ class ProjectWiki
update_project_activity
end
- def page_formatted_data(page)
- page_title, page_dir = page_title_and_dir(page.title)
-
- wiki.page_formatted_data(title: page_title, dir: page_dir, version: page.version)
- end
-
def page_title_and_dir(title)
return unless title
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index 08f4df7ea01..d0dc31476ff 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -14,7 +14,13 @@ class PrometheusMetric < ApplicationRecord
validates :project, presence: true, unless: :common?
validates :project, absence: true, if: :common?
+ scope :for_project, -> (project) { where(project: project) }
+ scope :for_group, -> (group) { where(group: group) }
+ scope :for_title, -> (title) { where(title: title) }
+ scope :for_y_label, -> (y_label) { where(y_label: y_label) }
+ scope :for_identifier, -> (identifier) { where(identifier: identifier) }
scope :common, -> { where(common: true) }
+ scope :ordered, -> { reorder(created_at: :asc) }
def priority
group_details(group).fetch(:priority)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 1857a59e01c..735e2bdea81 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -38,7 +38,7 @@ class ProtectedBranch < ApplicationRecord
end
def self.protected_refs(project)
- project.protected_branches.select(:name)
+ project.protected_branches
end
def self.branch_requires_code_owner_approval?(project, branch_name)
diff --git a/app/models/release.rb b/app/models/release.rb
index 5a7bfe2d495..401e8359f47 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Release < ApplicationRecord
+ include Presentable
include CacheMarkdownField
include Gitlab::Utils::StrongMemoize
@@ -26,13 +27,21 @@ class Release < ApplicationRecord
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
scope :sorted, -> { order(released_at: :desc) }
+ scope :preloaded, -> { includes(project: :namespace) }
scope :with_project_and_namespace, -> { includes(project: :namespace) }
+ scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
delegate :repository, to: :project
after_commit :create_evidence!, on: :create
after_commit :notify_new_release, on: :create
+ MAX_NUMBER_TO_DISPLAY = 3
+
+ def to_param
+ CGI.escape(tag)
+ end
+
def commit
strong_memoize(:commit) do
repository.commit(actual_sha)
@@ -60,6 +69,10 @@ class Release < ApplicationRecord
released_at.present? && released_at > Time.zone.now
end
+ def name
+ self.read_attribute(:name) || tag
+ end
+
private
def actual_sha
diff --git a/app/models/releases/source.rb b/app/models/releases/source.rb
index 4d3d54457af..2f00d25d768 100644
--- a/app/models/releases/source.rb
+++ b/app/models/releases/source.rb
@@ -6,11 +6,9 @@ module Releases
attr_accessor :project, :tag_name, :format
- FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
-
class << self
def all(project, tag_name)
- Releases::Source::FORMATS.map do |format|
+ Gitlab::Workhorse::ARCHIVE_FORMATS.map do |format|
Releases::Source.new(project: project,
tag_name: tag_name,
format: format)
diff --git a/app/models/service.rb b/app/models/service.rb
index 305cf7b78a2..6d5b974dd31 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -40,6 +40,7 @@ class Service < ApplicationRecord
scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) }
+ scope :by_type, -> (type) { where(type: type) }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 1927b54510e..f217c942e8e 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -55,7 +55,8 @@ class Todo < ApplicationRecord
scope :done, -> { with_state(:done) }
scope :for_action, -> (action) { where(action: action) }
scope :for_author, -> (author) { where(author: author) }
- scope :for_project, -> (project) { where(project: project) }
+ scope :for_project, -> (projects) { where(project: projects) }
+ scope :for_undeleted_projects, -> { joins(:project).merge(Project.without_deleted) }
scope :for_group, -> (group) { where(group: group) }
scope :for_type, -> (type) { where(target_type: type) }
scope :for_target, -> (id) { where(target_id: id) }
@@ -160,6 +161,10 @@ class Todo < ApplicationRecord
action == ASSIGNED
end
+ def done?
+ state == 'done'
+ end
+
def action_name
ACTION_NAMES[action]
end
diff --git a/app/models/user.rb b/app/models/user.rb
index eec8ad6edbb..d0e758b0055 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,9 +56,6 @@ class User < ApplicationRecord
BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
"administrator if you think this is an error."
- # Removed in GitLab 12.3. Keep until after 2019-09-22.
- self.ignored_columns += %i[support_bot]
-
MINIMUM_INACTIVE_DAYS = 180
# Override Devise::Models::Trackable#update_tracked_fields!
@@ -243,6 +240,8 @@ class User < ApplicationRecord
delegate :time_display_relative, :time_display_relative=, to: :user_preference
delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference
delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference
+ delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference
+ delegate :setup_for_company, :setup_for_company=, to: :user_preference
accepts_nested_attributes_for :user_preference, update_only: true
@@ -1423,14 +1422,13 @@ class User < ApplicationRecord
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
- # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
+ # <https://github.com/plataformatec/devise/blob/v4.7.1/lib/devise/models/lockable.rb#L104>
#
# rubocop: disable CodeReuse/ServiceClass
def increment_failed_attempts!
return if ::Gitlab::Database.read_only?
- self.failed_attempts ||= 0
- self.failed_attempts += 1
+ increment_failed_attempts
if attempts_exceeded?
lock_access! unless access_locked?
@@ -1458,7 +1456,7 @@ class User < ApplicationRecord
# Does the user have access to all private groups & projects?
# Overridden in EE to also check auditor?
def full_private_access?
- admin?
+ can?(:read_all_resources)
end
def update_two_factor_requirement
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 68241d2bd95..f9c562364cb 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -112,11 +112,6 @@ class WikiPage
wiki.page_title_and_dir(slug)&.last.to_s
end
- # The processed/formatted content of this page.
- def formatted_content
- @attributes[:formatted_content] ||= @wiki.page_formatted_data(@page)
- end
-
# The markup format for the page.
def format
@attributes[:format] || :markdown
diff --git a/app/models/zoom_meeting.rb b/app/models/zoom_meeting.rb
new file mode 100644
index 00000000000..a7ecd1e6a2c
--- /dev/null
+++ b/app/models/zoom_meeting.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class ZoomMeeting < ApplicationRecord
+ belongs_to :project, optional: false
+ belongs_to :issue, optional: false
+
+ validates :url, presence: true, length: { maximum: 255 }, zoom_url: true
+ validates :issue, same_project_association: true
+
+ enum issue_status: {
+ added: 1,
+ removed: 2
+ }
+
+ scope :added_to_issue, -> { where(issue_status: :added) }
+ scope :removed_from_issue, -> { where(issue_status: :removed) }
+ scope :canonical, -> (issue) { where(issue: issue).added_to_issue }
+
+ def self.canonical_meeting(issue)
+ canonical(issue)&.take
+ end
+
+ def self.canonical_meeting_url(issue)
+ canonical_meeting(issue)&.url
+ end
+end