Commit 8f9beefa authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent e4bf776a
......@@ -64,6 +64,7 @@ export default {
this.groupId,
term,
{
search_namespaces: true,
with_issues_enabled: true,
with_shared: false,
include_subgroups: true,
......
......@@ -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,
......
......@@ -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'),
......
......@@ -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: {
......
......@@ -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)
......
......@@ -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)
......
# 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
......@@ -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
......@@ -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 ) }
......
......@@ -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
......@@ -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'
......
......@@ -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
......
# frozen_string_literal: true
class LfsObjectsProject < ApplicationRecord
include ::EachBatch
belongs_to :project
belongs_to :lfs_object
......
......@@ -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) }
......
......@@ -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
......
......@@ -7,7 +7,6 @@ class ProjectGroupLink < ApplicationRecord
REPORTER = 20
DEVELOPER = 30
MAINTAINER = 40
MASTER = MAINTAINER # @deprecated
belongs_to :project
belongs_to :group
......
......@@ -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)
......
# 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
# 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
......@@ -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?
......
......@@ -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
......
# 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
# frozen_string_literal: true
class PrometheusAlertSerializer < BaseSerializer
entity PrometheusAlertEntity
end
......@@ -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
......
......@@ -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
......
......@@ -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
......
# 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
......@@ -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)
......
......@@ -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:
......
# 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
......@@ -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
......
---
title: Only enable searching of projects by full path / name on certain dropdowns
merge_request: 21910
author:
type: changed
---
title: Start merge request for custom dashboard if new branch is provided
merge_request: 27189
author:
type: added
---
title: Fix smartcard config initialization
merge_request: 27560
author:
type: fixed
---
title: Return 202 for command only notes in REST API
merge_request: 19624
author:
type: fixed
---
title: Create a rake task to cleanup unused LFS files
merge_request: 21747
author:
type: added
---
title: Reuse default generated snippet file name in repository
merge_request: 27673
author:
type: fixed
---
title: Fix new file not being created in non-ascii character folders
merge_request: 26165
author:
type: fixed
......@@ -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?
......@@ -671,6 +662,18 @@ Gitlab.ee do
end
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
#
......
......@@ -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
......
......@@ -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
}
......@@ -8498,6 +8499,56 @@ type UpdateNotePayload {
note: Note
}
"""
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
"""
......
......@@ -15331,6 +15331,33 @@
"isDeprecated": false,
"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,
......@@ -25662,6 +25689,142 @@
"enumValues": null,
"possibleTypes": null
},
{
"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",
......
......@@ -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
......
......@@ -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 |
......
......@@ -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)
......
# 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
......
......@@ -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:
......
# 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.
```
......@@ -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)|
......
......@@ -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
......@@ -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]
......
......@@ -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
......
......@@ -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
......
......@@ -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'
......
......@@ -14,8 +14,6 @@ module Gitlab
REPORTER = 20
DEVELOPER = 30
MAINTAINER = 40
# @deprecated
MASTER = MAINTAINER
OWNER = 50
# Branch protection settings
......
# 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
......@@ -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)
......
......@@ -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;
}
}
......
# 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
# 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
......@@ -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)
......
......@@ -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
......@@ -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
......
......@@ -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') }
......
......@@ -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",
......
......@@ -19,6 +19,7 @@
},
"additionalProperties": false
},
"commands_changes": { "type": "object", "additionalProperties": true },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"system": { "type": "boolean" },
......
......@@ -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);
......
......@@ -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)
......
......@@ -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
......
# 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
......@@ -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 } }
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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)
......
......@@ -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)
......
......@@ -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 } }
......
# 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
......@@ -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
......
......@@ -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
......
# 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
......@@ -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
......
......@@ -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
......
......@@ -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') }
......
......@@ -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))
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment