diff options
Diffstat (limited to 'app')
30 files changed, 435 insertions, 89 deletions
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 30f1e843e7b..8fd377938b4 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -64,6 +64,7 @@ export default { this.groupId, term, { + search_namespaces: true, with_issues_enabled: true, with_shared: false, include_subgroups: true, diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 66ce1ab5659..15c7c09366c 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -54,6 +54,7 @@ const projectSelect = () => { this.groupId, query.term, { + search_namespaces: true, with_issues_enabled: this.withIssuesEnabled, with_merge_requests_enabled: this.withMergeRequestsEnabled, with_shared: this.withShared, diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 03766c4877e..6c58f48dc74 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -134,7 +134,9 @@ export default { }, { attrs: { - href: `${this.newBlobPath}/${this.currentPath ? escape(this.currentPath) : ''}`, + href: `${this.newBlobPath}/${ + this.currentPath ? encodeURIComponent(this.currentPath) : '' + }`, class: 'qa-new-file-option', }, text: __('New file'), diff --git a/app/assets/javascripts/repository/components/table/parent_row.vue b/app/assets/javascripts/repository/components/table/parent_row.vue index a5c6c9822fb..f9fcbc356e8 100644 --- a/app/assets/javascripts/repository/components/table/parent_row.vue +++ b/app/assets/javascripts/repository/components/table/parent_row.vue @@ -25,10 +25,10 @@ export default { const splitArray = this.path.split('/'); splitArray.pop(); - return splitArray.join('/'); + return splitArray.map(p => encodeURIComponent(p)).join('/'); }, parentRoute() { - return { path: `/-/tree/${escape(this.commitRef)}/${escape(this.parentPath)}` }; + return { path: `/-/tree/${escape(this.commitRef)}/${this.parentPath}` }; }, }, methods: { diff --git a/app/finders/autocomplete/move_to_project_finder.rb b/app/finders/autocomplete/move_to_project_finder.rb index 491cce2232e..af6defc1fc6 100644 --- a/app/finders/autocomplete/move_to_project_finder.rb +++ b/app/finders/autocomplete/move_to_project_finder.rb @@ -25,7 +25,7 @@ module Autocomplete def execute current_user .projects_where_can_admin_issues - .optionally_search(search) + .optionally_search(search, include_namespace: true) .excluding_project(project_id) .eager_load_namespace_and_owner .sorted_by_name_asc_limited(LIMIT) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index c319d2fed87..961694bd91f 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -17,6 +17,7 @@ # tags: string[] # personal: boolean # search: string +# search_namespaces: boolean # non_archived: boolean # archived: 'only' or boolean # min_access_level: integer @@ -171,7 +172,7 @@ class ProjectsFinder < UnionFinder def by_search(items) params[:search] ||= params[:name] - params[:search].present? ? items.search(params[:search]) : items + items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?) end def by_deleted_status(items) diff --git a/app/models/concerns/alert_event_lifecycle.rb b/app/models/concerns/alert_event_lifecycle.rb new file mode 100644 index 00000000000..4d2b717ead2 --- /dev/null +++ b/app/models/concerns/alert_event_lifecycle.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module AlertEventLifecycle + extend ActiveSupport::Concern + + included do + validates :started_at, presence: true + validates :status, presence: true + + state_machine :status, initial: :none do + state :none, value: nil + + state :firing, value: 0 do + validates :payload_key, presence: true + validates :ended_at, absence: true + end + + state :resolved, value: 1 do + validates :ended_at, presence: true + end + + event :fire do + transition none: :firing + end + + event :resolve do + transition firing: :resolved + end + + before_transition to: :firing do |alert_event, transition| + started_at = transition.args.first + alert_event.started_at = started_at + end + + before_transition to: :resolved do |alert_event, transition| + ended_at = transition.args.first + alert_event.ended_at = ended_at || Time.current + end + end + + scope :firing, -> { where(status: status_value_for(:firing)) } + scope :resolved, -> { where(status: status_value_for(:resolved)) } + + scope :count_by_project_id, -> { group(:project_id).count } + + def self.status_value_for(name) + state_machines[:status].states[name].value + end + end +end diff --git a/app/models/concerns/optionally_search.rb b/app/models/concerns/optionally_search.rb index 4093429e372..06f8c3dc1cb 100644 --- a/app/models/concerns/optionally_search.rb +++ b/app/models/concerns/optionally_search.rb @@ -12,8 +12,8 @@ module OptionallySearch end # Optionally limits a result set to those matching the given search query. - def optionally_search(query = nil) - query.present? ? search(query) : all + def optionally_search(query = nil, **options) + query.present? ? search(query, **options) : all end end end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 01cb5a14762..7373f006d64 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -19,7 +19,6 @@ module ProtectedRefAccess end included do - scope :master, -> { maintainer } # @deprecated scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) } scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } scope :by_user, -> (user) { where(user_id: user ) } diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb index 333c9118aa5..4fae36f7b8d 100644 --- a/app/models/concerns/select_for_project_authorization.rb +++ b/app/models/concerns/select_for_project_authorization.rb @@ -11,8 +11,5 @@ module SelectForProjectAuthorization def select_as_maintainer_for_project_authorization select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"]) end - - # @deprecated - alias_method :select_as_master_for_project_authorization, :select_as_maintainer_for_project_authorization end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 3f9247b1544..e9b1c55726d 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -14,6 +14,7 @@ class Environment < ApplicationRecord has_many :successful_deployments, -> { success }, class_name: 'Deployment' has_many :active_deployments, -> { active }, class_name: 'Deployment' has_many :prometheus_alerts, inverse_of: :environment + has_many :self_managed_prometheus_alert_events, inverse_of: :environment has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus' diff --git a/app/models/group.rb b/app/models/group.rb index e9b3e3c3369..da69f7cc11e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -245,9 +245,6 @@ class Group < Namespace add_user(user, :maintainer, current_user: current_user) end - # @deprecated - alias_method :add_master, :add_maintainer - def add_owner(user, current_user = nil) add_user(user, :owner, current_user: current_user) end @@ -274,9 +271,6 @@ class Group < Namespace ::ContainerRepository.for_group_and_its_subgroups(self).exists? end - # @deprecated - alias_method :has_master?, :has_maintainer? - # Check if user is a last owner of the group. def last_owner?(user) has_owner?(user) && members_with_parents.owners.size == 1 diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 68ef84223c5..e1966eda277 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class LfsObjectsProject < ApplicationRecord + include ::EachBatch + belongs_to :project belongs_to :lfs_object diff --git a/app/models/member.rb b/app/models/member.rb index 99dee67346e..089efcb81dd 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -76,10 +76,8 @@ class Member < ApplicationRecord scope :developers, -> { active.where(access_level: DEVELOPER) } scope :maintainers, -> { active.where(access_level: MAINTAINER) } scope :non_guests, -> { where('members.access_level > ?', GUEST) } - scope :masters, -> { maintainers } # @deprecated - scope :owners, -> { active.where(access_level: OWNER) } + scope :owners, -> { active.where(access_level: OWNER) } scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } - scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated scope :with_user, -> (user) { where(user: user) } scope :with_source_id, ->(source_id) { where(source_id: source_id) } diff --git a/app/models/project.rb b/app/models/project.rb index ffdd13b72d5..34c9c7320be 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -255,6 +255,8 @@ class Project < ApplicationRecord has_many :prometheus_metrics has_many :prometheus_alerts, inverse_of: :project + has_many :prometheus_alert_events, inverse_of: :project + has_many :self_managed_prometheus_alert_events, inverse_of: :project # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy @@ -349,7 +351,6 @@ class Project < ApplicationRecord delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team - delegate :add_master, to: :team # @deprecated delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings delegate :root_ancestor, to: :namespace, allow_nil: true delegate :last_pipeline, to: :commit, allow_nil: true @@ -591,9 +592,9 @@ class Project < ApplicationRecord # case-insensitive. # # query - The search query as a String. - def search(query) - if Feature.enabled?(:project_search_by_full_path, default_enabled: true) - joins(:route).fuzzy_search(query, [Route.arel_table[:path], :name, :description]) + def search(query, include_namespace: false) + if include_namespace && Feature.enabled?(:project_search_by_full_path, default_enabled: true) + joins(:route).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name], :description]) else fuzzy_search(query, [:path, :name, :description]) end diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index bc16a34612a..b4071c6d4a6 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -7,7 +7,6 @@ class ProjectGroupLink < ApplicationRecord REPORTER = 20 DEVELOPER = 30 MAINTAINER = 40 - MASTER = MAINTAINER # @deprecated belongs_to :project belongs_to :group diff --git a/app/models/project_team.rb b/app/models/project_team.rb index de1fc55ba93..072d281e5f8 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -25,9 +25,6 @@ class ProjectTeam add_user(user, :maintainer, current_user: current_user) end - # @deprecated - alias_method :add_master, :add_maintainer - def add_role(user, role, current_user: nil) public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend end @@ -98,9 +95,6 @@ class ProjectTeam @maintainers ||= fetch_members(Gitlab::Access::MAINTAINER) end - # @deprecated - alias_method :masters, :maintainers - def owners @owners ||= if group @@ -156,9 +150,6 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MAINTAINER end - # @deprecated - alias_method :master?, :maintainer? - # Checks if `user` is authorized for this project, with at least the # `min_access_level` (if given). def member?(user, min_access_level = Gitlab::Access::GUEST) diff --git a/app/models/prometheus_alert_event.rb b/app/models/prometheus_alert_event.rb new file mode 100644 index 00000000000..7e61f6d5e3c --- /dev/null +++ b/app/models/prometheus_alert_event.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class PrometheusAlertEvent < ApplicationRecord + include AlertEventLifecycle + + belongs_to :project, optional: false, validate: true, inverse_of: :prometheus_alert_events + belongs_to :prometheus_alert, optional: false, validate: true, inverse_of: :prometheus_alert_events + has_and_belongs_to_many :related_issues, class_name: 'Issue', join_table: :issues_prometheus_alert_events # rubocop:disable Rails/HasAndBelongsToMany + + validates :payload_key, uniqueness: { scope: :prometheus_alert_id } + validates :started_at, presence: true + + delegate :title, :prometheus_metric_id, to: :prometheus_alert + + scope :for_environment, -> (environment) do + joins(:prometheus_alert).where(prometheus_alerts: { environment_id: environment }) + end + + scope :with_prometheus_alert, -> { includes(:prometheus_alert) } + + def self.last_by_project_id + ids = select(arel_table[:id].maximum.as('id')).group(:project_id).map(&:id) + with_prometheus_alert.find(ids) + end + + def self.find_or_initialize_by_payload_key(project, alert, payload_key) + find_or_initialize_by(project: project, prometheus_alert: alert, payload_key: payload_key) + end + + def self.find_by_payload_key(payload_key) + find_by(payload_key: payload_key) + end + + def self.status_value_for(name) + state_machines[:status].states[name].value + end + + def self.payload_key_for(gitlab_alert_id, started_at) + plain = [gitlab_alert_id, started_at].join('/') + + Digest::SHA1.hexdigest(plain) + end +end diff --git a/app/models/self_managed_prometheus_alert_event.rb b/app/models/self_managed_prometheus_alert_event.rb new file mode 100644 index 00000000000..d2d4a5c37d4 --- /dev/null +++ b/app/models/self_managed_prometheus_alert_event.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class SelfManagedPrometheusAlertEvent < ApplicationRecord + include AlertEventLifecycle + + belongs_to :project, validate: true, inverse_of: :self_managed_prometheus_alert_events + belongs_to :environment, validate: true, inverse_of: :self_managed_prometheus_alert_events + has_and_belongs_to_many :related_issues, class_name: 'Issue', join_table: :issues_self_managed_prometheus_alert_events # rubocop:disable Rails/HasAndBelongsToMany + + validates :started_at, presence: true + validates :payload_key, uniqueness: { scope: :project_id } + + def self.find_or_initialize_by_payload_key(project, payload_key) + find_or_initialize_by(project: project, payload_key: payload_key) do |event| + yield event if block_given? + end + end + + def self.payload_key_for(started_at, alert_name, query_expression) + plain = [started_at, alert_name, query_expression].join('/') + + Digest::SHA1.hexdigest(plain) + end +end diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb index f879f58b5a3..e60dbb4d141 100644 --- a/app/models/snippet_repository.rb +++ b/app/models/snippet_repository.rb @@ -48,15 +48,27 @@ class SnippetRepository < ApplicationRecord next_index = get_last_empty_file_index + 1 files.each do |file_entry| + file_entry[:file_path] = file_path_for(file_entry, next_index) { next_index += 1 } file_entry[:action] = infer_action(file_entry) unless file_entry[:action] - - if file_entry[:file_path].blank? - file_entry[:file_path] = build_empty_file_name(next_index) - next_index += 1 - end end end + def file_path_for(file_entry, next_index) + return file_entry[:file_path] if file_entry[:file_path].present? + return file_entry[:previous_path] if reuse_previous_path?(file_entry) + + build_empty_file_name(next_index).tap { yield } + end + + # If the user removed the file_path and the previous_path + # matches the EMPTY_FILE_PATTERN, we don't need to + # rename the file and build a new empty file name, + # we can just reuse the existing file name + def reuse_previous_path?(file_entry) + file_entry[:file_path].blank? && + EMPTY_FILE_PATTERN.match?(file_entry[:previous_path]) + end + def infer_action(file_entry) return :create if file_entry[:previous_path].blank? diff --git a/app/models/user.rb b/app/models/user.rb index 48438d0e7e2..7789326e8fa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1693,9 +1693,6 @@ class User < ApplicationRecord end end - # @deprecated - alias_method :owned_or_masters_groups, :owned_or_maintainers_groups - protected # override, from Devise::Validatable diff --git a/app/serializers/prometheus_alert_entity.rb b/app/serializers/prometheus_alert_entity.rb new file mode 100644 index 00000000000..413be511903 --- /dev/null +++ b/app/serializers/prometheus_alert_entity.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class PrometheusAlertEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :title + expose :query + expose :threshold + + expose :operator do |prometheus_alert| + prometheus_alert.computed_operator + end + + expose :alert_path do |prometheus_alert| + project_prometheus_alert_path(prometheus_alert.project, prometheus_alert.prometheus_metric_id, environment_id: prometheus_alert.environment.id, format: :json) + end + + private + + alias_method :prometheus_alert, :object + + def can_read_prometheus_alerts? + can?(request.current_user, :read_prometheus_alerts, prometheus_alert.project) + end +end diff --git a/app/serializers/prometheus_alert_serializer.rb b/app/serializers/prometheus_alert_serializer.rb new file mode 100644 index 00000000000..4dafb7216db --- /dev/null +++ b/app/serializers/prometheus_alert_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class PrometheusAlertSerializer < BaseSerializer + entity PrometheusAlertEntity +end diff --git a/app/services/metrics/dashboard/update_dashboard_service.rb b/app/services/metrics/dashboard/update_dashboard_service.rb index 65e6e195f79..25a727ad44c 100644 --- a/app/services/metrics/dashboard/update_dashboard_service.rb +++ b/app/services/metrics/dashboard/update_dashboard_service.rb @@ -12,7 +12,8 @@ module Metrics steps :check_push_authorized, :check_branch_name, :check_file_type, - :update_file + :update_file, + :create_merge_request def execute execute_steps @@ -49,6 +50,23 @@ module Metrics end end + def create_merge_request(result) + return success(result) if project.default_branch == branch + + merge_request_params = { + source_branch: branch, + target_branch: project.default_branch, + title: params[:commit_message] + } + merge_request = ::MergeRequests::CreateService.new(project, current_user, merge_request_params).execute + + if merge_request.persisted? + success(result.merge(merge_request: Gitlab::UrlBuilder.build(merge_request))) + else + error(merge_request.errors.full_messages.join(','), :bad_request) + end + end + def push_authorized? Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch) end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 4a0d85038ee..80bc4485988 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -17,57 +17,72 @@ module Notes # We execute commands (extracted from `params[:note]`) on the noteable # **before** we save the note because if the note consists of commands # only, there is no need be create a note! - quick_actions_service = QuickActionsService.new(project, current_user) - if quick_actions_service.supported?(note) - content, update_params, message = quick_actions_service.execute(note, quick_action_options) + execute_quick_actions(note) do |only_commands| + note.run_after_commit do + # Finish the harder work in the background + NewNoteWorker.perform_async(note.id) + end - only_commands = content.empty? + note_saved = note.with_transaction_returning_status do + !only_commands && note.save + end - note.note = content + when_saved(note) if note_saved end - note.run_after_commit do - # Finish the harder work in the background - NewNoteWorker.perform_async(note.id) - end + note + end - note_saved = note.with_transaction_returning_status do - !only_commands && note.save - end + private - if note_saved - if note.part_of_discussion? && note.discussion.can_convert_to_discussion? - note.discussion.convert_to_discussion!(save: true) - end + def execute_quick_actions(note) + return yield(false) unless quick_actions_service.supported?(note) - todo_service.new_note(note, current_user) - clear_noteable_diffs_cache(note) - Suggestions::CreateService.new(note).execute - increment_usage_counter(note) + content, update_params, message = quick_actions_service.execute(note, quick_action_options) + only_commands = content.empty? + note.note = content - if Feature.enabled?(:notes_create_service_tracking, project) - Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note)) - end - end + yield(only_commands) - if quick_actions_service.commands_executed_count.to_i > 0 - if update_params.present? - quick_actions_service.apply_updates(update_params, note) - note.commands_changes = update_params - end + do_commands(note, update_params, message, only_commands) + end - # We must add the error after we call #save because errors are reset - # when #save is called - if only_commands - note.errors.add(:commands_only, message.presence || _('Failed to apply commands.')) - end + def quick_actions_service + @quick_actions_service ||= QuickActionsService.new(project, current_user) + end + + def when_saved(note) + if note.part_of_discussion? && note.discussion.can_convert_to_discussion? + note.discussion.convert_to_discussion!(save: true) end - note + todo_service.new_note(note, current_user) + clear_noteable_diffs_cache(note) + Suggestions::CreateService.new(note).execute + increment_usage_counter(note) + + if Feature.enabled?(:notes_create_service_tracking, project) + Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note)) + end end - private + def do_commands(note, update_params, message, only_commands) + return if quick_actions_service.commands_executed_count.to_i.zero? + + if update_params.present? + quick_actions_service.apply_updates(update_params, note) + note.commands_changes = update_params + end + + # We must add the error after we call #save because errors are reset + # when #save is called + if only_commands + note.errors.add(:commands_only, message.presence || _('Failed to apply commands.')) + # Allow consumers to detect problems applying commands + note.errors.add(:commands, _('Failed to apply commands.')) unless message.present? + end + end # EE::Notes::CreateService would override this method def quick_action_options diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 3070e7b0e53..ed08f693901 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -55,6 +55,8 @@ module Notes # We must add the error after we call #save because errors are reset # when #save is called note.errors.add(:commands_only, message.presence || _('Commands did not apply')) + # Allow consumers to detect problems applying commands + note.errors.add(:commands, _('Commands did not apply')) unless message.present? Notes::DestroyService.new(project, current_user).execute(note) end diff --git a/app/services/projects/prometheus/alerts/create_events_service.rb b/app/services/projects/prometheus/alerts/create_events_service.rb new file mode 100644 index 00000000000..a29240947ff --- /dev/null +++ b/app/services/projects/prometheus/alerts/create_events_service.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Projects + module Prometheus + module Alerts + # Persists a series of Prometheus alert events as list of PrometheusAlertEvent. + class CreateEventsService < BaseService + def execute + create_events_from(alerts) + end + + private + + def create_events_from(alerts) + Array.wrap(alerts).map { |alert| create_event(alert) }.compact + end + + def create_event(payload) + parsed_alert = Gitlab::Alerting::Alert.new(project: project, payload: payload) + + return unless parsed_alert.valid? + + if parsed_alert.gitlab_managed? + create_managed_prometheus_alert_event(parsed_alert) + else + create_self_managed_prometheus_alert_event(parsed_alert) + end + end + + def alerts + params['alerts'] + end + + def find_alert(metric) + Projects::Prometheus::AlertsFinder + .new(project: project, metric: metric) + .execute + .first + end + + def create_managed_prometheus_alert_event(parsed_alert) + alert = find_alert(parsed_alert.metric_id) + payload_key = PrometheusAlertEvent.payload_key_for(parsed_alert.metric_id, parsed_alert.starts_at_raw) + + event = PrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, alert, payload_key) + + set_status(parsed_alert, event) + end + + def create_self_managed_prometheus_alert_event(parsed_alert) + payload_key = SelfManagedPrometheusAlertEvent.payload_key_for(parsed_alert.starts_at_raw, parsed_alert.title, parsed_alert.full_query) + + event = SelfManagedPrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, payload_key) do |event| + event.environment = parsed_alert.environment + event.title = parsed_alert.title + event.query_expression = parsed_alert.full_query + end + + set_status(parsed_alert, event) + end + + def set_status(parsed_alert, event) + persisted = case parsed_alert.status + when 'firing' + event.fire(parsed_alert.starts_at) + when 'resolved' + event.resolve(parsed_alert.ends_at) + end + + event if persisted + end + end + end + end +end diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 2998208f50b..9178b929656 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -38,19 +38,16 @@ module Snippets private def save_and_commit(snippet) - result = snippet.with_transaction_returning_status do - (snippet.save && snippet.store_mentions!).tap do |saved| - break false unless saved - - if Feature.enabled?(:version_snippets, current_user) - create_repository_for(snippet) - end - end + snippet_saved = snippet.with_transaction_returning_status do + snippet.save && snippet.store_mentions! end - create_commit(snippet) if result && snippet.repository_exists? + if snippet_saved && Feature.enabled?(:version_snippets, current_user) + create_repository_for(snippet) + create_commit(snippet) + end - result + snippet_saved rescue => e # Rescuing all because we can receive Creation exceptions, GRPC exceptions, Git exceptions, ... snippet.errors.add(:base, e.message) diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index dd0eeaa9359..19158e7173c 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -528,6 +528,13 @@ :resource_boundary: :unknown :weight: 2 :idempotent: +- :name: incident_management:incident_management_process_prometheus_alert + :feature_category: :incident_management + :has_external_dependencies: + :urgency: :low + :resource_boundary: :cpu + :weight: 2 + :idempotent: - :name: jira_importer:jira_import_advance_stage :feature_category: :importers :has_external_dependencies: diff --git a/app/workers/incident_management/process_prometheus_alert_worker.rb b/app/workers/incident_management/process_prometheus_alert_worker.rb new file mode 100644 index 00000000000..768e049c60e --- /dev/null +++ b/app/workers/incident_management/process_prometheus_alert_worker.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module IncidentManagement + class ProcessPrometheusAlertWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + + queue_namespace :incident_management + feature_category :incident_management + worker_resource_boundary :cpu + + def perform(project_id, alert_hash) + project = find_project(project_id) + return unless project + + parsed_alert = Gitlab::Alerting::Alert.new(project: project, payload: alert_hash) + event = find_prometheus_alert_event(parsed_alert) + + if event&.resolved? + issue = event.related_issues.order_created_at_desc.detect(&:opened?) + + close_issue(project, issue) + else + issue = create_issue(project, alert_hash) + + relate_issue_to_event(event, issue) + end + end + + private + + def find_project(project_id) + Project.find_by_id(project_id) + end + + def find_prometheus_alert_event(alert) + if alert.gitlab_managed? + find_gitlab_managed_event(alert) + else + find_self_managed_event(alert) + end + end + + def find_gitlab_managed_event(alert) + payload_key = payload_key_for_alert(alert) + + PrometheusAlertEvent.find_by_payload_key(payload_key) + end + + def find_self_managed_event(alert) + payload_key = payload_key_for_alert(alert) + + SelfManagedPrometheusAlertEvent.find_by_payload_key(payload_key) + end + + def payload_key_for_alert(alert) + if alert.gitlab_managed? + PrometheusAlertEvent.payload_key_for(alert.metric_id, alert.starts_at_raw) + else + SelfManagedPrometheusAlertEvent.payload_key_for(alert.starts_at_raw, alert.title, alert.full_query) + end + end + + def create_issue(project, alert) + IncidentManagement::CreateIssueService + .new(project, alert) + .execute + .dig(:issue) + end + + def close_issue(project, issue) + return if issue.blank? || issue.closed? + + processed_issue = Issues::CloseService + .new(project, User.alert_bot) + .execute(issue, system_note: false) + + SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if processed_issue.reset.closed? + end + + def relate_issue_to_event(event, issue) + return unless event && issue + + if event.related_issues.exclude?(issue) + event.related_issues << issue + end + end + end +end |