summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/entities.rb5
-rw-r--r--lib/api/issues.rb6
-rw-r--r--lib/gitlab/action_rate_limiter.rb4
-rw-r--r--lib/gitlab/background_migration/legacy_upload_mover.rb140
-rw-r--r--lib/gitlab/background_migration/legacy_uploads_migrator.rb23
-rw-r--r--lib/gitlab/background_migration/logger.rb12
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb2
-rw-r--r--lib/gitlab/ci/build/rules.rb37
-rw-r--r--lib/gitlab/ci/build/rules/rule.rb32
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb31
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb23
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/if.rb19
-rw-r--r--lib/gitlab/ci/config/entry/job.rb31
-rw-r--r--lib/gitlab/ci/config/entry/rules.rb33
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb42
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb70
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/yaml_processor.rb3
-rw-r--r--lib/gitlab/config/entry/validators.rb24
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/git_post_receive.rb11
-rw-r--r--lib/gitlab/grape_logging/loggers/client_env_logger.rb16
-rw-r--r--lib/gitlab/repository_cache_adapter.rb53
-rw-r--r--lib/gitlab/repository_set_cache.rb67
-rw-r--r--lib/gitlab/sentry.rb17
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb6
-rw-r--r--lib/gitlab/usage_data.rb8
-rw-r--r--lib/gitlab/usage_data_counters/note_counter.rb8
-rw-r--r--lib/gt_one_coercion.rb7
-rw-r--r--lib/prometheus/cleanup_multiproc_dir_service.rb23
-rw-r--r--lib/tasks/gitlab/uploads/legacy.rake27
33 files changed, 737 insertions, 52 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e500a93b31e..219ed45eff6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -18,7 +18,7 @@ module API
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
include: [
GrapeLogging::Loggers::FilterParameters.new(LOG_FILTERS),
- GrapeLogging::Loggers::ClientEnv.new,
+ Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new,
Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index e4f4e79cd46..a2f3e87ebd2 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -43,7 +43,7 @@ module API
path = params[:path]
before = params[:until]
after = params[:since]
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all]
+ ref = params[:ref_name].presence || user_project.try(:default_branch) || 'master' unless params[:all]
offset = (params[:page] - 1) * params[:per_page]
all = params[:all]
with_stats = params[:with_stats]
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 09253ab6b0e..5e66b4e76a5 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -645,7 +645,10 @@ module API
end
end
- expose :subscribed do |issue, options|
+ # Calculating the value of subscribed field triggers Markdown
+ # processing. We can't do that for multiple issues / merge
+ # requests in a single API request.
+ expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options|
issue.subscribed?(options[:current_user], options[:project] || issue.project)
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index d687acf3423..7819c2de515 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -96,7 +96,8 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
+ issuable_metadata: issuable_meta_data(issues, 'Issue', current_user),
+ include_subscribed: false
}
present issues, options
@@ -122,7 +123,8 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
+ issuable_metadata: issuable_meta_data(issues, 'Issue', current_user),
+ include_subscribed: false
}
present issues, options
diff --git a/lib/gitlab/action_rate_limiter.rb b/lib/gitlab/action_rate_limiter.rb
index fdb06d00548..0e8707af631 100644
--- a/lib/gitlab/action_rate_limiter.rb
+++ b/lib/gitlab/action_rate_limiter.rb
@@ -49,9 +49,9 @@ module Gitlab
request_information = {
message: 'Action_Rate_Limiter_Request',
env: type,
- ip: request.ip,
+ remote_ip: request.ip,
request_method: request.request_method,
- fullpath: request.fullpath
+ path: request.fullpath
}
if current_user
diff --git a/lib/gitlab/background_migration/legacy_upload_mover.rb b/lib/gitlab/background_migration/legacy_upload_mover.rb
new file mode 100644
index 00000000000..051c1176edb
--- /dev/null
+++ b/lib/gitlab/background_migration/legacy_upload_mover.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class takes a legacy upload and migrates it to the correct location
+ class LegacyUploadMover
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :upload, :project, :note
+ attr_accessor :logger
+
+ def initialize(upload)
+ @upload = upload
+ @note = Note.find_by(id: upload.model_id)
+ @project = note&.project
+ @logger = Gitlab::BackgroundMigration::Logger.build
+ end
+
+ def execute
+ return unless upload
+
+ if !project
+ # if we don't have models associated with the upload we can not move it
+ warn('Deleting upload due to model not found.')
+
+ destroy_legacy_upload
+ elsif note.is_a?(LegacyDiffNote)
+ return unless move_legacy_diff_file
+
+ migrate_upload
+ elsif !legacy_file_exists?
+ warn('Deleting upload due to file not found.')
+ destroy_legacy_upload
+ else
+ migrate_upload
+ end
+ end
+
+ private
+
+ def migrate_upload
+ return unless copy_upload_to_project
+
+ add_upload_link_to_note_text
+ destroy_legacy_file
+ destroy_legacy_upload
+ end
+
+ # we should proceed and log whenever one upload copy fails, no matter the reasons
+ # rubocop: disable Lint/RescueException
+ def copy_upload_to_project
+ @uploader = FileUploader.copy_to(legacy_file_uploader, project)
+
+ logger.info(
+ message: 'MigrateLegacyUploads: File copied successfully',
+ old_path: legacy_file_uploader.file.path, new_path: @uploader.file.path
+ )
+ true
+ rescue Exception => e
+ warn(
+ 'File could not be copied to project uploads',
+ file_path: legacy_file_uploader.file.path, error: e.message
+ )
+ false
+ end
+ # rubocop: enable Lint/RescueException
+
+ def destroy_legacy_upload
+ if note
+ note.remove_attachment = true
+ note.save
+ end
+
+ if upload.destroy
+ logger.info(message: 'MigrateLegacyUploads: Upload was destroyed.', upload: upload.inspect)
+ else
+ warn('MigrateLegacyUploads: Upload destroy failed.')
+ end
+ end
+
+ def destroy_legacy_file
+ legacy_file_uploader.file.delete
+ end
+
+ def add_upload_link_to_note_text
+ new_text = "#{note.note} \n #{@uploader.markdown_link}"
+ # Bypass validations because old data may have invalid
+ # noteable values. If we fail hard here, we may kill the
+ # entire background migration, which affects a range of notes.
+ note.update_attribute(:note, new_text)
+ end
+
+ def legacy_file_uploader
+ strong_memoize(:legacy_file_uploader) do
+ uploader = upload.build_uploader
+ uploader.retrieve_from_store!(File.basename(upload.path))
+ uploader
+ end
+ end
+
+ def legacy_file_exists?
+ legacy_file_uploader.file.exists?
+ end
+
+ # we should proceed and log whenever one upload copy fails, no matter the reasons
+ # rubocop: disable Lint/RescueException
+ def move_legacy_diff_file
+ old_path = upload.absolute_path
+ old_path_sub = '-/system/note/attachment'
+
+ if !File.exist?(old_path) || !old_path.include?(old_path_sub)
+ log_legacy_diff_note_problem(old_path)
+ return false
+ end
+
+ new_path = upload.absolute_path.sub(old_path_sub, '-/system/legacy_diff_note/attachment')
+ new_dir = File.dirname(new_path)
+ FileUtils.mkdir_p(new_dir)
+
+ FileUtils.mv(old_path, new_path)
+ rescue Exception => e
+ log_legacy_diff_note_problem(old_path, new_path, e)
+ false
+ end
+
+ def warn(message, params = {})
+ logger.warn(
+ params.merge(message: "MigrateLegacyUploads: #{message}", upload: upload.inspect)
+ )
+ end
+
+ def log_legacy_diff_note_problem(old_path, new_path = nil, error = nil)
+ warn('LegacyDiffNote upload could not be moved to a new path',
+ old_path: old_path, new_path: new_path, error: error&.message
+ )
+ end
+ # rubocop: enable Lint/RescueException
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/legacy_uploads_migrator.rb b/lib/gitlab/background_migration/legacy_uploads_migrator.rb
new file mode 100644
index 00000000000..a9d38a27e0c
--- /dev/null
+++ b/lib/gitlab/background_migration/legacy_uploads_migrator.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This migration takes all legacy uploads (that were uploaded using AttachmentUploader)
+ # and migrate them to the new (FileUploader) location (=under projects).
+ #
+ # We have dependencies (uploaders) in this migration because extracting code would add a lot of complexity
+ # and possible errors could appear as the logic in the uploaders is not trivial.
+ #
+ # This migration will be removed in 13.0 in order to get rid of a migration that depends on
+ # the application code.
+ class LegacyUploadsMigrator
+ include Database::MigrationHelpers
+
+ def perform(start_id, end_id)
+ Upload.where(id: start_id..end_id, uploader: 'AttachmentUploader').find_each do |upload|
+ LegacyUploadMover.new(upload).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/logger.rb b/lib/gitlab/background_migration/logger.rb
new file mode 100644
index 00000000000..4ea89771eff
--- /dev/null
+++ b/lib/gitlab/background_migration/logger.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Logger that can be used for migrations logging
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'migrations'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index 0698136166a..e9c8864123f 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def satisfied_by?(pipeline, seed)
- variables = seed.to_resource.scoped_variables_hash
+ variables = seed.scoped_variables_hash
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
new file mode 100644
index 00000000000..89623a809c9
--- /dev/null
+++ b/lib/gitlab/ci/build/rules.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules
+ include ::Gitlab::Utils::StrongMemoize
+
+ Result = Struct.new(:when, :start_in)
+
+ def initialize(rule_hashes, default_when = 'on_success')
+ @rule_list = Rule.fabricate_list(rule_hashes)
+ @default_when = default_when
+ end
+
+ def evaluate(pipeline, build)
+ if @rule_list.nil?
+ Result.new(@default_when)
+ elsif matched_rule = match_rule(pipeline, build)
+ Result.new(
+ matched_rule.attributes[:when] || @default_when,
+ matched_rule.attributes[:start_in]
+ )
+ else
+ Result.new('never')
+ end
+ end
+
+ private
+
+ def match_rule(pipeline, build)
+ @rule_list.find { |rule| rule.matches?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb
new file mode 100644
index 00000000000..8d52158c8d2
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule
+ attr_accessor :attributes
+
+ def self.fabricate_list(list)
+ list.map(&method(:new)) if list
+ end
+
+ def initialize(spec)
+ @clauses = []
+ @attributes = {}
+
+ spec.each do |type, value|
+ if clause = Clause.fabricate(type, value)
+ @clauses << clause
+ else
+ @attributes.merge!(type => value)
+ end
+ end
+ end
+
+ def matches?(pipeline, build)
+ @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
new file mode 100644
index 00000000000..ff0baf3348c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause
+ ##
+ # Abstract class that defines an interface of a single
+ # job rule specification.
+ #
+ # Used for job's inclusion rules configuration.
+ #
+ UnknownClauseError = Class.new(StandardError)
+
+ def self.fabricate(type, value)
+ type = type.to_s.camelize
+
+ self.const_get(type).new(value) if self.const_defined?(type)
+ end
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def satisfied_by?(pipeline, seed = nil)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
new file mode 100644
index 00000000000..81d2ee6c24c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::Changes < Rules::Rule::Clause
+ def initialize(globs)
+ @globs = Array(globs)
+ end
+
+ def satisfied_by?(pipeline, seed)
+ return true if pipeline.modified_paths.nil?
+
+ pipeline.modified_paths.any? do |path|
+ @globs.any? do |glob|
+ File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb
new file mode 100644
index 00000000000..18c3b450f95
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::If < Rules::Rule::Clause
+ def initialize(expression)
+ @expression = expression
+ end
+
+ def satisfied_by?(pipeline, seed)
+ variables = seed.scoped_variables_hash
+
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 29a52b9da17..6e11c582750 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,8 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[tags script only except type image services
+ ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze
+ ALLOWED_KEYS = %i[tags script only except rules type image services
allow_failure type stage when start_in artifacts cache
dependencies needs before_script after_script variables
environment coverage retry parallel extends].freeze
@@ -19,12 +20,19 @@ module Gitlab
REQUIRED_BY_NEEDS = %i[stage].freeze
validations do
+ validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs?
validates :config, presence: true
validates :script, presence: true
validates :name, presence: true
validates :name, type: Symbol
+ validates :config,
+ disallowed_keys: {
+ in: %i[only except when start_in],
+ message: 'key may not be used with `rules`'
+ },
+ if: :has_rules?
with_options allow_nil: true do
validates :tags, array_of_strings: true
@@ -32,17 +40,19 @@ module Gitlab
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 }
- validates :when,
- inclusion: { in: %w[on_success on_failure always manual delayed],
- message: 'should be on_success, on_failure, ' \
- 'always, manual or delayed' }
+ validates :when, inclusion: {
+ in: ALLOWED_WHEN,
+ message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
+ }
+
validates :dependencies, array_of_strings: true
validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true
+ validates :rules, array_of_hashes: true
end
validates :start_in, duration: { limit: '1 day' }, if: :delayed?
- validates :start_in, absence: true, unless: :delayed?
+ validates :start_in, absence: true, if: -> { has_rules? || !delayed? }
validate do
next unless dependencies.present?
@@ -91,6 +101,9 @@ module Gitlab
entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
+ entry :rules, Entry::Rules,
+ description: 'List of evaluable Rules to determine job inclusion.'
+
entry :variables, Entry::Variables,
description: 'Environment variables available for this job.'
@@ -112,7 +125,7 @@ module Gitlab
:parallel, :needs
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :needs, :retry, :parallel, :extends, :start_in
+ :needs, :retry, :parallel, :extends, :start_in, :rules
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -151,6 +164,10 @@ module Gitlab
self.when == 'delayed'
end
+ def has_rules?
+ @config.try(:key?, :rules)
+ end
+
def ignored?
allow_failure.nil? ? manual_action? : allow_failure
end
diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb
new file mode 100644
index 00000000000..65cad0880f5
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: Array
+ end
+
+ def compose!(deps = nil)
+ super(deps) do
+ @config.each_with_index do |rule, index|
+ @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule)
+ .value(rule)
+ .with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
new file mode 100644
index 00000000000..1f2a34ec90e
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules::Rule < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+
+ CLAUSES = %i[if changes].freeze
+ ALLOWED_KEYS = %i[if changes when start_in].freeze
+ ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
+
+ attributes :if, :changes, :when, :start_in
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: { with: Hash }
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, disallowed_keys: %i[start_in], unless: :specifies_delay?
+ validates :start_in, presence: true, if: :specifies_delay?
+ validates :start_in, duration: { limit: '1 day' }, if: :specifies_delay?
+
+ with_options allow_nil: true do
+ validates :if, expression: true
+ validates :changes, array_of_strings: true
+ validates :when, allowed_values: { in: ALLOWED_WHEN }
+ end
+ end
+
+ def specifies_delay?
+ self.when == 'delayed'
+ end
+
+ def default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index b0ce7457926..1066331062b 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -7,7 +7,7 @@ module Gitlab
class Build < Seed::Base
include Gitlab::Utils::StrongMemoize
- delegate :dig, to: :@attributes
+ delegate :dig, to: :@seed_attributes
# When the `ci_dag_limit_needs` is enabled it uses the lower limit
LOW_NEEDS_LIMIT = 5
@@ -15,14 +15,20 @@ module Gitlab
def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline
- @attributes = attributes
+ @seed_attributes = attributes
@previous_stages = previous_stages
@needs_attributes = dig(:needs_attributes)
+ @using_rules = attributes.key?(:rules)
+ @using_only = attributes.key?(:only)
+ @using_except = attributes.key?(:except)
+
@only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only))
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
+ @rules = Gitlab::Ci::Build::Rules
+ .new(attributes.delete(:rules))
end
def name
@@ -31,8 +37,13 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
- all_of_only? &&
- none_of_except?
+ if @using_rules
+ included_by_rules?
+ elsif @using_only || @using_except
+ all_of_only? && none_of_except?
+ else
+ true
+ end
end
end
@@ -45,19 +56,16 @@ module Gitlab
end
def attributes
- @attributes.merge(
- pipeline: @pipeline,
- project: @pipeline.project,
- user: @pipeline.user,
- ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: @pipeline.legacy_trigger,
- protected: @pipeline.protected_ref?
- )
+ @seed_attributes
+ .deep_merge(pipeline_attributes)
+ .deep_merge(rules_attributes)
end
def bridge?
- @attributes.to_h.dig(:options, :trigger).present?
+ attributes_hash = @seed_attributes.to_h
+ attributes_hash.dig(:options, :trigger).present? ||
+ (attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) &&
+ attributes_hash.dig(:options, :bridge_needs, :pipeline).present?)
end
def to_resource
@@ -70,6 +78,18 @@ module Gitlab
end
end
+ def scoped_variables_hash
+ strong_memoize(:scoped_variables_hash) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate rules before we persist a Build
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ ::Ci::Build.new(
+ @seed_attributes.merge(pipeline_attributes)
+ ).scoped_variables_hash
+ end
+ end
+
private
def all_of_only?
@@ -106,6 +126,28 @@ module Gitlab
HARD_NEEDS_LIMIT
end
end
+
+ def pipeline_attributes
+ {
+ pipeline: @pipeline,
+ project: @pipeline.project,
+ user: @pipeline.user,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: @pipeline.legacy_trigger,
+ protected: @pipeline.protected_ref?
+ }
+ end
+
+ def included_by_rules?
+ rules_attributes[:when] != 'never'
+ end
+
+ def rules_attributes
+ strong_memoize(:rules_attributes) do
+ @using_rules ? @rules.evaluate(@pipeline, self).to_h.compact : {}
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 4190de73e1f..90278122361 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -46,11 +46,14 @@ sast:
SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
SAST_RUN_ANALYZER_TIMEOUT \
+ SAST_JAVA_VERSION \
ANT_HOME \
ANT_PATH \
GRADLE_PATH \
JAVA_OPTS \
JAVA_PATH \
+ JAVA_8_VERSION \
+ JAVA_11_VERSION \
MAVEN_CLI_OPTS \
MAVEN_PATH \
MAVEN_REPO_PATH \
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 998130e5bd0..2e1eab270ff 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -55,7 +55,8 @@ module Gitlab
parallel: job[:parallel],
instance: job[:instance],
start_in: job[:start_in],
- trigger: job[:trigger]
+ trigger: job[:trigger],
+ bridge_needs: job[:needs]
}.compact }.compact
end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 0289e675c6b..374f929878e 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -20,8 +20,10 @@ module Gitlab
present_keys = value.try(:keys).to_a & options[:in]
if present_keys.any?
- record.errors.add(attribute, "contains disallowed keys: " +
- present_keys.join(', '))
+ message = options[:message] || "contains disallowed keys"
+ message += ": #{present_keys.join(', ')}"
+
+ record.errors.add(attribute, message)
end
end
end
@@ -65,6 +67,16 @@ module Gitlab
end
end
+ class ArrayOfHashesValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Array) && value.map { |hsh| hsh.is_a?(Hash) }.all?
+ record.errors.add(attribute, 'should be an array of hashes')
+ end
+ end
+ end
+
class ArrayOrStringValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.is_a?(Array) || value.is_a?(String)
@@ -231,6 +243,14 @@ module Gitlab
end
end
+ class ExpressionValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(String) && ::Gitlab::Ci::Pipeline::Expression::Statement.new(value).valid?
+ record.errors.add(attribute, 'Invalid expression syntax')
+ end
+ end
+ end
+
class PortNamePresentAndUniqueValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless value.is_a?(Array)
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 37fadb47736..75d9a2d55b9 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -129,8 +129,6 @@ module Gitlab
SAMPLE_DATA
end
- private
-
def checkout_sha(repository, newrev, ref)
# Checkout sha is nil when we remove branch or tag
return if Gitlab::Git.blank_ref?(newrev)
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index 24d752b8a4b..2a8bcd015a8 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -39,6 +39,17 @@ module Gitlab
end
end
+ def includes_default_branch?
+ # If the branch doesn't have a default branch yet, we presume the
+ # first branch pushed will be the default.
+ return true unless project.default_branch.present?
+
+ enum_for(:changes_refs).any? do |_oldrev, _newrev, ref|
+ Gitlab::Git.branch_ref?(ref) &&
+ Gitlab::Git.branch_name(ref) == project.default_branch
+ end
+ end
+
private
def deserialize_changes(changes)
diff --git a/lib/gitlab/grape_logging/loggers/client_env_logger.rb b/lib/gitlab/grape_logging/loggers/client_env_logger.rb
new file mode 100644
index 00000000000..3acc6f6a2ef
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/client_env_logger.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This is a fork of
+# https://github.com/aserafin/grape_logging/blob/master/lib/grape_logging/loggers/client_env.rb
+# to use remote_ip instead of ip.
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class ClientEnvLogger < ::GrapeLogging::Loggers::Base
+ def parameters(request, _)
+ { remote_ip: request.env["HTTP_X_FORWARDED_FOR"] || request.env["REMOTE_ADDR"], ua: request.env["HTTP_USER_AGENT"] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb
index e40c366ed02..75503ee1789 100644
--- a/lib/gitlab/repository_cache_adapter.rb
+++ b/lib/gitlab/repository_cache_adapter.rb
@@ -23,6 +23,37 @@ module Gitlab
end
end
+ # Caches and strongly memoizes the method as a Redis Set.
+ #
+ # This only works for methods that do not take any arguments. The method
+ # should return an Array of Strings to be cached.
+ #
+ # In addition to overriding the named method, a "name_include?" method is
+ # defined. This uses the "SISMEMBER" query to efficiently check membership
+ # without needing to load the entire set into memory.
+ #
+ # name - The name of the method to be cached.
+ # fallback - A value to fall back to if the repository does not exist, or
+ # in case of a Git error. Defaults to nil.
+ def cache_method_as_redis_set(name, fallback: nil)
+ uncached_name = alias_uncached_method(name)
+
+ define_method(name) do
+ cache_method_output_as_redis_set(name, fallback: fallback) do
+ __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ define_method("#{name}_include?") do |value|
+ # If the cache isn't populated, we can't rely on it
+ return redis_set_cache.include?(name, value) if redis_set_cache.exist?(name)
+
+ # Since we have to pull all branch names to populate the cache, use
+ # the data we already have to answer the query just this once
+ __send__(name).include?(value) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
# Caches truthy values from the method. All values are strongly memoized,
# and cached in RequestStore.
#
@@ -84,6 +115,11 @@ module Gitlab
raise NotImplementedError
end
+ # RepositorySetCache to be used. Should be overridden by the including class
+ def redis_set_cache
+ raise NotImplementedError
+ end
+
# List of cached methods. Should be overridden by the including class
def cached_methods
raise NotImplementedError
@@ -100,6 +136,18 @@ module Gitlab
end
end
+ # Caches and strongly memoizes the supplied block as a Redis Set. The result
+ # will be provided as a sorted array.
+ #
+ # name - The name of the method to be cached.
+ # fallback - A value to fall back to if the repository does not exist, or
+ # in case of a Git error. Defaults to nil.
+ def cache_method_output_as_redis_set(name, fallback: nil, &block)
+ memoize_method_output(name, fallback: fallback) do
+ redis_set_cache.fetch(name, &block).sort
+ end
+ end
+
# Caches truthy values from the supplied block. All values are strongly
# memoized, and cached in RequestStore.
#
@@ -154,6 +202,7 @@ module Gitlab
clear_memoization(memoizable_name(name))
end
+ expire_redis_set_method_caches(methods)
expire_request_store_method_caches(methods)
end
@@ -169,6 +218,10 @@ module Gitlab
end
end
+ def expire_redis_set_method_caches(methods)
+ methods.each { |name| redis_set_cache.expire(name) }
+ end
+
# All cached repository methods depend on the existence of a Git repository,
# so if the repository doesn't exist, we already know not to call it.
def fallback_early?(method_name)
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
new file mode 100644
index 00000000000..fb634328a95
--- /dev/null
+++ b/lib/gitlab/repository_set_cache.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# Interface to the Redis-backed cache store for keys that use a Redis set
+module Gitlab
+ class RepositorySetCache
+ attr_reader :repository, :namespace, :expires_in
+
+ def initialize(repository, extra_namespace: nil, expires_in: 2.weeks)
+ @repository = repository
+ @namespace = "#{repository.full_path}:#{repository.project.id}"
+ @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace
+ @expires_in = expires_in
+ end
+
+ def cache_key(type)
+ [type, namespace, 'set'].join(':')
+ end
+
+ def expire(key)
+ with { |redis| redis.del(cache_key(key)) }
+ end
+
+ def exist?(key)
+ with { |redis| redis.exists(cache_key(key)) }
+ end
+
+ def read(key)
+ with { |redis| redis.smembers(cache_key(key)) }
+ end
+
+ def write(key, value)
+ full_key = cache_key(key)
+
+ with do |redis|
+ redis.multi do
+ redis.del(full_key)
+
+ # Splitting into groups of 1000 prevents us from creating a too-long
+ # Redis command
+ value.in_groups_of(1000, false) { |subset| redis.sadd(full_key, subset) }
+
+ redis.expire(full_key, expires_in)
+ end
+ end
+
+ value
+ end
+
+ def fetch(key, &block)
+ if exist?(key)
+ read(key)
+ else
+ write(key, yield)
+ end
+ end
+
+ def include?(key, value)
+ with { |redis| redis.sismember(cache_key(key), value) }
+ end
+
+ private
+
+ def with(&blk)
+ Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 764db14d720..005cb3112b8 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -39,9 +39,14 @@ module Gitlab
# development and test. If you need development and test to behave
# just the same as production you can use this instead of
# track_exception.
+ #
+ # If the exception implements the method `sentry_extra_data` and that method
+ # returns a Hash, then the return value of that method will be merged into
+ # `extra`. Exceptions can use this mechanism to provide structured data
+ # to sentry in addition to their message and back-trace.
def self.track_acceptable_exception(exception, issue_url: nil, extra: {})
if enabled?
- extra[:issue_url] = issue_url if issue_url
+ extra = build_extra_data(exception, issue_url, extra)
context # Make sure we've set everything we know in the context
Raven.capture_exception(exception, tags: default_tags, extra: extra)
@@ -58,5 +63,15 @@ module Gitlab
locale: I18n.locale
}
end
+
+ def self.build_extra_data(exception, issue_url, extra)
+ exception.try(:sentry_extra_data)&.tap do |data|
+ extra.merge!(data) if data.is_a?(Hash)
+ end
+
+ extra.merge({ issue_url: issue_url }.compact)
+ end
+
+ private_class_method :build_extra_data
end
end
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index b06ffa9c121..3dc9521ee8b 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -3,6 +3,10 @@
module Gitlab
module SidekiqMiddleware
class Metrics
+ # SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq
+ # timeframes than the DEFAULT_BUCKET definition. Defined in seconds.
+ SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze
+
def initialize
@metrics = init_metrics
end
@@ -31,7 +35,7 @@ module Gitlab
def init_metrics
{
- sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job'),
+ sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', buckets: SIDEKIQ_LATENCY_BUCKETS),
sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :livesum)
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 038553c5dd7..353298e67b3 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -100,9 +100,7 @@ module Gitlab
.merge(services_usage)
.merge(approximate_counts)
}.tap do |data|
- if Feature.enabled?(:group_overview_security_dashboard)
- data[:counts][:user_preferences] = user_preferences_usage
- end
+ data[:counts][:user_preferences] = user_preferences_usage
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -190,8 +188,8 @@ module Gitlab
{} # augmented in EE
end
- def count(relation, fallback: -1)
- relation.count
+ def count(relation, count_by: nil, fallback: -1)
+ count_by ? relation.count(count_by) : relation.count
rescue ActiveRecord::StatementInvalid
fallback
end
diff --git a/lib/gitlab/usage_data_counters/note_counter.rb b/lib/gitlab/usage_data_counters/note_counter.rb
index e93a0bcfa27..672450ec82b 100644
--- a/lib/gitlab/usage_data_counters/note_counter.rb
+++ b/lib/gitlab/usage_data_counters/note_counter.rb
@@ -4,7 +4,7 @@ module Gitlab::UsageDataCounters
class NoteCounter < BaseCounter
KNOWN_EVENTS = %w[create].freeze
PREFIX = 'note'
- COUNTABLE_TYPES = %w[Snippet].freeze
+ COUNTABLE_TYPES = %w[Snippet Commit MergeRequest].freeze
class << self
def redis_key(event, noteable_type)
@@ -24,9 +24,9 @@ module Gitlab::UsageDataCounters
end
def totals
- {
- snippet_comment: read(:create, 'Snippet')
- }
+ COUNTABLE_TYPES.map do |countable_type|
+ [:"#{countable_type.underscore}_comment", read(:create, countable_type)]
+ end.to_h
end
private
diff --git a/lib/gt_one_coercion.rb b/lib/gt_one_coercion.rb
deleted file mode 100644
index 99be51bc8c6..00000000000
--- a/lib/gt_one_coercion.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class GtOneCoercion < Virtus::Attribute
- def coerce(value)
- [1, value.to_i].max
- end
-end
diff --git a/lib/prometheus/cleanup_multiproc_dir_service.rb b/lib/prometheus/cleanup_multiproc_dir_service.rb
new file mode 100644
index 00000000000..6418b4de166
--- /dev/null
+++ b/lib/prometheus/cleanup_multiproc_dir_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Prometheus
+ class CleanupMultiprocDirService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ FileUtils.rm_rf(old_metrics) if old_metrics
+ end
+
+ private
+
+ def old_metrics
+ strong_memoize(:old_metrics) do
+ Dir[File.join(multiprocess_files_dir, '*.db')] if multiprocess_files_dir
+ end
+ end
+
+ def multiprocess_files_dir
+ ::Prometheus::Client.configuration.multiprocess_files_dir
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/uploads/legacy.rake b/lib/tasks/gitlab/uploads/legacy.rake
new file mode 100644
index 00000000000..18fb8afe455
--- /dev/null
+++ b/lib/tasks/gitlab/uploads/legacy.rake
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :uploads do
+ namespace :legacy do
+ desc "GitLab | Uploads | Migrate all legacy attachments"
+ task migrate: :environment do
+ class Upload < ApplicationRecord
+ self.table_name = 'uploads'
+
+ include ::EachBatch
+ end
+
+ migration = 'LegacyUploadsMigrator'.freeze
+ batch_size = 5000
+ delay_interval = 5.minutes.to_i
+
+ Upload.where(uploader: 'AttachmentUploader').each_batch(of: batch_size) do |relation, index|
+ start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+ delay = index * delay_interval
+
+ BackgroundMigrationWorker.perform_in(delay, migration, [start_id, end_id])
+ end
+ end
+ end
+ end
+end