diff options
Diffstat (limited to 'app/models')
159 files changed, 1958 insertions, 1130 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb index 065bd5507be..a23190cc8b3 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -36,7 +36,7 @@ class ActiveSession timestamp = Time.current active_user_session = new( - ip_address: request.ip, + ip_address: request.remote_ip, browser: client.name, os: client.os_name, device_name: client.device_name, diff --git a/app/models/alert_management.rb b/app/models/alert_management.rb new file mode 100644 index 00000000000..0346b1f155f --- /dev/null +++ b/app/models/alert_management.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module AlertManagement + def self.table_name_prefix + 'alert_management_' + end +end diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index acaf474ecc2..af60ddd6f9a 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true +require_dependency 'alert_management' + module AlertManagement class Alert < ApplicationRecord + include IidRoutes include AtomicInternalId include ShaAttribute include Sortable + include Noteable include Gitlab::SQL::Pattern STATUSES = { @@ -23,9 +27,15 @@ module AlertManagement belongs_to :project belongs_to :issue, optional: true - has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) } - self.table_name = 'alert_management_alerts' + has_many :alert_assignees, inverse_of: :alert + has_many :assignees, through: :alert_assignees + + has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :ordered_notes, -> { fresh }, as: :noteable, class_name: 'Note' + has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id + + has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) } sha_attribute :fingerprint @@ -102,7 +112,7 @@ module AlertManagement scope :order_start_time, -> (sort_order) { order(started_at: sort_order) } scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) } - scope :order_events_count, -> (sort_order) { order(events: sort_order) } + scope :order_event_count, -> (sort_order) { order(events: sort_order) } scope :order_severity, -> (sort_order) { order(severity: sort_order) } scope :order_status, -> (sort_order) { order(status: sort_order) } @@ -110,12 +120,12 @@ module AlertManagement def self.sort_by_attribute(method) case method.to_s - when 'start_time_asc' then order_start_time(:asc) - when 'start_time_desc' then order_start_time(:desc) - when 'end_time_asc' then order_end_time(:asc) - when 'end_time_desc' then order_end_time(:desc) - when 'events_count_asc' then order_events_count(:asc) - when 'events_count_desc' then order_events_count(:desc) + when 'started_at_asc' then order_start_time(:asc) + when 'started_at_desc' then order_start_time(:desc) + when 'ended_at_asc' then order_end_time(:asc) + when 'ended_at_desc' then order_end_time(:desc) + when 'event_count_asc' then order_event_count(:asc) + when 'event_count_desc' then order_event_count(:desc) when 'severity_asc' then order_severity(:asc) when 'severity_desc' then order_severity(:desc) when 'status_asc' then order_status(:asc) @@ -135,8 +145,28 @@ module AlertManagement monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus] end + def register_new_event! + increment!(:events) + end + + # required for todos (typically contains an identifier like issue iid) + # no-op; we could use iid, but we don't have a reference prefix + def to_reference(_from = nil, full: false) + '' + end + + def execute_services + return unless project.has_active_services?(:alert_hooks) + + project.execute_services(hook_data, :alert_hooks) + end + private + def hook_data + Gitlab::DataBuilder::Alert.build(self) + end + def hosts_length return unless hosts diff --git a/app/models/alert_management/alert_assignee.rb b/app/models/alert_management/alert_assignee.rb new file mode 100644 index 00000000000..c74b2699182 --- /dev/null +++ b/app/models/alert_management/alert_assignee.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module AlertManagement + class AlertAssignee < ApplicationRecord + belongs_to :alert, inverse_of: :alert_assignees + belongs_to :assignee, class_name: 'User', foreign_key: :user_id + + validates :alert, presence: true + validates :assignee, presence: true, uniqueness: { scope: :alert_id } + end +end diff --git a/app/models/alert_management/alert_user_mention.rb b/app/models/alert_management/alert_user_mention.rb new file mode 100644 index 00000000000..d36aa80ee05 --- /dev/null +++ b/app/models/alert_management/alert_user_mention.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module AlertManagement + class AlertUserMention < UserMention + belongs_to :alert_management_alert, class_name: '::AlertManagement::Alert' + belongs_to :note + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 0979d03f6e6..c7e4d64d3d5 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -5,6 +5,10 @@ class ApplicationRecord < ActiveRecord::Base alias_method :reset, :reload + def self.without_order + reorder(nil) + end + def self.id_in(ids) where(id: ids) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b29d6731b08..425a0e05c7d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -19,6 +19,12 @@ class ApplicationSetting < ApplicationRecord belongs_to :instance_administrators_group, class_name: "Group" + def self.repository_storages_weighted_attributes + @repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze + end + + store_accessor :repository_storages_weighted, *Gitlab.config.repositories.storages.keys, prefix: true + # Include here so it can override methods from # `add_authentication_token_field` # We don't prepend for now because otherwise we'll need to @@ -39,6 +45,7 @@ class ApplicationSetting < ApplicationRecord cache_markdown_field :after_sign_up_text default_value_for :id, 1 + default_value_for :repository_storages_weighted, {} chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds @@ -136,6 +143,10 @@ class ApplicationSetting < ApplicationRecord presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :max_import_size, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :max_pages_size, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, @@ -152,6 +163,7 @@ class ApplicationSetting < ApplicationRecord validates :repository_storages, presence: true validate :check_repository_storages + validate :check_repository_storages_weighted validates :auto_devops_domain, allow_blank: true, @@ -271,6 +283,10 @@ class ApplicationSetting < ApplicationRecord validates :allowed_key_types, presence: true + repository_storages_weighted_attributes.each do |attribute| + validates attribute, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100 } + end + validates_each :restricted_visibility_levels do |record, attr, value| value&.each do |level| unless Gitlab::VisibilityLevel.options.value?(level) @@ -301,6 +317,13 @@ class ApplicationSetting < ApplicationRecord numericality: { greater_than: 0, less_than_or_equal_to: 10 }, if: :external_authorization_service_enabled + validates :spam_check_endpoint_url, + addressable_url: true, allow_blank: true + + validates :spam_check_endpoint_url, + presence: true, + if: :spam_check_endpoint_enabled + validates :external_auth_client_key, presence: true, if: -> (setting) { setting.external_auth_client_cert.present? } @@ -427,6 +450,12 @@ class ApplicationSetting < ApplicationRecord recaptcha_enabled || login_recaptcha_protection_enabled end + repository_storages_weighted_attributes.each do |attribute| + define_method :"#{attribute}=" do |value| + super(value.to_i) + end + end + private def parsed_grafana_url diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 221e4d5e0c6..d24136cc04a 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -86,6 +86,7 @@ module ApplicationSettingImplementation local_markdown_version: 0, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], + max_import_size: 50, mirror_available: true, outbound_local_requests_whitelist: [], password_authentication_enabled_for_git: true, @@ -104,6 +105,7 @@ module ApplicationSettingImplementation login_recaptcha_protection_enabled: false, repository_checks_enabled: true, repository_storages: ['default'], + repository_storages_weighted: { default: 100 }, require_two_factor_authentication: false, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], session_expire_delay: Settings.gitlab['session_expire_delay'], @@ -115,6 +117,8 @@ module ApplicationSettingImplementation sourcegraph_enabled: false, sourcegraph_url: nil, sourcegraph_public_only: true, + spam_check_endpoint_enabled: false, + spam_check_endpoint_url: nil, minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, namespace_storage_size_limit: 0, terminal_max_session_time: 0, @@ -151,7 +155,7 @@ module ApplicationSettingImplementation snowplow_app_id: nil, snowplow_iglu_registry_url: nil, custom_http_clone_url_root: nil, - productivity_analytics_start_date: Time.now, + productivity_analytics_start_date: Time.current, snippet_size_limit: 50.megabytes } end @@ -260,6 +264,10 @@ module ApplicationSettingImplementation Array(read_attribute(:repository_storages)) end + def repository_storages_weighted + read_attribute(:repository_storages_weighted) + end + def commit_email_hostname super.presence || self.class.default_commit_email_hostname end @@ -289,10 +297,21 @@ module ApplicationSettingImplementation performance_bar_allowed_group_id.present? end - # Choose one of the available repository storage options. Currently all have - # equal weighting. + def normalized_repository_storage_weights + strong_memoize(:normalized_repository_storage_weights) do + weights_total = repository_storages_weighted.values.reduce(:+) + + repository_storages_weighted.transform_values do |w| + next w if weights_total == 0 + + w.to_f / weights_total + end + end + end + + # Choose one of the available repository storage options based on a normalized weighted probability. def pick_repository_storage - repository_storages.sample + normalized_repository_storage_weights.max_by { |_, weight| rand**(1.0 / weight) }.first end def runners_registration_token @@ -420,6 +439,12 @@ module ApplicationSettingImplementation invalid.empty? end + def check_repository_storages_weighted + invalid = repository_storages_weighted.keys - Gitlab.config.repositories.storages.keys + errors.add(:repository_storages_weighted, "can't include: %{invalid_storages}" % { invalid_storages: invalid.join(", ") }) unless + invalid.empty? + end + def terms_exist return unless enforce_terms? diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 7ff0076c3e3..3bbd2e43a51 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -2,6 +2,9 @@ class AuditEvent < ApplicationRecord include CreatedAtFilterable + include IgnorableColumns + + ignore_column :updated_at, remove_with: '13.3', remove_after: '2020-08-22' serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize diff --git a/app/models/badge.rb b/app/models/badge.rb index 3400d6d407d..4339d419b48 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -18,7 +18,7 @@ class Badge < ApplicationRecord # This regex will build the new PLACEHOLDER_REGEX with the new information PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze - default_scope { order_created_at_asc } + default_scope { order_created_at_asc } # rubocop:disable Cop/DefaultScope scope :order_created_at_asc, -> { reorder(created_at: :asc) } diff --git a/app/models/blob.rb b/app/models/blob.rb index c8df6c7732a..874bf58530e 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -50,6 +50,7 @@ class Blob < SimpleDelegator BlobViewer::License, BlobViewer::Contributing, BlobViewer::Changelog, + BlobViewer::MetricsDashboardYml, BlobViewer::CargoToml, BlobViewer::Cartfile, @@ -57,6 +58,7 @@ class Blob < SimpleDelegator BlobViewer::Gemfile, BlobViewer::Gemspec, BlobViewer::GodepsJson, + BlobViewer::GoMod, BlobViewer::PackageJson, BlobViewer::Podfile, BlobViewer::Podspec, diff --git a/app/models/blob_viewer/go_mod.rb b/app/models/blob_viewer/go_mod.rb new file mode 100644 index 00000000000..ae57e2c0526 --- /dev/null +++ b/app/models/blob_viewer/go_mod.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module BlobViewer + class GoMod < DependencyManager + include ServerSide + include Gitlab::Utils::StrongMemoize + + MODULE_REGEX = / + \A (?# beginning of file) + module\s+ (?# module directive) + (?<name>.*?) (?# module name) + \s*(?:\/\/.*)? (?# comment) + (?:\n|\z) (?# newline or end of file) + /x.freeze + + self.file_types = %i(go_mod go_sum) + + def manager_name + 'Go Modules' + end + + def manager_url + 'https://golang.org/ref/mod' + end + + def package_type + 'go' + end + + def package_name + strong_memoize(:package_name) do + next if blob.name != 'go.mod' + next unless match = MODULE_REGEX.match(blob.data) + + match[:name] + end + end + + def package_url + Gitlab::Golang.package_url(package_name) + end + end +end diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb new file mode 100644 index 00000000000..c05fb5d88d6 --- /dev/null +++ b/app/models/blob_viewer/metrics_dashboard_yml.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module BlobViewer + class MetricsDashboardYml < Base + include ServerSide + include Gitlab::Utils::StrongMemoize + include Auxiliary + + self.partial_name = 'metrics_dashboard_yml' + self.loading_partial_name = 'metrics_dashboard_yml_loading' + self.file_types = %i(metrics_dashboard) + self.binary = false + + def valid? + errors.blank? + end + + def errors + strong_memoize(:errors) do + prepare! + parse_blob_data + end + end + + private + + def parse_blob_data + yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw! + + ::PerformanceMonitoring::PrometheusDashboard.from_json(yaml) + nil + rescue Gitlab::Config::Loader::FormatError => error + wrap_yml_syntax_error(error) + rescue ActiveModel::ValidationError => invalid + invalid.model.errors + end + + def wrap_yml_syntax_error(error) + ::PerformanceMonitoring::PrometheusDashboard.new.errors.tap do |errors| + errors.add(:'YAML syntax', error.message) + end + end + end +end diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb index 2f1cd830791..979f0e1ab92 100644 --- a/app/models/board_group_recent_visit.rb +++ b/app/models/board_group_recent_visit.rb @@ -14,7 +14,7 @@ class BoardGroupRecentVisit < ApplicationRecord def self.visited!(user, board) visit = find_or_create_by(user: user, group: board.group, board: board) - visit.touch if visit.updated_at < Time.now + visit.touch if visit.updated_at < Time.current rescue ActiveRecord::RecordNotUnique retry end diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb index 236d88e909c..509c8f97b83 100644 --- a/app/models/board_project_recent_visit.rb +++ b/app/models/board_project_recent_visit.rb @@ -14,7 +14,7 @@ class BoardProjectRecentVisit < ApplicationRecord def self.visited!(user, board) visit = find_or_create_by(user: user, project: board.project, board: board) - visit.touch if visit.updated_at < Time.now + visit.touch if visit.updated_at < Time.current rescue ActiveRecord::RecordNotUnique retry end diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb index 28aab279545..6e39d7e2204 100644 --- a/app/models/chat_team.rb +++ b/app/models/chat_team.rb @@ -12,6 +12,6 @@ class ChatTeam < ApplicationRecord # Either the group is not found, or the user doesn't have the proper # access on the mattermost instance. In the first case, we're done either way # in the latter case, we can't recover by retrying, so we just log what happened - Rails.logger.error("Mattermost team deletion failed: #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Mattermost team deletion failed: #{e}") end end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 1e92a47ab49..58c26e8c806 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -16,6 +16,9 @@ module Ci has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id + has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id + has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline + validates :ref, presence: true # rubocop:disable Cop/ActiveRecordSerialize diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7f64ea7dd97..b5e68b55f72 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -26,7 +26,8 @@ module Ci RUNNER_FEATURES = { upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? }, refspecs: -> (build) { build.merge_request_ref? }, - artifacts_exclude: -> (build) { build.supports_artifacts_exclude? } + artifacts_exclude: -> (build) { build.supports_artifacts_exclude? }, + release_steps: -> (build) { build.release_steps? } }.freeze DEFAULT_RETRIES = { @@ -39,6 +40,7 @@ module Ci has_one :resource, class_name: 'Ci::Resource', inverse_of: :build has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id + has_many :report_results, class_name: 'Ci::BuildReportResult', 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 @@ -55,6 +57,7 @@ module Ci delegate :url, to: :runner_session, prefix: true, allow_nil: true delegate :terminal_specification, to: :runner_session, allow_nil: true + delegate :service_specification, to: :runner_session, allow_nil: true delegate :gitlab_deploy_token, to: :project delegate :trigger_short_token, to: :trigger_request, allow_nil: true @@ -137,8 +140,8 @@ module Ci .includes(:metadata, :job_artifacts_metadata) end - scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } - scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.now) } + scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) } + scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) } scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) } @@ -259,7 +262,7 @@ module Ci end before_transition any => :waiting_for_resource do |build| - build.waiting_for_resource_at = Time.now + build.waiting_for_resource_at = Time.current end before_transition on: :enqueue_waiting_for_resource do |build| @@ -352,7 +355,7 @@ module Ci begin Ci::Build.retry(build, build.user) rescue Gitlab::Access::AccessDeniedError => ex - Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "Unable to auto-retry job #{build.id}: #{ex}" end end end @@ -576,7 +579,7 @@ module Ci def environment_changed_page_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - break variables unless environment_status + break variables unless environment_status && Feature.enabled?(:modifed_path_ci_variables, project) variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', value: environment_status.changed_paths.join(',')) variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', value: environment_status.changed_urls.join(',')) @@ -686,6 +689,10 @@ module Ci job_artifacts.any? end + def has_test_reports? + job_artifacts.test_reports.exists? + end + def has_old_trace? old_trace.present? end @@ -713,7 +720,7 @@ module Ci end def needs_touch? - Time.now - updated_at > 15.minutes.to_i + Time.current - updated_at > 15.minutes.to_i end def valid_token?(token) @@ -756,13 +763,13 @@ module Ci # and use that for `ExpireBuildInstanceArtifactsWorker`? def erase_erasable_artifacts! - job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll + job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll end def erase(opts = {}) return false unless erasable? - job_artifacts.destroy_all # rubocop: disable DestroyAll + job_artifacts.destroy_all # rubocop: disable Cop/DestroyAll erase_trace! update_erased!(opts[:erased_by]) end @@ -776,11 +783,11 @@ module Ci end def artifacts_expired? - artifacts_expire_at && artifacts_expire_at < Time.now + artifacts_expire_at && artifacts_expire_at < Time.current end def artifacts_expire_in - artifacts_expire_at - Time.now if artifacts_expire_at + artifacts_expire_at - Time.current if artifacts_expire_at end def artifacts_expire_in=(value) @@ -809,6 +816,7 @@ module Ci def steps [Gitlab::Ci::Build::Step.from_commands(self), + Gitlab::Ci::Build::Step.from_release(self), Gitlab::Ci::Build::Step.from_after_script(self)].compact end @@ -872,6 +880,16 @@ module Ci options&.dig(:artifacts, :reports)&.any? end + def supports_artifacts_exclude? + options&.dig(:artifacts, :exclude)&.any? && + Gitlab::Ci::Features.artifacts_exclude_enabled? + end + + def release_steps? + options.dig(:release)&.any? && + Gitlab::Ci::Features.release_generation_enabled? + end + def hide_secrets(trace) return unless trace @@ -945,11 +963,6 @@ module Ci failure_reason: :data_integrity_failure) end - def supports_artifacts_exclude? - options&.dig(:artifacts, :exclude)&.any? && - Gitlab::Ci::Features.artifacts_exclude_enabled? - end - def degradation_threshold var = yaml_variables.find { |v| v[:key] == DEGRADATION_THRESHOLD_VARIABLE_NAME } var[:value]&.to_i if var @@ -993,7 +1006,7 @@ module Ci end def update_erased!(user = nil) - self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) + self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil) end def unscoped_project @@ -1026,7 +1039,7 @@ module Ci end def has_expiring_artifacts? - artifacts_expire_at.present? && artifacts_expire_at > Time.now + artifacts_expire_at.present? && artifacts_expire_at > Time.current end def job_jwt_variables diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb index d3ff870e36a..2fcd1708cf4 100644 --- a/app/models/ci/build_dependencies.rb +++ b/app/models/ci/build_dependencies.rb @@ -45,7 +45,7 @@ module Ci end def valid_local? - return true if Feature.enabled?('ci_disable_validates_dependencies') + return true if Feature.enabled?(:ci_disable_validates_dependencies) local.all?(&:valid_dependency?) end diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb new file mode 100644 index 00000000000..530233ad5c0 --- /dev/null +++ b/app/models/ci/build_report_result.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Ci + class BuildReportResult < ApplicationRecord + extend Gitlab::Ci::Model + + self.primary_key = :build_id + + belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results + belongs_to :project, class_name: "Project", inverse_of: :build_report_results + + validates :build, :project, presence: true + validates :data, json_schema: { filename: "build_report_result_data" } + + store_accessor :data, :tests + + def tests_name + tests.dig("name") + end + + def tests_duration + tests.dig("duration") + end + + def tests_success + tests.dig("success").to_i + end + + def tests_failed + tests.dig("failed").to_i + end + + def tests_errored + tests.dig("errored").to_i + end + + def tests_skipped + tests.dig("skipped").to_i + end + + def tests_total + [tests_success, tests_failed, tests_errored, tests_skipped].sum + end + end +end diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index b46bbe69c7c..bc7f17f046c 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -7,6 +7,8 @@ module Ci extend Gitlab::Ci::Model TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' + DEFAULT_SERVICE_NAME = 'build'.freeze + DEFAULT_PORT_NAME = 'default_port'.freeze self.table_name = 'ci_builds_runner_session' @@ -23,6 +25,17 @@ module Ci channel_specification(wss_url, TERMINAL_SUBPROTOCOL) end + def service_specification(service: nil, path: nil, port: nil, subprotocols: nil) + return {} unless url.present? + + port = port.presence || DEFAULT_PORT_NAME + service = service.presence || DEFAULT_SERVICE_NAME + url = "#{self.url}/proxy/#{service}/#{port}/#{path}" + subprotocols = subprotocols.presence || ::Ci::BuildRunnerSession::TERMINAL_SUBPROTOCOL + + channel_specification(url, subprotocols) + end + private def channel_specification(url, subprotocol) @@ -37,5 +50,3 @@ module Ci end end end - -Ci::BuildRunnerSession.prepend_if_ee('EE::Ci::BuildRunnerSession') diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb index 3506b27e974..d6617b8c2eb 100644 --- a/app/models/ci/daily_build_group_report_result.rb +++ b/app/models/ci/daily_build_group_report_result.rb @@ -9,6 +9,8 @@ module Ci belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id belongs_to :project + validates :data, json_schema: { filename: "daily_build_group_report_result_data" } + def self.upsert_reports(data) upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any? end diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb index bf03b92259a..d215372bb45 100644 --- a/app/models/ci/freeze_period.rb +++ b/app/models/ci/freeze_period.rb @@ -5,7 +5,7 @@ module Ci include StripAttribute self.table_name = 'ci_freeze_periods' - default_scope { order(created_at: :asc) } + default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope belongs_to :project, inverse_of: :freeze_periods diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 4b2081f2977..779c6c0396f 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -24,7 +24,7 @@ module Ci def status strong_memoize(:status) do - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) Gitlab::Ci::Status::Composite .new(@jobs) .status diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb index c674f76d229..8245729a884 100644 --- a/app/models/ci/instance_variable.rb +++ b/app/models/ci/instance_variable.rb @@ -3,8 +3,13 @@ module Ci class InstanceVariable < ApplicationRecord extend Gitlab::Ci::Model + extend Gitlab::ProcessMemoryCache::Helper include Ci::NewHasVariable include Ci::Maskable + include Limitable + + self.limit_name = 'ci_instance_level_variables' + self.limit_scope = Limitable::GLOBAL_SCOPE alias_attribute :secret_value, :value @@ -12,8 +17,14 @@ module Ci message: "(%{value}) has already been taken" } + validates :encrypted_value, length: { + maximum: 1024, + too_long: 'The encrypted value of the provided variable exceeds %{count} bytes. Variables over 700 characters risk exceeding the limit.' + } + scope :unprotected, -> { where(protected: false) } - after_commit { self.class.touch_redis_cache_timestamp } + + after_commit { self.class.invalidate_memory_cache(:ci_instance_variable_data) } class << self def all_cached @@ -24,10 +35,6 @@ module Ci cached_data[:unprotected] end - def touch_redis_cache_timestamp(time = Time.current.to_f) - shared_backend.write(:ci_instance_variable_changed_at, time) - end - private def cached_data @@ -37,39 +44,13 @@ module Ci { all: all_records, unprotected: all_records.reject(&:protected?) } end end + end - def fetch_memory_cache(key, &payload) - cache = process_backend.read(key) - - if cache && !stale_cache?(cache) - cache[:data] - else - store_cache(key, &payload) - end - end - - def stale_cache?(cache_info) - shared_timestamp = shared_backend.read(:ci_instance_variable_changed_at) - return true unless shared_timestamp - - shared_timestamp.to_f > cache_info[:cached_at].to_f - end - - def store_cache(key) - data = yield - time = Time.current.to_f - - process_backend.write(key, data: data, cached_at: time) - touch_redis_cache_timestamp(time) - data - end - - def shared_backend - Rails.cache - end + private - def process_backend - Gitlab::ProcessMemoryCache.cache_backend + def validate_plan_limit_not_exceeded + if Gitlab::Ci::Features.instance_level_variables_limit_enabled? + super end end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index d931428dccd..8aba9356949 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -5,6 +5,7 @@ module Ci include AfterCommitQueue include ObjectStorage::BackgroundMove include UpdateProjectStatistics + include UsageStatistics include Sortable extend Gitlab::Ci::Model @@ -26,6 +27,7 @@ module Ci accessibility: 'gl-accessibility.json', codequality: 'gl-code-quality-report.json', sast: 'gl-sast-report.json', + secret_detection: 'gl-secret-detection-report.json', dependency_scanning: 'gl-dependency-scanning-report.json', container_scanning: 'gl-container-scanning-report.json', dast: 'gl-dast-report.json', @@ -37,7 +39,8 @@ module Ci dotenv: '.env', cobertura: 'cobertura-coverage.xml', terraform: 'tfplan.json', - cluster_applications: 'gl-cluster-applications.json' + cluster_applications: 'gl-cluster-applications.json', + requirements: 'requirements.json' }.freeze INTERNAL_TYPES = { @@ -62,13 +65,15 @@ module Ci accessibility: :raw, codequality: :raw, sast: :raw, + secret_detection: :raw, dependency_scanning: :raw, container_scanning: :raw, dast: :raw, license_management: :raw, license_scanning: :raw, performance: :raw, - terraform: :raw + terraform: :raw, + requirements: :raw }.freeze DOWNLOADABLE_TYPES = %w[ @@ -87,6 +92,8 @@ module Ci metrics performance sast + secret_detection + requirements ].freeze TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze @@ -109,6 +116,7 @@ module Ci after_save :update_file_store, if: :saved_change_to_file? + scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } @@ -147,7 +155,8 @@ module Ci where(file_type: types) end - scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) } + scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) } + scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) } scope :locked, -> { where(locked: true) } scope :unlocked, -> { where(locked: [false, nil]) } @@ -176,7 +185,9 @@ module Ci cobertura: 17, terraform: 18, # Transformed json accessibility: 19, - cluster_applications: 20 + cluster_applications: 20, + secret_detection: 21, ## EE-specific + requirements: 22 ## EE-specific } enum file_format: { @@ -242,8 +253,16 @@ module Ci super || self.file_location.nil? end + def expired? + expire_at.present? && expire_at < Time.current + end + + def expiring? + expire_at.present? && expire_at > Time.current + end + def expire_in - expire_at - Time.now if expire_at + expire_at - Time.current if expire_at end def expire_in=(value) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5db1635f64d..497e1a4d74a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -31,6 +31,7 @@ module Ci belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' belongs_to :merge_request, class_name: 'MergeRequest' belongs_to :external_pull_request + belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines has_internal_id :iid, scope: :project, presence: false, track_if: -> { !importing? }, ensure_if: -> { !importing? }, init: ->(s) do s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count @@ -40,11 +41,15 @@ module Ci has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline + has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline + has_many :job_artifacts, through: :builds has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :variables, class_name: 'Ci::PipelineVariable' has_many :deployments, through: :builds has_many :environments, -> { distinct }, through: :deployments + has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' + has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts # Merge requests for which the current pipeline is running against # the merge request's latest commit. @@ -56,7 +61,6 @@ module Ci has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' - has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' @@ -64,13 +68,6 @@ module Ci has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline - has_one :ref_status, ->(pipeline) { - # We use .read_attribute to save 1 extra unneeded query to load the :project. - unscope(:where) - .where(project_id: pipeline.read_attribute(:project_id), ref: pipeline.ref, tag: pipeline.tag) - # Sadly :inverse_of is not supported (yet) by Rails for composite PKs. - }, class_name: 'Ci::Ref', inverse_of: :pipelines - has_one :chat_data, class_name: 'Ci::PipelineChatData' has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline @@ -163,11 +160,11 @@ module Ci # Create a separate worker for each new operation before_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline| - pipeline.started_at = Time.now + pipeline.started_at = Time.current end before_transition any => [:success, :failed, :canceled] do |pipeline| - pipeline.finished_at = Time.now + pipeline.finished_at = Time.current pipeline.update_duration end @@ -235,12 +232,10 @@ module Ci end after_transition any => [:success, :failed] do |pipeline| + ref_status = pipeline.ci_ref&.update_status_by!(pipeline) + pipeline.run_after_commit do - if Feature.enabled?(:ci_pipeline_fixed_notifications) - PipelineUpdateCiRefStatusWorker.perform_async(pipeline.id) - else - PipelineNotificationWorker.perform_async(pipeline.id) - end + PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref_status) end end @@ -260,6 +255,7 @@ module Ci scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) } scope :for_ref, -> (ref) { where(ref: ref) } scope :for_id, -> (id) { where(id: id) } + scope :for_iid, -> (iid) { where(iid: iid) } scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) } scope :with_reports, -> (reports_scope) do @@ -397,11 +393,11 @@ module Ci end def ordered_stages - if Feature.enabled?(:ci_atomic_processing, project, default_enabled: false) + if ::Gitlab::Ci::Features.atomic_processing?(project) # The `Ci::Stage` contains all up-to date data # as atomic processing updates all data in-bulk stages - elsif Feature.enabled?(:ci_pipeline_persisted_stages, default_enabled: true) && complete? + elsif complete? # The `Ci::Stage` contains up-to date data only for `completed` pipelines # this is due to asynchronous processing of pipeline, and stages possibly # not updated inline with processing of pipeline @@ -445,7 +441,7 @@ module Ci end def legacy_stages - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) legacy_stages_using_composite_status else legacy_stages_using_sql @@ -798,13 +794,17 @@ module Ci @latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a end + def latest_report_builds(reports_scope = ::Ci::JobArtifact.with_reports) + builds.latest.with_reports(reports_scope) + end + def has_reports?(reports_scope) - complete? && builds.latest.with_reports(reports_scope).exists? + complete? && latest_report_builds(reports_scope).exists? end def test_reports Gitlab::Ci::Reports::TestReports.new.tap do |test_reports| - builds.latest.with_reports(Ci::JobArtifact.test_reports).preload(:project).find_each do |build| + latest_report_builds(Ci::JobArtifact.test_reports).preload(:project).find_each do |build| build.collect_test_reports!(test_reports) end end @@ -826,7 +826,7 @@ module Ci def coverage_reports Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports| - builds.latest.with_reports(Ci::JobArtifact.coverage_reports).each do |build| + latest_report_builds(Ci::JobArtifact.coverage_reports).each do |build| build.collect_coverage_reports!(coverage_reports) end end @@ -834,7 +834,7 @@ module Ci def terraform_reports ::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports| - builds.latest.with_reports(::Ci::JobArtifact.terraform_reports).each do |build| + latest_report_builds(::Ci::JobArtifact.terraform_reports).each do |build| build.collect_terraform_reports!(terraform_reports) end end @@ -969,6 +969,12 @@ module Ci processables.populate_scheduling_type! end + def ensure_ci_ref! + return unless Gitlab::Ci::Features.pipeline_fixed_notifications? + + self.ci_ref = Ci::Ref.ensure_for(self) + end + private def pipeline_data diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index 7e203cb67c4..2ccd8445aa8 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -27,9 +27,11 @@ module Ci # https://gitlab.com/gitlab-org/gitlab/issues/195991 pipeline: 7, chat: 8, + webide: 9, merge_request_event: 10, external_pull_request_event: 11, - parent_pipeline: 12 + parent_pipeline: 12, + ondemand_scan: 13 } end @@ -40,6 +42,7 @@ module Ci unknown_source: nil, repository_source: 1, auto_devops_source: 2, + webide_source: 3, remote_source: 4, external_project_source: 5, bridge_source: 6 diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index cc00500662d..ac5785d9c91 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -4,12 +4,8 @@ module Ci class Processable < ::CommitStatus include Gitlab::Utils::StrongMemoize - has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build - accepts_nested_attributes_for :needs - enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true - scope :preload_needs, -> { preload(:needs) } scope :with_needs, -> (names = nil) do diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb index a0782bc0444..be6062b6e6e 100644 --- a/app/models/ci/ref.rb +++ b/app/models/ci/ref.rb @@ -3,21 +3,62 @@ module Ci class Ref < ApplicationRecord extend Gitlab::Ci::Model + include Gitlab::OptimisticLocking - STATUSES = %w[success failed fixed].freeze - - belongs_to :project - belongs_to :last_updated_by_pipeline, foreign_key: :last_updated_by_pipeline_id, class_name: 'Ci::Pipeline' - # ActiveRecord doesn't support composite FKs for this reason we have to do the 'unscope(:where)' - # hack. - has_many :pipelines, ->(ref) { - # We use .read_attribute to save 1 extra unneeded query to load the :project. - unscope(:where) - .where(ref: ref.ref, project_id: ref.read_attribute(:project_id), tag: ref.tag) - # Sadly :inverse_of is not supported (yet) by Rails for composite PKs. - }, inverse_of: :ref_status - - validates :status, inclusion: { in: STATUSES } - validates :last_updated_by_pipeline, presence: true + FAILING_STATUSES = %w[failed broken still_failing].freeze + + belongs_to :project, inverse_of: :ci_refs + has_many :pipelines, class_name: 'Ci::Pipeline', foreign_key: :ci_ref_id, inverse_of: :ci_ref + + state_machine :status, initial: :unknown do + event :succeed do + transition unknown: :success + transition fixed: :success + transition %i[failed broken still_failing] => :fixed + end + + event :do_fail do + transition unknown: :failed + transition %i[failed broken] => :still_failing + transition %i[success fixed] => :broken + end + + state :unknown, value: 0 + state :success, value: 1 + state :failed, value: 2 + state :fixed, value: 3 + state :broken, value: 4 + state :still_failing, value: 5 + end + + class << self + def ensure_for(pipeline) + safe_find_or_create_by(project_id: pipeline.project_id, + ref_path: pipeline.source_ref_path) + end + + def failing_state?(status_name) + FAILING_STATUSES.include?(status_name) + end + end + + def last_finished_pipeline_id + Ci::Pipeline.where(ci_ref_id: self.id).finished.order(id: :desc).select(:id).take&.id + end + + def update_status_by!(pipeline) + return unless Gitlab::Ci::Features.pipeline_fixed_notifications? + + retry_lock(self) do + next unless last_finished_pipeline_id == pipeline.id + + case pipeline.status + when 'success' then self.succeed + when 'failed' then self.do_fail + end + + self.status_name + end + end end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index d4e9217ff9f..8fc273556f0 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,10 +23,17 @@ module Ci project_type: 3 } - ONLINE_CONTACT_TIMEOUT = 1.hour + # This `ONLINE_CONTACT_TIMEOUT` needs to be larger than + # `RUNNER_QUEUE_EXPIRY_TIME+UPDATE_CONTACT_COLUMN_EVERY` + # + ONLINE_CONTACT_TIMEOUT = 2.hours + + # The `RUNNER_QUEUE_EXPIRY_TIME` indicates the longest interval that + # Runner request needs to be refreshed by Rails instead of being handled + # by Workhorse RUNNER_QUEUE_EXPIRY_TIME = 1.hour - # This needs to be less than `ONLINE_CONTACT_TIMEOUT` + # The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner DB entry can be updated UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze @@ -81,6 +88,17 @@ module Ci joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups }) } + scope :belonging_to_group_or_project, -> (group_id, project_id) { + groups = ::Group.where(id: group_id) + + group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups }) + project_runners = joins(:runner_projects).where(ci_runner_projects: { project_id: project_id }) + + union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql + + from("(#{union_sql}) #{table_name}") + } + scope :belonging_to_parent_group_of_project, -> (project_id) { project_groups = ::Group.joins(:projects).where(projects: { id: project_id }) hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors @@ -145,14 +163,14 @@ module Ci # Searches for runners matching the given query. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # This method performs a *partial* match on tokens, thus a query for "a" # will match any runner where the token contains the letter "a". As a result # you should *not* use this method for non-admin purposes as otherwise users # might be able to query a list of all runners. # - # query - The search query as a String + # query - The search query as a String. # # Returns an ActiveRecord::Relation. def self.search(query) @@ -271,9 +289,9 @@ module Ci ensure_runner_queue_value == value if value.present? end - def update_cached_info(values) + def heartbeat(values) values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {} - values[:contacted_at] = Time.now + values[:contacted_at] = Time.current cache_attributes(values) @@ -309,7 +327,7 @@ module Ci real_contacted_at = read_attribute(:contacted_at) real_contacted_at.nil? || - (Time.now - real_contacted_at) >= contacted_at_max_age + (Time.current - real_contacted_at) >= contacted_at_max_age end def tag_constraints diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 1efa44c39c5..53c90fa56d5 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -38,7 +38,8 @@ module Clusters chart: chart, files: files.merge(cluster_issuer_file), preinstall: pre_install_script, - postinstall: post_install_script + postinstall: post_install_script, + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -47,7 +48,8 @@ module Clusters name: 'certmanager', rbac: cluster.platform_kubernetes_rbac?, files: files, - postdelete: post_delete_script + postdelete: post_delete_script, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb index 420e56c1742..2e5a8210b3c 100644 --- a/app/models/clusters/applications/crossplane.rb +++ b/app/models/clusters/applications/crossplane.rb @@ -35,7 +35,8 @@ module Clusters version: VERSION, rbac: cluster.platform_kubernetes_rbac?, chart: chart, - files: files + files: files, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb index 0d029aabc3b..58ac0c1f188 100644 --- a/app/models/clusters/applications/elastic_stack.rb +++ b/app/models/clusters/applications/elastic_stack.rb @@ -34,7 +34,8 @@ module Clusters repository: repository, files: files, preinstall: migrate_to_3_script, - postinstall: post_install_script + postinstall: post_install_script, + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -43,7 +44,8 @@ module Clusters name: 'elastic-stack', rbac: cluster.platform_kubernetes_rbac?, files: files, - postdelete: post_delete_script + postdelete: post_delete_script, + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -51,7 +53,7 @@ module Clusters super.merge('wait-for-elasticsearch.sh': File.read("#{Rails.root}/vendor/elastic_stack/wait-for-elasticsearch.sh")) end - def elasticsearch_client + def elasticsearch_client(timeout: nil) strong_memoize(:elasticsearch_client) do next unless kube_client @@ -63,6 +65,7 @@ module Clusters # 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] + faraday.options.timeout = timeout unless timeout.nil? end rescue Kubeclient::HttpError => error @@ -118,7 +121,8 @@ module Clusters Gitlab::Kubernetes::Helm::DeleteCommand.new( name: 'elastic-stack', rbac: cluster.platform_kubernetes_rbac?, - files: files + files: files, + local_tiller_enabled: cluster.local_tiller_enabled? ).delete_command, Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE) ] diff --git a/app/models/clusters/applications/fluentd.rb b/app/models/clusters/applications/fluentd.rb index 3fd6e870edc..1bcd39618f6 100644 --- a/app/models/clusters/applications/fluentd.rb +++ b/app/models/clusters/applications/fluentd.rb @@ -32,7 +32,8 @@ module Clusters version: VERSION, rbac: cluster.platform_kubernetes_rbac?, chart: chart, - files: files + files: files, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 4a1bcac4bb7..226a9c26db0 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -52,7 +52,8 @@ module Clusters Gitlab::Kubernetes::Helm::InitCommand.new( name: name, files: files, - rbac: cluster.platform_kubernetes_rbac? + rbac: cluster.platform_kubernetes_rbac?, + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -60,7 +61,8 @@ module Clusters Gitlab::Kubernetes::Helm::ResetCommand.new( name: name, files: files, - rbac: cluster.platform_kubernetes_rbac? + rbac: cluster.platform_kubernetes_rbac?, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index dd354198910..a44450ec7a9 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -63,7 +63,8 @@ module Clusters version: VERSION, rbac: cluster.platform_kubernetes_rbac?, chart: chart, - files: files + files: files, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 056ea355de6..b737f0f962f 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -45,7 +45,8 @@ module Clusters rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, - repository: repository + repository: repository, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 3047da12dd9..b55fc3c45fc 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -77,7 +77,8 @@ module Clusters chart: chart, files: files, repository: REPOSITORY, - postinstall: install_knative_metrics + postinstall: install_knative_metrics, + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -99,7 +100,8 @@ module Clusters rbac: cluster.platform_kubernetes_rbac?, files: files, predelete: delete_knative_services_and_metrics, - postdelete: delete_knative_istio_leftovers + postdelete: delete_knative_istio_leftovers, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 3183318690c..24bb1df6d22 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -37,7 +37,7 @@ module Clusters end after_transition any => :updating do |application| - application.update(last_update_started_at: Time.now) + application.update(last_update_started_at: Time.current) end end @@ -66,7 +66,8 @@ module Clusters rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, - postinstall: install_knative_metrics + postinstall: install_knative_metrics, + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -76,7 +77,8 @@ module Clusters version: version, rbac: cluster.platform_kubernetes_rbac?, chart: chart, - files: files_with_replaced_values(values) + files: files_with_replaced_values(values), + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -85,7 +87,8 @@ module Clusters name: name, rbac: cluster.platform_kubernetes_rbac?, files: files, - predelete: delete_knative_istio_metrics + predelete: delete_knative_istio_metrics, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index a861126908f..6d3b6c4ed8f 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.16.1' + VERSION = '0.17.1' self.table_name = 'clusters_applications_runners' @@ -36,7 +36,8 @@ module Clusters rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, - repository: repository + repository: repository, + local_tiller_enabled: cluster.local_tiller_enabled? ) end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 83f558af1a1..bde7a2104ba 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -36,6 +36,8 @@ module Clusters has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project' has_many :deployment_clusters has_many :deployments, inverse_of: :cluster + has_many :successful_deployments, -> { success }, class_name: 'Deployment' + has_many :environments, -> { distinct }, through: :deployments has_many :cluster_groups, class_name: 'Clusters::Group' has_many :groups, through: :cluster_groups, class_name: '::Group' @@ -125,12 +127,23 @@ module Clusters scope :gcp_installed, -> { gcp_provided.joins(:provider_gcp).merge(Clusters::Providers::Gcp.with_status(:created)) } scope :aws_installed, -> { aws_provided.joins(:provider_aws).merge(Clusters::Providers::Aws.with_status(:created)) } + scope :with_enabled_modsecurity, -> { joins(:application_ingress).merge(::Clusters::Applications::Ingress.modsecurity_enabled) } + scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) } + scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct } + scope :preload_elasticstack, -> { preload(:application_elastic_stack) } + scope :preload_environments, -> { preload(:environments) } + scope :managed, -> { where(managed: true) } scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) } scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } scope :with_management_project, -> { where.not(management_project: nil) } scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) } + scope :with_application_prometheus, -> { includes(:application_prometheus).joins(:application_prometheus) } + scope :with_project_alert_service_data, -> (project_ids) do + conditions = { projects: { alerts_service: [:data] } } + includes(conditions).joins(conditions).where(projects: { id: project_ids }) + end def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc) return [] if clusterable.is_a?(Instance) @@ -321,6 +334,10 @@ module Clusters end end + def local_tiller_enabled? + Feature.enabled?(:managed_apps_local_tiller, clusterable, default_enabled: false) + end + private def unique_management_project_environment_scope @@ -368,7 +385,10 @@ module Clusters def retrieve_nodes result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.get_nodes } - cluster_nodes = result[:response].to_a + + return unless result[:response] + + cluster_nodes = result[:response] result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.metrics_client.get_nodes } nodes_metrics = result[:response].to_a diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index 297d00aa281..c1f63758906 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -15,7 +15,7 @@ module Clusters def set_initial_status return unless not_installable? - self.status = status_states[:installable] if cluster&.application_helm_available? || ::Gitlab::Kubernetes::Helm.local_tiller_enabled? + self.status = status_states[:installable] if cluster&.application_helm_available? || cluster&.local_tiller_enabled? end def can_uninstall? diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb index 77c606553d2..ade27e69642 100644 --- a/app/models/clusters/concerns/application_data.rb +++ b/app/models/clusters/concerns/application_data.rb @@ -7,7 +7,8 @@ module Clusters Gitlab::Kubernetes::Helm::DeleteCommand.new( name: name, rbac: cluster.platform_kubernetes_rbac?, - files: files + files: files, + local_tiller_enabled: cluster.local_tiller_enabled? ) end @@ -32,7 +33,7 @@ module Clusters private def use_tiller_ssl? - return false if ::Gitlab::Kubernetes::Helm.local_tiller_enabled? + return false if cluster.local_tiller_enabled? cluster.application_helm.has_ssl? end diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 0b915126f8a..86d74ed7b1c 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -97,13 +97,21 @@ module Clusters application.status_reason = status_reason if status_reason end - before_transition any => [:installed, :updated] do |application, _| - # When installing any application we are also performing an update - # of tiller (see Gitlab::Kubernetes::Helm::ClientCommand) so - # therefore we need to reflect that in the database. - - unless ::Gitlab::Kubernetes::Helm.local_tiller_enabled? - application.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION) + before_transition any => [:installed, :updated] do |application, transition| + unless application.cluster.local_tiller_enabled? || application.is_a?(Clusters::Applications::Helm) + if transition.event == :make_externally_installed + # If an application is externally installed + # We assume the helm application is externally installed too + helm = application.cluster.application_helm || application.cluster.build_application_helm + + helm.make_externally_installed! + else + # When installing any application we are also performing an update + # of tiller (see Gitlab::Kubernetes::Helm::ClientCommand) so + # therefore we need to reflect that in the database. + + application.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION) + end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 7e99f128dad..475f82f23ca 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -14,6 +14,10 @@ class CommitStatus < ApplicationRecord belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' + has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build + + enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true + delegate :commit, to: :pipeline delegate :sha, :short_sha, :before_sha, to: :pipeline @@ -90,7 +94,12 @@ class CommitStatus < ApplicationRecord end before_save if: :status_changed?, unless: :importing? do - if Feature.disabled?(:ci_atomic_processing, project) + # we mark `processed` as always changed: + # another process might change its value and our object + # will not be refreshed to pick the change + self.processed_will_change! + + if !::Gitlab::Ci::Features.atomic_processing?(project) self.processed = nil elsif latest? self.processed = false # force refresh of all dependent ones @@ -132,15 +141,15 @@ class CommitStatus < ApplicationRecord end before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status| - commit_status.queued_at = Time.now + commit_status.queued_at = Time.current end before_transition [:created, :preparing, :pending] => :running do |commit_status| - commit_status.started_at = Time.now + commit_status.started_at = Time.current end before_transition any => [:success, :failed, :canceled] do |commit_status| - commit_status.finished_at = Time.now + commit_status.finished_at = Time.current end before_transition any => :failed do |commit_status, transition| @@ -185,8 +194,10 @@ class CommitStatus < ApplicationRecord end def self.update_as_processed! - # Marks items as processed, and increases `lock_version` (Optimisitc Locking) - update_all('processed=TRUE, lock_version=COALESCE(lock_version,0)+1') + # Marks items as processed + # we do not increase `lock_version`, as we are the one + # holding given lock_version (Optimisitc Locking) + update_all(processed: true) end def self.locking_enabled? @@ -276,7 +287,7 @@ class CommitStatus < ApplicationRecord end def schedule_stage_and_pipeline_update - if Feature.enabled?(:ci_atomic_processing, project) + if ::Gitlab::Ci::Features.atomic_processing?(project) # Atomic Processing requires only single Worker PipelineProcessWorker.perform_async(pipeline_id, [id]) else diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index d459af23a2f..de176ffde5c 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -55,7 +55,7 @@ module CacheableAttributes current_without_cache.tap { |current_record| current_record&.cache! } rescue => e if Rails.env.production? - Rails.logger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}") else raise e end diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index ccd90ea5900..7ea5382a4fa 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -18,7 +18,7 @@ module Ci variables.concat(deployment_variables(environment: environment)) variables.concat(yaml_variables) variables.concat(user_variables) - variables.concat(dependency_variables) if Feature.enabled?(:ci_dependency_variables, project) + variables.concat(dependency_variables) variables.concat(secret_instance_variables) variables.concat(secret_group_variables) variables.concat(secret_project_variables(environment: environment)) diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index 6314b46a7e3..af5f4e30d06 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -17,7 +17,7 @@ module EachBatch # Example: # # User.each_batch do |relation| - # relation.update_all(updated_at: Time.now) + # relation.update_all(updated_at: Time.current) # end # # The supplied block is also passed an optional batch index: diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb new file mode 100644 index 00000000000..60aa46ce04c --- /dev/null +++ b/app/models/concerns/featurable.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# == Featurable concern +# +# This concern adds features (tools) functionality to Project and Group +# To enable features you need to call `set_available_features` +# +# Example: +# +# class ProjectFeature +# include Featurable +# set_available_features %i(wiki merge_request) + +module Featurable + extend ActiveSupport::Concern + + # Can be enabled only for members, everyone or disabled + # Access control is made only for non private containers. + # + # Permission levels: + # + # Disabled: not enabled for anyone + # Private: enabled only for team members + # Enabled: enabled for everyone able to access the project + # Public: enabled for everyone (only allowed for pages) + DISABLED = 0 + PRIVATE = 10 + ENABLED = 20 + PUBLIC = 30 + + STRING_OPTIONS = HashWithIndifferentAccess.new({ + 'disabled' => DISABLED, + 'private' => PRIVATE, + 'enabled' => ENABLED, + 'public' => PUBLIC + }).freeze + + class_methods do + def set_available_features(available_features = []) + @available_features = available_features + + class_eval do + available_features.each do |feature| + define_method("#{feature}_enabled?") do + public_send("#{feature}_access_level") > DISABLED # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end + + def available_features + @available_features + end + + def access_level_attribute(feature) + feature = ensure_feature!(feature) + + "#{feature}_access_level".to_sym + end + + def quoted_access_level_column(feature) + attribute = connection.quote_column_name(access_level_attribute(feature)) + table = connection.quote_table_name(table_name) + + "#{table}.#{attribute}" + end + + def access_level_from_str(level) + STRING_OPTIONS.fetch(level) + end + + def str_from_access_level(level) + STRING_OPTIONS.key(level) + end + + def ensure_feature!(feature) + feature = feature.model_name.plural if feature.respond_to?(:model_name) + feature = feature.to_sym + raise ArgumentError, "invalid feature: #{feature}" unless available_features.include?(feature) + + feature + end + end + + def access_level(feature) + public_send(self.class.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend + end + + def feature_available?(feature, user) + # This feature might not be behind a feature flag at all, so default to true + return false unless ::Feature.enabled?(feature, user, default_enabled: true) + + get_permission(user, feature) + end + + def string_access_level(feature) + self.class.str_from_access_level(access_level(feature)) + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index b80f8c2bbb2..c885dea862f 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -66,7 +66,7 @@ module HasStatus # 1. By plucking all related objects, # 2. Or executes expensive SQL query def slow_composite_status(project:) - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) Gitlab::Ci::Status::Composite .new(all, with_allow_failure: columns_hash.key?('allow_failure')) .status @@ -160,7 +160,7 @@ module HasStatus if started_at && finished_at finished_at - started_at elsif started_at - Time.now - started_at + Time.current - started_at end end end diff --git a/app/models/concerns/import_state/sidekiq_job_tracker.rb b/app/models/concerns/import_state/sidekiq_job_tracker.rb index 55f171d158d..b7d0ed0f51b 100644 --- a/app/models/concerns/import_state/sidekiq_job_tracker.rb +++ b/app/models/concerns/import_state/sidekiq_job_tracker.rb @@ -5,14 +5,17 @@ module ImportState extend ActiveSupport::Concern included do + scope :with_jid, -> { where.not(jid: nil) } + scope :without_jid, -> { where(jid: nil) } + # Refreshes the expiration time of the associated import job ID. # # This method can be used by asynchronous importers to refresh the status, - # preventing the StuckImportJobsWorker from marking the import as failed. + # preventing the Gitlab::Import::StuckProjectImportJobsWorker from marking the import as failed. def refresh_jid_expiration return unless jid - Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) + Gitlab::SidekiqStatus.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION) end def self.jid_by(project_id:, status:) diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb new file mode 100644 index 00000000000..644a0ba1b5e --- /dev/null +++ b/app/models/concerns/integration.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Integration + extend ActiveSupport::Concern + + class_methods do + def with_custom_integration_for(integration, page = nil, per = nil) + custom_integration_project_ids = Service + .where(type: integration.type) + .where(inherit_from_id: nil) + .distinct # Required until https://gitlab.com/gitlab-org/gitlab/-/issues/207385 + .page(page) + .per(per) + .pluck(:project_id) + + Project.where(id: custom_integration_project_ids) + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index a1b14dca4ac..220af8ab7c7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -39,15 +39,6 @@ module Issuable locked: 4 }.with_indifferent_access.freeze - # This object is used to gather issuable meta data for displaying - # upvotes, downvotes, notes and closing merge requests count for issues and merge requests - # lists avoiding n+1 queries and improving performance. - IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do - def merge_requests_count(user = nil) - mrs_count - end - end - included do cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description, issuable_state_filter_enabled: true @@ -139,7 +130,6 @@ module Issuable 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 :with_label_ids, ->(label_ids) { joins(:label_links).where(label_links: { label_id: label_ids }) } - scope :any_label, -> { joins(:label_links).distinct } scope :join_project, -> { joins(:project) } scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) } scope :references_project, -> { references(:project) } @@ -185,6 +175,10 @@ module Issuable assignees.count > 1 end + def supports_weight? + false + end + private def description_max_length_for_new_records_is_valid @@ -201,7 +195,7 @@ module Issuable class_methods do # Searches for records with a matching title. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # query - The search query as a String # @@ -225,7 +219,7 @@ module Issuable # Searches for records with a matching title or description. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # query - The search query as a String # matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma. @@ -316,6 +310,14 @@ module Issuable end end + def any_label(sort = nil) + if sort + joins(:label_links).group(*grouping_columns(sort)) + else + joins(:label_links).distinct + end + end + # Includes table keys in group by clause when sorting # preventing errors in postgres # @@ -401,6 +403,10 @@ module Issuable participants(user).include?(user) end + def can_assign_epic?(user) + false + end + def to_hook_data(user, old_associations: {}) changes = previous_changes diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb index f320f54bb82..3cb0bd85936 100644 --- a/app/models/concerns/limitable.rb +++ b/app/models/concerns/limitable.rb @@ -2,6 +2,7 @@ module Limitable extend ActiveSupport::Concern + GLOBAL_SCOPE = :limitable_global_scope included do class_attribute :limit_scope @@ -14,14 +15,34 @@ module Limitable private def validate_plan_limit_not_exceeded + if GLOBAL_SCOPE == limit_scope + validate_global_plan_limit_not_exceeded + else + validate_scoped_plan_limit_not_exceeded + end + end + + def validate_scoped_plan_limit_not_exceeded scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend return unless scope_relation relation = self.class.where(limit_scope => scope_relation) + limits = scope_relation.actual_limits - if scope_relation.actual_limits.exceeded?(limit_name, relation) - errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") % - { name: limit_name.humanize(capitalize: false), count: scope_relation.actual_limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend - end + check_plan_limit_not_exceeded(limits, relation) + end + + def validate_global_plan_limit_not_exceeded + relation = self.class.all + limits = Plan.default.actual_limits + + check_plan_limit_not_exceeded(limits, relation) + end + + def check_plan_limit_not_exceeded(limits, relation) + return unless limits.exceeded?(limit_name, relation) + + errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") % + { name: limit_name.humanize(capitalize: false), count: limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index d157404f7bc..7b4485376d4 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -259,8 +259,8 @@ module Mentionable # for the test period. # During the test period the flag should be enabled at the group level. def store_mentioned_users_to_db_enabled? - return Feature.enabled?(:store_mentioned_users_to_db, self.project&.group) if self.respond_to?(:project) - return Feature.enabled?(:store_mentioned_users_to_db, self.group) if self.respond_to?(:group) + return Feature.enabled?(:store_mentioned_users_to_db, self.project&.group, default_enabled: true) if self.respond_to?(:project) + return Feature.enabled?(:store_mentioned_users_to_db, self.group, default_enabled: true) if self.respond_to?(:group) end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index fa5a79cc12b..5f24564dc56 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -97,26 +97,6 @@ module Milestoneish due_date && due_date.past? end - def group_milestone? - false - end - - def project_milestone? - false - end - - def legacy_group_milestone? - false - end - - def dashboard_milestone? - false - end - - def global_milestone? - false - end - def total_time_spent @total_time_spent ||= issues.joins(:timelogs).sum(:time_spent) + merge_requests.joins(:timelogs).sum(:time_spent) end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 933a0b167e2..183b902dd37 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -24,7 +24,7 @@ module Noteable # The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via # API call) def system_note_timestamp - @system_note_timestamp || Time.now # rubocop:disable Gitlab/ModuleWithInstanceVariables + @system_note_timestamp || Time.current # rubocop:disable Gitlab/ModuleWithInstanceVariables end attr_writer :system_note_timestamp diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb index 761a151a474..adb6a59e11c 100644 --- a/app/models/concerns/prometheus_adapter.rb +++ b/app/models/concerns/prometheus_adapter.rb @@ -44,7 +44,7 @@ module PrometheusAdapter { success: true, data: data, - last_update: Time.now.utc + last_update: Time.current.utc } rescue Gitlab::PrometheusClient::Error => err { success: false, result: err.message } diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 1653ecdb305..1d89a4497d9 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -50,7 +50,7 @@ module RelativePositioning # This method takes two integer values (positions) and # calculates the position between them. The range is huge as # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time - # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number + # when we have enough space. If distance is less than IDEAL_DISTANCE, we are calculating an average number. def position_between(pos_before, pos_after) pos_before ||= MIN_POSITION pos_after ||= MAX_POSITION diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index 5d78eea7fca..5174ae05d15 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -23,7 +23,10 @@ module ResolvableDiscussion :last_note ) - delegate :potentially_resolvable?, to: :first_note + delegate :potentially_resolvable?, + :noteable_id, + :noteable_type, + to: :first_note delegate :resolved_at, :resolved_by, @@ -79,7 +82,7 @@ module ResolvableDiscussion return false unless current_user return false unless resolvable? - current_user == self.noteable.author || + current_user == self.noteable.try(:author) || current_user.can?(:resolve_note, self.project) end diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 2d2d5fb7168..4e8a1bb643e 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -23,7 +23,7 @@ module ResolvableNote class_methods do # This method must be kept in sync with `#resolve!` def resolve!(current_user) - unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id) + unresolved.update_all(resolved_at: Time.current, resolved_by_id: current_user.id) end # This method must be kept in sync with `#unresolve!` @@ -57,7 +57,7 @@ module ResolvableNote return false unless resolvable? return false if resolved? - self.resolved_at = Time.now + self.resolved_at = Time.current self.resolved_by = current_user self.resolved_by_push = resolved_by_push diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index da4f2a79895..250889fdf8b 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -67,7 +67,7 @@ module Storage 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 + Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}") # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb index d29e6a01c56..8927e42dd97 100644 --- a/app/models/concerns/timebox.rb +++ b/app/models/concerns/timebox.rb @@ -7,7 +7,9 @@ module Timebox include CacheMarkdownField include Gitlab::SQL::Pattern include IidRoutes + include Referable include StripAttribute + include FromUnion TimeboxStruct = Struct.new(:title, :name, :id) do # Ensure these models match the interface required for exporting @@ -64,7 +66,11 @@ module Timebox groups = groups.compact if groups.is_a? Array groups = [] if groups.nil? - where(project_id: projects).or(where(group_id: groups)) + if Feature.enabled?(:optimized_timebox_queries, default_enabled: true) + from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false) + else + where(project_id: projects).or(where(group_id: groups)) + end end scope :within_timeframe, -> (start_date, end_date) do @@ -122,6 +128,35 @@ module Timebox end end + ## + # Returns the String necessary to reference a Timebox in Markdown. Group + # timeboxes only support name references, and do not support cross-project + # references. + # + # format - Symbol format to use (default: :iid, optional: :name) + # + # Examples: + # + # Milestone.first.to_reference # => "%1" + # Iteration.first.to_reference(format: :name) # => "*iteration:\"goal\"" + # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1" + # Iteration.first.to_reference(same_namespace_project) # => "gitlab-foss*iteration:1" + # + def to_reference(from = nil, format: :name, full: false) + format_reference = timebox_format_reference(format) + reference = "#{self.class.reference_prefix}#{format_reference}" + + if project + "#{project.to_reference_base(from, full: full)}#{reference}" + else + reference + end + end + + def reference_link_text(from = nil) + self.class.reference_prefix + self.title + end + def title=(value) write_attribute(:title, sanitize_title(value)) if value.present? end @@ -162,6 +197,20 @@ module Timebox private + def timebox_format_reference(format = :iid) + raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format) + + if group_timebox? && format == :iid + raise ArgumentError, _('Cannot refer to a group %{timebox_type} by an internal id!') % { timebox_type: timebox_name } + end + + if format == :name && !name.include?('"') + %("#{name}") + else + iid + end + end + # Timebox titles must be unique across project and group timeboxes def uniqueness_of_title if project diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 4099039dd96..a1f83884f02 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -4,6 +4,10 @@ module TokenAuthenticatable extend ActiveSupport::Concern class_methods do + def encrypted_token_authenticatable_fields + @encrypted_token_authenticatable_fields ||= [] + end + private def add_authentication_token_field(token_field, options = {}) @@ -12,6 +16,7 @@ module TokenAuthenticatable end token_authenticatable_fields.push(token_field) + encrypted_token_authenticatable_fields.push(token_field) if options[:encrypted] attr_accessor :cleartext_tokens diff --git a/app/models/concerns/update_highest_role.rb b/app/models/concerns/update_highest_role.rb index 7efc436c6c8..6432cc794a5 100644 --- a/app/models/concerns/update_highest_role.rb +++ b/app/models/concerns/update_highest_role.rb @@ -29,9 +29,7 @@ module UpdateHighestRole UpdateHighestRoleWorker.perform_in(HIGHEST_ROLE_JOB_DELAY, update_highest_role_attribute) else # use same logging as ExclusiveLeaseGuard - # rubocop:disable Gitlab/RailsLogger - Rails.logger.error('Cannot obtain an exclusive lease. There must be another instance already in execution.') - # rubocop:enable Gitlab/RailsLogger + Gitlab::AppLogger.error('Cannot obtain an exclusive lease. There must be another instance already in execution.') end end end diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb index 76bfbabf3b3..b1dd720d908 100644 --- a/app/models/container_expiration_policy.rb +++ b/app/models/container_expiration_policy.rb @@ -13,6 +13,8 @@ class ContainerExpirationPolicy < ApplicationRecord validates :cadence, presence: true, inclusion: { in: ->(_) { self.cadence_options.stringify_keys } } validates :older_than, inclusion: { in: ->(_) { self.older_than_options.stringify_keys } }, allow_nil: true validates :keep_n, inclusion: { in: ->(_) { self.keep_n_options.keys } }, allow_nil: true + validates :name_regex, untrusted_regexp: true, if: :enabled? + validates :name_regex_keep, untrusted_regexp: true, if: :enabled? scope :active, -> { where(enabled: true) } scope :preloaded, -> { preload(project: [:route]) } @@ -50,4 +52,8 @@ class ContainerExpirationPolicy < ApplicationRecord def set_next_run_at self.next_run_at = Time.zone.now + ChronicDuration.parse(cadence).seconds end + + def disable! + update_attribute(:enabled, false) + end end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 455c672cea3..b0f7edac2f3 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -16,7 +16,13 @@ class ContainerRepository < ApplicationRecord scope :ordered, -> { order(:name) } 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)) + project_scope = Project + .for_group_and_its_subgroups(group) + .with_container_registry + .select(:id) + + ContainerRepository + .joins("INNER JOIN (#{project_scope.to_sql}) projects on projects.id=container_repositories.project_id") end scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) } @@ -67,6 +73,12 @@ class ContainerRepository < ApplicationRecord end end + def tags_count + return 0 unless manifest && manifest['tags'] + + manifest['tags'].size + end + def blob(config) ContainerRegistry::Blob.new(self, config) end diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb deleted file mode 100644 index 48c09f4cd6b..00000000000 --- a/app/models/dashboard_group_milestone.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true -# Dashboard Group Milestones are milestones that allow us to pull more info out for the UI that the Milestone object doesn't allow for -class DashboardGroupMilestone < GlobalMilestone - extend ::Gitlab::Utils::Override - - attr_reader :group_name - - def initialize(milestone) - super - - @group_name = milestone.group.full_name - end - - def self.build_collection(groups, params) - milestones = Milestone.of_groups(groups.select(:id)) - .reorder_by_due_date_asc - .order_by_name_asc - 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 - - def merge_requests_enabled? - true - end -end diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb deleted file mode 100644 index fd59b94b737..00000000000 --- a/app/models/dashboard_milestone.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class DashboardMilestone < GlobalMilestone - attr_reader :project_name - - def initialize(milestone) - super - - @project_name = milestone.project.full_name - end - - def project_milestone? - true - end - - def merge_requests_enabled? - project.merge_requests_enabled? - end -end diff --git a/app/models/data_list.rb b/app/models/data_list.rb new file mode 100644 index 00000000000..12011cb17f7 --- /dev/null +++ b/app/models/data_list.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class DataList + def initialize(batch, data_fields_hash, klass) + @batch = batch + @data_fields_hash = data_fields_hash + @klass = klass + end + + def to_array + [klass, columns, values] + end + + private + + attr_reader :batch, :data_fields_hash, :klass + + def columns + data_fields_hash.keys << 'service_id' + end + + def values + batch.map { |row| data_fields_hash.values << row['id'] } + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index ba65acff7f3..aa3e3a8f66d 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -64,7 +64,7 @@ class Deployment < ApplicationRecord end before_transition any => [:success, :failed, :canceled] do |deployment| - deployment.finished_at = Time.now + deployment.finished_at = Time.current end after_transition any => :success do |deployment| diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb index e9b69eab7a7..0dca6333fa1 100644 --- a/app/models/design_management/design.rb +++ b/app/models/design_management/design.rb @@ -20,9 +20,11 @@ module DesignManagement has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :user_mentions, class_name: 'DesignUserMention', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + validates :project, :filename, presence: true validates :issue, presence: true, unless: :importing? - validates :filename, uniqueness: { scope: :issue_id } + validates :filename, uniqueness: { scope: :issue_id }, length: { maximum: 255 } validate :validate_file_is_image alias_attribute :title, :filename @@ -126,68 +128,23 @@ module DesignManagement # #12["filename with [] in it.jpg"] def to_reference(from = nil, full: false) infix = full ? '/designs' : '' - totally_simple = %r{ \A #{self.class.simple_file_name} \z }x - safe_name = if totally_simple.match?(filename) - filename - elsif filename =~ /[<>]/ - %Q{base64:#{Base64.strict_encode64(filename)}} - else - escaped = filename.gsub(%r{[\\"]}) { |x| "\\#{x}" } - %Q{"#{escaped}"} - end + safe_name = Sanitize.fragment(filename) "#{issue.to_reference(from, full: full)}#{infix}[#{safe_name}]" end def self.reference_pattern - @reference_pattern ||= begin - # Filenames can be escaped with double quotes to name filenames - # that include square brackets, or other special characters - %r{ - #{Issue.reference_pattern} - (\/designs)? - \[ - (?<design> #{simple_file_name} | #{quoted_file_name} | #{base_64_encoded_name}) - \] - }x - end - end - - def self.simple_file_name - %r{ - (?<simple_file_name> - ( \w | [_:,'-] | \. | \s )+ - \. - \w+ - ) - }x - end - - def self.base_64_encoded_name - %r{ - base64: - (?<base_64_encoded_name> - [A-Za-z0-9+\n]+ - =? - ) - }x - end - - def self.quoted_file_name - %r{ - " - (?<escaped_filename> - (\\ \\ | \\ " | [^"\\])+ - ) - " - }x + # no-op: We only support link_reference_pattern parsing end def self.link_reference_pattern @link_reference_pattern ||= begin - exts = SAFE_IMAGE_EXT + DANGEROUS_IMAGE_EXT path_segment = %r{issues/#{Gitlab::Regex.issue}/designs} - filename_pattern = %r{(?<simple_file_name>[a-z0-9_=-]+\.(#{exts.join('|')}))}i + ext = Regexp.new(Regexp.union(SAFE_IMAGE_EXT + DANGEROUS_IMAGE_EXT).source, Regexp::IGNORECASE) + valid_char = %r{[^/\s]} # any char that is not a forward slash or whitespace + filename_pattern = %r{ + (?<url_filename> #{valid_char}+ \. #{ext}) + }x super(path_segment, filename_pattern) end @@ -234,6 +191,11 @@ module DesignManagement alias_method :after_note_created, :after_note_changed alias_method :after_note_destroyed, :after_note_changed + # Part of the interface of objects we can create events about + def resource_parent + project + end + private def head_version diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb index 6be98fe3d44..55c9084caf2 100644 --- a/app/models/design_management/version.rb +++ b/app/models/design_management/version.rb @@ -88,7 +88,7 @@ module DesignManagement rows = design_actions.map { |action| action.row_attrs(version) } - Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) + Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert version.designs.reset version.validate! design_actions.each(&:performed) diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index ff39dbb59f3..4b2e62bf761 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -136,7 +136,7 @@ class DiffNote < Note # As an extra benefit, the returned `diff_file` already # has `highlighted_diff_lines` data set from Redis on # `Diff::FileCollection::MergeRequestDiff`. - file = noteable.diffs(original_position.diff_options).diff_files.first + file = original_position.find_diff_file_from(noteable) # if line is not found in persisted diffs, fallback and retrieve file from repository using gitaly # This is required because of https://gitlab.com/gitlab-org/gitlab/issues/42676 file = nil if file&.line_for_position(original_position).nil? && importing? diff --git a/app/models/discussion.rb b/app/models/discussion.rb index c07078c03dd..e928bb0959a 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -20,6 +20,7 @@ class Discussion :noteable_ability_name, :to_ability_name, :editable?, + :resolved_by_id, :system_note_with_references_visible_for?, :resource_parent, diff --git a/app/models/draft_note.rb b/app/models/draft_note.rb new file mode 100644 index 00000000000..febede9beba --- /dev/null +++ b/app/models/draft_note.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true +class DraftNote < ApplicationRecord + include DiffPositionableNote + include Gitlab::Utils::StrongMemoize + include Sortable + include ShaAttribute + + PUBLISH_ATTRS = %i(noteable_id noteable_type type note).freeze + DIFF_ATTRS = %i(position original_position change_position commit_id).freeze + + sha_attribute :commit_id + + # Attribute used to store quick actions changes and users referenced. + attr_accessor :commands_changes + attr_accessor :users_referenced + + # Text with quick actions filtered out + attr_accessor :rendered_note + + attr_accessor :review + + belongs_to :author, class_name: 'User' + belongs_to :merge_request + + validates :merge_request_id, presence: true + validates :author_id, presence: true, uniqueness: { scope: [:merge_request_id, :discussion_id] }, if: :discussion_id? + validates :discussion_id, allow_nil: true, format: { with: /\A\h{40}\z/ } + + scope :authored_by, ->(u) { where(author_id: u.id) } + + delegate :file_path, :file_hash, :file_identifier_hash, to: :diff_file, allow_nil: true + + def self.positions + where.not(position: nil) + .select(:position) + .map(&:position) + end + + def project + merge_request.target_project + end + + # noteable_id and noteable_type methods + # are used to generate discussion_id on Discussion.discussion_id + def noteable_id + merge_request_id + end + + def noteable + merge_request + end + + def noteable_type + "MergeRequest" + end + + def for_commit? + commit_id.present? + end + + def importing? + false + end + + def resolvable? + false + end + + def emoji_awardable? + false + end + + def on_diff? + position&.complete? + end + + def type + return 'DiffNote' if on_diff? + return 'DiscussionNote' if discussion_id.present? + + 'Note' + end + + def references + { + users: users_referenced, + commands: commands_changes + } + end + + def line_code + @line_code ||= diff_file&.line_code_for_position(original_position) + end + + def publish_params + attrs = PUBLISH_ATTRS.dup + attrs.concat(DIFF_ATTRS) if on_diff? + params = slice(*attrs) + params[:in_reply_to_discussion_id] = discussion_id if discussion_id.present? + params[:review_id] = review.id if review.present? + + params + end + + def self.preload_author(draft_notes) + ActiveRecord::Associations::Preloader.new.preload(draft_notes, { author: :status }) + end + + def diff_file + strong_memoize(:diff_file) do + file = original_position&.diff_file(project.repository) + + file&.unfold_diff_lines(original_position) + + file + end + end + + def commit + @commit ||= project.commit(commit_id) if commit_id.present? + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 21044771bbb..8dae2d760f5 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -339,7 +339,7 @@ class Environment < ApplicationRecord end def auto_stop_in - auto_stop_at - Time.now if auto_stop_at + auto_stop_at - Time.current if auto_stop_at end def auto_stop_in=(value) diff --git a/app/models/event.rb b/app/models/event.rb index 12b85697690..9c0fcbb354b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -7,36 +7,31 @@ class Event < ApplicationRecord include DeleteWithLimit include CreatedAtFilterable include Gitlab::Utils::StrongMemoize + include UsageStatistics - default_scope { reorder(nil) } - - CREATED = 1 - UPDATED = 2 - CLOSED = 3 - REOPENED = 4 - PUSHED = 5 - COMMENTED = 6 - MERGED = 7 - JOINED = 8 # User joined project - LEFT = 9 # User left project - DESTROYED = 10 - EXPIRED = 11 # User left project due to expiry + default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope ACTIONS = HashWithIndifferentAccess.new( - created: CREATED, - updated: UPDATED, - closed: CLOSED, - reopened: REOPENED, - pushed: PUSHED, - commented: COMMENTED, - merged: MERGED, - joined: JOINED, - left: LEFT, - destroyed: DESTROYED, - expired: EXPIRED + created: 1, + updated: 2, + closed: 3, + reopened: 4, + pushed: 5, + commented: 6, + merged: 7, + joined: 8, # User joined project + left: 9, # User left project + destroyed: 10, + expired: 11, # User left project due to expiry + approved: 12, + archived: 13 # Recoverable deletion ).freeze - WIKI_ACTIONS = [CREATED, UPDATED, DESTROYED].freeze + private_constant :ACTIONS + + WIKI_ACTIONS = [:created, :updated, :destroyed].freeze + + DESIGN_ACTIONS = [:created, :updated, :destroyed, :archived].freeze TARGET_TYPES = HashWithIndifferentAccess.new( issue: Issue, @@ -46,16 +41,20 @@ class Event < ApplicationRecord project: Project, snippet: Snippet, user: User, - wiki: WikiPage::Meta + wiki: WikiPage::Meta, + design: DesignManagement::Design ).freeze RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes + enum action: ACTIONS, _suffix: true + delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true + delegate :title, to: :design, prefix: true, allow_nil: true belongs_to :author, class_name: "User" belongs_to :project @@ -77,16 +76,16 @@ class Event < ApplicationRecord # Callbacks after_create :reset_project_activity after_create :set_last_repository_updated_at, if: :push_action? - after_create :track_user_interacted_projects + after_create ->(event) { UserInteractedProject.track(event) } # Scopes scope :recent, -> { reorder(id: :desc) } - scope :code_push, -> { where(action: PUSHED) } - scope :merged, -> { where(action: MERGED) } scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') } + scope :for_design, -> { where(target_type: 'DesignManagement::Design') } # Needed to implement feature flag: can be removed when feature flag is removed scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') } + scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') } scope :with_associations, -> do # We're using preload for "push_event_payload" as otherwise the association @@ -105,6 +104,13 @@ class Event < ApplicationRecord # should ensure the ID points to a valid user. validates :author_id, presence: true + validates :action_enum_value, + if: :design?, + inclusion: { + in: actions.values_at(*DESIGN_ACTIONS), + message: ->(event, _data) { "#{event.action} is not a valid design action" } + } + self.inheritance_column = 'action' class << self @@ -113,7 +119,7 @@ class Event < ApplicationRecord end def find_sti_class(action) - if action.to_i == PUSHED + if actions.fetch(action, action) == actions[:pushed] # action can be integer or symbol PushEvent else Event @@ -123,19 +129,15 @@ class Event < ApplicationRecord # Update Gitlab::ContributionsCalendar#activity_dates if this changes def contributions where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)", - Event::PUSHED, - %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED], - "Note", Event::COMMENTED) + actions[:pushed], + %w(MergeRequest Issue), [actions[:created], actions[:closed], actions[:merged]], + "Note", actions[:commented]) end def limit_recent(limit = 20, offset = nil) recent.limit(limit).offset(offset) end - def actions - ACTIONS.keys - end - def target_types TARGET_TYPES.keys end @@ -148,7 +150,9 @@ class Event < ApplicationRecord def visible_to_user?(user = nil) return false unless capability.present? - Ability.allowed?(user, capability, permission_object) + capability.all? do |rule| + Ability.allowed?(user, rule, permission_object) + end end def resource_parent @@ -159,46 +163,10 @@ class Event < ApplicationRecord target.try(:title) end - def created_action? - action == CREATED - end - def push_action? false end - def merged_action? - action == MERGED - end - - def closed_action? - action == CLOSED - end - - def reopened_action? - action == REOPENED - end - - def joined_action? - action == JOINED - end - - def left_action? - action == LEFT - end - - def expired_action? - action == EXPIRED - end - - def destroyed_action? - action == DESTROYED - end - - def commented_action? - action == COMMENTED - end - def membership_changed? joined_action? || left_action? || expired_action? end @@ -208,11 +176,11 @@ class Event < ApplicationRecord end def created_wiki_page? - wiki_page? && action == CREATED + wiki_page? && created_action? end def updated_wiki_page? - wiki_page? && action == UPDATED + wiki_page? && updated_action? end def created_target? @@ -239,6 +207,10 @@ class Event < ApplicationRecord target_type == 'WikiPage::Meta' end + def design? + target_type == 'DesignManagement::Design' + end + def milestone target if milestone? end @@ -247,6 +219,10 @@ class Event < ApplicationRecord target if issue? end + def design + target if design? + end + def merge_request target if merge_request? end @@ -266,6 +242,8 @@ class Event < ApplicationRecord def action_name if push_action? push_action_name + elsif design? + design_action_names[action.to_sym] elsif closed_action? "closed" elsif merged_action? @@ -386,34 +364,30 @@ class Event < ApplicationRecord protected - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - # - # TODO Refactor this method so we no longer need to disable the above cops - # https://gitlab.com/gitlab-org/gitlab/-/issues/216879. def capability @capability ||= begin - if push_action? || commit_note? - :download_code - elsif membership_changed? || created_project_action? - :read_project - elsif issue? || issue_note? - :read_issue - elsif merge_request? || merge_request_note? - :read_merge_request - elsif personal_snippet_note? || project_snippet_note? - :read_snippet - elsif milestone? - :read_milestone - elsif wiki_page? - :read_wiki - elsif design_note? - :read_design - end - end - end - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity + capabilities.flat_map do |ability, syms| + if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend + [ability] + else + [] + end + end + end + end + + def capabilities + { + download_code: %i[push_action? commit_note?], + read_project: %i[membership_changed? created_project_action?], + read_issue: %i[issue? issue_note?], + read_merge_request: %i[merge_request? merge_request_note?], + read_snippet: %i[personal_snippet_note? project_snippet_note?], + read_milestone: %i[milestone?], + read_wiki: %i[wiki_page?], + read_design: %i[design_note? design?] + } + end private @@ -455,11 +429,17 @@ class Event < ApplicationRecord .update_all(last_repository_updated_at: created_at) end - def track_user_interacted_projects - # Note the call to .available? is due to earlier migrations - # that would otherwise conflict with the call to .track - # (because the table does not exist yet). - UserInteractedProject.track(self) if UserInteractedProject.available? + def design_action_names + { + created: _('uploaded'), + updated: _('revised'), + destroyed: _('deleted'), + archived: _('archived') + } + end + + def action_enum_value + self.class.actions[action] end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb deleted file mode 100644 index 43de7454cb7..00000000000 --- a/app/models/global_milestone.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true -# Global Milestones are milestones that can be shared across multiple projects -class GlobalMilestone - include Milestoneish - - STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze - - attr_reader :milestone - alias_attribute :name, :title - - delegate :title, :state, :due_date, :start_date, :participants, :project, - :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title, - :timebox_id, :milestoneish_id, :resource_parent, :releases, to: :milestone - - def to_hash - { - name: title, - title: title, - group_name: group&.full_name, - project_name: project&.full_name - } - end - - def for_display - @milestone - end - - def self.build_collection(projects, params) - items = Milestone.of_projects(projects) - .reorder_by_due_date_asc - .order_by_name_asc - items = items.search_title(params[:search_title]) if params[:search_title].present? - - Milestone.filter_by_state(items, params[:state]).map { |m| new(m) } - end - - # necessary for legacy milestones - def self.build(projects, title) - milestones = Milestone.of_projects(projects).where(title: title) - return if milestones.blank? - - new(milestones.first) - end - - def self.states_count(projects, group = nil) - legacy_group_milestones_count = legacy_group_milestone_states_count(projects) - group_milestones_count = group_milestones_states_count(group) - - legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count| - legacy_group_milestones_count + group_milestones_count - end - end - - def self.group_milestones_states_count(group) - return STATE_COUNT_HASH unless group - - counts_by_state = Milestone.of_groups(group).count_by_state - - { - opened: counts_by_state['active'] || 0, - closed: counts_by_state['closed'] || 0, - all: counts_by_state.values.sum - } - end - - def self.legacy_group_milestone_states_count(projects) - return STATE_COUNT_HASH unless projects - - # We need to reorder(nil) on the projects, because the controller passes them in sorted. - relation = Milestone.of_projects(projects.reorder(nil)).count_by_state - - { - opened: relation['active'] || 0, - closed: relation['closed'] || 0, - all: relation.values.sum - } - end - - def initialize(milestone) - @milestone = milestone - end - - def active? - state == 'active' - end - - def closed? - state == 'closed' - end - - def issues - @issues ||= Issue.of_milestones(milestone).includes(:project, :assignees, :labels) - end - - def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignees, :labels) - end - - def labels - @labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title) - end - - def global_milestone? - true - end -end - -GlobalMilestone.include_if_ee('::EE::GlobalMilestone') diff --git a/app/models/group.rb b/app/models/group.rb index 04cb6b8b4da..dd7624ab420 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -325,15 +325,17 @@ class Group < Namespace def members_with_parents # Avoids an unnecessary SELECT when the group has no parents source_ids = - if parent_id + if has_parent? self_and_ancestors.reorder(nil).select(:id) else id end - GroupMember - .active_without_invites_and_requests - .where(source_id: source_ids) + group_hierarchy_members = GroupMember.active_without_invites_and_requests + .where(source_id: source_ids) + + GroupMember.from_union([group_hierarchy_members, + members_from_self_and_ancestor_group_shares]) end def members_from_self_and_ancestors_with_effective_access_level @@ -398,7 +400,7 @@ class Group < Namespace .first &.access_level - max_member_access || max_member_access_for_user_from_shared_groups(user) || GroupMember::NO_ACCESS + max_member_access || GroupMember::NO_ACCESS end def mattermost_team_params @@ -494,6 +496,11 @@ class Group < Namespace # TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904 end + def preload_shared_group_links + preloader = ActiveRecord::Associations::Preloader.new + preloader.preload(self, shared_with_group_links: [shared_with_group: :route]) + end + private def update_two_factor_requirement @@ -524,27 +531,39 @@ 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) + def members_from_self_and_ancestor_group_shares 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) + source_ids = + if has_parent? + self_and_ancestors.reorder(nil).select(:id) + else + id + end + + group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids) cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query) cte_alias = cte.table.alias(GroupGroupLink.table_name) - link = GroupGroupLink - .with(cte.to_arel) - .select(smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], - 'group_access')) - .from([group_member_table, cte.alias_to(group_group_link_table)]) - .where(group_member_table[:user_id].eq(user.id)) - .where(group_member_table[:requested_at].eq(nil)) - .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id])) - .where(group_member_table[:source_type].eq('Namespace')) - .reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access])) - .first - - link&.group_access + # Instead of members.access_level, we need to maximize that access_level at + # the respective group_group_links.group_access. + member_columns = GroupMember.attribute_names.map do |column_name| + if column_name == 'access_level' + smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], + 'access_level') + else + group_member_table[column_name] + end + end + + GroupMember + .with(cte.to_arel) + .select(*member_columns) + .from([group_member_table, cte.alias_to(group_group_link_table)]) + .where(group_member_table[:requested_at].eq(nil)) + .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id])) + .where(group_member_table[:source_type].eq('Namespace')) end def smallest_value_arel(args, column_alias) diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb new file mode 100644 index 00000000000..d1f1aa544cd --- /dev/null +++ b/app/models/group_deploy_key.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class GroupDeployKey < Key + self.table_name = 'group_deploy_keys' + + validates :user, presence: true + + def type + 'DeployKey' + end +end diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb index c233f59b1a6..fdc54ba33ab 100644 --- a/app/models/group_group_link.rb +++ b/app/models/group_group_link.rb @@ -14,6 +14,7 @@ class GroupGroupLink < ApplicationRecord presence: true scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) } + scope :public_or_visible_to_user, ->(group, user) { where(shared_group: group, shared_with_group: Group.public_or_visible_to_user(user)) } # rubocop:disable Cop/GroupPublicOrVisibleToUser def self.access_options Gitlab::Access.options_with_owner diff --git a/app/models/group_import_state.rb b/app/models/group_import_state.rb index 7773b887249..d22c1ac5550 100644 --- a/app/models/group_import_state.rb +++ b/app/models/group_import_state.rb @@ -5,7 +5,8 @@ class GroupImportState < ApplicationRecord belongs_to :group, inverse_of: :import_state - validates :group, :status, :jid, presence: true + validates :group, :status, presence: true + validates :jid, presence: true, if: -> { started? || finished? } state_machine :status, initial: :created do state :created, value: 0 @@ -31,4 +32,8 @@ class GroupImportState < ApplicationRecord state.update_column(:last_error, last_error) if last_error end end + + def in_progress? + created? || started? + end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb deleted file mode 100644 index 60e97174e50..00000000000 --- a/app/models/group_milestone.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true -# Group Milestones are milestones that can be shared among many projects within the same group -class GroupMilestone < GlobalMilestone - attr_reader :group, :milestones - - def self.build_collection(group, projects, params) - params = - { state: params[:state], search_title: params[:search_title] } - - project_milestones = Milestone.of_projects(projects) - project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present? - child_milestones = Milestone.filter_by_state(project_milestones, params[:state]) - grouped_milestones = child_milestones.group_by(&:title) - - grouped_milestones.map do |title, grouped| - new(title, grouped, group) - end - end - - def self.build(group, projects, title) - child_milestones = Milestone.of_projects(projects).where(title: title) - return if child_milestones.blank? - - new(title, child_milestones, group) - end - - def initialize(title, milestones, group) - @milestones = milestones - @group = group - end - - def milestone - @milestone ||= milestones.find { |m| m.description.present? } || milestones.first - end - - def issues_finder_params - { group_id: group.id } - end - - def legacy_group_milestone? - true - end - - def merge_requests_enabled? - true - end -end - -GroupMilestone.include_if_ee('::EE::GroupMilestone') diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index b6882701e23..21cf6bfa414 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -25,8 +25,6 @@ class InternalId < ApplicationRecord validates :usage, presence: true - REQUIRED_SCHEMA_VERSION = 20180305095250 - # Increments #last_value and saves the record # # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). @@ -63,24 +61,16 @@ class InternalId < ApplicationRecord class << self def track_greatest(subject, scope, usage, new_value, init) - return new_value unless available? - InternalIdGenerator.new(subject, scope, usage) .track_greatest(init, new_value) end def generate_next(subject, scope, usage, init) - # Shortcut if `internal_ids` table is not available (yet) - # This can be the case in other (unrelated) migration specs - return (init.call(subject) || 0) + 1 unless available? - InternalIdGenerator.new(subject, scope, usage) .generate(init) end def reset(subject, scope, usage, value) - return false unless available? - InternalIdGenerator.new(subject, scope, usage) .reset(value) end @@ -95,20 +85,6 @@ class InternalId < ApplicationRecord where(filter).delete_all end - - def available? - return true unless Rails.env.test? - - Gitlab::SafeRequestStore.fetch(:internal_ids_available_flag) do - ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION - end - end - - # Flushes cached information about schema - def reset_column_information - Gitlab::SafeRequestStore[:internal_ids_available_flag] = nil - super - end end class InternalIdGenerator diff --git a/app/models/issue.rb b/app/models/issue.rb index a04ac412940..5c5190f88b1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -59,6 +59,9 @@ class Issue < ApplicationRecord has_one :sentry_issue has_one :alert_management_alert, class_name: 'AlertManagement::Alert' + has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany + has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany + has_many :prometheus_alerts, through: :prometheus_alert_events accepts_nested_attributes_for :sentry_issue @@ -86,12 +89,14 @@ class Issue < ApplicationRecord scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) } scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) } scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) } + scope :with_alert_management_alerts, -> { joins(:alert_management_alert) } + scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) } + scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) } scope :public_only, -> { where(confidential: false) } scope :confidential_only, -> { where(confidential: true) } scope :counts_by_state, -> { reorder(nil).group(:state_id).count } - scope :with_alert_management_alerts, -> { joins(:alert_management_alert) } # An issue can be uniquely identified by project_id and iid # Takes one or more sets of composite IDs, expressed as hash-like records of @@ -139,6 +144,10 @@ class Issue < ApplicationRecord issue.closed_at = nil issue.closed_by = nil end + + after_transition any => :closed do |issue| + issue.resolve_associated_alert_management_alert + end end # Alias to state machine .with_state_id method @@ -344,10 +353,26 @@ class Issue < ApplicationRecord previous_changes['updated_at']&.first || updated_at end + def banzai_render_context(field) + super.merge(label_url_method: :project_issues_url) + end + def design_collection @design_collection ||= ::DesignManagement::DesignCollection.new(self) end + def resolve_associated_alert_management_alert + return unless alert_management_alert + return if alert_management_alert.resolve + + Gitlab::AppLogger.warn( + message: 'Cannot resolve an associated Alert Management alert', + issue_id: id, + alert_id: alert_management_alert.id, + alert_errors: alert_management_alert.errors.messages + ) + end + private def ensure_metrics diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb index d4e51dcfbca..a5e1957c096 100644 --- a/app/models/issue/metrics.rb +++ b/app/models/issue/metrics.rb @@ -11,11 +11,11 @@ class Issue::Metrics < ApplicationRecord def record! if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank? - self.first_associated_with_milestone_at = Time.now + self.first_associated_with_milestone_at = Time.current end if issue_assigned_to_list_label? && self.first_added_to_board_at.blank? - self.first_added_to_board_at = Time.now + self.first_added_to_board_at = Time.current end self.save diff --git a/app/models/iteration.rb b/app/models/iteration.rb index 1acd08f2063..2bda0725471 100644 --- a/app/models/iteration.rb +++ b/app/models/iteration.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Iteration < ApplicationRecord - include Timebox - self.table_name = 'sprints' attr_accessor :skip_future_date_validation @@ -15,9 +13,6 @@ class Iteration < ApplicationRecord include AtomicInternalId - has_many :issues, foreign_key: 'sprint_id' - has_many :merge_requests, foreign_key: 'sprint_id' - belongs_to :project belongs_to :group @@ -33,6 +28,12 @@ class Iteration < ApplicationRecord scope :upcoming, -> { with_state(:upcoming) } scope :started, -> { with_state(:started) } + scope :within_timeframe, -> (start_date, end_date) do + where('start_date is not NULL or due_date is not NULL') + .where('start_date is NULL or start_date <= ?', end_date) + .where('due_date is NULL or due_date >= ?', start_date) + end + state_machine :state_enum, initial: :upcoming do event :start do transition upcoming: :started @@ -62,6 +63,14 @@ class Iteration < ApplicationRecord else iterations.upcoming end end + + def reference_prefix + '*iteration:' + end + + def reference_pattern + nil + end end def state @@ -72,6 +81,10 @@ class Iteration < ApplicationRecord self.state_enum = STATE_ENUM_MAP[value] end + def resource_parent + group || project + end + private def start_or_due_dates_changed? @@ -98,3 +111,5 @@ class Iteration < ApplicationRecord end end end + +Iteration.prepend_if_ee('EE::Iteration') diff --git a/app/models/jira_import_state.rb b/app/models/jira_import_state.rb index 92147794e88..2d952c552a8 100644 --- a/app/models/jira_import_state.rb +++ b/app/models/jira_import_state.rb @@ -7,6 +7,7 @@ class JiraImportState < ApplicationRecord self.table_name = 'jira_imports' + ERROR_MESSAGE_SIZE = 1000 # 1000 characters limit STATUSES = { initial: 0, scheduled: 1, started: 2, failed: 3, finished: 4 }.freeze belongs_to :project @@ -14,6 +15,7 @@ class JiraImportState < ApplicationRecord belongs_to :label scope :by_jira_project_key, -> (jira_project_key) { where(jira_project_key: jira_project_key) } + scope :with_status, ->(statuses) { where(status: statuses) } validates :project, presence: true validates :jira_project_key, presence: true @@ -25,6 +27,8 @@ class JiraImportState < ApplicationRecord message: _('Cannot have multiple Jira imports running at the same time') } + before_save :ensure_error_message_size + alias_method :scheduled_by, :user state_machine :status, initial: :initial do @@ -47,7 +51,7 @@ class JiraImportState < ApplicationRecord after_transition initial: :scheduled do |state, _| state.run_after_commit do job_id = Gitlab::JiraImport::Stage::StartImportWorker.perform_async(project.id) - state.update(jid: job_id, scheduled_at: Time.now) if job_id + state.update(jid: job_id, scheduled_at: Time.current) if job_id end end @@ -65,6 +69,13 @@ class JiraImportState < ApplicationRecord end end + after_transition any => :failed do |state, transition| + arguments_hash = transition.args.first + error_message = arguments_hash&.dig(:error_message) + + state.update_column(:error_message, error_message) if error_message.present? + end + # Supress warning: # both JiraImportState and its :status machine have defined a different default for "status". # although both have same value but represented in 2 ways: integer(0) and symbol(:initial) @@ -102,4 +113,18 @@ class JiraImportState < ApplicationRecord def self.finished_imports_count finished.sum(:imported_issues_count) end + + def mark_as_failed(error_message) + sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) + + do_fail(error_message: error_message) + rescue ActiveRecord::ActiveRecordError => e + Gitlab::AppLogger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") + end + + private + + def ensure_error_message_size + self.error_message = error_message&.truncate(ERROR_MESSAGE_SIZE) + end end diff --git a/app/models/label.rb b/app/models/label.rb index 652b5e23490..910cc0d68cd 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -31,7 +31,7 @@ class Label < ApplicationRecord validates :title, uniqueness: { scope: [:group_id, :project_id] } validates :title, length: { maximum: 255 } - default_scope { order(title: :asc) } + default_scope { order(title: :asc) } # rubocop:disable Cop/DefaultScope scope :templates, -> { where(template: true, type: [Label.name, nil]) } scope :with_title, ->(title) { where(title: title) } @@ -133,7 +133,7 @@ class Label < ApplicationRecord # Searches for labels with a matching title or description. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # query - The search query as a String. # diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 6a86aebae39..3761484b15d 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -44,13 +44,13 @@ class LfsObject < ApplicationRecord file_store == LfsObjectUploader::Store::LOCAL end - # rubocop: disable DestroyAll + # rubocop: disable Cop/DestroyAll def self.destroy_unreferenced joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id") .where(lfs_objects_projects: { id: nil }) .destroy_all end - # rubocop: enable DestroyAll + # rubocop: enable Cop/DestroyAll def self.calculate_oid(path) self.hexdigest(path) diff --git a/app/models/license_template.rb b/app/models/license_template.rb index 73e403f98b4..bd24259984b 100644 --- a/app/models/license_template.rb +++ b/app/models/license_template.rb @@ -39,7 +39,7 @@ class LicenseTemplate end # Populate placeholders in the LicenseTemplate content - def resolve!(project_name: nil, fullname: nil, year: Time.now.year.to_s) + def resolve!(project_name: nil, fullname: nil, year: Time.current.year.to_s) # Ensure the string isn't shared with any other instance of LicenseTemplate new_content = content.dup new_content.gsub!(YEAR_TEMPLATE_REGEX, year) if year.present? diff --git a/app/models/member.rb b/app/models/member.rb index 791073da095..f2926d32d47 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -320,7 +320,7 @@ class Member < ApplicationRecord return false unless invite? self.invite_token = nil - self.invite_accepted_at = Time.now.utc + self.invite_accepted_at = Time.current.utc self.user = new_user diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 431a2ccf416..9a916cd40ae 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -13,12 +13,19 @@ class GroupMember < Member # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE validates :source_type, format: { with: /\ANamespace\z/ } - default_scope { where(source_type: SOURCE_TYPE) } + default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } - scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count } scope :of_ldap_type, -> { where(ldap: true) } + scope :count_users_by_group_id, -> do + if Feature.enabled?(:optimized_count_users_by_group_id) + group(:source_id).count + else + joins(:user).group(:source_id).count + end + end + after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index fa2e0cb8198..833b27756ab 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -9,7 +9,7 @@ class ProjectMember < Member default_value_for :source_type, SOURCE_TYPE validates :source_type, format: { with: /\AProject\z/ } validates :access_level, inclusion: { in: Gitlab::Access.values } - default_scope { where(source_type: SOURCE_TYPE) } + default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope scope :in_project, ->(project) { where(source_id: project.id) } scope :in_namespaces, ->(groups) do diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b4d0b729454..caf7b554427 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -88,6 +88,9 @@ class MergeRequest < ApplicationRecord has_many :deployments, through: :deployment_merge_requests + has_many :draft_notes + has_many :reviews, inverse_of: :merge_request + KNOWN_MERGE_PARAMS = [ :auto_merge_strategy, :should_remove_source_branch, @@ -101,7 +104,7 @@ class MergeRequest < ApplicationRecord after_create :ensure_merge_request_diff after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed - after_save :ensure_metrics, unless: :importing? + after_commit :ensure_metrics, on: [:create, :update], unless: :importing? after_commit :expire_etag_cache, unless: :importing? # When this attribute is true some MR validation is ignored @@ -541,13 +544,21 @@ class MergeRequest < ApplicationRecord merge_request_diffs.where.not(id: merge_request_diff.id) end - # Overwritten in EE - def note_positions_for_paths(paths, _user = nil) + def note_positions_for_paths(paths, user = nil) positions = notes.new_diff_notes.joins(:note_diff_file) .where('note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)', paths, paths) .positions - Gitlab::Diff::PositionCollection.new(positions, diff_head_sha) + collection = Gitlab::Diff::PositionCollection.new(positions, diff_head_sha) + + return collection unless user + + positions = draft_notes + .authored_by(user) + .positions + .select { |pos| paths.include?(pos.file_path) } + + collection.concat(positions) end def preloads_discussion_diff_highlighting? @@ -866,7 +877,7 @@ class MergeRequest < ApplicationRecord check_service = MergeRequests::MergeabilityCheckService.new(self) - if async && Feature.enabled?(:async_merge_request_check_mergeability, project, default_enabled: true) + if async check_service.async_execute else check_service.execute(retry_lease: false) @@ -885,11 +896,11 @@ class MergeRequest < ApplicationRecord end def merge_event - @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last + @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :merged).last end def closed_event - @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last + @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :closed).last end def work_in_progress? @@ -1158,6 +1169,7 @@ class MergeRequest < ApplicationRecord def mergeable_ci_state? return true unless project.only_allow_merge_if_pipeline_succeeds? return false unless actual_head_pipeline + return true if project.allow_merge_on_skipped_pipeline? && actual_head_pipeline.skipped? actual_head_pipeline.success? end @@ -1302,8 +1314,6 @@ class MergeRequest < ApplicationRecord end def has_accessibility_reports? - return false unless Feature.enabled?(:accessibility_report_view, project) - actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.accessibility_reports) end @@ -1568,6 +1578,10 @@ class MergeRequest < ApplicationRecord deployments.visible.includes(:environment).order(id: :desc).limit(10) end + def banzai_render_context(field) + super.merge(label_url_method: :project_merge_requests_url) + end + private def with_rebase_lock diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb index eecb10e6dbc..de97fc33f8d 100644 --- a/app/models/merge_request_context_commit.rb +++ b/app/models/merge_request_context_commit.rb @@ -20,7 +20,7 @@ class MergeRequestContextCommit < ApplicationRecord # create MergeRequestContextCommit by given commit sha and it's diff file record def self.bulk_insert(*args) - Gitlab::Database.bulk_insert('merge_request_context_commits', *args) + Gitlab::Database.bulk_insert('merge_request_context_commits', *args) # rubocop:disable Gitlab/BulkInsert end def to_commit diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb index 9dce7c53ab6..b89d1983ce3 100644 --- a/app/models/merge_request_context_commit_diff_file.rb +++ b/app/models/merge_request_context_commit_diff_file.rb @@ -12,6 +12,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord # create MergeRequestContextCommitDiffFile by given diff file record(s) def self.bulk_insert(*args) - Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) + Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index f793bd3d76f..66b27aeac91 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -437,7 +437,7 @@ class MergeRequestDiff < ApplicationRecord transaction do MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all - Gitlab::Database.bulk_insert('merge_request_diff_files', rows) + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert update!(stored_externally: false) end @@ -495,7 +495,7 @@ class MergeRequestDiff < ApplicationRecord rows = build_external_merge_request_diff_files(rows) if use_external_diff? # Faster inserts - Gitlab::Database.bulk_insert('merge_request_diff_files', rows) + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert end def build_external_diff_tempfile(rows) diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 2819ea7ce1e..9f6933d0879 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -27,6 +27,6 @@ class MergeRequestDiffCommit < ApplicationRecord ) end - Gitlab::Database.bulk_insert(self.table_name, rows) + Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert end end diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb index 8166880f0c9..3383dda20c9 100644 --- a/app/models/metrics/dashboard/annotation.rb +++ b/app/models/metrics/dashboard/annotation.rb @@ -3,6 +3,8 @@ module Metrics module Dashboard class Annotation < ApplicationRecord + include DeleteWithLimit + self.table_name = 'metrics_dashboard_annotations' belongs_to :environment, inverse_of: :metrics_dashboard_annotations @@ -14,14 +16,25 @@ module Metrics validates :panel_xid, length: { maximum: 255 } validate :single_ownership validate :orphaned_annotation + validate :ending_at_after_starting_at scope :after, ->(after) { where('starting_at >= ?', after) } scope :before, ->(before) { where('starting_at <= ?', before) } scope :for_dashboard, ->(dashboard_path) { where(dashboard_path: dashboard_path) } + scope :ending_before, ->(timestamp) { where('COALESCE(ending_at, starting_at) < ?', timestamp) } private + # If annotation has NULL in ending_at column that indicates, that this annotation IS TIED TO SINGLE POINT + # IN TIME designated by starting_at timestamp. It does NOT mean that annotation is ever going starting from + # stating_at timestamp + def ending_at_after_starting_at + return if ending_at.blank? || starting_at.blank? || starting_at <= ending_at + + errors.add(:ending_at, s_("Metrics::Dashboard::Annotation|can't be before starting_at time")) + end + def single_ownership return if cluster.nil? ^ environment.nil? diff --git a/app/models/milestone.rb b/app/models/milestone.rb index b5e4f62792e..58adfd5f70b 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -2,7 +2,6 @@ class Milestone < ApplicationRecord include Sortable - include Referable include Timebox include Milestoneish include FromUnion @@ -29,6 +28,7 @@ class Milestone < ApplicationRecord scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) } + scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) } validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } @@ -122,35 +122,6 @@ class Milestone < ApplicationRecord } end - ## - # Returns the String necessary to reference a Milestone in Markdown. Group - # milestones only support name references, and do not support cross-project - # references. - # - # format - Symbol format to use (default: :iid, optional: :name) - # - # Examples: - # - # Milestone.first.to_reference # => "%1" - # Milestone.first.to_reference(format: :name) # => "%\"goal\"" - # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1" - # Milestone.first.to_reference(same_namespace_project) # => "gitlab-foss%1" - # - def to_reference(from = nil, format: :name, full: false) - format_reference = milestone_format_reference(format) - reference = "#{self.class.reference_prefix}#{format_reference}" - - if project - "#{project.to_reference_base(from, full: full)}#{reference}" - else - reference - end - end - - def reference_link_text(from = nil) - self.class.reference_prefix + self.title - end - def for_display self end @@ -179,22 +150,12 @@ class Milestone < ApplicationRecord end end - private - - def milestone_format_reference(format = :iid) - raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format) - - if group_milestone? && format == :iid - raise ArgumentError, _('Cannot refer to a group milestone by an internal id!') - end - - if format == :name && !name.include?('"') - %("#{name}") - else - iid - end + def subgroup_milestone? + group_milestone? && parent.subgroup? end + private + def issues_finder_params { project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 8116f7a256f..90b4be7a674 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -100,11 +100,11 @@ class Namespace < ApplicationRecord # Searches for namespaces matching the given query. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # - # query - The search query as a String + # query - The search query as a String. # - # Returns an ActiveRecord::Relation + # Returns an ActiveRecord::Relation. def search(query) fuzzy_search(query, [:name, :path]) end @@ -277,7 +277,7 @@ class Namespace < ApplicationRecord end def has_parent? - parent.present? + parent_id.present? || parent.present? end def root_ancestor diff --git a/app/models/note.rb b/app/models/note.rb index d174ba8fe83..6b6a7c50b00 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -72,6 +72,7 @@ class Note < ApplicationRecord belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' + belongs_to :review, inverse_of: :notes has_many :todos @@ -273,6 +274,10 @@ class Note < ApplicationRecord noteable_type == "Snippet" end + def for_alert_mangement_alert? + noteable_type == 'AlertManagement::Alert' + end + def for_personal_snippet? noteable.is_a?(PersonalSnippet) end @@ -350,8 +355,10 @@ class Note < ApplicationRecord self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR end - def confidential? - confidential || noteable.try(:confidential?) + def confidential?(include_noteable: false) + return true if confidential + + include_noteable && noteable.try(:confidential?) end def editable? @@ -393,7 +400,13 @@ class Note < ApplicationRecord end def noteable_ability_name - for_snippet? ? 'snippet' : noteable_type.demodulize.underscore + if for_snippet? + 'snippet' + elsif for_alert_mangement_alert? + 'alert_management_alert' + else + noteable_type.demodulize.underscore + end end def can_be_discussion_note? @@ -520,7 +533,7 @@ class Note < ApplicationRecord end def banzai_render_context(field) - super.merge(noteable: noteable, system_note: system?) + super.merge(noteable: noteable, system_note: system?, label_url_method: noteable_label_url_method) end def retrieve_upload(_identifier, paths) @@ -603,6 +616,10 @@ class Note < ApplicationRecord errors.add(:base, _('Maximum number of comments exceeded')) if noteable.notes.count >= Noteable::MAX_NOTES_LIMIT end + + def noteable_label_url_method + for_merge_request? ? :project_merge_requests_url : :project_issues_url + end end Note.prepend_if_ee('EE::Note') diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index da5e4012f05..856496f0941 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -49,11 +49,11 @@ class PagesDomain < ApplicationRecord after_update :update_daemon, if: :saved_change_to_pages_config? after_destroy :update_daemon - scope :enabled, -> { where('enabled_until >= ?', Time.now ) } + scope :enabled, -> { where('enabled_until >= ?', Time.current ) } scope :needs_verification, -> do verified_at = arel_table[:verified_at] enabled_until = arel_table[:enabled_until] - threshold = Time.now + VERIFICATION_THRESHOLD + threshold = Time.current + VERIFICATION_THRESHOLD where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold)))) end @@ -69,7 +69,7 @@ class PagesDomain < ApplicationRecord from_union([user_provided, certificate_not_valid, certificate_expiring]) end - scope :for_removal, -> { where("remove_at < ?", Time.now) } + scope :for_removal, -> { where("remove_at < ?", Time.current) } scope :with_logging_info, -> { includes(project: [:namespace, :route]) } @@ -141,7 +141,7 @@ class PagesDomain < ApplicationRecord def expired? return false unless x509 - current = Time.new + current = Time.current current < x509.not_before || x509.not_after < current end diff --git a/app/models/pages_domain_acme_order.rb b/app/models/pages_domain_acme_order.rb index 63d7fbc8206..411456cc237 100644 --- a/app/models/pages_domain_acme_order.rb +++ b/app/models/pages_domain_acme_order.rb @@ -3,7 +3,7 @@ class PagesDomainAcmeOrder < ApplicationRecord belongs_to :pages_domain - scope :expired, -> { where("expires_at < ?", Time.now) } + scope :expired, -> { where("expires_at < ?", Time.current) } validates :pages_domain, presence: true validates :expires_at, presence: true diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb index 57222c61b36..b04e7e689cd 100644 --- a/app/models/performance_monitoring/prometheus_dashboard.rb +++ b/app/models/performance_monitoring/prometheus_dashboard.rb @@ -4,30 +4,38 @@ module PerformanceMonitoring class PrometheusDashboard include ActiveModel::Model - attr_accessor :dashboard, :panel_groups, :path, :environment, :priority, :templating + attr_accessor :dashboard, :panel_groups, :path, :environment, :priority, :templating, :links validates :dashboard, presence: true validates :panel_groups, presence: true class << self def from_json(json_content) - dashboard = new( - dashboard: json_content['dashboard'], - panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) } - ) - - dashboard.tap(&:validate!) + build_from_hash(json_content).tap(&:validate!) end def find_for(project:, user:, path:, options: {}) - dashboard_response = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path)) - return unless dashboard_response[:status] == :success + template = { path: path, environment: options[:environment] } + rsp = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path)) + + case rsp[:http_status] || rsp[:status] + when :success + new(template.merge(rsp[:dashboard] || {})) # when there is empty dashboard file returned rsp is still a success + when :unprocessable_entity + new(template) # validation error + else + nil # any other error + end + end + + private + + def build_from_hash(attributes) + return new unless attributes.is_a?(Hash) new( - { - path: path, - environment: options[:environment] - }.merge(dashboard_response[:dashboard]) + dashboard: attributes['dashboard'], + panel_groups: attributes['panel_groups']&.map { |group| PrometheusPanelGroup.from_json(group) } ) end end @@ -36,6 +44,15 @@ module PerformanceMonitoring self.as_json(only: yaml_valid_attributes).to_yaml end + # This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398 + # implementation. For new existing logic was reused to faster deliver MVC + def schema_validation_warnings + self.class.from_json(self.as_json) + nil + rescue ActiveModel::ValidationError => exception + exception.model.errors.map { |attr, error| "#{attr}: #{error}" } + end + private def yaml_valid_attributes diff --git a/app/models/performance_monitoring/prometheus_metric.rb b/app/models/performance_monitoring/prometheus_metric.rb index 7b8bef906fa..d67b1809d93 100644 --- a/app/models/performance_monitoring/prometheus_metric.rb +++ b/app/models/performance_monitoring/prometheus_metric.rb @@ -10,16 +10,24 @@ module PerformanceMonitoring validates :query, presence: true, unless: :query_range validates :query_range, presence: true, unless: :query - def self.from_json(json_content) - metric = PrometheusMetric.new( - id: json_content['id'], - unit: json_content['unit'], - label: json_content['label'], - query: json_content['query'], - query_range: json_content['query_range'] - ) + class << self + def from_json(json_content) + build_from_hash(json_content).tap(&:validate!) + end - metric.tap(&:validate!) + private + + def build_from_hash(attributes) + return new unless attributes.is_a?(Hash) + + new( + id: attributes['id'], + unit: attributes['unit'], + label: attributes['label'], + query: attributes['query'], + query_range: attributes['query_range'] + ) + end end end end diff --git a/app/models/performance_monitoring/prometheus_panel.rb b/app/models/performance_monitoring/prometheus_panel.rb index 3fe029abda0..a16a68ba832 100644 --- a/app/models/performance_monitoring/prometheus_panel.rb +++ b/app/models/performance_monitoring/prometheus_panel.rb @@ -8,17 +8,24 @@ module PerformanceMonitoring validates :title, presence: true validates :metrics, presence: true + class << self + def from_json(json_content) + build_from_hash(json_content).tap(&:validate!) + end - def self.from_json(json_content) - panel = new( - type: json_content['type'], - title: json_content['title'], - y_label: json_content['y_label'], - weight: json_content['weight'], - metrics: json_content['metrics'].map { |metric| PrometheusMetric.from_json(metric) } - ) + private - panel.tap(&:validate!) + def build_from_hash(attributes) + return new unless attributes.is_a?(Hash) + + new( + type: attributes['type'], + title: attributes['title'], + y_label: attributes['y_label'], + weight: attributes['weight'], + metrics: attributes['metrics']&.map { |metric| PrometheusMetric.from_json(metric) } + ) + end end def id(group_title) diff --git a/app/models/performance_monitoring/prometheus_panel_group.rb b/app/models/performance_monitoring/prometheus_panel_group.rb index e672545fce3..f88106f259b 100644 --- a/app/models/performance_monitoring/prometheus_panel_group.rb +++ b/app/models/performance_monitoring/prometheus_panel_group.rb @@ -8,15 +8,22 @@ module PerformanceMonitoring validates :group, presence: true validates :panels, presence: true + class << self + def from_json(json_content) + build_from_hash(json_content).tap(&:validate!) + end - def self.from_json(json_content) - panel_group = new( - group: json_content['group'], - priority: json_content['priority'], - panels: json_content['panels'].map { |panel| PrometheusPanel.from_json(panel) } - ) + private - panel_group.tap(&:validate!) + def build_from_hash(attributes) + return new unless attributes.is_a?(Hash) + + new( + group: attributes['group'], + priority: attributes['priority'], + panels: attributes['panels']&.map { |panel| PrometheusPanel.from_json(panel) } + ) + end end end end diff --git a/app/models/project.rb b/app/models/project.rb index c0dd2eb8584..845e9e83e78 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -33,6 +33,7 @@ class Project < ApplicationRecord include OptionallySearch include FromUnion include IgnorableColumns + include Integration extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -95,8 +96,7 @@ class Project < ApplicationRecord after_create :create_project_feature, unless: :project_feature after_create :create_ci_cd_settings, - unless: :ci_cd_settings, - if: proc { ProjectCiCdSetting.available? } + unless: :ci_cd_settings after_create :create_container_expiration_policy, unless: :container_expiration_policy @@ -198,7 +198,7 @@ class Project < ApplicationRecord has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting' has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting' has_one :grafana_integration, inverse_of: :project - has_one :project_setting, ->(project) { where_or_create_by(project: project) }, inverse_of: :project + has_one :project_setting, inverse_of: :project, autosave: true has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting' # Merge Requests for target project should be removed with it @@ -282,7 +282,7 @@ class Project < ApplicationRecord class_name: 'Ci::Pipeline', inverse_of: :project has_many :stages, class_name: 'Ci::Stage', inverse_of: :project - has_many :ci_refs, class_name: 'Ci::Ref' + has_many :ci_refs, class_name: 'Ci::Ref', inverse_of: :project # Ci::Build objects store data on the file system such as artifact files and # build traces. Currently there's no efficient way of removing this data in @@ -291,6 +291,7 @@ class Project < ApplicationRecord has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName' has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks + has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project has_many :job_artifacts, class_name: 'Ci::JobArtifact' has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' @@ -328,6 +329,9 @@ class Project < ApplicationRecord has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove' + has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project + has_many :reviews, inverse_of: :project + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_setting, update_only: true @@ -368,9 +372,11 @@ class Project < ApplicationRecord delegate :root_ancestor, to: :namespace, allow_nil: true delegate :last_pipeline, to: :commit, allow_nil: true delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true + delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true + delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?, :allow_merge_on_skipped_pipeline=, to: :project_setting # Validations validates :creator, presence: true, on: :create @@ -442,7 +448,7 @@ class Project < ApplicationRecord scope :archived, -> { where(archived: true) } scope :non_archived, -> { where(archived: false) } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } - scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } + scope :with_push, -> { joins(:events).merge(Event.pushed_action) } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :inc_routes, -> { includes(:route, namespace: :route) } scope :with_statistics, -> { includes(:statistics) } @@ -507,6 +513,11 @@ class Project < ApplicationRecord .where(project_pages_metadata: { project_id: nil }) end + scope :with_api_entity_associations, -> { + preload(:project_feature, :route, :tags, + group: :ip_restrictions, namespace: [:route, :owner]) + } + enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } chronic_duration_attr :build_timeout_human_readable, :build_timeout, @@ -521,6 +532,10 @@ class Project < ApplicationRecord # Used by Projects::CleanupService to hold a map of rewritten object IDs mount_uploader :bfg_object_map, AttachmentUploader + def self.with_web_entity_associations + preload(:project_feature, :route, :creator, :group, namespace: [:route, :owner]) + end + def self.eager_load_namespace_and_owner includes(namespace: :owner) end @@ -602,8 +617,7 @@ class Project < ApplicationRecord # Searches for a list of projects based on the query given in `query`. # # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive - # search. On MySQL a regular "LIKE" is used as it's already - # case-insensitive. + # search. # # query - The search query as a String. def search(query, include_namespace: false) @@ -713,6 +727,10 @@ class Project < ApplicationRecord super end + def project_setting + super.presence || build_project_setting + end + def all_pipelines if builds_enabled? super @@ -729,6 +747,10 @@ class Project < ApplicationRecord end end + def active_webide_pipelines(user:) + webide_pipelines.running_or_pending.for_user(user) + end + def autoclose_referenced_issues return true if super.nil? @@ -798,10 +820,6 @@ class Project < ApplicationRecord Feature.enabled?(:context_commits, default_enabled: true) end - def jira_issues_import_feature_flag_enabled? - Feature.enabled?(:jira_issue_import, self, default_enabled: true) - end - # LFS and hashed repository storage are required for using Design Management. def design_management_enabled? lfs_enabled? && hashed_storage?(:repository) @@ -889,18 +907,6 @@ class Project < ApplicationRecord latest_jira_import&.status || 'initial' end - def validate_jira_import_settings!(user: nil) - raise Projects::ImportService::Error, _('Jira import feature is disabled.') unless jira_issues_import_feature_flag_enabled? - raise Projects::ImportService::Error, _('Jira integration not configured.') unless jira_service&.active? - - if user - raise Projects::ImportService::Error, _('Cannot import because issues are not available in this project.') unless feature_available?(:issues, user) - raise Projects::ImportService::Error, _('You do not have permissions to run the import.') unless user.can?(:admin_project, self) - end - - raise Projects::ImportService::Error, _('Unable to connect to the Jira instance. Please check your Jira integration configuration.') unless jira_service.test(nil)[:success] - end - def human_import_status_name import_state&.human_status_name || 'none' end @@ -921,17 +927,15 @@ class Project < ApplicationRecord job_id end - # rubocop:disable Gitlab/RailsLogger def log_import_activity(job_id, type: :import) job_type = type.to_s.capitalize if job_id - Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.") + Gitlab::AppLogger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.") else - Rails.logger.error("#{job_type} job failed to create for #{full_path}.") + Gitlab::AppLogger.error("#{job_type} job failed to create for #{full_path}.") end end - # rubocop:enable Gitlab/RailsLogger def reset_cache_and_import_attrs run_after_commit do @@ -1007,7 +1011,7 @@ class Project < ApplicationRecord end def jira_import? - import_type == 'jira' && latest_jira_import.present? && jira_issues_import_feature_flag_enabled? + import_type == 'jira' && latest_jira_import.present? end def gitlab_project_import? @@ -1036,7 +1040,7 @@ class Project < ApplicationRecord remote_mirrors.stuck.update_all( update_status: :failed, last_error: _('The remote mirror took to long to complete.'), - last_update_at: Time.now + last_update_at: Time.current ) end @@ -1194,14 +1198,6 @@ class Project < ApplicationRecord get_issue(issue_id) end - def default_issue_tracker - gitlab_issue_tracker_service || create_gitlab_issue_tracker_service - end - - def issues_tracker - external_issue_tracker || default_issue_tracker - end - def external_issue_reference_pattern external_issue_tracker.class.reference_pattern(only_long: issues_enabled?) end @@ -1257,7 +1253,7 @@ class Project < ApplicationRecord available_services_names.map do |service_name| find_or_initialize_service(service_name) - end + end.sort_by(&:title) end def disabled_services @@ -1267,16 +1263,7 @@ class Project < ApplicationRecord def find_or_initialize_service(name) return if disabled_services.include?(name) - service = find_service(services, name) - return service if service - - template = find_service(services_templates, name) - - if template - Service.build_from_template(id, template) - else - public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend - end + find_service(services, name) || build_from_instance_or_template(name) || public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend end # rubocop: disable CodeReuse/ServiceClass @@ -1781,17 +1768,15 @@ class Project < ApplicationRecord ensure_pages_metadatum.update!(deployed: false) end - # rubocop:disable Gitlab/RailsLogger def write_repository_config(gl_full_path: full_path) # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using # the import rake task. repository.raw_repository.write_config(full_path: gl_full_path) rescue Gitlab::Git::Repository::NoRepository => e - Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") + Gitlab::AppLogger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") nil end - # rubocop:enable Gitlab/RailsLogger def after_import repository.expire_content_cache @@ -1834,17 +1819,15 @@ class Project < ApplicationRecord @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self) end - # rubocop:disable Gitlab/RailsLogger def add_export_job(current_user:, after_export_strategy: nil, params: {}) job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params) if job_id - Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}" + Gitlab::AppLogger.info "Export job started for project ID #{self.id} with job ID #{job_id}" else - Rails.logger.error "Export job failed to start for project ID #{self.id}" + Gitlab::AppLogger.error "Export job failed to start for project ID #{self.id}" end end - # rubocop:enable Gitlab/RailsLogger def import_export_shared @import_export_shared ||= Gitlab::ImportExport::Shared.new(self) @@ -2082,21 +2065,6 @@ class Project < ApplicationRecord end end - def change_repository_storage(new_repository_storage_key) - return if repository_read_only? - return if repository_storage == new_repository_storage_key - - raise ArgumentError unless ::Gitlab.config.repositories.storages.key?(new_repository_storage_key) - - storage_move = repository_storage_moves.create!( - source_storage_name: repository_storage, - destination_storage_name: new_repository_storage_key - ) - storage_move.schedule! - - self.repository_read_only = true - end - def pushes_since_gc Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } end @@ -2438,12 +2406,32 @@ class Project < ApplicationRecord touch(:last_activity_at, :last_repository_updated_at) end + def metrics_setting + super || build_metrics_setting + end + private def find_service(services, name) services.find { |service| service.to_param == name } end + def build_from_instance_or_template(name) + instance = find_service(services_instances, name) + return Service.build_from_integration(id, instance) if instance + + template = find_service(services_templates, name) + return Service.build_from_integration(id, template) if template + end + + def services_templates + @services_templates ||= Service.templates + end + + def services_instances + @services_instances ||= Service.instances + end + def closest_namespace_setting(name) namespace.closest_setting(name) end @@ -2572,10 +2560,6 @@ class Project < ApplicationRecord end end - def services_templates - @services_templates ||= Service.where(template: true) - end - def ensure_pages_metadatum pages_metadatum || create_pages_metadatum! rescue ActiveRecord::RecordNotUnique diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index c295837002a..e5fc481b035 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -3,9 +3,6 @@ class ProjectCiCdSetting < ApplicationRecord belongs_to :project, inverse_of: :ci_cd_settings - # The version of the schema that first introduced this model/table. - MINIMUM_SCHEMA_VERSION = 20180403035759 - DEFAULT_GIT_DEPTH = 50 before_create :set_default_git_depth @@ -20,16 +17,6 @@ class ProjectCiCdSetting < ApplicationRecord default_value_for :forward_deployment_enabled, true - def self.available? - @available ||= - ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION - end - - def self.reset_column_information - @available = nil - super - end - def forward_deployment_enabled? super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true) end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 9201cd24d66..b3ebcbd4b17 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -1,51 +1,16 @@ # frozen_string_literal: true class ProjectFeature < ApplicationRecord - # == Project features permissions - # - # Grants access level to project tools - # - # Tools can be enabled only for users, everyone or disabled - # Access control is made only for non private projects - # - # levels: - # - # Disabled: not enabled for anyone - # Private: enabled only for team members - # Enabled: enabled for everyone able to access the project - # Public: enabled for everyone (only allowed for pages) - # - - # Permission levels - DISABLED = 0 - PRIVATE = 10 - ENABLED = 20 - PUBLIC = 30 + include Featurable FEATURES = %i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard).freeze + + set_available_features(FEATURES) + PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER }.freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze - STRING_OPTIONS = HashWithIndifferentAccess.new({ - 'disabled' => DISABLED, - 'private' => PRIVATE, - 'enabled' => ENABLED, - 'public' => PUBLIC - }).freeze class << self - def access_level_attribute(feature) - feature = ensure_feature!(feature) - - "#{feature}_access_level".to_sym - end - - def quoted_access_level_column(feature) - attribute = connection.quote_column_name(access_level_attribute(feature)) - table = connection.quote_table_name(table_name) - - "#{table}.#{attribute}" - end - def required_minimum_access_level(feature) feature = ensure_feature!(feature) @@ -60,24 +25,6 @@ class ProjectFeature < ApplicationRecord required_minimum_access_level(feature) end end - - def access_level_from_str(level) - STRING_OPTIONS.fetch(level) - end - - def str_from_access_level(level) - STRING_OPTIONS.key(level) - end - - private - - def ensure_feature!(feature) - feature = feature.model_name.plural if feature.respond_to?(:model_name) - feature = feature.to_sym - raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature) - - feature - end end # Default scopes force us to unscope here since a service may need to check @@ -107,45 +54,6 @@ class ProjectFeature < ApplicationRecord end end - def feature_available?(feature, user) - # This feature might not be behind a feature flag at all, so default to true - return false unless ::Feature.enabled?(feature, user, default_enabled: true) - - get_permission(user, feature) - end - - def access_level(feature) - public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend - end - - def string_access_level(feature) - ProjectFeature.str_from_access_level(access_level(feature)) - end - - def builds_enabled? - builds_access_level > DISABLED - end - - def wiki_enabled? - wiki_access_level > DISABLED - end - - def merge_requests_enabled? - merge_requests_access_level > DISABLED - end - - def forking_enabled? - forking_access_level > DISABLED - end - - def issues_enabled? - issues_access_level > DISABLED - end - - def pages_enabled? - pages_access_level > DISABLED - end - def public_pages? return true unless Gitlab.config.pages.access_control @@ -164,7 +72,7 @@ class ProjectFeature < ApplicationRecord # which cannot be higher than repository access level def repository_children_level validator = lambda do |field| - level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend + level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend not_allowed = level > repository_access_level self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed end @@ -175,8 +83,8 @@ class ProjectFeature < ApplicationRecord # Validates access level for other than pages cannot be PUBLIC def allowed_access_levels validator = lambda do |field| - level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend - not_allowed = level > ProjectFeature::ENABLED + level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend + not_allowed = level > ENABLED self.errors.add(field, "cannot have public visibility level") if not_allowed end diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index f1c491d1a05..f065246e8af 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -15,8 +15,6 @@ class ProjectGroupLink < ApplicationRecord scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) } - after_commit :refresh_group_members_authorized_projects - alias_method :shared_with_group, :group def self.access_options @@ -49,10 +47,6 @@ class ProjectGroupLink < ApplicationRecord errors.add(:base, _("Project cannot be shared with the group it is in or one of its ancestors.")) end end - - def refresh_group_members_authorized_projects - group.refresh_members_authorized_projects - end end ProjectGroupLink.prepend_if_ee('EE::ProjectGroupLink') diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb index e434ea58729..4bd3ffbea2f 100644 --- a/app/models/project_import_state.rb +++ b/app/models/project_import_state.rb @@ -84,7 +84,11 @@ class ProjectImportState < ApplicationRecord update_column(:last_error, sanitized_message) rescue ActiveRecord::ActiveRecordError => e - Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::Import::Logger.error( + message: 'Error setting import status to failed', + error: e.message, + original_error: sanitized_message + ) ensure @errors = original_errors end diff --git a/app/models/project_metrics_setting.rb b/app/models/project_metrics_setting.rb index a2a7dc571a4..c66d0f52f4c 100644 --- a/app/models/project_metrics_setting.rb +++ b/app/models/project_metrics_setting.rb @@ -4,6 +4,13 @@ class ProjectMetricsSetting < ApplicationRecord belongs_to :project validates :external_dashboard_url, + allow_nil: true, length: { maximum: 255 }, addressable_url: { enforce_sanitization: true, ascii_only: true } + + enum dashboard_timezone: { local: 0, utc: 1 } + + def dashboard_timezone=(val) + super(val&.downcase) + end end diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb index e88cc5cfca6..b18d9765a57 100644 --- a/app/models/project_repository_storage_move.rb +++ b/app/models/project_repository_storage_move.rb @@ -18,6 +18,7 @@ class ProjectRepositoryStorageMove < ApplicationRecord on: :create, presence: true, inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } } + validate :project_repository_writable, on: :create state_machine initial: :initial do event :schedule do @@ -36,7 +37,9 @@ class ProjectRepositoryStorageMove < ApplicationRecord transition [:initial, :scheduled, :started] => :failed end - after_transition initial: :scheduled do |storage_move, _| + after_transition initial: :scheduled do |storage_move| + storage_move.project.update_column(:repository_read_only, true) + storage_move.run_after_commit do ProjectUpdateRepositoryStorageWorker.perform_async( storage_move.project_id, @@ -46,6 +49,17 @@ class ProjectRepositoryStorageMove < ApplicationRecord end end + after_transition started: :finished do |storage_move| + storage_move.project.update_columns( + repository_read_only: false, + repository_storage: storage_move.destination_storage_name + ) + end + + after_transition started: :failed do |storage_move| + storage_move.project.update_column(:repository_read_only, false) + end + state :initial, value: 1 state :scheduled, value: 2 state :started, value: 3 @@ -55,4 +69,10 @@ class ProjectRepositoryStorageMove < ApplicationRecord scope :order_created_at_desc, -> { order(created_at: :desc) } scope :with_projects, -> { includes(project: :route) } + + private + + def project_repository_writable + errors.add(:project, _('is read only')) if project&.repository_read_only? + end end diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb index 16bf37fd189..58c47accfd1 100644 --- a/app/models/project_services/alerts_service.rb +++ b/app/models/project_services/alerts_service.rb @@ -41,7 +41,7 @@ class AlertsService < Service end def description - _('Receive alerts on GitLab from any source') + _('Authorize external services to send alerts to GitLab') end def detailed_description diff --git a/app/models/project_services/chat_message/alert_message.rb b/app/models/project_services/chat_message/alert_message.rb new file mode 100644 index 00000000000..c8913775843 --- /dev/null +++ b/app/models/project_services/chat_message/alert_message.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module ChatMessage + class AlertMessage < BaseMessage + attr_reader :title + attr_reader :alert_url + attr_reader :severity + attr_reader :events + attr_reader :status + attr_reader :started_at + + def initialize(params) + @project_name = params[:project_name] || params.dig(:project, :path_with_namespace) + @project_url = params.dig(:project, :web_url) || params[:project_url] + @title = params.dig(:object_attributes, :title) + @alert_url = params.dig(:object_attributes, :url) + @severity = params.dig(:object_attributes, :severity) + @events = params.dig(:object_attributes, :events) + @status = params.dig(:object_attributes, :status) + @started_at = params.dig(:object_attributes, :started_at) + end + + def attachments + [{ + title: title, + title_link: alert_url, + color: attachment_color, + fields: attachment_fields + }] + end + + def message + "Alert firing in #{project_name}" + end + + private + + def attachment_color + "#C95823" + end + + def attachment_fields + [ + { + title: "Severity", + value: severity.to_s.humanize, + short: true + }, + { + title: "Events", + value: events, + short: true + }, + { + title: "Status", + value: status.to_s.humanize, + short: true + }, + { + title: "Start time", + value: format_time(started_at), + short: true + } + ] + end + + # This formats time into the following format + # April 23rd, 2020 1:06AM UTC + def format_time(time) + time = Time.zone.parse(time.to_s) + time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z") + end + end +end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index 0a2d9120adc..c4fcdff8386 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -48,7 +48,7 @@ module ChatMessage end def merge_request_message - "#{user_combined_name} #{state_or_action_text} #{merge_request_link} in #{project_link}" + "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}" end def merge_request_link diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 1cd3837433f..f4c6938fa78 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -183,7 +183,7 @@ module ChatMessage if ref_type == 'tag' "#{project_url}/-/tags/#{ref}" else - "#{project_url}/commits/#{ref}" + "#{project_url}/-/commits/#{ref}" end end @@ -200,14 +200,14 @@ module ChatMessage end def pipeline_failed_jobs_url - "#{project_url}/pipelines/#{pipeline_id}/failures" + "#{project_url}/-/pipelines/#{pipeline_id}/failures" end def pipeline_url if failed_jobs.any? pipeline_failed_jobs_url else - "#{project_url}/pipelines/#{pipeline_id}" + "#{project_url}/-/pipelines/#{pipeline_id}" end end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index c92e8ecb31c..ad531412fb7 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -252,8 +252,8 @@ class HipchatService < Service status = pipeline_attributes[:status] duration = pipeline_attributes[:duration] - branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>" - pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>" + branch_link = "<a href=\"#{project_url}/-/commits/#{CGI.escape(ref)}\">#{ref}</a>" + pipeline_url = "<a href=\"#{project_url}/-/pipelines/#{pipeline_id}\">##{pipeline_id}</a>" "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 53da874ede8..bb4d35cad22 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -6,6 +6,8 @@ class JiraService < IssueTrackerService include ApplicationHelper include ActionView::Helpers::AssetUrlHelper + PROJECTS_PER_PAGE = 50 + validates :url, public_url: true, presence: true, if: :activated? validates :api_url, public_url: true, allow_blank: true validates :username, presence: true, if: :activated? @@ -201,6 +203,10 @@ class JiraService < IssueTrackerService add_comment(data, jira_issue) end + def valid_connection? + test(nil)[:success] + end + def test(_) result = test_settings success = result.present? @@ -209,11 +215,6 @@ class JiraService < IssueTrackerService { success: success, result: result } end - # Jira does not need test data. - def test_data(_, _) - nil - end - override :support_close_issue? def support_close_issue? true @@ -413,17 +414,9 @@ class JiraService < IssueTrackerService # Handle errors when doing Jira API calls def jira_request yield - rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error + rescue => error @error = error - log_error( - "Error sending message", - client_url: client_url, - error: { - exception_class: error.class.name, - exception_message: error.message, - exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace) - } - ) + log_error("Error sending message", client_url: client_url, error: @error.message) nil end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index a58a264de5e..c11b2f7cc65 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -56,12 +56,6 @@ class PipelinesEmailService < Service project&.ci_pipelines&.any? end - def test_data(project, user) - data = Gitlab::DataBuilder::Pipeline.build(project.ci_pipelines.last) - data[:user] = user.hook_attrs - data - end - def fields [ { type: 'textarea', diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 4a28d1ff2b0..44a41969b1c 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -5,6 +5,8 @@ class PrometheusService < MonitoringService # Access to prometheus is directly through the API prop_accessor :api_url + prop_accessor :google_iap_service_account_json + prop_accessor :google_iap_audience_client_id boolean_accessor :manual_configuration # We need to allow the self-monitoring project to connect to the internal @@ -49,7 +51,7 @@ class PrometheusService < MonitoringService end def fields - [ + result = [ { type: 'checkbox', name: 'manual_configuration', @@ -64,6 +66,28 @@ class PrometheusService < MonitoringService required: true } ] + + if Feature.enabled?(:prometheus_service_iap_auth) + result += [ + { + type: 'text', + name: 'google_iap_audience_client_id', + title: 'Google IAP Audience Client ID', + placeholder: s_('PrometheusService|Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)'), + autocomplete: 'off', + required: false + }, + { + type: 'textarea', + name: 'google_iap_service_account_json', + title: 'Google IAP Service Account JSON', + placeholder: s_('PrometheusService|Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }'), + required: false + } + ] + end + + result end # Check we can connect to the Prometheus API @@ -77,7 +101,14 @@ class PrometheusService < MonitoringService def prometheus_client return unless should_return_client? - Gitlab::PrometheusClient.new(api_url, allow_local_requests: allow_local_api_url?) + options = { allow_local_requests: allow_local_api_url? } + + if Feature.enabled?(:prometheus_service_iap_auth) && behind_iap? + # Adds the Authorization header + options[:headers] = iap_client.apply({}) + end + + Gitlab::PrometheusClient.new(api_url, options) end def prometheus_available? @@ -149,4 +180,12 @@ class PrometheusService < MonitoringService Prometheus::CreateDefaultAlertsWorker.perform_async(project_id) end + + def behind_iap? + manual_configuration? && google_iap_audience_client_id.present? && google_iap_service_account_json.present? + end + + def iap_client + @iap_client ||= Google::Auth::Credentials.new(Gitlab::Json.parse(google_iap_service_account_json), target_audience: google_iap_audience_client_id).client + end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 6d567bb1383..79245e84238 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class SlackService < ChatNotificationService + prop_accessor EVENT_CHANNEL['alert'] + def title 'Slack notifications' end @@ -21,13 +23,25 @@ class SlackService < ChatNotificationService 'https://hooks.slack.com/services/…' end + def supported_events + additional = [] + additional << 'alert' + + super + additional + end + + def get_message(object_kind, data) + return ChatMessage::AlertMessage.new(data) if object_kind == 'alert' + + super + end + module Notifier private def notify(message, opts) # See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient)) - notifier.ping( message.pretext, attachments: message.attachments, diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 7c93faf3928..9022d3e879d 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -4,10 +4,6 @@ class ProjectSetting < ApplicationRecord belongs_to :project, inverse_of: :project_setting self.primary_key = :project_id - - def self.where_or_create_by(attrs) - where(primary_key => safe_find_or_create_by(attrs)) - end end ProjectSetting.prepend_if_ee('EE::ProjectSetting') diff --git a/app/models/prometheus_alert_event.rb b/app/models/prometheus_alert_event.rb index 7e61f6d5e3c..25f58a0b9d5 100644 --- a/app/models/prometheus_alert_event.rb +++ b/app/models/prometheus_alert_event.rb @@ -34,10 +34,4 @@ class PrometheusAlertEvent < ApplicationRecord def self.status_value_for(name) state_machines[:status].states[name].value end - - def self.payload_key_for(gitlab_alert_id, started_at) - plain = [gitlab_alert_id, started_at].join('/') - - Digest::SHA1.hexdigest(plain) - end end diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 5cab686f20b..0f626cb618e 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -68,7 +68,7 @@ class PushEvent < Event end def self.sti_name - PUSHED + actions[:pushed] end def push_action? @@ -111,7 +111,7 @@ class PushEvent < Event end def validate_push_action - return if action == PUSHED + return if pushed_action? errors.add(:action, "the action #{action.inspect} is not valid") end diff --git a/app/models/releases/evidence.rb b/app/models/releases/evidence.rb index 1aac7e33e41..7c428f5ad03 100644 --- a/app/models/releases/evidence.rb +++ b/app/models/releases/evidence.rb @@ -1,44 +1,35 @@ # frozen_string_literal: true -class Releases::Evidence < ApplicationRecord - include ShaAttribute - include Presentable +module Releases + class Evidence < ApplicationRecord + include ShaAttribute + include Presentable - belongs_to :release, inverse_of: :evidences + belongs_to :release, inverse_of: :evidences - before_validation :generate_summary_and_sha + default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope - default_scope { order(created_at: :asc) } + sha_attribute :summary_sha + alias_attribute :collected_at, :created_at + alias_attribute :sha, :summary_sha - sha_attribute :summary_sha - alias_attribute :collected_at, :created_at - - def milestones - @milestones ||= release.milestones.includes(:issues) - end - - ## - # Return `summary` without sensitive information. - # - # Removing issues from summary in order to prevent leaking confidential ones. - # See more https://gitlab.com/gitlab-org/gitlab/issues/121930 - def summary - safe_summary = read_attribute(:summary) - - safe_summary.dig('release', 'milestones')&.each do |milestone| - milestone.delete('issues') + def milestones + @milestones ||= release.milestones.includes(:issues) end - safe_summary - end - - private + ## + # Return `summary` without sensitive information. + # + # Removing issues from summary in order to prevent leaking confidential ones. + # See more https://gitlab.com/gitlab-org/gitlab/issues/121930 + def summary + safe_summary = read_attribute(:summary) - def generate_summary_and_sha - summary = Evidences::EvidenceSerializer.new.represent(self) # rubocop: disable CodeReuse/Serializer - return unless summary + safe_summary.dig('release', 'milestones')&.each do |milestone| + milestone.delete('issues') + end - self.summary = summary - self.summary_sha = Gitlab::CryptoHelper.sha256(summary) + safe_summary + end end end diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb index 65be2a22958..dc7e78a85a9 100644 --- a/app/models/releases/link.rb +++ b/app/models/releases/link.rb @@ -14,6 +14,13 @@ module Releases scope :sorted, -> { order(created_at: :desc) } + enum link_type: { + other: 0, + runbook: 1, + package: 2, + image: 3 + } + def internal? url.start_with?(release.project.web_url) end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 8e7612e63c8..8b15d481c1b 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -68,13 +68,13 @@ class RemoteMirror < ApplicationRecord after_transition any => :started do |remote_mirror, _| Gitlab::Metrics.add_event(:remote_mirrors_running) - remote_mirror.update(last_update_started_at: Time.now) + remote_mirror.update(last_update_started_at: Time.current) end after_transition started: :finished do |remote_mirror, _| Gitlab::Metrics.add_event(:remote_mirrors_finished) - timestamp = Time.now + timestamp = Time.current remote_mirror.update!( last_update_at: timestamp, last_successful_update_at: timestamp, @@ -86,7 +86,7 @@ class RemoteMirror < ApplicationRecord after_transition started: :failed do |remote_mirror| Gitlab::Metrics.add_event(:remote_mirrors_failed) - remote_mirror.update(last_update_at: Time.now) + remote_mirror.update(last_update_at: Time.current) remote_mirror.run_after_commit do RemoteMirrorNotificationWorker.perform_async(remote_mirror.id) @@ -144,9 +144,9 @@ class RemoteMirror < ApplicationRecord return unless sync? if recently_scheduled? - RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.now) + RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.current) else - RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.now) + RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.current) end end @@ -261,7 +261,7 @@ class RemoteMirror < ApplicationRecord def recently_scheduled? return false unless self.last_update_started_at - self.last_update_started_at >= Time.now - backoff_delay + self.last_update_started_at >= Time.current - backoff_delay end def reset_fields diff --git a/app/models/repository.rb b/app/models/repository.rb index 2673033ff1f..911cfc7db38 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -950,7 +950,6 @@ class Repository async_remove_remote(remote_name) if tmp_remote_name end - # rubocop:disable Gitlab/RailsLogger def async_remove_remote(remote_name) return unless remote_name return unless project @@ -958,14 +957,13 @@ class Repository job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name) if job_id - Rails.logger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.") + Gitlab::AppLogger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.") else - Rails.logger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.") + Gitlab::AppLogger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.") end job_id end - # rubocop:enable Gitlab/RailsLogger def fetch_source_branch!(source_repository, source_branch, local_ref) raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref) @@ -1171,7 +1169,7 @@ class Repository if target target.committed_date else - Time.now + Time.current end end end diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb index e468d716239..6b1793a551f 100644 --- a/app/models/repository_language.rb +++ b/app/models/repository_language.rb @@ -4,7 +4,7 @@ class RepositoryLanguage < ApplicationRecord belongs_to :project belongs_to :programming_language - default_scope { includes(:programming_language) } + default_scope { includes(:programming_language) } # rubocop:disable Cop/DefaultScope validates :project, presence: true validates :share, inclusion: { in: 0..100, message: "The share of a language is between 0 and 100" } diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index 845be408d5e..cc96698be09 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -71,6 +71,14 @@ class ResourceLabelEvent < ResourceEvent end end + def self.visible_to_user?(user, events) + ResourceLabelEvent.preload_label_subjects(events) + + events.select do |event| + Ability.allowed?(user, :read_label, event) + end + end + private def label_reference diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb index 039f26d8e3f..36068cf508b 100644 --- a/app/models/resource_milestone_event.rb +++ b/app/models/resource_milestone_event.rb @@ -9,6 +9,8 @@ class ResourceMilestoneEvent < ResourceEvent validate :exactly_one_issuable + scope :include_relations, -> { includes(:user, milestone: [:project, :group]) } + enum action: { add: 1, remove: 2 @@ -26,4 +28,12 @@ class ResourceMilestoneEvent < ResourceEvent def milestone_title milestone&.title end + + def milestone_parent + milestone&.parent + end + + def issuable + issue || merge_request + end end diff --git a/app/models/review.rb b/app/models/review.rb new file mode 100644 index 00000000000..5a30e2963c8 --- /dev/null +++ b/app/models/review.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Review < ApplicationRecord + include Participable + include Mentionable + + belongs_to :author, class_name: 'User', foreign_key: :author_id, inverse_of: :reviews + belongs_to :merge_request, inverse_of: :reviews + belongs_to :project, inverse_of: :reviews + + has_many :notes, -> { order(:id) }, inverse_of: :review + + delegate :name, to: :author, prefix: true + + participant :author + + def all_references(current_user = nil, extractor: nil) + ext = super + + notes.each do |note| + note.all_references(current_user, extractor: ext) + end + + ext + end + + def user_mentions + merge_request.user_mentions.where.not(note_id: nil) + end +end diff --git a/app/models/route.rb b/app/models/route.rb index 63a0461807b..706589e79b8 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -42,7 +42,7 @@ class Route < ApplicationRecord old_path = route.path # Callbacks must be run manually - route.update_columns(attributes.merge(updated_at: Time.now)) + route.update_columns(attributes.merge(updated_at: Time.current)) # We are not calling route.delete_conflicting_redirects here, in hopes # of avoiding deadlocks. The parent (self, in this method) already diff --git a/app/models/self_managed_prometheus_alert_event.rb b/app/models/self_managed_prometheus_alert_event.rb index d2d4a5c37d4..cf26563e92d 100644 --- a/app/models/self_managed_prometheus_alert_event.rb +++ b/app/models/self_managed_prometheus_alert_event.rb @@ -15,10 +15,4 @@ class SelfManagedPrometheusAlertEvent < ApplicationRecord yield event if block_given? end end - - def self.payload_key_for(started_at, alert_name, query_expression) - plain = [started_at, alert_name, query_expression].join('/') - - Digest::SHA1.hexdigest(plain) - end end diff --git a/app/models/service.rb b/app/models/service.rb index fb4d9a77077..2880526c9de 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -22,6 +22,7 @@ class Service < ApplicationRecord serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize default_value_for :active, false + default_value_for :alert_events, true default_value_for :push_events, true default_value_for :issues_events, true default_value_for :confidential_issues_events, true @@ -72,6 +73,7 @@ class Service < ApplicationRecord scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :deployment_hooks, -> { where(deployment_events: true, active: true) } + scope :alert_hooks, -> { where(alert_events: true, active: true) } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } scope :deployment, -> { where(category: 'deployment') } @@ -134,8 +136,12 @@ class Service < ApplicationRecord %w(active) end - def test_data(project, user) - Gitlab::DataBuilder::Push.build_sample(project, user) + def to_service_hash + as_json(methods: :type, except: %w[id template instance project_id]) + end + + def to_data_fields_hash + data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id') end def event_channel_names @@ -164,7 +170,7 @@ class Service < ApplicationRecord end def configurable_events - events = self.class.supported_events + events = supported_events # No need to disable individual triggers when there is only one if events.count == 1 @@ -335,17 +341,19 @@ class Service < ApplicationRecord services_names.map { |service_name| "#{service_name}_service".camelize } end - def self.build_from_template(project_id, template) - service = template.dup + def self.build_from_integration(project_id, integration) + service = integration.dup - if template.supports_data_fields? - data_fields = template.data_fields.dup + if integration.supports_data_fields? + data_fields = integration.data_fields.dup data_fields.service = service end service.template = false + service.instance = false + service.inherit_from_id = integration.id if integration.instance? service.project_id = project_id - service.active = false if service.active? && service.invalid? + service.active = false if service.invalid? service end @@ -394,6 +402,8 @@ class Service < ApplicationRecord "Event will be triggered when a commit is created/updated" when "deployment" "Event will be triggered when a deployment finishes" + when "alert" + "Event will be triggered when a new, unique alert is recorded" end end diff --git a/app/models/service_list.rb b/app/models/service_list.rb new file mode 100644 index 00000000000..fa3760f0c56 --- /dev/null +++ b/app/models/service_list.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ServiceList + def initialize(batch, service_hash, extra_hash = {}) + @batch = batch + @service_hash = service_hash + @extra_hash = extra_hash + end + + def to_array + [Service, columns, values] + end + + private + + attr_reader :batch, :service_hash, :extra_hash + + def columns + (service_hash.keys << 'project_id') + extra_hash.keys + end + + def values + batch.map do |project_id| + (service_hash.values << project_id) + extra_hash.values + end + end +end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 72ebdf61787..b63ab003711 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -337,7 +337,7 @@ class Snippet < ApplicationRecord class << self # Searches for snippets with a matching title, description or file name. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # query - The search query as a String. # diff --git a/app/models/snippet_input_action.rb b/app/models/snippet_input_action.rb new file mode 100644 index 00000000000..7f4ab775ab0 --- /dev/null +++ b/app/models/snippet_input_action.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class SnippetInputAction + include ActiveModel::Validations + + ACTIONS = %i[create update delete move].freeze + + ACTIONS.each do |action_const| + define_method "#{action_const}_action?" do + action == action_const + end + end + + attr_reader :action, :previous_path, :file_path, :content + + validates :action, inclusion: { in: ACTIONS, message: "%{value} is not a valid action" } + validates :previous_path, presence: true, if: :move_action? + validates :file_path, presence: true + validates :content, presence: true, if: -> (action) { action.create_action? || action.update_action? } + validate :ensure_same_file_path_and_previous_path, if: :update_action? + validate :ensure_allowed_action + + def initialize(action: nil, previous_path: nil, file_path: nil, content: nil, allowed_actions: nil) + @action = action&.to_sym + @previous_path = previous_path + @file_path = file_path + @content = content + @allowed_actions = Array(allowed_actions).map(&:to_sym) + end + + def to_commit_action + { + action: action, + previous_path: build_previous_path, + file_path: file_path, + content: content + } + end + + private + + def build_previous_path + return previous_path unless update_action? + + previous_path.presence || file_path + end + + def ensure_same_file_path_and_previous_path + return if previous_path.blank? || file_path.blank? + return if previous_path == file_path + + errors.add(:file_path, "can't be different from the previous_path attribute") + end + + def ensure_allowed_action + return if @allowed_actions.empty? + + unless @allowed_actions.include?(action) + errors.add(:action, 'is not allowed') + end + end +end diff --git a/app/models/snippet_input_action_collection.rb b/app/models/snippet_input_action_collection.rb new file mode 100644 index 00000000000..38313e3a980 --- /dev/null +++ b/app/models/snippet_input_action_collection.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class SnippetInputActionCollection + include Gitlab::Utils::StrongMemoize + + attr_reader :actions + + delegate :empty?, :any?, :[], to: :actions + + def initialize(actions = [], allowed_actions: nil) + @actions = actions.map { |action| SnippetInputAction.new(action.merge(allowed_actions: allowed_actions)) } + end + + def to_commit_actions + strong_memoize(:commit_actions) do + actions.map { |action| action.to_commit_action } + end + end + + def valid? + strong_memoize(:valid) do + actions.all?(&:valid?) + end + end +end diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb index 72690ad7d04..7e34988c7a0 100644 --- a/app/models/ssh_host_key.rb +++ b/app/models/ssh_host_key.rb @@ -107,7 +107,7 @@ class SshHostKey if status.success? && !errors.present? { known_hosts: known_hosts } else - Rails.logger.debug("Failed to detect SSH host keys for #{id}: #{errors}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.debug("Failed to detect SSH host keys for #{id}: #{errors}") { error: 'Failed to detect SSH host keys' } end diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 345172cca76..f643d52587e 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -35,7 +35,7 @@ module Storage gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki") return true rescue => e - Rails.logger.error "Exception renaming #{old_full_path} -> #{new_full_path}: #{e}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Exception renaming #{old_full_path} -> #{new_full_path}: #{e}") # Returning false does not rollback after_* transaction but gives # us information about failing some of tasks return false diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index c4e047ff9d1..6ed074b2190 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -2,6 +2,8 @@ module Terraform class State < ApplicationRecord + include UsageStatistics + DEFAULT = '{"version":1}'.freeze HEX_REGEXP = %r{\A\h+\z}.freeze UUID_LENGTH = 32 diff --git a/app/models/todo.rb b/app/models/todo.rb index dc42551f0ab..102f36a991e 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -66,6 +66,8 @@ class Todo < ApplicationRecord scope :with_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) } scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) } + enum resolved_by_action: { system_done: 0, api_all_done: 1, api_done: 2, mark_all_done: 3, mark_done: 4 }, _prefix: :resolved_by + state_machine :state, initial: :pending do event :done do transition [:pending] => :done @@ -100,17 +102,17 @@ class Todo < ApplicationRecord state.nil? ? exists?(target: target) : exists?(target: target, state: state) end - # Updates the state of a relation of todos to the new state. + # Updates attributes of a relation of todos to the new state. # - # new_state - The new state of the todos. + # new_attributes - The new attributes of the todos. # # Returns an `Array` containing the IDs of the updated todos. - def update_state(new_state) - # Only update those that are not really on that state - base = where.not(state: new_state).except(:order) + def batch_update(**new_attributes) + # Only update those that have different state + base = where.not(state: new_attributes[:state]).except(:order) ids = base.pluck(:id) - base.update_all(state: new_state, updated_at: Time.now) + base.update_all(new_attributes.merge(updated_at: Time.current)) ids end @@ -187,6 +189,10 @@ class Todo < ApplicationRecord target_type == DesignManagement::Design.name end + def for_alert? + target_type == AlertManagement::Alert.name + end + # override to return commits, which are not active record def target if for_commit? diff --git a/app/models/uploads/base.rb b/app/models/uploads/base.rb index 442ed733566..7555c72e101 100644 --- a/app/models/uploads/base.rb +++ b/app/models/uploads/base.rb @@ -7,7 +7,7 @@ module Uploads attr_reader :logger def initialize(logger: nil) - @logger = Rails.logger # rubocop:disable Gitlab/RailsLogger + @logger = Gitlab::AppLogger end def delete_keys_async(keys_to_delete) diff --git a/app/models/user.rb b/app/models/user.rb index 927ffa4d12b..431a5b3a5b7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -69,7 +69,6 @@ class User < ApplicationRecord MINIMUM_INACTIVE_DAYS = 180 - ignore_column :bot_type, remove_with: '13.1', remove_after: '2020-05-22' ignore_column :ghost, remove_with: '13.2', remove_after: '2020-06-22' # Override Devise::Models::Trackable#update_tracked_fields! @@ -181,6 +180,8 @@ class User < ApplicationRecord has_one :user_highest_role has_one :user_canonical_email + has_many :reviews, foreign_key: :author_id, inverse_of: :author + # # Validations # @@ -264,18 +265,21 @@ class User < ApplicationRecord # User's role enum role: { software_developer: 0, development_team_lead: 1, devops_engineer: 2, systems_administrator: 3, security_analyst: 4, data_analyst: 5, product_manager: 6, product_designer: 7, other: 8 }, _suffix: true + delegate :notes_filter_for, + :set_notes_filter, + :first_day_of_week, :first_day_of_week=, + :timezone, :timezone=, + :time_display_relative, :time_display_relative=, + :time_format_in_24h, :time_format_in_24h=, + :show_whitespace_in_diffs, :show_whitespace_in_diffs=, + :tab_width, :tab_width=, + :sourcegraph_enabled, :sourcegraph_enabled=, + :setup_for_company, :setup_for_company=, + :render_whitespace_in_code, :render_whitespace_in_code=, + :experience_level, :experience_level=, + to: :user_preference + delegate :path, to: :namespace, allow_nil: true, prefix: true - delegate :notes_filter_for, to: :user_preference - delegate :set_notes_filter, to: :user_preference - delegate :first_day_of_week, :first_day_of_week=, to: :user_preference - delegate :timezone, :timezone=, to: :user_preference - 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 :tab_width, :tab_width=, to: :user_preference - delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference - delegate :setup_for_company, :setup_for_company=, to: :user_preference - delegate :render_whitespace_in_code, :render_whitespace_in_code=, to: :user_preference delegate :job_title, :job_title=, to: :user_detail, allow_nil: true accepts_nested_attributes_for :user_preference, update_only: true @@ -342,6 +346,7 @@ class User < ApplicationRecord where('EXISTS (?)', ::PersonalAccessToken .where('personal_access_tokens.user_id = users.id') + .without_impersonation .expiring_and_not_notified(at).select(1)) end scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } @@ -517,7 +522,7 @@ class User < ApplicationRecord # Searches users matching the given query. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # query - The search query as a String # @@ -560,7 +565,7 @@ class User < ApplicationRecord # searches user by given pattern # it compares name, email, username fields and user's secondary emails with given pattern - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. def search_with_secondary_emails(query) return none if query.blank? @@ -689,7 +694,7 @@ class User < ApplicationRecord @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token) self.reset_password_token = enc - self.reset_password_sent_at = Time.now.utc + self.reset_password_sent_at = Time.current.utc @reset_token end @@ -716,7 +721,7 @@ class User < ApplicationRecord otp_grace_period_started_at: nil, otp_backup_codes: nil ) - self.u2f_registrations.destroy_all # rubocop: disable DestroyAll + self.u2f_registrations.destroy_all # rubocop: disable Cop/DestroyAll end end @@ -957,11 +962,11 @@ class User < ApplicationRecord end def allow_password_authentication_for_web? - Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user? && !ultraauth_user? + Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user? end def allow_password_authentication_for_git? - Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user? && !ultraauth_user? + Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user? end def can_change_username? @@ -1049,14 +1054,6 @@ class User < ApplicationRecord end end - def ultraauth_user? - if identities.loaded? - identities.find { |identity| Gitlab::Auth::OAuth::Provider.ultraauth_provider?(identity.provider) && !identity.extern_uid.nil? } - else - identities.exists?(["provider = ? AND extern_uid IS NOT NULL", "ultraauth"]) - end - end - def ldap_identity @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"]) end @@ -1129,7 +1126,7 @@ class User < ApplicationRecord if !Gitlab.config.ldap.enabled false elsif ldap_user? - !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.now + !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current else false end @@ -1378,7 +1375,7 @@ class User < ApplicationRecord def contributed_projects events = Event.select(:project_id) .contributions.where(author_id: self) - .where("created_at > ?", Time.now - 1.year) + .where("created_at > ?", Time.current - 1.year) .distinct .reorder(nil) @@ -1642,16 +1639,12 @@ class User < ApplicationRecord super.presence || build_user_detail end - def todos_limited_to(ids) - todos.where(id: ids) - end - def pending_todo_for(target) todos.find_by(target: target, state: :pending) end def password_expired? - !!(password_expires_at && password_expires_at < Time.now) + !!(password_expires_at && password_expires_at < Time.current) end def can_be_deactivated? @@ -1832,7 +1825,7 @@ class User < ApplicationRecord def update_highest_role? return false unless persisted? - (previous_changes.keys & %w(state user_type ghost)).any? + (previous_changes.keys & %w(state user_type)).any? end def update_highest_role_attribute diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb index f6f72f4b77a..1c615777018 100644 --- a/app/models/user_interacted_project.rb +++ b/app/models/user_interacted_project.rb @@ -9,9 +9,6 @@ class UserInteractedProject < ApplicationRecord CACHE_EXPIRY_TIME = 1.day - # Schema version required for this model - REQUIRED_SCHEMA_VERSION = 20180223120443 - class << self def track(event) # For events without a project, we simply don't care. @@ -38,17 +35,6 @@ class UserInteractedProject < ApplicationRecord end end - # Check if we can safely call .track (table exists) - def available? - @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization - end - - # Flushes cached information about schema - def reset_column_information - @available_flag = nil - super - end - private def cached_exists?(project_id:, user_id:, &block) diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 48a56cded0e..d3b3a46bf74 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -15,6 +15,8 @@ class UserPreference < ApplicationRecord less_than_or_equal_to: Gitlab::TabWidth::MAX } + enum experience_level: { novice: 0, experienced: 1 } + default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false default_value_for :time_display_relative, value: true, allows_nil: false diff --git a/app/models/web_ide_terminal.rb b/app/models/web_ide_terminal.rb new file mode 100644 index 00000000000..ef70df2405f --- /dev/null +++ b/app/models/web_ide_terminal.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class WebIdeTerminal + include ::Gitlab::Routing + + attr_reader :build, :project + + delegate :id, :status, to: :build + + def initialize(build) + @build = build + @project = build.project + end + + def show_path + web_ide_terminal_route_generator(:show) + end + + def retry_path + web_ide_terminal_route_generator(:retry) + end + + def cancel_path + web_ide_terminal_route_generator(:cancel) + end + + def terminal_path + terminal_project_job_path(project, build, format: :ws) + end + + def proxy_websocket_path + proxy_project_job_path(project, build, format: :ws) + end + + def services + build.services.map(&:alias).compact + Array(build.image&.alias) + end + + private + + def web_ide_terminal_route_generator(action, options = {}) + options.reverse_merge!(action: action, + controller: 'projects/web_ide_terminals', + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: build.id, + only_path: true) + + url_for(options) + end +end diff --git a/app/models/wiki.rb b/app/models/wiki.rb index 54bcec32095..4c497cc304c 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -205,7 +205,7 @@ class Wiki end def wiki_base_path - Gitlab.config.gitlab.relative_url_root + web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '') + web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '') end private diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb index 712ba79bbd2..df2fe25b08b 100644 --- a/app/models/wiki_directory.rb +++ b/app/models/wiki_directory.rb @@ -15,6 +15,6 @@ class WikiDirectory # Relative path to the partial to be used when rendering collections # of this object. def to_partial_path - 'projects/wikis/wiki_directory' + '../shared/wikis/wiki_directory' end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 319cdd38d93..9e4e2f68d38 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -261,8 +261,7 @@ class WikiPage # Relative path to the partial to be used when rendering collections # of this object. def to_partial_path - # TODO: Move into shared/ with https://gitlab.com/gitlab-org/gitlab/-/issues/196054 - 'projects/wikis/wiki_page' + '../shared/wikis/wiki_page' end def id @@ -271,7 +270,10 @@ class WikiPage def title_changed? if persisted? - old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(page.url_path)) + # A page's `title` will be returned from Gollum/Gitaly with any +<> + # characters changed to -, whereas the `path` preserves these characters. + path_without_extension = Pathname(page.path).sub_ext('').to_s + old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(path_without_extension)) new_title, new_dir = wiki.page_title_and_dir(self.class.unhyphenize(title)) new_title != old_title || (title.include?('/') && new_dir != old_dir) diff --git a/app/models/wiki_page/meta.rb b/app/models/wiki_page/meta.rb index 474968122b1..215d84dc463 100644 --- a/app/models/wiki_page/meta.rb +++ b/app/models/wiki_page/meta.rb @@ -120,7 +120,7 @@ class WikiPage end def insert_slugs(strings, is_new, canonical_slug) - creation = Time.now.utc + creation = Time.current.utc slug_attrs = strings.map do |slug| { diff --git a/app/models/wiki_page/slug.rb b/app/models/wiki_page/slug.rb index 246fa8d6442..c1725d34921 100644 --- a/app/models/wiki_page/slug.rb +++ b/app/models/wiki_page/slug.rb @@ -16,11 +16,11 @@ class WikiPage scope :canonical, -> { where(canonical: true) } def update_columns(attrs = {}) - super(attrs.reverse_merge(updated_at: Time.now.utc)) + super(attrs.reverse_merge(updated_at: Time.current.utc)) end def self.update_all(attrs = {}) - super(attrs.reverse_merge(updated_at: Time.now.utc)) + super(attrs.reverse_merge(updated_at: Time.current.utc)) end end end |