diff options
86 files changed, 1992 insertions, 135 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 diff --git a/bin/secpick b/bin/secpick index fd3de2756ec..603c6d2c05d 100755 --- a/bin/secpick +++ b/bin/secpick @@ -159,7 +159,11 @@ module Secpick options[:branch] ||= `git rev-parse --abbrev-ref HEAD` options[:remote] ||= DEFAULT_REMOTE - abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.value?(nil) + nil_options = options.select {|_, v| v.nil? } + unless nil_options.empty? + abort("Missing: #{nil_options.keys.join(', ')}. Use #{$0} --help to see the list of options available".red) + end + abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/ end end diff --git a/changelogs/unreleased/20444-limit-full-path-search.yml b/changelogs/unreleased/20444-limit-full-path-search.yml new file mode 100644 index 00000000000..e4a4444118d --- /dev/null +++ b/changelogs/unreleased/20444-limit-full-path-search.yml @@ -0,0 +1,5 @@ +--- +title: Only enable searching of projects by full path / name on certain dropdowns +merge_request: 21910 +author: +type: changed diff --git a/changelogs/unreleased/208174-create-merge-request.yml b/changelogs/unreleased/208174-create-merge-request.yml new file mode 100644 index 00000000000..a2007b46dbd --- /dev/null +++ b/changelogs/unreleased/208174-create-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Start merge request for custom dashboard if new branch is provided +merge_request: 27189 +author: +type: added diff --git a/changelogs/unreleased/210596-fix_smartcard_config_initializer.yml b/changelogs/unreleased/210596-fix_smartcard_config_initializer.yml new file mode 100644 index 00000000000..f5c3d765495 --- /dev/null +++ b/changelogs/unreleased/210596-fix_smartcard_config_initializer.yml @@ -0,0 +1,5 @@ +--- +title: Fix smartcard config initialization +merge_request: 27560 +author: +type: fixed diff --git a/changelogs/unreleased/35627-api-response-for-adding-a-note-returns-http-400-for-command-only-no.yml b/changelogs/unreleased/35627-api-response-for-adding-a-note-returns-http-400-for-command-only-no.yml new file mode 100644 index 00000000000..359832986da --- /dev/null +++ b/changelogs/unreleased/35627-api-response-for-adding-a-note-returns-http-400-for-command-only-no.yml @@ -0,0 +1,5 @@ +--- +title: Return 202 for command only notes in REST API +merge_request: 19624 +author: +type: fixed diff --git a/changelogs/unreleased/36628-create-a-rake-task-to-cleanup-unused-lfs-files.yml b/changelogs/unreleased/36628-create-a-rake-task-to-cleanup-unused-lfs-files.yml new file mode 100644 index 00000000000..c03bebfe40d --- /dev/null +++ b/changelogs/unreleased/36628-create-a-rake-task-to-cleanup-unused-lfs-files.yml @@ -0,0 +1,5 @@ +--- +title: Create a rake task to cleanup unused LFS files +merge_request: 21747 +author: +type: added diff --git a/changelogs/unreleased/fj-reuse-default-snippet-name.yml b/changelogs/unreleased/fj-reuse-default-snippet-name.yml new file mode 100644 index 00000000000..554ad0809f9 --- /dev/null +++ b/changelogs/unreleased/fj-reuse-default-snippet-name.yml @@ -0,0 +1,5 @@ +--- +title: Reuse default generated snippet file name in repository +merge_request: 27673 +author: +type: fixed diff --git a/changelogs/unreleased/ph-encodeUriComponentNewDirectoryPath.yml b/changelogs/unreleased/ph-encodeUriComponentNewDirectoryPath.yml new file mode 100644 index 00000000000..27384e54e91 --- /dev/null +++ b/changelogs/unreleased/ph-encodeUriComponentNewDirectoryPath.yml @@ -0,0 +1,5 @@ +--- +title: Fix new file not being created in non-ascii character folders +merge_request: 26165 +author: +type: fixed diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 81de0ac6818..79bfcfd79e1 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -74,15 +74,6 @@ if Settings.ldap['enabled'] || Rails.env.test? end end -Gitlab.ee do - Settings['smartcard'] ||= Settingslogic.new({}) - Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? - Settings.smartcard['client_certificate_required_host'] = Settings.gitlab['host'] if Settings.smartcard['client_certificate_required_host'].nil? - Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil? - Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil? - Settings.smartcard['san_extensions'] = false if Settings.smartcard['san_extensions'].nil? -end - Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = true if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? @@ -672,6 +663,18 @@ Gitlab.ee do end # +# Smartcard +# +Gitlab.ee do + Settings['smartcard'] ||= Settingslogic.new({}) + Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? + Settings.smartcard['client_certificate_required_host'] = Settings.gitlab.host if Settings.smartcard['client_certificate_required_host'].nil? + Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil? + Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil? + Settings.smartcard['san_extensions'] = false if Settings.smartcard['san_extensions'].nil? +end + +# # Extra customization # Settings['extra'] ||= Settingslogic.new({}) diff --git a/config/routes/project.rb b/config/routes/project.rb index c37b5528f71..b86fd48e222 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -339,6 +339,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end namespace :prometheus do + resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do + post :notify, on: :collection + member do + get :metrics_dashboard + end + end + resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do get :active_common, on: :collection end diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 2d78174d669..ea11a203921 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -5046,6 +5046,7 @@ type Mutation { will be destroyed during the update, and no Note will be returned """ updateNote(input: UpdateNoteInput!): UpdateNotePayload + updateRequirement(input: UpdateRequirementInput!): UpdateRequirementPayload updateSnippet(input: UpdateSnippetInput!): UpdateSnippetPayload } @@ -8499,6 +8500,56 @@ type UpdateNotePayload { } """ +Autogenerated input type of UpdateRequirement +""" +input UpdateRequirementInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The iid of the requirement to update + """ + iid: String! + + """ + The project full path the requirement is associated with + """ + projectPath: ID! + + """ + State of the requirement + """ + state: RequirementState + + """ + Title of the requirement + """ + title: String +} + +""" +Autogenerated return type of UpdateRequirement +""" +type UpdateRequirementPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Reasons why the mutation failed. + """ + errors: [String!]! + + """ + The requirement after mutation + """ + requirement: Requirement +} + +""" Autogenerated input type of UpdateSnippet """ input UpdateSnippetInput { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 2be4573cafe..9e3460c0b03 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -15332,6 +15332,33 @@ "deprecationReason": null }, { + "name": "updateRequirement", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateRequirementInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateRequirementPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updateSnippet", "description": null, "args": [ @@ -25664,6 +25691,142 @@ }, { "kind": "INPUT_OBJECT", + "name": "UpdateRequirementInput", + "description": "Autogenerated input type of UpdateRequirement", + "fields": null, + "inputFields": [ + { + "name": "title", + "description": "Title of the requirement", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the requirement", + "type": { + "kind": "ENUM", + "name": "RequirementState", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "iid", + "description": "The iid of the requirement to update", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "projectPath", + "description": "The project full path the requirement is associated with", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateRequirementPayload", + "description": "Autogenerated return type of UpdateRequirement", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Reasons why the mutation failed.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requirement", + "description": "The requirement after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Requirement", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", "name": "UpdateSnippetInput", "description": "Autogenerated input type of UpdateSnippet", "fields": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6a79a407dac..dfbd08be898 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1372,6 +1372,16 @@ Autogenerated return type of UpdateNote | `errors` | String! => Array | Reasons why the mutation failed. | | `note` | Note | The note after mutation | +## UpdateRequirementPayload + +Autogenerated return type of UpdateRequirement + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Reasons why the mutation failed. | +| `requirement` | Requirement | The requirement after mutation | + ## UpdateSnippetPayload Autogenerated return type of UpdateSnippet diff --git a/doc/api/projects.md b/doc/api/projects.md index ae9f7de427d..04775b0339d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -46,6 +46,7 @@ GET /projects | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Return list of projects matching the search criteria | +| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` | | `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | | `owned` | boolean | no | Limit by projects explicitly owned by the current user | | `membership` | boolean | no | Limit by projects that the current user is a member of | diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 460bb6d25df..030cced8c2b 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -77,6 +77,7 @@ request is as follows: 1. The merge request author resolves only the threads they have fully addressed. If there's an open reply or thread, a suggestion, a question, or anything else, the thread should be left to be resolved by the reviewer. + 1. It should not be assumed that all feedback requires their recommended changes to be incorporated into the MR before it is merged. It is a judgment call by the MR author and the reviewer as to if this is required, or if a follow-up issue should be created to address the feedback in the future after the MR in question is merged. 1. If your MR touches code that executes shell commands, reads or opens files, or handles paths to files on disk, make sure it adheres to the [shell command guidelines](../shell_commands.md) diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index bbae713676d..bcce6a8a096 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -1,5 +1,61 @@ # Cleanup +## Remove unreferenced LFS files from filesystem + +DANGER: **Danger:** +Do not run this within 12 hours of a GitLab upgrade. This is to ensure that all background migrations have finished, which otherwise may lead to data loss. + +When you remove LFS files from a repository's history, they become orphaned and continue to consume disk space. With this rake task, you can remove invalid references from the database, which +will allow garbage collection of LFS files. + +For example: + +```shell +# omnibus-gitlab +sudo gitlab-rake gitlab:cleanup:orphan_lfs_file_references PROJECT_PATH="gitlab-org/gitlab-foss" + +# installation from source +bundle exec rake gitlab:cleanup:orphan_lfs_file_references RAILS_ENV=production PROJECT_PATH="gitlab-org/gitlab-foss" +``` + +You can also specify the project with `PROJECT_ID` instead of `PROJECT_PATH`. + +For example: + +```shell +$ sudo gitlab-rake gitlab:cleanup:orphan_lfs_file_references PROJECT_PATH="gitlab-org/gitlab-foss" +I, [2019-12-13T16:35:31.764962 #82356] INFO -- : Looking for orphan LFS files for project GitLab Org / GitLab Foss +I, [2019-12-13T16:35:31.923659 #82356] INFO -- : Removed invalid references: 12 +``` + +By default, this task does not delete anything but shows how many file references it can +delete. Run the command with `DRY_RUN=false` if you actually want to +delete the references. You can also use `LIMIT={number}` parameter to limit the number of deleted references. + +Note that this rake task only removes the references to LFS files. Unreferenced LFS files will be garbage-collected +later (once a day). If you need to garbage collect them immediately, run +`rake gitlab:cleanup:orphan_lfs_files` described below. + +## Remove unreferenced LFS files + +Unreferenced LFS files are removed on a daily basis but you can remove them immediately if +you need to. For example: + +```shell +# omnibus-gitlab +sudo gitlab-rake gitlab:cleanup:orphan_lfs_files + +# installation from source +bundle exec rake gitlab:cleanup:orphan_lfs_files +``` + +Example output: + +```shell +$ sudo gitlab-rake gitlab:cleanup:orphan_lfs_files +I, [2020-01-08T20:51:17.148765 #43765] INFO -- : Removed unreferenced LFS files: 12 +``` + ## Remove garbage from filesystem Clean up local project upload files if they don't exist in GitLab database. The diff --git a/doc/topics/airgap/index.md b/doc/topics/airgap/index.md index abeb3a4b1d3..fc92dd5519d 100644 --- a/doc/topics/airgap/index.md +++ b/doc/topics/airgap/index.md @@ -3,6 +3,11 @@ Computers in an air-gapped network are isolated from the public internet as a security measure. This page lists all the information available for running GitLab in an air-gapped environment. +## Quick start + +If you plan to deploy a GitLab instance on a physically-isolated and offline network, see the +[quick start guide](quick_start_guide.md) for configuration steps. + ## Features Follow these best practices to use GitLab's features in an offline environment: diff --git a/doc/topics/airgap/quick_start_guide.md b/doc/topics/airgap/quick_start_guide.md new file mode 100644 index 00000000000..c4c02af3c85 --- /dev/null +++ b/doc/topics/airgap/quick_start_guide.md @@ -0,0 +1,157 @@ +# Getting started with an air-gapped GitLab Installation + +This is a step-by-step guide that helps you install, configure, and use a self-managed GitLab +instance entirely offline. + +## Installation + +NOTE: **Note:** +This guide assumes the server is Ubuntu 18.04. Instructions for other servers may vary. + +NOTE: **Note:** +This guide assumes the server host resolves as `my-host`, which you should replace with your +server's name. + +Follow the installation instructions [as outlined in the omnibus install +guide](https://about.gitlab.com/install/#ubuntu), but make sure to specify an `http` +URL for the `EXTERNAL_URL` installation step. Once installed, we will manually +configure the SSL ourselves. + +It is strongly recommended to setup a domain for IP resolution rather than bind +to the server's IP address. This better ensures a stable target for our certs' CN +and will make long-term resolution simpler. + +```shell +sudo EXTERNAL_URL="http://my-host.internal" install gitlab-ee +``` + +## Enabling SSL + +Follow these steps to enable SSL for your fresh instance. Note that these steps reflect those for +[manually configuring SSL in Omnibus's NGINX configuration](https://docs.gitlab.com/omnibus/settings/nginx.html#manually-configuring-https): + +1. Make the following changes to `/etc/gitlab/gitlab.rb`: + + ```ruby + # Update external_url from "http" to "https" + external_url "https://example.gitlab.com" + + # Set Let's Encrypt to false + letsencrypt['enable'] = false + ``` + +1. Create the following directories with the appropriate permissions for generating self-signed + certificates: + + ```shell + sudo mkdir -p /etc/gitlab/ssl + sudo chmod 755 /etc/gitlab/ssl + sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/gitlab/ssl/my-host.internal.key -out /etc/gitlab/ssl/my-host.internal.crt + ``` + +1. Reconfigure your instance to apply the changes: + + ```shell + sudo gitlab-ctl reconfigure + ``` + +## Enabling the GitLab Container Registry + +Follow these steps to enable the container registry. Note that these steps reflect those for +[configuring the container registry under an existing domain](../../administration/packages/container_registry.md#configure-container-registry-under-an-existing-gitlab-domain): + +1. Make the following changes to `/etc/gitlab/gitlab.rb`: + + ```ruby + # Change external_registry_url to match external_url, but append the port 4567 + external_url "https://example.gitlab.com" + registry_external_url "https://example.gitlab.com:4567" + ``` + +1. Reconfigure your instance to apply the changes: + + ```shell + sudo gitlab-ctl reconfigure + ``` + +## Allow the docker daemon to trust the registry and GitLab Runner + +Provide your Docker daemon with your certs by +[following the steps for using trusted certificates with your registry](../../administration/packages/container_registry.md#using-self-signed-certificates-with-container-registry): + +```shell +sudo mkdir -p /etc/docker/certs.d/my-host.internal:5000 + +sudo cp /etc/gitlab/ssl/my-host.internal.crt /etc/docker/certs.d/my-host.internal:5000/ca.crt +``` + +Provide your GitLab Runner (to be installed next) with your certs by +[following the steps for using trusted certificates with your Runner](https://docs.gitlab.com/runner/install/docker.html#installing-trusted-ssl-server-certificates): + +```shell +sudo mkdir -p /etc/gitlab-runner/certs + +sudo cp /etc/gitlab/ssl/my-host.internal.crt /etc/gitlab-runner/certs/ca.crt +``` + +## Enabling GitLab Runner + +[Following a similar process to the steps for installing our GitLab Runner as a +Docker service](https://docs.gitlab.com/runner/install/docker.html#docker-image-installation), we must first register our Runner: + +```shell +$ sudo docker run --rm -it -v /etc/gitlab-runner:/etc/gitlab-runner gitlab/gitlab-runner register +Updating CA certificates... +Runtime platform arch=amd64 os=linux pid=7 revision=1b659122 version=12.8.0 +Running in system-mode. + +Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/): +https://my-host.internal +Please enter the gitlab-ci token for this runner: +XXXXXXXXXXX +Please enter the gitlab-ci description for this runner: +[eb18856e13c0]: +Please enter the gitlab-ci tags for this runner (comma separated): + +Registering runner... succeeded runner=FSMwkvLZ +Please enter the executor: custom, docker, virtualbox, kubernetes, docker+machine, docker-ssh+machine, docker-ssh, parallels, shell, ssh: +docker +Please enter the default Docker image (e.g. ruby:2.6): +ruby:2.6 +Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! +``` + +Now we must add some additional configuration to our runner: + +Make the following changes to `/etc/gitlab-runner/config.toml`: + +- Add docker socket to volumes `volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]` +- Add `pull_policy = "if-not-present"` to the executor configuration + +Now we can start our Runner: + +```shell +sudo docker run -d --restart always --name gitlab-runner -v /etc/gitlab-runner:/etc/gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock gitlab/gitlab-runner:latest +90646b6587127906a4ee3f2e51454c6e1f10f26fc7a0b03d9928d8d0d5897b64 +``` + +### Authenticating the registry against the host OS + +As noted in [Docker's registry authentication documentation](https://docs.docker.com/registry/insecure/#docker-still-complains-about-the-certificate-when-using-authentication), +certain versions of Docker require trusting the certificate chain at the OS level. + +In the case of Ubuntu, this involves using `update-ca-certificates`: + +```shell +sudo cp /etc/docker/certs.d/my-host.internal\:5000/ca.crt /usr/local/share/ca-certificates/my-host.internal.crt + +sudo update-ca-certificates +``` + +If all goes well, this is what you should see: + +``` +1 added, 0 removed; done. +Running hooks in /etc/ca-certificates/update.d... +done. +``` diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 2679dc85f42..dd2bb75b1df 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -53,7 +53,7 @@ The following languages and package managers are supported. | Go | [Godep](https://github.com/tools/godep), go get ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), gvt ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), glide ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), dep ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), trash ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) and govendor ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), [go mod](https://github.com/golang/go/wiki/Modules) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)| | Java | [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)| | .NET | [Nuget](https://www.nuget.org/) (.NET Framework is supported via the [mono project](https://www.mono-project.com/). Windows specific dependencies are not supported at this time.) |[License Finder](https://github.com/pivotal/LicenseFinder)| -| Python | [pip](https://pip.pypa.io/en/stable/) |[License Finder](https://github.com/pivotal/LicenseFinder)| +| Python | [pip](https://pip.pypa.io/en/stable/) (Python is supported through [requirements.txt](https://pip.readthedocs.io/en/1.1/requirements.html) and [Pipfile.lock](https://github.com/pypa/pipfile#pipfilelock).) |[License Finder](https://github.com/pivotal/LicenseFinder)| | Ruby | [gem](https://rubygems.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)| | Erlang | [rebar](https://www.rebar3.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)| | Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage) , [CocoaPods v0.39 and below](https://cocoapods.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)| diff --git a/lib/api/entities/note.rb b/lib/api/entities/note.rb index dcfb9a6d670..4d140454ff0 100644 --- a/lib/api/entities/note.rb +++ b/lib/api/entities/note.rb @@ -25,6 +25,14 @@ module API # Avoid N+1 queries as much as possible expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) } + + expose(:commands_changes) { |note| note.commands_changes || {} } + end + + # To be returned if the note was command-only + class NoteCommands < Grape::Entity + expose(:commands_changes) { |note| note.commands_changes || {} } + expose(:summary) { |note| note.errors[:commands_only] } end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index c3b5654e217..47784dc771e 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -505,6 +505,7 @@ module API finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility] finder_params[:archived] = archived_param unless params[:archived].nil? finder_params[:search] = params[:search] if params[:search] + finder_params[:search_namespaces] = true if params[:search_namespaces].present? finder_params[:user] = params.delete(:user) if params[:user] finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes] finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index bed0345a608..c85a38fc18b 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -113,6 +113,7 @@ module API end def create_note(noteable, opts) + whitelist_query_limiting authorize!(:create_note, noteable) parent = noteable_parent(noteable) @@ -139,6 +140,10 @@ module API present discussion, with: Entities::Discussion end + + def whitelist_query_limiting + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/211538') + end end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 7237fa24bab..3eafc1ead77 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -82,9 +82,13 @@ module API note = create_note(noteable, opts) - if note.valid? + if note.errors.keys == [:commands_only] + status 202 + present note, with: Entities::NoteCommands + elsif note.valid? present note, with: Entities.const_get(note.class.name, false) else + note.errors.delete(:commands_only) if note.errors.has_key?(:commands) bad_request!("Note #{note.errors.messages}") end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 3717e25d997..a33418c3336 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -63,6 +63,7 @@ module API optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'Limit by visibility' optional :search, type: String, desc: 'Return list of projects matching the search criteria' + optional :search_namespaces, type: Boolean, desc: "Include ancestor namespaces when matching search criteria" optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of' diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 6492ccc286a..bf4438fb518 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -14,8 +14,6 @@ module Gitlab REPORTER = 20 DEVELOPER = 30 MAINTAINER = 40 - # @deprecated - MASTER = MAINTAINER OWNER = 50 # Branch protection settings diff --git a/lib/gitlab/cleanup/orphan_lfs_file_references.rb b/lib/gitlab/cleanup/orphan_lfs_file_references.rb new file mode 100644 index 00000000000..5789fe4f92d --- /dev/null +++ b/lib/gitlab/cleanup/orphan_lfs_file_references.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Gitlab + module Cleanup + class OrphanLfsFileReferences + include Gitlab::Utils::StrongMemoize + + attr_reader :project, :dry_run, :logger, :limit + + DEFAULT_REMOVAL_LIMIT = 1000 + + def initialize(project, dry_run: true, logger: nil, limit: nil) + @project = project + @dry_run = dry_run + @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger + @limit = limit + end + + def run! + log_info("Looking for orphan LFS files for project #{project.name_with_namespace}") + + remove_orphan_references + end + + private + + def remove_orphan_references + invalid_references = project.lfs_objects_projects.where(lfs_object: orphan_objects) # rubocop:disable CodeReuse/ActiveRecord + + if dry_run + log_info("Found invalid references: #{invalid_references.count}") + else + count = 0 + invalid_references.each_batch(of: limit || DEFAULT_REMOVAL_LIMIT) do |relation| + count += relation.delete_all + end + + log_info("Removed invalid references: #{count}") + end + end + + def lfs_oids_from_repository + project.repository.gitaly_blob_client.get_all_lfs_pointers(nil).map(&:lfs_oid) + end + + def orphan_oids + lfs_oids_from_database - lfs_oids_from_repository + end + + def lfs_oids_from_database + oids = [] + + project.lfs_objects.each_batch do |relation| + oids += relation.pluck(:oid) # rubocop:disable CodeReuse/ActiveRecord + end + + oids + end + + def orphan_objects + LfsObject.where(oid: orphan_oids) # rubocop:disable CodeReuse/ActiveRecord + end + + def log_info(msg) + logger.info("#{'[DRY RUN] ' if dry_run}#{msg}") + end + end + end +end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index c26aa848d5a..a56a0435673 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -64,6 +64,40 @@ namespace :gitlab do end end + desc 'GitLab | Cleanup | Clean orphan LFS file references' + task orphan_lfs_file_references: :gitlab_environment do + warn_user_is_not_gitlab + + project = find_project + + unless project + logger.info "Specify the project with PROJECT_ID={number} or PROJECT_PATH={namespace/project-name}".color(:red) + exit + end + + cleaner = Gitlab::Cleanup::OrphanLfsFileReferences.new( + project, + dry_run: dry_run?, + logger: logger, + limit: limit + ) + + cleaner.run! + + if dry_run? + logger.info "To clean up these files run this command with DRY_RUN=false".color(:yellow) + end + end + + desc 'GitLab | Cleanup | Clean orphan LFS files' + task orphan_lfs_files: :gitlab_environment do + warn_user_is_not_gitlab + + removed_files = RemoveUnreferencedLfsObjectsWorker.new.perform + + logger.info "Removed unreferenced LFS files: #{removed_files.count}".color(:green) + end + namespace :sessions do desc "GitLab | Cleanup | Sessions | Clean ActiveSession lookup keys" task active_sessions_lookup_keys: :gitlab_environment do @@ -136,6 +170,14 @@ namespace :gitlab do ENV['NICENESS'].presence end + def find_project + if ENV['PROJECT_ID'] + Project.find_by_id(ENV['PROJECT_ID']&.to_i) + elsif ENV['PROJECT_PATH'] + Project.find_by_full_path(ENV['PROJECT_PATH']) + end + end + # rubocop:disable Gitlab/RailsLogger def logger return @logger if defined?(@logger) diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js index bf0e98da139..45cdef2ba86 100644 --- a/scripts/frontend/prettier.js +++ b/scripts/frontend/prettier.js @@ -78,10 +78,15 @@ const checkFileWithOptions = (filePath, options) => passedCount += 1; } else { if (!didWarn) { - console.log(warningMessage); + // \x1b[31m make text red + // \x1b[1m make text bold + // %s warningMessage + // \x1b[0m reset text color (so logs after aren't red) + const redBoldText = '\x1b[1m\x1b[31;1m%s\x1b[0m'; + console.log(redBoldText, warningMessage); didWarn = true; } - console.log(`Prettify Manually : ${filePath}`); + console.log(`yarn prettier --write ${filePath}`); failedCount += 1; } } diff --git a/spec/factories/prometheus_alert_event.rb b/spec/factories/prometheus_alert_event.rb new file mode 100644 index 00000000000..281fbacabe2 --- /dev/null +++ b/spec/factories/prometheus_alert_event.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :prometheus_alert_event do + project { prometheus_alert.project } + prometheus_alert + sequence(:payload_key) { |n| "hash payload key #{n}" } + status { PrometheusAlertEvent.status_value_for(:firing) } + started_at { Time.now } + + trait :resolved do + status { PrometheusAlertEvent.status_value_for(:resolved) } + ended_at { Time.now } + payload_key { nil } + end + + trait :none do + status { nil } + started_at { nil } + end + end +end diff --git a/spec/factories/self_managed_prometheus_alert_event.rb b/spec/factories/self_managed_prometheus_alert_event.rb new file mode 100644 index 00000000000..238942e2c46 --- /dev/null +++ b/spec/factories/self_managed_prometheus_alert_event.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :self_managed_prometheus_alert_event do + project + sequence(:payload_key) { |n| "hash payload key #{n}" } + status { SelfManagedPrometheusAlertEvent.status_value_for(:firing) } + title { 'alert' } + query_expression { 'vector(2)' } + started_at { Time.now } + + trait :resolved do + status { SelfManagedPrometheusAlertEvent.status_value_for(:resolved) } + ended_at { Time.now } + payload_key { nil } + end + + trait :none do + status { nil } + started_at { nil } + end + end +end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 5b2e98804b0..e03d7b6d1f7 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -100,6 +100,8 @@ describe 'Group issues page' do find('.empty-state .js-lazy-loaded') find('.new-project-item-link').click + find('.select2-input').set(group.name) + page.within('.select2-results') do expect(page).to have_content(project.full_name) expect(page).not_to have_content(project_with_issues_disabled.full_name) diff --git a/spec/finders/autocomplete/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb index f997dd32c40..9129a3b65be 100644 --- a/spec/finders/autocomplete/move_to_project_finder_spec.rb +++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Autocomplete::MoveToProjectFinder do - let(:user) { create(:user) } - let(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } let(:no_access_project) { create(:project) } let(:guest_project) { create(:project) } @@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a) .to eq([wadus_project]) end + + it 'allows searching by parent namespace' do + group = create(:group) + other_project = create(:project, group: group) + other_project.add_maintainer(user) + + expect(described_class.new(user, project_id: project.id, search: group.name).execute.to_a) + .to contain_exactly(other_project) + end end end end diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index ee8606e474e..8d3564ca3c0 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -123,7 +123,7 @@ describe GroupDescendantsFinder do project = create(:project, namespace: group) other_project = create(:project) other_project.project_group_links.create(group: group, - group_access: ProjectGroupLink::MASTER) + group_access: ProjectGroupLink::MAINTAINER) expect(finder.execute).to contain_exactly(project) end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 6a04ca0eb67..eb3e28d1668 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do include AdminModeHelper describe '#execute' do - let(:user) { create(:user) } - let(:group) { create(:group, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group, :public) } - let!(:private_project) do + let_it_be(:private_project) do create(:project, :private, name: 'A', path: 'A') end - let!(:internal_project) do + let_it_be(:internal_project) do create(:project, :internal, group: group, name: 'B', path: 'B') end - let!(:public_project) do + let_it_be(:public_project) do create(:project, :public, group: group, name: 'C', path: 'C') end - let!(:shared_project) do + let_it_be(:shared_project) do create(:project, :private, name: 'D', path: 'D') end @@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do it { is_expected.to eq([public_project]) } end + describe 'filter by group name' do + let(:params) { { name: group.name, search_namespaces: true } } + + it { is_expected.to eq([public_project, internal_project]) } + end + describe 'filter by archived' do let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') } diff --git a/spec/fixtures/api/schemas/entities/discussion.json b/spec/fixtures/api/schemas/entities/discussion.json index bcc1db79e83..16622ef6887 100644 --- a/spec/fixtures/api/schemas/entities/discussion.json +++ b/spec/fixtures/api/schemas/entities/discussion.json @@ -54,7 +54,8 @@ "cached_markdown_version": { "type": "integer" }, "human_access": { "type": ["string", "null"] }, "toggle_award_path": { "type": "string" }, - "path": { "type": "string" } + "path": { "type": "string" }, + "commands_changes": { "type": "object", "additionalProperties": true } }, "required": [ "id", "attachment", "author", "created_at", "updated_at", diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json index 0f9c221fd6d..9668327adc4 100644 --- a/spec/fixtures/api/schemas/public_api/v4/notes.json +++ b/spec/fixtures/api/schemas/public_api/v4/notes.json @@ -19,6 +19,7 @@ }, "additionalProperties": false }, + "commands_changes": { "type": "object", "additionalProperties": true }, "created_at": { "type": "date" }, "updated_at": { "type": "date" }, "system": { "type": "boolean" }, diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js index 904798e0b83..b4800112fee 100644 --- a/spec/frontend/repository/components/table/parent_row_spec.js +++ b/spec/frontend/repository/components/table/parent_row_spec.js @@ -31,10 +31,11 @@ describe('Repository parent row component', () => { }); it.each` - path | to - ${'app'} | ${'/-/tree/master/'} - ${'app/assets'} | ${'/-/tree/master/app'} - ${'app/assets#/test'} | ${'/-/tree/master/app/assets%23'} + path | to + ${'app'} | ${'/-/tree/master/'} + ${'app/assets'} | ${'/-/tree/master/app'} + ${'app/assets#/test'} | ${'/-/tree/master/app/assets%23'} + ${'app/assets#/test/world'} | ${'/-/tree/master/app/assets%23/test'} `('renders link in $path to $to', ({ path, to }) => { factory(path); diff --git a/spec/models/concerns/optionally_search_spec.rb b/spec/models/concerns/optionally_search_spec.rb index ff4212ddf18..71cf536db89 100644 --- a/spec/models/concerns/optionally_search_spec.rb +++ b/spec/models/concerns/optionally_search_spec.rb @@ -22,12 +22,22 @@ describe OptionallySearch do it 'delegates to the search method' do expect(model) .to receive(:search) - .with('foo') + .with('foo', {}) model.optionally_search('foo') end end + context 'when an option is provided' do + it 'delegates to the search method' do + expect(model) + .to receive(:search) + .with('foo', some_option: true) + + model.optionally_search('foo', some_option: true) + end + end + context 'when no query is given' do it 'returns the current relation' do expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 44be4985439..ceb6382eb6c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -108,6 +108,8 @@ describe Project do it { is_expected.to have_many(:external_pull_requests) } it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:source_pipelines) } + it { is_expected.to have_many(:prometheus_alert_events) } + it { is_expected.to have_many(:self_managed_prometheus_alert_events) } it_behaves_like 'model with repository' do let_it_be(:container) { create(:project, :repository, path: 'somewhere') } @@ -1757,7 +1759,7 @@ describe Project do expect(described_class.search(project.path.upcase)).to eq([project]) end - context 'by full path' do + context 'when include_namespace is true' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, group: group) } @@ -1767,11 +1769,11 @@ describe Project do end it 'returns projects that match the group path' do - expect(described_class.search(group.path)).to eq([project]) + expect(described_class.search(group.path, include_namespace: true)).to eq([project]) end it 'returns projects that match the full path' do - expect(described_class.search(project.full_path)).to eq([project]) + expect(described_class.search(project.full_path, include_namespace: true)).to eq([project]) end end @@ -1781,11 +1783,11 @@ describe Project do end it 'returns no results when searching by group path' do - expect(described_class.search(group.path)).to be_empty + expect(described_class.search(group.path, include_namespace: true)).to be_empty end it 'returns no results when searching by full path' do - expect(described_class.search(project.full_path)).to be_empty + expect(described_class.search(project.full_path, include_namespace: true)).to be_empty end end end diff --git a/spec/models/prometheus_alert_event_spec.rb b/spec/models/prometheus_alert_event_spec.rb new file mode 100644 index 00000000000..040113643dd --- /dev/null +++ b/spec/models/prometheus_alert_event_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PrometheusAlertEvent do + subject { build(:prometheus_alert_event) } + + let(:alert) { subject.prometheus_alert } + + describe 'associations' do + it { is_expected.to belong_to(:prometheus_alert).required } + end + + describe 'validations' do + it { is_expected.to be_valid } + + it { is_expected.to validate_presence_of(:prometheus_alert).with_message(:required) } + it { is_expected.to validate_uniqueness_of(:payload_key).scoped_to(:prometheus_alert_id) } + it { is_expected.to validate_presence_of(:started_at) } + + describe 'payload_key & ended_at' do + context 'absent if firing?' do + subject { build(:prometheus_alert_event) } + + it { is_expected.to validate_presence_of(:payload_key) } + it { is_expected.not_to validate_presence_of(:ended_at) } + end + + context 'present if resolved?' do + subject { build(:prometheus_alert_event, :resolved) } + + it { is_expected.not_to validate_presence_of(:payload_key) } + it { is_expected.to validate_presence_of(:ended_at) } + end + end + end + + describe '#title' do + it 'delegates to alert' do + expect(subject.title).to eq(alert.title) + end + end + + describe 'prometheus_metric_id' do + it 'delegates to alert' do + expect(subject.prometheus_metric_id).to eq(alert.prometheus_metric_id) + end + end + + describe 'transaction' do + describe 'fire' do + let(:started_at) { Time.now } + + context 'when status is none' do + subject { build(:prometheus_alert_event, :none) } + + it 'fires an event' do + result = subject.fire(started_at) + + expect(result).to eq(true) + expect(subject).to be_firing + expect(subject.started_at).to be_like_time(started_at) + end + end + + context 'when firing' do + subject { build(:prometheus_alert_event) } + + it 'cannot fire again' do + result = subject.fire(started_at) + + expect(result).to eq(false) + end + end + end + + describe 'resolve' do + let(:ended_at) { Time.now } + + context 'when firing' do + subject { build(:prometheus_alert_event) } + + it 'resolves an event' do + result = subject.resolve!(ended_at) + + expect(result).to eq(true) + expect(subject).to be_resolved + expect(subject.ended_at).to be_like_time(ended_at) + end + end + + context 'when resolved' do + subject { build(:prometheus_alert_event, :resolved) } + + it 'cannot resolve again' do + result = subject.resolve(ended_at) + + expect(result).to eq(false) + end + end + end + end +end diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb index 6861e03282a..c31fe192367 100644 --- a/spec/models/snippet_repository_spec.rb +++ b/spec/models/snippet_repository_spec.rb @@ -140,6 +140,41 @@ describe SnippetRepository do let_it_be(:named_snippet) { { file_path: 'fee.txt', content: 'bar', action: :create } } let_it_be(:unnamed_snippet) { { file_path: '', content: 'dummy', action: :create } } + context 'when existing file has a default name' do + let(:default_name) { 'snippetfile1.txt' } + let(:new_file) { { file_path: '', content: 'bar' } } + let(:existing_file) { { previous_path: default_name, file_path: '', content: 'new_content' } } + + before do + expect(blob_at(snippet, default_name)).to be_nil + + snippet_repository.multi_files_action(user, [new_file], commit_opts) + + expect(blob_at(snippet, default_name)).to be + end + + it 'reuses the existing file name' do + snippet_repository.multi_files_action(user, [existing_file], commit_opts) + + blob = blob_at(snippet, default_name) + expect(blob.data).to eq existing_file[:content] + end + end + + context 'when file name consists of one or several whitespaces' do + let(:default_name) { 'snippetfile1.txt' } + let(:new_file) { { file_path: ' ', content: 'bar' } } + + it 'assigns a new name to the file' do + expect(blob_at(snippet, default_name)).to be_nil + + snippet_repository.multi_files_action(user, [new_file], commit_opts) + + blob = blob_at(snippet, default_name) + expect(blob.data).to eq new_file[:content] + end + end + context 'when some files are not named' do let(:data) { [named_snippet] + Array.new(2) { unnamed_snippet.clone } } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 54bb2e670da..cec4995c620 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -302,7 +302,7 @@ describe API::Groups do before do group1.add_developer(user2) - group3.add_master(user2) + group3.add_maintainer(user2) end it 'returns an array of groups the user has at least master access' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 6cf978e717e..3fb14eb9d5a 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe API::Notes do - let(:user) { create(:user) } - let!(:project) { create(:project, :public, namespace: user.namespace) } + let!(:user) { create(:user) } + let!(:project) { create(:project, :public) } let(:private_user) { create(:user) } before do @@ -226,14 +226,56 @@ describe API::Notes do let(:note) { merge_request_note } end + let(:request_body) { 'Hi!' } + let(:request_path) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes" } + + subject { post api(request_path, user), params: { body: request_body } } + + context 'a command only note' do + let(:assignee) { create(:user) } + let(:request_body) { "/assign #{assignee.to_reference}" } + + before do + project.add_developer(assignee) + project.add_developer(user) + end + + it 'returns 202 Accepted status' do + subject + + expect(response).to have_gitlab_http_status(:accepted) + end + + it 'does not actually create a new note' do + expect { subject }.not_to change { Note.where(system: false).count } + end + + it 'does however create a system note about the change' do + expect { subject }.to change { Note.system.count }.by(1) + end + + it 'applies the commands' do + expect { subject }.to change { merge_request.reset.assignees } + end + + it 'reports the changes' do + subject + + expect(json_response).to include( + 'commands_changes' => include( + 'assignee_ids' => [Integer] + ), + 'summary' => include("Assigned #{assignee.to_reference}.") + ) + end + end + context 'when the merge request discussion is locked' do before do merge_request.update_attribute(:discussion_locked, true) end context 'when a user is a team member' do - subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), params: { body: 'Hi!' } } - it 'returns 200 status' do subject diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb index 91139b987df..ee55d1c54b7 100644 --- a/spec/requests/api/pages/internal_access_spec.rb +++ b/spec/requests/api/pages/internal_access_spec.rb @@ -19,7 +19,7 @@ describe "Internal Project Pages Access" do before do allow(Gitlab.config.pages).to receive(:access_control).and_return(true) group.add_owner(owner) - project.add_master(master) + project.add_maintainer(master) project.add_developer(developer) project.add_reporter(reporter) project.add_guest(guest) diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb index 7c592ccfd43..146c6a389f3 100644 --- a/spec/requests/api/pages/private_access_spec.rb +++ b/spec/requests/api/pages/private_access_spec.rb @@ -19,7 +19,7 @@ describe "Private Project Pages Access" do before do allow(Gitlab.config.pages).to receive(:access_control).and_return(true) group.add_owner(owner) - project.add_master(master) + project.add_maintainer(master) project.add_developer(developer) project.add_reporter(reporter) project.add_guest(guest) diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb index f2fe64434c6..7d929e2a287 100644 --- a/spec/requests/api/pages/public_access_spec.rb +++ b/spec/requests/api/pages/public_access_spec.rb @@ -19,7 +19,7 @@ describe "Public Project Pages Access" do before do allow(Gitlab.config.pages).to receive(:access_control).and_return(true) group.add_owner(owner) - project.add_master(master) + project.add_maintainer(master) project.add_developer(developer) project.add_reporter(reporter) project.add_guest(guest) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 8706b941e4f..c4f4801e372 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -362,6 +362,21 @@ describe API::Projects do end end + context 'and using search and search_namespaces is true' do + let(:group) { create(:group) } + let!(:project_in_group) { create(:project, group: group) } + + before do + group.add_guest(user) + end + + it_behaves_like 'projects response' do + let(:filter) { { search: group.name, search_namespaces: true } } + let(:current_user) { user } + let(:projects) { [project_in_group] } + end + end + context 'and using id_after' do it_behaves_like 'projects response' do let(:filter) { { id_after: project2.id } } diff --git a/spec/serializers/prometheus_alert_entity_spec.rb b/spec/serializers/prometheus_alert_entity_spec.rb new file mode 100644 index 00000000000..5121c62a0e0 --- /dev/null +++ b/spec/serializers/prometheus_alert_entity_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PrometheusAlertEntity do + let(:user) { create(:user) } + let(:prometheus_alert) { create(:prometheus_alert) } + let(:request) { double('prometheus_alert', current_user: user) } + let(:entity) { described_class.new(prometheus_alert, request: request) } + + subject { entity.as_json } + + context 'when user can read prometheus alerts' do + before do + prometheus_alert.project.add_maintainer(user) + stub_licensed_features(prometheus_alerts: true) + end + + it 'exposes prometheus_alert attributes' do + expect(subject).to include(:id, :title, :query, :operator, :threshold) + end + + it 'exposes alert_path' do + expect(subject).to include(:alert_path) + end + end +end diff --git a/spec/services/metrics/dashboard/update_dashboard_service_spec.rb b/spec/services/metrics/dashboard/update_dashboard_service_spec.rb index 227041344d7..6ba4b4035e4 100644 --- a/spec/services/metrics/dashboard/update_dashboard_service_spec.rb +++ b/spec/services/metrics/dashboard/update_dashboard_service_spec.rb @@ -92,6 +92,8 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto end context 'Files::UpdateService success' do + let(:merge_request) { project.merge_requests.last } + before do allow(::Files::UpdateService).to receive(:new).and_return(double(execute: { status: :success })) end @@ -107,6 +109,31 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto expect(service_call[:status]).to be :success expect(service_call[:http_status]).to be :created expect(service_call[:dashboard]).to match dashboard_details + expect(service_call[:merge_request]).to eq(Gitlab::UrlBuilder.build(merge_request)) + end + + context 'when the merge request does not succeed' do + let(:error_message) { 'There was an error' } + + let(:merge_request) do + build(:merge_request, target_project: project, source_project: project, author: user) + end + + before do + merge_request.errors.add(:base, error_message) + allow_next_instance_of(::MergeRequests::CreateService) do |mr| + allow(mr).to receive(:execute).and_return(merge_request) + end + end + + it 'returns an appropriate message and status code', :aggregate_failures do + result = service_call + + expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step) + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(:bad_request) + expect(result[:message]).to eq(error_message) + end end context 'with escaped characters in file name' do @@ -125,6 +152,25 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto expect(service_call[:dashboard]).to match dashboard_details end end + + context 'when pushing to the default branch' do + let(:branch) { 'master' } + + it 'does not create a merge request', :aggregate_failures do + dashboard_details = { + path: '.gitlab/dashboards/custom_dashboard.yml', + display_name: 'custom_dashboard.yml', + default: false, + system_dashboard: false + } + + expect(::MergeRequests::CreateService).not_to receive(:new) + expect(service_call.keys).to contain_exactly(:dashboard, :http_status, :status) + expect(service_call[:status]).to be :success + expect(service_call[:http_status]).to be :created + expect(service_call[:dashboard]).to match dashboard_details + end + end end context 'Files::UpdateService fails' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 8b43844eb96..9fa8f807330 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -2790,7 +2790,7 @@ describe NotificationService, :mailer do let!(:developer) { create(:user) } before do - project.add_master(master) + project.add_maintainer(master) end it 'sends the email to owners and masters' do diff --git a/spec/services/projects/prometheus/alerts/create_events_service_spec.rb b/spec/services/projects/prometheus/alerts/create_events_service_spec.rb new file mode 100644 index 00000000000..1d726db6ce3 --- /dev/null +++ b/spec/services/projects/prometheus/alerts/create_events_service_spec.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::Prometheus::Alerts::CreateEventsService do + let(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:metric) { create(:prometheus_metric, project: project) } + let(:service) { described_class.new(project, user, alerts_payload) } + + shared_examples 'events persisted' do |expected_count| + subject { service.execute } + + it 'returns proper amount of created events' do + expect(subject.size).to eq(expected_count) + end + + it 'increments event count' do + expect { subject }.to change { PrometheusAlertEvent.count }.to(expected_count) + end + end + + shared_examples 'no events persisted' do + subject { service.execute } + + it 'returns no created events' do + expect(subject).to be_empty + end + + it 'does not change event count' do + expect { subject }.not_to change { PrometheusAlertEvent.count } + end + end + + shared_examples 'self managed events persisted' do + subject { service.execute } + + it 'returns created events' do + expect(subject).not_to be_empty + end + + it 'does change self managed event count' do + expect { subject }.to change { SelfManagedPrometheusAlertEvent.count } + end + end + + context 'with valid alerts_payload' do + let!(:alert) { create(:prometheus_alert, prometheus_metric: metric, project: project) } + + let(:events) { service.execute } + + context 'with a firing payload' do + let(:started_at) { truncate_to_second(Time.now) } + let(:firing_event) { alert_payload(status: 'firing', started_at: started_at) } + let(:alerts_payload) { { 'alerts' => [firing_event] } } + + it_behaves_like 'events persisted', 1 + + it 'returns created event' do + event = events.first + + expect(event).to be_firing + expect(event.started_at).to eq(started_at) + expect(event.ended_at).to be_nil + end + + context 'with 2 different firing events' do + let(:another_firing_event) { alert_payload(status: 'firing', started_at: started_at + 1) } + let(:alerts_payload) { { 'alerts' => [firing_event, another_firing_event] } } + + it_behaves_like 'events persisted', 2 + end + + context 'with already persisted firing event' do + before do + service.execute + end + + it_behaves_like 'no events persisted' + end + + context 'with duplicate payload' do + let(:alerts_payload) { { 'alerts' => [firing_event, firing_event] } } + + it_behaves_like 'events persisted', 1 + end + end + + context 'with a resolved payload' do + let(:started_at) { truncate_to_second(Time.now) } + let(:ended_at) { started_at + 1 } + let(:payload_key) { PrometheusAlertEvent.payload_key_for(alert.prometheus_metric_id, utc_rfc3339(started_at)) } + let(:resolved_event) { alert_payload(status: 'resolved', started_at: started_at, ended_at: ended_at) } + let(:alerts_payload) { { 'alerts' => [resolved_event] } } + + context 'with a matching firing event' do + before do + create(:prometheus_alert_event, + prometheus_alert: alert, + payload_key: payload_key, + started_at: started_at) + end + + it 'does not create an additional event' do + expect { service.execute }.not_to change { PrometheusAlertEvent.count } + end + + it 'marks firing event as `resolved`' do + expect(events.size).to eq(1) + + event = events.first + expect(event).to be_resolved + expect(event.started_at).to eq(started_at) + expect(event.ended_at).to eq(ended_at) + end + + context 'with duplicate payload' do + let(:alerts_payload) { { 'alerts' => [resolved_event, resolved_event] } } + + it 'does not create an additional event' do + expect { service.execute }.not_to change { PrometheusAlertEvent.count } + end + + it 'marks firing event as `resolved` only once' do + expect(events.size).to eq(1) + end + end + end + + context 'without a matching firing event' do + context 'due to payload_key' do + let(:payload_key) { 'some other payload_key' } + + before do + create(:prometheus_alert_event, + prometheus_alert: alert, + payload_key: payload_key, + started_at: started_at) + end + + it_behaves_like 'no events persisted' + end + + context 'due to status' do + before do + create(:prometheus_alert_event, :resolved, + prometheus_alert: alert, + started_at: started_at) + end + + it_behaves_like 'no events persisted' + end + end + + context 'with already resolved event' do + before do + service.execute + end + + it_behaves_like 'no events persisted' + end + end + + context 'with a metric from another project' do + let(:another_project) { create(:project) } + let(:metric) { create(:prometheus_metric, project: another_project) } + let(:alerts_payload) { { 'alerts' => [alert_payload] } } + + let!(:alert) do + create(:prometheus_alert, + prometheus_metric: metric, + project: another_project) + end + + it_behaves_like 'no events persisted' + end + end + + context 'with invalid payload' do + let(:alert) { create(:prometheus_alert, prometheus_metric: metric, project: project) } + + describe '`alerts` key' do + context 'is missing' do + let(:alerts_payload) { {} } + + it_behaves_like 'no events persisted' + end + + context 'is nil' do + let(:alerts_payload) { { 'alerts' => nil } } + + it_behaves_like 'no events persisted' + end + + context 'is empty' do + let(:alerts_payload) { { 'alerts' => [] } } + + it_behaves_like 'no events persisted' + end + + context 'is not a Hash' do + let(:alerts_payload) { { 'alerts' => [:not_a_hash] } } + + it_behaves_like 'no events persisted' + end + + describe '`status`' do + context 'is missing' do + let(:alerts_payload) { { 'alerts' => [alert_payload(status: nil)] } } + + it_behaves_like 'no events persisted' + end + + context 'is invalid' do + let(:alerts_payload) { { 'alerts' => [alert_payload(status: 'invalid')] } } + + it_behaves_like 'no events persisted' + end + end + + describe '`started_at`' do + context 'is missing' do + let(:alerts_payload) { { 'alerts' => [alert_payload(started_at: nil)] } } + + it_behaves_like 'no events persisted' + end + + context 'is invalid' do + let(:alerts_payload) { { 'alerts' => [alert_payload(started_at: 'invalid date')] } } + + it_behaves_like 'no events persisted' + end + end + + describe '`ended_at`' do + context 'is missing and status is resolved' do + let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: nil, status: 'resolved')] } } + + it_behaves_like 'no events persisted' + end + + context 'is invalid and status is resolved' do + let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: 'invalid date', status: 'resolved')] } } + + it_behaves_like 'no events persisted' + end + end + + describe '`labels`' do + describe '`gitlab_alert_id`' do + context 'is missing' do + let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil)] } } + + it_behaves_like 'no events persisted' + end + + context 'is missing but title is given' do + let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil, title: 'alert')] } } + + it_behaves_like 'self managed events persisted' + end + + context 'is missing and environment name is given' do + let(:environment) { create(:environment, project: project) } + let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil, title: 'alert', environment: environment.name)] } } + + it_behaves_like 'self managed events persisted' + + it 'associates the environment to the alert event' do + service.execute + + expect(SelfManagedPrometheusAlertEvent.last.environment).to eq environment + end + end + + context 'is invalid' do + let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: '-1')] } } + + it_behaves_like 'no events persisted' + end + end + end + end + end + + private + + def alert_payload(status: 'firing', started_at: Time.now, ended_at: Time.now, gitlab_alert_id: alert.prometheus_metric_id, title: nil, environment: nil) + payload = {} + + payload['status'] = status if status + payload['startsAt'] = utc_rfc3339(started_at) if started_at + payload['endsAt'] = utc_rfc3339(ended_at) if ended_at + payload['labels'] = {} + payload['labels']['gitlab_alert_id'] = gitlab_alert_id.to_s if gitlab_alert_id + payload['labels']['alertname'] = title if title + payload['labels']['gitlab_environment_name'] = environment if environment + + payload + end + + # Example: 2018-09-27T18:25:31.079079416Z + def utc_rfc3339(date) + date.utc.rfc3339 + rescue + date + end + + def truncate_to_second(date) + date.change(usec: 0) + end +end diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb index ffad3c8b8e5..26c80ee05b3 100644 --- a/spec/services/snippets/create_service_spec.rb +++ b/spec/services/snippets/create_service_spec.rb @@ -193,6 +193,12 @@ describe Snippets::CreateService do subject end + it 'destroys the snippet_repository' do + subject + + expect(SnippetRepository.count).to be_zero + end + it 'returns the error' do response = subject diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index 0c52af43465..72e8b920192 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -172,6 +172,8 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| if parent_type == 'projects' context 'by a project owner' do + let(:user) { project.owner } + it 'sets the creation time on the new note' do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb index 92ccc195a9a..8db18895c24 100644 --- a/spec/tasks/gitlab/cleanup_rake_spec.rb +++ b/spec/tasks/gitlab/cleanup_rake_spec.rb @@ -120,6 +120,71 @@ describe 'gitlab:cleanup rake tasks' do end end + describe 'gitlab:cleanup:orphan_lfs_file_references' do + subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_file_references') } + + let(:project) { create(:project, :repository) } + + before do + stub_env('PROJECT_ID', project.id) + end + + it 'runs the task without errors' do + expect(Gitlab::Cleanup::OrphanLfsFileReferences) + .to receive(:new).and_call_original + + expect { rake_task }.not_to raise_error + end + + context 'with DRY_RUN set to false' do + before do + stub_env('DRY_RUN', 'false') + end + + it 'passes dry_run correctly' do + expect(Gitlab::Cleanup::OrphanLfsFileReferences) + .to receive(:new) + .with(project, + limit: anything, + dry_run: false, + logger: anything) + .and_call_original + + rake_task + end + end + + context 'with LIMIT set to 100' do + before do + stub_env('LIMIT', '100') + end + + it 'passes limit as integer' do + expect(Gitlab::Cleanup::OrphanLfsFileReferences) + .to receive(:new) + .with(project, + limit: 100, + dry_run: true, + logger: anything) + .and_call_original + + rake_task + end + end + end + + describe 'gitlab:cleanup:orphan_lfs_files' do + subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_files') } + + it 'runs RemoveUnreferencedLfsObjectsWorker' do + expect_any_instance_of(RemoveUnreferencedLfsObjectsWorker) + .to receive(:perform) + .and_call_original + + rake_task + end + end + context 'sessions' do describe 'gitlab:cleanup:sessions:active_sessions_lookup_keys', :clean_gitlab_redis_shared_state do subject(:rake_task) { run_rake_task('gitlab:cleanup:sessions:active_sessions_lookup_keys') } diff --git a/spec/views/projects/artifacts/_artifact.html.haml_spec.rb b/spec/views/projects/artifacts/_artifact.html.haml_spec.rb index 460b63efa2f..b3bf54e143a 100644 --- a/spec/views/projects/artifacts/_artifact.html.haml_spec.rb +++ b/spec/views/projects/artifacts/_artifact.html.haml_spec.rb @@ -38,7 +38,7 @@ RSpec.describe "projects/artifacts/_artifact.html.haml" do let(:user) { create(:user) } it 'has a delete button' do - allow_any_instance_of(ProjectTeam).to receive(:max_member_access).and_return(Gitlab::Access::MASTER) + allow_any_instance_of(ProjectTeam).to receive(:max_member_access).and_return(Gitlab::Access::MAINTAINER) render_partial expect(rendered).to have_link('Delete artifacts', href: project_artifact_path(project, project.job_artifacts.first)) diff --git a/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb b/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb new file mode 100644 index 00000000000..19ef2635882 --- /dev/null +++ b/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IncidentManagement::ProcessPrometheusAlertWorker do + describe '#perform' do + let_it_be(:project) { create(:project) } + let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) } + let_it_be(:payload_key) { PrometheusAlertEvent.payload_key_for(prometheus_alert.prometheus_metric_id, prometheus_alert.created_at.rfc3339) } + let!(:prometheus_alert_event) { create(:prometheus_alert_event, prometheus_alert: prometheus_alert, payload_key: payload_key) } + + let(:alert_params) do + { + startsAt: prometheus_alert.created_at.rfc3339, + labels: { + gitlab_alert_id: prometheus_alert.prometheus_metric_id + } + }.with_indifferent_access + end + + it 'creates an issue' do + expect { subject.perform(project.id, alert_params) } + .to change(Issue, :count) + .by(1) + end + + it 'relates issue to an event' do + expect { subject.perform(project.id, alert_params) } + .to change(prometheus_alert.related_issues, :count) + .from(0) + .to(1) + end + + context 'resolved event' do + let(:issue) { create(:issue, project: project) } + + before do + prometheus_alert_event.related_issues << issue + prometheus_alert_event.resolve + end + + it 'does not create an issue' do + expect { subject.perform(project.id, alert_params) } + .not_to change(Issue, :count) + end + + it 'closes the existing issue' do + expect { subject.perform(project.id, alert_params) } + .to change { issue.reload.state } + .from('opened') + .to('closed') + end + + it 'leaves a system note on the issue' do + expect(SystemNoteService) + .to receive(:auto_resolve_prometheus_alert) + + subject.perform(project.id, alert_params) + end + end + + context 'when project could not be found' do + let(:non_existing_project_id) { (Project.maximum(:id) || 0) + 1 } + + it 'does not create an issue' do + expect { subject.perform(non_existing_project_id, alert_params) } + .not_to change(Issue, :count) + end + + it 'does not relate issue to an event' do + expect { subject.perform(non_existing_project_id, alert_params) } + .not_to change(prometheus_alert.related_issues, :count) + end + end + + context 'when event could not be found' do + before do + alert_params[:labels][:gitlab_alert_id] = (PrometheusAlertEvent.maximum(:id) || 0) + 1 + end + + it 'does not create an issue' do + expect { subject.perform(project.id, alert_params) } + .not_to change(Issue, :count) + end + + it 'does not relate issue to an event' do + expect { subject.perform(project.id, alert_params) } + .not_to change(prometheus_alert.related_issues, :count) + end + end + + context 'when issue could not be created' do + before do + allow_next_instance_of(IncidentManagement::CreateIssueService) do |instance| + allow(instance).to receive(:execute).and_return( { error: true } ) + end + end + + it 'does not relate issue to an event' do + expect { subject.perform(project.id, alert_params) } + .not_to change(prometheus_alert.related_issues, :count) + end + end + + context 'self-managed alert' do + let(:alert_name) { 'alert' } + let(:starts_at) { Time.now.rfc3339 } + + let!(:prometheus_alert_event) do + payload_key = SelfManagedPrometheusAlertEvent.payload_key_for(starts_at, alert_name, 'vector(1)') + create(:self_managed_prometheus_alert_event, project: project, payload_key: payload_key) + end + + let(:alert_params) do + { + startsAt: starts_at, + generatorURL: 'http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1', + labels: { + alertname: alert_name + } + }.with_indifferent_access + end + + it 'creates an issue' do + expect { subject.perform(project.id, alert_params) } + .to change(Issue, :count) + .by(1) + end + + it 'relates issue to an event' do + expect { subject.perform(project.id, alert_params) } + .to change(prometheus_alert_event.related_issues, :count) + .from(0) + .to(1) + end + + context 'when event could not be found' do + before do + alert_params[:generatorURL] = 'http://somethingelse.com' + end + + it 'creates an issue' do + expect { subject.perform(project.id, alert_params) } + .to change(Issue, :count) + .by(1) + end + + it 'does not relate issue to an event' do + expect { subject.perform(project.id, alert_params) } + .not_to change(prometheus_alert.related_issues, :count) + end + end + end + end +end |