Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-03-25 18:08:10 +00:00
parent 6f2065c468
commit 5d75b2b9a9
31 changed files with 1846 additions and 1185 deletions

View File

@ -425,7 +425,7 @@ gem 'gitlab-mail_room', '~> 0.0.3', require: 'mail_room'
gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
gem 'ruby-prof', '~> 1.0.0'
gem 'ruby-prof', '~> 1.3.0'
gem 'stackprof', '~> 0.2.15', require: false
gem 'rbtrace', '~> 0.4', require: false
gem 'memory_profiler', '~> 0.9', require: false

View File

@ -951,7 +951,7 @@ GEM
i18n
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (1.0.0)
ruby-prof (1.3.1)
ruby-progressbar (1.10.1)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
@ -1358,7 +1358,7 @@ DEPENDENCIES
rubocop-performance (~> 1.4.1)
rubocop-rspec (~> 1.37.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 1.0.0)
ruby-prof (~> 1.3.0)
ruby-progressbar
ruby_parser (~> 3.8)
rubyzip (~> 2.0.0)

View File

@ -68,6 +68,9 @@ module BulkInsertSafe
# @param [Boolean] validate Whether validations should run on [items]
# @param [Integer] batch_size How many items should at most be inserted at once
# @param [Boolean] skip_duplicates Marks duplicates as allowed, and skips inserting them
# @param [Symbol] returns Pass :ids to return an array with the primary key values
# for all inserted records or nil to omit the underlying
# RETURNING SQL clause entirely.
# @param [Proc] handle_attributes Block that will receive each item attribute hash
# prior to insertion for further processing
#
@ -78,10 +81,11 @@ module BulkInsertSafe
#
# @return true if operation succeeded, throws otherwise.
#
def bulk_insert!(items, validate: true, skip_duplicates: false, batch_size: DEFAULT_BATCH_SIZE, &handle_attributes)
def bulk_insert!(items, validate: true, skip_duplicates: false, returns: nil, batch_size: DEFAULT_BATCH_SIZE, &handle_attributes)
_bulk_insert_all!(items,
validate: validate,
on_duplicate: skip_duplicates ? :skip : :raise,
returns: returns,
unique_by: nil,
batch_size: batch_size,
&handle_attributes)
@ -94,6 +98,9 @@ module BulkInsertSafe
# @param [Boolean] validate Whether validations should run on [items]
# @param [Integer] batch_size How many items should at most be inserted at once
# @param [Symbol/Array] unique_by Defines index or columns to use to consider item duplicate
# @param [Symbol] returns Pass :ids to return an array with the primary key values
# for all inserted or updated records or nil to omit the
# underlying RETURNING SQL clause entirely.
# @param [Proc] handle_attributes Block that will receive each item attribute hash
# prior to insertion for further processing
#
@ -109,10 +116,11 @@ module BulkInsertSafe
#
# @return true if operation succeeded, throws otherwise.
#
def bulk_upsert!(items, unique_by:, validate: true, batch_size: DEFAULT_BATCH_SIZE, &handle_attributes)
def bulk_upsert!(items, unique_by:, returns: nil, validate: true, batch_size: DEFAULT_BATCH_SIZE, &handle_attributes)
_bulk_insert_all!(items,
validate: validate,
on_duplicate: :update,
returns: returns,
unique_by: unique_by,
batch_size: batch_size,
&handle_attributes)
@ -120,21 +128,30 @@ module BulkInsertSafe
private
def _bulk_insert_all!(items, on_duplicate:, unique_by:, validate:, batch_size:, &handle_attributes)
return true if items.empty?
def _bulk_insert_all!(items, on_duplicate:, returns:, unique_by:, validate:, batch_size:, &handle_attributes)
return [] if items.empty?
returning =
case returns
when :ids
[primary_key]
when nil
false
else
raise ArgumentError, "returns needs to be :ids or nil"
end
transaction do
items.each_slice(batch_size) do |item_batch|
items.each_slice(batch_size).flat_map do |item_batch|
attributes = _bulk_insert_item_attributes(
item_batch, validate, &handle_attributes)
ActiveRecord::InsertAll
.new(self, attributes, on_duplicate: on_duplicate, unique_by: unique_by)
.execute
.new(self, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by)
.execute
.pluck(primary_key)
end
end
true
end
def _bulk_insert_item_attributes(items, validate_items)

View File

@ -58,7 +58,7 @@ module Projects
end
def tree_saver_class
if ::Feature.enabled?(:streaming_serializer, project)
if ::Feature.enabled?(:streaming_serializer, project, default_enabled: true)
Gitlab::ImportExport::Project::TreeSaver
else
# Once we remove :streaming_serializer feature flag, Project::LegacyTreeSaver should be removed as well

View File

@ -0,0 +1,124 @@
# frozen_string_literal: true
module Projects
module Prometheus
module Alerts
class NotifyService < BaseService
include Gitlab::Utils::StrongMemoize
include IncidentManagement::Settings
def execute(token)
return false unless valid_payload_size?
return false unless valid_version?
return false unless valid_alert_manager_token?(token)
persist_events
send_alert_email if send_email?
process_incident_issues if process_issues?
true
end
private
def valid_payload_size?
Gitlab::Utils::DeepSize.new(params).valid?
end
def send_email?
incident_management_setting.send_email && firings.any?
end
def firings
@firings ||= alerts_by_status('firing')
end
def alerts_by_status(status)
alerts.select { |alert| alert['status'] == status }
end
def alerts
params['alerts']
end
def valid_version?
params['version'] == '4'
end
def valid_alert_manager_token?(token)
valid_for_manual?(token) || valid_for_managed?(token)
end
def valid_for_manual?(token)
prometheus = project.find_or_initialize_service('prometheus')
return false unless prometheus.manual_configuration?
if setting = project.alerting_setting
compare_token(token, setting.token)
else
token.nil?
end
end
def valid_for_managed?(token)
prometheus_application = available_prometheus_application(project)
return false unless prometheus_application
if token
compare_token(token, prometheus_application.alert_manager_token)
else
prometheus_application.alert_manager_token.nil?
end
end
def available_prometheus_application(project)
alert_id = gitlab_alert_id
return unless alert_id
alert = find_alert(project, alert_id)
return unless alert
cluster = alert.environment.deployment_platform&.cluster
return unless cluster&.enabled?
return unless cluster.application_prometheus_available?
cluster.application_prometheus
end
def find_alert(project, metric)
Projects::Prometheus::AlertsFinder
.new(project: project, metric: metric)
.execute
.first
end
def gitlab_alert_id
alerts&.first&.dig('labels', 'gitlab_alert_id')
end
def compare_token(expected, actual)
return unless expected && actual
ActiveSupport::SecurityUtils.secure_compare(expected, actual)
end
def send_alert_email
notification_service
.async
.prometheus_alerts_fired(project, firings)
end
def process_incident_issues
alerts.each do |alert|
IncidentManagement::ProcessPrometheusAlertWorker
.perform_async(project.id, alert.to_h)
end
end
def persist_events
CreateEventsService.new(project, nil, params).execute
end
end
end
end
end

View File

@ -0,0 +1,5 @@
---
title: Enable streaming serializer feature flag by default.
merge_request: 27813
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Add support for Okta as a SCIM provider
merge_request: 25649
author:
type: added

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
# Patch to use COPY in db/structure.sql when populating schema_migrations table
# This is intended to reduce potential for merge conflicts in db/structure.sql
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(Gitlab::Database::PostgresqlAdapter::SchemaVersionsCopyMixin)

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ tasks such as:
To request access to Chatops on GitLab.com:
1. Log into <https://ops.gitlab.net/users/sign_in> **using the same username** as for GitLab.com (you may have to rename it).
1. Ask in the [#production](https://gitlab.slack.com/messages/production) channel to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
1. Ask in the [#production](https://gitlab.slack.com/messages/production) channel for an existing member to add you to the `chatops` project in Ops. They can do it by running `/chatops run member add <username> gitlab-com/chatops --ops` command in that channel.
NOTE: **Note:** If you had to change your username for GitLab.com on the first step, make sure [to reflect this information](https://gitlab.com/gitlab-com/www-gitlab-com#adding-yourself-to-the-team-page) on [the team page](https://about.gitlab.com/company/team/).

View File

@ -150,6 +150,10 @@ module API
authorize! :download_code, release
end
def authorize_create_evidence!
# This is a separate method so that EE can extend its behaviour
end
def release
@release ||= user_project.releases.find_by_tag(params[:tag])
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Gitlab
module Database
module PostgresqlAdapter
module SchemaVersionsCopyMixin
extend ActiveSupport::Concern
def dump_schema_information # :nodoc:
versions = schema_migration.all_versions
copy_versions_sql(versions) if versions.any?
end
private
def copy_versions_sql(versions)
sm_table = quote_table_name(schema_migration.table_name)
sql = +"COPY #{sm_table} (version) FROM STDIN;\n"
sql << versions.map { |v| Integer(v) }.sort.join("\n")
sql << "\n\\.\n"
sql
end
end
end
end
end

View File

@ -168,9 +168,9 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def self.print_by_total_time(result, options = {})
default_options = { sort_method: :total_time }
default_options = { sort_method: :total_time, filter_by: :total_time }
Gitlab::Profiler::TotalTimeFlatPrinter.new(result).print(STDOUT, default_options.merge(options))
RubyProf::FlatPrinter.new(result).print(STDOUT, default_options.merge(options))
end
end
end

View File

@ -1,42 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Profiler
class TotalTimeFlatPrinter < RubyProf::FlatPrinter
def max_percent
@options[:max_percent] || 100
end
# Copied from:
# <https://github.com/ruby-prof/ruby-prof/blob/master/lib/ruby-prof/printers/flat_printer.rb>
#
# The changes are just to filter by total time, not self time, and add a
# max_percent option as well.
def print_methods(thread)
total_time = thread.total_time
methods = thread.methods.sort_by(&sort_method).reverse
sum = 0
methods.each do |method|
total_percent = (method.total_time / total_time) * 100
next if total_percent < min_percent
next if total_percent > max_percent
sum += method.self_time
@output << "%6.2f %9.3f %9.3f %9.3f %9.3f %8d %s%-30s %s\n" % [
method.self_time / total_time * 100, # %self
method.total_time, # total
method.self_time, # self
method.wait_time, # wait
method.children_time, # children
method.called, # calls
method.recursive? ? "*" : " ", # cycle
method.full_name, # method_name
method_location(method) # location
]
end
end
end
end
end

View File

@ -4,75 +4,110 @@ require 'toml-rb'
module Gitlab
module SetupHelper
class << self
# We cannot create config.toml files for all possible Gitaly configuations.
# For instance, if Gitaly is running on another machine then it makes no
# sense to write a config.toml file on the current machine. This method will
# only generate a configuration for the most common and simplest case: when
# we have exactly one Gitaly process and we are sure it is running locally
# because it uses a Unix socket.
# For development and testing purposes, an extra storage is added to gitaly,
# which is not known to Rails, but must be explicitly stubbed.
def gitaly_configuration_toml(gitaly_dir, storage_paths, gitaly_ruby: true)
storages = []
address = nil
def create_configuration(dir, storage_paths, force: false)
generate_configuration(
configuration_toml(dir, storage_paths),
get_config_path(dir),
force: force
)
end
Gitlab.config.repositories.storages.each do |key, val|
if address
if address != val['gitaly_address']
raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
# rubocop:disable Rails/Output
def generate_configuration(toml_data, config_path, force: false)
FileUtils.rm_f(config_path) if force
File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f|
f.puts toml_data
end
rescue Errno::EEXIST
puts 'Skipping config.toml generation:'
puts 'A configuration file already exists.'
rescue ArgumentError => e
puts 'Skipping config.toml generation:'
puts e.message
end
# rubocop:enable Rails/Output
module Gitaly
extend Gitlab::SetupHelper
class << self
# We cannot create config.toml files for all possible Gitaly configuations.
# For instance, if Gitaly is running on another machine then it makes no
# sense to write a config.toml file on the current machine. This method will
# only generate a configuration for the most common and simplest case: when
# we have exactly one Gitaly process and we are sure it is running locally
# because it uses a Unix socket.
# For development and testing purposes, an extra storage is added to gitaly,
# which is not known to Rails, but must be explicitly stubbed.
def configuration_toml(gitaly_dir, storage_paths, gitaly_ruby: true)
storages = []
address = nil
Gitlab.config.repositories.storages.each do |key, val|
if address
if address != val['gitaly_address']
raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
end
elsif URI(val['gitaly_address']).scheme != 'unix'
raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
else
address = val['gitaly_address']
end
elsif URI(val['gitaly_address']).scheme != 'unix'
raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
else
address = val['gitaly_address']
storages << { name: key, path: storage_paths[key] }
end
storages << { name: key, path: storage_paths[key] }
config = { socket_path: address.sub(/\Aunix:/, '') }
if Rails.env.test?
storage_path = Rails.root.join('tmp', 'tests', 'second_storage').to_s
storages << { name: 'test_second_storage', path: storage_path }
config[:auth] = { token: 'secret' }
# Compared to production, tests run in constrained environments. This
# number is meant to grow with the number of concurrent rails requests /
# sidekiq jobs, and concurrency will be low anyway in test.
config[:git] = { catfile_cache_size: 5 }
end
config[:storage] = storages
internal_socket_dir = File.join(gitaly_dir, 'internal_sockets')
FileUtils.mkdir(internal_socket_dir) unless File.exist?(internal_socket_dir)
config[:internal_socket_dir] = internal_socket_dir
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
TomlRB.dump(config)
end
if Rails.env.test?
storage_path = Rails.root.join('tmp', 'tests', 'second_storage').to_s
storages << { name: 'test_second_storage', path: storage_path }
private
def get_config_path(dir)
File.join(dir, 'config.toml')
end
config = { socket_path: address.sub(/\Aunix:/, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
internal_socket_dir = File.join(gitaly_dir, 'internal_sockets')
FileUtils.mkdir(internal_socket_dir) unless File.exist?(internal_socket_dir)
config[:internal_socket_dir] = internal_socket_dir
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
if Rails.env.test?
# Compared to production, tests run in constrained environments. This
# number is meant to grow with the number of concurrent rails requests /
# sidekiq jobs, and concurrency will be low anyway in test.
config[:git] = { catfile_cache_size: 5 }
end
TomlRB.dump(config)
end
end
# rubocop:disable Rails/Output
def create_gitaly_configuration(dir, storage_paths, force: false)
config_path = File.join(dir, 'config.toml')
FileUtils.rm_f(config_path) if force
module Praefect
extend Gitlab::SetupHelper
class << self
def configuration_toml(gitaly_dir, storage_paths)
nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }]
config = { socket_path: "#{gitaly_dir}/praefect.socket", virtual_storage_name: 'default', token: 'secret', node: nodes }
config[:token] = 'secret' if Rails.env.test?
File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f|
f.puts gitaly_configuration_toml(dir, storage_paths)
TomlRB.dump(config)
end
private
def get_config_path(dir)
File.join(dir, 'praefect.config.toml')
end
rescue Errno::EEXIST
puts "Skipping config.toml generation:"
puts "A configuration file already exists."
rescue ArgumentError => e
puts "Skipping config.toml generation:"
puts e.message
end
# rubocop:enable Rails/Output
end
end
end

View File

@ -27,7 +27,7 @@ Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]")
end
storage_paths = { 'default' => args.storage_path }
Gitlab::SetupHelper.create_gitaly_configuration(args.dir, storage_paths)
Gitlab::SetupHelper::Gitaly.create_configuration(args.dir, storage_paths)
Dir.chdir(args.dir) do
# In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present?

View File

@ -48,7 +48,7 @@ class UploadedFile
return if path.blank? && remote_id.blank?
file_path = nil
if path
if path.present?
file_path = File.realpath(path)
paths = Array(upload_paths) << Dir.tmpdir

View File

@ -40,7 +40,7 @@
"@babel/preset-env": "^7.8.4",
"@gitlab/at.js": "^1.5.5",
"@gitlab/svgs": "^1.115.0",
"@gitlab/ui": "^10.0.0",
"@gitlab/ui": "^10.0.1",
"@gitlab/visual-review-tools": "1.5.1",
"@sentry/browser": "^5.10.2",
"@sourcegraph/code-host-integration": "0.0.33",

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
context 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/issues/36817', type: :bug } do
context 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/30226', type: :bug } do
describe 'Merge request rebasing' do
it 'user rebases source branch of merge request' do
Flow::Login.sign_in

View File

@ -17,13 +17,16 @@ class GitalyTestBuild
check_gitaly_config!
# Starting gitaly further validates its configuration
pid = start_gitaly
Process.kill('TERM', pid)
gitaly_pid = start_gitaly
praefect_pid = start_praefect
Process.kill('TERM', gitaly_pid)
Process.kill('TERM', praefect_pid)
# Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'.
# Without this a gitaly executable created in the setup-test-env job
# will look stale compared to GITALY_SERVER_VERSION.
FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24))
FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'praefect'), mtime: Time.now + (1 << 24))
end
end

View File

@ -13,10 +13,9 @@ class GitalyTestSpawn
# # Uncomment line below to see all gitaly logs merged into CI trace
# spawn('sleep 1; tail -f log/gitaly-test.log')
pid = start_gitaly
# In local development this pid file is used by rspec.
IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), pid)
IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), start_gitaly)
IO.write(File.expand_path('../tmp/tests/praefect.pid', __dir__), start_praefect)
end
end

View File

@ -37,16 +37,31 @@ module GitalyTest
env_hash
end
def config_path
File.join(tmp_tests_gitaly_dir, 'config.toml')
def config_path(service)
case service
when :gitaly
File.join(tmp_tests_gitaly_dir, 'config.toml')
when :praefect
File.join(tmp_tests_gitaly_dir, 'praefect.config.toml')
end
end
def start_gitaly
args = %W[#{tmp_tests_gitaly_dir}/gitaly #{config_path}]
pid = spawn(env, *args, [:out, :err] => 'log/gitaly-test.log')
start(:gitaly)
end
def start_praefect
start(:praefect)
end
def start(service)
args = ["#{tmp_tests_gitaly_dir}/#{service}"]
args.push("-config") if service == :praefect
args.push(config_path(service))
pid = spawn(env, *args, [:out, :err] => "log/#{service}-test.log")
begin
try_connect!
try_connect!(service)
rescue
Process.kill('TERM', pid)
raise
@ -68,11 +83,11 @@ module GitalyTest
abort 'bundle check failed' unless system(env, 'bundle', 'check', chdir: File.dirname(gemfile))
end
def read_socket_path
def read_socket_path(service)
# This code needs to work in an environment where we cannot use bundler,
# so we cannot easily use the toml-rb gem. This ad-hoc parser should be
# good enough.
config_text = IO.read(config_path)
config_text = IO.read(config_path(service))
config_text.lines.each do |line|
match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/)
@ -80,14 +95,14 @@ module GitalyTest
return match_data[1] if match_data
end
raise "failed to find socket_path in #{config_path}"
raise "failed to find socket_path in #{config_path(service)}"
end
def try_connect!
print "Trying to connect to gitaly: "
def try_connect!(service)
print "Trying to connect to #{service}: "
timeout = 20
delay = 0.1
socket = read_socket_path
socket = read_socket_path(service)
Integer(timeout / delay).times do
UNIXSocket.new(socket)

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Database::PostgresqlAdapter::SchemaVersionsCopyMixin do
let(:schema_migration) { double('schem_migration', table_name: table_name, all_versions: versions) }
let(:versions) { %w(5 2 1000 200 4 93 2) }
let(:table_name) { "schema_migrations" }
let(:instance) do
Object.new.extend(described_class)
end
before do
allow(instance).to receive(:schema_migration).and_return(schema_migration)
allow(instance).to receive(:quote_table_name).with(table_name).and_return("\"#{table_name}\"")
end
subject { instance.dump_schema_information }
it 'uses COPY FROM STDIN' do
expect(subject.split("\n").first).to match(/COPY "schema_migrations" \(version\) FROM STDIN;/)
end
it 'contains a sorted list of versions by their numeric value' do
version_lines = subject.split("\n")[1..-2].map(&:to_i)
expect(version_lines).to eq(versions.map(&:to_i).sort)
end
it 'contains a end-of-data marker' do
expect(subject).to end_with("\\.\n")
end
context 'with non-Integer versions' do
let(:versions) { %w(5 2 4 abc) }
it 'raises an error' do
expect { subject }.to raise_error(/invalid value for Integer/)
end
end
end

View File

@ -59,6 +59,16 @@ describe UploadedFile do
expect(subject.sha256).to eq('sha256')
expect(subject.remote_id).to eq('remote_id')
end
it 'handles a blank path' do
params['file.path'] = ''
# Not a real file, so can't determine size itself
params['file.size'] = 1.byte
expect { described_class.from_params(params, :file, upload_path) }
.not_to raise_error
end
end
end

View File

@ -129,10 +129,37 @@ describe BulkInsertSafe do
end.not_to change { described_class.count }
end
it 'does nothing and returns true when items are empty' do
expect(described_class.bulk_insert!([])).to be(true)
it 'does nothing and returns an empty array when items are empty' do
expect(described_class.bulk_insert!([])).to eq([])
expect(described_class.count).to eq(0)
end
context 'with returns option set' do
context 'when is set to :ids' do
it 'return an array with the primary key values for all inserted records' do
items = described_class.valid_list(1)
expect(described_class.bulk_insert!(items, returns: :ids)).to contain_exactly(a_kind_of(Integer))
end
end
context 'when is set to nil' do
it 'returns an empty array' do
items = described_class.valid_list(1)
expect(described_class.bulk_insert!(items, returns: nil)).to eq([])
end
end
context 'when is set to anything else' do
it 'raises an error' do
items = described_class.valid_list(1)
expect { described_class.bulk_insert!([items], returns: [:id, :name]) }
.to raise_error(ArgumentError, "returns needs to be :ids or nil")
end
end
end
end
context 'when duplicate items are to be inserted' do

View File

@ -6,6 +6,7 @@ describe API::Releases do
let(:project) { create(:project, :repository, :private) }
let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
let(:developer) { create(:user) }
let(:guest) { create(:user) }
let(:non_project_member) { create(:user) }
let(:commit) { create(:commit, project: project) }
@ -15,6 +16,7 @@ describe API::Releases do
project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.add_guest(guest)
project.add_developer(developer)
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
@ -248,6 +250,24 @@ describe API::Releases do
.to match_array(release.sources.map(&:url))
end
context 'with evidence' do
let!(:evidence) { create(:evidence, release: release) }
it 'returns the evidence' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(json_response['evidences'].count).to eq(1)
end
it '#collected_at' do
Timecop.freeze(Time.now.round) do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(json_response['evidences'].first['collected_at'].to_datetime.to_i).to be_within(1.minute).of(release.evidences.first.created_at.to_i)
end
end
end
context 'when release has link asset' do
let!(:link) do
create(:release_link,

View File

@ -0,0 +1,343 @@
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Prometheus::Alerts::NotifyService do
let_it_be(:project, reload: true) { create(:project) }
let(:service) { described_class.new(project, nil, payload) }
let(:token_input) { 'token' }
let!(:setting) do
create(:project_incident_management_setting, project: project, send_email: true, create_issue: true)
end
let(:subject) { service.execute(token_input) }
before do
# We use `let_it_be(:project)` so we make sure to clear caches
project.clear_memoization(:licensed_feature_available)
end
shared_examples 'sends notification email' do
let(:notification_service) { spy }
it 'sends a notification for firing alerts only' do
expect(NotificationService)
.to receive(:new)
.and_return(notification_service)
expect(notification_service)
.to receive_message_chain(:async, :prometheus_alerts_fired)
expect(subject).to eq(true)
end
end
shared_examples 'processes incident issues' do |amount|
let(:create_incident_service) { spy }
it 'processes issues' do
expect(IncidentManagement::ProcessPrometheusAlertWorker)
.to receive(:perform_async)
.with(project.id, kind_of(Hash))
.exactly(amount).times
Sidekiq::Testing.inline! do
expect(subject).to eq(true)
end
end
end
shared_examples 'does not process incident issues' do
it 'does not process issues' do
expect(IncidentManagement::ProcessPrometheusAlertWorker)
.not_to receive(:perform_async)
expect(subject).to eq(true)
end
end
shared_examples 'persists events' do
let(:create_events_service) { spy }
it 'persists events' do
expect(Projects::Prometheus::Alerts::CreateEventsService)
.to receive(:new)
.and_return(create_events_service)
expect(create_events_service)
.to receive(:execute)
expect(subject).to eq(true)
end
end
shared_examples 'notifies alerts' do
it_behaves_like 'sends notification email'
it_behaves_like 'persists events'
end
shared_examples 'no notifications' do
let(:notification_service) { spy }
let(:create_events_service) { spy }
it 'does not notify' do
expect(notification_service).not_to receive(:async)
expect(create_events_service).not_to receive(:execute)
expect(subject).to eq(false)
end
end
context 'with valid payload' do
let(:alert_firing) { create(:prometheus_alert, project: project) }
let(:alert_resolved) { create(:prometheus_alert, project: project) }
let(:payload_raw) { payload_for(firing: [alert_firing], resolved: [alert_resolved]) }
let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
let(:payload_alert_firing) { payload_raw['alerts'].first }
let(:token) { 'token' }
context 'with project specific cluster' do
using RSpec::Parameterized::TableSyntax
where(:cluster_enabled, :status, :configured_token, :token_input, :result) do
true | :installed | token | token | :success
true | :installed | nil | nil | :success
true | :updated | token | token | :success
true | :updating | token | token | :failure
true | :installed | token | 'x' | :failure
true | :installed | nil | token | :failure
true | :installed | token | nil | :failure
true | nil | token | token | :failure
false | :installed | token | token | :failure
end
with_them do
before do
cluster = create(:cluster, :provided_by_user,
projects: [project],
enabled: cluster_enabled)
if status
create(:clusters_applications_prometheus, status,
cluster: cluster,
alert_manager_token: configured_token)
end
end
case result = params[:result]
when :success
it_behaves_like 'notifies alerts'
when :failure
it_behaves_like 'no notifications'
else
raise "invalid result: #{result.inspect}"
end
end
end
context 'without project specific cluster' do
let!(:cluster) { create(:cluster, enabled: true) }
it_behaves_like 'no notifications'
end
context 'with manual prometheus installation' do
using RSpec::Parameterized::TableSyntax
where(:alerting_setting, :configured_token, :token_input, :result) do
true | token | token | :success
true | token | 'x' | :failure
true | token | nil | :failure
false | nil | nil | :success
false | nil | token | :failure
end
with_them do
let(:alert_manager_token) { token_input }
before do
create(:prometheus_service, project: project)
if alerting_setting
create(:project_alerting_setting,
project: project,
token: configured_token)
end
end
case result = params[:result]
when :success
it_behaves_like 'notifies alerts'
when :failure
it_behaves_like 'no notifications'
else
raise "invalid result: #{result.inspect}"
end
end
end
context 'alert emails' do
before do
create(:prometheus_service, project: project)
create(:project_alerting_setting, project: project, token: token)
end
context 'when incident_management_setting does not exist' do
let!(:setting) { nil }
it_behaves_like 'persists events'
it 'does not send notification email', :sidekiq_might_not_need_inline do
expect_any_instance_of(NotificationService)
.not_to receive(:async)
expect(subject).to eq(true)
end
end
context 'when incident_management_setting.send_email is true' do
it_behaves_like 'notifies alerts'
end
context 'incident_management_setting.send_email is false' do
let!(:setting) do
create(:project_incident_management_setting, send_email: false, project: project)
end
it_behaves_like 'persists events'
it 'does not send notification' do
expect(NotificationService).not_to receive(:new)
expect(subject).to eq(true)
end
end
end
context 'process incident issues' do
before do
create(:prometheus_service, project: project)
create(:project_alerting_setting, project: project, token: token)
end
context 'with create_issue setting enabled' do
before do
setting.update!(create_issue: true)
end
it_behaves_like 'processes incident issues', 2
context 'multiple firing alerts' do
let(:payload_raw) do
payload_for(firing: [alert_firing, alert_firing], resolved: [])
end
it_behaves_like 'processes incident issues', 2
end
context 'without firing alerts' do
let(:payload_raw) do
payload_for(firing: [], resolved: [alert_resolved])
end
it_behaves_like 'processes incident issues', 1
end
end
context 'with create_issue setting disabled' do
before do
setting.update!(create_issue: false)
end
it_behaves_like 'does not process incident issues'
end
end
end
context 'with invalid payload' do
context 'without version' do
let(:payload) { {} }
it_behaves_like 'no notifications'
end
context 'when version is not "4"' do
let(:payload) { { 'version' => '5' } }
it_behaves_like 'no notifications'
end
context 'with missing alerts' do
let(:payload) { { 'version' => '4' } }
it_behaves_like 'no notifications'
end
context 'when the payload is too big' do
let(:payload) { { 'the-payload-is-too-big' => true } }
let(:deep_size_object) { instance_double(Gitlab::Utils::DeepSize, valid?: false) }
before do
allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object)
end
it_behaves_like 'no notifications'
it 'does not process issues' do
expect(IncidentManagement::ProcessPrometheusAlertWorker)
.not_to receive(:perform_async)
subject
end
end
end
private
def payload_for(firing: [], resolved: [])
status = firing.any? ? 'firing' : 'resolved'
alerts = firing + resolved
alert_name = alerts.first.title
prometheus_metric_id = alerts.first.prometheus_metric_id.to_s
alerts_map = \
firing.map { |alert| map_alert_payload('firing', alert) } +
resolved.map { |alert| map_alert_payload('resolved', alert) }
# See https://prometheus.io/docs/alerting/configuration/#%3Cwebhook_config%3E
{
'version' => '4',
'receiver' => 'gitlab',
'status' => status,
'alerts' => alerts_map,
'groupLabels' => {
'alertname' => alert_name
},
'commonLabels' => {
'alertname' => alert_name,
'gitlab' => 'hook',
'gitlab_alert_id' => prometheus_metric_id
},
'commonAnnotations' => {},
'externalURL' => '',
'groupKey' => "{}:{alertname=\'#{alert_name}\'}"
}
end
def map_alert_payload(status, alert)
{
'status' => status,
'labels' => {
'alertname' => alert.title,
'gitlab' => 'hook',
'gitlab_alert_id' => alert.prometheus_metric_id.to_s
},
'annotations' => {},
'startsAt' => '2018-09-24T08:57:31.095725221Z',
'endsAt' => '0001-01-01T00:00:00Z',
'generatorURL' => 'http://prometheus-prometheus-server-URL'
}
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rspec/mocks'
require 'toml-rb'
module TestEnv
extend ActiveSupport::Concern
@ -87,7 +86,7 @@ module TestEnv
'conflict-resolvable-fork' => '404fa3f'
}.freeze
TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze
REPOS_STORAGE = 'default'.freeze
SECOND_STORAGE_PATH = Rails.root.join('tmp', 'tests', 'second_storage')
@ -140,7 +139,7 @@ module TestEnv
#
# Keeps gitlab-shell and gitlab-test
def clean_test_path
Dir[TMP_TEST_PATH].each do |entry|
Dir[File.join(TMP_TEST_PATH, '**')].each do |entry|
unless test_dirs.include?(File.basename(entry))
FileUtils.rm_rf(entry)
end
@ -164,7 +163,8 @@ module TestEnv
install_dir: gitaly_dir,
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{install_gitaly_args}]") do
Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
Gitlab::SetupHelper::Gitaly.create_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
start_gitaly(gitaly_dir)
end
end
@ -192,17 +192,38 @@ module TestEnv
end
end
@gitaly_pid = Integer(File.read('tmp/tests/gitaly.pid'))
gitaly_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly.pid')))
praefect_pid = Integer(File.read(TMP_TEST_PATH.join('praefect.pid')))
Kernel.at_exit { stop_gitaly }
Kernel.at_exit { stop(gitaly_pid) }
Kernel.at_exit { stop(praefect_pid) }
wait_gitaly
wait('gitaly')
wait('praefect')
end
def wait_gitaly
def stop(pid)
Process.kill('KILL', pid)
rescue Errno::ESRCH
# The process can already be gone if the test run was INTerrupted.
end
def gitaly_url
ENV.fetch('GITALY_REPO_URL', nil)
end
def socket_path(service)
TMP_TEST_PATH.join('gitaly', "#{service}.socket").to_s
end
def praefect_socket_path
"unix:" + socket_path(:praefect)
end
def wait(service)
sleep_time = 10
sleep_interval = 0.1
socket = Gitlab::GitalyClient.address('default').sub('unix:', '')
socket = socket_path(service)
Integer(sleep_time / sleep_interval).times do
Socket.unix(socket)
@ -211,19 +232,7 @@ module TestEnv
sleep sleep_interval
end
raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
end
def stop_gitaly
return unless @gitaly_pid
Process.kill('KILL', @gitaly_pid)
rescue Errno::ESRCH
# The process can already be gone if the test run was INTerrupted.
end
def gitaly_url
ENV.fetch('GITALY_REPO_URL', nil)
raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds"
end
def setup_workhorse

11
spec/support/praefect.rb Normal file
View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require_relative 'helpers/test_env'
RSpec.configure do |config|
config.before(:each, :praefect) do
allow(Gitlab.config.repositories.storages['default']).to receive(:[]).and_call_original
allow(Gitlab.config.repositories.storages['default']).to receive(:[]).with('gitaly_address')
.and_return(TestEnv.praefect_socket_path)
end
end

View File

@ -45,11 +45,11 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass|
expect { target_class.bulk_insert!(items) }.to change { target_class.count }.by(items.size)
end
it 'returns true' do
it 'returns an empty array' do
items = valid_items_for_bulk_insertion
expect(items).not_to be_empty
expect(target_class.bulk_insert!(items)).to be true
expect(target_class.bulk_insert!(items)).to eq([])
end
end
@ -69,7 +69,7 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass|
# it is not always possible to create invalid items
if items.any?
expect(target_class.bulk_insert!(items, validate: false)).to be(true)
expect(target_class.bulk_insert!(items, validate: false)).to eq([])
expect(target_class.count).to eq(items.size)
end
end

View File

@ -786,10 +786,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.115.0.tgz#2762ad045d5a2bd728f74fcb4c00caa9bd6dbc22"
integrity sha512-jlmNGqCTpSiPFrNbLaW6GGXNbvIShLdrpeYTtSEz/yFJMClQfPjHc8Zm9bl/PqAM5d/yGQqk8e+rBc4LeAhEfg==
"@gitlab/ui@^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-10.0.0.tgz#dced1119237f328367e8c4922cf4e1ae986fac54"
integrity sha512-+qsojtfE5mhryjJyReXBY9C3J4s4jlRpHfEcaCFuhcebtq5Uhd6xgLwgxT+E7fMvtLQpGATMo1DiD80yhLb2pQ==
"@gitlab/ui@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-10.0.1.tgz#65deee8ded3b8d003dfd74cd93c7eb0549e11b37"
integrity sha512-RMOJjpZjmWJnu0ebfGJsPOn6/ko+HlfHYbBXBImpTIk6Xsr5AaRjT4yCYEoefZ55jK/SJ2nxHytqrMe26wjfDA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"