diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 13:54:15 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 13:54:15 +0000 |
commit | be834a25982746ffd85252ff502df42bb88cb9d5 (patch) | |
tree | b4d6a8ba0931e12fac08f05abea33a3b8ec2c8a2 /app/services | |
parent | ee925a3597f27e92f83a50937a64068109675b3d (diff) | |
download | gitlab-ce-6e86631ace2b2f57c5e24fc472c890ff4aa7f751.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc32
Diffstat (limited to 'app/services')
86 files changed, 1659 insertions, 370 deletions
diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb index 34d6008cb6a..80e27c21d5b 100644 --- a/app/services/admin/propagate_integration_service.rb +++ b/app/services/admin/propagate_integration_service.rb @@ -14,59 +14,19 @@ module Admin private # rubocop: disable Cop/InBatches - # rubocop: disable CodeReuse/ActiveRecord def update_inherited_integrations - Service.where(type: integration.type, inherit_from_id: integration.id).in_batches(of: BATCH_SIZE) do |batch| - bulk_update_from_integration(batch) + Service.by_type(integration.type).inherit_from_id(integration.id).in_batches(of: BATCH_SIZE) do |services| + min_id, max_id = services.pick("MIN(services.id), MAX(services.id)") + PropagateIntegrationInheritWorker.perform_async(integration.id, min_id, max_id) end end # rubocop: enable Cop/InBatches - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def bulk_update_from_integration(batch) - # Retrieving the IDs instantiates the ActiveRecord relation (batch) - # into concrete models, otherwise update_all will clear the relation. - # https://stackoverflow.com/q/34811646/462015 - batch_ids = batch.pluck(:id) - - Service.transaction do - batch.update_all(service_hash) - - if data_fields_present? - integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash) - end - end - end - # rubocop: enable CodeReuse/ActiveRecord def create_integration_for_groups_without_integration - loop do - batch = Group.uncached { group_ids_without_integration(integration, BATCH_SIZE) } - - bulk_create_from_integration(batch, 'group') unless batch.empty? - - break if batch.size < BATCH_SIZE + Group.without_integration(integration).each_batch(of: BATCH_SIZE) do |groups| + min_id, max_id = groups.pick("MIN(namespaces.id), MAX(namespaces.id)") + PropagateIntegrationGroupWorker.perform_async(integration.id, min_id, max_id) end end - - def service_hash - @service_hash ||= integration.to_service_hash - .tap { |json| json['inherit_from_id'] = integration.id } - end - - # rubocop:disable CodeReuse/ActiveRecord - def group_ids_without_integration(integration, limit) - services = Service - .select('1') - .where('services.group_id = namespaces.id') - .where(type: integration.type) - - Group - .where('NOT EXISTS (?)', services) - .limit(limit) - .pluck(:id) - end - # rubocop:enable CodeReuse/ActiveRecord end end diff --git a/app/services/admin/propagate_service_template.rb b/app/services/admin/propagate_service_template.rb index cd0d2d5d03f..07be3c1027d 100644 --- a/app/services/admin/propagate_service_template.rb +++ b/app/services/admin/propagate_service_template.rb @@ -9,11 +9,5 @@ module Admin create_integration_for_projects_without_integration end - - private - - def service_hash - @service_hash ||= integration.to_service_hash - end end end diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb index 95ae84a85a4..5c7698f724a 100644 --- a/app/services/alert_management/process_prometheus_alert_service.rb +++ b/app/services/alert_management/process_prometheus_alert_service.rb @@ -47,7 +47,7 @@ module AlertManagement def create_alert_management_alert if alert.save alert.execute_services - SystemNoteService.create_new_alert(alert, Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus]) + SystemNoteService.create_new_alert(alert, Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]) return end diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb index d7630dbdac9..3c21844ec62 100644 --- a/app/services/audit_event_service.rb +++ b/app/services/audit_event_service.rb @@ -53,7 +53,6 @@ class AuditEventService private - attr_accessor :authentication_event attr_reader :ip_address def build_author(author) @@ -99,23 +98,35 @@ class AuditEventService end def mark_as_authentication_event! - self.authentication_event = true + @authentication_event = true end def authentication_event? - authentication_event + @authentication_event end def log_security_event_to_database return if Gitlab::Database.read_only? - AuditEvent.create(base_payload.merge(details: @details)) + event = AuditEvent.new(base_payload.merge(details: @details)) + save_or_track event + + event end def log_authentication_event_to_database return unless Gitlab::Database.read_write? && authentication_event? - AuthenticationEvent.create(authentication_event_payload) + event = AuthenticationEvent.new(authentication_event_payload) + save_or_track event + + event + end + + def save_or_track(event) + event.save! + rescue => e + Gitlab::ErrorTracking.track_exception(e, audit_event_type: event.class.to_s) end end diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 1a5dc790c41..2ccaea64d14 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -3,7 +3,11 @@ module Boards class CreateService < Boards::BaseService def execute - create_board! if can_create_board? + unless can_create_board? + return ServiceResponse.error(message: "You don't have the permission to create a board for this resource.") + end + + create_board! end private @@ -15,12 +19,16 @@ module Boards def create_board! board = parent.boards.create(params) - if board.persisted? - board.lists.create(list_type: :backlog) - board.lists.create(list_type: :closed) + unless board.persisted? + return ServiceResponse.error(message: "There was an error when creating a board.", payload: board) + end + + board.tap do |created_board| + created_board.lists.create(list_type: :backlog) + created_board.lists.create(list_type: :closed) end - board + ServiceResponse.success(payload: board) end end end diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb index e20805d0405..ebac0f07fe1 100644 --- a/app/services/boards/lists/destroy_service.rb +++ b/app/services/boards/lists/destroy_service.rb @@ -4,7 +4,9 @@ module Boards module Lists class DestroyService < Boards::BaseService def execute(list) - return false unless list.destroyable? + unless list.destroyable? + return ServiceResponse.error(message: "The list cannot be destroyed. Only label lists can be destroyed.") + end @board = list.board @@ -12,6 +14,8 @@ module Boards decrement_higher_lists(list) remove_list(list) end + + ServiceResponse.success end private @@ -26,7 +30,7 @@ module Boards # rubocop: enable CodeReuse/ActiveRecord def remove_list(list) - list.destroy + list.destroy! end end end diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb new file mode 100644 index 00000000000..23b89b0d8a9 --- /dev/null +++ b/app/services/bulk_create_integration_service.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class BulkCreateIntegrationService + def initialize(integration, batch, association) + @integration = integration + @batch = batch + @association = association + end + + def execute + service_list = ServiceList.new(batch, service_hash, association).to_array + + Service.transaction do + results = bulk_insert(*service_list) + + if integration.data_fields_present? + data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array + + bulk_insert(*data_list) + end + + run_callbacks(batch) if association == 'project' + end + end + + private + + attr_reader :integration, :batch, :association + + def bulk_insert(klass, columns, values_array) + items_to_insert = values_array.map { |array| Hash[columns.zip(array)] } + + klass.insert_all(items_to_insert, returning: [:id]) + end + + # rubocop: disable CodeReuse/ActiveRecord + def run_callbacks(batch) + if integration.issue_tracker? + Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true) + end + + if integration.type == 'ExternalWikiService' + Project.where(id: batch.select(:id)).update_all(has_external_wiki: true) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def service_hash + if integration.template? + integration.to_service_hash + else + integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id } + end + end + + def data_fields_hash + integration.to_data_fields_hash + end +end diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb new file mode 100644 index 00000000000..74d77618f2c --- /dev/null +++ b/app/services/bulk_update_integration_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class BulkUpdateIntegrationService + def initialize(integration, batch) + @integration = integration + @batch = batch + end + + # rubocop: disable CodeReuse/ActiveRecord + def execute + Service.transaction do + batch.update_all(service_hash) + + if integration.data_fields_present? + integration.data_fields.class.where(service_id: batch.select(:id)).update_all(data_fields_hash) + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + + private + + attr_reader :integration, :batch + + def service_hash + integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id } + end + + def data_fields_hash + integration.to_data_fields_hash + end +end diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb index 1fe65898d55..5efb3805bf7 100644 --- a/app/services/ci/create_job_artifacts_service.rb +++ b/app/services/ci/create_job_artifacts_service.rb @@ -52,24 +52,15 @@ module Ci attr_reader :job, :project def validate_requirements(artifact_type:, filesize:) - return forbidden_type_error(artifact_type) if forbidden_type?(artifact_type) return too_large_error if too_large?(artifact_type, filesize) success end - def forbidden_type?(type) - lsif?(type) && !code_navigation_enabled? - end - def too_large?(type, size) size > max_size(type) if size end - def code_navigation_enabled? - Feature.enabled?(:code_navigation, project, default_enabled: true) - end - def lsif?(type) type == LSIF_ARTIFACT_TYPE end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 70ad18e80eb..3f1a2d1350d 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -82,8 +82,7 @@ module Ci schedule_head_pipeline_update if pipeline.persisted? # If pipeline is not persisted, try to recover IID - pipeline.reset_project_iid unless pipeline.persisted? || - Feature.disabled?(:ci_pipeline_rewind_iid, project, default_enabled: true) + pipeline.reset_project_iid unless pipeline.persisted? pipeline end diff --git a/app/services/ci/daily_build_group_report_result_service.rb b/app/services/ci/daily_build_group_report_result_service.rb index 6cdf3c88f8c..c32fc27c274 100644 --- a/app/services/ci/daily_build_group_report_result_service.rb +++ b/app/services/ci/daily_build_group_report_result_service.rb @@ -3,8 +3,6 @@ module Ci class DailyBuildGroupReportResultService def execute(pipeline) - return unless Feature.enabled?(:ci_daily_code_coverage, pipeline.project, default_enabled: true) - DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline)) end diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb index 32abd1a7626..8343e0f8cd0 100644 --- a/app/services/ci/expire_pipeline_cache_service.rb +++ b/app/services/ci/expire_pipeline_cache_service.rb @@ -32,11 +32,18 @@ module Ci Gitlab::Routing.url_helpers.project_new_merge_request_path(project, format: :json) end + def pipelines_project_merge_request_path(merge_request) + Gitlab::Routing.url_helpers.pipelines_project_merge_request_path(merge_request.target_project, merge_request, format: :json) + end + + def merge_request_widget_path(merge_request) + Gitlab::Routing.url_helpers.cached_widget_project_json_merge_request_path(merge_request.project, merge_request, format: :json) + end + def each_pipelines_merge_request_path(pipeline) pipeline.all_merge_requests.each do |merge_request| - path = Gitlab::Routing.url_helpers.pipelines_project_merge_request_path(merge_request.target_project, merge_request, format: :json) - - yield(path) + yield(pipelines_project_merge_request_path(merge_request)) + yield(merge_request_widget_path(merge_request)) end end diff --git a/app/services/ci/pipelines/create_artifact_service.rb b/app/services/ci/pipelines/create_artifact_service.rb index b7d334e436d..bfaf317241a 100644 --- a/app/services/ci/pipelines/create_artifact_service.rb +++ b/app/services/ci/pipelines/create_artifact_service.rb @@ -3,7 +3,6 @@ module Ci module Pipelines class CreateArtifactService def execute(pipeline) - return unless ::Gitlab::Ci::Features.coverage_report_view?(pipeline.project) return unless pipeline.can_generate_coverage_reports? return if pipeline.has_coverage_reports? diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 18bae26613f..e511e26adfe 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -31,14 +31,14 @@ module Ci # rubocop: disable CodeReuse/ActiveRecord def update_retried # find the latest builds for each name - latest_statuses = pipeline.statuses.latest + latest_statuses = pipeline.latest_statuses .group(:name) .having('count(*) > 1') .pluck(Arel.sql('MAX(id)'), 'name') # mark builds that are retried if latest_statuses.any? - pipeline.statuses.latest + pipeline.latest_statuses .where(name: latest_statuses.map(&:second)) .where.not(id: latest_statuses.map(&:first)) .update_all(retried: true) diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 6b2e6c245f3..f397ada0696 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -58,7 +58,7 @@ module Ci build = project.builds.new(attributes) build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(build)) build.retried = false - BulkInsertableAssociations.with_bulk_insert(enabled: ::Gitlab::Ci::Features.bulk_insert_on_create?(project)) do + BulkInsertableAssociations.with_bulk_insert do build.save! end build diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb index 31c7178c9e7..241eba733ea 100644 --- a/app/services/ci/update_build_queue_service.rb +++ b/app/services/ci/update_build_queue_service.rb @@ -9,9 +9,7 @@ module Ci private def tick_for(build, runners) - if Feature.enabled?(:ci_update_queues_for_online_runners, build.project, default_enabled: true) - runners = runners.with_recent_runner_queue - end + runners = runners.with_recent_runner_queue runners.each do |runner| runner.pick_build!(build) diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb index 61e4c77c1e5..cc8e2060888 100644 --- a/app/services/ci/update_build_state_service.rb +++ b/app/services/ci/update_build_state_service.rb @@ -2,7 +2,10 @@ module Ci class UpdateBuildStateService - Result = Struct.new(:status, keyword_init: true) + include ::Gitlab::Utils::StrongMemoize + include ::Gitlab::ExclusiveLeaseHelpers + + Result = Struct.new(:status, :backoff, keyword_init: true) ACCEPT_TIMEOUT = 5.minutes.freeze @@ -17,44 +20,65 @@ module Ci def execute overwrite_trace! if has_trace? - if accept_request? - accept_build_state! - else - check_migration_state - update_build_state! + unless accept_available? + return update_build_state! + end + + ensure_pending_state! + + in_build_trace_lock do + process_build_state! end end private - def accept_build_state! - if Time.current - ensure_pending_state.created_at > ACCEPT_TIMEOUT - metrics.increment_trace_operation(operation: :discarded) + def overwrite_trace! + metrics.increment_trace_operation(operation: :overwrite) - return update_build_state! + build.trace.set(params[:trace]) if Gitlab::Ci::Features.trace_overwrite? + end + + def ensure_pending_state! + pending_state.created_at + end + + def process_build_state! + if live_chunks_pending? + if pending_state_outdated? + discard_build_trace! + update_build_state! + else + accept_build_state! + end + else + validate_build_trace! + update_build_state! end + end + def accept_build_state! build.trace_chunks.live.find_each do |chunk| chunk.schedule_to_persist! end metrics.increment_trace_operation(operation: :accepted) - Result.new(status: 202) - end - - def overwrite_trace! - metrics.increment_trace_operation(operation: :overwrite) - - build.trace.set(params[:trace]) if Gitlab::Ci::Features.trace_overwrite? + ::Gitlab::Ci::Runner::Backoff.new(pending_state.created_at).then do |backoff| + Result.new(status: 202, backoff: backoff.to_seconds) + end end - def check_migration_state - return unless accept_available? + def validate_build_trace! + return unless has_chunks? - if has_chunks? && !live_chunks_pending? + unless live_chunks_pending? metrics.increment_trace_operation(operation: :finalized) end + + unless ::Gitlab::Ci::Trace::Checksum.new(build).valid? + metrics.increment_trace_operation(operation: :invalid) + end end def update_build_state! @@ -76,12 +100,24 @@ module Ci end end + def discard_build_trace! + metrics.increment_trace_operation(operation: :discarded) + end + def accept_available? !build_running? && has_checksum? && chunks_migration_enabled? end - def accept_request? - accept_available? && live_chunks_pending? + def live_chunks_pending? + build.trace_chunks.live.any? + end + + def has_chunks? + build.trace_chunks.any? + end + + def pending_state_outdated? + Time.current - pending_state.created_at > ACCEPT_TIMEOUT end def build_state @@ -96,18 +132,14 @@ module Ci params.dig(:checksum).present? end - def has_chunks? - build.trace_chunks.any? - end - - def live_chunks_pending? - build.trace_chunks.live.any? - end - def build_running? build_state == 'running' end + def pending_state + strong_memoize(:pending_state) { ensure_pending_state } + end + def ensure_pending_state Ci::BuildPendingState.create_or_find_by!( build_id: build.id, @@ -121,6 +153,32 @@ module Ci build.pending_state end + ## + # This method is releasing an exclusive lock on a build trace the moment we + # conclude that build status has been written and the build state update + # has been committed to the database. + # + # Because a build state machine schedules a bunch of workers to run after + # build status transition to complete, we do not want to keep the lease + # until all the workers are scheduled because it opens a possibility of + # race conditions happening. + # + # Instead of keeping the lease until the transition is fully done and + # workers are scheduled, we immediately release the lock after the database + # commit happens. + # + def in_build_trace_lock(&block) + build.trace.lock do |_, lease| # rubocop:disable CodeReuse/ActiveRecord + build.run_on_status_commit { lease.cancel } + + yield + end + rescue ::Gitlab::Ci::Trace::LockedError + metrics.increment_trace_operation(operation: :locked) + + accept_build_state! + end + def chunks_migration_enabled? ::Gitlab::Ci::Features.accept_trace?(build.project) end diff --git a/app/services/concerns/admin/propagate_service.rb b/app/services/concerns/admin/propagate_service.rb index 974408f678c..065ab6f7ff9 100644 --- a/app/services/concerns/admin/propagate_service.rb +++ b/app/services/concerns/admin/propagate_service.rb @@ -4,9 +4,7 @@ module Admin module PropagateService extend ActiveSupport::Concern - BATCH_SIZE = 100 - - delegate :data_fields_present?, to: :integration + BATCH_SIZE = 10_000 class_methods do def propagate(integration) @@ -23,51 +21,10 @@ module Admin attr_reader :integration def create_integration_for_projects_without_integration - loop do - batch_ids = Project.uncached { Project.ids_without_integration(integration, BATCH_SIZE) } - - bulk_create_from_integration(batch_ids, 'project') unless batch_ids.empty? - - break if batch_ids.size < BATCH_SIZE - end - end - - def bulk_create_from_integration(batch_ids, association) - service_list = ServiceList.new(batch_ids, service_hash, association).to_array - - Service.transaction do - results = bulk_insert(*service_list) - - if data_fields_present? - data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array - - bulk_insert(*data_list) - end - - run_callbacks(batch_ids) if association == 'project' + Project.without_integration(integration).each_batch(of: BATCH_SIZE) do |projects| + min_id, max_id = projects.pick("MIN(projects.id), MAX(projects.id)") + PropagateIntegrationProjectWorker.perform_async(integration.id, min_id, max_id) end end - - def bulk_insert(klass, columns, values_array) - items_to_insert = values_array.map { |array| Hash[columns.zip(array)] } - - klass.insert_all(items_to_insert, returning: [:id]) - end - - # rubocop: disable CodeReuse/ActiveRecord - def run_callbacks(batch_ids) - if integration.issue_tracker? - Project.where(id: batch_ids).update_all(has_external_issue_tracker: true) - end - - if integration.type == 'ExternalWikiService' - Project.where(id: batch_ids).update_all(has_external_wiki: true) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - def data_fields_hash - @data_fields_hash ||= integration.to_data_fields_hash - end end end diff --git a/app/services/design_management/copy_design_collection.rb b/app/services/design_management/copy_design_collection.rb new file mode 100644 index 00000000000..66cf6112062 --- /dev/null +++ b/app/services/design_management/copy_design_collection.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module DesignManagement + module CopyDesignCollection + end +end diff --git a/app/services/design_management/copy_design_collection/copy_service.rb b/app/services/design_management/copy_design_collection/copy_service.rb new file mode 100644 index 00000000000..68f69a9c8db --- /dev/null +++ b/app/services/design_management/copy_design_collection/copy_service.rb @@ -0,0 +1,306 @@ +# frozen_string_literal: true + +# Service to copy a DesignCollection from one Issue to another. +# Copies the DesignCollection's Designs, Versions, and Notes on Designs. +module DesignManagement + module CopyDesignCollection + class CopyService < DesignService + # rubocop: disable CodeReuse/ActiveRecord + def initialize(project, user, params = {}) + super + + @target_issue = params.fetch(:target_issue) + @target_project = @target_issue.project + @target_repository = @target_project.design_repository + @target_design_collection = @target_issue.design_collection + @temporary_branch = "CopyDesignCollectionService_#{SecureRandom.hex}" + + @designs = DesignManagement::Design.unscoped.where(issue: issue).order(:id).load + @versions = DesignManagement::Version.unscoped.where(issue: issue).order(:id).includes(:designs).load + + @sha_attribute = Gitlab::Database::ShaAttribute.new + @shas = [] + @event_enum_map = DesignManagement::DesignAction::EVENT_FOR_GITALY_ACTION.invert + end + # rubocop: enable CodeReuse/ActiveRecord + + def execute + return error('User cannot copy design collection to issue') unless user_can_copy? + return error('Target design collection must first be queued') unless target_design_collection.copy_in_progress? + return error('Design collection has no designs') if designs.empty? + return error('Target design collection already has designs') unless target_design_collection.empty? + + with_temporary_branch do + copy_commits! + + ActiveRecord::Base.transaction do + design_ids = copy_designs! + version_ids = copy_versions! + copy_actions!(design_ids, version_ids) + link_lfs_files! + copy_notes!(design_ids) + finalize! + end + end + + ServiceResponse.success + rescue => error + log_exception(error) + + target_design_collection.error_copy! + + error('Designs were unable to be copied successfully') + end + + private + + attr_reader :designs, :event_enum_map, :sha_attribute, :shas, :temporary_branch, + :target_design_collection, :target_issue, :target_repository, + :target_project, :versions + + alias_method :merge_branch, :target_branch + + def log_exception(exception) + payload = { + issue_id: issue.id, + project_id: project.id, + target_issue_id: target_issue.id, + target_project: target_project.id + } + + Gitlab::ErrorTracking.track_exception(exception, payload) + end + + def error(message) + ServiceResponse.error(message: message) + end + + def user_can_copy? + current_user.can?(:read_design, design_collection) && + current_user.can?(:admin_issue, target_issue) + end + + def with_temporary_branch(&block) + target_repository.create_if_not_exists + + create_master_branch! if target_repository.empty? + create_temporary_branch! + + yield + ensure + remove_temporary_branch! + end + + # A project that does not have any designs will have a blank design + # repository. To create a temporary branch from `master` we need + # create `master` first by adding a file to it. + def create_master_branch! + target_repository.create_file( + current_user, + ".CopyDesignCollectionService_#{Time.now.to_i}", + '.gitlab', + message: "Commit to create #{merge_branch} branch in CopyDesignCollectionService", + branch_name: merge_branch + ) + end + + def create_temporary_branch! + target_repository.add_branch( + current_user, + temporary_branch, + target_repository.root_ref + ) + end + + def remove_temporary_branch! + return unless target_repository.branch_exists?(temporary_branch) + + target_repository.rm_branch(current_user, temporary_branch) + end + + # Merge the temporary branch containing the commits to `master` + # and update the state of the target_design_collection. + def finalize! + source_sha = shas.last + + target_repository.raw.merge( + current_user, + source_sha, + merge_branch, + 'CopyDesignCollectionService finalize merge' + ) { nil } + + target_design_collection.end_copy! + end + + # rubocop: disable CodeReuse/ActiveRecord + def copy_commits! + # Execute another query to include actions and their designs + DesignManagement::Version.unscoped.where(id: versions).order(:id).includes(actions: :design).find_each(batch_size: 100) do |version| + gitaly_actions = version.actions.map do |action| + design = action.design + # Map the raw Action#event enum value to a Gitaly "action" for the + # `Repository#multi_action` call. + gitaly_action_name = @event_enum_map[action.event_before_type_cast] + # `content` will be the LfsPointer file and not the design file, + # and can be nil for deletions. + content = blobs.dig(version.sha, design.filename)&.data + file_path = DesignManagement::Design.build_full_path(target_issue, design) + + { + action: gitaly_action_name, + file_path: file_path, + content: content + }.compact + end + + sha = target_repository.multi_action( + current_user, + branch_name: temporary_branch, + message: commit_message(version), + actions: gitaly_actions + ) + + shas << sha + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def copy_designs! + design_attributes = attributes_config[:design_attributes] + + new_rows = designs.map do |design| + design.attributes.slice(*design_attributes).merge( + issue_id: target_issue.id, + project_id: target_project.id + ) + end + + # TODO Replace `Gitlab::Database.bulk_insert` with `BulkInsertSafe` + # once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed. + ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert + DesignManagement::Design.table_name, + new_rows, + return_ids: true + ) + end + + def copy_versions! + version_attributes = attributes_config[:version_attributes] + # `shas` are the list of Git commits made during the Git copy phase, + # and will be ordered 1:1 with old versions + shas_enum = shas.to_enum + + new_rows = versions.map do |version| + version.attributes.slice(*version_attributes).merge( + issue_id: target_issue.id, + sha: sha_attribute.serialize(shas_enum.next) + ) + end + + # TODO Replace `Gitlab::Database.bulk_insert` with `BulkInsertSafe` + # once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed. + ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert + DesignManagement::Version.table_name, + new_rows, + return_ids: true + ) + end + + # rubocop: disable CodeReuse/ActiveRecord + def copy_actions!(new_design_ids, new_version_ids) + # Create a map of <Old design id> => <New design id> + design_id_map = new_design_ids.each_with_index.to_h do |design_id, i| + [designs[i].id, design_id] + end + + # Create a map of <Old version id> => <New version id> + version_id_map = new_version_ids.each_with_index.to_h do |version_id, i| + [versions[i].id, version_id] + end + + actions = DesignManagement::Action.unscoped.select(:design_id, :version_id, :event).where(design: designs, version: versions) + + new_rows = actions.map do |action| + { + design_id: design_id_map[action.design_id], + version_id: version_id_map[action.version_id], + event: action.event_before_type_cast + } + end + + # We cannot use `BulkInsertSafe` because of the uploader mounted in `Action`. + ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert + DesignManagement::Action.table_name, + new_rows + ) + end + # rubocop: enable CodeReuse/ActiveRecord + + def commit_message(version) + "Copy commit #{version.sha} from issue #{issue.to_reference(full: true)}" + end + + # rubocop: disable CodeReuse/ActiveRecord + def copy_notes!(design_ids) + new_designs = DesignManagement::Design.unscoped.find(design_ids) + + # Execute another query to filter only designs with notes + DesignManagement::Design.unscoped.where(id: designs).joins(:notes).distinct.find_each(batch_size: 100) do |old_design| + new_design = new_designs.find { |d| d.filename == old_design.filename } + + Notes::CopyService.new(current_user, old_design, new_design).execute + end + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def link_lfs_files! + oids = blobs.values.flat_map(&:values).map(&:lfs_oid) + repository_type = LfsObjectsProject.repository_types[:design] + + new_rows = LfsObject.where(oid: oids).find_each(batch_size: 1000).map do |lfs_object| + { + project_id: target_project.id, + lfs_object_id: lfs_object.id, + repository_type: repository_type + } + end + + # We cannot use `BulkInsertSafe` due to the LfsObjectsProject#update_project_statistics + # callback that fires after_commit. + ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert + LfsObjectsProject.table_name, + new_rows, + on_conflict: :do_nothing # Upsert + ) + end + # rubocop: enable CodeReuse/ActiveRecord + + # Blob data is used to find the oids for LfsObjects and to copy to Git. + # Blobs are reasonably small in memory, as their data are LFS Pointer files. + # + # Returns all blobs for the designs as a Hash of `{ Blob#commit_id => { Design#filename => Blob } }` + def blobs + @blobs ||= begin + items = versions.flat_map { |v| v.designs.map { |d| [v.sha, DesignManagement::Design.build_full_path(issue, d)] } } + + repository.blobs_at(items).each_with_object({}) do |blob, h| + design = designs.find { |d| DesignManagement::Design.build_full_path(issue, d) == blob.path } + + h[blob.commit_id] ||= {} + h[blob.commit_id][design.filename] = blob + end + end + end + + def attributes_config + @attributes_config ||= YAML.load_file(attributes_config_file).symbolize_keys + end + + def attributes_config_file + Rails.root.join('lib/gitlab/design_management/copy_design_collection_model_attributes.yml') + end + end + end +end diff --git a/app/services/design_management/copy_design_collection/queue_service.rb b/app/services/design_management/copy_design_collection/queue_service.rb new file mode 100644 index 00000000000..f76917dbe47 --- /dev/null +++ b/app/services/design_management/copy_design_collection/queue_service.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Service for setting the initial copy_state on the target DesignCollection +# and queuing a CopyDesignCollectionWorker. +module DesignManagement + module CopyDesignCollection + class QueueService + def initialize(current_user, issue, target_issue) + @current_user = current_user + @issue = issue + @target_issue = target_issue + @target_design_collection = target_issue.design_collection + end + + def execute + return error('User cannot copy designs to issue') unless user_can_copy? + return error('Target design collection copy state must be `ready`') unless target_design_collection.can_start_copy? + + target_design_collection.start_copy! + + DesignManagement::CopyDesignCollectionWorker.perform_async(current_user.id, issue.id, target_issue.id) + + ServiceResponse.success + end + + private + + delegate :design_collection, to: :issue + + attr_reader :current_user, :issue, :target_design_collection, :target_issue + + def error(message) + ServiceResponse.error(message: message) + end + + def user_can_copy? + current_user.can?(:read_design, issue) && + current_user.can?(:admin_issue, target_issue) + end + end + end +end diff --git a/app/services/design_management/design_service.rb b/app/services/design_management/design_service.rb index 54e53609646..5aa2a2f73bc 100644 --- a/app/services/design_management/design_service.rb +++ b/app/services/design_management/design_service.rb @@ -19,6 +19,7 @@ module DesignManagement def collection issue.design_collection end + alias_method :design_collection, :collection def repository collection.repository diff --git a/app/services/design_management/generate_image_versions_service.rb b/app/services/design_management/generate_image_versions_service.rb index 213aac164ff..e56d163c461 100644 --- a/app/services/design_management/generate_image_versions_service.rb +++ b/app/services/design_management/generate_image_versions_service.rb @@ -48,6 +48,9 @@ module DesignManagement # Store and process the file action.image_v432x230.store!(raw_file) action.save! + rescue CarrierWave::IntegrityError => e + Gitlab::ErrorTracking.log_exception(e, project_id: project.id, design_id: action.design_id, version_id: action.version_id) + log_error(e.message) rescue CarrierWave::UploadError => e Gitlab::ErrorTracking.track_exception(e, project_id: project.id, design_id: action.design_id, version_id: action.version_id) log_error(e.message) diff --git a/app/services/design_management/runs_design_actions.rb b/app/services/design_management/runs_design_actions.rb index 4bd6bb45658..ee6aa9286d3 100644 --- a/app/services/design_management/runs_design_actions.rb +++ b/app/services/design_management/runs_design_actions.rb @@ -4,14 +4,15 @@ module DesignManagement module RunsDesignActions NoActions = Class.new(StandardError) - # this concern requires the following methods to be implemented: + # This concern requires the following methods to be implemented: # current_user, target_branch, repository, commit_message # # Before calling `run_actions`, you should ensure the repository exists, by # calling `repository.create_if_not_exists`. # # @raise [NoActions] if actions are empty - def run_actions(actions) + # @return [DesignManagement::Version] + def run_actions(actions, skip_system_notes: false) raise NoActions if actions.empty? sha = repository.multi_action(current_user, @@ -21,14 +22,14 @@ module DesignManagement ::DesignManagement::Version .create_for_designs(actions, sha, current_user) - .tap { |version| post_process(version) } + .tap { |version| post_process(version, skip_system_notes) } end private - def post_process(version) + def post_process(version, skip_system_notes) version.run_after_commit_or_now do - ::DesignManagement::NewVersionWorker.perform_async(id) + ::DesignManagement::NewVersionWorker.perform_async(id, skip_system_notes) end end end diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb index 0446d2f1ee8..8dcc678e87e 100644 --- a/app/services/design_management/save_designs_service.rb +++ b/app/services/design_management/save_designs_service.rb @@ -16,11 +16,15 @@ module DesignManagement def execute return error("Not allowed!") unless can_create_designs? return error("Only #{MAX_FILES} files are allowed simultaneously") if files.size > MAX_FILES + return error("Duplicate filenames are not allowed!") if files.map(&:original_filename).uniq.length != files.length + return error("Design copy is in progress") if design_collection.copy_in_progress? uploaded_designs, version = upload_designs! skipped_designs = designs - uploaded_designs create_events + design_collection.reset_copy! + success({ designs: uploaded_designs, version: version, skipped_designs: skipped_designs }) rescue ::ActiveRecord::RecordInvalid => e error(e.message) @@ -34,7 +38,10 @@ module DesignManagement ::DesignManagement::Version.with_lock(project.id, repository) do actions = build_actions - [actions.map(&:design), actions.presence && run_actions(actions)] + [ + actions.map(&:design), + actions.presence && run_actions(actions) + ] end end diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb new file mode 100644 index 00000000000..9b27df90992 --- /dev/null +++ b/app/services/feature_flags/base_service.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module FeatureFlags + class BaseService < ::BaseService + include Gitlab::Utils::StrongMemoize + + AUDITABLE_ATTRIBUTES = %w(name description active).freeze + + protected + + def audit_event(feature_flag) + message = audit_message(feature_flag) + + return if message.blank? + + details = + { + custom_message: message, + target_id: feature_flag.id, + target_type: feature_flag.class.name, + target_details: feature_flag.name + } + + ::AuditEventService.new( + current_user, + feature_flag.project, + details + ) + end + + def save_audit_event(audit_event) + return unless audit_event + + audit_event.security_event + end + + def created_scope_message(scope) + "Created rule <strong>#{scope.environment_scope}</strong> "\ + "and set it as <strong>#{scope.active ? "active" : "inactive"}</strong> "\ + "with strategies <strong>#{scope.strategies}</strong>." + end + + def feature_flag_by_name + strong_memoize(:feature_flag_by_name) do + project.operations_feature_flags.find_by_name(params[:name]) + end + end + + def feature_flag_scope_by_environment_scope + strong_memoize(:feature_flag_scope_by_environment_scope) do + feature_flag_by_name.scopes.find_by_environment_scope(params[:environment_scope]) + end + end + end +end diff --git a/app/services/feature_flags/create_service.rb b/app/services/feature_flags/create_service.rb new file mode 100644 index 00000000000..b4ca90f7aae --- /dev/null +++ b/app/services/feature_flags/create_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module FeatureFlags + class CreateService < FeatureFlags::BaseService + def execute + return error('Access Denied', 403) unless can_create? + return error('Version is invalid', :bad_request) unless valid_version? + return error('New version feature flags are not enabled for this project', :bad_request) unless flag_version_enabled? + + ActiveRecord::Base.transaction do + feature_flag = project.operations_feature_flags.new(params) + + if feature_flag.save + save_audit_event(audit_event(feature_flag)) + + success(feature_flag: feature_flag) + else + error(feature_flag.errors.full_messages, 400) + end + end + end + + private + + def audit_message(feature_flag) + message_parts = ["Created feature flag <strong>#{feature_flag.name}</strong>", + "with description <strong>\"#{feature_flag.description}\"</strong>."] + + message_parts += feature_flag.scopes.map do |scope| + created_scope_message(scope) + end + + message_parts.join(" ") + end + + def can_create? + Ability.allowed?(current_user, :create_feature_flag, project) + end + + def valid_version? + !params.key?(:version) || Operations::FeatureFlag.versions.key?(params[:version]) + end + + def flag_version_enabled? + params[:version] != 'new_version_flag' || new_version_feature_flags_enabled? + end + + def new_version_feature_flags_enabled? + ::Feature.enabled?(:feature_flags_new_version, project, default_enabled: true) + end + end +end diff --git a/app/services/feature_flags/destroy_service.rb b/app/services/feature_flags/destroy_service.rb new file mode 100644 index 00000000000..c77e3e03ec3 --- /dev/null +++ b/app/services/feature_flags/destroy_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module FeatureFlags + class DestroyService < FeatureFlags::BaseService + def execute(feature_flag) + destroy_feature_flag(feature_flag) + end + + private + + def destroy_feature_flag(feature_flag) + return error('Access Denied', 403) unless can_destroy?(feature_flag) + + ActiveRecord::Base.transaction do + if feature_flag.destroy + save_audit_event(audit_event(feature_flag)) + + success(feature_flag: feature_flag) + else + error(feature_flag.errors.full_messages) + end + end + end + + def audit_message(feature_flag) + "Deleted feature flag <strong>#{feature_flag.name}</strong>." + end + + def can_destroy?(feature_flag) + Ability.allowed?(current_user, :destroy_feature_flag, feature_flag) + end + end +end diff --git a/app/services/feature_flags/disable_service.rb b/app/services/feature_flags/disable_service.rb new file mode 100644 index 00000000000..8a443ac1795 --- /dev/null +++ b/app/services/feature_flags/disable_service.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module FeatureFlags + class DisableService < BaseService + def execute + return error('Feature Flag not found', 404) unless feature_flag_by_name + return error('Feature Flag Scope not found', 404) unless feature_flag_scope_by_environment_scope + return error('Strategy not found', 404) unless strategy_exist_in_persisted_data? + + ::FeatureFlags::UpdateService + .new(project, current_user, update_params) + .execute(feature_flag_by_name) + end + + private + + def update_params + if remaining_strategies.empty? + params_to_destroy_scope + else + params_to_update_scope + end + end + + def remaining_strategies + strong_memoize(:remaining_strategies) do + feature_flag_scope_by_environment_scope.strategies.reject do |strategy| + strategy['name'] == params[:strategy]['name'] && + strategy['parameters'] == params[:strategy]['parameters'] + end + end + end + + def strategy_exist_in_persisted_data? + feature_flag_scope_by_environment_scope.strategies != remaining_strategies + end + + def params_to_destroy_scope + { scopes_attributes: [{ id: feature_flag_scope_by_environment_scope.id, _destroy: true }] } + end + + def params_to_update_scope + { scopes_attributes: [{ id: feature_flag_scope_by_environment_scope.id, strategies: remaining_strategies }] } + end + end +end diff --git a/app/services/feature_flags/enable_service.rb b/app/services/feature_flags/enable_service.rb new file mode 100644 index 00000000000..b4cbb32e003 --- /dev/null +++ b/app/services/feature_flags/enable_service.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module FeatureFlags + class EnableService < BaseService + def execute + if feature_flag_by_name + update_feature_flag + else + create_feature_flag + end + end + + private + + def create_feature_flag + ::FeatureFlags::CreateService + .new(project, current_user, create_params) + .execute + end + + def update_feature_flag + ::FeatureFlags::UpdateService + .new(project, current_user, update_params) + .execute(feature_flag_by_name) + end + + def create_params + if params[:environment_scope] == '*' + params_to_create_flag_with_default_scope + else + params_to_create_flag_with_additional_scope + end + end + + def update_params + if feature_flag_scope_by_environment_scope + params_to_update_scope + else + params_to_create_scope + end + end + + def params_to_create_flag_with_default_scope + { + name: params[:name], + scopes_attributes: [ + { + active: true, + environment_scope: '*', + strategies: [params[:strategy]] + } + ] + } + end + + def params_to_create_flag_with_additional_scope + { + name: params[:name], + scopes_attributes: [ + { + active: false, + environment_scope: '*' + }, + { + active: true, + environment_scope: params[:environment_scope], + strategies: [params[:strategy]] + } + ] + } + end + + def params_to_create_scope + { + scopes_attributes: [{ + active: true, + environment_scope: params[:environment_scope], + strategies: [params[:strategy]] + }] + } + end + + def params_to_update_scope + { + scopes_attributes: [{ + id: feature_flag_scope_by_environment_scope.id, + active: true, + strategies: feature_flag_scope_by_environment_scope.strategies | [params[:strategy]] + }] + } + end + end +end diff --git a/app/services/feature_flags/update_service.rb b/app/services/feature_flags/update_service.rb new file mode 100644 index 00000000000..c837e50b104 --- /dev/null +++ b/app/services/feature_flags/update_service.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module FeatureFlags + class UpdateService < FeatureFlags::BaseService + AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES = { + 'active' => 'active state', + 'environment_scope' => 'environment scope', + 'strategies' => 'strategies' + }.freeze + + def execute(feature_flag) + return error('Access Denied', 403) unless can_update?(feature_flag) + + ActiveRecord::Base.transaction do + feature_flag.assign_attributes(params) + + feature_flag.strategies.each do |strategy| + if strategy.name_changed? && strategy.name_was == ::Operations::FeatureFlags::Strategy::STRATEGY_GITLABUSERLIST + strategy.user_list = nil + end + end + + audit_event = audit_event(feature_flag) + + if feature_flag.save + save_audit_event(audit_event) + + success(feature_flag: feature_flag) + else + error(feature_flag.errors.full_messages, :bad_request) + end + end + end + + private + + def audit_message(feature_flag) + changes = changed_attributes_messages(feature_flag) + changes += changed_scopes_messages(feature_flag) + + return if changes.empty? + + "Updated feature flag <strong>#{feature_flag.name}</strong>. " + changes.join(" ") + end + + def changed_attributes_messages(feature_flag) + feature_flag.changes.slice(*AUDITABLE_ATTRIBUTES).map do |attribute_name, changes| + "Updated #{attribute_name} "\ + "from <strong>\"#{changes.first}\"</strong> to "\ + "<strong>\"#{changes.second}\"</strong>." + end + end + + def changed_scopes_messages(feature_flag) + feature_flag.scopes.map do |scope| + if scope.new_record? + created_scope_message(scope) + elsif scope.marked_for_destruction? + deleted_scope_message(scope) + else + updated_scope_message(scope) + end + end.compact # updated_scope_message can return nil if nothing has been changed + end + + def deleted_scope_message(scope) + "Deleted rule <strong>#{scope.environment_scope}</strong>." + end + + def updated_scope_message(scope) + changes = scope.changes.slice(*AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES.keys) + return if changes.empty? + + message = "Updated rule <strong>#{scope.environment_scope}</strong> " + message += changes.map do |attribute_name, change| + name = AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES[attribute_name] + "#{name} from <strong>#{change.first}</strong> to <strong>#{change.second}</strong>" + end.join(' ') + + message + '.' + end + + def can_update?(feature_flag) + Ability.allowed?(current_user, :update_feature_flag, feature_flag) + end + end +end diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index dcb32b4c84b..93a0d139001 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -76,12 +76,20 @@ module Git def branch_change_hooks enqueue_process_commit_messages enqueue_jira_connect_sync_messages + enqueue_metrics_dashboard_sync end def branch_remove_hooks project.repository.after_remove_branch(expire_cache: false) end + def enqueue_metrics_dashboard_sync + return unless Feature.enabled?(:sync_metrics_dashboards, project) + return unless default_branch? + + ::Metrics::Dashboard::SyncDashboardsWorker.perform_async(project.id) + end + # Schedules processing of commit messages def enqueue_process_commit_messages referencing_commits = limited_commits.select(&:matches_cross_reference_regex?) diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb index fa3019ee9d6..87e2be858c0 100644 --- a/app/services/git/wiki_push_service.rb +++ b/app/services/git/wiki_push_service.rb @@ -34,9 +34,7 @@ module Git def can_process_wiki_events? # TODO: Support activity events for group wikis # https://gitlab.com/gitlab-org/gitlab/-/issues/209306 - return false unless wiki.is_a?(ProjectWiki) - - Feature.enabled?(:wiki_events_on_git_push, wiki.container) + wiki.is_a?(ProjectWiki) end def push_changes diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index ce583095168..4747e1d5ac5 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -15,6 +15,8 @@ module Groups after_build_hook(@group, params) + inherit_group_shared_runners_settings + unless can_use_visibility_level? && can_create_group? return @group end @@ -28,9 +30,12 @@ module Groups @group.build_chat_team(name: response['name'], team_id: response['id']) end - if @group.save - @group.add_owner(current_user) - add_settings_record + Group.transaction do + if @group.save + @group.add_owner(current_user) + @group.create_namespace_settings + Service.create_from_active_default_integrations(@group, :group_id) if Feature.enabled?(:group_level_integrations) + end end @group @@ -84,8 +89,11 @@ module Groups params[:visibility_level] = Gitlab::CurrentSettings.current_application_settings.default_group_visibility end - def add_settings_record - @group.create_namespace_settings + def inherit_group_shared_runners_settings + return unless @group.parent + + @group.shared_runners_enabled = @group.parent.shared_runners_enabled + @group.allow_descendants_override_disabled_shared_runners = @group.parent.allow_descendants_override_disabled_shared_runners end end end diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb index a5c776f8fc2..a0ddc50e5e0 100644 --- a/app/services/groups/import_export/import_service.rb +++ b/app/services/groups/import_export/import_service.rb @@ -13,7 +13,7 @@ module Groups end def async_execute - group_import_state = GroupImportState.safe_find_or_create_by!(group: group) + group_import_state = GroupImportState.safe_find_or_create_by!(group: group, user: current_user) jid = GroupImportWorker.perform_async(current_user.id, group.id) if jid.present? diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 2bd571f60af..70f5c7e2ea7 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -103,6 +103,9 @@ module Groups @group.parent = @new_parent_group @group.clear_memoization(:self_and_ancestors_ids) + + inherit_group_shared_runners_settings + @group.save! end @@ -161,6 +164,17 @@ module Groups group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.') }.freeze end + + def inherit_group_shared_runners_settings + parent_setting = @group.parent&.shared_runners_setting + return unless parent_setting + + if @group.shared_runners_setting_higher_than?(parent_setting) + result = Groups::UpdateSharedRunnersService.new(@group, current_user, shared_runners_setting: parent_setting).execute + + raise TransferError, result[:message] unless result[:status] == :success + end + end end end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 81393681dc0..382a3dbf0f7 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -19,6 +19,8 @@ module Groups return false unless valid_path_change_with_npm_packages? + return false unless update_shared_runners + before_assignment_hook(group, params) group.assign_attributes(params) @@ -98,6 +100,17 @@ module Groups params[:share_with_group_lock] != group.share_with_group_lock end + + def update_shared_runners + return true if params[:shared_runners_setting].nil? + + result = Groups::UpdateSharedRunnersService.new(group, current_user, shared_runners_setting: params.delete(:shared_runners_setting)).execute + + return true if result[:status] == :success + + group.errors.add(:update_shared_runners, result[:message]) + false + end end end diff --git a/app/services/groups/update_shared_runners_service.rb b/app/services/groups/update_shared_runners_service.rb index 63f57104510..639c5bf6ae0 100644 --- a/app/services/groups/update_shared_runners_service.rb +++ b/app/services/groups/update_shared_runners_service.rb @@ -7,44 +7,24 @@ module Groups validate_params - enable_or_disable_shared_runners! - allow_or_disallow_descendants_override_disabled_shared_runners! + update_shared_runners success - rescue Group::UpdateSharedRunnersError => error + rescue ActiveRecord::RecordInvalid, ArgumentError => error error(error.message) end private def validate_params - if Gitlab::Utils.to_boolean(params[:shared_runners_enabled]) && !params[:allow_descendants_override_disabled_shared_runners].nil? - raise Group::UpdateSharedRunnersError, 'Cannot set shared_runners_enabled to true and allow_descendants_override_disabled_shared_runners' + unless Namespace::SHARED_RUNNERS_SETTINGS.include?(params[:shared_runners_setting]) + raise ArgumentError, "state must be one of: #{Namespace::SHARED_RUNNERS_SETTINGS.join(', ')}" end end - def enable_or_disable_shared_runners! - return if params[:shared_runners_enabled].nil? - - if Gitlab::Utils.to_boolean(params[:shared_runners_enabled]) - group.enable_shared_runners! - else - group.disable_shared_runners! - end - end - - def allow_or_disallow_descendants_override_disabled_shared_runners! - return if params[:allow_descendants_override_disabled_shared_runners].nil? - - # Needs to reset group because if both params are present could result in error - group.reset - - if Gitlab::Utils.to_boolean(params[:allow_descendants_override_disabled_shared_runners]) - group.allow_descendants_override_disabled_shared_runners! - else - group.disallow_descendants_override_disabled_shared_runners! - end + def update_shared_runners + group.update_shared_runners_setting!(params[:shared_runners_setting]) end end end diff --git a/app/services/incident_management/incidents/create_service.rb b/app/services/incident_management/incidents/create_service.rb index 5b925e0f440..cff288d602b 100644 --- a/app/services/incident_management/incidents/create_service.rb +++ b/app/services/incident_management/incidents/create_service.rb @@ -24,7 +24,7 @@ module IncidentManagement return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid? - issue.update_severity(severity) + update_severity_for(issue) success(issue) end @@ -40,6 +40,10 @@ module IncidentManagement def error(message, issue = nil) ServiceResponse.error(payload: { issue: issue }, message: message) end + + def update_severity_for(issue) + ::IncidentManagement::Incidents::UpdateSeverityService.new(issue, current_user, severity).execute + end end end end diff --git a/app/services/incident_management/incidents/update_severity_service.rb b/app/services/incident_management/incidents/update_severity_service.rb new file mode 100644 index 00000000000..5b150f3f02e --- /dev/null +++ b/app/services/incident_management/incidents/update_severity_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module IncidentManagement + module Incidents + class UpdateSeverityService < BaseService + def initialize(issuable, current_user, severity) + super(issuable.project, current_user) + + @issuable = issuable + @severity = severity.to_s.downcase + @severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(@severity) + end + + def execute + return unless issuable.incident? + + update_severity! + add_system_note + end + + private + + attr_reader :issuable, :severity + + def issuable_severity + issuable.issuable_severity || issuable.build_issuable_severity(issue_id: issuable.id) + end + + def update_severity! + issuable_severity.update!(severity: severity) + end + + def add_system_note + ::IncidentManagement::AddSeveritySystemNoteWorker.perform_async(issuable.id, current_user.id) + end + end + end +end diff --git a/app/services/incident_management/pager_duty/process_webhook_service.rb b/app/services/incident_management/pager_duty/process_webhook_service.rb index fd8252f75fb..027425e4aaa 100644 --- a/app/services/incident_management/pager_duty/process_webhook_service.rb +++ b/app/services/incident_management/pager_duty/process_webhook_service.rb @@ -34,7 +34,7 @@ module IncidentManagement strong_memoize(:pager_duty_processable_events) do ::PagerDuty::WebhookPayloadParser .call(params.to_h) - .filter { |msg| msg['event'].in?(PAGER_DUTY_PROCESSABLE_EVENT_TYPES) } + .filter { |msg| msg['event'].to_s.in?(PAGER_DUTY_PROCESSABLE_EVENT_TYPES) } end end diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb index b185ab592ff..c84074039ea 100644 --- a/app/services/issuable/clone/attributes_rewriter.rb +++ b/app/services/issuable/clone/attributes_rewriter.rb @@ -56,7 +56,7 @@ module Issuable end def copy_resource_weight_events - return unless original_entity.respond_to?(:resource_weight_events) + return unless both_respond_to?(:resource_weight_events) copy_events(ResourceWeightEvent.table_name, original_entity.resource_weight_events) do |event| event.attributes diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 1672ba2830a..60e5293e218 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -322,7 +322,7 @@ class IssuableBaseService < BaseService def change_severity(issuable) if severity = params.delete(:severity) - issuable.update_severity(severity) + ::IncidentManagement::Incidents::UpdateSeverityService.new(issuable, current_user, severity).execute end end @@ -366,6 +366,7 @@ class IssuableBaseService < BaseService } associations[:total_time_spent] = issuable.total_time_spent if issuable.respond_to?(:total_time_spent) associations[:description] = issuable.description + associations[:reviewers] = issuable.reviewers.to_a if issuable.allows_reviewers? associations end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 60e0d1eec3d..e8747b9d6d8 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -23,11 +23,15 @@ module Issues # to receive service desk emails on the new moved issue. update_service_desk_sent_notifications + queue_copy_designs + new_entity end private + attr_reader :target_project + def update_service_desk_sent_notifications return unless original_entity.from_service_desk? @@ -46,7 +50,7 @@ module Issues new_params = { id: nil, iid: nil, - project: @target_project, + project: target_project, author: original_entity.author, assignee_ids: original_entity.assignee_ids } @@ -58,6 +62,23 @@ module Issues CreateService.new(@target_project, @current_user, new_params).execute(skip_system_notes: true) end + def queue_copy_designs + return unless copy_designs_enabled? && original_entity.designs.present? + + response = DesignManagement::CopyDesignCollection::QueueService.new( + current_user, + original_entity, + new_entity + ).execute + + log_error(response.message) if response.error? + end + + def copy_designs_enabled? + Feature.enabled?(:design_management_copy_designs, old_project) && + Feature.enabled?(:design_management_copy_designs, target_project) + end + def mark_as_moved original_entity.update(moved_to: new_entity) end @@ -75,7 +96,7 @@ module Issues end def add_note_from - SystemNoteService.noteable_moved(new_entity, @target_project, + SystemNoteService.noteable_moved(new_entity, target_project, original_entity, current_user, direction: :from) end diff --git a/app/services/lfs/push_service.rb b/app/services/lfs/push_service.rb index 6e1a11ebff8..9b947fbed07 100644 --- a/app/services/lfs/push_service.rb +++ b/app/services/lfs/push_service.rb @@ -12,7 +12,7 @@ module Lfs def execute lfs_objects_relation.each_batch(of: BATCH_SIZE) do |objects| - push_objects(objects) + push_objects!(objects) end success @@ -30,8 +30,8 @@ module Lfs project.lfs_objects_for_repository_types(nil, :project) end - def push_objects(objects) - rsp = lfs_client.batch('upload', objects) + def push_objects!(objects) + rsp = lfs_client.batch!('upload', objects) objects = objects.index_by(&:oid) rsp.fetch('objects', []).each do |spec| @@ -53,14 +53,14 @@ module Lfs return end - lfs_client.upload(object, upload, authenticated: authenticated) + lfs_client.upload!(object, upload, authenticated: authenticated) end def verify_object!(object, spec) - # TODO: the remote has requested that we make another call to verify that - # the object has been sent correctly. - # https://gitlab.com/gitlab-org/gitlab/-/issues/250654 - log_error("LFS upload verification requested, but not supported for #{object.oid}") + authenticated = spec['authenticated'] + verify = spec.dig('actions', 'verify') + + lfs_client.verify!(object, verify, authenticated: authenticated) end def url diff --git a/app/services/members/invitation_reminder_email_service.rb b/app/services/members/invitation_reminder_email_service.rb new file mode 100644 index 00000000000..e589cdc2fa3 --- /dev/null +++ b/app/services/members/invitation_reminder_email_service.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Members + class InvitationReminderEmailService + include Gitlab::Utils::StrongMemoize + + attr_reader :invitation + + MAX_INVITATION_LIFESPAN = 14.0 + REMINDER_RATIO = [2, 5, 10].freeze + + def initialize(invitation) + @invitation = invitation + end + + def execute + return unless experiment_enabled? + + reminder_index = days_on_which_to_send_reminders.index(days_after_invitation_sent) + return unless reminder_index + + invitation.send_invitation_reminder(reminder_index) + end + + private + + def experiment_enabled? + Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, invitation.invite_email) + end + + def days_after_invitation_sent + (Date.today - invitation.created_at.to_date).to_i + end + + def days_on_which_to_send_reminders + # Don't send any reminders if the invitation has expired or expires today + return [] if invitation.expires_at && invitation.expires_at <= Date.today + + # Calculate the number of days on which to send reminders based on the MAX_INVITATION_LIFESPAN and the REMINDER_RATIO + REMINDER_RATIO.map { |number_of_days| ((number_of_days * invitation_lifespan_in_days) / MAX_INVITATION_LIFESPAN).ceil }.uniq + end + + def invitation_lifespan_in_days + # When the invitation lifespan is more than 14 days or does not expire, send the reminders within 14 days + strong_memoize(:invitation_lifespan_in_days) do + if invitation.expires_at + [(invitation.expires_at - invitation.created_at.to_date).to_i, MAX_INVITATION_LIFESPAN].min + else + MAX_INVITATION_LIFESPAN + end + end + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index abc3f99797d..aa591312c6a 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -110,6 +110,10 @@ module MergeRequests return end + unless merge_request.allows_multiple_reviewers? + params[:reviewer_ids] = params[:reviewer_ids].first(1) + end + reviewer_ids = params[:reviewer_ids].select { |reviewer_id| user_can_read?(merge_request, reviewer_id) } if params[:reviewer_ids].map(&:to_s) == [IssuableFinder::Params::NONE] @@ -130,6 +134,11 @@ module MergeRequests merge_request, merge_request.project, current_user, old_assignees) end + def create_reviewer_note(merge_request, old_reviewers) + SystemNoteService.change_issuable_reviewers( + merge_request, merge_request.project, current_user, old_reviewers) + end + def create_pipeline_for(merge_request, user) MergeRequests::CreatePipelineService.new(project, user).execute(merge_request) end diff --git a/app/services/merge_requests/export_csv_service.rb b/app/services/merge_requests/export_csv_service.rb new file mode 100644 index 00000000000..2755fc6687c --- /dev/null +++ b/app/services/merge_requests/export_csv_service.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module MergeRequests + class ExportCsvService + include Gitlab::Routing.url_helpers + include GitlabRoutingHelper + + # Target attachment size before base64 encoding + TARGET_FILESIZE = 15.megabytes + + def initialize(merge_requests) + @merge_requests = merge_requests + end + + def csv_data + csv_builder.render(TARGET_FILESIZE) + end + + private + + def csv_builder + @csv_builder ||= CsvBuilder.new(@merge_requests.with_csv_entity_associations, header_to_value_hash) + end + + def header_to_value_hash + { + 'MR IID' => 'iid', + 'URL' => -> (merge_request) { merge_request_url(merge_request) }, + 'Title' => 'title', + 'State' => 'state', + 'Description' => 'description', + 'Source Branch' => 'source_branch', + 'Target Branch' => 'target_branch', + 'Source Project ID' => 'source_project_id', + 'Target Project ID' => 'target_project_id', + 'Author' => -> (merge_request) { merge_request.author.name }, + 'Author Username' => -> (merge_request) { merge_request.author.username }, + 'Assignees' => -> (merge_request) { merge_request.assignees.map(&:name).join(', ') }, + 'Assignee Usernames' => -> (merge_request) { merge_request.assignees.map(&:username).join(', ') }, + 'Approvers' => -> (merge_request) { merge_request.approved_by_users.map(&:name).join(', ') }, + 'Approver Usernames' => -> (merge_request) { merge_request.approved_by_users.map(&:username).join(', ') }, + 'Merged User' => -> (merge_request) { merge_request.metrics&.merged_by&.name.to_s }, + 'Merged Username' => -> (merge_request) { merge_request.metrics&.merged_by&.username.to_s }, + 'Milestone ID' => -> (merge_request) { merge_request&.milestone&.id || '' }, + 'Created At (UTC)' => -> (merge_request) { merge_request.created_at.utc }, + 'Updated At (UTC)' => -> (merge_request) { merge_request.updated_at.utc } + } + end + end +end diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb index 79011094e88..c5640047899 100644 --- a/app/services/merge_requests/ff_merge_service.rb +++ b/app/services/merge_requests/ff_merge_service.rb @@ -27,7 +27,7 @@ module MergeRequests rescue StandardError => e raise MergeError, "Something went wrong during merge: #{e.message}" ensure - merge_request.update(in_progress_merge_commit_sha: nil) + merge_request.update_and_mark_in_progress_merge_commit_sha(nil) end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 437e87dadf7..ba22b458777 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -84,7 +84,7 @@ module MergeRequests merge_request.update!(merge_commit_sha: commit_id) ensure - merge_request.update_column(:in_progress_merge_commit_sha, nil) + merge_request.update_and_mark_in_progress_merge_commit_sha(nil) end def try_merge diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb index a3c39fa2e32..12c04772ef4 100644 --- a/app/services/merge_requests/mergeability_check_service.rb +++ b/app/services/merge_requests/mergeability_check_service.rb @@ -88,7 +88,7 @@ module MergeRequests sleep_sec: retry_lease ? 1.second : 0 } - in_lock(lease_key, lease_opts, &block) + in_lock(lease_key, **lease_opts, &block) end def payload diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 405b8fe9c9e..0873c20b99c 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -184,7 +184,7 @@ module MergeRequests def abort_auto_merge_with_todo(merge_request, reason) response = abort_auto_merge(merge_request, reason) - response = ServiceResponse.new(response) + response = ServiceResponse.new(**response) return unless response.success? todo_service.merge_request_became_unmergeable(merge_request) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 1468bfd6bb6..8c069ea5bb0 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -112,6 +112,8 @@ module MergeRequests end def handle_reviewers_change(merge_request, old_reviewers) + create_reviewer_note(merge_request, old_reviewers) + notification_service.async.changed_reviewer_of_merge_request(merge_request, current_user, old_reviewers) todo_service.reassigned_reviewable(merge_request, current_user, old_reviewers) end diff --git a/app/services/metrics/dashboard/custom_dashboard_service.rb b/app/services/metrics/dashboard/custom_dashboard_service.rb index f0f19bf2ba3..bde8e86851a 100644 --- a/app/services/metrics/dashboard/custom_dashboard_service.rb +++ b/app/services/metrics/dashboard/custom_dashboard_service.rb @@ -42,6 +42,12 @@ module Metrics def cache_key "project_#{project.id}_metrics_dashboard_#{dashboard_path}" end + + def sequence + [ + ::Gitlab::Metrics::Dashboard::Stages::CustomDashboardMetricsInserter + ] + super + end end end end diff --git a/app/services/namespace_settings/update_service.rb b/app/services/namespace_settings/update_service.rb new file mode 100644 index 00000000000..3c9b7b637ac --- /dev/null +++ b/app/services/namespace_settings/update_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module NamespaceSettings + class UpdateService + include ::Gitlab::Allowable + + attr_reader :current_user, :group, :settings_params + + def initialize(current_user, group, settings) + @current_user = current_user + @group = group + @settings_params = settings + end + + def execute + if group.namespace_settings + group.namespace_settings.attributes = settings_params + else + group.build_namespace_settings(settings_params) + end + end + end +end + +NamespaceSettings::UpdateService.prepend_if_ee('EE::NamespaceSettings::UpdateService') diff --git a/app/services/notification_recipients/builder/default.rb b/app/services/notification_recipients/builder/default.rb index 790ce57452c..19527ba84e6 100644 --- a/app/services/notification_recipients/builder/default.rb +++ b/app/services/notification_recipients/builder/default.rb @@ -34,6 +34,9 @@ module NotificationRecipients when :reassign_merge_request, :reassign_issue add_recipients(previous_assignees, :mention, nil) add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED) + when :change_reviewer_merge_request + add_recipients(previous_assignees, :mention, nil) + add_recipients(target.reviewers, :mention, NotificationReason::REVIEW_REQUESTED) end add_subscribed_users diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 731d72c41d4..f343433360e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -238,6 +238,33 @@ class NotificationService end end + # When we change reviewer in a merge_request we should send an email to: + # + # * merge_request old reviewers if their notification level is not Disabled + # * merge_request new reviewers if their notification level is not Disabled + # * users with custom level checked with "change reviewer merge request" + # + def changed_reviewer_of_merge_request(merge_request, current_user, previous_reviewers = []) + recipients = NotificationRecipients::BuildService.build_recipients( + merge_request, + current_user, + action: "change_reviewer", + previous_assignees: previous_reviewers + ) + + previous_reviewer_ids = previous_reviewers.map(&:id) + + recipients.each do |recipient| + mailer.changed_reviewer_of_merge_request_email( + recipient.user.id, + merge_request.id, + previous_reviewer_ids, + current_user.id, + recipient.reason + ).deliver_later + end + end + # When we add labels to a merge request we should send an email to: # # * watchers of the mr's labels diff --git a/app/services/packages/create_event_service.rb b/app/services/packages/create_event_service.rb new file mode 100644 index 00000000000..d009cba2812 --- /dev/null +++ b/app/services/packages/create_event_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Packages + class CreateEventService < BaseService + def execute + event_scope = scope.is_a?(::Packages::Package) ? scope.package_type : scope + + ::Packages::Event.create!( + event_type: event_name, + originator: current_user&.id, + originator_type: originator_type, + event_scope: event_scope + ) + end + + private + + def scope + params[:scope] + end + + def event_name + params[:event_name] + end + + def originator_type + case current_user + when User + :user + when DeployToken + :deploy_token + else + :guest + end + end + end +end diff --git a/app/services/packages/create_package_service.rb b/app/services/packages/create_package_service.rb index 397a5f74e0a..e3b0ad218e2 100644 --- a/app/services/packages/create_package_service.rb +++ b/app/services/packages/create_package_service.rb @@ -10,6 +10,7 @@ module Packages .with_package_type(package_type) .safe_find_or_create_by!(name: name, version: version) do |pkg| pkg.creator = package_creator + yield pkg if block_given? end end diff --git a/app/services/packages/generic/create_package_file_service.rb b/app/services/packages/generic/create_package_file_service.rb new file mode 100644 index 00000000000..4d49c63799f --- /dev/null +++ b/app/services/packages/generic/create_package_file_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Packages + module Generic + class CreatePackageFileService < BaseService + def execute + ::Packages::Package.transaction do + create_package_file(find_or_create_package) + end + end + + private + + def find_or_create_package + package_params = { + name: params[:package_name], + version: params[:package_version], + build: params[:build] + } + + ::Packages::Generic::FindOrCreatePackageService + .new(project, current_user, package_params) + .execute + end + + def create_package_file(package) + file_params = { + file: params[:file], + size: params[:file].size, + file_sha256: params[:file].sha256, + file_name: params[:file_name] + } + + ::Packages::CreatePackageFileService.new(package, file_params).execute + end + end + end +end diff --git a/app/services/packages/generic/find_or_create_package_service.rb b/app/services/packages/generic/find_or_create_package_service.rb new file mode 100644 index 00000000000..8a8459d167e --- /dev/null +++ b/app/services/packages/generic/find_or_create_package_service.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Packages + module Generic + class FindOrCreatePackageService < ::Packages::CreatePackageService + def execute + find_or_create_package!(::Packages::Package.package_types['generic']) do |package| + if params[:build].present? + package.build_info = Packages::BuildInfo.new(pipeline: params[:build].pipeline) + end + end + end + end + end +end diff --git a/app/services/pod_logs/base_service.rb b/app/services/pod_logs/base_service.rb index 8936f9b67a5..e4b6ad31e33 100644 --- a/app/services/pod_logs/base_service.rb +++ b/app/services/pod_logs/base_service.rb @@ -10,6 +10,8 @@ module PodLogs CACHE_KEY_GET_POD_LOG = 'get_pod_log' K8S_NAME_MAX_LENGTH = 253 + self.reactive_cache_work_type = :external_dependency + def id cluster.id end diff --git a/app/services/pod_logs/elasticsearch_service.rb b/app/services/pod_logs/elasticsearch_service.rb index f79562c8ab3..58d1bfbf835 100644 --- a/app/services/pod_logs/elasticsearch_service.rb +++ b/app/services/pod_logs/elasticsearch_service.rb @@ -11,7 +11,6 @@ module PodLogs :pod_logs, :filter_return_keys - self.reactive_cache_work_type = :external_dependency self.reactive_cache_worker_finder = ->(id, _cache_key, namespace, params) { new(::Clusters::Cluster.find(id), namespace, params: params) } private diff --git a/app/services/pod_logs/kubernetes_service.rb b/app/services/pod_logs/kubernetes_service.rb index b573ceae1aa..03b84f98973 100644 --- a/app/services/pod_logs/kubernetes_service.rb +++ b/app/services/pod_logs/kubernetes_service.rb @@ -17,7 +17,6 @@ module PodLogs :split_logs, :filter_return_keys - self.reactive_cache_work_type = :external_dependency self.reactive_cache_worker_finder = ->(id, _cache_key, namespace, params) { new(::Clusters::Cluster.find(id), namespace, params: params) } private diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb index bfce5f1ad63..affac45fc3d 100644 --- a/app/services/projects/alerting/notify_service.rb +++ b/app/services/projects/alerting/notify_service.rb @@ -7,43 +7,34 @@ module Projects include ::IncidentManagement::Settings def execute(token) + return bad_request unless valid_payload_size? return forbidden unless alerts_service_activated? return unauthorized unless valid_token?(token) - alert = process_alert + process_alert return bad_request unless alert.persisted? - process_incident_issues(alert) if process_issues? + process_incident_issues if process_issues? send_alert_email if send_email? ServiceResponse.success - rescue Gitlab::Alerting::NotificationPayloadParser::BadPayloadError - bad_request end private delegate :alerts_service, :alerts_service_activated?, to: :project - def am_alert_params - strong_memoize(:am_alert_params) do - Gitlab::AlertManagement::AlertParams.from_generic_alert(project: project, payload: params.to_h) - end - end - def process_alert - existing_alert = find_alert_by_fingerprint(am_alert_params[:fingerprint]) - - if existing_alert - process_existing_alert(existing_alert) + if alert.persisted? + process_existing_alert else create_alert end end - def process_existing_alert(alert) - if am_alert_params[:ended_at].present? - process_resolved_alert(alert) + def process_existing_alert + if incoming_payload.ends_at.present? + process_resolved_alert else alert.register_new_event! end @@ -51,10 +42,10 @@ module Projects alert end - def process_resolved_alert(alert) + def process_resolved_alert return unless auto_close_incident? - if alert.resolve(am_alert_params[:ended_at]) + if alert.resolve(incoming_payload.ends_at) close_issue(alert.issue) end @@ -72,20 +63,16 @@ module Projects end def create_alert - alert = AlertManagement::Alert.create(am_alert_params.except(:ended_at)) - alert.execute_services if alert.persisted? - SystemNoteService.create_new_alert(alert, 'Generic Alert Endpoint') - - alert - end - - def find_alert_by_fingerprint(fingerprint) - return unless fingerprint + return unless alert.save - AlertManagement::Alert.not_resolved.for_fingerprint(project, fingerprint).first + alert.execute_services + SystemNoteService.create_new_alert( + alert, + alert.monitoring_tool || 'Generic Alert Endpoint' + ) end - def process_incident_issues(alert) + def process_incident_issues return if alert.issue ::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id) @@ -94,11 +81,33 @@ module Projects def send_alert_email notification_service .async - .prometheus_alerts_fired(project, [parsed_payload]) + .prometheus_alerts_fired(project, [alert.attributes]) + end + + def alert + strong_memoize(:alert) do + existing_alert || new_alert + end + end + + def existing_alert + return unless incoming_payload.gitlab_fingerprint + + AlertManagement::Alert.not_resolved.for_fingerprint(project, incoming_payload.gitlab_fingerprint).first + end + + def new_alert + AlertManagement::Alert.new(**incoming_payload.alert_params, ended_at: nil) + end + + def incoming_payload + strong_memoize(:incoming_payload) do + Gitlab::AlertManagement::Payload.parse(project, params.to_h) + end end - def parsed_payload - Gitlab::Alerting::NotificationPayloadParser.call(params.to_h, project) + def valid_payload_size? + Gitlab::Utils::DeepSize.new(params).valid? end def valid_token?(token) diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb index 204a54ff23a..31500043544 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -25,7 +25,10 @@ module Projects tag_names = tags.map(&:name) Projects::ContainerRepository::DeleteTagsService - .new(container_repository.project, current_user, tags: tag_names) + .new(container_repository.project, + current_user, + tags: tag_names, + container_expiration_policy: params['container_expiration_policy']) .execute(container_repository) end diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb index a23a6a369b2..9fc3ec0aafb 100644 --- a/app/services/projects/container_repository/delete_tags_service.rb +++ b/app/services/projects/container_repository/delete_tags_service.rb @@ -7,7 +7,10 @@ module Projects def execute(container_repository) @container_repository = container_repository - return error('access denied') unless can?(current_user, :destroy_container_image, project) + + unless params[:container_expiration_policy] + return error('access denied') unless can?(current_user, :destroy_container_image, project) + end @tag_names = params[:tags] return error('not tags specified') if @tag_names.blank? @@ -23,9 +26,7 @@ module Projects end def delete_service - fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true) - - if fast_delete_enabled && @container_repository.client.supports_tag_delete? + if @container_repository.client.supports_tag_delete? ::Projects::ContainerRepository::Gitlab::DeleteTagsService.new(@container_repository, @tag_names) else ::Projects::ContainerRepository::ThirdParty::DeleteTagsService.new(@container_repository, @tag_names) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 68b40fdd8f1..6fc8e8f8935 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -19,6 +19,10 @@ module Projects @project = Project.new(params) + # If a project is newly created it should have shared runners settings + # based on its group having it enabled. This is like the "default value" + @project.shared_runners_enabled = false if !params.key?(:shared_runners_enabled) && @project.group && @project.group.shared_runners_setting != 'enabled' + # Make sure that the user is allowed to use the specified visibility level if project_visibility.restricted? deny_visibility_level(@project, project_visibility.visibility_level) @@ -162,7 +166,7 @@ module Projects if @project.save unless @project.gitlab_project_import? - create_services_from_active_instances_or_templates(@project) + Service.create_from_active_default_integrations(@project, :project_id, with_templates: true) @project.create_labels end @@ -228,15 +232,6 @@ module Projects private - # rubocop: disable CodeReuse/ActiveRecord - def create_services_from_active_instances_or_templates(project) - Service.active.where(instance: true).or(Service.active.where(template: true)).group_by(&:type).each do |type, records| - service = records.find(&:instance?) || records.find(&:template?) - Service.build_from_integration(project.id, service).save! - end - end - # rubocop: enable CodeReuse/ActiveRecord - def project_namespace @project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace end diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb index d32ead76d00..c002aca32db 100644 --- a/app/services/projects/prometheus/alerts/notify_service.rb +++ b/app/services/projects/prometheus/alerts/notify_service.rb @@ -125,7 +125,7 @@ module Projects notification_service .async - .prometheus_alerts_fired(project, firings) + .prometheus_alerts_fired(project, alerts_attributes) end def process_prometheus_alerts @@ -136,6 +136,18 @@ module Projects end end + def alerts_attributes + firings.map do |payload| + alert_params = Gitlab::AlertManagement::Payload.parse( + project, + payload, + monitoring_tool: Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] + ).alert_params + + AlertManagement::Alert.new(alert_params).attributes + end + end + def bad_request ServiceResponse.error(message: 'Bad Request', http_status: :bad_request) end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index dba5177718d..013861631a1 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -88,6 +88,10 @@ module Projects # Move uploads move_project_uploads(project) + # If a project is being transferred to another group it means it can already + # have shared runners enabled but we need to check whether the new group allows that. + project.shared_runners_enabled = false if project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable' + project.old_path_with_namespace = @old_path update_repository_configuration(@new_path) diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index 5c41f00aac2..40cf916e2f5 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -2,12 +2,14 @@ module Projects class UpdateRemoteMirrorService < BaseService + include Gitlab::Utils::StrongMemoize + MAX_TRIES = 3 def execute(remote_mirror, tries) return success unless remote_mirror.enabled? - if Gitlab::UrlBlocker.blocked_url?(CGI.unescape(Gitlab::UrlSanitizer.sanitize(remote_mirror.url))) + if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url)) return error("The remote mirror URL is invalid.") end @@ -27,6 +29,12 @@ module Projects private + def normalized_url(url) + strong_memoize(:normalized_url) do + CGI.unescape(Gitlab::UrlSanitizer.sanitize(url)) + end + end + def update_mirror(remote_mirror) remote_mirror.update_start! remote_mirror.ensure_remote! diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index bb430811497..d44f5e637f1 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -135,8 +135,8 @@ module Projects end def ensure_wiki_exists - ProjectWiki.new(project, project.owner).wiki - rescue Wiki::CouldNotCreateWikiError + return if project.create_wiki + log_error("Could not create wiki for #{project.full_name}") Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki').increment end diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb index 4273acfbf8b..a465632ccfb 100644 --- a/app/services/quick_actions/target_service.rb +++ b/app/services/quick_actions/target_service.rb @@ -17,12 +17,16 @@ module QuickActions # rubocop: disable CodeReuse/ActiveRecord def issue(type_id) + return project.issues.build if type_id.nil? + IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.issues.build end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def merge_request(type_id) + return project.merge_requests.build if type_id.nil? + MergeRequestsFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.merge_requests.build end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb index c253154c1b7..4ff8973773d 100644 --- a/app/services/resource_access_tokens/create_service.rb +++ b/app/services/resource_access_tokens/create_service.rb @@ -32,20 +32,11 @@ module ResourceAccessTokens attr_reader :resource_type, :resource def feature_enabled? - return false if ::Gitlab.com? - - ::Feature.enabled?(:resource_access_token, resource, default_enabled: true) + return true unless ::Gitlab.com? end def has_permission_to_create? - case resource_type - when 'project' - can?(current_user, :admin_project, resource) - when 'group' - can?(current_user, :admin_group, resource) - else - false - end + %w(project group).include?(resource_type) && can?(current_user, :admin_resource_access_tokens, resource) end def create_user diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index fab02697cf0..5f80b07aa59 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -4,6 +4,8 @@ module Search class GlobalService include Gitlab::Utils::StrongMemoize + ALLOWED_SCOPES = %w(issues merge_requests milestones users).freeze + attr_accessor :current_user, :params def initialize(user, params) @@ -14,7 +16,8 @@ module Search Gitlab::SearchResults.new(current_user, params[:search], projects, - filters: { state: params[:state] }) + sort: params[:sort], + filters: { state: params[:state], confidential: params[:confidential] }) end def projects @@ -22,10 +25,7 @@ module Search end def allowed_scopes - strong_memoize(:allowed_scopes) do - allowed_scopes = %w[issues merge_requests milestones] - allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) - end + ALLOWED_SCOPES end def scope diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index 68778aa2768..24409a04e74 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -16,7 +16,7 @@ module Search params[:search], projects, group: group, - filters: { state: params[:state] } + filters: { state: params[:state], confidential: params[:confidential] } ) end diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index 5eba909c23b..b1142b816d0 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -2,6 +2,10 @@ module Search class ProjectService + include Gitlab::Utils::StrongMemoize + + ALLOWED_SCOPES = %w(notes issues merge_requests milestones wiki_blobs commits users).freeze + attr_accessor :project, :current_user, :params def initialize(project, user, params) @@ -13,15 +17,17 @@ module Search params[:search], project: project, repository_ref: params[:repository_ref], - filters: { state: params[:state] }) + filters: { confidential: params[:confidential], state: params[:state] } + ) end - def scope - @scope ||= begin - allowed_scopes = %w[notes issues merge_requests milestones wiki_blobs commits] - allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) + def allowed_scopes + ALLOWED_SCOPES + end - allowed_scopes.delete(params[:scope]) { 'blobs' } + def scope + strong_memoize(:scope) do + allowed_scopes.include?(params[:scope]) ? params[:scope] : 'blobs' end end end diff --git a/app/services/snippets/base_service.rb b/app/services/snippets/base_service.rb index 53a04e5a398..278857b7933 100644 --- a/app/services/snippets/base_service.rb +++ b/app/services/snippets/base_service.rb @@ -4,6 +4,9 @@ module Snippets class BaseService < ::BaseService include SpamCheckMethods + UPDATE_COMMIT_MSG = 'Update snippet' + INITIAL_COMMIT_MSG = 'Initial commit' + CreateRepositoryError = Class.new(StandardError) attr_reader :uploaded_assets, :snippet_actions @@ -85,5 +88,20 @@ module Snippets def restricted_files_actions nil end + + def commit_attrs(snippet, msg) + { + branch_name: snippet.default_branch, + message: msg + } + end + + def delete_repository(snippet) + snippet.repository.remove + snippet.snippet_repository&.delete + + # Purge any existing value for repository_exists? + snippet.repository.expire_exists_cache + end end end diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 5c9b2eb1aea..d7181883c39 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -59,7 +59,7 @@ module Snippets log_error(e.message) # If the commit action failed we need to remove the repository if exists - @snippet.repository.remove if @snippet.repository_exists? + delete_repository(@snippet) if @snippet.repository_exists? # If the snippet was created, we need to remove it as we # would do like if it had had any validation error @@ -81,12 +81,9 @@ module Snippets end def create_commit - commit_attrs = { - branch_name: @snippet.default_branch, - message: 'Initial commit' - } + attrs = commit_attrs(@snippet, INITIAL_COMMIT_MSG) - @snippet.snippet_repository.multi_files_action(current_user, files_to_commit(@snippet), commit_attrs) + @snippet.snippet_repository.multi_files_action(current_user, files_to_commit(@snippet), **attrs) end def move_temporary_files diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb index a0e9ab6ffda..0115cd19287 100644 --- a/app/services/snippets/update_service.rb +++ b/app/services/snippets/update_service.rb @@ -37,7 +37,10 @@ module Snippets # is implemented. # Once we can perform different operations through this service # we won't need to keep track of the `content` and `file_name` fields - if snippet_actions.any? + # + # If the repository does not exist we don't need to update `params` + # because we need to commit the information from the database + if snippet_actions.any? && snippet.repository_exists? params[:content] = snippet_actions[0].content if snippet_actions[0].content params[:file_name] = snippet_actions[0].file_path end @@ -52,7 +55,11 @@ module Snippets # the repository we can just return return true unless committable_attributes? - create_repository_for(snippet) + unless snippet.repository_exists? + create_repository_for(snippet) + create_first_commit_using_db_data(snippet) + end + create_commit(snippet) true @@ -72,13 +79,7 @@ module Snippets # If the commit action failed we remove it because # we don't want to leave empty repositories # around, to allow cloning them. - if repository_empty?(snippet) - snippet.repository.remove - snippet.snippet_repository&.delete - end - - # Purge any existing value for repository_exists? - snippet.repository.expire_exists_cache + delete_repository(snippet) if repository_empty?(snippet) false end @@ -89,15 +90,25 @@ module Snippets raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists? end + # If the user provides `snippet_actions` and the repository + # does not exist, we need to commit first the snippet info stored + # in the database. Mostly because the content inside `snippet_actions` + # would assume that the file is already in the repository. + def create_first_commit_using_db_data(snippet) + return if snippet_actions.empty? + + attrs = commit_attrs(snippet, INITIAL_COMMIT_MSG) + actions = [{ file_path: snippet.file_name, content: snippet.content }] + + snippet.snippet_repository.multi_files_action(current_user, actions, **attrs) + end + def create_commit(snippet) raise UpdateError unless snippet.snippet_repository - commit_attrs = { - branch_name: snippet.default_branch, - message: 'Update snippet' - } + attrs = commit_attrs(snippet, UPDATE_COMMIT_MSG) - snippet.snippet_repository.multi_files_action(current_user, files_to_commit(snippet), commit_attrs) + snippet.snippet_repository.multi_files_action(current_user, files_to_commit(snippet), **attrs) end # Because we are removing repositories we don't want to remove diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb index b745b67f566..ab83fc401e9 100644 --- a/app/services/spam/spam_action_service.rb +++ b/app/services/spam/spam_action_service.rb @@ -45,7 +45,7 @@ module Spam attr_reader :user, :context def allowlisted?(user) - user.respond_to?(:gitlab_employee) && user.gitlab_employee? + user.try(:gitlab_employee?) || user.try(:gitlab_bot?) end def perform_spam_service_check(api) diff --git a/app/services/static_site_editor/config_service.rb b/app/services/static_site_editor/config_service.rb index 987ee071976..7b3115468a5 100644 --- a/app/services/static_site_editor/config_service.rb +++ b/app/services/static_site_editor/config_service.rb @@ -4,18 +4,38 @@ module StaticSiteEditor class ConfigService < ::BaseContainerService ValidationError = Class.new(StandardError) - def execute + def initialize(container:, current_user: nil, params: {}) + super + @project = container + @repository = project.repository + @ref = params.fetch(:ref) + end + + def execute check_access! + file_config = load_file_config! + file_data = file_config.to_hash_with_defaults + generated_data = load_generated_config.data + + check_for_duplicate_keys!(generated_data, file_data) + data = merged_data(generated_data, file_data) + ServiceResponse.success(payload: data) rescue ValidationError => e ServiceResponse.error(message: e.message) + rescue => e + Gitlab::ErrorTracking.track_and_raise_exception(e) end private - attr_reader :project + attr_reader :project, :repository, :ref + + def static_site_editor_config_file + '.gitlab/static-site-editor.yml' + end def check_access! unless can?(current_user, :download_code, project) @@ -23,27 +43,43 @@ module StaticSiteEditor end end - def data - check_for_duplicate_keys! - generated_data.merge(file_data) + def load_file_config! + yaml = yaml_from_repo.presence || '{}' + file_config = Gitlab::StaticSiteEditor::Config::FileConfig.new(yaml) + + unless file_config.valid? + raise ValidationError, file_config.errors.first + end + + file_config + rescue Gitlab::StaticSiteEditor::Config::FileConfig::ConfigError => e + raise ValidationError, e.message end - def generated_data - @generated_data ||= Gitlab::StaticSiteEditor::Config::GeneratedConfig.new( - project.repository, - params.fetch(:ref), + def load_generated_config + Gitlab::StaticSiteEditor::Config::GeneratedConfig.new( + repository, + ref, params.fetch(:path), params[:return_url] - ).data - end - - def file_data - @file_data ||= Gitlab::StaticSiteEditor::Config::FileConfig.new.data + ) end - def check_for_duplicate_keys! + def check_for_duplicate_keys!(generated_data, file_data) duplicate_keys = generated_data.keys & file_data.keys raise ValidationError.new("Duplicate key(s) '#{duplicate_keys}' found.") if duplicate_keys.present? end + + def merged_data(generated_data, file_data) + generated_data.merge(file_data) + end + + def yaml_from_repo + repository.blob_data_at(ref, static_site_editor_config_file) + rescue GRPC::NotFound + # Return nil in the case of a GRPC::NotFound exception, so the default config will be used. + # Allow any other unexpected exception will be tracked and re-raised. + nil + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index df042fdc393..1a4374f2e94 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -41,6 +41,10 @@ module SystemNoteService ::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_assignees(old_assignees) end + def change_issuable_reviewers(issuable, project, author, old_reviewers) + ::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_reviewers(old_reviewers) + end + def relate_issue(noteable, noteable_ref, user) ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref) end @@ -308,6 +312,10 @@ module SystemNoteService ::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project).create_new_alert(monitoring_tool) end + def change_incident_severity(incident, author) + ::SystemNotes::IncidentService.new(noteable: incident, project: incident.project, author: author).change_incident_severity + end + private def merge_requests_service(noteable, project, author) diff --git a/app/services/system_notes/incident_service.rb b/app/services/system_notes/incident_service.rb new file mode 100644 index 00000000000..4628662f0e9 --- /dev/null +++ b/app/services/system_notes/incident_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module SystemNotes + class IncidentService < ::SystemNotes::BaseService + # Called when the severity of an Incident has changed + # + # Example Note text: + # + # "changed the severity to Medium - S3" + # + # Returns the created Note object + def change_incident_severity + severity = noteable.severity + + if severity_label = IssuableSeverity::SEVERITY_LABELS[severity.to_sym] + body = "changed the severity to **#{severity_label}**" + + create_note(NoteSummary.new(noteable, project, author, body, action: 'severity')) + else + Gitlab::AppLogger.error( + message: 'Cannot create a system note for severity change', + noteable_class: noteable.class.to_s, + noteable_id: noteable.id, + severity: severity + ) + end + end + end +end diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index 2252503d97e..784bd6b9699 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -81,6 +81,32 @@ module SystemNotes create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee')) end + # Called when the reviewers of an issuable is changed or removed + # + # reviewers - Users being requested to review, or nil + # + # Example Note text: + # + # "requested review from @user1 and @user2" + # + # "requested review from @user1, @user2 and @user3 and removed review request for @user4 and @user5" + # + # Returns the created Note object + def change_issuable_reviewers(old_reviewers) + unassigned_users = old_reviewers - noteable.reviewers + added_users = noteable.reviewers - old_reviewers + text_parts = [] + + Gitlab::I18n.with_default_locale do + text_parts << "requested review from #{added_users.map(&:to_reference).to_sentence}" if added_users.any? + text_parts << "removed review request for #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any? + end + + body = text_parts.join(' and ') + + create_note(NoteSummary.new(noteable, project, author, body, action: 'reviewer')) + end + # Called when the title of a Noteable is changed # # old_title - Previous String title @@ -242,19 +268,7 @@ module SystemNotes # # Returns the created Note object def change_status(status, source = nil) - body = status.dup - body << " via #{source.gfm_reference(project)}" if source - - action = status == 'reopened' ? 'opened' : status - - # A state event which results in a synthetic note will be - # created by EventCreateService if change event tracking - # is enabled. - if state_change_tracking_enabled? - create_resource_state_event(status: status, mentionable_source: source) - else - create_note(NoteSummary.new(noteable, project, author, body, action: action)) - end + create_resource_state_event(status: status, mentionable_source: source) end # Check if a cross reference to a noteable from a mentioner already exists @@ -312,23 +326,11 @@ module SystemNotes end def close_after_error_tracking_resolve - if state_change_tracking_enabled? - create_resource_state_event(status: 'closed', close_after_error_tracking_resolve: true) - else - body = 'resolved the corresponding error and closed the issue.' - - create_note(NoteSummary.new(noteable, project, author, body, action: 'closed')) - end + create_resource_state_event(status: 'closed', close_after_error_tracking_resolve: true) end def auto_resolve_prometheus_alert - if state_change_tracking_enabled? - create_resource_state_event(status: 'closed', close_auto_resolve_prometheus_alert: true) - else - body = 'automatically closed this issue because the alert resolved.' - - create_note(NoteSummary.new(noteable, project, author, body, action: 'closed')) - end + create_resource_state_event(status: 'closed', close_auto_resolve_prometheus_alert: true) end private @@ -361,11 +363,6 @@ module SystemNotes .execute(params) end - def state_change_tracking_enabled? - noteable.respond_to?(:resource_state_events) && - ::Feature.enabled?(:track_resource_state_change_events, noteable.project, default_enabled: true) - end - def issue_activity_counter Gitlab::UsageDataCounters::IssueActivityUniqueCounter end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 2fc46f033dd..e3f02bf85f0 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -104,7 +104,6 @@ module Users def build_user_params(skip_authorization:) if current_user&.admin? user_params = params.slice(*admin_create_params) - user_params[:created_by_id] = current_user&.id if params[:reset_password] user_params.merge!(force_random_password: true, password_expires_at: nil) @@ -125,6 +124,8 @@ module Users end end + user_params[:created_by_id] = current_user&.id + if user_default_internal_regex_enabled? && !user_params.key?(:external) user_params[:external] = user_external? end |