summaryrefslogtreecommitdiff
path: root/app/models/concerns
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/concerns')
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb40
-rw-r--r--app/models/concerns/ci/contextable.rb47
-rw-r--r--app/models/concerns/ci/has_variable.rb17
-rw-r--r--app/models/concerns/cross_database_modification.rb122
-rw-r--r--app/models/concerns/has_environment_scope.rb8
-rw-r--r--app/models/concerns/issuable.rb14
-rw-r--r--app/models/concerns/mirror_authentication.rb9
-rw-r--r--app/models/concerns/noteable.rb6
-rw-r--r--app/models/concerns/packages/debian/distribution.rb8
-rw-r--r--app/models/concerns/resolvable_discussion.rb10
-rw-r--r--app/models/concerns/taskable.rb15
-rw-r--r--app/models/concerns/timebox.rb13
-rw-r--r--app/models/concerns/token_authenticatable.rb12
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb36
14 files changed, 269 insertions, 88 deletions
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index 324e0fb57cb..7cc4bc569d3 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
module Analytics
module CycleAnalytics
module StageEventModel
@@ -16,12 +15,39 @@ module Analytics
scope :authored, ->(user) { where(author_id: user) }
scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) }
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
+ scope :order_by_end_event, -> (direction) do
+ # ORDER BY end_event_timestamp, merge_request_id/issue_id, start_event_timestamp
+ # start_event_timestamp must be included in the ORDER BY clause for the duration
+ # calculation to work: SELECT end_event_timestamp - start_event_timestamp
+ keyset_order(
+ :end_event_timestamp => { order_expression: arel_order(arel_table[:end_event_timestamp], direction), distinct: false },
+ issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true },
+ :start_event_timestamp => { order_expression: arel_order(arel_table[:start_event_timestamp], direction), distinct: false }
+ )
+ end
+ scope :order_by_duration, -> (direction) do
+ # ORDER BY EXTRACT('epoch', end_event_timestamp - start_event_timestamp)
+ duration = Arel::Nodes::Subtraction.new(
+ arel_table[:end_event_timestamp],
+ arel_table[:start_event_timestamp]
+ )
+ duration_in_seconds = Arel::Nodes::Extract.new(duration, :epoch)
+
+ keyset_order(
+ :total_time => { order_expression: arel_order(duration_in_seconds, direction), distinct: false, sql_type: 'double precision' },
+ issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true }
+ )
+ end
end
def issuable_id
attributes[self.class.issuable_id_column.to_s]
end
+ def total_time
+ read_attribute(:total_time) || (end_event_timestamp - start_event_timestamp).to_f
+ end
+
class_methods do
def upsert_data(data)
upsert_values = data.map do |row|
@@ -68,6 +94,18 @@ module Analytics
result = connection.execute(query)
result.cmd_tuples
end
+
+ def keyset_order(column_definition_options)
+ built_definitions = column_definition_options.map do |attribute_name, column_options|
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: attribute_name, **column_options)
+ end
+
+ order(Gitlab::Pagination::Keyset::Order.build(built_definitions))
+ end
+
+ def arel_order(arel_node, direction)
+ direction.to_sym == :desc ? arel_node.desc : arel_node.asc
+ end
end
end
end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index ed3b422251f..88b7bb89b89 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -11,26 +11,9 @@ module Ci
#
def scoped_variables(environment: expanded_environment_name, dependencies: true)
track_duration do
- variables = pipeline.variables_builder.scoped_variables(self, environment: environment, dependencies: dependencies)
-
- next variables if pipeline.use_variables_builder_definitions?
-
- variables.concat(project.predefined_variables)
- variables.concat(pipeline.predefined_variables)
- variables.concat(runner.predefined_variables) if runnable? && runner
- variables.concat(kubernetes_variables)
- variables.concat(deployment_variables(environment: environment))
- variables.concat(yaml_variables)
- variables.concat(user_variables)
- variables.concat(dependency_variables) if dependencies
- variables.concat(secret_instance_variables)
- variables.concat(secret_group_variables(environment: environment))
- variables.concat(secret_project_variables(environment: environment))
- variables.concat(trigger_request.user_variables) if trigger_request
- variables.concat(pipeline.variables)
- variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
-
- variables
+ pipeline
+ .variables_builder
+ .scoped_variables(self, environment: environment, dependencies: dependencies)
end
end
@@ -60,29 +43,5 @@ module Ci
scoped_variables(environment: nil, dependencies: false)
end
end
-
- def user_variables
- pipeline.variables_builder.user_variables(user)
- end
-
- def kubernetes_variables
- pipeline.variables_builder.kubernetes_variables(self)
- end
-
- def deployment_variables(environment:)
- pipeline.variables_builder.deployment_variables(job: self, environment: environment)
- end
-
- def secret_instance_variables
- pipeline.variables_builder.secret_instance_variables(ref: git_ref)
- end
-
- def secret_group_variables(environment: expanded_environment_name)
- pipeline.variables_builder.secret_group_variables(environment: environment, ref: git_ref)
- end
-
- def secret_project_variables(environment: expanded_environment_name)
- pipeline.variables_builder.secret_project_variables(environment: environment, ref: git_ref)
- end
end
end
diff --git a/app/models/concerns/ci/has_variable.rb b/app/models/concerns/ci/has_variable.rb
index 7309469c77e..3b437fbba16 100644
--- a/app/models/concerns/ci/has_variable.rb
+++ b/app/models/concerns/ci/has_variable.rb
@@ -31,7 +31,24 @@ module Ci
end
def to_runner_variable
+ var_cache_key = to_runner_variable_cache_key
+
+ return uncached_runner_variable unless var_cache_key
+
+ ::Gitlab::SafeRequestStore.fetch(var_cache_key) { uncached_runner_variable }
+ end
+
+ private
+
+ def uncached_runner_variable
{ key: key, value: value, public: false, file: file? }
end
+
+ def to_runner_variable_cache_key
+ return unless persisted?
+
+ variable_id = read_attribute(self.class.primary_key)
+ "#{self.class}#to_runner_variable:#{variable_id}:#{key}"
+ end
end
end
diff --git a/app/models/concerns/cross_database_modification.rb b/app/models/concerns/cross_database_modification.rb
new file mode 100644
index 00000000000..85645e482f6
--- /dev/null
+++ b/app/models/concerns/cross_database_modification.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+module CrossDatabaseModification
+ extend ActiveSupport::Concern
+
+ class TransactionStackTrackRecord
+ DEBUG_STACK = Rails.env.test? && ENV['DEBUG_GITLAB_TRANSACTION_STACK']
+ LOG_FILENAME = Rails.root.join("log", "gitlab_transaction_stack.log")
+
+ EXCLUDE_DEBUG_TRACE = %w[
+ lib/gitlab/database/query_analyzer
+ app/models/concerns/cross_database_modification.rb
+ ].freeze
+
+ def self.logger
+ @logger ||= Logger.new(LOG_FILENAME, formatter: ->(_, _, _, msg) { Gitlab::Json.dump(msg) + "\n" })
+ end
+
+ def self.log_gitlab_transactions_stack(action: nil, example: nil)
+ return unless DEBUG_STACK
+
+ message = "gitlab_transactions_stack performing #{action}"
+ message += " in example #{example}" if example
+
+ cleaned_backtrace = Gitlab::BacktraceCleaner.clean_backtrace(caller)
+ .reject { |line| EXCLUDE_DEBUG_TRACE.any? { |exclusion| line.include?(exclusion) } }
+ .first(5)
+
+ logger.warn({
+ message: message,
+ action: action,
+ gitlab_transactions_stack: ::ApplicationRecord.gitlab_transactions_stack,
+ caller: cleaned_backtrace,
+ thread: Thread.current.object_id
+ })
+ end
+
+ def initialize(subject, gitlab_schema)
+ @subject = subject
+ @gitlab_schema = gitlab_schema
+ @subject.gitlab_transactions_stack.push(gitlab_schema)
+
+ self.class.log_gitlab_transactions_stack(action: :after_push)
+ end
+
+ def done!
+ unless @done
+ @done = true
+
+ self.class.log_gitlab_transactions_stack(action: :before_pop)
+ @subject.gitlab_transactions_stack.pop
+ end
+
+ true
+ end
+
+ def trigger_transactional_callbacks?
+ false
+ end
+
+ def before_committed!
+ end
+
+ def rolledback!(force_restore_state: false, should_run_callbacks: true)
+ done!
+ end
+
+ def committed!(should_run_callbacks: true)
+ done!
+ end
+ end
+
+ included do
+ private_class_method :gitlab_schema
+ end
+
+ class_methods do
+ def gitlab_transactions_stack
+ Thread.current[:gitlab_transactions_stack] ||= []
+ end
+
+ def transaction(**options, &block)
+ if track_gitlab_schema_in_current_transaction?
+ super(**options) do
+ # Hook into current transaction to ensure that once
+ # the `COMMIT` is executed the `gitlab_transactions_stack`
+ # will be allowing to execute `after_commit_queue`
+ record = TransactionStackTrackRecord.new(self, gitlab_schema)
+
+ begin
+ connection.current_transaction.add_record(record)
+
+ yield
+ ensure
+ record.done!
+ end
+ end
+ else
+ super(**options, &block)
+ end
+ end
+
+ def track_gitlab_schema_in_current_transaction?
+ return false unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:track_gitlab_schema_in_current_transaction, default_enabled: :yaml)
+ rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
+ false
+ end
+
+ def gitlab_schema
+ case self.name
+ when 'ActiveRecord::Base', 'ApplicationRecord'
+ :gitlab_main
+ when 'Ci::ApplicationRecord'
+ :gitlab_ci
+ else
+ Gitlab::Database::GitlabSchema.table_schema(table_name) if table_name
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/has_environment_scope.rb b/app/models/concerns/has_environment_scope.rb
index 9553abe4dd3..c01996c0c4c 100644
--- a/app/models/concerns/has_environment_scope.rb
+++ b/app/models/concerns/has_environment_scope.rb
@@ -70,6 +70,14 @@ module HasEnvironmentScope
relation
end
+
+ scope :for_environment, ->(environment) do
+ if environment
+ on_environment(environment)
+ else
+ where(environment_scope: '*')
+ end
+ end
end
def environment_scope=(new_environment_scope)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index dcd80201d3f..0138c0ad20f 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -194,6 +194,8 @@ module Issuable
end
def supports_escalation?
+ return false unless ::Feature.enabled?(:incident_escalations, project)
+
incident?
end
@@ -363,9 +365,10 @@ module Issuable
end
# Includes table keys in group by clause when sorting
- # preventing errors in postgres
+ # preventing errors in Postgres
+ #
+ # Returns an array of Arel columns
#
- # Returns an array of arel columns
def grouping_columns(sort)
sort = sort.to_s
grouping_columns = [arel_table[:id]]
@@ -384,9 +387,10 @@ module Issuable
end
# Includes all table keys in group by clause when sorting
- # preventing errors in postgres when using CTE search optimisation
+ # preventing errors in Postgres when using CTE search optimization
+ #
+ # Returns an array of Arel columns
#
- # Returns an array of arel columns
def issue_grouping_columns(use_cte: false)
if use_cte
attribute_names.map { |attr| arel_table[attr.to_sym] }
@@ -576,7 +580,7 @@ module Issuable
##
# Overridden in MergeRequest
#
- def wipless_title_changed(old_title)
+ def draftless_title_changed(old_title)
old_title != title
end
end
diff --git a/app/models/concerns/mirror_authentication.rb b/app/models/concerns/mirror_authentication.rb
index 4dbf4dcec77..14c8be93ce0 100644
--- a/app/models/concerns/mirror_authentication.rb
+++ b/app/models/concerns/mirror_authentication.rb
@@ -4,11 +4,6 @@
# implements support for persisting the necessary data in a `credentials`
# serialized attribute. It also needs an `url` method to be defined
module MirrorAuthentication
- SSH_PRIVATE_KEY_OPTS = {
- type: 'RSA',
- bits: 4096
- }.freeze
-
extend ActiveSupport::Concern
included do
@@ -84,10 +79,10 @@ module MirrorAuthentication
return if ssh_private_key.blank?
comment = "git@#{::Gitlab.config.gitlab.host}"
- ::SSHKey.new(ssh_private_key, comment: comment).ssh_public_key
+ SSHData::PrivateKey.parse(ssh_private_key).first.public_key.openssh(comment: comment)
end
def generate_ssh_private_key!
- self.ssh_private_key = ::SSHKey.generate(SSH_PRIVATE_KEY_OPTS).private_key
+ self.ssh_private_key = SSHData::PrivateKey::RSA.generate(4096).openssl.to_pem
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index ea4fe5b27dc..c1aac235d33 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -176,7 +176,7 @@ module Noteable
Gitlab::Routing.url_helpers.project_noteable_notes_path(
project,
- target_type: self.class.name.underscore,
+ target_type: noteable_target_type_name,
target_id: id
)
end
@@ -201,6 +201,10 @@ module Noteable
project_email.sub('@', "-#{iid}@")
end
+ def noteable_target_type_name
+ model_name.singular
+ end
+
private
# Synthetic system notes don't have discussion IDs because these are generated dynamically
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 2d46889ce6a..1520ec0828e 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -97,12 +97,8 @@ module Packages
end
def package_files
- if Feature.enabled?(:packages_installable_package_files, default_enabled: :yaml)
- ::Packages::PackageFile.installable
- .for_package_ids(packages.select(:id))
- else
- ::Packages::PackageFile.for_package_ids(packages.select(:id))
- end
+ ::Packages::PackageFile.installable
+ .for_package_ids(packages.select(:id))
end
private
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index aae338e9759..92a88d2f7c8 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -99,6 +99,12 @@ module ResolvableDiscussion
update { |notes| notes.unresolve! }
end
+ def clear_memoized_values
+ self.class.memoized_values.each do |name|
+ clear_memoization(name)
+ end
+ end
+
private
def update
@@ -110,8 +116,6 @@ module ResolvableDiscussion
# Set the notes array to the updated notes
@notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
- self.class.memoized_values.each do |name|
- clear_memoization(name)
- end
+ clear_memoized_values
end
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 4d1c1d44af7..e41a0ca28f9 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -15,17 +15,16 @@ module Taskable
INCOMPLETE_PATTERN = /(\[\s\])/.freeze
ITEM_PATTERN = %r{
^
- (?:(?:>\s{0,4})*) # optional blockquote characters
- (?:\s*(?:[-+*]|(?:\d+\.)))+ # list prefix (one or more) required - task item has to be always in a list
- \s+ # whitespace prefix has to be always presented for a list item
- (\[\s\]|\[[xX]\]) # checkbox
- (\s.+) # followed by whitespace and some text.
+ (?:(?:>\s{0,4})*) # optional blockquote characters
+ ((?:\s*(?:[-+*]|(?:\d+\.)))+) # list prefix (one or more) required - task item has to be always in a list
+ \s+ # whitespace prefix has to be always presented for a list item
+ (\[\s\]|\[[xX]\]) # checkbox
+ (\s.+) # followed by whitespace and some text.
}x.freeze
def self.get_tasks(content)
- content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
- # ITEM_PATTERN strips out the hyphen, but Item requires it. Rabble rabble.
- TaskList::Item.new("- #{checkbox}", label.strip)
+ content.to_s.scan(ITEM_PATTERN).map do |prefix, checkbox, label|
+ TaskList::Item.new("#{prefix} #{checkbox}", label.strip)
end
end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 3fe9d7f4d71..943ef3fa59f 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -51,7 +51,7 @@ module Timebox
validate :dates_within_4_digits
cache_markdown_field :title, pipeline: :single_line
- cache_markdown_field :description
+ cache_markdown_field :description, issuable_reference_expansion_enabled: true
belongs_to :project
belongs_to :group
@@ -125,17 +125,6 @@ module Timebox
fuzzy_search(query, [:title, :description])
end
- # Searches for timeboxes with a matching title.
- #
- # This method uses ILIKE on PostgreSQL
- #
- # query - The search query as a String
- #
- # Returns an ActiveRecord::Relation.
- def search_title(query)
- fuzzy_search(query, [:title])
- end
-
def filter_by_state(timeboxes, state)
case state
when 'closed' then timeboxes.closed
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 34c8630bb90..f44ad8ebe90 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -64,6 +64,18 @@ module TokenAuthenticatable
mod.define_method("format_#{token_field}") do |token|
token
end
+
+ mod.define_method("#{token_field}_expires_at") do
+ strategy.expires_at(self)
+ end
+
+ mod.define_method("#{token_field}_expired?") do
+ strategy.expired?(self)
+ end
+
+ mod.define_method("#{token_field}_with_expiration") do
+ strategy.token_with_expiration(self)
+ end
end
def token_authenticatable_module
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index f72a41f06b1..2cec4ab460e 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -7,6 +7,7 @@ module TokenAuthenticatableStrategies
def initialize(klass, token_field, options)
@klass = klass
@token_field = token_field
+ @expires_at_field = "#{token_field}_expires_at"
@options = options
end
@@ -44,6 +45,25 @@ module TokenAuthenticatableStrategies
instance.save! if Gitlab::Database.read_write?
end
+ def expires_at(instance)
+ instance.read_attribute(@expires_at_field)
+ end
+
+ def expired?(instance)
+ return false unless expirable? && token_expiration_enforced?
+
+ exp = expires_at(instance)
+ !!exp && Time.current > exp
+ end
+
+ def expirable?
+ !!@options[:expires_at]
+ end
+
+ def token_with_expiration(instance)
+ API::Support::TokenWithExpiration.new(self, instance)
+ end
+
def self.fabricate(model, field, options)
if options[:digest] && options[:encrypted]
raise ArgumentError, _('Incompatible options set!')
@@ -64,6 +84,10 @@ module TokenAuthenticatableStrategies
new_token = generate_available_token
formatted_token = format_token(instance, new_token)
set_token(instance, formatted_token)
+
+ if expirable?
+ instance[@expires_at_field] = @options[:expires_at].to_proc.call(instance)
+ end
end
def unique
@@ -82,11 +106,21 @@ module TokenAuthenticatableStrategies
end
def relation(unscoped)
- unscoped ? @klass.unscoped : @klass
+ unscoped ? @klass.unscoped : @klass.where(not_expired)
end
def token_set?(instance)
raise NotImplementedError
end
+
+ def token_expiration_enforced?
+ return true unless @options[:expiration_enforced?]
+
+ @options[:expiration_enforced?].to_proc.call(@klass)
+ end
+
+ def not_expired
+ Arel.sql("#{@expires_at_field} IS NULL OR #{@expires_at_field} >= NOW()") if expirable? && token_expiration_enforced?
+ end
end
end