diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-19 22:11:55 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-19 22:11:55 +0000 |
commit | 5a8431feceba47fd8e1804d9aa1b1730606b71d5 (patch) | |
tree | e5df8e0ceee60f4af8093f5c4c2f934b8abced05 /app/models | |
parent | 4d477238500c347c6553d335d920bedfc5a46869 (diff) | |
download | gitlab-ce-5a8431feceba47fd8e1804d9aa1b1730606b71d5.tar.gz |
Add latest changes from gitlab-org/gitlab@12-5-stable-ee
Diffstat (limited to 'app/models')
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 |