Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-01-20 12:16:11 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-20 12:16:11 +0300
commitedaa33dee2ff2f7ea3fac488d41558eb5f86d68c (patch)
tree11f143effbfeba52329fb7afbd05e6e2a3790241 /lib
parentd8a5691316400a0f7ec4f83832698f1988eb27c1 (diff)
Add latest changes from gitlab-org/gitlab@14-7-stable-eev14.7.0-rc42
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/ci/helpers/runner.rb8
-rw-r--r--lib/api/ci/job_artifacts.rb11
-rw-r--r--lib/api/ci/runner.rb20
-rw-r--r--lib/api/ci/runners.rb7
-rw-r--r--lib/api/ci/triggers.rb2
-rw-r--r--lib/api/debian_project_packages.rb1
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities/group_detail.rb2
-rw-r--r--lib/api/entities/issue_basic.rb2
-rw-r--r--lib/api/entities/merge_request_basic.rb4
-rw-r--r--lib/api/entities/project.rb3
-rw-r--r--lib/api/entities/project_with_access.rb2
-rw-r--r--lib/api/entities/resource_access_token.rb2
-rw-r--r--lib/api/helpers/integrations_helpers.rb16
-rw-r--r--lib/api/helpers/members_helpers.rb2
-rw-r--r--lib/api/helpers/projects_helpers.rb2
-rw-r--r--lib/api/helpers/rate_limiter.rb5
-rw-r--r--lib/api/integrations.rb9
-rw-r--r--lib/api/internal/base.rb4
-rw-r--r--lib/api/internal/kubernetes.rb2
-rw-r--r--lib/api/internal/mail_room.rb51
-rw-r--r--lib/api/issues.rb4
-rw-r--r--lib/api/package_files.rb19
-rw-r--r--lib/api/project_container_repositories.rb1
-rw-r--r--lib/api/projects.rb1
-rw-r--r--lib/api/resource_access_tokens.rb10
-rw-r--r--lib/api/rubygem_packages.rb9
-rw-r--r--lib/api/search.rb14
-rw-r--r--lib/api/terraform/modules/v1/packages.rb6
-rw-r--r--lib/api/users.rb8
-rw-r--r--lib/api/v3/github.rb8
-rw-r--r--lib/backup.rb13
-rw-r--r--lib/backup/database.rb2
-rw-r--r--lib/backup/files.rb8
-rw-r--r--lib/backup/gitaly_backup.rb47
-rw-r--r--lib/backup/gitaly_rpc_backup.rb2
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/backup/packages.rb13
-rw-r--r--lib/backup/repositories.rb4
-rw-r--r--lib/backup/terraform_state.rb13
-rw-r--r--lib/banzai/filter/base_sanitization_filter.rb2
-rw-r--r--lib/banzai/filter/footnote_filter.rb62
-rw-r--r--lib/banzai/filter/markdown_engines/common_mark.rb36
-rw-r--r--lib/banzai/filter/markdown_post_escape_filter.rb18
-rw-r--r--lib/banzai/filter/plantuml_filter.rb7
-rw-r--r--lib/banzai/filter/references/abstract_reference_filter.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb19
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb14
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb2
-rw-r--r--lib/banzai/renderer/common_mark/html.rb21
-rw-r--r--lib/bulk_imports/common/extractors/ndjson_extractor.rb34
-rw-r--r--lib/bulk_imports/common/pipelines/uploads_pipeline.rb14
-rw-r--r--lib/bulk_imports/ndjson_pipeline.rb2
-rw-r--r--lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb31
-rw-r--r--lib/feature.rb9
-rw-r--r--lib/gitlab.rb50
-rw-r--r--lib/gitlab/anonymous_session.rb8
-rw-r--r--lib/gitlab/application_rate_limiter.rb10
-rw-r--r--lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb6
-rw-r--r--lib/gitlab/auth.rb12
-rw-r--r--lib/gitlab/auth/auth_finders.rb2
-rw-r--r--lib/gitlab/auth/ldap/config.rb3
-rw-r--r--lib/gitlab/auth/o_auth/user.rb4
-rw-r--r--lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb77
-rw-r--r--lib/gitlab/background_migration/backfill_ci_project_mirrors.rb52
-rw-r--r--lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb32
-rw-r--r--lib/gitlab/background_migration/base_job.rb23
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_rename.rb14
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb56
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_type_change.rb14
-rw-r--r--lib/gitlab/background_migration/copy_column.rb41
-rw-r--r--lib/gitlab/background_migration/encrypt_static_object_token.rb70
-rw-r--r--lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb124
-rw-r--r--lib/gitlab/background_migration/job_coordinator.rb14
-rw-r--r--lib/gitlab/background_migration/migrate_legacy_artifacts.rb130
-rw-r--r--lib/gitlab/background_migration/populate_test_reports_issue_id.rb14
-rw-r--r--lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb148
-rw-r--r--lib/gitlab/background_migration/remove_duplicate_services.rb58
-rw-r--r--lib/gitlab/checks/changes_access.rb35
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb5
-rw-r--r--lib/gitlab/ci/build/status/reason.rb37
-rw-r--r--lib/gitlab/ci/config.rb2
-rw-r--r--lib/gitlab/ci/config/entry/root.rb8
-rw-r--r--lib/gitlab/ci/jwt_v2.rb17
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb48
-rw-r--r--lib/gitlab/ci/pipeline/chain/create_deployments.rb15
-rw-r--r--lib/gitlab/ci/pipeline/chain/seed.rb9
-rw-r--r--lib/gitlab/ci/pipeline/logger.rb36
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb16
-rw-r--r--lib/gitlab/ci/pipeline/seed/context.rb11
-rw-r--r--lib/gitlab/ci/queue/metrics.rb37
-rw-r--r--lib/gitlab/ci/status/build/factory.rb3
-rw-r--r--lib/gitlab/ci/status/build/waiting_for_approval.rb24
-rw-r--r--lib/gitlab/ci/tags/bulk_insert.rb20
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml16
-rw-r--r--lib/gitlab/ci/templates/Ruby.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml1
-rw-r--r--lib/gitlab/ci/templates/Security/DAST-On-Demand-API-Scan.gitlab-ci.yml27
-rw-r--r--lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/trace/remote_checksum.rb1
-rw-r--r--lib/gitlab/ci/trace/stream.rb5
-rw-r--r--lib/gitlab/ci/variables/builder.rb64
-rw-r--r--lib/gitlab/ci/yaml_processor.rb8
-rw-r--r--lib/gitlab/color_schemes.rb28
-rw-r--r--lib/gitlab/config/entry/configurable.rb3
-rw-r--r--lib/gitlab/config/entry/factory.rb5
-rw-r--r--lib/gitlab/config/entry/node.rb20
-rw-r--r--lib/gitlab/content_security_policy/config_loader.rb2
-rw-r--r--lib/gitlab/data_builder/archive_trace.rb19
-rw-r--r--lib/gitlab/data_builder/deployment.rb3
-rw-r--r--lib/gitlab/database/background_migration/batched_job.rb18
-rw-r--r--lib/gitlab/database/background_migration/batched_migration.rb2
-rw-r--r--lib/gitlab/database/background_migration_job.rb2
-rw-r--r--lib/gitlab/database/batch_counter.rb29
-rw-r--r--lib/gitlab/database/gitlab_loose_foreign_keys.yml93
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml12
-rw-r--r--lib/gitlab/database/grant.rb2
-rw-r--r--lib/gitlab/database/load_balancing/setup.rb4
-rw-r--r--lib/gitlab/database/loose_index_scan_distinct_count.rb102
-rw-r--r--lib/gitlab/database/migration_helpers.rb180
-rw-r--r--lib/gitlab/database/migrations/background_migration_helpers.rb70
-rw-r--r--lib/gitlab/database/partitioning/partition_manager.rb4
-rw-r--r--lib/gitlab/database/partitioning/sliding_list_strategy.rb28
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb20
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb3
-rw-r--r--lib/gitlab/database/reflection.rb29
-rw-r--r--lib/gitlab/database/reindexing.rb15
-rw-r--r--lib/gitlab/database/reindexing/coordinator.rb19
-rw-r--r--lib/gitlab/database_importers/work_items/base_type_importer.rb4
-rw-r--r--lib/gitlab/email.rb1
-rw-r--r--lib/gitlab/email/failure_handler.rb46
-rw-r--r--lib/gitlab/error_tracking/processor/sidekiq_processor.rb2
-rw-r--r--lib/gitlab/event_store.rb42
-rw-r--r--lib/gitlab/event_store/event.rb54
-rw-r--r--lib/gitlab/event_store/store.rb54
-rw-r--r--lib/gitlab/event_store/subscriber.rb36
-rw-r--r--lib/gitlab/event_store/subscription.rb37
-rw-r--r--lib/gitlab/exceptions_app.rb43
-rw-r--r--lib/gitlab/experimentation.rb8
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/gpg/commit.rb2
-rw-r--r--lib/gitlab/http.rb3
-rw-r--r--lib/gitlab/i18n.rb22
-rw-r--r--lib/gitlab/import/set_async_jid.rb2
-rw-r--r--lib/gitlab/import_export/base/relation_factory.rb2
-rw-r--r--lib/gitlab/import_export/group/relation_tree_restorer.rb10
-rw-r--r--lib/gitlab/import_export/project/import_export.yml2
-rw-r--r--lib/gitlab/jwt_authenticatable.rb36
-rw-r--r--lib/gitlab/kas.rb2
-rw-r--r--lib/gitlab/lfs/client.rb81
-rw-r--r--lib/gitlab/logger.rb6
-rw-r--r--lib/gitlab/mail_room.rb16
-rw-r--r--lib/gitlab/mail_room/authenticator.rb50
-rw-r--r--lib/gitlab/merge_requests/commit_message_generator.rb72
-rw-r--r--lib/gitlab/metrics/exporter/base_exporter.rb45
-rw-r--r--lib/gitlab/metrics/exporter/gc_request_middleware.rb19
-rw-r--r--lib/gitlab/metrics/exporter/health_checks_middleware.rb35
-rw-r--r--lib/gitlab/metrics/exporter/metrics_middleware.rb41
-rw-r--r--lib/gitlab/metrics/exporter/sidekiq_exporter.rb11
-rw-r--r--lib/gitlab/metrics/exporter/web_exporter.rb8
-rw-r--r--lib/gitlab/metrics/samplers/action_cable_sampler.rb4
-rw-r--r--lib/gitlab/metrics/samplers/base_sampler.rb14
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb4
-rw-r--r--lib/gitlab/middleware/multipart.rb4
-rw-r--r--lib/gitlab/middleware/webhook_recursion_detection.rb19
-rw-r--r--lib/gitlab/pages.rb2
-rw-r--r--lib/gitlab/pagination/keyset/column_order_definition.rb25
-rw-r--r--lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb19
-rw-r--r--lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data.rb37
-rw-r--r--lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb6
-rw-r--r--lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb4
-rw-r--r--lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb15
-rw-r--r--lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb16
-rw-r--r--lib/gitlab/pagination/keyset/sql_type_missing_error.rb19
-rw-r--r--lib/gitlab/password.rb14
-rw-r--r--lib/gitlab/redis/multi_store.rb229
-rw-r--r--lib/gitlab/redis/sessions.rb36
-rw-r--r--lib/gitlab/redis/sessions_store_helper.rb27
-rw-r--r--lib/gitlab/regex.rb26
-rw-r--r--lib/gitlab/repository_archive_rate_limiter.rb2
-rw-r--r--lib/gitlab/search/params.rb11
-rw-r--r--lib/gitlab/sherlock.rb21
-rw-r--r--lib/gitlab/sherlock/collection.rb51
-rw-r--r--lib/gitlab/sherlock/file_sample.rb33
-rw-r--r--lib/gitlab/sherlock/line_profiler.rb100
-rw-r--r--lib/gitlab/sherlock/line_sample.rb38
-rw-r--r--lib/gitlab/sherlock/location.rb28
-rw-r--r--lib/gitlab/sherlock/middleware.rb43
-rw-r--r--lib/gitlab/sherlock/query.rb112
-rw-r--r--lib/gitlab/sherlock/transaction.rb140
-rw-r--r--lib/gitlab/sidekiq_logging/json_formatter.rb1
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/monitor.rb2
-rw-r--r--lib/gitlab/sidekiq_status.rb22
-rw-r--r--lib/gitlab/sidekiq_status/client_middleware.rb4
-rw-r--r--lib/gitlab/sourcegraph.rb7
-rw-r--r--lib/gitlab/ssh_public_key.rb26
-rw-r--r--lib/gitlab/themes.rb40
-rw-r--r--lib/gitlab/tracking/standard_context.rb5
-rw-r--r--lib/gitlab/untrusted_regexp/ruby_syntax.rb16
-rw-r--r--lib/gitlab/usage_data.rb33
-rw-r--r--lib/gitlab/usage_data_counters/counter_events/package_events.yml7
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb2
-rw-r--r--lib/gitlab/usage_data_counters/known_events/ci_templates.yml8
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml10
-rw-r--r--lib/gitlab/usage_data_counters/known_events/package_events.yml32
-rw-r--r--lib/gitlab/usage_data_counters/known_events/quickactions.yml8
-rw-r--r--lib/gitlab/utils/sanitize_node_link.rb6
-rw-r--r--lib/gitlab/utils/usage_data.rb12
-rw-r--r--lib/gitlab/web_hooks.rb7
-rw-r--r--lib/gitlab/web_hooks/recursion_detection.rb94
-rw-r--r--lib/gitlab/web_hooks/recursion_detection/uuid.rb46
-rw-r--r--lib/gitlab/workhorse.rb6
-rw-r--r--lib/gitlab_edition.rb50
-rw-r--r--lib/sidebars/groups/menus/ci_cd_menu.rb4
-rw-r--r--lib/sidebars/groups/menus/settings_menu.rb14
-rw-r--r--lib/sidebars/projects/menus/infrastructure_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/issues_menu.rb6
-rw-r--r--lib/tasks/gitlab/backup.rake109
-rw-r--r--lib/tasks/gitlab/cleanup.rake4
-rw-r--r--lib/tasks/gitlab/db.rake2
-rw-r--r--lib/tasks/gitlab/docs/compile_deprecations.rake39
-rw-r--r--lib/tasks/gitlab/docs/redirect.rake2
-rw-r--r--lib/tasks/gitlab/gitaly.rake38
-rw-r--r--lib/tasks/gitlab/seed/group_seed.rake2
-rw-r--r--lib/version_check.rb8
228 files changed, 2884 insertions, 2424 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index dcecaeae558..5984879413f 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -299,6 +299,7 @@ module API
mount ::API::Internal::Lfs
mount ::API::Internal::Pages
mount ::API::Internal::Kubernetes
+ mount ::API::Internal::MailRoom
version 'v3', using: :path do
# Although the following endpoints are kept behind V3 namespace,
diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
index 72c388160b4..43ed35b99fd 100644
--- a/lib/api/ci/helpers/runner.rb
+++ b/lib/api/ci/helpers/runner.rb
@@ -11,14 +11,6 @@ module API
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'
JOB_TOKEN_PARAM = :token
- def runner_registration_token_valid?
- ActiveSupport::SecurityUtils.secure_compare(params[:token], Gitlab::CurrentSettings.runners_registration_token)
- end
-
- def runner_registrar_valid?(type)
- Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
- end
-
def authenticate_runner!
forbidden! unless current_runner
diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb
index 6431436b50d..ca76d2664f8 100644
--- a/lib/api/ci/job_artifacts.rb
+++ b/lib/api/ci/job_artifacts.rb
@@ -137,6 +137,17 @@ module API
status :no_content
end
+
+ desc 'Expire the artifacts files from a project'
+ delete ':id/artifacts' do
+ not_found! unless Feature.enabled?(:bulk_expire_project_artifacts, default_enabled: :yaml)
+
+ authorize_destroy_artifacts!
+
+ ::Ci::JobArtifacts::DeleteProjectArtifactsService.new(project: user_project).execute
+
+ accepted!
+ end
end
end
end
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 4317789f7aa..fef6a7891c2 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -15,6 +15,7 @@ module API
params do
requires :token, type: String, desc: 'Registration token'
optional :description, type: String, desc: %q(Runner's description)
+ optional :maintainer_note, type: String, desc: %q(Runner's maintainer notes)
optional :info, type: Hash, desc: %q(Runner's metadata)
optional :active, type: Boolean, desc: 'Should Runner be active'
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
@@ -25,24 +26,11 @@ module API
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
post '/', feature_category: :runner do
- attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout])
+ attributes = attributes_for_keys(%i[description maintainer_note active locked run_untagged tag_list access_level maximum_timeout])
.merge(get_runner_details_from_request)
- attributes =
- if runner_registration_token_valid?
- # Create shared runner. Requires admin access
- attributes.merge(runner_type: :instance_type)
- elsif runner_registrar_valid?('project') && @project = Project.find_by_runners_token(params[:token])
- # Create a specific runner for the project
- attributes.merge(runner_type: :project_type, projects: [@project])
- elsif runner_registrar_valid?('group') && @group = Group.find_by_runners_token(params[:token])
- # Create a specific runner for the group
- attributes.merge(runner_type: :group_type, groups: [@group])
- else
- forbidden!
- end
-
- @runner = ::Ci::Runner.create(attributes)
+ @runner = ::Ci::RegisterRunnerService.new.execute(params[:token], attributes)
+ forbidden! unless @runner
if @runner.persisted?
present @runner, with: Entities::Ci::RunnerRegistrationDetails
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index ef712c84804..f21782a698f 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -229,7 +229,12 @@ module API
use :pagination
end
get ':id/runners' do
- runners = ::Ci::Runner.belonging_to_group(user_group.id, include_ancestors: true)
+ runners = if ::Feature.enabled?(:ci_find_runners_by_ci_mirrors, user_group, default_enabled: :yaml)
+ ::Ci::Runner.belonging_to_group_and_ancestors(user_group.id)
+ else
+ ::Ci::Runner.legacy_belonging_to_group(user_group.id, include_ancestors: true)
+ end
+
runners = apply_filter(runners, params)
present paginate(runners), with: Entities::Ci::Runner
diff --git a/lib/api/ci/triggers.rb b/lib/api/ci/triggers.rb
index 6a2b16e1568..ae89b475ef8 100644
--- a/lib/api/ci/triggers.rb
+++ b/lib/api/ci/triggers.rb
@@ -5,7 +5,7 @@ module API
class Triggers < ::API::Base
include PaginationParams
- HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase
+ HTTP_GITLAB_EVENT_HEADER = "HTTP_#{::Gitlab::WebHooks::GITLAB_EVENT_HEADER}".underscore.upcase
feature_category :continuous_integration
diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb
index 497ce2f4356..5fb11db8938 100644
--- a/lib/api/debian_project_packages.rb
+++ b/lib/api/debian_project_packages.rb
@@ -83,7 +83,6 @@ module API
::Packages::Debian::ProcessChangesWorker.perform_async(package_file.id, current_user.id) # rubocop:disable CodeReuse/Worker
end
- track_package_event('push_package', :debian, user: current_user, project: authorized_user_project, namespace: authorized_user_project.namespace)
created!
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index 80a50ded522..486ff5d89bc 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -165,3 +165,5 @@ module API
end
end
end
+
+API::Deployments.prepend_mod
diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb
index 5eaccbc7154..e6872709432 100644
--- a/lib/api/entities/group_detail.rb
+++ b/lib/api/entities/group_detail.rb
@@ -4,7 +4,7 @@ module API
module Entities
class GroupDetail < Group
expose :shared_with_groups do |group, options|
- SharedGroupWithGroup.represent(group.shared_with_group_links.public_or_visible_to_user(group, options[:current_user]))
+ SharedGroupWithGroup.represent(group.shared_with_group_links_visible_to_user(options[:current_user]))
end
expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] }
expose :prevent_sharing_groups_outside_hierarchy, if: ->(group) { group.root? }
diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb
index 6125dc05a6e..20f66c026e6 100644
--- a/lib/api/entities/issue_basic.rb
+++ b/lib/api/entities/issue_basic.rb
@@ -23,7 +23,7 @@ module API
expose :issue_type,
as: :type,
format_with: :upcase,
- documentation: { type: "String", desc: "One of #{::WorkItem::Type.allowed_types_for_issues.map(&:upcase)}" }
+ documentation: { type: "String", desc: "One of #{::WorkItems::Type.allowed_types_for_issues.map(&:upcase)}" }
expose :assignee, using: ::API::Entities::UserBasic do |issue|
issue.assignees.first
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
index d5cf2f653db..55d58166590 100644
--- a/lib/api/entities/merge_request_basic.rb
+++ b/lib/api/entities/merge_request_basic.rb
@@ -3,9 +3,13 @@
module API
module Entities
class MergeRequestBasic < IssuableEntity
+ # Deprecated in favour of merge_user
expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
merge_request.metrics&.merged_by
end
+ expose :merge_user, using: Entities::UserBasic do |merge_request|
+ merge_request.metrics&.merged_by || merge_request.merge_user
+ end
expose :merged_at do |merge_request, _options|
merge_request.metrics&.merged_at
end
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index 1b9299ed17e..74097dc2883 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -82,6 +82,8 @@ module API
expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do
project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project)
end
+ expose :mr_default_target_self, if: -> (project) { project.forked? }
+
expose :import_status
expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project|
@@ -130,6 +132,7 @@ module API
Ability.allowed?(options[:current_user], :change_repository_storage, project)
}
expose :keep_latest_artifacts_available?, as: :keep_latest_artifact
+ expose :runner_token_expiration_interval
# rubocop: disable CodeReuse/ActiveRecord
def self.preload_resource(project)
diff --git a/lib/api/entities/project_with_access.rb b/lib/api/entities/project_with_access.rb
index ac89cb52e43..b541ccbadcf 100644
--- a/lib/api/entities/project_with_access.rb
+++ b/lib/api/entities/project_with_access.rb
@@ -8,7 +8,7 @@ module API
if options[:project_members]
options[:project_members].find { |member| member.source_id == project.id }
else
- project.project_member(options[:current_user])
+ project.member(options[:current_user])
end
end
diff --git a/lib/api/entities/resource_access_token.rb b/lib/api/entities/resource_access_token.rb
index a1c7b28af45..569fd16f488 100644
--- a/lib/api/entities/resource_access_token.rb
+++ b/lib/api/entities/resource_access_token.rb
@@ -4,7 +4,7 @@ module API
module Entities
class ResourceAccessToken < Entities::PersonalAccessToken
expose :access_level do |token, options|
- options[:project].project_member(token.user).access_level
+ options[:resource].member(token.user).access_level
end
end
end
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index e7fdb6645a5..3af0dd4c532 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -314,25 +314,33 @@ module API
required: false,
name: :datadog_site,
type: String,
- desc: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site'
+ desc: 'The Datadog site to send data to. To send data to the EU site, use datadoghq.eu'
},
{
required: false,
name: :api_url,
type: String,
- desc: '(Advanced) Define the full URL for your Datadog site directly'
+ desc: '(Advanced) The full URL for your Datadog site'
},
+ # TODO: uncomment this field once :datadog_integration_logs_collection is rolled out
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/346339
+ # {
+ # required: false,
+ # name: :archive_trace_events,
+ # type: Boolean,
+ # desc: 'When enabled, job logs will be collected by Datadog and shown along pipeline execution traces'
+ # },
{
required: false,
name: :datadog_service,
type: String,
- desc: 'Name of this GitLab instance that all data will be tagged with'
+ desc: 'Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments'
},
{
required: false,
name: :datadog_env,
type: String,
- desc: 'The environment tag that traces will be tagged with'
+ desc: 'For self-managed deployments, set the env tag for all the data sent to Datadog. How do I use tags?'
}
],
'discord' => [
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
index c2710be6c03..6c20993431d 100644
--- a/lib/api/helpers/members_helpers.rb
+++ b/lib/api/helpers/members_helpers.rb
@@ -50,7 +50,7 @@ module API
end
def find_all_members_for_group(group)
- GroupMembersFinder.new(group).execute
+ GroupMembersFinder.new(group, current_user).execute(include_relations: [:inherited, :direct, :shared_from_groups])
end
def present_members(members)
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index d7de8bd8b8b..00f745067e7 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -71,6 +71,7 @@ module API
optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
optional :packages_enabled, type: Boolean, desc: 'Enable project packages feature'
optional :squash_option, type: String, values: %w(never always default_on default_off), desc: 'Squash default for project. One of `never`, `always`, `default_on`, or `default_off`.'
+ optional :mr_default_target_self, Boolean, desc: 'Merge requests of this forked project targets itself by default'
end
params :optional_project_params_ee do
@@ -169,6 +170,7 @@ module API
:packages_enabled,
:service_desk_enabled,
:keep_latest_artifact,
+ :mr_default_target_self,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,
diff --git a/lib/api/helpers/rate_limiter.rb b/lib/api/helpers/rate_limiter.rb
index 7d87c74097d..0ad4f089907 100644
--- a/lib/api/helpers/rate_limiter.rb
+++ b/lib/api/helpers/rate_limiter.rb
@@ -10,6 +10,7 @@ module API
# See app/controllers/concerns/check_rate_limit.rb for Rails controllers version
module RateLimiter
def check_rate_limit!(key, scope:, **options)
+ return if bypass_header_set?
return unless rate_limiter.throttled?(key, scope: scope, **options)
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
@@ -24,6 +25,10 @@ module API
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
+
+ def bypass_header_set?
+ ::Gitlab::Throttle.bypass_header.present? && request.get_header(Gitlab::Throttle.bypass_header) == '1'
+ end
end
end
end
diff --git a/lib/api/integrations.rb b/lib/api/integrations.rb
index bab8e556a73..ff1d88e35f0 100644
--- a/lib/api/integrations.rb
+++ b/lib/api/integrations.rb
@@ -111,7 +111,14 @@ module API
integration = user_project.find_or_initialize_integration(params[:slug].underscore)
destroy_conditionally!(integration) do
- attrs = integration_attributes(integration).index_with { nil }.merge(active: false)
+ attrs = integration_attributes(integration).index_with do |attr|
+ column = integration.column_for_attribute(attr)
+ if column.is_a?(ActiveRecord::ConnectionAdapters::NullColumn)
+ nil
+ else
+ column.default
+ end
+ end.merge(active: false)
render_api_error!('400 Bad Request', 400) unless integration.update(attrs)
end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index d8e39d089e4..48157a91477 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -43,6 +43,10 @@ module API
# This is a separate method so that EE can alter its behaviour more
# easily.
+ if Feature.enabled?(:rate_limit_gitlab_shell, default_enabled: :yaml)
+ check_rate_limit!(:gitlab_shell_operation, scope: [params[:action], params[:project], actor.key_or_user])
+ end
+
# Stores some Git-specific env thread-safely
env = parse_env
Gitlab::Git::HookEnv.set(gl_repository, env) if container
diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb
index f3974236fe3..3977da4bda4 100644
--- a/lib/api/internal/kubernetes.rb
+++ b/lib/api/internal/kubernetes.rb
@@ -53,7 +53,7 @@ module API
def check_agent_token
unauthorized! unless agent_token
- agent_token.track_usage
+ Clusters::AgentTokens::TrackUsageService.new(agent_token).execute
end
end
diff --git a/lib/api/internal/mail_room.rb b/lib/api/internal/mail_room.rb
new file mode 100644
index 00000000000..6e24cf6e7c5
--- /dev/null
+++ b/lib/api/internal/mail_room.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module API
+ # This internal endpoint receives webhooks sent from the MailRoom component.
+ # This component constantly listens to configured email accounts. When it
+ # finds any incoming email or service desk email, it makes a POST request to
+ # this endpoint. The target mailbox type is indicated in the request path.
+ # The email raw content is attached to the request body.
+ #
+ # For more information, please visit https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/644
+ module Internal
+ class MailRoom < ::API::Base
+ feature_category :service_desk
+
+ before do
+ authenticate_gitlab_mailroom_request!
+ end
+
+ helpers do
+ def authenticate_gitlab_mailroom_request!
+ unauthorized! unless Gitlab::MailRoom::Authenticator.verify_api_request(headers, params[:mailbox_type])
+ end
+ end
+
+ namespace 'internal' do
+ namespace 'mail_room' do
+ params do
+ requires :mailbox_type, type: String,
+ desc: 'The destination mailbox type configuration. Must either be incoming_email or service_desk_email'
+ end
+ post "/*mailbox_type" do
+ worker = Gitlab::MailRoom.worker_for(params[:mailbox_type])
+ raw = request.body.read
+ begin
+ worker.perform_async(raw)
+ rescue Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
+ receiver = Gitlab::Email::Receiver.new(raw)
+ reason = Gitlab::Email::FailureHandler.handle(receiver, Gitlab::Email::EmailTooLarge.new)
+
+ status 400
+ break { success: false, message: reason }
+ end
+
+ status 200
+ { success: true }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 4d67cbd1272..46124a74e9d 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -82,7 +82,7 @@ module API
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :due_date, type: String, values: %w[0 overdue week month next_month_and_previous_two_weeks] << '',
desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`'
- optional :issue_type, type: String, values: WorkItem::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItem::Type.allowed_types_for_issues.join(', ')}"
+ optional :issue_type, type: String, values: WorkItems::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItems::Type.allowed_types_for_issues.join(', ')}"
use :issues_stats_params
use :pagination
@@ -99,7 +99,7 @@ module API
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
- optional :issue_type, type: String, values: WorkItem::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItem::Type.allowed_types_for_issues.join(', ')}"
+ optional :issue_type, type: String, values: WorkItems::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItems::Type.allowed_types_for_issues.join(', ')}"
use :optional_issue_params_ee
end
diff --git a/lib/api/package_files.rb b/lib/api/package_files.rb
index 79ebf18ff27..5e421da2c55 100644
--- a/lib/api/package_files.rb
+++ b/lib/api/package_files.rb
@@ -28,10 +28,15 @@ module API
package = ::Packages::PackageFinder
.new(user_project, params[:package_id]).execute
- files = package.package_files
- .preload_pipelines
+ package_files = if Feature.enabled?(:packages_installable_package_files, default_enabled: :yaml)
+ package.installable_package_files
+ else
+ package.package_files
+ end
- present paginate(files), with: ::API::Entities::PackageFile
+ package_files = package_files.preload_pipelines
+
+ present paginate(package_files), with: ::API::Entities::PackageFile
end
desc 'Remove a package file' do
@@ -50,7 +55,13 @@ module API
not_found! unless package
- package_file = package.package_files.find_by_id(params[:package_file_id])
+ package_files = if Feature.enabled?(:packages_installable_package_files, default_enabled: :yaml)
+ package.installable_package_files
+ else
+ package.package_files
+ end
+
+ package_file = package_files.find_by_id(params[:package_file_id])
not_found! unless package_file
diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb
index 82b6082c3fe..d4efca6e8f2 100644
--- a/lib/api/project_container_repositories.rb
+++ b/lib/api/project_container_repositories.rb
@@ -123,7 +123,6 @@ module API
end
delete ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do
authorize_destroy_container_image!
- validate_tag!
result = ::Projects::ContainerRepository::DeleteTagsService
.new(repository.project, current_user, tags: [declared_params[:tag_name]])
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 887c76941cf..d772079372c 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -363,6 +363,7 @@ module API
optional :name, type: String, desc: 'The name that will be assigned to the fork'
optional :description, type: String, desc: 'The description that will be assigned to the fork'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the fork'
+ optional :mr_default_target_self, Boolean, desc: 'Merge requests of this forked project targets itself by default'
end
post ':id/fork', feature_category: :source_code_management do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20759')
diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb
index f42acc6b2eb..e52f8fd9111 100644
--- a/lib/api/resource_access_tokens.rb
+++ b/lib/api/resource_access_tokens.rb
@@ -8,7 +8,7 @@ module API
feature_category :authentication_and_authorization
- %w[project].each do |source_type|
+ %w[project group].each do |source_type|
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get list of all access tokens for the specified resource' do
detail 'This feature was introduced in GitLab 13.9.'
@@ -23,8 +23,8 @@ module API
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute.preload_users
- resource.project_members.load
- present paginate(tokens), with: Entities::ResourceAccessToken, project: resource
+ resource.members.load
+ present paginate(tokens), with: Entities::ResourceAccessToken, resource: resource
end
desc 'Revoke a resource access token' do
@@ -58,7 +58,7 @@ module API
requires :id, type: String, desc: "The #{source_type} ID"
requires :name, type: String, desc: "Resource access token name"
requires :scopes, type: Array[String], desc: "The permissions of the token"
- optional :access_level, type: Integer, desc: "The access level of the token in the project"
+ optional :access_level, type: Integer, desc: "The access level of the token in the #{source_type}"
optional :expires_at, type: Date, desc: "The expiration date of the token"
end
post ':id/access_tokens' do
@@ -71,7 +71,7 @@ module API
).execute
if token_response.success?
- present token_response.payload[:access_token], with: Entities::ResourceAccessTokenWithToken, project: resource
+ present token_response.payload[:access_token], with: Entities::ResourceAccessTokenWithToken, resource: resource
else
bad_request!(token_response.message)
end
diff --git a/lib/api/rubygem_packages.rb b/lib/api/rubygem_packages.rb
index 9ef6ec03a41..3effa370e84 100644
--- a/lib/api/rubygem_packages.rb
+++ b/lib/api/rubygem_packages.rb
@@ -66,9 +66,12 @@ module API
get "gems/:file_name", requirements: FILE_NAME_REQUIREMENTS do
authorize!(:read_package, user_project)
- package_file = ::Packages::PackageFile.for_rubygem_with_file_name(
- user_project, params[:file_name]
- ).last!
+ package_files = ::Packages::PackageFile
+ .for_rubygem_with_file_name(user_project, params[:file_name])
+
+ package_files = package_files.installable if Feature.enabled?(:packages_installable_package_files, default_enabled: :yaml)
+
+ package_file = package_files.last!
track_package_event('pull_package', :rubygems, project: user_project, namespace: user_project.namespace)
diff --git a/lib/api/search.rb b/lib/api/search.rb
index fbdbe3476db..60a7e944b43 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -4,7 +4,11 @@ module API
class Search < ::API::Base
include PaginationParams
- before { authenticate! }
+ before do
+ authenticate!
+
+ check_rate_limit!(:user_email_lookup, scope: [current_user]) if search_service.params.email_lookup?
+ end
feature_category :global_search
@@ -36,7 +40,7 @@ module API
}.freeze
end
- def search(additional_params = {})
+ def search_service(additional_params = {})
search_params = {
scope: params[:scope],
search: params[:search],
@@ -50,7 +54,11 @@ module API
sort: params[:sort]
}.merge(additional_params)
- results = SearchService.new(current_user, search_params).search_objects(preload_method)
+ SearchService.new(current_user, search_params)
+ end
+
+ def search(additional_params = {})
+ results = search_service(additional_params).search_objects(preload_method)
Gitlab::UsageDataCounters::SearchCounter.count(:all_searches)
diff --git a/lib/api/terraform/modules/v1/packages.rb b/lib/api/terraform/modules/v1/packages.rb
index ad5a4ae7ea6..970fdeba734 100644
--- a/lib/api/terraform/modules/v1/packages.rb
+++ b/lib/api/terraform/modules/v1/packages.rb
@@ -71,7 +71,11 @@ module API
def package_file
strong_memoize(:package_file) do
- package.package_files.first
+ if Feature.enabled?(:packages_installable_package_files, default_enabled: :yaml)
+ package.installable_package_files.first
+ else
+ package.package_files.first
+ end
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index ce0a0e9b502..eeb5244466a 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -142,11 +142,15 @@ module API
get ":id", feature_category: :users do
forbidden!('Not authorized!') unless current_user
+ if Feature.enabled?(:rate_limit_user_by_id_endpoint, type: :development)
+ check_rate_limit! :users_get_by_id, scope: current_user unless current_user.admin?
+ end
+
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
- opts = { with: current_user&.admin? ? Entities::UserDetailsWithAdmin : Entities::User, current_user: current_user }
+ opts = { with: current_user.admin? ? Entities::UserDetailsWithAdmin : Entities::User, current_user: current_user }
user, opts = with_custom_attributes(user, opts)
present user, opts
@@ -1072,7 +1076,7 @@ module API
attrs = declared_params(include_missing: false)
- service = ::Users::UpsertCreditCardValidationService.new(attrs).execute
+ service = ::Users::UpsertCreditCardValidationService.new(attrs, user).execute
if service.success?
present user.credit_card_validation, with: Entities::UserCreditCardValidations
diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb
index d6c026963e1..c86b7785ce2 100644
--- a/lib/api/v3/github.rb
+++ b/lib/api/v3/github.rb
@@ -183,7 +183,9 @@ module API
params do
use :project_full_path
end
- get ':namespace/:project/pulls' do
+ # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/337269
+ get ':namespace/:project/pulls', urgency: :low do
user_project = find_project_with_access(params)
merge_requests = authorized_merge_requests_for_project(user_project)
@@ -236,7 +238,9 @@ module API
use :project_full_path
use :pagination
end
- get ':namespace/:project/branches' do
+ # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/337268
+ get ':namespace/:project/branches', urgency: :low do
user_project = find_project_with_access(params)
update_project_feature_usage_for(user_project)
diff --git a/lib/backup.rb b/lib/backup.rb
index 91682645a9a..95b595d885a 100644
--- a/lib/backup.rb
+++ b/lib/backup.rb
@@ -16,19 +16,6 @@ module Backup
end
end
- class RepositoryBackupError < Backup::Error
- attr_reader :container, :backup_repos_path
-
- def initialize(container, backup_repos_path)
- @container = container
- @backup_repos_path = backup_repos_path
- end
-
- def message
- "Failed to create compressed file '#{backup_repos_path}' when trying to backup the following paths: '#{container.disk_path}'"
- end
- end
-
class DatabaseBackupError < Backup::Error
attr_reader :config, :db_file_name
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index f07fd786b4b..a4ac404d245 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -61,7 +61,7 @@ module Backup
report_success(success)
progress.flush
- raise Backup::Error, 'Backup failed' unless success
+ raise DatabaseBackupError.new(config, db_file_name) unless success
end
def restore
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 42cfff98239..4e51dcfb79e 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -37,7 +37,7 @@ module Backup
unless status == 0
puts output
- raise Backup::Error, 'Backup failed'
+ raise_custom_error
end
tar_cmd = [tar, exclude_dirs(:tar), %W[-C #{@backup_files_dir} -cf - .]].flatten
@@ -49,7 +49,7 @@ module Backup
end
unless pipeline_succeeded?(tar_status: status_list[0], gzip_status: status_list[1], output: output)
- raise Backup::Error, "Backup operation failed: #{output}"
+ raise_custom_error
end
end
@@ -143,5 +143,9 @@ module Backup
end
end
end
+
+ def raise_custom_error
+ raise FileBackupError.new(app_files_dir, backup_tarball)
+ end
end
end
diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb
index b104beed39c..8ac09e94004 100644
--- a/lib/backup/gitaly_backup.rb
+++ b/lib/backup/gitaly_backup.rb
@@ -2,11 +2,17 @@
module Backup
# Backup and restores repositories using gitaly-backup
+ #
+ # gitaly-backup can work in parallel and accepts a list of repositories
+ # through input pipe using a specific json format for both backup and restore
class GitalyBackup
- def initialize(progress, parallel: nil, parallel_storage: nil)
+ # @param [StringIO] progress IO interface to output progress
+ # @param [Integer] max_parallelism max parallelism when running backups
+ # @param [Integer] storage_parallelism max parallelism per storage (is affected by max_parallelism)
+ def initialize(progress, max_parallelism: nil, storage_parallelism: nil)
@progress = progress
- @parallel = parallel
- @parallel_storage = parallel_storage
+ @max_parallelism = max_parallelism
+ @storage_parallelism = storage_parallelism
end
def start(type)
@@ -22,20 +28,20 @@ module Backup
end
args = []
- args += ['-parallel', @parallel.to_s] if @parallel
- args += ['-parallel-storage', @parallel_storage.to_s] if @parallel_storage
+ args += ['-parallel', @max_parallelism.to_s] if @max_parallelism
+ args += ['-parallel-storage', @storage_parallelism.to_s] if @storage_parallelism
- @stdin, stdout, @thread = Open3.popen2(build_env, bin_path, command, '-path', backup_repos_path, *args)
+ @input_stream, stdout, @thread = Open3.popen2(build_env, bin_path, command, '-path', backup_repos_path, *args)
@out_reader = Thread.new do
IO.copy_stream(stdout, @progress)
end
end
- def wait
+ def finish!
return unless started?
- @stdin.close
+ @input_stream.close
[@thread, @out_reader].each(&:join)
status = @thread.value
@@ -49,12 +55,7 @@ module Backup
repository = repo_type.repository_for(container)
- @stdin.puts({
- storage_name: repository.storage,
- relative_path: repository.relative_path,
- gl_project_path: repository.gl_project_path,
- always_create: repo_type.project?
- }.merge(Gitlab::GitalyClient.connection_data(repository.storage)).to_json)
+ schedule_backup_job(repository, always_create: repo_type.project?)
end
def parallel_enqueue?
@@ -63,6 +64,24 @@ module Backup
private
+ # Schedule a new backup job through a non-blocking JSON based pipe protocol
+ #
+ # @see https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/gitaly-backup.md
+ def schedule_backup_job(repository, always_create:)
+ connection_params = Gitlab::GitalyClient.connection_data(repository.storage)
+
+ json_job = {
+ address: connection_params['address'],
+ token: connection_params['token'],
+ storage_name: repository.storage,
+ relative_path: repository.relative_path,
+ gl_project_path: repository.gl_project_path,
+ always_create: always_create
+ }.to_json
+
+ @input_stream.puts(json_job)
+ end
+
def build_env
{
'SSL_CERT_FILE' => OpenSSL::X509::DEFAULT_CERT_FILE,
diff --git a/lib/backup/gitaly_rpc_backup.rb b/lib/backup/gitaly_rpc_backup.rb
index baac4eb26ca..bbd83cd2157 100644
--- a/lib/backup/gitaly_rpc_backup.rb
+++ b/lib/backup/gitaly_rpc_backup.rb
@@ -23,7 +23,7 @@ module Backup
end
end
- def wait
+ def finish!
@type = nil
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 1bdc4965e5d..ed2e001cefc 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -2,7 +2,7 @@
module Backup
class Manager
- ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs terraform_state registry packages].freeze
FOLDERS_TO_BACKUP = %w[repositories db].freeze
FILE_NAME_SUFFIX = '_gitlab_backup.tar'
diff --git a/lib/backup/packages.rb b/lib/backup/packages.rb
new file mode 100644
index 00000000000..7b6a8f086ed
--- /dev/null
+++ b/lib/backup/packages.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Backup
+ class Packages < Backup::Files
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
+ super('packages', Settings.packages.storage_path, excludes: ['tmp'])
+ end
+ end
+end
diff --git a/lib/backup/repositories.rb b/lib/backup/repositories.rb
index 0b5a62529b4..4c39e58c87d 100644
--- a/lib/backup/repositories.rb
+++ b/lib/backup/repositories.rb
@@ -40,7 +40,7 @@ module Backup
raise errors.pop unless errors.empty?
ensure
- strategy.wait
+ strategy.finish!
end
def restore
@@ -48,7 +48,7 @@ module Backup
enqueue_consecutive
ensure
- strategy.wait
+ strategy.finish!
cleanup_snippets_without_repositories
restore_object_pools
diff --git a/lib/backup/terraform_state.rb b/lib/backup/terraform_state.rb
new file mode 100644
index 00000000000..5f71e18f1b4
--- /dev/null
+++ b/lib/backup/terraform_state.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Backup
+ class TerraformState < Backup::Files
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
+ super('terraform_state', Settings.terraform_state.storage_path, excludes: ['tmp'])
+ end
+ end
+end
diff --git a/lib/banzai/filter/base_sanitization_filter.rb b/lib/banzai/filter/base_sanitization_filter.rb
index 7ea32c4b1e7..4e350a59fa0 100644
--- a/lib/banzai/filter/base_sanitization_filter.rb
+++ b/lib/banzai/filter/base_sanitization_filter.rb
@@ -42,7 +42,7 @@ module Banzai
# Allow any protocol in `a` elements
# and then remove links with unsafe protocols
allowlist[:protocols].delete('a')
- allowlist[:transformers].push(self.class.method(:remove_unsafe_links))
+ allowlist[:transformers].push(self.class.method(:sanitize_unsafe_links))
# Remove `rel` attribute from `a` elements
allowlist[:transformers].push(self.class.remove_rel)
diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb
index 00a38f02141..537b7c80d91 100644
--- a/lib/banzai/filter/footnote_filter.rb
+++ b/lib/banzai/filter/footnote_filter.rb
@@ -7,13 +7,14 @@ module Banzai
# Footnotes are supported in CommonMark. However we were stripping
# the ids during sanitization. Those are now allowed.
#
- # Footnotes are numbered the same - the first one has `id=fn1`, the
- # second is `id=fn2`, etc. In order to allow footnotes when rendering
- # multiple markdown blocks on a page, we need to make each footnote
- # reference unique.
- #
+ # Footnotes are numbered as an increasing integer starting at `1`.
+ # The `id` associated with a footnote is based on the footnote reference
+ # string. For example, `[^foot]` will generate `id="fn-foot"`.
+ # In order to allow footnotes when rendering multiple markdown blocks
+ # on a page, we need to make each footnote reference unique.
+
# This filter adds a random number to each footnote (the same number
- # can be used for a single render). So you get `id=fn1-4335` and `id=fn2-4335`.
+ # can be used for a single render). So you get `id=fn-1-4335` and `id=fn-foot-4335`.
#
class FootnoteFilter < HTML::Pipeline::Filter
FOOTNOTE_ID_PREFIX = 'fn-'
@@ -26,53 +27,24 @@ module Banzai
CSS_FOOTNOTE = 'sup > a[data-footnote-ref]'
XPATH_FOOTNOTE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_FOOTNOTE).freeze
- # only needed when feature flag use_cmark_renderer is turned off
- INTEGER_PATTERN = /\A\d+\z/.freeze
- FOOTNOTE_ID_PREFIX_OLD = 'fn'
- FOOTNOTE_LINK_ID_PREFIX_OLD = 'fnref'
- FOOTNOTE_LI_REFERENCE_PATTERN_OLD = /\A#{FOOTNOTE_ID_PREFIX_OLD}\d+\z/.freeze
- FOOTNOTE_LINK_REFERENCE_PATTERN_OLD = /\A#{FOOTNOTE_LINK_ID_PREFIX_OLD}\d+\z/.freeze
- FOOTNOTE_START_NUMBER = 1
- CSS_SECTION_OLD = "ol > li[id=#{FOOTNOTE_ID_PREFIX_OLD}#{FOOTNOTE_START_NUMBER}]"
- XPATH_SECTION_OLD = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION_OLD).freeze
-
def call
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- # Sanitization stripped off the section class - add it back in
- return doc unless section_node = doc.at_xpath(XPATH_SECTION)
+ # Sanitization stripped off the section class - add it back in
+ return doc unless section_node = doc.at_xpath(XPATH_SECTION)
- section_node.append_class('footnotes')
- else
- return doc unless first_footnote = doc.at_xpath(XPATH_SECTION_OLD)
- return doc unless first_footnote.parent
-
- first_footnote.parent.wrap('<section class="footnotes">')
- end
+ section_node.append_class('footnotes')
rand_suffix = "-#{random_number}"
modified_footnotes = {}
- xpath_footnote = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- XPATH_FOOTNOTE
- else
- Gitlab::Utils::Nokogiri.css_to_xpath('sup > a[id]')
- end
-
- doc.xpath(xpath_footnote).each do |link_node|
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
- ref_num.gsub!(/[[:punct:]]/, '\\\\\&')
- else
- ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX_OLD)
- end
+ doc.xpath(XPATH_FOOTNOTE).each do |link_node|
+ ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
+ ref_num.gsub!(/[[:punct:]]/, '\\\\\&')
- css = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? "section[data-footnotes] li[id=#{fn_id(ref_num)}]" : "li[id=#{fn_id(ref_num)}]"
+ css = "section[data-footnotes] li[id=#{fn_id(ref_num)}]"
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath(css)
footnote_node = doc.at_xpath(node_xpath)
if footnote_node || modified_footnotes[ref_num]
- next if Feature.disabled?(:use_cmark_renderer, default_enabled: :yaml) && !INTEGER_PATTERN.match?(ref_num)
-
link_node[:href] += rand_suffix
link_node[:id] += rand_suffix
@@ -103,13 +75,11 @@ module Banzai
end
def fn_id(num)
- prefix = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? FOOTNOTE_ID_PREFIX : FOOTNOTE_ID_PREFIX_OLD
- "#{prefix}#{num}"
+ "#{FOOTNOTE_ID_PREFIX}#{num}"
end
def fnref_id(num)
- prefix = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? FOOTNOTE_LINK_ID_PREFIX : FOOTNOTE_LINK_ID_PREFIX_OLD
- "#{prefix}#{num}"
+ "#{FOOTNOTE_LINK_ID_PREFIX}#{num}"
end
end
end
diff --git a/lib/banzai/filter/markdown_engines/common_mark.rb b/lib/banzai/filter/markdown_engines/common_mark.rb
index dc94e3c925a..cf368e28beb 100644
--- a/lib/banzai/filter/markdown_engines/common_mark.rb
+++ b/lib/banzai/filter/markdown_engines/common_mark.rb
@@ -4,8 +4,8 @@
# This module is used in Banzai::Filter::MarkdownFilter.
# Used gem is `commonmarker` which is a ruby wrapper for libcmark (CommonMark parser)
# including GitHub's GFM extensions.
+# We now utilize the renderer built in `C`, rather than the ruby based renderer.
# Homepage: https://github.com/gjtorikian/commonmarker
-
module Banzai
module Filter
module MarkdownEngines
@@ -22,57 +22,29 @@ module Banzai
:VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
].freeze
- RENDER_OPTIONS_C = [
+ RENDER_OPTIONS = [
:GITHUB_PRE_LANG, # use GitHub-style <pre lang> for fenced code blocks.
:FOOTNOTES, # render footnotes.
:FULL_INFO_STRING, # include full info strings of code blocks in separate attribute.
:UNSAFE # allow raw/custom HTML and unsafe links.
].freeze
- # The `:GITHUB_PRE_LANG` option is not used intentionally because
- # it renders a fence block with language as `<pre lang="LANG"><code>some code\n</code></pre>`
- # while GitLab's syntax is `<pre><code lang="LANG">some code\n</code></pre>`.
- # If in the future the syntax is about to be made GitHub-compatible, please, add `:GITHUB_PRE_LANG` render option below
- # and remove `code_block` method from `lib/banzai/renderer/common_mark/html.rb`.
- RENDER_OPTIONS_RUBY = [
- # as of commonmarker 0.18.0, we need to use :UNSAFE to get the same as the original :DEFAULT
- # https://github.com/gjtorikian/commonmarker/pull/81
- :UNSAFE # allow raw/custom HTML and unsafe links.
- ].freeze
-
def initialize(context)
@context = context
- @renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options) if Feature.disabled?(:use_cmark_renderer, default_enabled: :yaml)
end
def render(text)
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- CommonMarker.render_html(text, render_options, extensions)
- else
- doc = CommonMarker.render_doc(text, PARSE_OPTIONS, extensions)
-
- @renderer.render(doc)
- end
+ CommonMarker.render_html(text, render_options, EXTENSIONS)
end
private
- def extensions
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- EXTENSIONS
- else
- EXTENSIONS + [
- :tagfilter # strips out several "unsafe" HTML tags from being used: https://github.github.com/gfm/#disallowed-raw-html-extension-
- ].freeze
- end
- end
-
def render_options
@context[:no_sourcepos] ? render_options_no_sourcepos : render_options_sourcepos
end
def render_options_no_sourcepos
- Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? RENDER_OPTIONS_C : RENDER_OPTIONS_RUBY
+ RENDER_OPTIONS
end
def render_options_sourcepos
diff --git a/lib/banzai/filter/markdown_post_escape_filter.rb b/lib/banzai/filter/markdown_post_escape_filter.rb
index b979b7573ae..09ae09a22ae 100644
--- a/lib/banzai/filter/markdown_post_escape_filter.rb
+++ b/lib/banzai/filter/markdown_post_escape_filter.rb
@@ -8,8 +8,10 @@ module Banzai
NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze
SPAN_REGEX = %r{<span>(.*?)</span>}.freeze
- CSS_A = 'a'
- XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
+ CSS_A = 'a'
+ XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
+ CSS_LANG_TAG = 'pre'
+ XPATH_LANG_TAG = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_LANG_TAG).freeze
def call
return doc unless result[:escaped_literals]
@@ -32,22 +34,12 @@ module Banzai
node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title']
end
- doc.xpath(lang_tag).each do |node|
+ doc.xpath(XPATH_LANG_TAG).each do |node|
node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang']
end
doc
end
-
- private
-
- def lang_tag
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- Gitlab::Utils::Nokogiri.css_to_xpath('pre')
- else
- Gitlab::Utils::Nokogiri.css_to_xpath('code')
- end
- end
end
end
end
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
index 3f160960d23..68a99702d6f 100644
--- a/lib/banzai/filter/plantuml_filter.rb
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -25,12 +25,7 @@ module Banzai
private
def lang_tag
- @lang_tag ||=
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- Gitlab::Utils::Nokogiri.css_to_xpath('pre[lang="plantuml"] > code').freeze
- else
- Gitlab::Utils::Nokogiri.css_to_xpath('pre > code[lang="plantuml"]').freeze
- end
+ @lang_tag ||= Gitlab::Utils::Nokogiri.css_to_xpath('pre[lang="plantuml"] > code').freeze
end
def settings
diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb
index 7a23326bafa..a34519799d5 100644
--- a/lib/banzai/filter/references/abstract_reference_filter.rb
+++ b/lib/banzai/filter/references/abstract_reference_filter.rb
@@ -216,6 +216,8 @@ module Banzai
url_for_object_cached(object, parent)
end
+ url.chomp!(matches[:format]) if matches.names.include?("format")
+
content = link_content || object_link_text(object, matches)
link = %(<a href="#{url}" #{data}
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index d5f45ff7689..fe189b1b0c9 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -28,12 +28,10 @@ module Banzai
allowlist[:attributes]['li'] = %w[id]
allowlist[:transformers].push(self.class.remove_non_footnote_ids)
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- # Allow section elements with data-footnotes attribute
- allowlist[:elements].push('section')
- allowlist[:attributes]['section'] = %w(data-footnotes)
- allowlist[:attributes]['a'].push('data-footnote-ref', 'data-footnote-backref')
- end
+ # Allow section elements with data-footnotes attribute
+ allowlist[:elements].push('section')
+ allowlist[:attributes]['section'] = %w(data-footnotes)
+ allowlist[:attributes]['a'].push('data-footnote-ref', 'data-footnote-backref')
allowlist
end
@@ -61,13 +59,8 @@ module Banzai
return unless node.name == 'a' || node.name == 'li'
return unless node.has_attribute?('id')
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN
- return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN
- else
- return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN_OLD
- return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN_OLD
- end
+ return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN
+ return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN
node.remove_attribute('id')
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 9fcfcf4acc4..07f82c98666 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -14,7 +14,7 @@ module Banzai
LANG_PARAMS_DELIMITER = ':'
LANG_PARAMS_ATTR = 'data-lang-params'
- CSS = 'pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code'
+ CSS = 'pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code:only-child'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
@@ -70,11 +70,11 @@ module Banzai
private
def parse_lang_params(node)
- node = node.parent if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
+ node = node.parent
# Commonmarker's FULL_INFO_STRING render option works with the space delimiter.
# But the current behavior of GitLab's markdown renderer is different - it grabs everything as the single
- # line, including language and its options. To keep backward compatability, we have to parse the old format and
+ # line, including language and its options. To keep backward compatibility, we have to parse the old format and
# merge with the new one.
#
# Behaviors before separating language and its parameters:
@@ -91,11 +91,7 @@ module Banzai
return unless language
language, language_params = language.split(LANG_PARAMS_DELIMITER, 2)
-
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- language_params = [node.attr('data-meta'), language_params].compact.join(' ')
- end
-
+ language_params = [node.attr('data-meta'), language_params].compact.join(' ')
formatted_language_params = format_language_params(language_params)
[language, formatted_language_params]
@@ -110,8 +106,8 @@ module Banzai
(Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
end
+ # Replace the parent `pre` element with the entire highlighted block
def replace_parent_pre_element(node, highlighted)
- # Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index 1664fa1f9ff..3e28f06b783 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -8,8 +8,6 @@ module Banzai
self.reference_type = :merge_request
def nodes_visible_to_user(user, nodes)
- return super if Feature.disabled?(:optimize_merge_request_parser, user, default_enabled: :yaml)
-
merge_request_nodes = nodes.select { |node| node.has_attribute?(self.class.data_attribute) }
records = projects_for_nodes(merge_request_nodes)
diff --git a/lib/banzai/renderer/common_mark/html.rb b/lib/banzai/renderer/common_mark/html.rb
deleted file mode 100644
index d9a2d9a9564..00000000000
--- a/lib/banzai/renderer/common_mark/html.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# Remove this entire file when removing `use_cmark_renderer` feature flag and switching to the CMARK html renderer.
-# https://gitlab.com/gitlab-org/gitlab/-/issues/345744
-module Banzai
- module Renderer
- module CommonMark
- class HTML < CommonMarker::HtmlRenderer
- def code_block(node)
- block do
- out("<pre#{sourcepos(node)}><code")
- out(' lang="', node.fence_info, '"') if node.fence_info.present?
- out('>')
- out(escape_html(node.string_content))
- out('</code></pre>')
- end
- end
- end
- end
- end
-end
diff --git a/lib/bulk_imports/common/extractors/ndjson_extractor.rb b/lib/bulk_imports/common/extractors/ndjson_extractor.rb
index ecd7c08bd25..04febebff8e 100644
--- a/lib/bulk_imports/common/extractors/ndjson_extractor.rb
+++ b/lib/bulk_imports/common/extractors/ndjson_extractor.rb
@@ -4,49 +4,47 @@ module BulkImports
module Common
module Extractors
class NdjsonExtractor
- include Gitlab::ImportExport::CommandLineUtil
- include Gitlab::Utils::StrongMemoize
-
def initialize(relation:)
@relation = relation
- @tmp_dir = Dir.mktmpdir
+ @tmpdir = Dir.mktmpdir
end
def extract(context)
- download_service(tmp_dir, context).execute
- decompression_service(tmp_dir).execute
- relations = ndjson_reader(tmp_dir).consume_relation('', relation)
+ download_service(context).execute
+ decompression_service.execute
+
+ records = ndjson_reader.consume_relation('', relation)
- BulkImports::Pipeline::ExtractedData.new(data: relations)
+ BulkImports::Pipeline::ExtractedData.new(data: records)
end
- def remove_tmp_dir
- FileUtils.remove_entry(tmp_dir)
+ def remove_tmpdir
+ FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
end
private
- attr_reader :relation, :tmp_dir
+ attr_reader :relation, :tmpdir
def filename
- @filename ||= "#{relation}.ndjson.gz"
+ "#{relation}.ndjson.gz"
end
- def download_service(tmp_dir, context)
+ def download_service(context)
@download_service ||= BulkImports::FileDownloadService.new(
configuration: context.configuration,
relative_url: context.entity.relation_download_url_path(relation),
- dir: tmp_dir,
+ tmpdir: tmpdir,
filename: filename
)
end
- def decompression_service(tmp_dir)
- @decompression_service ||= BulkImports::FileDecompressionService.new(dir: tmp_dir, filename: filename)
+ def decompression_service
+ @decompression_service ||= BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: filename)
end
- def ndjson_reader(tmp_dir)
- @ndjson_reader ||= Gitlab::ImportExport::Json::NdjsonReader.new(tmp_dir)
+ def ndjson_reader
+ @ndjson_reader ||= Gitlab::ImportExport::Json::NdjsonReader.new(tmpdir)
end
end
end
diff --git a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb
index 2ac4e533c1d..d7b9d6920ea 100644
--- a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb
+++ b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb
@@ -15,7 +15,7 @@ module BulkImports
decompression_service.execute
extraction_service.execute
- upload_file_paths = Dir.glob(File.join(tmp_dir, '**', '*'))
+ upload_file_paths = Dir.glob(File.join(tmpdir, '**', '*'))
BulkImports::Pipeline::ExtractedData.new(data: upload_file_paths)
end
@@ -37,7 +37,7 @@ module BulkImports
end
def after_run(_)
- FileUtils.remove_entry(tmp_dir) if Dir.exist?(tmp_dir)
+ FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
end
private
@@ -46,17 +46,17 @@ module BulkImports
BulkImports::FileDownloadService.new(
configuration: context.configuration,
relative_url: context.entity.relation_download_url_path(relation),
- dir: tmp_dir,
+ tmpdir: tmpdir,
filename: targz_filename
)
end
def decompression_service
- BulkImports::FileDecompressionService.new(dir: tmp_dir, filename: targz_filename)
+ BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename)
end
def extraction_service
- BulkImports::ArchiveExtractionService.new(tmpdir: tmp_dir, filename: tar_filename)
+ BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename)
end
def relation
@@ -71,8 +71,8 @@ module BulkImports
"#{tar_filename}.gz"
end
- def tmp_dir
- @tmp_dir ||= Dir.mktmpdir('bulk_imports')
+ def tmpdir
+ @tmpdir ||= Dir.mktmpdir('bulk_imports')
end
def file_uploader
diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb
index d5475a8b324..d85e51984df 100644
--- a/lib/bulk_imports/ndjson_pipeline.rb
+++ b/lib/bulk_imports/ndjson_pipeline.rb
@@ -68,7 +68,7 @@ module BulkImports
end
def after_run(_)
- extractor.remove_tmp_dir if extractor.respond_to?(:remove_tmp_dir)
+ extractor.remove_tmpdir if extractor.respond_to?(:remove_tmpdir)
end
def relation_class(relation_key)
diff --git a/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb b/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb
index 4d742225ff7..2492a023cbe 100644
--- a/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb
@@ -8,15 +8,16 @@ module BulkImports
transformer ::BulkImports::Common::Transformers::ProhibitedAttributesTransformer
- def extract(context)
- download_service(tmp_dir, context).execute
- decompression_service(tmp_dir).execute
+ def extract(_context)
+ download_service.execute
+ decompression_service.execute
+
project_attributes = json_decode(json_attributes)
BulkImports::Pipeline::ExtractedData.new(data: project_attributes)
end
- def transform(_, data)
+ def transform(_context, data)
subrelations = config.portable_relations_tree.keys.map(&:to_s)
Gitlab::ImportExport::AttributeCleaner.clean(
@@ -26,42 +27,42 @@ module BulkImports
).except(*subrelations)
end
- def load(_, data)
+ def load(_context, data)
portable.assign_attributes(data)
portable.reconcile_shared_runners_setting!
portable.drop_visibility_level!
portable.save!
end
- def after_run(_)
- FileUtils.remove_entry(tmp_dir)
+ def after_run(_context)
+ FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
end
def json_attributes
- @json_attributes ||= File.read(File.join(tmp_dir, filename))
+ @json_attributes ||= File.read(File.join(tmpdir, filename))
end
private
- def tmp_dir
- @tmp_dir ||= Dir.mktmpdir
+ def tmpdir
+ @tmpdir ||= Dir.mktmpdir('bulk_imports')
end
def config
@config ||= BulkImports::FileTransfer.config_for(portable)
end
- def download_service(tmp_dir, context)
+ def download_service
@download_service ||= BulkImports::FileDownloadService.new(
configuration: context.configuration,
- relative_url: context.entity.relation_download_url_path(BulkImports::FileTransfer::BaseConfig::SELF_RELATION),
- dir: tmp_dir,
+ relative_url: context.entity.relation_download_url_path(BulkImports::FileTransfer::BaseConfig::SELF_RELATION),
+ tmpdir: tmpdir,
filename: compressed_filename
)
end
- def decompression_service(tmp_dir)
- @decompression_service ||= BulkImports::FileDecompressionService.new(dir: tmp_dir, filename: compressed_filename)
+ def decompression_service
+ @decompression_service ||= BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: compressed_filename)
end
def compressed_filename
diff --git a/lib/feature.rb b/lib/feature.rb
index f301f206b46..12b4ef07dd6 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -29,6 +29,15 @@ class Feature
class << self
delegate :group, to: :flipper
+ def feature_flags_available?
+ # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
+ active_db_connection = ActiveRecord::Base.connection.active? rescue false # rubocop:disable Database/MultipleDatabases
+
+ active_db_connection && Feature::FlipperFeature.table_exists?
+ rescue ActiveRecord::NoDatabaseError
+ false
+ end
+
def all
flipper.features.to_a
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index d93d7acbaad..2449554d3c0 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -1,10 +1,15 @@
# frozen_string_literal: true
require 'pathname'
+require 'forwardable'
+
+require_relative 'gitlab_edition'
module Gitlab
- def self.root
- Pathname.new(File.expand_path('..', __dir__))
+ class << self
+ extend Forwardable
+
+ def_delegators :GitlabEdition, :root, :extensions, :ee?, :ee, :jh?, :jh
end
def self.version_info
@@ -89,47 +94,6 @@ module Gitlab
Rails.env.development? || Rails.env.test?
end
- def self.extensions
- if jh?
- %w[ee jh]
- elsif ee?
- %w[ee]
- else
- %w[]
- end
- end
-
- def self.ee?
- @is_ee ||=
- # We use this method when the Rails environment is not loaded. This
- # means that checking the presence of the License class could result in
- # this method returning `false`, even for an EE installation.
- #
- # The `FOSS_ONLY` is always `string` or `nil`
- # Thus the nil or empty string will result
- # in using default value: false
- #
- # The behavior needs to be synchronised with
- # config/helpers/is_ee_env.js
- root.join('ee/app/models/license.rb').exist? &&
- !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
- end
-
- def self.jh?
- @is_jh ||=
- ee? &&
- root.join('jh').exist? &&
- !%w[true 1].include?(ENV['EE_ONLY'].to_s)
- end
-
- def self.ee
- yield if ee?
- end
-
- def self.jh
- yield if jh?
- end
-
def self.http_proxy_env?
HTTP_PROXY_ENV_VARS.any? { |name| ENV[name] }
end
diff --git a/lib/gitlab/anonymous_session.rb b/lib/gitlab/anonymous_session.rb
index e58240e16b4..6904945a755 100644
--- a/lib/gitlab/anonymous_session.rb
+++ b/lib/gitlab/anonymous_session.rb
@@ -2,14 +2,12 @@
module Gitlab
class AnonymousSession
- include ::Gitlab::Redis::SessionsStoreHelper
-
def initialize(remote_ip)
@remote_ip = remote_ip
end
def count_session_ip
- redis_store_class.with do |redis|
+ Gitlab::Redis::Sessions.with do |redis|
redis.pipelined do |pipeline|
pipeline.incr(session_lookup_name)
pipeline.expire(session_lookup_name, 24.hours)
@@ -18,13 +16,13 @@ module Gitlab
end
def session_count
- redis_store_class.with do |redis|
+ Gitlab::Redis::Sessions.with do |redis|
redis.get(session_lookup_name).to_i
end
end
def cleanup_session_per_ip_count
- redis_store_class.with do |redis|
+ Gitlab::Redis::Sessions.with do |redis|
redis.del(session_lookup_name)
end
end
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index fb90ad9e275..12f1b15f820 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -49,9 +49,15 @@ module Gitlab
group_testing_hook: { threshold: 5, interval: 1.minute },
profile_add_new_email: { threshold: 5, interval: 1.minute },
web_hook_calls: { interval: 1.minute },
+ users_get_by_id: { threshold: 10, interval: 1.minute },
+ username_exists: { threshold: 20, interval: 1.minute },
+ user_sign_up: { threshold: 20, interval: 1.minute },
profile_resend_email_confirmation: { threshold: 5, interval: 1.minute },
+ profile_update_username: { threshold: 10, interval: 1.minute },
update_environment_canary_ingress: { threshold: 1, interval: 1.minute },
- auto_rollback_deployment: { threshold: 1, interval: 3.minutes }
+ auto_rollback_deployment: { threshold: 1, interval: 3.minutes },
+ user_email_lookup: { threshold: -> { application_settings.user_email_lookup_limit }, interval: 1.minute },
+ gitlab_shell_operation: { threshold: 600, interval: 1.minute }
}.freeze
end
@@ -59,7 +65,7 @@ module Gitlab
# be throttled.
#
# @param key [Symbol] Key attribute registered in `.rate_limits`
- # @param scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
+ # @param scope [Array<ActiveRecord>] Array of ActiveRecord models, Strings or Symbols to scope throttling to a specific request (e.g. per user per project)
# @param threshold [Integer] Optional threshold value to override default one registered in `.rate_limits`
# @param users_allowlist [Array<String>] Optional list of usernames to exclude from the limit. This param will only be functional if Scope includes a current user.
# @param peek [Boolean] Optional. When true the key will not be incremented but the current throttled state will be returned.
diff --git a/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb b/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
index 3ada3f947ee..b2e1c9e2379 100644
--- a/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
+++ b/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
@@ -7,11 +7,7 @@ module Gitlab
register_for 'gitlab-html-pipeline'
def format(node, lang, opts)
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- %(<pre #{lang ? %[lang="#{lang}"] : ''}><code>#{node.content}</code></pre>)
- else
- %(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>)
- end
+ %(<pre #{lang ? %[lang="#{lang}"] : ''}><code>#{node.content}</code></pre>)
end
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 3e982168339..38bc50a2cb8 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -84,7 +84,7 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
- break if user && !can_user_login_with_non_expired_password?(user)
+ break if user && !user.can_log_in_with_non_expired_password?
authenticators = []
@@ -187,7 +187,7 @@ module Gitlab
if valid_oauth_token?(token)
user = User.id_in(token.resource_owner_id).first
- return unless user && can_user_login_with_non_expired_password?(user)
+ return unless user && user.can_log_in_with_non_expired_password?
Gitlab::Auth::Result.new(user, nil, :oauth, abilities_for_scopes(token.scopes))
end
@@ -210,7 +210,7 @@ module Gitlab
return unless token_bot_in_project?(token.user, project) || token_bot_in_group?(token.user, project)
end
- if can_user_login_with_non_expired_password?(token.user) || token.user.project_bot?
+ if token.user.can_log_in_with_non_expired_password? || token.user.project_bot?
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end
end
@@ -309,7 +309,7 @@ module Gitlab
return unless build.project.builds_enabled?
if build.user
- return unless can_user_login_with_non_expired_password?(build.user) || (build.user.project_bot? && build.project.bots&.include?(build.user))
+ return unless build.user.can_log_in_with_non_expired_password? || (build.user.project_bot? && build.project.bots&.include?(build.user))
# If user is assigned to build, use restricted credentials of user
Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
@@ -406,10 +406,6 @@ module Gitlab
user.increment_failed_attempts!
end
-
- def can_user_login_with_non_expired_password?(user)
- user.can?(:log_in) && !user.password_expired_if_applicable?
- end
end
end
end
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index 9c33a5fc872..ecda96af403 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -165,7 +165,7 @@ module Gitlab
authorization_token, _options = token_and_options(current_request)
- ::Clusters::AgentToken.find_by_token(authorization_token)
+ ::Clusters::AgentToken.active.find_by_token(authorization_token)
end
def find_runner_from_token
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index 7bfe776fed0..82c6411c712 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -206,7 +206,8 @@ module Gitlab
def base_options
{
host: options['host'],
- port: options['port']
+ port: options['port'],
+ hosts: options['hosts']
}
end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index feb5fea4c85..9f142727ebb 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -230,8 +230,8 @@ module Gitlab
name: name.strip.presence || valid_username,
username: valid_username,
email: email,
- password: auth_hash.password,
- password_confirmation: auth_hash.password,
+ password: Gitlab::Password.test_default(21),
+ password_confirmation: Gitlab::Password.test_default(21),
password_automatically_set: true
}
end
diff --git a/lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb b/lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb
new file mode 100644
index 00000000000..2247747ba08
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to create ci_namespace_mirrors entries in batches
+ class BackfillCiNamespaceMirrors
+ class Namespace < ActiveRecord::Base # rubocop:disable Style/Documentation
+ include ::EachBatch
+
+ self.table_name = 'namespaces'
+ self.inheritance_column = nil
+
+ scope :base_query, -> do
+ select(:id, :parent_id)
+ end
+ end
+
+ PAUSE_SECONDS = 0.1
+ SUB_BATCH_SIZE = 500
+
+ def perform(start_id, end_id)
+ batch_query = Namespace.base_query.where(id: start_id..end_id)
+ batch_query.each_batch(of: SUB_BATCH_SIZE) do |sub_batch|
+ first, last = sub_batch.pluck(Arel.sql('MIN(id), MAX(id)')).first
+ ranged_query = Namespace.unscoped.base_query.where(id: first..last)
+
+ update_sql = <<~SQL
+ INSERT INTO ci_namespace_mirrors (namespace_id, traversal_ids)
+ #{insert_values(ranged_query)}
+ ON CONFLICT (namespace_id) DO NOTHING
+ SQL
+ # We do nothing on conflict because we consider they were already filled.
+
+ Namespace.connection.execute(update_sql)
+
+ sleep PAUSE_SECONDS
+ end
+
+ mark_job_as_succeeded(start_id, end_id)
+ end
+
+ private
+
+ def insert_values(batch)
+ calculated_traversal_ids(
+ batch.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
+ )
+ end
+
+ # Copied from lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb
+ def calculated_traversal_ids(batch)
+ <<~SQL
+ WITH RECURSIVE cte(source_id, namespace_id, parent_id, height) AS (
+ (
+ SELECT batch.id, batch.id, batch.parent_id, 1
+ FROM (#{batch.to_sql}) AS batch
+ )
+ UNION ALL
+ (
+ SELECT cte.source_id, n.id, n.parent_id, cte.height+1
+ FROM namespaces n, cte
+ WHERE n.id = cte.parent_id
+ )
+ )
+ SELECT flat_hierarchy.source_id as namespace_id,
+ array_agg(flat_hierarchy.namespace_id ORDER BY flat_hierarchy.height DESC) as traversal_ids
+ FROM (SELECT * FROM cte FOR UPDATE) flat_hierarchy
+ GROUP BY flat_hierarchy.source_id
+ SQL
+ end
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded('BackfillCiNamespaceMirrors', arguments)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_ci_project_mirrors.rb b/lib/gitlab/background_migration/backfill_ci_project_mirrors.rb
new file mode 100644
index 00000000000..ff6ab9928b0
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_ci_project_mirrors.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to create ci_project_mirrors entries in batches
+ class BackfillCiProjectMirrors
+ class Project < ActiveRecord::Base # rubocop:disable Style/Documentation
+ include ::EachBatch
+
+ self.table_name = 'projects'
+
+ scope :base_query, -> do
+ select(:id, :namespace_id)
+ end
+ end
+
+ PAUSE_SECONDS = 0.1
+ SUB_BATCH_SIZE = 500
+
+ def perform(start_id, end_id)
+ batch_query = Project.base_query.where(id: start_id..end_id)
+ batch_query.each_batch(of: SUB_BATCH_SIZE) do |sub_batch|
+ first, last = sub_batch.pluck(Arel.sql('MIN(id), MAX(id)')).first
+ ranged_query = Project.unscoped.base_query.where(id: first..last)
+
+ update_sql = <<~SQL
+ INSERT INTO ci_project_mirrors (project_id, namespace_id)
+ #{insert_values(ranged_query)}
+ ON CONFLICT (project_id) DO NOTHING
+ SQL
+ # We do nothing on conflict because we consider they were already filled.
+
+ Project.connection.execute(update_sql)
+
+ sleep PAUSE_SECONDS
+ end
+
+ mark_job_as_succeeded(start_id, end_id)
+ end
+
+ private
+
+ def insert_values(batch)
+ batch.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433').to_sql
+ end
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded('BackfillCiProjectMirrors', arguments)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb b/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb
new file mode 100644
index 00000000000..2d46ff6b933
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # BackfillIncidentIssueEscalationStatuses adds
+ # IncidentManagement::IssuableEscalationStatus records for existing Incident issues.
+ # They will be added with no policy, and escalations_started_at as nil.
+ class BackfillIncidentIssueEscalationStatuses
+ def perform(start_id, stop_id)
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO incident_management_issuable_escalation_statuses (issue_id, created_at, updated_at)
+ SELECT issues.id, current_timestamp, current_timestamp
+ FROM issues
+ WHERE issues.issue_type = 1
+ AND issues.id BETWEEN #{start_id} AND #{stop_id}
+ ON CONFLICT (issue_id) DO NOTHING;
+ SQL
+
+ mark_job_as_succeeded(start_id, stop_id)
+ end
+
+ private
+
+ def mark_job_as_succeeded(*arguments)
+ ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ self.class.name.demodulize,
+ arguments
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/base_job.rb b/lib/gitlab/background_migration/base_job.rb
new file mode 100644
index 00000000000..e21e7e0e4a3
--- /dev/null
+++ b/lib/gitlab/background_migration/base_job.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Simple base class for background migration job classes which are executed through the sidekiq queue.
+ #
+ # Any job class that inherits from the base class will have connection to the tracking database set on
+ # initialization.
+ class BaseJob
+ def initialize(connection:)
+ @connection = connection
+ end
+
+ def perform(*arguments)
+ raise NotImplementedError, "subclasses of #{self.class.name} must implement #{__method__}"
+ end
+
+ private
+
+ attr_reader :connection
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_rename.rb b/lib/gitlab/background_migration/cleanup_concurrent_rename.rb
deleted file mode 100644
index d3f366f3480..00000000000
--- a/lib/gitlab/background_migration/cleanup_concurrent_rename.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Background migration for cleaning up a concurrent column rename.
- class CleanupConcurrentRename < CleanupConcurrentSchemaChange
- RESCHEDULE_DELAY = 10.minutes
-
- def cleanup_concurrent_schema_change(table, old_column, new_column)
- cleanup_concurrent_column_rename(table, old_column, new_column)
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb b/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb
deleted file mode 100644
index 91b50c1a493..00000000000
--- a/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Base class for background migration for rename/type changes.
- class CleanupConcurrentSchemaChange
- include Database::MigrationHelpers
-
- # table - The name of the table the migration is performed for.
- # old_column - The name of the old (to drop) column.
- # new_column - The name of the new column.
- def perform(table, old_column, new_column)
- return unless column_exists?(table, new_column) && column_exists?(table, old_column)
-
- rows_to_migrate = define_model_for(table)
- .where(new_column => nil)
- .where
- .not(old_column => nil)
-
- if rows_to_migrate.any?
- BackgroundMigrationWorker.perform_in(
- RESCHEDULE_DELAY,
- self.class.name,
- [table, old_column, new_column]
- )
- else
- cleanup_concurrent_schema_change(table, old_column, new_column)
- end
- end
-
- def cleanup_concurrent_schema_change(_table, _old_column, _new_column)
- raise NotImplementedError
- end
-
- # These methods are necessary so we can re-use the migration helpers in
- # this class.
- def connection
- ActiveRecord::Base.connection
- end
-
- def method_missing(name, *args, &block)
- connection.__send__(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
- end
-
- def respond_to_missing?(*args)
- connection.respond_to?(*args) || super
- end
-
- def define_model_for(table)
- Class.new(ActiveRecord::Base) do
- self.table_name = table
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb b/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
deleted file mode 100644
index 48411095dbb..00000000000
--- a/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Background migration for cleaning up a concurrent column type changeb.
- class CleanupConcurrentTypeChange < CleanupConcurrentSchemaChange
- RESCHEDULE_DELAY = 10.minutes
-
- def cleanup_concurrent_schema_change(table, old_column, new_column)
- cleanup_concurrent_column_type_change(table, old_column)
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/copy_column.rb b/lib/gitlab/background_migration/copy_column.rb
deleted file mode 100644
index ef70f37d5eb..00000000000
--- a/lib/gitlab/background_migration/copy_column.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # CopyColumn is a simple (reusable) background migration that can be used to
- # update the value of a column based on the value of another column in the
- # same table.
- #
- # For this background migration to work the table that is migrated _has_ to
- # have an `id` column as the primary key.
- class CopyColumn
- # table - The name of the table that contains the columns.
- # copy_from - The column containing the data to copy.
- # copy_to - The column to copy the data to.
- # start_id - The start ID of the range of rows to update.
- # end_id - The end ID of the range of rows to update.
- def perform(table, copy_from, copy_to, start_id, end_id)
- return unless connection.column_exists?(table, copy_to)
-
- quoted_table = connection.quote_table_name(table)
- quoted_copy_from = connection.quote_column_name(copy_from)
- quoted_copy_to = connection.quote_column_name(copy_to)
-
- # We're using raw SQL here since this job may be frequently executed. As
- # a result dynamically defining models would lead to many unnecessary
- # schema information queries.
- connection.execute <<-SQL.strip_heredoc
- UPDATE #{quoted_table}
- SET #{quoted_copy_to} = #{quoted_copy_from}
- WHERE id BETWEEN #{start_id} AND #{end_id}
- AND #{quoted_copy_from} IS NOT NULL
- AND #{quoted_copy_to} IS NULL
- SQL
- end
-
- def connection
- ActiveRecord::Base.connection
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/encrypt_static_object_token.rb b/lib/gitlab/background_migration/encrypt_static_object_token.rb
new file mode 100644
index 00000000000..80931353e2f
--- /dev/null
+++ b/lib/gitlab/background_migration/encrypt_static_object_token.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Populates "static_object_token_encrypted" field with encrypted versions
+ # of values from "static_object_token" field
+ class EncryptStaticObjectToken
+ # rubocop:disable Style/Documentation
+ class User < ActiveRecord::Base
+ include ::EachBatch
+ self.table_name = 'users'
+ scope :with_static_object_token, -> { where.not(static_object_token: nil) }
+ scope :without_static_object_token_encrypted, -> { where(static_object_token_encrypted: nil) }
+ end
+ # rubocop:enable Style/Documentation
+
+ BATCH_SIZE = 100
+
+ def perform(start_id, end_id)
+ ranged_query = User
+ .where(id: start_id..end_id)
+ .with_static_object_token
+ .without_static_object_token_encrypted
+
+ ranged_query.each_batch(of: BATCH_SIZE) do |sub_batch|
+ first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first
+
+ batch_query = User.unscoped
+ .where(id: first..last)
+ .with_static_object_token
+ .without_static_object_token_encrypted
+
+ user_tokens = batch_query.pluck(:id, :static_object_token)
+
+ user_encrypted_tokens = user_tokens.map do |(id, plaintext_token)|
+ next if plaintext_token.blank?
+
+ [id, Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext_token)]
+ end
+
+ encrypted_tokens_sql = user_encrypted_tokens.compact.map { |(id, token)| "(#{id}, '#{token}')" }.join(',')
+
+ if user_encrypted_tokens.present?
+ User.connection.execute(<<~SQL)
+ WITH cte(cte_id, cte_token) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
+ SELECT *
+ FROM (VALUES #{encrypted_tokens_sql}) AS t (id, token)
+ )
+ UPDATE #{User.table_name}
+ SET static_object_token_encrypted = cte_token
+ FROM cte
+ WHERE cte_id = id
+ SQL
+ end
+
+ mark_job_as_succeeded(start_id, end_id)
+ end
+ end
+
+ private
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ self.class.name.demodulize,
+ arguments
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb b/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb
new file mode 100644
index 00000000000..2b049ea2d2f
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'parser/ruby27'
+
+module Gitlab
+ module BackgroundMigration
+ # This migration fixes raw_metadata entries which have incorrectly been passed a Ruby Hash instead of JSON data.
+ class FixVulnerabilityOccurrencesWithHashesAsRawMetadata
+ CLUSTER_IMAGE_SCANNING_REPORT_TYPE = 7
+ GENERIC_REPORT_TYPE = 99
+
+ # Type error is used to handle unexpected types when parsing stringified hashes.
+ class TypeError < ::StandardError
+ attr_reader :message, :type
+
+ def initialize(message, type)
+ @message = message
+ @type = type
+ end
+ end
+
+ # Migration model namespace isolated from application code.
+ class Finding < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'vulnerability_occurrences'
+
+ scope :by_api_report_types, -> { where(report_type: [CLUSTER_IMAGE_SCANNING_REPORT_TYPE, GENERIC_REPORT_TYPE]) }
+ end
+
+ def perform(start_id, end_id)
+ Finding.by_api_report_types.where(id: start_id..end_id).each do |finding|
+ next if valid_json?(finding.raw_metadata)
+
+ metadata = hash_from_s(finding.raw_metadata)
+
+ finding.update(raw_metadata: metadata.to_json) if metadata
+ end
+ mark_job_as_succeeded(start_id, end_id)
+ end
+
+ def hash_from_s(str_hash)
+ ast = Parser::Ruby27.parse(str_hash)
+
+ unless ast.type == :hash
+ ::Gitlab::AppLogger.error(message: "expected raw_metadata to be a hash", type: ast.type)
+ return
+ end
+
+ parse_hash(ast)
+ rescue Parser::SyntaxError => e
+ ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message)
+ nil
+ rescue TypeError => e
+ ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message, type: e.type)
+ nil
+ end
+
+ private
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ 'FixVulnerabilityOccurrencesWithHashesAsRawMetadata',
+ arguments
+ )
+ end
+
+ def valid_json?(metadata)
+ Oj.load(metadata)
+ true
+ rescue Oj::ParseError, Encoding::UndefinedConversionError
+ false
+ end
+
+ def parse_hash(hash)
+ out = {}
+ hash.children.each do |node|
+ unless node.type == :pair
+ raise TypeError.new("expected child of hash to be a `pair`", node.type)
+ end
+
+ key, value = node.children
+
+ key = parse_key(key)
+ value = parse_value(value)
+
+ out[key] = value
+ end
+
+ out
+ end
+
+ def parse_key(key)
+ case key.type
+ when :sym, :str, :int
+ key.children.first
+ else
+ raise TypeError.new("expected key to be either symbol, string, or integer", key.type)
+ end
+ end
+
+ def parse_value(value)
+ case value.type
+ when :sym, :str, :int
+ value.children.first
+ # rubocop:disable Lint/BooleanSymbol
+ when :true
+ true
+ when :false
+ false
+ # rubocop:enable Lint/BooleanSymbol
+ when :nil
+ nil
+ when :array
+ value.children.map { |c| parse_value(c) }
+ when :hash
+ parse_hash(value)
+ else
+ raise TypeError.new("value of a pair was an unexpected type", value.type)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/job_coordinator.rb b/lib/gitlab/background_migration/job_coordinator.rb
index cfbe7167677..5dc77f935e3 100644
--- a/lib/gitlab/background_migration/job_coordinator.rb
+++ b/lib/gitlab/background_migration/job_coordinator.rb
@@ -36,6 +36,8 @@ module Gitlab
attr_reader :worker_class
+ delegate :minimum_interval, :perform_in, to: :worker_class
+
def queue
@queue ||= worker_class.sidekiq_options['queue']
end
@@ -79,7 +81,7 @@ module Gitlab
def perform(class_name, arguments)
with_shared_connection do
- migration_class_for(class_name).new.perform(*arguments)
+ migration_instance_for(class_name).perform(*arguments)
end
end
@@ -113,6 +115,16 @@ module Gitlab
enqueued_job?([retry_set], migration_class)
end
+ def migration_instance_for(class_name)
+ migration_class = migration_class_for(class_name)
+
+ if migration_class < Gitlab::BackgroundMigration::BaseJob
+ migration_class.new(connection: connection)
+ else
+ migration_class.new
+ end
+ end
+
def migration_class_for(class_name)
Gitlab::BackgroundMigration.const_get(class_name, false)
end
diff --git a/lib/gitlab/background_migration/migrate_legacy_artifacts.rb b/lib/gitlab/background_migration/migrate_legacy_artifacts.rb
deleted file mode 100644
index 23d99274232..00000000000
--- a/lib/gitlab/background_migration/migrate_legacy_artifacts.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: true
-# rubocop:disable Metrics/ClassLength
-
-module Gitlab
- module BackgroundMigration
- ##
- # The class to migrate job artifacts from `ci_builds` to `ci_job_artifacts`
- class MigrateLegacyArtifacts
- FILE_LOCAL_STORE = 1 # equal to ObjectStorage::Store::LOCAL
- ARCHIVE_FILE_TYPE = 1 # equal to Ci::JobArtifact.file_types['archive']
- METADATA_FILE_TYPE = 2 # equal to Ci::JobArtifact.file_types['metadata']
- LEGACY_PATH_FILE_LOCATION = 1 # equal to Ci::JobArtifact.file_location['legacy_path']
-
- def perform(start_id, stop_id)
- ActiveRecord::Base.transaction do
- insert_archives(start_id, stop_id)
- insert_metadatas(start_id, stop_id)
- delete_legacy_artifacts(start_id, stop_id)
- end
- end
-
- private
-
- def insert_archives(start_id, stop_id)
- ActiveRecord::Base.connection.execute <<~SQL
- INSERT INTO
- ci_job_artifacts (
- project_id,
- job_id,
- expire_at,
- file_location,
- created_at,
- updated_at,
- file,
- size,
- file_store,
- file_type
- )
- SELECT
- project_id,
- id,
- artifacts_expire_at #{add_missing_db_timezone},
- #{LEGACY_PATH_FILE_LOCATION},
- created_at #{add_missing_db_timezone},
- created_at #{add_missing_db_timezone},
- artifacts_file,
- artifacts_size,
- COALESCE(artifacts_file_store, #{FILE_LOCAL_STORE}),
- #{ARCHIVE_FILE_TYPE}
- FROM
- ci_builds
- WHERE
- id BETWEEN #{start_id.to_i} AND #{stop_id.to_i}
- AND artifacts_file <> ''
- AND NOT EXISTS (
- SELECT
- 1
- FROM
- ci_job_artifacts
- WHERE
- ci_builds.id = ci_job_artifacts.job_id
- AND ci_job_artifacts.file_type = #{ARCHIVE_FILE_TYPE})
- SQL
- end
-
- def insert_metadatas(start_id, stop_id)
- ActiveRecord::Base.connection.execute <<~SQL
- INSERT INTO
- ci_job_artifacts (
- project_id,
- job_id,
- expire_at,
- file_location,
- created_at,
- updated_at,
- file,
- size,
- file_store,
- file_type
- )
- SELECT
- project_id,
- id,
- artifacts_expire_at #{add_missing_db_timezone},
- #{LEGACY_PATH_FILE_LOCATION},
- created_at #{add_missing_db_timezone},
- created_at #{add_missing_db_timezone},
- artifacts_metadata,
- NULL,
- COALESCE(artifacts_metadata_store, #{FILE_LOCAL_STORE}),
- #{METADATA_FILE_TYPE}
- FROM
- ci_builds
- WHERE
- id BETWEEN #{start_id.to_i} AND #{stop_id.to_i}
- AND artifacts_file <> ''
- AND artifacts_metadata <> ''
- AND NOT EXISTS (
- SELECT
- 1
- FROM
- ci_job_artifacts
- WHERE
- ci_builds.id = ci_job_artifacts.job_id
- AND ci_job_artifacts.file_type = #{METADATA_FILE_TYPE})
- SQL
- end
-
- def delete_legacy_artifacts(start_id, stop_id)
- ActiveRecord::Base.connection.execute <<~SQL
- UPDATE
- ci_builds
- SET
- artifacts_file = NULL,
- artifacts_file_store = NULL,
- artifacts_size = NULL,
- artifacts_metadata = NULL,
- artifacts_metadata_store = NULL
- WHERE
- id BETWEEN #{start_id.to_i} AND #{stop_id.to_i}
- AND artifacts_file <> ''
- SQL
- end
-
- def add_missing_db_timezone
- 'at time zone \'UTC\''
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/populate_test_reports_issue_id.rb b/lib/gitlab/background_migration/populate_test_reports_issue_id.rb
new file mode 100644
index 00000000000..301efd0c943
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_test_reports_issue_id.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+# rubocop: disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class PopulateTestReportsIssueId
+ def perform(start_id, stop_id)
+ # NO OP
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::PopulateTestReportsIssueId.prepend_mod
diff --git a/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb b/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
index 84ff7423254..c1b8de1f6aa 100644
--- a/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
+++ b/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# rubocop: disable Style/Documentation
-class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
+class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid # rubocop:disable Metrics/ClassLength
# rubocop: disable Gitlab/NamespacedClass
class VulnerabilitiesIdentifier < ActiveRecord::Base
self.table_name = "vulnerability_identifiers"
@@ -9,10 +9,14 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
end
class VulnerabilitiesFinding < ActiveRecord::Base
+ include EachBatch
include ShaAttribute
self.table_name = "vulnerability_occurrences"
+
+ has_many :signatures, foreign_key: 'finding_id', class_name: 'VulnerabilityFindingSignature', inverse_of: :finding
belongs_to :primary_identifier, class_name: 'VulnerabilitiesIdentifier', inverse_of: :primary_findings, foreign_key: 'primary_identifier_id'
+
REPORT_TYPES = {
sast: 0,
dependency_scanning: 1,
@@ -20,7 +24,9 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
dast: 3,
secret_detection: 4,
coverage_fuzzing: 5,
- api_fuzzing: 6
+ api_fuzzing: 6,
+ cluster_image_scanning: 7,
+ generic: 99
}.with_indifferent_access.freeze
enum report_type: REPORT_TYPES
@@ -28,6 +34,25 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
sha_attribute :location_fingerprint
end
+ class VulnerabilityFindingSignature < ActiveRecord::Base
+ include ShaAttribute
+
+ self.table_name = 'vulnerability_finding_signatures'
+ belongs_to :finding, foreign_key: 'finding_id', inverse_of: :signatures, class_name: 'VulnerabilitiesFinding'
+
+ sha_attribute :signature_sha
+ end
+
+ class VulnerabilitiesFindingPipeline < ActiveRecord::Base
+ include EachBatch
+ self.table_name = "vulnerability_occurrence_pipelines"
+ end
+
+ class Vulnerability < ActiveRecord::Base
+ include EachBatch
+ self.table_name = "vulnerabilities"
+ end
+
class CalculateFindingUUID
FINDING_NAMESPACES_IDS = {
development: "a143e9e2-41b3-47bc-9a19-081d089229f4",
@@ -52,35 +77,122 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
end
# rubocop: enable Gitlab/NamespacedClass
+ # rubocop: disable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength
def perform(start_id, end_id)
- findings = VulnerabilitiesFinding
- .joins(:primary_identifier)
- .select(:id, :report_type, :fingerprint, :location_fingerprint, :project_id)
- .where(id: start_id..end_id)
-
- mappings = findings.each_with_object({}) do |finding, hash|
- hash[finding] = { uuid: calculate_uuid_v5_for_finding(finding) }
+ unless Feature.enabled?(:migrate_vulnerability_finding_uuids, default_enabled: true)
+ return log_info('Migration is disabled by the feature flag', start_id: start_id, end_id: end_id)
end
- ::Gitlab::Database::BulkUpdate.execute(%i[uuid], mappings)
+ log_info('Migration started', start_id: start_id, end_id: end_id)
- logger.info(message: 'RecalculateVulnerabilitiesOccurrencesUuid Migration: recalculation is done for:',
- finding_ids: mappings.keys.pluck(:id))
+ VulnerabilitiesFinding
+ .joins(:primary_identifier)
+ .includes(:signatures)
+ .select(:id, :report_type, :primary_identifier_id, :fingerprint, :location_fingerprint, :project_id, :created_at, :vulnerability_id, :uuid)
+ .where(id: start_id..end_id)
+ .each_batch(of: 50) do |relation|
+ duplicates = find_duplicates(relation)
+ remove_findings(ids: duplicates) if duplicates.present?
+
+ to_update = relation.reject { |finding| duplicates.include?(finding.id) }
+
+ begin
+ known_uuids = Set.new
+ to_be_deleted = []
+
+ mappings = to_update.each_with_object({}) do |finding, hash|
+ uuid = calculate_uuid_v5_for_finding(finding)
+
+ if known_uuids.add?(uuid)
+ hash[finding] = { uuid: uuid }
+ else
+ to_be_deleted << finding.id
+ end
+ end
+
+ # It is technically still possible to have duplicate uuids
+ # if the data integrity is broken somehow and the primary identifiers of
+ # the findings are pointing to different projects with the same fingerprint values.
+ if to_be_deleted.present?
+ log_info('Conflicting UUIDs found within the batch', finding_ids: to_be_deleted)
+
+ remove_findings(ids: to_be_deleted)
+ end
+
+ ::Gitlab::Database::BulkUpdate.execute(%i[uuid], mappings) if mappings.present?
+
+ log_info('Recalculation is done', finding_ids: mappings.keys.pluck(:id))
+ rescue ActiveRecord::RecordNotUnique => error
+ log_info('RecordNotUnique error received')
+
+ match_data = /\(uuid\)=\((?<uuid>\S{36})\)/.match(error.message)
+
+ # This exception returns the **correct** UUIDv5 which probably comes from a later record
+ # and it's the one we can drop in the easiest way before retrying the UPDATE query
+ if match_data
+ uuid = match_data[:uuid]
+ log_info('Conflicting UUID found', uuid: uuid)
+
+ id = VulnerabilitiesFinding.find_by(uuid: uuid)&.id
+ remove_findings(ids: id) if id
+ retry
+ else
+ log_error('Couldnt find conflicting uuid')
+
+ Gitlab::ErrorTracking.track_and_raise_exception(error)
+ end
+ end
+ end
mark_job_as_succeeded(start_id, end_id)
rescue StandardError => error
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
+ log_error('An exception happened')
+
+ Gitlab::ErrorTracking.track_and_raise_exception(error)
end
+ # rubocop: disable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength
private
+ def find_duplicates(relation)
+ to_exclude = []
+ relation.flat_map do |record|
+ # Assuming we're scanning id 31 and the duplicate is id 40
+ # first we'd process 31 and add 40 to the list of ids to remove
+ # then we would process record 40 and add 31 to the list of removals
+ # so we would drop both records
+ to_exclude << record.id
+
+ VulnerabilitiesFinding.where(
+ report_type: record.report_type,
+ location_fingerprint: record.location_fingerprint,
+ primary_identifier_id: record.primary_identifier_id,
+ project_id: record.project_id
+ ).where.not(id: to_exclude).pluck(:id)
+ end
+ end
+
+ def remove_findings(ids:)
+ ids = Array(ids)
+ log_info('Removing Findings and associated records', ids: ids)
+
+ vulnerability_ids = VulnerabilitiesFinding.where(id: ids).pluck(:vulnerability_id).uniq.compact
+
+ VulnerabilitiesFindingPipeline.where(occurrence_id: ids).each_batch { |batch| batch.delete_all }
+ Vulnerability.where(id: vulnerability_ids).each_batch { |batch| batch.delete_all }
+ VulnerabilitiesFinding.where(id: ids).delete_all
+ end
+
def calculate_uuid_v5_for_finding(vulnerability_finding)
return unless vulnerability_finding
+ signatures = vulnerability_finding.signatures.sort_by { |signature| signature.algorithm_type_before_type_cast }
+ location_fingerprint = signatures.last&.signature_sha || vulnerability_finding.location_fingerprint
+
uuid_v5_name_components = {
report_type: vulnerability_finding.report_type,
primary_identifier_fingerprint: vulnerability_finding.fingerprint,
- location_fingerprint: vulnerability_finding.location_fingerprint,
+ location_fingerprint: location_fingerprint,
project_id: vulnerability_finding.project_id
}
@@ -89,6 +201,14 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
CalculateFindingUUID.call(name)
end
+ def log_info(message, **extra)
+ logger.info(migrator: 'RecalculateVulnerabilitiesOccurrencesUuid', message: message, **extra)
+ end
+
+ def log_error(message, **extra)
+ logger.error(migrator: 'RecalculateVulnerabilitiesOccurrencesUuid', message: message, **extra)
+ end
+
def logger
@logger ||= Gitlab::BackgroundMigration::Logger.build
end
diff --git a/lib/gitlab/background_migration/remove_duplicate_services.rb b/lib/gitlab/background_migration/remove_duplicate_services.rb
deleted file mode 100644
index 59fb9143a72..00000000000
--- a/lib/gitlab/background_migration/remove_duplicate_services.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Remove duplicated service records with the same project and type.
- # These were created in the past for unknown reasons, and should be blocked
- # now by the uniqueness validation in the Service model.
- class RemoveDuplicateServices
- # See app/models/service
- class Service < ActiveRecord::Base
- include EachBatch
-
- self.table_name = 'services'
- self.inheritance_column = :_type_disabled
-
- scope :project_ids_with_duplicates, -> do
- select(:project_id)
- .distinct
- .where.not(project_id: nil)
- .group(:project_id, :type)
- .having('count(*) > 1')
- end
-
- scope :types_with_duplicates, -> (project_ids) do
- select(:project_id, :type)
- .where(project_id: project_ids)
- .group(:project_id, :type)
- .having('count(*) > 1')
- end
- end
-
- def perform(*project_ids)
- types_with_duplicates = Service.types_with_duplicates(project_ids).pluck(:project_id, :type)
-
- types_with_duplicates.each do |project_id, type|
- remove_duplicates(project_id, type)
- end
- end
-
- private
-
- def remove_duplicates(project_id, type)
- scope = Service.where(project_id: project_id, type: type)
-
- # Build a subquery to determine which service record is actually in use,
- # by querying for it without specifying an order.
- #
- # This should match the record returned by `Project#find_service`,
- # and the `has_one` service associations on `Project`.
- correct_service = scope.select(:id).limit(1)
-
- # Delete all other services with the same `project_id` and `type`
- duplicate_services = scope.where.not(id: correct_service)
- duplicate_services.delete_all
- end
- end
- end
-end
diff --git a/lib/gitlab/checks/changes_access.rb b/lib/gitlab/checks/changes_access.rb
index 3ce2e50c548..84c01cf4baf 100644
--- a/lib/gitlab/checks/changes_access.rb
+++ b/lib/gitlab/checks/changes_access.rb
@@ -33,18 +33,33 @@ module Gitlab
# changes. This set may also contain commits which are not referenced by
# any of the new revisions.
def commits
+ allow_quarantine = true
+
newrevs = @changes.map do |change|
+ oldrev = change[:oldrev]
newrev = change[:newrev]
- newrev unless newrev.blank? || Gitlab::Git.blank_ref?(newrev)
+
+ next if blank_rev?(newrev)
+
+ # In case any of the old revisions is blank, then we cannot reliably
+ # detect which commits are new for a given change when enumerating
+ # objects via the object quarantine directory given that the client
+ # may have pushed too many commits, and we don't know when to
+ # terminate the walk. We thus fall back to using `git rev-list --not
+ # --all`, which is a lot less efficient but at least can only ever
+ # returns commits which really are new.
+ allow_quarantine = false if allow_quarantine && blank_rev?(oldrev)
+
+ newrev
end.compact
return [] if newrevs.empty?
- @commits ||= project.repository.new_commits(newrevs, allow_quarantine: true)
+ @commits ||= project.repository.new_commits(newrevs, allow_quarantine: allow_quarantine)
end
# All commits which have been newly introduced via the given revision.
- def commits_for(newrev)
+ def commits_for(oldrev, newrev)
commits_by_id = commits.index_by(&:id)
result = []
@@ -65,9 +80,11 @@ module Gitlab
# Only add the parent ID to the pending set if we actually know its
# commit to guards us against readding an ID which we have already
- # queued up before.
+ # queued up before. Furthermore, we stop walking as soon as we hit
+ # `oldrev` such that we do not include any commits in our checks
+ # which have been "over-pushed" by the client.
commit.parent_ids.each do |parent_id|
- pending.add(parent_id) if commits_by_id.has_key?(parent_id)
+ pending.add(parent_id) if commits_by_id.has_key?(parent_id) && parent_id != oldrev
end
result << commit
@@ -80,10 +97,10 @@ module Gitlab
@single_changes_accesses ||=
changes.map do |change|
commits =
- if change[:newrev].blank? || Gitlab::Git.blank_ref?(change[:newrev])
+ if blank_rev?(change[:newrev])
[]
else
- Gitlab::Lazy.new { commits_for(change[:newrev]) }
+ Gitlab::Lazy.new { commits_for(change[:oldrev], change[:newrev]) }
end
Checks::SingleChangeAccess.new(
@@ -109,6 +126,10 @@ module Gitlab
def bulk_access_checks!
Gitlab::Checks::LfsCheck.new(self).validate!
end
+
+ def blank_rev?(rev)
+ rev.blank? || Gitlab::Git.blank_ref?(rev)
+ end
end
end
end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index afe0ccb361e..7ade9ca5085 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -35,7 +35,10 @@ module Gitlab
# patterns can be matched only when branch or tag is used
# the pattern matching does not work for merge requests pipelines
if pipeline.branch? || pipeline.tag?
- if regexp = Gitlab::UntrustedRegexp::RubySyntax.fabricate(pattern, fallback: true)
+ regexp = Gitlab::UntrustedRegexp::RubySyntax
+ .fabricate(pattern, fallback: true, project: pipeline.project)
+
+ if regexp
regexp.match?(pipeline.ref)
else
pattern == pipeline.ref
diff --git a/lib/gitlab/ci/build/status/reason.rb b/lib/gitlab/ci/build/status/reason.rb
new file mode 100644
index 00000000000..82e07faef63
--- /dev/null
+++ b/lib/gitlab/ci/build/status/reason.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ module Status
+ class Reason
+ attr_reader :build, :failure_reason, :exit_code
+
+ def initialize(build, failure_reason, exit_code = nil)
+ @build = build
+ @failure_reason = failure_reason
+ @exit_code = exit_code
+ end
+
+ def failure_reason_enum
+ ::CommitStatus.failure_reasons[failure_reason]
+ end
+
+ def force_allow_failure?
+ return false if exit_code.nil?
+
+ !build.allow_failure? && build.allowed_to_fail_with_code?(exit_code)
+ end
+
+ def self.fabricate(build, reason)
+ if reason.is_a?(self)
+ new(build, reason.failure_reason, reason.exit_code)
+ else
+ new(build, reason)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 42b487fdf81..4c98941e032 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -36,7 +36,7 @@ module Gitlab
end
@root = self.logger.instrument(:config_compose) do
- Entry::Root.new(@config).tap(&:compose!)
+ Entry::Root.new(@config, project: project, user: user).tap(&:compose!)
end
rescue *rescue_errors => e
raise Config::ConfigError, e.message
diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb
index e6290ef2479..41a3c87037b 100644
--- a/lib/gitlab/ci/config/entry/root.rb
+++ b/lib/gitlab/ci/config/entry/root.rb
@@ -59,7 +59,8 @@ module Gitlab
entry :types, Entry::Stages,
description: 'Deprecated: stages for this pipeline.',
- reserved: true
+ reserved: true,
+ deprecation: { deprecated: '9.0', warning: '14.8', removed: '15.0', documentation: 'https://docs.gitlab.com/ee/ci/yaml/#deprecated-keywords' }
entry :cache, Entry::Caches,
description: 'Configure caching between build jobs.',
@@ -122,8 +123,9 @@ module Gitlab
# Deprecated `:types` key workaround - if types are defined and
# stages are not defined we use types definition as stages.
#
- if types_defined? && !stages_defined?
- @entries[:stages] = @entries[:types]
+ if types_defined?
+ @entries[:stages] = @entries[:types] unless stages_defined?
+ log_and_warn_deprecated_entry(@entries[:types])
end
@entries.delete(:types)
diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb
new file mode 100644
index 00000000000..278353220e4
--- /dev/null
+++ b/lib/gitlab/ci/jwt_v2.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class JwtV2 < Jwt
+ private
+
+ def reserved_claims
+ super.merge(
+ iss: Settings.gitlab.base_url,
+ sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}",
+ aud: Settings.gitlab.base_url
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index 15b0ff3c04d..54b54bd0514 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -9,13 +9,13 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
def perform!
- logger.instrument(:pipeline_save) do
+ logger.instrument_with_sql(:pipeline_save) do
BulkInsertableAssociations.with_bulk_insert do
- tags = extract_tag_list_by_status
-
- pipeline.transaction do
- pipeline.save!
- CommitStatus.bulk_insert_tags!(statuses, tags) if bulk_insert_tags?
+ with_bulk_insert_tags do
+ pipeline.transaction do
+ pipeline.save!
+ CommitStatus.bulk_insert_tags!(statuses) if bulk_insert_tags?
+ end
end
end
end
@@ -29,32 +29,26 @@ module Gitlab
private
- def statuses
- strong_memoize(:statuses) do
- pipeline.stages.flat_map(&:statuses)
+ def bulk_insert_tags?
+ strong_memoize(:bulk_insert_tags) do
+ ::Feature.enabled?(:ci_bulk_insert_tags, project, default_enabled: :yaml)
end
end
- # We call `job.tag_list=` to assign tags to the jobs from the
- # Chain::Seed step which uses the `@tag_list` instance variable to
- # store them on the record. We remove them here because we want to
- # bulk insert them, otherwise they would be inserted and assigned one
- # by one with callbacks. We must use `remove_instance_variable`
- # because having the instance variable defined would still run the callbacks
- def extract_tag_list_by_status
- return {} unless bulk_insert_tags?
-
- statuses.each.with_object({}) do |job, acc|
- tag_list = job.clear_memoization(:tag_list)
- next unless tag_list
-
- acc[job.name] = tag_list
- end
+ def with_bulk_insert_tags
+ previous = Thread.current['ci_bulk_insert_tags']
+ Thread.current['ci_bulk_insert_tags'] = bulk_insert_tags?
+ yield
+ ensure
+ Thread.current['ci_bulk_insert_tags'] = previous
end
- def bulk_insert_tags?
- strong_memoize(:bulk_insert_tags) do
- ::Feature.enabled?(:ci_bulk_insert_tags, project, default_enabled: :yaml)
+ def statuses
+ strong_memoize(:statuses) do
+ pipeline
+ .stages
+ .flat_map(&:statuses)
+ .select { |status| status.respond_to?(:tag_list) }
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/create_deployments.rb b/lib/gitlab/ci/pipeline/chain/create_deployments.rb
index b92aa89d62d..b913ba3c87d 100644
--- a/lib/gitlab/ci/pipeline/chain/create_deployments.rb
+++ b/lib/gitlab/ci/pipeline/chain/create_deployments.rb
@@ -5,8 +5,6 @@ module Gitlab
module Pipeline
module Chain
class CreateDeployments < Chain::Base
- DeploymentCreationError = Class.new(StandardError)
-
def perform!
return unless pipeline.create_deployment_in_separate_transaction?
@@ -24,18 +22,7 @@ module Gitlab
end
def create_deployment(build)
- return unless build.instance_of?(::Ci::Build) && build.persisted_environment.present?
-
- deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
- .new(build, build.persisted_environment).to_resource
-
- return unless deployment
-
- deployment.deployable = build
- deployment.save!
- rescue ActiveRecord::RecordInvalid => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
- DeploymentCreationError.new(e.message), build_id: build.id)
+ ::Deployments::CreateForBuildService.new.execute(build)
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb
index 356eeb76908..feae123f216 100644
--- a/lib/gitlab/ci/pipeline/chain/seed.rb
+++ b/lib/gitlab/ci/pipeline/chain/seed.rb
@@ -53,13 +53,18 @@ module Gitlab
end
def context
- Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: root_variables)
+ Gitlab::Ci::Pipeline::Seed::Context.new(
+ pipeline,
+ root_variables: root_variables,
+ logger: logger
+ )
end
def root_variables
logger.instrument(:pipeline_seed_merge_variables) do
::Gitlab::Ci::Variables::Helpers.merge_variables(
- @command.yaml_processor_result.root_variables, @command.workflow_rules_result.variables
+ @command.yaml_processor_result.root_variables,
+ @command.workflow_rules_result.variables
)
end
end
diff --git a/lib/gitlab/ci/pipeline/logger.rb b/lib/gitlab/ci/pipeline/logger.rb
index 97f7dddd09a..fbba12c11a9 100644
--- a/lib/gitlab/ci/pipeline/logger.rb
+++ b/lib/gitlab/ci/pipeline/logger.rb
@@ -37,6 +37,16 @@ module Gitlab
result
end
+ def instrument_with_sql(operation, &block)
+ op_start_db_counters = current_db_counter_payload
+
+ result = instrument(operation, &block)
+
+ observe_sql_counters(operation, op_start_db_counters, current_db_counter_payload)
+
+ result
+ end
+
def observe(operation, value)
return unless enabled?
@@ -50,11 +60,20 @@ module Gitlab
class: self.class.name.to_s,
pipeline_creation_caller: caller,
project_id: project.id,
- pipeline_id: pipeline.id,
pipeline_persisted: pipeline.persisted?,
pipeline_source: pipeline.source,
pipeline_creation_service_duration_s: age
- }.stringify_keys.merge(observations_hash)
+ }
+
+ if pipeline.persisted?
+ attributes[:pipeline_builds_tags_count] = pipeline.tags_count
+ attributes[:pipeline_builds_distinct_tags_count] = pipeline.distinct_tags_count
+ attributes[:pipeline_id] = pipeline.id
+ end
+
+ attributes.compact!
+ attributes.stringify_keys!
+ attributes.merge!(observations_hash)
destination.info(attributes)
end
@@ -97,6 +116,19 @@ module Gitlab
def observations
@observations ||= Hash.new { |hash, key| hash[key] = [] }
end
+
+ def observe_sql_counters(operation, start_db_counters, end_db_counters)
+ end_db_counters.each do |key, value|
+ result = value - start_db_counters.fetch(key, 0)
+ next if result == 0
+
+ observe("#{operation}_#{key}", result)
+ end
+ end
+
+ def current_db_counter_payload
+ ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 762292f0fa3..5a0ad695741 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -41,12 +41,14 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
- if @using_rules
- rules_result.pass?
- elsif @using_only || @using_except
- all_of_only? && none_of_except?
- else
- true
+ logger.instrument(:pipeline_seed_build_inclusion) do
+ if @using_rules
+ rules_result.pass?
+ elsif @using_only || @using_except
+ all_of_only? && none_of_except?
+ else
+ true
+ end
end
end
end
@@ -122,6 +124,8 @@ module Gitlab
private
+ delegate :logger, to: :@context
+
def all_of_only?
@only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
diff --git a/lib/gitlab/ci/pipeline/seed/context.rb b/lib/gitlab/ci/pipeline/seed/context.rb
index 6194a78f682..c0b8ebeb833 100644
--- a/lib/gitlab/ci/pipeline/seed/context.rb
+++ b/lib/gitlab/ci/pipeline/seed/context.rb
@@ -5,11 +5,18 @@ module Gitlab
module Pipeline
module Seed
class Context
- attr_reader :pipeline, :root_variables
+ attr_reader :pipeline, :root_variables, :logger
- def initialize(pipeline, root_variables: [])
+ def initialize(pipeline, root_variables: [], logger: nil)
@pipeline = pipeline
@root_variables = root_variables
+ @logger = logger || build_logger
+ end
+
+ private
+
+ def build_logger
+ ::Gitlab::Ci::Pipeline::Logger.new(project: pipeline.project)
end
end
end
diff --git a/lib/gitlab/ci/queue/metrics.rb b/lib/gitlab/ci/queue/metrics.rb
index 7f45d626922..54fb1d19ea8 100644
--- a/lib/gitlab/ci/queue/metrics.rb
+++ b/lib/gitlab/ci/queue/metrics.rb
@@ -69,17 +69,6 @@ module Gitlab
self.class.attempt_counter.increment
end
- # rubocop: disable CodeReuse/ActiveRecord
- def jobs_running_for_project(job)
- return '+Inf' unless runner.instance_type?
-
- # excluding currently started job
- running_jobs_count = job.project.builds.running.where(runner: ::Ci::Runner.instance_type)
- .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
- running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def increment_queue_operation(operation)
self.class.increment_queue_operation(operation)
end
@@ -242,6 +231,32 @@ module Gitlab
Gitlab::Metrics.histogram(name, comment, labels, buckets)
end
end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def jobs_running_for_project(job)
+ return '+Inf' unless runner.instance_type?
+
+ # excluding currently started job
+ running_jobs_count = running_jobs_relation(job)
+ .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
+
+ if running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET
+ running_jobs_count
+ else
+ "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
+ end
+ end
+
+ def running_jobs_relation(job)
+ if ::Feature.enabled?(:ci_pending_builds_maintain_denormalized_data, default_enabled: :yaml)
+ ::Ci::RunningBuild.instance_type.where(project_id: job.project_id)
+ else
+ job.project.builds.running.where(runner: ::Ci::Runner.instance_type)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index 7e5afbad806..a4434e2c144 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -14,7 +14,8 @@ module Gitlab
Status::Build::WaitingForResource,
Status::Build::Preparing,
Status::Build::Pending,
- Status::Build::Skipped],
+ Status::Build::Skipped,
+ Status::Build::WaitingForApproval],
[Status::Build::Cancelable,
Status::Build::Retryable],
[Status::Build::FailedUnmetPrerequisites,
diff --git a/lib/gitlab/ci/status/build/waiting_for_approval.rb b/lib/gitlab/ci/status/build/waiting_for_approval.rb
new file mode 100644
index 00000000000..59869a947a9
--- /dev/null
+++ b/lib/gitlab/ci/status/build/waiting_for_approval.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class WaitingForApproval < Status::Extended
+ def illustration
+ {
+ image: 'illustrations/manual_action.svg',
+ size: 'svg-394',
+ title: 'Waiting for approval',
+ content: "This job deploys to the protected environment \"#{subject.deployment&.environment&.name}\" which requires approvals. Use the Deployments API to approve or reject the deployment."
+ }
+ end
+
+ def self.matches?(build, user)
+ build.waiting_for_deployment_approval?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/tags/bulk_insert.rb b/lib/gitlab/ci/tags/bulk_insert.rb
index a299df7e2d9..29f3731a9b4 100644
--- a/lib/gitlab/ci/tags/bulk_insert.rb
+++ b/lib/gitlab/ci/tags/bulk_insert.rb
@@ -4,12 +4,13 @@ module Gitlab
module Ci
module Tags
class BulkInsert
+ include Gitlab::Utils::StrongMemoize
+
TAGGINGS_BATCH_SIZE = 1000
TAGS_BATCH_SIZE = 500
- def initialize(statuses, tag_list_by_status)
+ def initialize(statuses)
@statuses = statuses
- @tag_list_by_status = tag_list_by_status
end
def insert!
@@ -20,7 +21,18 @@ module Gitlab
private
- attr_reader :statuses, :tag_list_by_status
+ attr_reader :statuses
+
+ def tag_list_by_status
+ strong_memoize(:tag_list_by_status) do
+ statuses.each.with_object({}) do |status, acc|
+ tag_list = status.tag_list
+ next unless tag_list
+
+ acc[status] = tag_list
+ end
+ end
+ end
def persist_build_tags!
all_tags = tag_list_by_status.values.flatten.uniq.reject(&:blank?)
@@ -54,7 +66,7 @@ module Gitlab
def build_taggings_attributes(tag_records_by_name)
taggings = statuses.flat_map do |status|
- tag_list = tag_list_by_status[status.name]
+ tag_list = tag_list_by_status[status]
next unless tag_list
tags = tag_records_by_name.values_at(*tag_list)
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 00b771f1e5c..6942631a97f 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -7,7 +7,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
- CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.24-gitlab.1"
+ CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.26"
needs: []
script:
- export SOURCE_CODE=$PWD
diff --git a/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
index 18f0f20203d..42487cc0c67 100644
--- a/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
@@ -14,6 +14,8 @@ variables:
image: "$SECURE_ANALYZERS_PREFIX/secrets:$SECRETS_ANALYZER_VERSION"
services: []
allow_failure: true
+ variables:
+ GIT_DEPTH: "50"
# `rules` must be overridden explicitly by each child job
# see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
artifacts:
@@ -29,8 +31,16 @@ secret_detection:
script:
- if [ -n "$CI_COMMIT_TAG" ]; then echo "Skipping Secret Detection for tags. No code changes have occurred."; exit 0; fi
- if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then echo "Running Secret Detection on default branch."; /analyzer run; exit 0; fi
- - git fetch origin $CI_DEFAULT_BRANCH $CI_COMMIT_REF_NAME
- - git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_COMMIT_REF_NAME > "$CI_COMMIT_SHA"_commit_list.txt
- - export SECRET_DETECTION_COMMITS_FILE="$CI_COMMIT_SHA"_commit_list.txt
+ - |
+ git fetch origin $CI_DEFAULT_BRANCH $CI_COMMIT_REF_NAME
+ git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/${CI_DEFAULT_BRANCH}..refs/remotes/origin/${CI_COMMIT_REF_NAME} >${CI_COMMIT_SHA}_commit_list.txt
+ if [[ $(wc -l <${CI_COMMIT_SHA}_commit_list.txt) -eq "0" ]]; then
+ # if git log produces 0 or 1 commits we should scan $CI_COMMIT_SHA only
+ export SECRET_DETECTION_COMMITS=$CI_COMMIT_SHA
+ else
+ # +1 because busybox wc only counts \n and there is no trailing \n
+ echo "scanning $(($(wc -l <${CI_COMMIT_SHA}_commit_list.txt) + 1)) commits"
+ export SECRET_DETECTION_COMMITS_FILE=${CI_COMMIT_SHA}_commit_list.txt
+ fi
- /analyzer run
- rm "$CI_COMMIT_SHA"_commit_list.txt
diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
index 0c8b98dc1cf..1660a9250e3 100644
--- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
@@ -29,7 +29,7 @@ before_script:
- ruby -v # Print out ruby version for debugging
# Uncomment next line if your rails app needs a JS runtime:
# - apt-get update -q && apt-get install nodejs -yqq
- - bundle config set path 'vendor' # Install dependencies into ./vendor/ruby
+ - bundle config set path 'vendor' # Install dependencies into ./vendor/ruby
- bundle install -j $(nproc)
# Optional - Delete if not using `rubocop`
diff --git a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
index 7243f240eed..f7f016b5e57 100644
--- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
@@ -33,6 +33,7 @@ coverage_fuzzing_unlicensed:
before_script:
- export COVFUZZ_JOB_TOKEN=$CI_JOB_TOKEN
- export COVFUZZ_PRIVATE_TOKEN=$CI_PRIVATE_TOKEN
+ - export COVFUZZ_PROJECT_PATH=$CI_PROJECT_PATH
- export COVFUZZ_PROJECT_ID=$CI_PROJECT_ID
- if [ -x "$(command -v apt-get)" ] ; then apt-get update && apt-get install -y wget; fi
- wget -O gitlab-cov-fuzz "${COVFUZZ_URL_PREFIX}"/"${COVFUZZ_VERSION}"/binaries/gitlab-cov-fuzz_Linux_x86_64
diff --git a/lib/gitlab/ci/templates/Security/DAST-On-Demand-API-Scan.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST-On-Demand-API-Scan.gitlab-ci.yml
new file mode 100644
index 00000000000..6888e955467
--- /dev/null
+++ b/lib/gitlab/ci/templates/Security/DAST-On-Demand-API-Scan.gitlab-ci.yml
@@ -0,0 +1,27 @@
+stages:
+ - build
+ - test
+ - deploy
+ - dast
+
+variables:
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+ DAST_API_VERSION: "1"
+ DAST_API_IMAGE: $SECURE_ANALYZERS_PREFIX/api-fuzzing:$DAST_API_VERSION
+
+dast:
+ stage: dast
+ image: $DAST_API_IMAGE
+ variables:
+ GIT_STRATEGY: none
+ allow_failure: true
+ script:
+ - /peach/analyzer-dast-api
+ artifacts:
+ when: always
+ paths:
+ - gl-assets
+ - gl-dast-api-report.json
+ - gl-*.log
+ reports:
+ dast: gl-dast-api-report.json
diff --git a/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
index e554742735c..12c987a8d37 100644
--- a/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
@@ -5,9 +5,11 @@
include:
- template: Terraform/Base.latest.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
+ - template: Jobs/SAST-IaC.latest.gitlab-ci.yml
stages:
- validate
+ - test
- build
- deploy
diff --git a/lib/gitlab/ci/trace/remote_checksum.rb b/lib/gitlab/ci/trace/remote_checksum.rb
index d57f3888ec0..7f43d91e6d7 100644
--- a/lib/gitlab/ci/trace/remote_checksum.rb
+++ b/lib/gitlab/ci/trace/remote_checksum.rb
@@ -26,7 +26,6 @@ module Gitlab
delegate :aws?, :google?, to: :object_store_config, prefix: :provider
def fetch_md5_checksum
- return unless Feature.enabled?(:ci_archived_build_trace_checksum, trace_artifact.project, default_enabled: :yaml)
return unless object_store_config.enabled?
return if trace_artifact.local_store?
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 2d31049a0c9..dd435ba05b7 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -11,10 +11,6 @@ module Gitlab
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
- delegate :valid?, to: :stream, allow_nil: true
-
- alias_method :present?, :valid?
-
def initialize(metrics = Trace::Metrics.new)
@stream = yield
@stream&.binmode
@@ -24,6 +20,7 @@ module Gitlab
def valid?
self.stream.present?
end
+ alias_method :present?, :valid?
def file?
self.path.present?
diff --git a/lib/gitlab/ci/variables/builder.rb b/lib/gitlab/ci/variables/builder.rb
index 3e2c2c7fc1a..4c777527ebc 100644
--- a/lib/gitlab/ci/variables/builder.rb
+++ b/lib/gitlab/ci/variables/builder.rb
@@ -13,12 +13,76 @@ module Gitlab
def scoped_variables(job, environment:, dependencies:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables(job))
+
+ next variables unless pipeline.use_variables_builder_definitions?
+
+ variables.concat(project.predefined_variables)
+ variables.concat(pipeline.predefined_variables)
+ variables.concat(job.runner.predefined_variables) if job.runnable? && job.runner
+ variables.concat(kubernetes_variables(job))
+ variables.concat(deployment_variables(environment: environment, job: job))
+ variables.concat(job.yaml_variables)
+ variables.concat(user_variables(job.user))
+ variables.concat(job.dependency_variables) if dependencies
+ variables.concat(secret_instance_variables(ref: job.git_ref))
+ variables.concat(secret_group_variables(environment: environment, ref: job.git_ref))
+ variables.concat(secret_project_variables(environment: environment, ref: job.git_ref))
+ variables.concat(job.trigger_request.user_variables) if job.trigger_request
+ variables.concat(pipeline.variables)
+ variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
+ end
+ end
+
+ def kubernetes_variables(job)
+ ::Gitlab::Ci::Variables::Collection.new.tap do |collection|
+ # Should get merged with the cluster kubeconfig in deployment_variables, see
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/335089
+ template = ::Ci::GenerateKubeconfigService.new(job).execute
+
+ if template.valid?
+ collection.append(key: 'KUBECONFIG', value: template.to_yaml, public: false, file: true)
+ end
end
end
+ def deployment_variables(environment:, job:)
+ return [] unless environment
+
+ project.deployment_variables(
+ environment: environment,
+ kubernetes_namespace: job.expanded_kubernetes_namespace
+ )
+ end
+
+ def user_variables(user)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables if user.blank?
+
+ variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
+ variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
+ variables.append(key: 'GITLAB_USER_LOGIN', value: user.username)
+ variables.append(key: 'GITLAB_USER_NAME', value: user.name)
+ end
+ end
+
+ def secret_instance_variables(ref:)
+ project.ci_instance_variables_for(ref: ref)
+ end
+
+ def secret_group_variables(environment:, ref:)
+ return [] unless project.group
+
+ project.group.ci_variables_for(ref, project, environment: environment)
+ end
+
+ def secret_project_variables(environment:, ref:)
+ project.ci_variables_for(ref: ref, environment: environment)
+ end
+
private
attr_reader :pipeline
+ delegate :project, to: :pipeline
def predefined_variables(job)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 296b0cfded2..553508c8638 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -86,11 +86,19 @@ module Gitlab
def validate_job_needs!(name, job)
return unless needs = job.dig(:needs, :job)
+ validate_duplicate_needs!(name, needs)
+
needs.each do |need|
validate_job_dependency!(name, need[:name], 'need')
end
end
+ def validate_duplicate_needs!(name, needs)
+ unless needs.uniq == needs
+ error!("#{name} has duplicate entries in the needs section.")
+ end
+ end
+
def validate_job_dependency!(name, dependency, dependency_type = 'dependency')
unless @jobs[dependency.to_sym]
error!("#{name} job: undefined #{dependency_type}: #{dependency}")
diff --git a/lib/gitlab/color_schemes.rb b/lib/gitlab/color_schemes.rb
index 620b4a8aee6..3884f5f0428 100644
--- a/lib/gitlab/color_schemes.rb
+++ b/lib/gitlab/color_schemes.rb
@@ -7,21 +7,23 @@ module Gitlab
# Struct class representing a single Scheme
Scheme = Struct.new(:id, :name, :css_class)
- SCHEMES = [
- Scheme.new(1, 'White', 'white'),
- Scheme.new(2, 'Dark', 'dark'),
- Scheme.new(3, 'Solarized Light', 'solarized-light'),
- Scheme.new(4, 'Solarized Dark', 'solarized-dark'),
- Scheme.new(5, 'Monokai', 'monokai'),
- Scheme.new(6, 'None', 'none')
- ].freeze
+ def self.available_schemes
+ [
+ Scheme.new(1, s_('SynthaxHighlightingTheme|Light'), 'white'),
+ Scheme.new(2, s_('SynthaxHighlightingTheme|Dark'), 'dark'),
+ Scheme.new(3, s_('SynthaxHighlightingTheme|Solarized Light'), 'solarized-light'),
+ Scheme.new(4, s_('SynthaxHighlightingTheme|Solarized Dark'), 'solarized-dark'),
+ Scheme.new(5, s_('SynthaxHighlightingTheme|Monokai'), 'monokai'),
+ Scheme.new(6, s_('SynthaxHighlightingTheme|None'), 'none')
+ ]
+ end
# Convenience method to get a space-separated String of all the color scheme
# classes that might be applied to a code block.
#
# Returns a String
def self.body_classes
- SCHEMES.collect(&:css_class).uniq.join(' ')
+ available_schemes.collect(&:css_class).uniq.join(' ')
end
# Get a Scheme by its ID
@@ -32,12 +34,12 @@ module Gitlab
#
# Returns a Scheme
def self.by_id(id)
- SCHEMES.detect { |s| s.id == id } || default
+ available_schemes.detect { |s| s.id == id } || default
end
# Returns the number of defined Schemes
def self.count
- SCHEMES.size
+ available_schemes.size
end
# Get the default Scheme
@@ -51,7 +53,7 @@ module Gitlab
#
# Yields the Scheme object
def self.each(&block)
- SCHEMES.each(&block)
+ available_schemes.each(&block)
end
# Get the Scheme for the specified user, or the default
@@ -68,7 +70,7 @@ module Gitlab
end
def self.valid_ids
- SCHEMES.map(&:id)
+ available_schemes.map(&:id)
end
end
end
diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb
index 6bf77ebaa5b..aa6c724c2a3 100644
--- a/lib/gitlab/config/entry/configurable.rb
+++ b/lib/gitlab/config/entry/configurable.rb
@@ -76,7 +76,7 @@ module Gitlab
private
# rubocop: disable CodeReuse/ActiveRecord
- def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil, metadata: {})
+ def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil, deprecation: nil, metadata: {})
entry_name = key.to_sym
raise ArgumentError, "Entry '#{key}' already defined in '#{name}'" if @nodes.to_h[entry_name]
@@ -85,6 +85,7 @@ module Gitlab
.with(default: default)
.with(inherit: inherit)
.with(reserved: reserved)
+ .with(deprecation: deprecation)
.metadata(metadata)
@nodes ||= {}
diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb
index f76c98f7cbf..61f2071b62f 100644
--- a/lib/gitlab/config/entry/factory.rb
+++ b/lib/gitlab/config/entry/factory.rb
@@ -32,6 +32,10 @@ module Gitlab
self
end
+ def deprecation
+ @attributes[:deprecation]
+ end
+
def description
@attributes[:description]
end
@@ -84,6 +88,7 @@ module Gitlab
node.parent = @attributes[:parent]
node.default = @attributes[:default]
node.description = @attributes[:description]
+ node.deprecation = @attributes[:deprecation]
end
end
end
diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb
index 32912cb1046..6ce7046262b 100644
--- a/lib/gitlab/config/entry/node.rb
+++ b/lib/gitlab/config/entry/node.rb
@@ -10,7 +10,7 @@ module Gitlab
InvalidError = Class.new(StandardError)
attr_reader :config, :metadata
- attr_accessor :key, :parent, :default, :description
+ attr_accessor :key, :parent, :default, :description, :deprecation
def initialize(config, **metadata)
@config = config
@@ -128,6 +128,24 @@ module Gitlab
private
attr_reader :entries
+
+ def log_and_warn_deprecated_entry(entry)
+ user = metadata[:user]
+ project = metadata[:project]
+
+ if project && user
+ Gitlab::AppJsonLogger.info(event: 'ci_used_deprecated_keyword',
+ entry: entry.key.to_s,
+ user_id: user.id,
+ project_id: project.id)
+ end
+
+ deprecation = entry.deprecation
+ add_warning(
+ "`#{entry.key}` is deprecated in " \
+ "#{deprecation[:deprecated]} and will be removed in #{deprecation[:removed]}."
+ )
+ end
end
end
end
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index 87bc2ace204..78ba0916808 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -147,7 +147,7 @@ module Gitlab
# Using 'self' in the CSP introduces several CSP bypass opportunities
# for this reason we list the URLs where GitLab frames itself instead
def self.allow_framed_gitlab_paths(directives)
- ['/admin/', '/assets/', '/-/speedscope/index.html'].map do |path|
+ ['/admin/', '/assets/', '/-/speedscope/index.html', '/-/sandbox/mermaid'].map do |path|
append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path))
end
end
diff --git a/lib/gitlab/data_builder/archive_trace.rb b/lib/gitlab/data_builder/archive_trace.rb
new file mode 100644
index 00000000000..f6dd6130104
--- /dev/null
+++ b/lib/gitlab/data_builder/archive_trace.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DataBuilder
+ module ArchiveTrace
+ extend self
+
+ def build(job)
+ {
+ object_kind: 'archive_trace',
+ trace_url: job.job_artifacts_trace.file.url,
+ build_id: job.id,
+ pipeline_id: job.pipeline_id,
+ project: job.project.hook_attrs
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb
index 267c2d32ca9..a4508bc93c5 100644
--- a/lib/gitlab/data_builder/deployment.rb
+++ b/lib/gitlab/data_builder/deployment.rb
@@ -25,7 +25,8 @@ module Gitlab
user: deployment.deployed_by.hook_attrs,
user_url: Gitlab::UrlBuilder.build(deployment.deployed_by),
commit_url: Gitlab::UrlBuilder.build(deployment.commit),
- commit_title: deployment.commit.title
+ commit_title: deployment.commit.title,
+ ref: deployment.ref
}
end
end
diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb
index 503172dd750..290fa51692a 100644
--- a/lib/gitlab/database/background_migration/batched_job.rb
+++ b/lib/gitlab/database/background_migration/batched_job.rb
@@ -12,17 +12,6 @@ module Gitlab
MAX_ATTEMPTS = 3
STUCK_JOBS_TIMEOUT = 1.hour.freeze
- belongs_to :batched_migration, foreign_key: :batched_background_migration_id
-
- scope :active, -> { where(status: [:pending, :running]) }
- scope :stuck, -> { active.where('updated_at <= ?', STUCK_JOBS_TIMEOUT.ago) }
- scope :retriable, -> {
- failed_jobs = where(status: :failed).where('attempts < ?', MAX_ATTEMPTS)
-
- from_union([failed_jobs, self.stuck])
- }
- scope :except_succeeded, -> { where(status: self.statuses.except(:succeeded).values) }
-
enum status: {
pending: 0,
running: 1,
@@ -30,7 +19,14 @@ module Gitlab
succeeded: 3
}
+ belongs_to :batched_migration, foreign_key: :batched_background_migration_id
+
+ scope :active, -> { where(status: [:pending, :running]) }
+ scope :stuck, -> { active.where('updated_at <= ?', STUCK_JOBS_TIMEOUT.ago) }
+ scope :retriable, -> { from_union([failed.where('attempts < ?', MAX_ATTEMPTS), self.stuck]) }
+ scope :except_succeeded, -> { where(status: self.statuses.except(:succeeded).values) }
scope :successful_in_execution_order, -> { where.not(finished_at: nil).succeeded.order(:finished_at) }
+ scope :with_preloads, -> { preload(:batched_migration) }
delegate :job_class, :table_name, :column_name, :job_arguments,
to: :batched_migration, prefix: :migration
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
index 2844cbe4a74..2f066039874 100644
--- a/lib/gitlab/database/background_migration/batched_migration.rb
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -113,7 +113,7 @@ module Gitlab
end
def smoothed_time_efficiency(number_of_jobs: 10, alpha: 0.2)
- jobs = batched_jobs.successful_in_execution_order.reverse_order.limit(number_of_jobs)
+ jobs = batched_jobs.successful_in_execution_order.reverse_order.limit(number_of_jobs).with_preloads
return if jobs.size < number_of_jobs
diff --git a/lib/gitlab/database/background_migration_job.rb b/lib/gitlab/database/background_migration_job.rb
index c046571a111..c0e3016fd3d 100644
--- a/lib/gitlab/database/background_migration_job.rb
+++ b/lib/gitlab/database/background_migration_job.rb
@@ -2,7 +2,7 @@
module Gitlab
module Database
- class BackgroundMigrationJob < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
+ class BackgroundMigrationJob < SharedModel
include EachBatch
include BulkInsertSafe
diff --git a/lib/gitlab/database/batch_counter.rb b/lib/gitlab/database/batch_counter.rb
index 6c0ce9e481a..417511618e4 100644
--- a/lib/gitlab/database/batch_counter.rb
+++ b/lib/gitlab/database/batch_counter.rb
@@ -52,12 +52,7 @@ module Gitlab
batch_end = [batch_start + batch_size, finish].min
batch_relation = build_relation_batch(batch_start, batch_end, mode)
- op_args = @operation_args
- if @operation == :count && @operation_args.blank? && use_loose_index_scan_for_distinct_values?(mode)
- op_args = [Gitlab::Database::LooseIndexScanDistinctCount::COLUMN_ALIAS]
- end
-
- results = merge_results(results, batch_relation.send(@operation, *op_args)) # rubocop:disable GitlabSecurity/PublicSend
+ results = merge_results(results, batch_relation.send(@operation, *@operation_args)) # rubocop:disable GitlabSecurity/PublicSend
batch_start = batch_end
rescue ActiveRecord::QueryCanceled => error
# retry with a safe batch size & warmer cache
@@ -67,18 +62,6 @@ module Gitlab
log_canceled_batch_fetch(batch_start, mode, batch_relation.to_sql, error)
return FALLBACK
end
- rescue Gitlab::Database::LooseIndexScanDistinctCount::ColumnConfigurationError => error
- Gitlab::AppJsonLogger
- .error(
- event: 'batch_count',
- relation: @relation.table_name,
- operation: @operation,
- operation_args: @operation_args,
- mode: mode,
- message: "LooseIndexScanDistinctCount column error: #{error.message}"
- )
-
- return FALLBACK
end
sleep(SLEEP_TIME_IN_SECONDS)
@@ -104,11 +87,7 @@ module Gitlab
private
def build_relation_batch(start, finish, mode)
- if use_loose_index_scan_for_distinct_values?(mode)
- Gitlab::Database::LooseIndexScanDistinctCount.new(@relation, @column).build_query(from: start, to: finish)
- else
- @relation.select(@column).public_send(mode).where(between_condition(start, finish)) # rubocop:disable GitlabSecurity/PublicSend
- end
+ @relation.select(@column).public_send(mode).where(between_condition(start, finish)) # rubocop:disable GitlabSecurity/PublicSend
end
def batch_size_for_mode_and_operation(mode, operation)
@@ -151,10 +130,6 @@ module Gitlab
)
end
- def use_loose_index_scan_for_distinct_values?(mode)
- Feature.enabled?(:loose_index_scan_for_distinct_values) && not_group_by_query? && mode == :distinct
- end
-
def not_group_by_query?
!@relation.is_a?(ActiveRecord::Relation) || @relation.group_values.blank?
end
diff --git a/lib/gitlab/database/gitlab_loose_foreign_keys.yml b/lib/gitlab/database/gitlab_loose_foreign_keys.yml
index 0343c054f23..d694165574d 100644
--- a/lib/gitlab/database/gitlab_loose_foreign_keys.yml
+++ b/lib/gitlab/database/gitlab_loose_foreign_keys.yml
@@ -1,3 +1,12 @@
+---
+dast_site_profiles_pipelines:
+ - table: ci_pipelines
+ column: ci_pipeline_id
+ on_delete: async_delete
+vulnerability_feedback:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_nullify
ci_pipeline_chat_data:
- table: chat_names
column: chat_name_id
@@ -6,7 +15,7 @@ dast_scanner_profiles_builds:
- table: ci_builds
column: ci_build_id
on_delete: async_delete
-dast_scanner_profiles_builds:
+dast_site_profiles_builds:
- table: ci_builds
column: ci_build_id
on_delete: async_delete
@@ -18,10 +27,48 @@ clusters_applications_runners:
- table: ci_runners
column: runner_id
on_delete: async_nullify
+ci_job_token_project_scope_links:
+ - table: users
+ column: added_by_id
+ on_delete: async_nullify
+ci_daily_build_group_report_results:
+ - table: namespaces
+ column: group_id
+ on_delete: async_delete
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_freeze_periods:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_pending_builds:
+ - table: namespaces
+ column: namespace_id
+ on_delete: async_delete
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_resource_groups:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_runner_namespaces:
+ - table: namespaces
+ column: namespace_id
+ on_delete: async_delete
+ci_running_builds:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
ci_namespace_mirrors:
- table: namespaces
column: namespace_id
on_delete: async_delete
+ci_build_report_results:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
ci_builds:
- table: users
column: user_id
@@ -43,6 +90,22 @@ ci_project_mirrors:
- table: namespaces
column: namespace_id
on_delete: async_delete
+ci_unit_tests:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+merge_requests:
+ - table: ci_pipelines
+ column: head_pipeline_id
+ on_delete: async_nullify
+vulnerability_statistics:
+ - table: ci_pipelines
+ column: latest_pipeline_id
+ on_delete: async_nullify
+vulnerability_occurrence_pipelines:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_delete
packages_build_infos:
- table: ci_pipelines
column: pipeline_id
@@ -67,3 +130,31 @@ project_pages_metadata:
- table: ci_job_artifacts
column: artifacts_archive_id
on_delete: async_nullify
+ci_pipeline_schedules:
+ - table: users
+ column: owner_id
+ on_delete: async_nullify
+ci_group_variables:
+ - table: namespaces
+ column: group_id
+ on_delete: async_delete
+ci_minutes_additional_packs:
+ - table: namespaces
+ column: namespace_id
+ on_delete: async_delete
+requirements_management_test_reports:
+ - table: ci_builds
+ column: build_id
+ on_delete: async_nullify
+security_scans:
+ - table: ci_builds
+ column: build_id
+ on_delete: async_delete
+ci_secure_files:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_pipeline_artifacts:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index 24c2d634780..fb5d8cfa32f 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -107,6 +107,7 @@ ci_runner_projects: :gitlab_ci
ci_runners: :gitlab_ci
ci_running_builds: :gitlab_ci
ci_sources_pipelines: :gitlab_ci
+ci_secure_files: :gitlab_ci
ci_sources_projects: :gitlab_ci
ci_stages: :gitlab_ci
ci_subscriptions_projects: :gitlab_ci
@@ -200,7 +201,7 @@ experiment_subjects: :gitlab_main
experiment_users: :gitlab_main
external_approval_rules: :gitlab_main
external_approval_rules_protected_branches: :gitlab_main
-external_pull_requests: :gitlab_main
+external_pull_requests: :gitlab_ci
external_status_checks: :gitlab_main
external_status_checks_protected_branches: :gitlab_main
feature_gates: :gitlab_main
@@ -231,6 +232,7 @@ gpg_key_subkeys: :gitlab_main
gpg_signatures: :gitlab_main
grafana_integrations: :gitlab_main
group_custom_attributes: :gitlab_main
+group_crm_settings: :gitlab_main
group_deletion_schedules: :gitlab_main
group_deploy_keys: :gitlab_main
group_deploy_keys_groups: :gitlab_main
@@ -460,6 +462,8 @@ security_findings: :gitlab_main
security_orchestration_policy_configurations: :gitlab_main
security_orchestration_policy_rule_schedules: :gitlab_main
security_scans: :gitlab_main
+security_training_providers: :gitlab_main
+security_trainings: :gitlab_main
self_managed_prometheus_alert_events: :gitlab_main
sent_notifications: :gitlab_main
sentry_issues: :gitlab_main
@@ -521,13 +525,7 @@ vulnerabilities: :gitlab_main
vulnerability_exports: :gitlab_main
vulnerability_external_issue_links: :gitlab_main
vulnerability_feedback: :gitlab_main
-vulnerability_finding_evidence_assets: :gitlab_main
-vulnerability_finding_evidence_headers: :gitlab_main
-vulnerability_finding_evidence_requests: :gitlab_main
-vulnerability_finding_evidence_responses: :gitlab_main
vulnerability_finding_evidences: :gitlab_main
-vulnerability_finding_evidence_sources: :gitlab_main
-vulnerability_finding_evidence_supporting_messages: :gitlab_main
vulnerability_finding_links: :gitlab_main
vulnerability_finding_signatures: :gitlab_main
vulnerability_findings_remediations: :gitlab_main
diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb
index c8a30c68bc6..0093848ee6f 100644
--- a/lib/gitlab/database/grant.rb
+++ b/lib/gitlab/database/grant.rb
@@ -10,7 +10,7 @@ module Gitlab
# We _must not_ use quote_table_name as this will produce double
# quotes on PostgreSQL and for "has_table_privilege" we need single
# quotes.
- connection = ActiveRecord::Base.connection # rubocop: disable Database/MultipleDatabases
+ connection = ApplicationRecord.connection
quoted_table = connection.quote(table)
begin
diff --git a/lib/gitlab/database/load_balancing/setup.rb b/lib/gitlab/database/load_balancing/setup.rb
index ef38f42f50b..126c8bb2aa6 100644
--- a/lib/gitlab/database/load_balancing/setup.rb
+++ b/lib/gitlab/database/load_balancing/setup.rb
@@ -104,11 +104,9 @@ module Gitlab
end
end
- # rubocop:disable Database/MultipleDatabases
def connection
- use_model_load_balancing? ? super : ActiveRecord::Base.connection
+ use_model_load_balancing? ? super : ApplicationRecord.connection
end
- # rubocop:enable Database/MultipleDatabases
end
end
end
diff --git a/lib/gitlab/database/loose_index_scan_distinct_count.rb b/lib/gitlab/database/loose_index_scan_distinct_count.rb
deleted file mode 100644
index 26be07f91c4..00000000000
--- a/lib/gitlab/database/loose_index_scan_distinct_count.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- # This class builds efficient batched distinct query by using loose index scan.
- # Consider the following example:
- # > Issue.distinct(:project_id).where(project_id: (1...100)).count
- #
- # Note: there is an index on project_id
- #
- # This query will read each element in the index matching the project_id filter.
- # If for a project_id has 100_000 issues, all 100_000 elements will be read.
- #
- # A loose index scan will only read one entry from the index for each project_id to reduce the number of disk reads.
- #
- # Usage:
- #
- # Gitlab::Database::LooseIndexScanDisctinctCount.new(Issue, :project_id).count(from: 1, to: 100)
- #
- # The query will return the number of distinct projects_ids between 1 and 100
- #
- # Getting the Arel query:
- #
- # Gitlab::Database::LooseIndexScanDisctinctCount.new(Issue, :project_id).build_query(from: 1, to: 100)
- class LooseIndexScanDistinctCount
- COLUMN_ALIAS = 'distinct_count_column'
-
- ColumnConfigurationError = Class.new(StandardError)
-
- def initialize(scope, column)
- if scope.is_a?(ActiveRecord::Relation)
- @scope = scope
- @model = scope.model
- else
- @scope = scope.where({})
- @model = scope
- end
-
- @column = transform_column(column)
- end
-
- def count(from:, to:)
- build_query(from: from, to: to).count(COLUMN_ALIAS)
- end
-
- def build_query(from:, to:) # rubocop:disable Metrics/AbcSize
- cte = Gitlab::SQL::RecursiveCTE.new(:counter_cte, union_args: { remove_order: false })
- table = model.arel_table
-
- cte << @scope
- .dup
- .select(column.as(COLUMN_ALIAS))
- .where(column.gteq(from))
- .where(column.lt(to))
- .order(column)
- .limit(1)
-
- inner_query = @scope
- .dup
- .where(column.gt(cte.table[COLUMN_ALIAS]))
- .where(column.lt(to))
- .select(column.as(COLUMN_ALIAS))
- .order(column)
- .limit(1)
-
- cte << cte.table
- .project(Arel::Nodes::Grouping.new(Arel.sql(inner_query.to_sql)).as(COLUMN_ALIAS))
- .where(cte.table[COLUMN_ALIAS].lt(to))
-
- model
- .with
- .recursive(cte.to_arel)
- .from(cte.alias_to(table))
- .unscope(where: :source_type)
- .unscope(where: model.inheritance_column) # Remove STI query, not needed here
- end
-
- private
-
- attr_reader :column, :model
-
- # Transforms the column so it can be used in Arel expressions
- #
- # 'table.column' => 'table.column'
- # 'column' => 'table_name.column'
- # :column => 'table_name.column'
- # Arel::Attributes::Attribute => name of the column
- def transform_column(column)
- if column.is_a?(String) || column.is_a?(Symbol)
- column_as_string = column.to_s
- column_as_string = "#{model.table_name}.#{column_as_string}" unless column_as_string.include?('.')
-
- Arel.sql(column_as_string)
- elsif column.is_a?(Arel::Attributes::Attribute)
- column
- else
- raise ColumnConfigurationError, "Cannot transform the column: #{column.inspect}, please provide the column name as string"
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 4245dd80714..aa5ac1e3486 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -778,186 +778,6 @@ module Gitlab
install_rename_triggers(table, old, new)
end
- # Changes the column type of a table using a background migration.
- #
- # Because this method uses a background migration it's more suitable for
- # large tables. For small tables it's better to use
- # `change_column_type_concurrently` since it can complete its work in a
- # much shorter amount of time and doesn't rely on Sidekiq.
- #
- # Example usage:
- #
- # class Issue < ActiveRecord::Base
- # self.table_name = 'issues'
- #
- # include EachBatch
- #
- # def self.to_migrate
- # where('closed_at IS NOT NULL')
- # end
- # end
- #
- # change_column_type_using_background_migration(
- # Issue.to_migrate,
- # :closed_at,
- # :datetime_with_timezone
- # )
- #
- # Reverting a migration like this is done exactly the same way, just with
- # a different type to migrate to (e.g. `:datetime` in the above example).
- #
- # relation - An ActiveRecord relation to use for scheduling jobs and
- # figuring out what table we're modifying. This relation _must_
- # have the EachBatch module included.
- #
- # column - The name of the column for which the type will be changed.
- #
- # new_type - The new type of the column.
- #
- # batch_size - The number of rows to schedule in a single background
- # migration.
- #
- # interval - The time interval between every background migration.
- def change_column_type_using_background_migration(
- relation,
- column,
- new_type,
- batch_size: 10_000,
- interval: 10.minutes
- )
-
- unless relation.model < EachBatch
- raise TypeError, 'The relation must include the EachBatch module'
- end
-
- temp_column = "#{column}_for_type_change"
- table = relation.table_name
- max_index = 0
-
- add_column(table, temp_column, new_type)
- install_rename_triggers(table, column, temp_column)
-
- # Schedule the jobs that will copy the data from the old column to the
- # new one. Rows with NULL values in our source column are skipped since
- # the target column is already NULL at this point.
- relation.where.not(column => nil).each_batch(of: batch_size) do |batch, index|
- start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
- max_index = index
-
- migrate_in(
- index * interval,
- 'CopyColumn',
- [table, column, temp_column, start_id, end_id]
- )
- end
-
- # Schedule the renaming of the column to happen (initially) 1 hour after
- # the last batch finished.
- migrate_in(
- (max_index * interval) + 1.hour,
- 'CleanupConcurrentTypeChange',
- [table, column, temp_column]
- )
-
- if perform_background_migration_inline?
- # To ensure the schema is up to date immediately we perform the
- # migration inline in dev / test environments.
- Gitlab::BackgroundMigration.steal('CopyColumn')
- Gitlab::BackgroundMigration.steal('CleanupConcurrentTypeChange')
- end
- end
-
- # Renames a column using a background migration.
- #
- # Because this method uses a background migration it's more suitable for
- # large tables. For small tables it's better to use
- # `rename_column_concurrently` since it can complete its work in a much
- # shorter amount of time and doesn't rely on Sidekiq.
- #
- # Example usage:
- #
- # rename_column_using_background_migration(
- # :users,
- # :feed_token,
- # :rss_token
- # )
- #
- # table - The name of the database table containing the column.
- #
- # old - The old column name.
- #
- # new - The new column name.
- #
- # type - The type of the new column. If no type is given the old column's
- # type is used.
- #
- # batch_size - The number of rows to schedule in a single background
- # migration.
- #
- # interval - The time interval between every background migration.
- def rename_column_using_background_migration(
- table,
- old_column,
- new_column,
- type: nil,
- batch_size: 10_000,
- interval: 10.minutes
- )
-
- check_trigger_permissions!(table)
-
- old_col = column_for(table, old_column)
- new_type = type || old_col.type
- max_index = 0
-
- add_column(table, new_column, new_type,
- limit: old_col.limit,
- precision: old_col.precision,
- scale: old_col.scale)
-
- # We set the default value _after_ adding the column so we don't end up
- # updating any existing data with the default value. This isn't
- # necessary since we copy over old values further down.
- change_column_default(table, new_column, old_col.default) if old_col.default
-
- install_rename_triggers(table, old_column, new_column)
-
- model = Class.new(ActiveRecord::Base) do
- self.table_name = table
-
- include ::EachBatch
- end
-
- # Schedule the jobs that will copy the data from the old column to the
- # new one. Rows with NULL values in our source column are skipped since
- # the target column is already NULL at this point.
- model.where.not(old_column => nil).each_batch(of: batch_size) do |batch, index|
- start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
- max_index = index
-
- migrate_in(
- index * interval,
- 'CopyColumn',
- [table, old_column, new_column, start_id, end_id]
- )
- end
-
- # Schedule the renaming of the column to happen (initially) 1 hour after
- # the last batch finished.
- migrate_in(
- (max_index * interval) + 1.hour,
- 'CleanupConcurrentRename',
- [table, old_column, new_column]
- )
-
- if perform_background_migration_inline?
- # To ensure the schema is up to date immediately we perform the
- # migration inline in dev / test environments.
- Gitlab::BackgroundMigration.steal('CopyColumn')
- Gitlab::BackgroundMigration.steal('CleanupConcurrentRename')
- end
- end
-
def convert_to_bigint_column(column)
"#{column}_convert_to_bigint"
end
diff --git a/lib/gitlab/database/migrations/background_migration_helpers.rb b/lib/gitlab/database/migrations/background_migration_helpers.rb
index 8c33c41ce77..4f1b490cc8f 100644
--- a/lib/gitlab/database/migrations/background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/background_migration_helpers.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
module Gitlab
module Database
module Migrations
@@ -45,11 +44,11 @@ module Gitlab
raise "#{model_class} does not have an ID column of #{primary_column_name} to use for batch ranges" unless model_class.column_names.include?(primary_column_name.to_s)
raise "#{primary_column_name} is not an integer column" unless model_class.columns_hash[primary_column_name.to_s].type == :integer
+ job_coordinator = coordinator_for_tracking_database
+
# To not overload the worker too much we enforce a minimum interval both
# when scheduling and performing jobs.
- if delay_interval < BackgroundMigrationWorker.minimum_interval
- delay_interval = BackgroundMigrationWorker.minimum_interval
- end
+ delay_interval = [delay_interval, job_coordinator.minimum_interval].max
final_delay = 0
batch_counter = 0
@@ -60,14 +59,14 @@ module Gitlab
start_id, end_id = relation.pluck(min, max).first
- # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
+ # `SingleDatabaseWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to
# spread the work over time.
final_delay = initial_delay + delay_interval * index
full_job_arguments = [start_id, end_id] + other_job_arguments
track_in_database(job_class_name, full_job_arguments) if track_jobs
- migrate_in(final_delay, job_class_name, full_job_arguments)
+ migrate_in(final_delay, job_class_name, full_job_arguments, coordinator: job_coordinator)
batch_counter += 1
end
@@ -91,9 +90,11 @@ module Gitlab
# delay_interval - The duration between each job's scheduled time
# batch_size - The maximum number of jobs to fetch to memory from the database.
def requeue_background_migration_jobs_by_range_at_intervals(job_class_name, delay_interval, batch_size: BATCH_SIZE, initial_delay: 0)
+ job_coordinator = coordinator_for_tracking_database
+
# To not overload the worker too much we enforce a minimum interval both
# when scheduling and performing jobs.
- delay_interval = [delay_interval, BackgroundMigrationWorker.minimum_interval].max
+ delay_interval = [delay_interval, job_coordinator.minimum_interval].max
final_delay = 0
job_counter = 0
@@ -103,7 +104,7 @@ module Gitlab
job_batch.each do |job|
final_delay = initial_delay + delay_interval * job_counter
- migrate_in(final_delay, job_class_name, job.arguments)
+ migrate_in(final_delay, job_class_name, job.arguments, coordinator: job_coordinator)
job_counter += 1
end
@@ -132,56 +133,33 @@ module Gitlab
# This method does not garauntee that all jobs completed successfully.
# It can only be used if the previous background migration used the queue_background_migration_jobs_by_range_at_intervals helper.
def finalize_background_migration(class_name, delete_tracking_jobs: ['succeeded'])
+ job_coordinator = coordinator_for_tracking_database
+
# Empty the sidekiq queue.
- Gitlab::BackgroundMigration.steal(class_name)
+ job_coordinator.steal(class_name)
# Process pending tracked jobs.
jobs = Gitlab::Database::BackgroundMigrationJob.pending.for_migration_class(class_name)
+
jobs.find_each do |job|
- BackgroundMigrationWorker.new.perform(job.class_name, job.arguments)
+ job_coordinator.perform(job.class_name, job.arguments)
end
# Empty the sidekiq queue.
- Gitlab::BackgroundMigration.steal(class_name)
+ job_coordinator.steal(class_name)
# Delete job tracking rows.
delete_job_tracking(class_name, status: delete_tracking_jobs) if delete_tracking_jobs
end
- def perform_background_migration_inline?
- Rails.env.test? || Rails.env.development?
- end
-
- def migrate_async(*args)
- with_migration_context do
- BackgroundMigrationWorker.perform_async(*args)
- end
- end
-
- def migrate_in(*args)
- with_migration_context do
- BackgroundMigrationWorker.perform_in(*args)
- end
- end
-
- def bulk_migrate_in(*args)
+ def migrate_in(*args, coordinator: coordinator_for_tracking_database)
with_migration_context do
- BackgroundMigrationWorker.bulk_perform_in(*args)
+ coordinator.perform_in(*args)
end
end
- def bulk_migrate_async(*args)
- with_migration_context do
- BackgroundMigrationWorker.bulk_perform_async(*args)
- end
- end
-
- def with_migration_context(&block)
- Gitlab::ApplicationContext.with_context(caller_id: self.class.to_s, &block)
- end
-
def delete_queued_jobs(class_name)
- Gitlab::BackgroundMigration.steal(class_name) do |job|
+ coordinator_for_tracking_database.steal(class_name) do |job|
job.delete
false
@@ -196,9 +174,21 @@ module Gitlab
private
+ def with_migration_context(&block)
+ Gitlab::ApplicationContext.with_context(caller_id: self.class.to_s, &block)
+ end
+
def track_in_database(class_name, arguments)
Gitlab::Database::BackgroundMigrationJob.create!(class_name: class_name, arguments: arguments)
end
+
+ def coordinator_for_tracking_database
+ Gitlab::BackgroundMigration.coordinator_for_database(tracking_database)
+ end
+
+ def tracking_database
+ Gitlab::BackgroundMigration::DEFAULT_TRACKING_DATABASE
+ end
end
end
end
diff --git a/lib/gitlab/database/partitioning/partition_manager.rb b/lib/gitlab/database/partitioning/partition_manager.rb
index aa824dfbd2f..ba6fa0cf278 100644
--- a/lib/gitlab/database/partitioning/partition_manager.rb
+++ b/lib/gitlab/database/partitioning/partition_manager.rb
@@ -64,6 +64,10 @@ module Gitlab
# with_lock_retries starts a requires_new transaction most of the time, but not on the last iteration
with_lock_retries do
connection.transaction(requires_new: false) do # so we open a transaction here if not already in progress
+ # Partitions might not get created (IF NOT EXISTS) so explicit locking will not happen.
+ # This LOCK TABLE ensures to have exclusive lock as the first step.
+ connection.execute "LOCK TABLE #{connection.quote_table_name(model.table_name)} IN ACCESS EXCLUSIVE MODE"
+
partitions.each do |partition|
connection.execute partition.to_sql
diff --git a/lib/gitlab/database/partitioning/sliding_list_strategy.rb b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
index 21b86b43ae7..e9865fb91d6 100644
--- a/lib/gitlab/database/partitioning/sliding_list_strategy.rb
+++ b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
@@ -44,7 +44,18 @@ module Gitlab
def extra_partitions
possibly_extra = current_partitions[0...-1] # Never consider the most recent partition
- possibly_extra.take_while { |p| detach_partition_if.call(p.value) }
+ extra = possibly_extra.take_while { |p| detach_partition_if.call(p.value) }
+
+ default_value = current_default_value
+ if extra.any? { |p| p.value == default_value }
+ Gitlab::AppLogger.error(message: "Inconsistent partition detected: partition with value #{current_default_value} should not be deleted because it's used as the default value.",
+ partition_number: current_default_value,
+ table_name: model.table_name)
+
+ extra = extra.reject { |p| p.value == default_value }
+ end
+
+ extra
end
def after_adding_partitions
@@ -64,6 +75,21 @@ module Gitlab
private
+ def current_default_value
+ column_name = model.connection.quote(partitioning_key)
+ table_name = model.connection.quote(model.table_name)
+
+ value = model.connection.select_value <<~SQL
+ SELECT columns.column_default AS default_value
+ FROM information_schema.columns columns
+ WHERE columns.column_name = #{column_name} AND columns.table_name = #{table_name}
+ SQL
+
+ raise "No default value found for the #{partitioning_key} column within #{model.name}" if value.nil?
+
+ Integer(value)
+ end
+
def ensure_partitioning_column_ignored!
unless model.ignored_columns.include?(partitioning_key.to_s)
raise "Add #{partitioning_key} to #{model.name}.ignored_columns to use it with SlidingListStrategy"
diff --git a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
index 17a42d997e6..f551fa06cad 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
@@ -4,7 +4,7 @@ module Gitlab
module Database
module PartitioningMigrationHelpers
# Class that will generically copy data from a given table into its corresponding partitioned table
- class BackfillPartitionedTable
+ class BackfillPartitionedTable < ::Gitlab::BackgroundMigration::BaseJob
include ::Gitlab::Database::DynamicModelHelpers
SUB_BATCH_SIZE = 2_500
@@ -21,7 +21,7 @@ module Gitlab
return
end
- bulk_copy = BulkCopy.new(source_table, partitioned_table, source_column)
+ bulk_copy = BulkCopy.new(source_table, partitioned_table, source_column, connection: connection)
parent_batch_relation = relation_scoped_to_range(source_table, source_column, start_id, stop_id)
parent_batch_relation.each_batch(of: SUB_BATCH_SIZE) do |sub_batch|
@@ -36,10 +36,6 @@ module Gitlab
private
- def connection
- ActiveRecord::Base.connection
- end
-
def transaction_open?
connection.transaction_open?
end
@@ -53,7 +49,8 @@ module Gitlab
end
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
- define_batchable_model(source_table).where(source_key_column => start_id..stop_id)
+ define_batchable_model(source_table)
+ .where(source_key_column => start_id..stop_id)
end
def mark_jobs_as_succeeded(*arguments)
@@ -64,12 +61,13 @@ module Gitlab
class BulkCopy
DELIMITER = ', '
- attr_reader :source_table, :destination_table, :source_column
+ attr_reader :source_table, :destination_table, :source_column, :connection
- def initialize(source_table, destination_table, source_column)
+ def initialize(source_table, destination_table, source_column, connection:)
@source_table = source_table
@destination_table = destination_table
@source_column = source_column
+ @connection = connection
end
def copy_between(start_id, stop_id)
@@ -85,10 +83,6 @@ module Gitlab
private
- def connection
- @connection ||= ActiveRecord::Base.connection
- end
-
def column_listing
@column_listing ||= connection.columns(source_table).map(&:name).join(DELIMITER)
end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index c382d2f0715..984c708aa48 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -406,7 +406,8 @@ module Gitlab
end
def copy_missed_records(source_table_name, partitioned_table_name, source_column)
- backfill_table = BackfillPartitionedTable.new
+ backfill_table = BackfillPartitionedTable.new(connection: connection)
+
relation = ::Gitlab::Database::BackgroundMigrationJob.pending
.for_partitioning_migration(MIGRATION_CLASS_NAME, source_table_name)
diff --git a/lib/gitlab/database/reflection.rb b/lib/gitlab/database/reflection.rb
index 48a4de28541..3ea7277571f 100644
--- a/lib/gitlab/database/reflection.rb
+++ b/lib/gitlab/database/reflection.rb
@@ -105,6 +105,35 @@ module Gitlab
row['system_identifier']
end
+ def flavor
+ {
+ # Based on https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/
+ 'Amazon Aurora PostgreSQL' => { statement: 'SELECT AURORA_VERSION()', error: /PG::UndefinedFunction/ },
+ # Based on https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html#PostgreSQL.Concepts.General.FeatureSupport.Extensions,
+ # this is also available for both Aurora and RDS, so we need to check for the former first.
+ 'PostgreSQL on Amazon RDS' => { statement: 'SHOW rds.extensions', error: /PG::UndefinedObject/ },
+ # Based on https://cloud.google.com/sql/docs/postgres/flags#postgres-c this should be specific
+ # to Cloud SQL for PostgreSQL
+ 'Cloud SQL for PostgreSQL' => { statement: 'SHOW cloudsql.iam_authentication', error: /PG::UndefinedObject/ },
+ # Based on
+ # - https://docs.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-extensions
+ # - https://docs.microsoft.com/en-us/azure/postgresql/concepts-extensions
+ # this should be available only for Azure Database for PostgreSQL - Flexible Server.
+ 'Azure Database for PostgreSQL - Flexible Server' => { statement: 'SHOW azure.extensions', error: /PG::UndefinedObject/ },
+ # Based on
+ # - https://docs.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-servers
+ # - https://docs.microsoft.com/en-us/azure/postgresql/concepts-servers#managing-your-server
+ # this database is present on both Flexible and Single server, so we should check the former first.
+ 'Azure Database for PostgreSQL - Single Server' => { statement: "SELECT datname FROM pg_database WHERE datname = 'azure_maintenance'" }
+ }.each do |flavor, conditions|
+ return flavor if connection.execute(conditions[:statement]).to_a.present?
+ rescue ActiveRecord::StatementInvalid => e
+ raise if conditions[:error] && !e.message.match?(conditions[:error])
+ end
+
+ nil
+ end
+
private
def connection
diff --git a/lib/gitlab/database/reindexing.rb b/lib/gitlab/database/reindexing.rb
index 6ffe14249f0..91c3fcc7d72 100644
--- a/lib/gitlab/database/reindexing.rb
+++ b/lib/gitlab/database/reindexing.rb
@@ -76,20 +76,7 @@ module Gitlab
def self.cleanup_leftovers!
PostgresIndex.reindexing_leftovers.each do |index|
- Gitlab::AppLogger.info("Removing index #{index.identifier} which is a leftover, temporary index from previous reindexing activity")
-
- retries = Gitlab::Database::WithLockRetriesOutsideTransaction.new(
- connection: index.connection,
- timing_configuration: REMOVE_INDEX_RETRY_CONFIG,
- klass: self.class,
- logger: Gitlab::AppLogger
- )
-
- retries.run(raise_on_exhaustion: false) do
- index.connection.tap do |conn|
- conn.execute("DROP INDEX CONCURRENTLY IF EXISTS #{conn.quote_table_name(index.schema)}.#{conn.quote_table_name(index.name)}")
- end
- end
+ Coordinator.new(index).drop
end
end
end
diff --git a/lib/gitlab/database/reindexing/coordinator.rb b/lib/gitlab/database/reindexing/coordinator.rb
index 3e4a83aa2e7..b4f7da999df 100644
--- a/lib/gitlab/database/reindexing/coordinator.rb
+++ b/lib/gitlab/database/reindexing/coordinator.rb
@@ -31,6 +31,25 @@ module Gitlab
end
end
+ def drop
+ try_obtain_lease do
+ Gitlab::AppLogger.info("Removing index #{index.identifier} which is a leftover, temporary index from previous reindexing activity")
+
+ retries = Gitlab::Database::WithLockRetriesOutsideTransaction.new(
+ connection: index.connection,
+ timing_configuration: REMOVE_INDEX_RETRY_CONFIG,
+ klass: self.class,
+ logger: Gitlab::AppLogger
+ )
+
+ retries.run(raise_on_exhaustion: false) do
+ index.connection.tap do |conn|
+ conn.execute("DROP INDEX CONCURRENTLY IF EXISTS #{conn.quote_table_name(index.schema)}.#{conn.quote_table_name(index.name)}")
+ end
+ end
+ end
+ end
+
private
def with_notifications(action)
diff --git a/lib/gitlab/database_importers/work_items/base_type_importer.rb b/lib/gitlab/database_importers/work_items/base_type_importer.rb
index c5acdb41de5..2d9700cb2bc 100644
--- a/lib/gitlab/database_importers/work_items/base_type_importer.rb
+++ b/lib/gitlab/database_importers/work_items/base_type_importer.rb
@@ -5,8 +5,8 @@ module Gitlab
module WorkItems
module BaseTypeImporter
def self.import
- WorkItem::Type::BASE_TYPES.each do |type, attributes|
- WorkItem::Type.create!(base_type: type, **attributes.slice(:name, :icon_name))
+ ::WorkItems::Type::BASE_TYPES.each do |type, attributes|
+ ::WorkItems::Type.create!(base_type: type, **attributes.slice(:name, :icon_name))
end
end
end
diff --git a/lib/gitlab/email.rb b/lib/gitlab/email.rb
index 5f935880764..2e8f076c5d8 100644
--- a/lib/gitlab/email.rb
+++ b/lib/gitlab/email.rb
@@ -18,5 +18,6 @@ module Gitlab
InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError)
InvalidAttachment = Class.new(ProcessingError)
+ EmailTooLarge = Class.new(ProcessingError)
end
end
diff --git a/lib/gitlab/email/failure_handler.rb b/lib/gitlab/email/failure_handler.rb
new file mode 100644
index 00000000000..1079a9c2bb6
--- /dev/null
+++ b/lib/gitlab/email/failure_handler.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Email
+ module FailureHandler
+ def self.handle(receiver, error)
+ can_retry = false
+ reason =
+ case error
+ when Gitlab::Email::UnknownIncomingEmail
+ s_("EmailError|We couldn't figure out what the email is for. Please create your issue or comment through the web interface.")
+ when Gitlab::Email::SentNotificationNotFoundError
+ s_("EmailError|We couldn't figure out what the email is in reply to. Please create your comment through the web interface.")
+ when Gitlab::Email::ProjectNotFound
+ s_("EmailError|We couldn't find the project. Please check if there's any typo.")
+ when Gitlab::Email::EmptyEmailError
+ can_retry = true
+ s_("EmailError|It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies.")
+ when Gitlab::Email::UserNotFoundError
+ s_("EmailError|We couldn't figure out what user corresponds to the email. Please create your comment through the web interface.")
+ when Gitlab::Email::UserBlockedError
+ s_("EmailError|Your account has been blocked. If you believe this is in error, contact a staff member.")
+ when Gitlab::Email::UserNotAuthorizedError
+ s_("EmailError|You are not allowed to perform this action. If you believe this is in error, contact a staff member.")
+ when Gitlab::Email::NoteableNotFoundError
+ s_("EmailError|The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member.")
+ when Gitlab::Email::InvalidAttachment
+ error.message
+ when Gitlab::Email::InvalidRecordError
+ can_retry = true
+ error.message
+ when Gitlab::Email::EmailTooLarge
+ s_("EmailError|We couldn't process your email because it is too large. Please create your issue or comment through the web interface.")
+ end
+
+ if reason
+ receiver.mail.body = nil
+
+ EmailRejectionMailer.rejection(reason, receiver.mail.encoded, can_retry).deliver_later
+ end
+
+ reason
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/error_tracking/processor/sidekiq_processor.rb b/lib/gitlab/error_tracking/processor/sidekiq_processor.rb
index 0d2f673d73c..cc8cfd827f1 100644
--- a/lib/gitlab/error_tracking/processor/sidekiq_processor.rb
+++ b/lib/gitlab/error_tracking/processor/sidekiq_processor.rb
@@ -53,6 +53,8 @@ module Gitlab
# 'args' in :job => from default error handler
job_holder = sidekiq.key?('args') ? sidekiq : sidekiq[:job]
+ return event unless job_holder
+
if job_holder['args']
job_holder['args'] = filter_arguments(job_holder['args'], job_holder['class']).to_a
end
diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb
new file mode 100644
index 00000000000..3d7b6b27eb0
--- /dev/null
+++ b/lib/gitlab/event_store.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+# Gitlab::EventStore is a simple pub-sub mechanism that lets you publish
+# domain events and use Sidekiq workers as event handlers.
+#
+# It can be used to decouple domains from different bounded contexts
+# by publishing domain events and let any interested parties subscribe
+# to them.
+#
+module Gitlab
+ module EventStore
+ Error = Class.new(StandardError)
+ InvalidEvent = Class.new(Error)
+ InvalidSubscriber = Class.new(Error)
+
+ def self.publish(event)
+ instance.publish(event)
+ end
+
+ def self.instance
+ @instance ||= configure!
+ end
+
+ # Define all event subscriptions using:
+ #
+ # store.subscribe(DomainA::SomeWorker, to: DomainB::SomeEvent)
+ #
+ # It is possible to subscribe to a subset of events matching a condition:
+ #
+ # store.subscribe(DomainA::SomeWorker, to: DomainB::SomeEvent), if: ->(event) { event.data == :some_value }
+ #
+ def self.configure!
+ Store.new do |store|
+ ###
+ # Add subscriptions here:
+
+ store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
+ end
+ end
+ private_class_method :configure!
+ end
+end
diff --git a/lib/gitlab/event_store/event.rb b/lib/gitlab/event_store/event.rb
new file mode 100644
index 00000000000..ee0c329b8e8
--- /dev/null
+++ b/lib/gitlab/event_store/event.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+# An Event object represents a domain event that occurred in a bounded context.
+# By publishing events we notify other bounded contexts about something
+# that happened, so that they can react to it.
+#
+# Define new event classes under `app/events/<namespace>/` with a name
+# representing something that happened in the past:
+#
+# class Projects::ProjectCreatedEvent < Gitlab::EventStore::Event
+# def schema
+# {
+# 'type' => 'object',
+# 'properties' => {
+# 'project_id' => { 'type' => 'integer' }
+# }
+# }
+# end
+# end
+#
+# To publish it:
+#
+# Gitlab::EventStore.publish(
+# Projects::ProjectCreatedEvent.new(data: { project_id: project.id })
+# )
+#
+module Gitlab
+ module EventStore
+ class Event
+ attr_reader :data
+
+ def initialize(data:)
+ validate_schema!(data)
+ @data = data
+ end
+
+ def schema
+ raise NotImplementedError, 'must specify schema to validate the event'
+ end
+
+ private
+
+ def validate_schema!(data)
+ unless data.is_a?(Hash)
+ raise Gitlab::EventStore::InvalidEvent, "Event data must be a Hash"
+ end
+
+ unless JSONSchemer.schema(schema).valid?(data.deep_stringify_keys)
+ raise Gitlab::EventStore::InvalidEvent, "Data for event #{self.class} does not match the defined schema: #{schema}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/event_store/store.rb b/lib/gitlab/event_store/store.rb
new file mode 100644
index 00000000000..ecf3cd7e562
--- /dev/null
+++ b/lib/gitlab/event_store/store.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module EventStore
+ class Store
+ attr_reader :subscriptions
+
+ def initialize
+ @subscriptions = Hash.new { |h, k| h[k] = [] }
+
+ yield(self) if block_given?
+
+ # freeze the subscriptions as safety measure to avoid further
+ # subcriptions after initialization.
+ lock!
+ end
+
+ def subscribe(worker, to:, if: nil)
+ condition = binding.local_variable_get('if')
+
+ Array(to).each do |event|
+ validate_subscription!(worker, event)
+ subscriptions[event] << Gitlab::EventStore::Subscription.new(worker, condition)
+ end
+ end
+
+ def publish(event)
+ unless event.is_a?(Event)
+ raise InvalidEvent, "Event being published is not an instance of Gitlab::EventStore::Event: got #{event.inspect}"
+ end
+
+ subscriptions[event.class].each do |subscription|
+ subscription.consume_event(event)
+ end
+ end
+
+ private
+
+ def lock!
+ @subscriptions.freeze
+ end
+
+ def validate_subscription!(subscriber, event_class)
+ unless event_class < Event
+ raise InvalidEvent, "Event being subscribed to is not a subclass of Gitlab::EventStore::Event: got #{event_class}"
+ end
+
+ unless subscriber.respond_to?(:perform_async)
+ raise InvalidSubscriber, "Subscriber is not an ApplicationWorker: got #{subscriber}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/event_store/subscriber.rb b/lib/gitlab/event_store/subscriber.rb
new file mode 100644
index 00000000000..cf326d1f9e4
--- /dev/null
+++ b/lib/gitlab/event_store/subscriber.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# This module should be included in order to turn an ApplicationWorker
+# into a Subscriber.
+# This module overrides the `perform` method and provides a better and
+# safer interface for handling events via `handle_event` method.
+#
+# @example:
+# class SomeEventSubscriber
+# include ApplicationWorker
+# include Gitlab::EventStore::Subscriber
+#
+# def handle_event(event)
+# # ...
+# end
+# end
+
+module Gitlab
+ module EventStore
+ module Subscriber
+ def perform(event_type, data)
+ raise InvalidEvent, event_type unless self.class.const_defined?(event_type)
+
+ event = event_type.constantize.new(
+ data: data.with_indifferent_access
+ )
+
+ handle_event(event)
+ end
+
+ def handle_event(event)
+ raise NotImplementedError, 'you must implement this methods in order to handle events'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/event_store/subscription.rb b/lib/gitlab/event_store/subscription.rb
new file mode 100644
index 00000000000..e5c92ab969f
--- /dev/null
+++ b/lib/gitlab/event_store/subscription.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module EventStore
+ class Subscription
+ attr_reader :worker, :condition
+
+ def initialize(worker, condition)
+ @worker = worker
+ @condition = condition
+ end
+
+ def consume_event(event)
+ return unless condition_met?(event)
+
+ worker.perform_async(event.class.name, event.data)
+ # TODO: Log dispatching of events to subscriber
+
+ # We rescue and track any exceptions here because we don't want to
+ # impact other subscribers if one is faulty.
+ # The method `condition_met?`, since it can run a block, it might encounter
+ # a bug. By raising an exception here we could interrupt the publishing
+ # process, preventing other subscribers from consuming the event.
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, event_class: event.class.name, event_data: event.data)
+ end
+
+ private
+
+ def condition_met?(event)
+ return true unless condition
+
+ condition.call(event)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/exceptions_app.rb b/lib/gitlab/exceptions_app.rb
new file mode 100644
index 00000000000..de07b788fb9
--- /dev/null
+++ b/lib/gitlab/exceptions_app.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require_relative 'utils/override'
+
+module Gitlab
+ class ExceptionsApp < ActionDispatch::PublicExceptions
+ extend ::Gitlab::Utils::Override
+
+ REQUEST_ID_PLACEHOLDER = '<!-- REQUEST_ID -->'
+ REQUEST_ID_PARAGRAPH = '<p>Request ID: <code>%s</code></p>'
+
+ override :call
+ def call(env)
+ status, headers, body = super
+
+ if html_rendered? && body.first&.include?(REQUEST_ID_PLACEHOLDER)
+ body = [insert_request_id(env, body.first)]
+ headers['X-GitLab-Custom-Error'] = '1'
+ end
+
+ [status, headers, body]
+ end
+
+ private
+
+ override :render_html
+ def render_html(status)
+ @html_rendered = true
+
+ super
+ end
+
+ def html_rendered?
+ !!@html_rendered
+ end
+
+ def insert_request_id(env, body)
+ request_id = ERB::Util.html_escape(ActionDispatch::Request.new(env).request_id)
+
+ body.gsub(REQUEST_ID_PLACEHOLDER, REQUEST_ID_PARAGRAPH % [request_id])
+ end
+ end
+end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 4cc653bec43..7edda290204 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -30,14 +30,6 @@
module Gitlab
module Experimentation
EXPERIMENTS = {
- remove_known_trial_form_fields_welcoming: {
- tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFieldsWelcoming',
- rollout_strategy: :user
- },
- remove_known_trial_form_fields_noneditable: {
- tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFieldsNoneditable',
- rollout_strategy: :user
- }
}.freeze
class << self
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index bb3ba1129fc..ac3b4de0988 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -57,6 +57,7 @@ module Gitlab
push_frontend_feature_flag(:improved_emoji_picker, default_enabled: :yaml)
push_frontend_feature_flag(:new_header_search, default_enabled: :yaml)
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
+ push_frontend_feature_flag(:sandboxed_mermaid, default_enabled: :yaml)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 59882e8d4f8..ab7de14b07a 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -102,7 +102,7 @@ module Gitlab
end
def verification_status(gpg_key)
- return :multiple_signatures if multiple_signatures? && Feature.enabled?(:multiple_gpg_signatures, @commit.project, default_enabled: :yaml)
+ return :multiple_signatures if multiple_signatures?
return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
return :unverified unless verified_signature&.valid?
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index 1b860001ac0..d0918fc39bc 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -49,11 +49,12 @@ module Gitlab
return httparty_perform_request(http_method, path, options_with_timeouts, &block)
end
- start_time = Gitlab::Metrics::System.monotonic_time
+ start_time = nil
read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
tracked_timeout_error = false
httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
+ start_time ||= Gitlab::Metrics::System.monotonic_time
elapsed = Gitlab::Metrics::System.monotonic_time - start_time
if elapsed > read_total_timeout
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index 12203cab8c8..f056381b86a 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -43,27 +43,27 @@ module Gitlab
TRANSLATION_LEVELS = {
'bg' => 0,
'cs_CZ' => 0,
- 'da_DK' => 51,
+ 'da_DK' => 49,
'de' => 15,
'en' => 100,
'eo' => 0,
- 'es' => 39,
+ 'es' => 38,
'fil_PH' => 0,
- 'fr' => 12,
+ 'fr' => 11,
'gl_ES' => 0,
'id_ID' => 0,
'it' => 2,
- 'ja' => 35,
- 'ko' => 11,
- 'nb_NO' => 33,
+ 'ja' => 36,
+ 'ko' => 12,
+ 'nb_NO' => 32,
'nl_NL' => 0,
'pl_PL' => 5,
- 'pt_BR' => 49,
- 'ro_RO' => 23,
- 'ru' => 25,
- 'tr_TR' => 15,
+ 'pt_BR' => 50,
+ 'ro_RO' => 22,
+ 'ru' => 26,
+ 'tr_TR' => 14,
'uk' => 45,
- 'zh_CN' => 95,
+ 'zh_CN' => 98,
'zh_HK' => 2,
'zh_TW' => 3
}.freeze
diff --git a/lib/gitlab/import/set_async_jid.rb b/lib/gitlab/import/set_async_jid.rb
index 054fcdb433f..527d84477fe 100644
--- a/lib/gitlab/import/set_async_jid.rb
+++ b/lib/gitlab/import/set_async_jid.rb
@@ -13,7 +13,7 @@ module Gitlab
def self.set_jid(import_state)
jid = generate_jid(import_state)
- Gitlab::SidekiqStatus.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION, value: 2)
+ Gitlab::SidekiqStatus.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
import_state.update_column(:jid, jid)
end
diff --git a/lib/gitlab/import_export/base/relation_factory.rb b/lib/gitlab/import_export/base/relation_factory.rb
index 6749ef4e276..8a8c74c302d 100644
--- a/lib/gitlab/import_export/base/relation_factory.rb
+++ b/lib/gitlab/import_export/base/relation_factory.rb
@@ -196,7 +196,7 @@ module Gitlab
end
def use_attributes_permitter?
- Feature.enabled?(:permitted_attributes_for_import_export, default_enabled: :yaml)
+ true
end
def existing_or_new_object
diff --git a/lib/gitlab/import_export/group/relation_tree_restorer.rb b/lib/gitlab/import_export/group/relation_tree_restorer.rb
index cbc8ee9e18b..c2cbd2fdf47 100644
--- a/lib/gitlab/import_export/group/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/group/relation_tree_restorer.rb
@@ -118,7 +118,7 @@ module Gitlab
end
def filter_attributes(params)
- if use_attributes_permitter? && attributes_permitter.permitted_attributes_defined?(importable_class_sym)
+ if attributes_permitter.permitted_attributes_defined?(importable_class_sym)
attributes_permitter.permit(importable_class_sym, params)
else
Gitlab::ImportExport::AttributeCleaner.clean(
@@ -132,10 +132,6 @@ module Gitlab
@attributes_permitter ||= Gitlab::ImportExport::AttributesPermitter.new
end
- def use_attributes_permitter?
- Feature.enabled?(:permitted_attributes_for_import_export, default_enabled: :yaml)
- end
-
def present_override_params
# we filter out the empty strings from the overrides
# keeping the default values configured
@@ -264,14 +260,12 @@ module Gitlab
@relation_reader.sort_ci_pipelines_by_id
end
- # Enable logging of each top-level relation creation when Importing
- # into a Group if feature flag is enabled
+ # Enable logging of each top-level relation creation when Importing into a Group
def log_relation_creation(importable, relation_key, relation_object)
root_ancestor_group = importable.try(:root_ancestor)
return unless root_ancestor_group
return unless root_ancestor_group.instance_of?(::Group)
- return unless Feature.enabled?(:log_import_export_relation_creation, root_ancestor_group)
@shared.logger.info(
importable_type: importable.class.to_s,
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index ef146359da9..059f6bd42e3 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -120,6 +120,7 @@ included_attributes:
- :name
ci_cd_settings:
- :group_runners_enabled
+ - :runner_token_expiration_interval
metrics_setting:
- :dashboard_timezone
- :external_dashboard_url
@@ -904,6 +905,7 @@ excluded_attributes:
- :release_id
project_members:
- :source_id
+ - :member_namespace_id
- :invite_email_success
- :state
group_members:
diff --git a/lib/gitlab/jwt_authenticatable.rb b/lib/gitlab/jwt_authenticatable.rb
index 1270a148e8d..08d9f69497e 100644
--- a/lib/gitlab/jwt_authenticatable.rb
+++ b/lib/gitlab/jwt_authenticatable.rb
@@ -13,26 +13,38 @@ module Gitlab
module ClassMethods
include Gitlab::Utils::StrongMemoize
- def decode_jwt_for_issuer(issuer, encoded_message)
- JWT.decode(
- encoded_message,
- secret,
- true,
- { iss: issuer, verify_iss: true, algorithm: 'HS256' }
- )
+ def decode_jwt(encoded_message, jwt_secret = secret, issuer: nil, iat_after: nil)
+ options = { algorithm: 'HS256' }
+ options = options.merge(iss: issuer, verify_iss: true) if issuer.present?
+ options = options.merge(verify_iat: true) if iat_after.present?
+
+ decoded_message = JWT.decode(encoded_message, jwt_secret, true, options)
+ payload = decoded_message[0]
+ if iat_after.present?
+ raise JWT::DecodeError, "JWT iat claim is missing" if payload['iat'].blank?
+
+ iat = payload['iat'].to_i
+ raise JWT::ExpiredSignature, 'Token has expired' if iat < iat_after.to_i
+ end
+
+ decoded_message
end
def secret
strong_memoize(:secret) do
- Base64.strict_decode64(File.read(secret_path).chomp).tap do |bytes|
- raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
- end
+ read_secret(secret_path)
+ end
+ end
+
+ def read_secret(path)
+ Base64.strict_decode64(File.read(path).chomp).tap do |bytes|
+ raise "#{path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
end
end
- def write_secret
+ def write_secret(path = secret_path)
bytes = SecureRandom.random_bytes(SECRET_LENGTH)
- File.open(secret_path, 'w:BINARY', 0600) do |f|
+ File.open(path, 'w:BINARY', 0600) do |f|
f.chmod(0600) # If the file already existed, the '0600' passed to 'open' above was a no-op.
f.write(Base64.strict_encode64(bytes))
end
diff --git a/lib/gitlab/kas.rb b/lib/gitlab/kas.rb
index 408b3afc128..ed7787ffc49 100644
--- a/lib/gitlab/kas.rb
+++ b/lib/gitlab/kas.rb
@@ -11,7 +11,7 @@ module Gitlab
class << self
def verify_api_request(request_headers)
- decode_jwt_for_issuer(JWT_ISSUER, request_headers[INTERNAL_API_REQUEST_HEADER])
+ decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER], issuer: JWT_ISSUER)
rescue JWT::DecodeError
nil
end
diff --git a/lib/gitlab/lfs/client.rb b/lib/gitlab/lfs/client.rb
index a05e8107cad..10df9262cca 100644
--- a/lib/gitlab/lfs/client.rb
+++ b/lib/gitlab/lfs/client.rb
@@ -36,7 +36,7 @@ module Gitlab
headers: build_request_headers
)
- raise BatchSubmitError unless rsp.success?
+ raise BatchSubmitError.new(http_response: rsp) unless rsp.success?
# HTTParty provides rsp.parsed_response, but it only kicks in for the
# application/json content type in the response, which we can't rely on
@@ -53,19 +53,13 @@ module Gitlab
params = {
body_stream: file,
- headers: {
- 'Content-Length' => object.size.to_s,
- 'Content-Type' => 'application/octet-stream',
- 'User-Agent' => GIT_LFS_USER_AGENT
- }.merge(upload_action['header'] || {})
+ headers: upload_headers(object, upload_action)
}
- authenticated = true if params[:headers].key?('Authorization')
- params[:basic_auth] = basic_auth unless authenticated
-
- rsp = Gitlab::HTTP.put(upload_action['href'], params)
+ url = set_basic_auth_and_extract_lfs_url!(params, upload_action['href'])
+ rsp = Gitlab::HTTP.put(url, params)
- raise ObjectUploadError unless rsp.success?
+ raise ObjectUploadError.new(http_response: rsp) unless rsp.success?
ensure
file&.close
end
@@ -76,20 +70,51 @@ module Gitlab
headers: build_request_headers(verify_action['header'])
}
- authenticated = true if params[:headers].key?('Authorization')
- params[:basic_auth] = basic_auth unless authenticated
-
- rsp = Gitlab::HTTP.post(verify_action['href'], params)
+ url = set_basic_auth_and_extract_lfs_url!(params, verify_action['href'])
+ rsp = Gitlab::HTTP.post(url, params)
- raise ObjectVerifyError unless rsp.success?
+ raise ObjectVerifyError.new(http_response: rsp) unless rsp.success?
end
private
+ def set_basic_auth_and_extract_lfs_url!(params, raw_url)
+ authenticated = true if params[:headers].key?('Authorization')
+ params[:basic_auth] = basic_auth unless authenticated
+ strip_userinfo = authenticated || params[:basic_auth].present?
+ lfs_url(raw_url, strip_userinfo)
+ end
+
def build_request_headers(extra_headers = nil)
DEFAULT_HEADERS.merge(extra_headers || {})
end
+ def upload_headers(object, upload_action)
+ # This uses the httprb library to handle case-insensitive HTTP headers
+ headers = ::HTTP::Headers.new
+ headers.merge!(upload_action['header'])
+ transfer_encodings = Array(headers['Transfer-Encoding']&.split(',')).map(&:strip)
+
+ headers['Content-Length'] = object.size.to_s unless transfer_encodings.include?('chunked')
+ headers['Content-Type'] = 'application/octet-stream'
+ headers['User-Agent'] = GIT_LFS_USER_AGENT
+
+ headers.to_h
+ end
+
+ def lfs_url(raw_url, strip_userinfo)
+ # HTTParty will give precedence to the username/password
+ # specified in the URL. This causes problems with Azure DevOps,
+ # which includes a username in the URL. Stripping the userinfo
+ # from the URL allows the provided HTTP Basic Authentication
+ # credentials to be used.
+ if strip_userinfo
+ Gitlab::UrlSanitizer.new(raw_url).sanitized_url
+ else
+ raw_url
+ end
+ end
+
attr_reader :credentials
def batch_url
@@ -105,9 +130,21 @@ module Gitlab
{ username: credentials[:user], password: credentials[:password] }
end
- class BatchSubmitError < StandardError
+ class HttpError < StandardError
+ def initialize(http_response:)
+ super
+
+ @http_response = http_response
+ end
+
+ def http_error
+ "HTTP status #{@http_response.code}"
+ end
+ end
+
+ class BatchSubmitError < HttpError
def message
- "Failed to submit batch"
+ "Failed to submit batch: #{http_error}"
end
end
@@ -122,15 +159,15 @@ module Gitlab
end
end
- class ObjectUploadError < StandardError
+ class ObjectUploadError < HttpError
def message
- "Failed to upload object"
+ "Failed to upload object: #{http_error}"
end
end
- class ObjectVerifyError < StandardError
+ class ObjectVerifyError < HttpError
def message
- "Failed to verify object"
+ "Failed to verify object: #{http_error}"
end
end
end
diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb
index 89a4e36a232..53ad0d9cb4d 100644
--- a/lib/gitlab/logger.rb
+++ b/lib/gitlab/logger.rb
@@ -33,7 +33,11 @@ module Gitlab
def self.build
Gitlab::SafeRequestStore[self.cache_key] ||=
- new(self.full_log_path, level: ::Logger::DEBUG)
+ new(self.full_log_path, level: log_level)
+ end
+
+ def self.log_level(fallback: ::Logger::DEBUG)
+ ENV.fetch('GITLAB_LOG_LEVEL', fallback)
end
def self.full_log_path
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index 75d27ed8cc1..e93a297cee4 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -25,7 +25,7 @@ module Gitlab
# Email specific configuration which is merged with configuration
# fetched from YML config file.
- ADDRESS_SPECIFIC_CONFIG = {
+ MAILBOX_SPECIFIC_CONFIGS = {
incoming_email: {
queue: 'email_receiver',
worker: 'EmailReceiverWorker'
@@ -38,7 +38,15 @@ module Gitlab
class << self
def enabled_configs
- @enabled_configs ||= configs.select { |config| enabled?(config) }
+ @enabled_configs ||= configs.select { |_key, config| enabled?(config) }
+ end
+
+ def enabled_mailbox_types
+ enabled_configs.keys.map(&:to_s)
+ end
+
+ def worker_for(mailbox_type)
+ MAILBOX_SPECIFIC_CONFIGS.try(:[], mailbox_type.to_sym).try(:[], :worker).try(:safe_constantize)
end
private
@@ -48,7 +56,7 @@ module Gitlab
end
def configs
- ADDRESS_SPECIFIC_CONFIG.keys.map { |key| fetch_config(key) }
+ MAILBOX_SPECIFIC_CONFIGS.to_h { |key, _value| [key, fetch_config(key)] }
end
def fetch_config(config_key)
@@ -63,7 +71,7 @@ module Gitlab
def merged_configs(config_key)
yml_config = load_yaml.fetch(config_key, {})
- specific_config = ADDRESS_SPECIFIC_CONFIG.fetch(config_key, {})
+ specific_config = MAILBOX_SPECIFIC_CONFIGS.fetch(config_key, {})
DEFAULT_CONFIG.merge(specific_config, yml_config) do |_key, oldval, newval|
newval.nil? ? oldval : newval
end
diff --git a/lib/gitlab/mail_room/authenticator.rb b/lib/gitlab/mail_room/authenticator.rb
new file mode 100644
index 00000000000..26ebdca8beb
--- /dev/null
+++ b/lib/gitlab/mail_room/authenticator.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MailRoom
+ class Authenticator
+ include JwtAuthenticatable
+
+ SecretConfigurationError = Class.new(StandardError)
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Mailroom-Api-Request'
+ INTERNAL_API_REQUEST_JWT_ISSUER = 'gitlab-mailroom'
+
+ # Only allow token generated within the last 5 minutes
+ EXPIRATION = 5.minutes
+
+ class << self
+ def verify_api_request(request_headers, mailbox_type)
+ mailbox_type = mailbox_type.to_sym
+ return false if enabled_configs[mailbox_type].blank?
+
+ decode_jwt(
+ request_headers[INTERNAL_API_REQUEST_HEADER],
+ secret(mailbox_type),
+ issuer: INTERNAL_API_REQUEST_JWT_ISSUER, iat_after: Time.current - EXPIRATION
+ )
+ rescue JWT::DecodeError => e
+ ::Gitlab::AppLogger.warn("Fail to decode MailRoom JWT token: #{e.message}") if Rails.env.development?
+
+ false
+ end
+
+ def secret(mailbox_type)
+ strong_memoize("jwt_secret_#{mailbox_type}".to_sym) do
+ secret_path = enabled_configs[mailbox_type][:secret_file]
+ raise SecretConfigurationError, "#{mailbox_type}'s secret_file configuration is missing" if secret_path.blank?
+
+ begin
+ read_secret(secret_path)
+ rescue StandardError => e
+ raise SecretConfigurationError, "Fail to read #{mailbox_type}'s secret: #{e.message}"
+ end
+ end
+ end
+
+ def enabled_configs
+ Gitlab::MailRoom.enabled_configs
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/merge_requests/commit_message_generator.rb b/lib/gitlab/merge_requests/commit_message_generator.rb
index 0e9ec6f5cb3..0515c17fe5d 100644
--- a/lib/gitlab/merge_requests/commit_message_generator.rb
+++ b/lib/gitlab/merge_requests/commit_message_generator.rb
@@ -2,8 +2,9 @@
module Gitlab
module MergeRequests
class CommitMessageGenerator
- def initialize(merge_request:)
+ def initialize(merge_request:, current_user:)
@merge_request = merge_request
+ @current_user = @merge_request.metrics&.merged_by || @merge_request.merge_user || current_user
end
def merge_message
@@ -15,57 +16,66 @@ module Gitlab
def squash_message
return unless @merge_request.target_project.squash_commit_template.present?
- replace_placeholders(@merge_request.target_project.squash_commit_template)
+ replace_placeholders(@merge_request.target_project.squash_commit_template, squash: true)
end
private
attr_reader :merge_request
+ attr_reader :current_user
PLACEHOLDERS = {
- 'source_branch' => ->(merge_request) { merge_request.source_branch.to_s },
- 'target_branch' => ->(merge_request) { merge_request.target_branch.to_s },
- 'title' => ->(merge_request) { merge_request.title },
- 'issues' => ->(merge_request) do
- return "" if merge_request.visible_closing_issues_for.blank?
+ 'source_branch' => ->(merge_request, _, _) { merge_request.source_branch.to_s },
+ 'target_branch' => ->(merge_request, _, _) { merge_request.target_branch.to_s },
+ 'title' => ->(merge_request, _, _) { merge_request.title },
+ 'issues' => ->(merge_request, _, _) do
+ return if merge_request.visible_closing_issues_for.blank?
closes_issues_references = merge_request.visible_closing_issues_for.map do |issue|
issue.to_reference(merge_request.target_project)
end
"Closes #{closes_issues_references.to_sentence}"
end,
- 'description' => ->(merge_request) { merge_request.description.presence || '' },
- 'reference' => ->(merge_request) { merge_request.to_reference(full: true) },
- 'first_commit' => -> (merge_request) { merge_request.first_commit&.safe_message&.strip.presence || '' },
- 'first_multiline_commit' => -> (merge_request) { merge_request.first_multiline_commit&.safe_message&.strip.presence || merge_request.title }
+ 'description' => ->(merge_request, _, _) { merge_request.description },
+ 'reference' => ->(merge_request, _, _) { merge_request.to_reference(full: true) },
+ 'first_commit' => -> (merge_request, _, _) { merge_request.first_commit&.safe_message&.strip },
+ 'first_multiline_commit' => -> (merge_request, _, _) { merge_request.first_multiline_commit&.safe_message&.strip.presence || merge_request.title },
+ 'url' => ->(merge_request, _, _) { Gitlab::UrlBuilder.build(merge_request) },
+ 'approved_by' => ->(merge_request, _, _) { merge_request.approved_by_users.map { |user| "Approved-by: #{user.name} <#{user.commit_email_or_default}>" }.join("\n") },
+ 'merged_by' => ->(_, user, _) { "#{user&.name} <#{user&.commit_email_or_default}>" },
+ 'co_authored_by' => ->(merge_request, merged_by, squash) do
+ commit_author = squash ? merge_request.author : merged_by
+ merge_request.recent_commits
+ .to_h { |commit| [commit.author_email, commit.author_name] }
+ .except(commit_author&.commit_email_or_default)
+ .map { |author_email, author_name| "Co-authored-by: #{author_name} <#{author_email}>" }
+ .join("\n")
+ end
}.freeze
- PLACEHOLDERS_REGEX = Regexp.union(PLACEHOLDERS.keys.map do |key|
- Regexp.new(Regexp.escape(key))
- end).freeze
-
- BLANK_PLACEHOLDERS_REGEXES = (PLACEHOLDERS.map do |key, value|
- [key, Regexp.new("[\n\r]+%{#{Regexp.escape(key)}}$")]
- end).to_h.freeze
+ PLACEHOLDERS_COMBINED_REGEX = /%{(#{Regexp.union(PLACEHOLDERS.keys)})}/.freeze
- def replace_placeholders(message)
- # convert CRLF to LF
+ def replace_placeholders(message, squash: false)
+ # Convert CRLF to LF.
message = message.delete("\r")
- # Remove placeholders that correspond to empty values and are the last word in the line
- # along with all whitespace characters preceding them.
- # This allows us to recreate previous default merge commit message behaviour - we skipped new line character
- # before empty description and before closed issues when none were present.
- PLACEHOLDERS.each do |key, value|
- unless value.call(merge_request).present?
- message = message.gsub(BLANK_PLACEHOLDERS_REGEXES[key], '')
- end
+ used_variables = message.scan(PLACEHOLDERS_COMBINED_REGEX).map { |value| value[0] }.uniq
+ values = used_variables.to_h do |variable_name|
+ ["%{#{variable_name}}", PLACEHOLDERS[variable_name].call(merge_request, current_user, squash)]
end
+ names_of_empty_variables = values.filter_map { |name, value| name if value.blank? }
- Gitlab::StringPlaceholderReplacer
- .replace_string_placeholders(message, PLACEHOLDERS_REGEX) do |key|
- PLACEHOLDERS[key].call(merge_request)
+ # Remove lines that contain empty variable placeholder and nothing else.
+ if names_of_empty_variables.present?
+ # If there is blank line or EOF after it, remove blank line before it as well.
+ message = message.gsub(/\n\n#{Regexp.union(names_of_empty_variables)}(\n\n|\Z)/, '\1')
+ # Otherwise, remove only the line it is in.
+ message = message.gsub(/^#{Regexp.union(names_of_empty_variables)}\n/, '')
end
+ # Substitute all variables with their values.
+ message = message.gsub(Regexp.union(values.keys), values) if values.present?
+
+ message
end
end
end
diff --git a/lib/gitlab/metrics/exporter/base_exporter.rb b/lib/gitlab/metrics/exporter/base_exporter.rb
index 47c862c0232..190d3d3fd2f 100644
--- a/lib/gitlab/metrics/exporter/base_exporter.rb
+++ b/lib/gitlab/metrics/exporter/base_exporter.rb
@@ -11,44 +11,41 @@ module Gitlab
attr_accessor :readiness_checks
- def initialize(settings, **options)
+ def initialize(settings, log_enabled:, log_file:, gc_requests: false, **options)
super(**options)
@settings = settings
+ @gc_requests = gc_requests
+
+ # log_enabled does not exist for all exporters
+ log_sink = log_enabled ? File.join(Rails.root, 'log', log_file) : File::NULL
+ @logger = WEBrick::Log.new(log_sink)
+ @logger.time_format = "[%Y-%m-%dT%H:%M:%S.%L%z]"
end
def enabled?
settings.enabled
end
- def log_filename
- raise NotImplementedError
- end
-
private
- attr_reader :settings
+ attr_reader :settings, :logger
def start_working
- logger = WEBrick::Log.new(log_filename)
- logger.time_format = "[%Y-%m-%dT%H:%M:%S.%L%z]"
-
access_log = [
[logger, WEBrick::AccessLog::COMBINED_LOG_FORMAT]
]
@server = ::WEBrick::HTTPServer.new(
Port: settings.port, BindAddress: settings.address,
- Logger: logger, AccessLog: access_log)
- server.mount_proc '/readiness' do |req, res|
- render_probe(readiness_probe, req, res)
- end
- server.mount_proc '/liveness' do |req, res|
- render_probe(liveness_probe, req, res)
- end
+ Logger: logger, AccessLog: access_log
+ )
server.mount '/', Rack::Handler::WEBrick, rack_app
true
+ rescue StandardError => e
+ logger.error(e)
+ false
end
def run_thread
@@ -72,8 +69,16 @@ module Gitlab
end
def rack_app
+ readiness = readiness_probe
+ liveness = liveness_probe
+ pid = thread_name
+ gc_requests = @gc_requests
+
Rack::Builder.app do
use Rack::Deflater
+ use Gitlab::Metrics::Exporter::MetricsMiddleware, pid
+ use Gitlab::Metrics::Exporter::HealthChecksMiddleware, readiness, liveness
+ use Gitlab::Metrics::Exporter::GcRequestMiddleware if gc_requests
use ::Prometheus::Client::Rack::Exporter if ::Gitlab::Metrics.metrics_folder_present?
run -> (env) { [404, {}, ['']] }
end
@@ -86,14 +91,6 @@ module Gitlab
def liveness_probe
::Gitlab::HealthChecks::Probes::Collection.new
end
-
- def render_probe(probe, req, res)
- result = probe.execute
-
- res.status = result.http_status
- res.content_type = 'application/json; charset=utf-8'
- res.body = result.json.to_json
- end
end
end
end
diff --git a/lib/gitlab/metrics/exporter/gc_request_middleware.rb b/lib/gitlab/metrics/exporter/gc_request_middleware.rb
new file mode 100644
index 00000000000..3806b0e2bd1
--- /dev/null
+++ b/lib/gitlab/metrics/exporter/gc_request_middleware.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Exporter
+ class GcRequestMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env).tap do
+ GC.start
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/exporter/health_checks_middleware.rb b/lib/gitlab/metrics/exporter/health_checks_middleware.rb
new file mode 100644
index 00000000000..c43b8004b72
--- /dev/null
+++ b/lib/gitlab/metrics/exporter/health_checks_middleware.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Exporter
+ class HealthChecksMiddleware
+ def initialize(app, readiness_probe, liveness_probe)
+ @app = app
+ @readiness_probe = readiness_probe
+ @liveness_probe = liveness_probe
+ end
+
+ def call(env)
+ case env['PATH_INFO']
+ when '/readiness' then render_probe(@readiness_probe)
+ when '/liveness' then render_probe(@liveness_probe)
+ else @app.call(env)
+ end
+ end
+
+ private
+
+ def render_probe(probe)
+ result = probe.execute
+
+ [
+ result.http_status,
+ { 'Content-Type' => 'application/json; charset=utf-8' },
+ [result.json.to_json]
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/exporter/metrics_middleware.rb b/lib/gitlab/metrics/exporter/metrics_middleware.rb
new file mode 100644
index 00000000000..e17f1c13cf0
--- /dev/null
+++ b/lib/gitlab/metrics/exporter/metrics_middleware.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Exporter
+ class MetricsMiddleware
+ def initialize(app, pid)
+ @app = app
+ default_labels = {
+ pid: pid
+ }
+ @requests_total = Gitlab::Metrics.counter(
+ :exporter_http_requests_total, 'Total number of HTTP requests', default_labels
+ )
+ @request_durations = Gitlab::Metrics.histogram(
+ :exporter_http_request_duration_seconds,
+ 'HTTP request duration histogram (seconds)',
+ default_labels,
+ [0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
+ )
+ end
+
+ def call(env)
+ start = Gitlab::Metrics::System.monotonic_time
+ @app.call(env).tap do |response|
+ duration = Gitlab::Metrics::System.monotonic_time - start
+
+ labels = {
+ method: env['REQUEST_METHOD'].downcase,
+ path: env['PATH_INFO'].to_s,
+ code: response.first.to_s
+ }
+
+ @requests_total.increment(labels)
+ @request_durations.observe(labels, duration)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
index eea71fda6a0..afecf6546f8 100644
--- a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
+++ b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
@@ -4,12 +4,11 @@ module Gitlab
module Metrics
module Exporter
class SidekiqExporter < BaseExporter
- def log_filename
- if settings['log_enabled']
- File.join(Rails.root, 'log', 'sidekiq_exporter.log')
- else
- File::NULL
- end
+ def initialize(settings, **options)
+ super(settings,
+ log_enabled: settings['log_enabled'],
+ log_file: 'sidekiq_exporter.log',
+ **options)
end
end
end
diff --git a/lib/gitlab/metrics/exporter/web_exporter.rb b/lib/gitlab/metrics/exporter/web_exporter.rb
index d41484aaaa7..c05ad8ccf42 100644
--- a/lib/gitlab/metrics/exporter/web_exporter.rb
+++ b/lib/gitlab/metrics/exporter/web_exporter.rb
@@ -26,8 +26,8 @@ module Gitlab
attr_reader :running
# This exporter is always run on master process
- def initialize
- super(Settings.monitoring.web_exporter)
+ def initialize(**options)
+ super(Settings.monitoring.web_exporter, log_enabled: true, log_file: 'web_exporter.log', **options)
# DEPRECATED:
# these `readiness_checks` are deprecated
@@ -39,10 +39,6 @@ module Gitlab
]
end
- def log_filename
- File.join(Rails.root, 'log', 'web_exporter.log')
- end
-
def mark_as_not_running!
@running = false
end
diff --git a/lib/gitlab/metrics/samplers/action_cable_sampler.rb b/lib/gitlab/metrics/samplers/action_cable_sampler.rb
index adce3030d0d..1f50371cae9 100644
--- a/lib/gitlab/metrics/samplers/action_cable_sampler.rb
+++ b/lib/gitlab/metrics/samplers/action_cable_sampler.rb
@@ -6,8 +6,8 @@ module Gitlab
class ActionCableSampler < BaseSampler
DEFAULT_SAMPLING_INTERVAL_SECONDS = 5
- def initialize(interval = nil, action_cable: ::ActionCable.server)
- super(interval)
+ def initialize(action_cable: ::ActionCable.server, **options)
+ super(**options)
@action_cable = action_cable
end
diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb
index 52d80c3c27e..b2a9de21145 100644
--- a/lib/gitlab/metrics/samplers/base_sampler.rb
+++ b/lib/gitlab/metrics/samplers/base_sampler.rb
@@ -9,7 +9,10 @@ module Gitlab
attr_reader :interval
# interval - The sampling interval in seconds.
- def initialize(interval = nil)
+ # warmup - When true, takes a single sample eagerly before entering the sampling loop.
+ # This can be useful to ensure that all metrics files exist after `start` returns,
+ # since prometheus-client-mmap creates them lazily upon first access.
+ def initialize(interval: nil, logger: Logger.new($stdout), warmup: false, **options)
interval ||= ENV[interval_env_key]&.to_i
interval ||= self.class::DEFAULT_SAMPLING_INTERVAL_SECONDS
interval_half = interval.to_f / 2
@@ -17,13 +20,16 @@ module Gitlab
@interval = interval
@interval_steps = (-interval_half..interval_half).step(0.1).to_a
- super()
+ @logger = logger
+ @warmup = warmup
+
+ super(**options)
end
def safe_sample
sample
rescue StandardError => e
- ::Gitlab::AppLogger.warn("#{self.class}: #{e}, stopping")
+ @logger.warn("#{self.class}: #{e}, stopping")
stop
end
@@ -63,6 +69,8 @@ module Gitlab
def start_working
@running = true
+ safe_sample if @warmup
+
true
end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index b1c5e9800da..d71ee671b8d 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -7,12 +7,12 @@ module Gitlab
DEFAULT_SAMPLING_INTERVAL_SECONDS = 60
GC_REPORT_BUCKETS = [0.01, 0.05, 0.1, 0.2, 0.3, 0.5, 1].freeze
- def initialize(*)
+ def initialize(...)
GC::Profiler.clear
metrics[:process_start_time_seconds].set(labels, Time.now.to_i)
- super
+ super(...)
end
def metrics
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index a047015e54f..0b7b5e23b75 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -116,7 +116,7 @@ module Gitlab
jwt_token = params[param_key]
raise "Empty JWT param: #{param_key}" if jwt_token.blank?
- payload = Gitlab::Workhorse.decode_jwt(jwt_token).first
+ payload = Gitlab::Workhorse.decode_jwt_with_issuer(jwt_token).first
raise "Invalid JWT payload: not a Hash" unless payload.is_a?(Hash)
upload_params = payload.fetch(JWT_PARAM_FIXED_KEY, {})
@@ -172,7 +172,7 @@ module Gitlab
encoded_message = env.delete(RACK_ENV_KEY)
return @app.call(env) if encoded_message.blank?
- message = ::Gitlab::Workhorse.decode_jwt(encoded_message)[0]
+ message = ::Gitlab::Workhorse.decode_jwt_with_issuer(encoded_message)[0]
::Gitlab::Middleware::Multipart::Handler.new(env, message).with_open_files do
@app.call(env)
diff --git a/lib/gitlab/middleware/webhook_recursion_detection.rb b/lib/gitlab/middleware/webhook_recursion_detection.rb
new file mode 100644
index 00000000000..2677445852c
--- /dev/null
+++ b/lib/gitlab/middleware/webhook_recursion_detection.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Middleware
+ class WebhookRecursionDetection
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ headers = ActionDispatch::Request.new(env).headers
+
+ ::Gitlab::WebHooks::RecursionDetection.set_from_headers(headers)
+
+ @app.call(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb
index 98e87e9e915..4e0e5102bec 100644
--- a/lib/gitlab/pages.rb
+++ b/lib/gitlab/pages.rb
@@ -10,7 +10,7 @@ module Gitlab
class << self
def verify_api_request(request_headers)
- decode_jwt_for_issuer('gitlab-pages', request_headers[INTERNAL_API_REQUEST_HEADER])
+ decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER], issuer: 'gitlab-pages')
rescue JWT::DecodeError
false
end
diff --git a/lib/gitlab/pagination/keyset/column_order_definition.rb b/lib/gitlab/pagination/keyset/column_order_definition.rb
index 2b968c4253f..302e7b406b1 100644
--- a/lib/gitlab/pagination/keyset/column_order_definition.rb
+++ b/lib/gitlab/pagination/keyset/column_order_definition.rb
@@ -114,6 +114,20 @@ module Gitlab
# - When the order is a calculated expression or the column is in another table (JOIN-ed)
#
# If the add_to_projections is true, the query builder will automatically add the column to the SELECT values
+ #
+ # **sql_type**
+ #
+ # The SQL type of the column or SQL expression. This is an optional field which is only required when using the
+ # column with the InOperatorOptimization class.
+ #
+ # Example: When the order expression is a calculated SQL expression.
+ #
+ # {
+ # attribute_name: 'id_times_count',
+ # order_expression: Arel.sql('(id * count)').asc,
+ # sql_type: 'integer' # the SQL type here must match with the type of the produced data by the order_expression. Putting 'text' here would be incorrect.
+ # }
+ #
class ColumnOrderDefinition
REVERSED_ORDER_DIRECTIONS = { asc: :desc, desc: :asc }.freeze
REVERSED_NULL_POSITIONS = { nulls_first: :nulls_last, nulls_last: :nulls_first }.freeze
@@ -122,7 +136,8 @@ module Gitlab
attr_reader :attribute_name, :column_expression, :order_expression, :add_to_projections, :order_direction
- def initialize(attribute_name:, order_expression:, column_expression: nil, reversed_order_expression: nil, nullable: :not_nullable, distinct: true, order_direction: nil, add_to_projections: false)
+ # rubocop: disable Metrics/ParameterLists
+ def initialize(attribute_name:, order_expression:, column_expression: nil, reversed_order_expression: nil, nullable: :not_nullable, distinct: true, order_direction: nil, sql_type: nil, add_to_projections: false)
@attribute_name = attribute_name
@order_expression = order_expression
@column_expression = column_expression || calculate_column_expression(order_expression)
@@ -130,8 +145,10 @@ module Gitlab
@reversed_order_expression = reversed_order_expression || calculate_reversed_order(order_expression)
@nullable = parse_nullable(nullable, distinct)
@order_direction = parse_order_direction(order_expression, order_direction)
+ @sql_type = sql_type
@add_to_projections = add_to_projections
end
+ # rubocop: enable Metrics/ParameterLists
def reverse
self.class.new(
@@ -185,6 +202,12 @@ module Gitlab
sql_string
end
+ def sql_type
+ raise Gitlab::Pagination::Keyset::SqlTypeMissingError.for_column(self) if @sql_type.nil?
+
+ @sql_type
+ end
+
private
attr_reader :reversed_order_expression, :nullable, :distinct
diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb
index 3f620f74eca..93b28661bb0 100644
--- a/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb
+++ b/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb
@@ -4,23 +4,35 @@ module Gitlab
module Pagination
module Keyset
module InOperatorOptimization
+ # This class is used for wrapping an Arel column with
+ # convenient helper methods in order to make the query
+ # building for the InOperatorOptimization a bit cleaner.
class ColumnData
attr_reader :original_column_name, :as, :arel_table
- def initialize(original_column_name, as, arel_table)
- @original_column_name = original_column_name.to_s
+ # column - name of the DB column
+ # as - custom alias for the column
+ # arel_table - relation where the column is located
+ def initialize(column, as, arel_table)
+ @original_column_name = column
@as = as.to_s
@arel_table = arel_table
end
+ # Generates: `issues.name AS my_alias`
def projection
arel_column.as(as)
end
+ # Generates: issues.name`
def arel_column
arel_table[original_column_name]
end
+ # overridden in OrderByColumnData class
+ alias_method :column_expression, :arel_column
+
+ # Generates: `issues.my_alias`
def arel_column_as
arel_table[as]
end
@@ -29,8 +41,9 @@ module Gitlab
"#{arel_table.name}_#{original_column_name}_array"
end
+ # Generates: SELECT ARRAY_AGG(...) AS issues_name_array
def array_aggregated_column
- Arel::Nodes::NamedFunction.new('ARRAY_AGG', [arel_column]).as(array_aggregated_column_name)
+ Arel::Nodes::NamedFunction.new('ARRAY_AGG', [column_expression]).as(array_aggregated_column_name)
end
end
end
diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data.rb
new file mode 100644
index 00000000000..9cb1ba1542d
--- /dev/null
+++ b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ module Keyset
+ module InOperatorOptimization
+ class OrderByColumnData < ColumnData
+ extend ::Gitlab::Utils::Override
+
+ attr_reader :column
+
+ # column - a ColumnOrderDefinition object
+ # as - custom alias for the column
+ # arel_table - relation where the column is located
+ def initialize(column, as, arel_table)
+ super(column.attribute_name.to_s, as, arel_table)
+ @column = column
+ end
+
+ override :arel_column
+ def arel_column
+ column.column_expression
+ end
+
+ override :column_expression
+ def column_expression
+ arel_table[original_column_name]
+ end
+
+ def column_for_projection
+ column.column_expression.as(original_column_name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb
index d8c69a74e6b..d6513114d08 100644
--- a/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb
+++ b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb
@@ -9,16 +9,16 @@ module Gitlab
# This class exposes collection methods for the order by columns
#
- # Example: by modelling the `issues.created_at ASC, issues.id ASC` ORDER BY
+ # Example: by modeling the `issues.created_at ASC, issues.id ASC` ORDER BY
# SQL clause, this class will receive two ColumnOrderDefinition objects
def initialize(columns, arel_table)
@columns = columns.map do |column|
- ColumnData.new(column.attribute_name, "order_by_columns_#{column.attribute_name}", arel_table)
+ OrderByColumnData.new(column, "order_by_columns_#{column.attribute_name}", arel_table)
end
end
def arel_columns
- columns.map(&:arel_column)
+ columns.map(&:column_for_projection)
end
def array_aggregated_columns
diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb
index 53faf8469f2..065a3a0cf20 100644
--- a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb
+++ b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb
@@ -120,7 +120,7 @@ module Gitlab
.from(array_cte)
.join(Arel.sql("LEFT JOIN LATERAL (#{initial_keyset_query.to_sql}) #{table_name} ON TRUE"))
- order_by_columns.each { |column| q.where(column.arel_column.not_eq(nil)) }
+ order_by_columns.each { |column| q.where(column.column_expression.not_eq(nil)) }
q.as('array_scope_lateral_query')
end
@@ -231,7 +231,7 @@ module Gitlab
order
.apply_cursor_conditions(keyset_scope, cursor_values, use_union_optimization: true)
- .reselect(*order_by_columns.arel_columns)
+ .reselect(*order_by_columns.map(&:column_for_projection))
.limit(1)
end
diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb
index fc2b56048f6..932aa0c2d28 100644
--- a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb
+++ b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb
@@ -12,11 +12,7 @@ module Gitlab
end
def initializer_columns
- order_by_columns.map do |column|
- column_name = column.original_column_name.to_s
- type = model.columns_hash[column_name].sql_type
- "NULL::#{type} AS #{column_name}"
- end
+ order_by_columns.map { |column_data| null_with_type_cast(column_data) }
end
def columns
@@ -30,6 +26,15 @@ module Gitlab
private
attr_reader :model, :order_by_columns
+
+ def null_with_type_cast(column_data)
+ column_name = column_data.original_column_name.to_s
+ active_record_column = model.columns_hash[column_name]
+
+ type = active_record_column ? active_record_column.sql_type : column_data.column.sql_type
+
+ "NULL::#{type} AS #{column_name}"
+ end
end
end
end
diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb
index b12c33d6e51..51f38c1da58 100644
--- a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb
+++ b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb
@@ -9,6 +9,8 @@ module Gitlab
RECORDS_COLUMN = 'records'
def initialize(finder_query, model, order_by_columns)
+ verify_order_by_attributes_on_model!(model, order_by_columns)
+
@finder_query = finder_query
@order_by_columns = order_by_columns
@table_name = model.table_name
@@ -34,6 +36,20 @@ module Gitlab
private
attr_reader :finder_query, :order_by_columns, :table_name
+
+ def verify_order_by_attributes_on_model!(model, order_by_columns)
+ order_by_columns.map(&:column).each do |column|
+ unless model.columns_hash[column.attribute_name.to_s]
+ text = <<~TEXT
+ The "RecordLoaderStrategy" does not support the following ORDER BY column because
+ it's not available on the \"#{model.table_name}\" table: #{column.attribute_name}
+
+ Omit the "finder_query" parameter to use the "OrderValuesLoaderStrategy".
+ TEXT
+ raise text
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/pagination/keyset/sql_type_missing_error.rb b/lib/gitlab/pagination/keyset/sql_type_missing_error.rb
new file mode 100644
index 00000000000..0525ae13e9c
--- /dev/null
+++ b/lib/gitlab/pagination/keyset/sql_type_missing_error.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+module Gitlab
+ module Pagination
+ module Keyset
+ class SqlTypeMissingError < StandardError
+ def self.for_column(column)
+ message = <<~TEXT
+ The "sql_type" attribute is not set for the following column definition:
+ #{column.attribute_name}
+
+ See the ColumnOrderDefinition class for more context.
+ TEXT
+
+ new(message)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/password.rb b/lib/gitlab/password.rb
new file mode 100644
index 00000000000..00aef8754d6
--- /dev/null
+++ b/lib/gitlab/password.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# This module is used to return fake strong password for tests
+
+module Gitlab
+ module Password
+ DEFAULT_LENGTH = 12
+ TEST_DEFAULT = "123qweQWE!@#" + "0" * (User.password_length.max - DEFAULT_LENGTH)
+ def self.test_default(length = 12)
+ password_length = [[User.password_length.min, length].max, User.password_length.max].min
+ TEST_DEFAULT[...password_length]
+ end
+ end
+end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
deleted file mode 100644
index 500b62bf0e8..00000000000
--- a/lib/gitlab/redis/multi_store.rb
+++ /dev/null
@@ -1,229 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Redis
- class MultiStore
- include Gitlab::Utils::StrongMemoize
-
- class ReadFromPrimaryError < StandardError
- def message
- 'Value not found on the redis primary store. Read from the redis secondary store successful.'
- end
- end
- class MethodMissingError < StandardError
- def message
- 'Method missing. Falling back to execute method on the redis secondary store.'
- end
- end
-
- attr_reader :primary_store, :secondary_store, :instance_name
-
- FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis primary_store.'
- FAILED_TO_WRITE_ERROR_MESSAGE = 'Failed to write to the redis primary_store.'
-
- SKIP_LOG_METHOD_MISSING_FOR_COMMANDS = %i(info).freeze
-
- READ_COMMANDS = %i(
- get
- mget
- smembers
- scard
- ).freeze
-
- WRITE_COMMANDS = %i(
- set
- setnx
- setex
- sadd
- srem
- del
- pipelined
- flushdb
- ).freeze
-
- def initialize(primary_store, secondary_store, instance_name)
- @primary_store = primary_store
- @secondary_store = secondary_store
- @instance_name = instance_name
-
- validate_stores!
- end
- # rubocop:disable GitlabSecurity/PublicSend
- READ_COMMANDS.each do |name|
- define_method(name) do |*args, &block|
- if use_primary_and_secondary_stores?
- read_command(name, *args, &block)
- else
- default_store.send(name, *args, &block)
- end
- end
- end
-
- WRITE_COMMANDS.each do |name|
- define_method(name) do |*args, &block|
- if use_primary_and_secondary_stores?
- write_command(name, *args, &block)
- else
- default_store.send(name, *args, &block)
- end
- end
- end
-
- def method_missing(...)
- return @instance.send(...) if @instance
-
- log_method_missing(...)
-
- default_store.send(...)
- end
- # rubocop:enable GitlabSecurity/PublicSend
-
- def respond_to_missing?(command_name, include_private = false)
- true
- end
-
- # This is needed because of Redis::Rack::Connection is requiring Redis::Store
- # https://github.com/redis-store/redis-rack/blob/a833086ba494083b6a384a1a4e58b36573a9165d/lib/redis/rack/connection.rb#L15
- # Done similarly in https://github.com/lsegal/yard/blob/main/lib/yard/templates/template.rb#L122
- def is_a?(klass)
- return true if klass == default_store.class
-
- super(klass)
- end
- alias_method :kind_of?, :is_a?
-
- def to_s
- use_primary_and_secondary_stores? ? primary_store.to_s : default_store.to_s
- end
-
- def use_primary_and_secondary_stores?
- feature_flags_available? &&
- Feature.enabled?("use_primary_and_secondary_stores_for_#{instance_name.underscore}", default_enabled: :yaml) &&
- !same_redis_store?
- end
-
- def use_primary_store_as_default?
- feature_flags_available? &&
- Feature.enabled?("use_primary_store_as_default_for_#{instance_name.underscore}", default_enabled: :yaml) &&
- !same_redis_store?
- end
-
- private
-
- def default_store
- use_primary_store_as_default? ? primary_store : secondary_store
- end
-
- def log_method_missing(command_name, *_args)
- return if SKIP_LOG_METHOD_MISSING_FOR_COMMANDS.include?(command_name)
-
- log_error(MethodMissingError.new, command_name)
- increment_method_missing_count(command_name)
- end
-
- def read_command(command_name, *args, &block)
- if @instance
- send_command(@instance, command_name, *args, &block)
- else
- read_one_with_fallback(command_name, *args, &block)
- end
- end
-
- def write_command(command_name, *args, &block)
- if @instance
- send_command(@instance, command_name, *args, &block)
- else
- write_both(command_name, *args, &block)
- end
- end
-
- def read_one_with_fallback(command_name, *args, &block)
- begin
- value = send_command(primary_store, command_name, *args, &block)
- rescue StandardError => e
- log_error(e, command_name,
- multi_store_error_message: FAILED_TO_READ_ERROR_MESSAGE)
- end
-
- value ||= fallback_read(command_name, *args, &block)
-
- value
- end
-
- def fallback_read(command_name, *args, &block)
- value = send_command(secondary_store, command_name, *args, &block)
-
- if value
- log_error(ReadFromPrimaryError.new, command_name)
- increment_read_fallback_count(command_name)
- end
-
- value
- end
-
- def write_both(command_name, *args, &block)
- begin
- send_command(primary_store, command_name, *args, &block)
- rescue StandardError => e
- log_error(e, command_name,
- multi_store_error_message: FAILED_TO_WRITE_ERROR_MESSAGE)
- end
-
- send_command(secondary_store, command_name, *args, &block)
- end
-
- def same_redis_store?
- strong_memoize(:same_redis_store) do
- # <Redis client v4.4.0 for redis:///path_to/redis/redis.socket/5>"
- primary_store.inspect == secondary_store.inspect
- end
- end
-
- # rubocop:disable GitlabSecurity/PublicSend
- def send_command(redis_instance, command_name, *args, &block)
- if block_given?
- # Make sure that block is wrapped and executed only on the redis instance that is executing the block
- redis_instance.send(command_name, *args) do |*params|
- with_instance(redis_instance, *params, &block)
- end
- else
- redis_instance.send(command_name, *args)
- end
- end
- # rubocop:enable GitlabSecurity/PublicSend
-
- def with_instance(instance, *params)
- @instance = instance
-
- yield(*params)
- ensure
- @instance = nil
- end
-
- def increment_read_fallback_count(command_name)
- @read_fallback_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_read_fallback_total, 'Client side Redis MultiStore reading fallback')
- @read_fallback_counter.increment(command: command_name, instance_name: instance_name)
- end
-
- def increment_method_missing_count(command_name)
- @method_missing_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_method_missing_total, 'Client side Redis MultiStore method missing')
- @method_missing_counter.increment(command: command_name, instance_name: instance_name)
- end
-
- def validate_stores!
- raise ArgumentError, 'primary_store is required' unless primary_store
- raise ArgumentError, 'secondary_store is required' unless secondary_store
- raise ArgumentError, 'instance_name is required' unless instance_name
- raise ArgumentError, 'invalid primary_store' unless primary_store.is_a?(::Redis)
- raise ArgumentError, 'invalid secondary_store' unless secondary_store.is_a?(::Redis)
- end
-
- def log_error(exception, command_name, extra = {})
- Gitlab::ErrorTracking.log_exception(
- exception,
- command_name: command_name,
- extra: extra.merge(instance_name: instance_name))
- end
- end
- end
-end
diff --git a/lib/gitlab/redis/sessions.rb b/lib/gitlab/redis/sessions.rb
index c547828d907..ddcfdf6e798 100644
--- a/lib/gitlab/redis/sessions.rb
+++ b/lib/gitlab/redis/sessions.rb
@@ -9,39 +9,9 @@ module Gitlab
IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2'
OTP_SESSIONS_NAMESPACE = 'session:otp'
- class << self
- # The data we store on Sessions used to be stored on SharedState.
- def config_fallback
- SharedState
- end
-
- private
-
- def redis
- # Don't use multistore if redis.sessions configuration is not provided
- return super if config_fallback?
-
- primary_store = ::Redis.new(params)
- secondary_store = ::Redis.new(config_fallback.params)
-
- MultiStore.new(primary_store, secondary_store, store_name)
- end
- end
-
- def store(extras = {})
- # Don't use multistore if redis.sessions configuration is not provided
- return super if self.class.config_fallback?
-
- primary_store = create_redis_store(redis_store_options, extras)
- secondary_store = create_redis_store(self.class.config_fallback.params, extras)
-
- MultiStore.new(primary_store, secondary_store, self.class.store_name)
- end
-
- private
-
- def create_redis_store(options, extras)
- ::Redis::Store.new(options.merge(extras))
+ # The data we store on Sessions used to be stored on SharedState.
+ def self.config_fallback
+ SharedState
end
end
end
diff --git a/lib/gitlab/redis/sessions_store_helper.rb b/lib/gitlab/redis/sessions_store_helper.rb
deleted file mode 100644
index c80442847f1..00000000000
--- a/lib/gitlab/redis/sessions_store_helper.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Redis
- module SessionsStoreHelper
- extend ActiveSupport::Concern
-
- module StoreMethods
- def redis_store_class
- use_redis_session_store? ? Gitlab::Redis::Sessions : Gitlab::Redis::SharedState
- end
-
- private
-
- def use_redis_session_store?
- Gitlab::Utils.to_boolean(ENV['GITLAB_USE_REDIS_SESSIONS_STORE'], default: true)
- end
- end
-
- include StoreMethods
-
- included do
- extend StoreMethods
- end
- end
- end
-end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 8e139ae0709..b07b9c79858 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -79,7 +79,11 @@ module Gitlab
def nuget_version_regex
@nuget_version_regex ||= /
- \A#{_semver_major_minor_patch_regex}(\.\d*)?#{_semver_prerelease_build_regex}\z
+ \A#{_semver_major_regex}
+ \.#{_semver_minor_regex}
+ (\.#{_semver_patch_regex})?
+ (\.\d*)?
+ #{_semver_prerelease_build_regex}\z
/x.freeze
end
@@ -167,9 +171,25 @@ module Gitlab
# regexes rather than being used alone.
def _semver_major_minor_patch_regex
@_semver_major_minor_patch_regex ||= /
+ #{_semver_major_regex}\.#{_semver_minor_regex}\.#{_semver_patch_regex}
+ /x.freeze
+ end
+
+ def _semver_major_regex
+ @_semver_major_regex ||= /
(?<major>0|[1-9]\d*)
- \.(?<minor>0|[1-9]\d*)
- \.(?<patch>0|[1-9]\d*)
+ /x.freeze
+ end
+
+ def _semver_minor_regex
+ @_semver_minor_regex ||= /
+ (?<minor>0|[1-9]\d*)
+ /x.freeze
+ end
+
+ def _semver_patch_regex
+ @_semver_patch_regex ||= /
+ (?<patch>0|[1-9]\d*)
/x.freeze
end
diff --git a/lib/gitlab/repository_archive_rate_limiter.rb b/lib/gitlab/repository_archive_rate_limiter.rb
index 31a3dc34bf6..d395b1aba7f 100644
--- a/lib/gitlab/repository_archive_rate_limiter.rb
+++ b/lib/gitlab/repository_archive_rate_limiter.rb
@@ -3,7 +3,7 @@
module Gitlab
module RepositoryArchiveRateLimiter
def check_archive_rate_limit!(current_user, project, &block)
- return unless Feature.enabled?(:archive_rate_limit)
+ return unless Feature.enabled?(:archive_rate_limit, default_enabled: :yaml)
threshold = current_user ? nil : 100
diff --git a/lib/gitlab/search/params.rb b/lib/gitlab/search/params.rb
index e6a1305a82a..1ae14e5e618 100644
--- a/lib/gitlab/search/params.rb
+++ b/lib/gitlab/search/params.rb
@@ -7,6 +7,7 @@ module Gitlab
SEARCH_CHAR_LIMIT = 4096
SEARCH_TERM_LIMIT = 64
+ MIN_TERM_LENGTH = 3
# Generic validation
validates :query_string, length: { maximum: SEARCH_CHAR_LIMIT }
@@ -53,6 +54,10 @@ module Gitlab
errors[:query_string].none? { |msg| msg.include? SEARCH_TERM_LIMIT.to_s }
end
+ def email_lookup?
+ search_terms.any? { |term| term =~ URI::MailTo::EMAIL_REGEXP }
+ end
+
def validate
if detect_abuse?
abuse_detection.validate
@@ -75,8 +80,12 @@ module Gitlab
@detect_abuse
end
+ def search_terms
+ @search_terms ||= query_string.split.select { |word| word.length >= MIN_TERM_LENGTH }
+ end
+
def not_too_many_terms
- if query_string.split.count { |word| word.length >= 3 } > SEARCH_TERM_LIMIT
+ if search_terms.count > SEARCH_TERM_LIMIT
errors.add :query_string, "has too many search terms (maximum is #{SEARCH_TERM_LIMIT})"
end
end
diff --git a/lib/gitlab/sherlock.rb b/lib/gitlab/sherlock.rb
deleted file mode 100644
index a1471c9de47..00000000000
--- a/lib/gitlab/sherlock.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'securerandom'
-
-module Gitlab
- module Sherlock
- @collection = Collection.new
-
- class << self
- attr_reader :collection
- end
-
- def self.enabled?
- Rails.env.development? && !!ENV['ENABLE_SHERLOCK']
- end
-
- def self.enable_line_profiler?
- RUBY_ENGINE == 'ruby'
- end
- end
-end
diff --git a/lib/gitlab/sherlock/collection.rb b/lib/gitlab/sherlock/collection.rb
deleted file mode 100644
index ce3a376cf75..00000000000
--- a/lib/gitlab/sherlock/collection.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- # A collection of transactions recorded by Sherlock.
- #
- # Method calls for this class are synchronized using a mutex to allow
- # sharing of a single Collection instance between threads (e.g. when using
- # Puma as a webserver).
- class Collection
- include Enumerable
-
- def initialize
- @transactions = []
- @mutex = Mutex.new
- end
-
- def add(transaction)
- synchronize { @transactions << transaction }
- end
-
- alias_method :<<, :add
-
- def each(&block)
- synchronize { @transactions.each(&block) }
- end
-
- def clear
- synchronize { @transactions.clear }
- end
-
- def empty?
- synchronize { @transactions.empty? }
- end
-
- def find_transaction(id)
- find { |trans| trans.id == id }
- end
-
- def newest_first
- sort { |a, b| b.finished_at <=> a.finished_at }
- end
-
- private
-
- def synchronize(&block)
- @mutex.synchronize(&block)
- end
- end
- end
-end
diff --git a/lib/gitlab/sherlock/file_sample.rb b/lib/gitlab/sherlock/file_sample.rb
deleted file mode 100644
index 5d10d8c4877..00000000000
--- a/lib/gitlab/sherlock/file_sample.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- class FileSample
- attr_reader :id, :file, :line_samples, :events, :duration
-
- # file - The full path to the file this sample belongs to.
- # line_samples - An array of LineSample objects.
- # duration - The total execution time in milliseconds.
- # events - The total amount of events.
- def initialize(file, line_samples, duration, events)
- @id = SecureRandom.uuid
- @file = file
- @line_samples = line_samples
- @duration = duration
- @events = events
- end
-
- def relative_path
- @relative_path ||= @file.gsub(%r{^#{Rails.root}/?}, '')
- end
-
- def to_param
- @id
- end
-
- def source
- @source ||= File.read(@file)
- end
- end
- end
-end
diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb
deleted file mode 100644
index aa25eb5a571..00000000000
--- a/lib/gitlab/sherlock/line_profiler.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- # Class for profiling code on a per line basis.
- #
- # The LineProfiler class can be used to profile code on per line basis
- # without littering your code with Ruby implementation specific profiling
- # methods.
- #
- # This profiler only includes samples taking longer than a given threshold
- # and those that occur in the actual application (e.g. files from Gems are
- # ignored).
- class LineProfiler
- # The minimum amount of time that has to be spent in a file for it to be
- # included in a list of samples.
- MINIMUM_DURATION = 10.0
-
- # Profiles the given block.
- #
- # Example:
- #
- # profiler = LineProfiler.new
- #
- # retval, samples = profiler.profile do
- # "cats are amazing"
- # end
- #
- # retval # => "cats are amazing"
- # samples # => [#<Gitlab::Sherlock::FileSample ...>, ...]
- #
- # Returns an Array containing the block's return value and an Array of
- # FileSample objects.
- def profile(&block)
- if mri?
- profile_mri(&block)
- else
- raise NotImplementedError,
- 'Line profiling is not supported on this platform'
- end
- end
-
- # Profiles the given block using rblineprof (MRI only).
- def profile_mri
- require 'rblineprof'
-
- retval = nil
- samples = lineprof(/^#{Rails.root}/) { retval = yield }
-
- file_samples = aggregate_rblineprof(samples)
-
- [retval, file_samples]
- end
-
- # Returns an Array of file samples based on the output of rblineprof.
- #
- # lineprof_stats - A Hash containing rblineprof statistics on a per file
- # basis.
- #
- # Returns an Array of FileSample objects.
- def aggregate_rblineprof(lineprof_stats)
- samples = []
-
- lineprof_stats.each do |(file, stats)|
- source_lines = File.read(file).each_line.to_a
- line_samples = []
-
- total_duration = microsec_to_millisec(stats[0][0])
- total_events = stats[0][2]
-
- next if total_duration <= MINIMUM_DURATION
-
- stats[1..].each_with_index do |data, index|
- next unless source_lines[index]
-
- duration = microsec_to_millisec(data[0])
- events = data[2]
-
- line_samples << LineSample.new(duration, events)
- end
-
- samples << FileSample
- .new(file, line_samples, total_duration, total_events)
- end
-
- samples
- end
-
- private
-
- def microsec_to_millisec(microsec)
- microsec / 1000.0
- end
-
- def mri?
- RUBY_ENGINE == 'ruby'
- end
- end
- end
-end
diff --git a/lib/gitlab/sherlock/line_sample.rb b/lib/gitlab/sherlock/line_sample.rb
deleted file mode 100644
index c92fa9ea1ff..00000000000
--- a/lib/gitlab/sherlock/line_sample.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- class LineSample
- attr_reader :duration, :events
-
- # duration - The execution time in milliseconds.
- # events - The amount of events.
- def initialize(duration, events)
- @duration = duration
- @events = events
- end
-
- # Returns the sample duration percentage relative to the given duration.
- #
- # Example:
- #
- # sample.duration # => 150
- # sample.percentage_of(1500) # => 10.0
- #
- # total_duration - The total duration to compare with.
- #
- # Returns a float
- def percentage_of(total_duration)
- (duration.to_f / total_duration) * 100.0
- end
-
- # Returns true if the current sample takes up the majority of the given
- # duration.
- #
- # total_duration - The total duration to compare with.
- def majority_of?(total_duration)
- percentage_of(total_duration) >= 30
- end
- end
- end
-end
diff --git a/lib/gitlab/sherlock/location.rb b/lib/gitlab/sherlock/location.rb
deleted file mode 100644
index 4bba60f3490..00000000000
--- a/lib/gitlab/sherlock/location.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- class Location
- attr_reader :path, :line
-
- SHERLOCK_DIR = File.dirname(__FILE__)
-
- # Creates a new Location from a `Thread::Backtrace::Location`.
- def self.from_ruby_location(location)
- new(location.path, location.lineno)
- end
-
- # path - The full path of the frame as a String.
- # line - The line number of the frame as a Fixnum.
- def initialize(path, line)
- @path = path
- @line = line
- end
-
- # Returns true if the current frame originated from the application.
- def application?
- @path.start_with?(Rails.root.to_s) && !path.start_with?(SHERLOCK_DIR)
- end
- end
- end
-end
diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb
deleted file mode 100644
index f7b08d58e49..00000000000
--- a/lib/gitlab/sherlock/middleware.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- # Rack middleware used for tracking request metrics.
- class Middleware
- CONTENT_TYPES = %r{text/html|application/json}i.freeze
-
- IGNORE_PATHS = %r{^/sherlock}.freeze
-
- def initialize(app)
- @app = app
- end
-
- # env - A Hash containing Rack environment details.
- def call(env)
- if instrument?(env)
- call_with_instrumentation(env)
- else
- @app.call(env)
- end
- end
-
- def call_with_instrumentation(env)
- trans = transaction_from_env(env)
- retval = trans.run { @app.call(env) }
-
- Sherlock.collection.add(trans)
-
- retval
- end
-
- def instrument?(env)
- !!(env['HTTP_ACCEPT'] =~ CONTENT_TYPES &&
- env['REQUEST_URI'] !~ IGNORE_PATHS)
- end
-
- def transaction_from_env(env)
- Transaction.new(env['REQUEST_METHOD'], env['REQUEST_URI'])
- end
- end
- end
-end
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
deleted file mode 100644
index 6f1d2ad23c1..00000000000
--- a/lib/gitlab/sherlock/query.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- class Query
- attr_reader :id, :query, :started_at, :finished_at, :backtrace
-
- # SQL identifiers that should be prefixed with newlines.
- PREFIX_NEWLINE = %r{
- \s+(FROM
- |(LEFT|RIGHT)?INNER\s+JOIN
- |(LEFT|RIGHT)?OUTER\s+JOIN
- |WHERE
- |AND
- |GROUP\s+BY
- |ORDER\s+BY
- |LIMIT
- |OFFSET)\s+}ix.freeze # Vim indent breaks when this is on a newline :<
-
- # Creates a new Query using a String and a separate Array of bindings.
- #
- # query - A String containing a SQL query, optionally with numeric
- # placeholders (`$1`, `$2`, etc).
- #
- # bindings - An Array of ActiveRecord columns and their values.
- # started_at - The start time of the query as a Time-like object.
- # finished_at - The completion time of the query as a Time-like object.
- #
- # Returns a new Query object.
- def self.new_with_bindings(query, bindings, started_at, finished_at)
- bindings.each_with_index do |(_, value), index|
- quoted_value = ActiveRecord::Base.connection.quote(value)
-
- query = query.gsub("$#{index + 1}", quoted_value)
- end
-
- new(query, started_at, finished_at)
- end
-
- # query - The SQL query as a String (without placeholders).
- # started_at - The start time of the query as a Time-like object.
- # finished_at - The completion time of the query as a Time-like object.
- def initialize(query, started_at, finished_at)
- @id = SecureRandom.uuid
- @query = query
- @started_at = started_at
- @finished_at = finished_at
- @backtrace = caller_locations.map do |loc|
- Location.from_ruby_location(loc)
- end
-
- unless @query.end_with?(';')
- @query = "#{@query};"
- end
- end
-
- # Returns the query duration in milliseconds.
- def duration
- @duration ||= (@finished_at - @started_at) * 1000.0
- end
-
- def to_param
- @id
- end
-
- # Returns a human readable version of the query.
- def formatted_query
- @formatted_query ||= format_sql(@query)
- end
-
- # Returns the last application frame of the backtrace.
- def last_application_frame
- @last_application_frame ||= @backtrace.find(&:application?)
- end
-
- # Returns an Array of application frames (excluding Gems and the likes).
- def application_backtrace
- @application_backtrace ||= @backtrace.select(&:application?)
- end
-
- # Returns the query plan as a String.
- def explain
- unless @explain
- ActiveRecord::Base.connection.transaction do
- @explain = raw_explain(@query).values.flatten.join("\n")
-
- # Roll back any queries that mutate data so we don't mess up
- # anything when running explain on an INSERT, UPDATE, DELETE, etc.
- raise ActiveRecord::Rollback
- end
- end
-
- @explain
- end
-
- private
-
- def raw_explain(query)
- explain = "EXPLAIN ANALYZE #{query};"
-
- ActiveRecord::Base.connection.execute(explain)
- end
-
- def format_sql(query)
- query.each_line
- .map { |line| line.strip }
- .join("\n")
- .gsub(PREFIX_NEWLINE) { "\n#{Regexp.last_match(1)} " }
- end
- end
- end
-end
diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb
deleted file mode 100644
index d04624977dc..00000000000
--- a/lib/gitlab/sherlock/transaction.rb
+++ /dev/null
@@ -1,140 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Sherlock
- class Transaction
- attr_reader :id, :type, :path, :queries, :file_samples, :started_at,
- :finished_at, :view_counts
-
- # type - The type of transaction (e.g. "GET", "POST", etc)
- # path - The path of the transaction (e.g. the HTTP request path)
- def initialize(type, path)
- @id = SecureRandom.uuid
- @type = type
- @path = path
- @queries = []
- @file_samples = []
- @started_at = nil
- @finished_at = nil
- @thread = Thread.current
- @view_counts = Hash.new(0)
- end
-
- # Runs the transaction and returns the block's return value.
- def run
- @started_at = Time.now
-
- retval = with_subscriptions do
- profile_lines { yield }
- end
-
- @finished_at = Time.now
-
- retval
- end
-
- # Returns the duration in seconds.
- def duration
- @duration ||= started_at && finished_at ? finished_at - started_at : 0
- end
-
- # Returns the total query duration in seconds.
- def query_duration
- @query_duration ||= @queries.map { |q| q.duration }.inject(:+) / 1000.0
- end
-
- def to_param
- @id
- end
-
- # Returns the queries sorted in descending order by their durations.
- def sorted_queries
- @queries.sort { |a, b| b.duration <=> a.duration }
- end
-
- # Returns the file samples sorted in descending order by their durations.
- def sorted_file_samples
- @file_samples.sort { |a, b| b.duration <=> a.duration }
- end
-
- # Finds a query by the given ID.
- #
- # id - The query ID as a String.
- #
- # Returns a Query object if one could be found, nil otherwise.
- def find_query(id)
- @queries.find { |query| query.id == id }
- end
-
- # Finds a file sample by the given ID.
- #
- # id - The query ID as a String.
- #
- # Returns a FileSample object if one could be found, nil otherwise.
- def find_file_sample(id)
- @file_samples.find { |sample| sample.id == id }
- end
-
- def profile_lines
- retval = nil
-
- if Sherlock.enable_line_profiler?
- retval, @file_samples = LineProfiler.new.profile { yield }
- else
- retval = yield
- end
-
- retval
- end
-
- def subscribe_to_active_record
- ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
- next unless same_thread?
-
- unless data.fetch(:cached, data[:name] == 'CACHE')
- track_query(data[:sql].strip, data[:binds], start, finish)
- end
- end
- end
-
- def subscribe_to_action_view
- regex = /render_(template|partial)\.action_view/
-
- ActiveSupport::Notifications.subscribe(regex) do |_, start, finish, _, data|
- next unless same_thread?
-
- track_view(data[:identifier])
- end
- end
-
- private
-
- def track_query(query, bindings, start, finish)
- @queries << Query.new_with_bindings(query, bindings, start, finish)
- end
-
- def track_view(path)
- @view_counts[path] += 1
- end
-
- def with_subscriptions
- ar_subscriber = subscribe_to_active_record
- av_subscriber = subscribe_to_action_view
-
- retval = yield
-
- ActiveSupport::Notifications.unsubscribe(ar_subscriber)
- ActiveSupport::Notifications.unsubscribe(av_subscriber)
-
- retval
- end
-
- # In case somebody uses a multi-threaded server locally (e.g. Puma) we
- # _only_ want to track notifications that originate from the transaction
- # thread.
- def same_thread?
- Thread.current == @thread
- end
- end
- end
-end
diff --git a/lib/gitlab/sidekiq_logging/json_formatter.rb b/lib/gitlab/sidekiq_logging/json_formatter.rb
index a6281bbdf26..dd50fef8c3d 100644
--- a/lib/gitlab/sidekiq_logging/json_formatter.rb
+++ b/lib/gitlab/sidekiq_logging/json_formatter.rb
@@ -2,6 +2,7 @@
# This is needed for sidekiq-cluster
require 'json'
+require 'sidekiq/job_retry'
module Gitlab
module SidekiqLogging
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index 3438bc0f3ef..a9bfcce2e0a 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -2,6 +2,8 @@
require 'active_record'
require 'active_record/log_subscriber'
+require 'sidekiq/job_logger'
+require 'sidekiq/job_retry'
module Gitlab
module SidekiqLogging
diff --git a/lib/gitlab/sidekiq_middleware/monitor.rb b/lib/gitlab/sidekiq_middleware/monitor.rb
index ed825dbfd60..d38fed3b768 100644
--- a/lib/gitlab/sidekiq_middleware/monitor.rb
+++ b/lib/gitlab/sidekiq_middleware/monitor.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'sidekiq/job_retry'
+
module Gitlab
module SidekiqMiddleware
class Monitor
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index 120d18f63f2..66417b3697e 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -29,16 +29,15 @@ module Gitlab
# for most jobs.
DEFAULT_EXPIRATION = 30.minutes.to_i
- DEFAULT_VALUE = 1
- DEFAULT_VALUE_MESSAGE = 'Keys using the default value for SidekiqStatus detected'
-
# Starts tracking of the given job.
#
# jid - The Sidekiq job ID
# expire - The expiration time of the Redis key.
- def self.set(jid, expire = DEFAULT_EXPIRATION, value: DEFAULT_VALUE)
+ def self.set(jid, expire = DEFAULT_EXPIRATION)
+ return unless expire
+
Sidekiq.redis do |redis|
- redis.set(key_for(jid), value, ex: expire)
+ redis.set(key_for(jid), 1, ex: expire)
end
end
@@ -94,17 +93,10 @@ module Gitlab
return [] if job_ids.empty?
keys = job_ids.map { |jid| key_for(jid) }
- results = Sidekiq.redis { |redis| redis.mget(*keys) }
-
- if Feature.enabled?(:log_implicit_sidekiq_status_calls, default_enabled: :yaml)
- to_log = keys.zip(results).select do |_key, result|
- result == DEFAULT_VALUE.to_s
- end.map(&:first)
-
- Sidekiq.logger.info(message: DEFAULT_VALUE_MESSAGE, keys: to_log) if to_log.any?
- end
- results.map { |result| !result.nil? }
+ Sidekiq
+ .redis { |redis| redis.mget(*keys) }
+ .map { |result| !result.nil? }
end
# Returns the JIDs that are completed
diff --git a/lib/gitlab/sidekiq_status/client_middleware.rb b/lib/gitlab/sidekiq_status/client_middleware.rb
index cee7270f2fb..8471696dcea 100644
--- a/lib/gitlab/sidekiq_status/client_middleware.rb
+++ b/lib/gitlab/sidekiq_status/client_middleware.rb
@@ -4,10 +4,8 @@ module Gitlab
module SidekiqStatus
class ClientMiddleware
def call(_, job, _, _)
- status_expiration = job['status_expiration'] || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION
- value = job['status_expiration'] ? 2 : Gitlab::SidekiqStatus::DEFAULT_VALUE
+ Gitlab::SidekiqStatus.set(job['jid'], job['status_expiration'])
- Gitlab::SidekiqStatus.set(job['jid'], status_expiration, value: value)
yield
end
end
diff --git a/lib/gitlab/sourcegraph.rb b/lib/gitlab/sourcegraph.rb
index 7ef6ab32bd4..892c4468107 100644
--- a/lib/gitlab/sourcegraph.rb
+++ b/lib/gitlab/sourcegraph.rb
@@ -8,13 +8,14 @@ module Gitlab
end
def feature_available?
- # The sourcegraph_bundle feature could be conditionally applied, so check if `!off?`
- !feature.off?
+ # The sourcegraph feature could be conditionally applied, so check if `!off?`
+ # We also can't just check !off? because the ActiveRecord might not exist yet
+ self.feature_enabled? || !feature.off?
end
def feature_enabled?(actor = nil)
# Some CI jobs grep for Feature.enabled? in our codebase, so it is important this reference stays around.
- Feature.enabled?(:sourcegraph, actor)
+ Feature.enabled?(:sourcegraph, actor, default_enabled: :yaml)
end
private
diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb
index 6df54852d02..314cc5e2db6 100644
--- a/lib/gitlab/ssh_public_key.rb
+++ b/lib/gitlab/ssh_public_key.rb
@@ -2,13 +2,15 @@
module Gitlab
class SSHPublicKey
- Technology = Struct.new(:name, :key_class, :supported_sizes)
+ Technology = Struct.new(:name, :key_class, :supported_sizes, :supported_algorithms)
+ # See https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT for the list of
+ # supported algorithms.
TECHNOLOGIES = [
- Technology.new(:rsa, OpenSSL::PKey::RSA, [1024, 2048, 3072, 4096]),
- Technology.new(:dsa, OpenSSL::PKey::DSA, [1024, 2048, 3072]),
- Technology.new(:ecdsa, OpenSSL::PKey::EC, [256, 384, 521]),
- Technology.new(:ed25519, Net::SSH::Authentication::ED25519::PubKey, [256])
+ Technology.new(:rsa, OpenSSL::PKey::RSA, [1024, 2048, 3072, 4096], %w(ssh-rsa)),
+ Technology.new(:dsa, OpenSSL::PKey::DSA, [1024, 2048, 3072], %w(ssh-dss)),
+ Technology.new(:ecdsa, OpenSSL::PKey::EC, [256, 384, 521], %w(ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521)),
+ Technology.new(:ed25519, Net::SSH::Authentication::ED25519::PubKey, [256], %w(ssh-ed25519))
].freeze
def self.technology(name)
@@ -19,8 +21,20 @@ module Gitlab
TECHNOLOGIES.find { |tech| key.is_a?(tech.key_class) }
end
+ def self.supported_types
+ TECHNOLOGIES.map(&:name)
+ end
+
def self.supported_sizes(name)
- technology(name)&.supported_sizes
+ technology(name).supported_sizes
+ end
+
+ def self.supported_algorithms
+ TECHNOLOGIES.flat_map { |tech| tech.supported_algorithms }
+ end
+
+ def self.supported_algorithms_for_name(name)
+ technology(name).supported_algorithms
end
def self.sanitize(key_content)
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index ac1522b8a6c..228da9ee370 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -13,26 +13,28 @@ module Gitlab
Theme = Struct.new(:id, :name, :css_class, :css_filename, :primary_color)
# All available Themes
- THEMES = [
- Theme.new(1, 'Indigo', 'ui-indigo', 'theme_indigo', '#292961'),
- Theme.new(6, 'Light Indigo', 'ui-light-indigo', 'theme_light_indigo', '#4b4ba3'),
- Theme.new(4, 'Blue', 'ui-blue', 'theme_blue', '#1a3652'),
- Theme.new(7, 'Light Blue', 'ui-light-blue', 'theme_light_blue', '#2261a1'),
- Theme.new(5, 'Green', 'ui-green', 'theme_green', '#0d4524'),
- Theme.new(8, 'Light Green', 'ui-light-green', 'theme_light_green', '#156b39'),
- Theme.new(9, 'Red', 'ui-red', 'theme_red', '#691a16'),
- Theme.new(10, 'Light Red', 'ui-light-red', 'theme_light_red', '#a62e21'),
- Theme.new(2, 'Dark', 'ui-dark', 'theme_dark', '#303030'),
- Theme.new(3, 'Light', 'ui-light', 'theme_light', '#666'),
- Theme.new(11, 'Dark Mode (alpha)', 'gl-dark', nil, '#303030')
- ].freeze
+ def available_themes
+ [
+ Theme.new(1, s_('NavigationTheme|Indigo'), 'ui-indigo', 'theme_indigo', '#292961'),
+ Theme.new(6, s_('NavigationTheme|Light Indigo'), 'ui-light-indigo', 'theme_light_indigo', '#4b4ba3'),
+ Theme.new(4, s_('NavigationTheme|Blue'), 'ui-blue', 'theme_blue', '#1a3652'),
+ Theme.new(7, s_('NavigationTheme|Light Blue'), 'ui-light-blue', 'theme_light_blue', '#2261a1'),
+ Theme.new(5, s_('NavigationTheme|Green'), 'ui-green', 'theme_green', '#0d4524'),
+ Theme.new(8, s_('NavigationTheme|Light Green'), 'ui-light-green', 'theme_light_green', '#156b39'),
+ Theme.new(9, s_('NavigationTheme|Red'), 'ui-red', 'theme_red', '#691a16'),
+ Theme.new(10, s_('NavigationTheme|Light Red'), 'ui-light-red', 'theme_light_red', '#a62e21'),
+ Theme.new(2, s_('NavigationTheme|Dark'), 'ui-dark', 'theme_dark', '#303030'),
+ Theme.new(3, s_('NavigationTheme|Light'), 'ui-light', 'theme_light', '#666'),
+ Theme.new(11, s_('NavigationTheme|Dark Mode (alpha)'), 'gl-dark', nil, '#303030')
+ ]
+ end
# Convenience method to get a space-separated String of all the theme
# classes that might be applied to the `body` element
#
# Returns a String
def body_classes
- THEMES.collect(&:css_class).uniq.join(' ')
+ available_themes.collect(&:css_class).uniq.join(' ')
end
# Get a Theme by its ID
@@ -43,12 +45,12 @@ module Gitlab
#
# Returns a Theme
def by_id(id)
- THEMES.detect { |t| t.id == id } || default
+ available_themes.detect { |t| t.id == id } || default
end
# Returns the number of defined Themes
def count
- THEMES.size
+ available_themes.size
end
# Get the default Theme
@@ -62,7 +64,7 @@ module Gitlab
#
# Yields the Theme object
def each(&block)
- THEMES.each(&block)
+ available_themes.each(&block)
end
# Get the Theme for the specified user, or the default
@@ -79,7 +81,7 @@ module Gitlab
end
def self.valid_ids
- THEMES.map(&:id)
+ available_themes.map(&:id)
end
private
@@ -87,7 +89,7 @@ module Gitlab
def default_id
@default_id ||= begin
id = Gitlab.config.gitlab.default_theme.to_i
- theme_ids = THEMES.map(&:id)
+ theme_ids = available_themes.map(&:id)
theme_ids.include?(id) ? id : APPLICATION_DEFAULT
end
diff --git a/lib/gitlab/tracking/standard_context.rb b/lib/gitlab/tracking/standard_context.rb
index 837390b91fb..542dc476526 100644
--- a/lib/gitlab/tracking/standard_context.rb
+++ b/lib/gitlab/tracking/standard_context.rb
@@ -3,7 +3,7 @@
module Gitlab
module Tracking
class StandardContext
- GITLAB_STANDARD_SCHEMA_URL = 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-7'
+ GITLAB_STANDARD_SCHEMA_URL = 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-8'
GITLAB_RAILS_SOURCE = 'gitlab-rails'
def initialize(namespace: nil, project: nil, user: nil, **extra)
@@ -46,7 +46,8 @@ module Gitlab
extra: extra,
user_id: user&.id,
namespace_id: namespace&.id,
- project_id: project_id
+ project_id: project_id,
+ context_generated_at: Time.current
}
end
diff --git a/lib/gitlab/untrusted_regexp/ruby_syntax.rb b/lib/gitlab/untrusted_regexp/ruby_syntax.rb
index 6adf119aa75..010214cf295 100644
--- a/lib/gitlab/untrusted_regexp/ruby_syntax.rb
+++ b/lib/gitlab/untrusted_regexp/ruby_syntax.rb
@@ -20,13 +20,13 @@ module Gitlab
!!self.fabricate(pattern, fallback: fallback)
end
- def self.fabricate(pattern, fallback: false)
- self.fabricate!(pattern, fallback: fallback)
+ def self.fabricate(pattern, fallback: false, project: nil)
+ self.fabricate!(pattern, fallback: fallback, project: project)
rescue RegexpError
nil
end
- def self.fabricate!(pattern, fallback: false)
+ def self.fabricate!(pattern, fallback: false, project: nil)
raise RegexpError, 'Pattern is not string!' unless pattern.is_a?(String)
matches = pattern.match(PATTERN)
@@ -38,6 +38,16 @@ module Gitlab
raise unless fallback &&
Feature.enabled?(:allow_unsafe_ruby_regexp, default_enabled: false)
+ if Feature.enabled?(:ci_unsafe_regexp_logger, type: :ops, default_enabled: :yaml)
+ Gitlab::AppJsonLogger.info(
+ class: self.class.name,
+ regexp: pattern.to_s,
+ fabricated: 'unsafe ruby regexp',
+ project_id: project&.id,
+ project_path: project&.full_path
+ )
+ end
+
create_ruby_regexp(matches[:regexp], matches[:flags])
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 917c273d3f6..adf920d8b52 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -304,7 +304,8 @@ module Gitlab
# rubocop: disable UsageData/LargeTable
adapter: alt_usage_data { ApplicationRecord.database.adapter_name },
version: alt_usage_data { ApplicationRecord.database.version },
- pg_system_id: alt_usage_data { ApplicationRecord.database.system_id }
+ pg_system_id: alt_usage_data { ApplicationRecord.database.system_id },
+ flavor: alt_usage_data { ApplicationRecord.database.flavor }
# rubocop: enable UsageData/LargeTable
},
mail: {
@@ -521,11 +522,7 @@ module Gitlab
projects_with_disable_overriding_approvers_per_merge_request: count(::Project.where(time_period.merge(disable_overriding_approvers_per_merge_request: true))),
projects_without_disable_overriding_approvers_per_merge_request: count(::Project.where(time_period.merge(disable_overriding_approvers_per_merge_request: [false, nil]))),
remote_mirrors: distinct_count(::Project.with_remote_mirrors.where(time_period), :creator_id),
- snippets: distinct_count(::Snippet.where(time_period), :author_id),
- suggestions: distinct_count(::Note.with_suggestions.where(time_period),
- :author_id,
- start: minimum_id(::User),
- finish: maximum_id(::User))
+ snippets: distinct_count(::Snippet.where(time_period), :author_id)
}.tap do |h|
if time_period.present?
h[:merge_requests_users] = merge_requests_users(time_period)
@@ -552,33 +549,11 @@ module Gitlab
user_auth_by_provider: distinct_count_user_auth_by_provider(time_period),
unique_users_all_imports: unique_users_all_imports(time_period),
bulk_imports: {
- gitlab: DEPRECATED_VALUE,
gitlab_v1: count(::BulkImport.where(**time_period, source_type: :gitlab))
},
project_imports: project_imports(time_period),
issue_imports: issue_imports(time_period),
- group_imports: group_imports(time_period),
-
- # Deprecated data to be removed
- projects_imported: {
- total: DEPRECATED_VALUE,
- gitlab_project: DEPRECATED_VALUE,
- gitlab: DEPRECATED_VALUE,
- github: DEPRECATED_VALUE,
- bitbucket: DEPRECATED_VALUE,
- bitbucket_server: DEPRECATED_VALUE,
- gitea: DEPRECATED_VALUE,
- git: DEPRECATED_VALUE,
- manifest: DEPRECATED_VALUE
- },
- issues_imported: {
- jira: DEPRECATED_VALUE,
- fogbugz: DEPRECATED_VALUE,
- phabricator: DEPRECATED_VALUE,
- csv: DEPRECATED_VALUE
- },
- groups_imported: DEPRECATED_VALUE
- # End of deprecated keys
+ group_imports: group_imports(time_period)
}
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/usage_data_counters/counter_events/package_events.yml b/lib/gitlab/usage_data_counters/counter_events/package_events.yml
index 21d637e7152..a64d0ff7e24 100644
--- a/lib/gitlab/usage_data_counters/counter_events/package_events.yml
+++ b/lib/gitlab/usage_data_counters/counter_events/package_events.yml
@@ -5,12 +5,8 @@
- i_package_conan_delete_package
- i_package_conan_pull_package
- i_package_conan_push_package
-- i_package_container_delete_package
-- i_package_container_pull_package
-- i_package_container_push_package
- i_package_debian_delete_package
- i_package_debian_pull_package
-- i_package_debian_push_package
- i_package_delete_package
- i_package_delete_package_by_deploy_token
- i_package_delete_package_by_guest
@@ -56,9 +52,6 @@
- i_package_rubygems_delete_package
- i_package_rubygems_pull_package
- i_package_rubygems_push_package
-- i_package_tag_delete_package
-- i_package_tag_pull_package
-- i_package_tag_push_package
- i_package_terraform_module_delete_package
- i_package_terraform_module_pull_package
- i_package_terraform_module_push_package
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index 8fc8bb5d344..c6e9db6a314 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -104,7 +104,7 @@ module Gitlab
events_names = events_for_category(category)
event_results = events_names.each_with_object({}) do |event, hash|
- hash["#{event}_weekly"] = unique_events(**weekly_time_range.merge(event_names: [event]))
+ hash["#{event}_weekly"] = unique_events(**weekly_time_range.merge(event_names: [event])) unless event == "i_package_composer_deploy_token"
hash["#{event}_monthly"] = unique_events(**monthly_time_range.merge(event_names: [event]))
end
diff --git a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
index d90960b344c..55ed9a42512 100644
--- a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
+++ b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
@@ -103,6 +103,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_security_dast_on_demand_api_scan
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_security_coverage_fuzzing
category: ci_templates
redis_slot: ci_templates
@@ -539,6 +543,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_implicit_security_dast_on_demand_api_scan
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_implicit_security_coverage_fuzzing
category: ci_templates
redis_slot: ci_templates
diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml
index bb98a0b262a..fc610f1e2d6 100644
--- a/lib/gitlab/usage_data_counters/known_events/common.yml
+++ b/lib/gitlab/usage_data_counters/known_events/common.yml
@@ -368,8 +368,18 @@
redis_slot: testing
category: testing
aggregation: weekly
+- name: users_clicking_license_testing_visiting_external_website
+ redis_slot: testing
+ category: testing
+ aggregation: weekly
# Container Security - Network Policies
- name: clusters_using_network_policies_ui
redis_slot: network_policies
category: network_policies
aggregation: weekly
+# Geo group
+- name: g_geo_proxied_requests
+ category: geo
+ redis_slot: geo
+ aggregation: daily
+ feature_flag: track_geo_proxy_events
diff --git a/lib/gitlab/usage_data_counters/known_events/package_events.yml b/lib/gitlab/usage_data_counters/known_events/package_events.yml
index e5031599dd0..debdbd8614f 100644
--- a/lib/gitlab/usage_data_counters/known_events/package_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/package_events.yml
@@ -15,22 +15,6 @@
category: user_packages
aggregation: weekly
redis_slot: package
-- name: i_package_container_deploy_token
- category: deploy_token_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_container_user
- category: user_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_debian_deploy_token
- category: deploy_token_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_debian_user
- category: user_packages
- aggregation: weekly
- redis_slot: package
- name: i_package_generic_deploy_token
category: deploy_token_packages
aggregation: weekly
@@ -39,14 +23,6 @@
category: user_packages
aggregation: weekly
redis_slot: package
-- name: i_package_golang_deploy_token
- category: deploy_token_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_golang_user
- category: user_packages
- aggregation: weekly
- redis_slot: package
- name: i_package_helm_deploy_token
category: deploy_token_packages
aggregation: weekly
@@ -95,14 +71,6 @@
category: user_packages
aggregation: weekly
redis_slot: package
-- name: i_package_tag_deploy_token
- category: deploy_token_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_tag_user
- category: user_packages
- aggregation: weekly
- redis_slot: package
- name: i_package_terraform_module_deploy_token
category: deploy_token_packages
aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/known_events/quickactions.yml b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
index d831ac02dd1..44f6b42d584 100644
--- a/lib/gitlab/usage_data_counters/known_events/quickactions.yml
+++ b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
@@ -39,6 +39,10 @@
category: quickactions
redis_slot: quickactions
aggregation: weekly
+- name: i_quickactions_clear_health_status
+ category: quickactions
+ redis_slot: quickactions
+ aggregation: weekly
- name: i_quickactions_clone
category: quickactions
redis_slot: quickactions
@@ -263,6 +267,10 @@
category: quickactions
redis_slot: quickactions
aggregation: weekly
+- name: i_quickactions_health_status
+ category: quickactions
+ redis_slot: quickactions
+ aggregation: weekly
- name: i_quickactions_wip
category: quickactions
redis_slot: quickactions
diff --git a/lib/gitlab/utils/sanitize_node_link.rb b/lib/gitlab/utils/sanitize_node_link.rb
index ab5d18e9c8a..b0dfa087fcf 100644
--- a/lib/gitlab/utils/sanitize_node_link.rb
+++ b/lib/gitlab/utils/sanitize_node_link.rb
@@ -8,6 +8,12 @@ module Gitlab
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
ATTRS_TO_SANITIZE = %w(href src data-src data-canonical-src).freeze
+ # sanitize 6.0 requires only a context argument. Do not add any default
+ # arguments to this method.
+ def sanitize_unsafe_links(env)
+ remove_unsafe_links(env)
+ end
+
def remove_unsafe_links(env, remove_invalid_links: true)
node = env[:node]
diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb
index e347168f419..6c182f98dd0 100644
--- a/lib/gitlab/utils/usage_data.rb
+++ b/lib/gitlab/utils/usage_data.rb
@@ -169,7 +169,8 @@ module Gitlab
return -1 if args.any?(&:negative?)
args.sum
- rescue StandardError
+ rescue StandardError => error
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
FALLBACK
end
@@ -179,7 +180,8 @@ module Gitlab
else
value
end
- rescue StandardError
+ rescue StandardError => error
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
fallback
end
@@ -295,13 +297,15 @@ module Gitlab
def redis_usage_counter
yield
- rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent, Gitlab::UsageDataCounters::HLLRedisCounter::EventError
+ rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent, Gitlab::UsageDataCounters::HLLRedisCounter::EventError => error
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
FALLBACK
end
def redis_usage_data_totals(counter)
counter.totals
- rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
+ rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent => error
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
counter.fallback_totals
end
end
diff --git a/lib/gitlab/web_hooks.rb b/lib/gitlab/web_hooks.rb
new file mode 100644
index 00000000000..349c7a020cc
--- /dev/null
+++ b/lib/gitlab/web_hooks.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WebHooks
+ GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
+ end
+end
diff --git a/lib/gitlab/web_hooks/recursion_detection.rb b/lib/gitlab/web_hooks/recursion_detection.rb
new file mode 100644
index 00000000000..1b5350d4a4e
--- /dev/null
+++ b/lib/gitlab/web_hooks/recursion_detection.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+# This module detects and blocks recursive webhook requests.
+#
+# Recursion can happen when a webhook has been configured to make a call
+# to its own GitLab instance (i.e., its API), and during the execution of
+# the call the webhook is triggered again to create an infinite loop of
+# being triggered.
+#
+# Additionally the module blocks a webhook once the number of requests to
+# the instance made by a series of webhooks triggering other webhooks reaches
+# a limit.
+#
+# Blocking recursive webhooks allows GitLab to continue to support workflows
+# that use webhooks to call the API non-recursively, or do not go on to
+# trigger an unreasonable number of other webhooks.
+module Gitlab
+ module WebHooks
+ module RecursionDetection
+ COUNT_LIMIT = 100
+ TOUCH_CACHE_TTL = 30.minutes
+
+ class << self
+ def set_from_headers(headers)
+ uuid = headers[UUID::HEADER]
+
+ return unless uuid
+
+ set_request_uuid(uuid)
+ end
+
+ def set_request_uuid(uuid)
+ UUID.instance.request_uuid = uuid
+ end
+
+ # Before a webhook is executed, `.register!` should be called.
+ # Adds the webhook ID to a cache (see `#cache_key_for_hook` for
+ # details of the cache).
+ def register!(hook)
+ cache_key = cache_key_for_hook(hook)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.multi do
+ redis.sadd(cache_key, hook.id)
+ redis.expire(cache_key, TOUCH_CACHE_TTL)
+ end
+ end
+ end
+
+ # Returns true if the webhook ID is present in the cache, or if the
+ # number of IDs in the cache exceeds the limit (see
+ # `#cache_key_for_hook` for details of the cache).
+ def block?(hook)
+ # If a request UUID has not been set then we know the request was not
+ # made by a webhook, and no recursion is possible.
+ return false unless UUID.instance.request_uuid
+
+ cache_key = cache_key_for_hook(hook)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.sismember(cache_key, hook.id) ||
+ redis.scard(cache_key) >= COUNT_LIMIT
+ end
+ end
+
+ def header(hook)
+ UUID.instance.header(hook)
+ end
+
+ def to_log(hook)
+ {
+ uuid: UUID.instance.uuid_for_hook(hook),
+ ids: ::Gitlab::Redis::SharedState.with { |redis| redis.smembers(cache_key_for_hook(hook)).map(&:to_i) }
+ }
+ end
+
+ private
+
+ # Returns a cache key scoped to a UUID.
+ #
+ # The particular UUID will be either:
+ #
+ # - A UUID that was recycled from the request headers if the request was made by a webhook.
+ # - a new UUID initialized for the webhook.
+ #
+ # This means that cycles of webhooks that are triggered from other webhooks
+ # will share the same cache, and other webhooks will use a new cache.
+ def cache_key_for_hook(hook)
+ [:webhook_recursion_detection, UUID.instance.uuid_for_hook(hook)].join(':')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/web_hooks/recursion_detection/uuid.rb b/lib/gitlab/web_hooks/recursion_detection/uuid.rb
new file mode 100644
index 00000000000..9c52399818d
--- /dev/null
+++ b/lib/gitlab/web_hooks/recursion_detection/uuid.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WebHooks
+ module RecursionDetection
+ class UUID
+ HEADER = "#{::Gitlab::WebHooks::GITLAB_EVENT_HEADER}-UUID"
+
+ include Singleton
+
+ attr_accessor :request_uuid
+
+ def initialize
+ self.new_uuids_for_hooks = {}
+ end
+
+ class << self
+ # Back the Singleton with RequestStore so it is isolated to this request.
+ def instance
+ Gitlab::SafeRequestStore[:web_hook_recursion_detection_uuid] ||= new
+ end
+ end
+
+ # Returns a UUID, which will be either:
+ #
+ # - The UUID that was recycled from the request headers if the request was made by a webhook.
+ # - A new UUID initialized for the webhook.
+ def uuid_for_hook(hook)
+ request_uuid || new_uuid_for_hook(hook)
+ end
+
+ def header(hook)
+ { HEADER => uuid_for_hook(hook) }
+ end
+
+ private
+
+ attr_accessor :new_uuids_for_hooks
+
+ def new_uuid_for_hook(hook)
+ new_uuids_for_hooks[hook.id] ||= SecureRandom.uuid
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 3a905a2e1c5..19d30daa577 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -203,11 +203,11 @@ module Gitlab
end
def verify_api_request!(request_headers)
- decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER])
+ decode_jwt_with_issuer(request_headers[INTERNAL_API_REQUEST_HEADER])
end
- def decode_jwt(encoded_message)
- decode_jwt_for_issuer('gitlab-workhorse', encoded_message)
+ def decode_jwt_with_issuer(encoded_message)
+ decode_jwt(encoded_message, issuer: 'gitlab-workhorse')
end
def secret_path
diff --git a/lib/gitlab_edition.rb b/lib/gitlab_edition.rb
new file mode 100644
index 00000000000..6eb6b52c357
--- /dev/null
+++ b/lib/gitlab_edition.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'pathname'
+
+module GitlabEdition
+ def self.root
+ Pathname.new(File.expand_path('..', __dir__))
+ end
+
+ def self.extensions
+ if jh?
+ %w[ee jh]
+ elsif ee?
+ %w[ee]
+ else
+ %w[]
+ end
+ end
+
+ def self.ee?
+ @is_ee ||=
+ # We use this method when the Rails environment is not loaded. This
+ # means that checking the presence of the License class could result in
+ # this method returning `false`, even for an EE installation.
+ #
+ # The `FOSS_ONLY` is always `string` or `nil`
+ # Thus the nil or empty string will result
+ # in using default value: false
+ #
+ # The behavior needs to be synchronised with
+ # config/helpers/is_ee_env.js
+ root.join('ee/app/models/license.rb').exist? &&
+ !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
+ end
+
+ def self.jh?
+ @is_jh ||=
+ ee? &&
+ root.join('jh').exist? &&
+ !%w[true 1].include?(ENV['EE_ONLY'].to_s)
+ end
+
+ def self.ee
+ yield if ee?
+ end
+
+ def self.jh
+ yield if jh?
+ end
+end
diff --git a/lib/sidebars/groups/menus/ci_cd_menu.rb b/lib/sidebars/groups/menus/ci_cd_menu.rb
index f5bce57f496..a1f98b918e6 100644
--- a/lib/sidebars/groups/menus/ci_cd_menu.rb
+++ b/lib/sidebars/groups/menus/ci_cd_menu.rb
@@ -34,10 +34,8 @@ module Sidebars
)
end
- # TODO Proper policies, such as `read_group_runners`, should be implemented per
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
def show_runners?
- can?(context.current_user, :admin_group, context.group) &&
+ can?(context.current_user, :read_group_runners, context.group) &&
Feature.enabled?(:runner_list_group_view_vue_ui, context.group, default_enabled: :yaml)
end
end
diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb
index f0239ca6a1a..810b467ed2d 100644
--- a/lib/sidebars/groups/menus/settings_menu.rb
+++ b/lib/sidebars/groups/menus/settings_menu.rb
@@ -10,6 +10,7 @@ module Sidebars
add_item(general_menu_item)
add_item(integrations_menu_item)
+ add_item(access_tokens_menu_item)
add_item(group_projects_menu_item)
add_item(repository_menu_item)
add_item(ci_cd_menu_item)
@@ -56,6 +57,19 @@ module Sidebars
)
end
+ def access_tokens_menu_item
+ unless can?(context.current_user, :read_resource_access_tokens, context.group)
+ return ::Sidebars::NilMenuItem.new(item_id: :access_tokens)
+ end
+
+ ::Sidebars::MenuItem.new(
+ title: _('Access Tokens'),
+ link: group_settings_access_tokens_path(context.group),
+ active_routes: { path: 'access_tokens#index' },
+ item_id: :access_tokens
+ )
+ end
+
def group_projects_menu_item
::Sidebars::MenuItem.new(
title: _('Projects'),
diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb
index 1018bdd545b..060a5be5f57 100644
--- a/lib/sidebars/projects/menus/infrastructure_menu.rb
+++ b/lib/sidebars/projects/menus/infrastructure_menu.rb
@@ -100,7 +100,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project),
- active_routes: { controller: [:google_cloud, :service_accounts] },
+ active_routes: { controller: [:google_cloud, :service_accounts, :deployments] },
item_id: :google_cloud
)
end
diff --git a/lib/sidebars/projects/menus/issues_menu.rb b/lib/sidebars/projects/menus/issues_menu.rb
index 3774bec2f13..51eea3d850d 100644
--- a/lib/sidebars/projects/menus/issues_menu.rb
+++ b/lib/sidebars/projects/menus/issues_menu.rb
@@ -8,7 +8,7 @@ module Sidebars
override :configure_menu_items
def configure_menu_items
- return unless can?(context.current_user, :read_issue, context.project)
+ return false unless show_issues_menu_items?
add_item(list_menu_item)
add_item(boards_menu_item)
@@ -70,6 +70,10 @@ module Sidebars
private
+ def show_issues_menu_items?
+ can?(context.current_user, :read_issue, context.project)
+ end
+
def list_menu_item
::Sidebars::MenuItem.new(
title: _('List'),
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index cc10d73f76a..0bca63a64f5 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -9,14 +9,9 @@ namespace :gitlab do
task create: :gitlab_environment do
warn_user_is_not_gitlab
- Rake::Task['gitlab:backup:db:create'].invoke
- Rake::Task['gitlab:backup:repo:create'].invoke
- Rake::Task['gitlab:backup:uploads:create'].invoke
- Rake::Task['gitlab:backup:builds:create'].invoke
- Rake::Task['gitlab:backup:artifacts:create'].invoke
- Rake::Task['gitlab:backup:pages:create'].invoke
- Rake::Task['gitlab:backup:lfs:create'].invoke
- Rake::Task['gitlab:backup:registry:create'].invoke
+ %w(db repo uploads builds artifacts pages lfs terraform_state registry packages).each do |type|
+ Rake::Task["gitlab:backup:#{type}:create"].invoke
+ end
backup = Backup::Manager.new(progress)
backup.write_info
@@ -83,7 +78,9 @@ namespace :gitlab do
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
Rake::Task['gitlab:backup:pages:restore'].invoke unless backup.skipped?('pages')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
+ Rake::Task['gitlab:backup:terraform_state:restore'].invoke unless backup.skipped?('terraform_state')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
+ Rake::Task['gitlab:backup:packages:restore'].invoke unless backup.skipped?('packages')
Rake::Task['gitlab:shell:setup'].invoke
Rake::Task['cache:clear'].invoke
@@ -133,8 +130,12 @@ namespace :gitlab do
if ENV["SKIP"] && ENV["SKIP"].include?("db")
puts_time "[SKIPPED]".color(:cyan)
else
- Backup::Database.new(progress).dump
- puts_time "done".color(:green)
+ begin
+ Backup::Database.new(progress).dump
+ puts_time "done".color(:green)
+ rescue Backup::DatabaseBackupError => e
+ progress.puts "#{e.message}"
+ end
end
end
@@ -166,8 +167,12 @@ namespace :gitlab do
if ENV["SKIP"] && ENV["SKIP"].include?("builds")
puts_time "[SKIPPED]".color(:cyan)
else
- Backup::Builds.new(progress).dump
- puts_time "done".color(:green)
+ begin
+ Backup::Builds.new(progress).dump
+ puts_time "done".color(:green)
+ rescue Backup::FileBackupError => e
+ progress.puts "#{e.message}"
+ end
end
end
@@ -185,8 +190,12 @@ namespace :gitlab do
if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
puts_time "[SKIPPED]".color(:cyan)
else
- Backup::Uploads.new(progress).dump
- puts_time "done".color(:green)
+ begin
+ Backup::Uploads.new(progress).dump
+ puts_time "done".color(:green)
+ rescue Backup::FileBackupError => e
+ progress.puts "#{e.message}"
+ end
end
end
@@ -204,8 +213,12 @@ namespace :gitlab do
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
puts_time "[SKIPPED]".color(:cyan)
else
- Backup::Artifacts.new(progress).dump
- puts_time "done".color(:green)
+ begin
+ Backup::Artifacts.new(progress).dump
+ puts_time "done".color(:green)
+ rescue Backup::FileBackupError => e
+ progress.puts "#{e.message}"
+ end
end
end
@@ -223,8 +236,12 @@ namespace :gitlab do
if ENV["SKIP"] && ENV["SKIP"].include?("pages")
puts_time "[SKIPPED]".color(:cyan)
else
- Backup::Pages.new(progress).dump
- puts_time "done".color(:green)
+ begin
+ Backup::Pages.new(progress).dump
+ puts_time "done".color(:green)
+ rescue Backup::FileBackupError => e
+ progress.puts "#{e.message}"
+ end
end
end
@@ -242,8 +259,12 @@ namespace :gitlab do
if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
puts_time "[SKIPPED]".color(:cyan)
else
- Backup::Lfs.new(progress).dump
- puts_time "done".color(:green)
+ begin
+ Backup::Lfs.new(progress).dump
+ puts_time "done".color(:green)
+ rescue Backup::FileBackupError => e
+ progress.puts "#{e.message}"
+ end
end
end
@@ -254,6 +275,25 @@ namespace :gitlab do
end
end
+ namespace :terraform_state do
+ task create: :gitlab_environment do
+ puts_time "Dumping terraform states ... ".color(:blue)
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("terraform_state")
+ puts_time "[SKIPPED]".color(:cyan)
+ else
+ Backup::TerraformState.new(progress).dump
+ puts_time "done".color(:green)
+ end
+ end
+
+ task restore: :gitlab_environment do
+ puts_time "Restoring terraform states ... ".color(:blue)
+ Backup::TerraformState.new(progress).restore
+ puts_time "done".color(:green)
+ end
+ end
+
namespace :registry do
task create: :gitlab_environment do
puts_time "Dumping container registry images ... ".color(:blue)
@@ -262,8 +302,12 @@ namespace :gitlab do
if ENV["SKIP"] && ENV["SKIP"].include?("registry")
puts_time "[SKIPPED]".color(:cyan)
else
- Backup::Registry.new(progress).dump
- puts_time "done".color(:green)
+ begin
+ Backup::Registry.new(progress).dump
+ puts_time "done".color(:green)
+ rescue Backup::FileBackupError => e
+ progress.puts "#{e.message}"
+ end
end
else
puts_time "[DISABLED]".color(:cyan)
@@ -282,6 +326,25 @@ namespace :gitlab do
end
end
+ namespace :packages do
+ task create: :gitlab_environment do
+ puts_time "Dumping packages ... ".color(:blue)
+
+ if ENV['SKIP'] && ENV['SKIP'].include?('packages')
+ puts_time "[SKIPPED]".color(:cyan)
+ else
+ Backup::Packages.new(progress).dump
+ puts_time "done".color(:green)
+ end
+ end
+
+ task restore: :gitlab_environment do
+ puts_time "Restoring packages ...".color(:blue)
+ Backup::Packages.new(progress).restore
+ puts_time "done".color(:green)
+ end
+ end
+
def puts_time(msg)
progress.puts "#{Time.now} -- #{msg}"
Gitlab::BackupLogger.info(message: "#{Rainbow.uncolor(msg)}")
@@ -302,7 +365,7 @@ namespace :gitlab do
if Feature.enabled?(:gitaly_backup, default_enabled: :yaml)
max_concurrency = ENV['GITLAB_BACKUP_MAX_CONCURRENCY'].presence
max_storage_concurrency = ENV['GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY'].presence
- Backup::GitalyBackup.new(progress, parallel: max_concurrency, parallel_storage: max_storage_concurrency)
+ Backup::GitalyBackup.new(progress, max_parallelism: max_concurrency, storage_parallelism: max_storage_concurrency)
else
Backup::GitalyRpcBackup.new(progress)
end
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 8f033a41e3d..f908a7606fa 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -100,15 +100,13 @@ namespace :gitlab do
namespace :sessions do
desc "GitLab | Cleanup | Sessions | Clean ActiveSession lookup keys"
task active_sessions_lookup_keys: :gitlab_environment do
- use_redis_session_store = Gitlab::Utils.to_boolean(ENV['GITLAB_USE_REDIS_SESSIONS_STORE'], default: true)
- redis_store_class = use_redis_session_store ? Gitlab::Redis::Sessions : Gitlab::Redis::SharedState
session_key_pattern = "#{Gitlab::Redis::Sessions::USER_SESSIONS_LOOKUP_NAMESPACE}:*"
last_save_check = Time.at(0)
wait_time = 10.seconds
cursor = 0
total_users_scanned = 0
- redis_store_class.with do |redis|
+ Gitlab::Redis::Sessions.with do |redis|
begin
cursor, keys = redis.scan(cursor, match: session_key_pattern)
total_users_scanned += keys.count
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 9e733fc3a0f..efb0e1ef1e1 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -170,7 +170,7 @@ namespace :gitlab do
# the `ActiveRecord::Base.connection` might be switched to another one
# This is due to `if should_reconnect`:
# https://github.com/rails/rails/blob/a81aeb63a007ede2fe606c50539417dada9030c7/activerecord/lib/active_record/railties/databases.rake#L622
- ActiveRecord::Base.establish_connection :main
+ ActiveRecord::Base.establish_connection :main # rubocop: disable Database/EstablishConnection
Rake::Task['gitlab:db:create_dynamic_partitions'].invoke
end
diff --git a/lib/tasks/gitlab/docs/compile_deprecations.rake b/lib/tasks/gitlab/docs/compile_deprecations.rake
index dc9788cb0b2..4ac68a9f850 100644
--- a/lib/tasks/gitlab/docs/compile_deprecations.rake
+++ b/lib/tasks/gitlab/docs/compile_deprecations.rake
@@ -4,19 +4,19 @@ namespace :gitlab do
namespace :docs do
desc "Generate deprecation list from individual files"
task :compile_deprecations do
- require_relative '../../../../tooling/deprecations/docs'
-
- File.write(Deprecations::Docs.path, Deprecations::Docs.render)
-
- puts "Deprecations compiled to #{Deprecations::Docs.path}"
+ require_relative '../../../../tooling/docs/deprecation_handling'
+ path = Rails.root.join("doc/update/deprecations.md")
+ File.write(path, Docs::DeprecationHandling.new('deprecation').render)
+ puts "Deprecations compiled to #{path}"
end
desc "Check that the deprecation doc is up to date"
task :check_deprecations do
- require_relative '../../../../tooling/deprecations/docs'
+ require_relative '../../../../tooling/docs/deprecation_handling'
+ path = Rails.root.join("doc/update/deprecations.md")
- contents = Deprecations::Docs.render
- doc = File.read(Deprecations::Docs.path)
+ contents = Docs::DeprecationHandling.new('deprecation').render
+ doc = File.read(path)
if doc == contents
puts "Deprecations doc is up to date."
@@ -25,5 +25,28 @@ namespace :gitlab do
abort
end
end
+
+ desc "Generate removal list from individual files"
+ task :compile_removals do
+ require_relative '../../../../tooling/docs/deprecation_handling'
+ path = Rails.root.join("doc/update/removals.md")
+ File.write(path, Docs::DeprecationHandling.new('removal').render)
+ puts "Removals compiled to #{path}"
+ end
+
+ desc "Check that the removal doc is up to date"
+ task :check_removals do
+ require_relative '../../../../tooling/docs/deprecation_handling'
+ path = Rails.root.join("doc/update/removals.md")
+ contents = Docs::DeprecationHandling.new('removal').render
+ doc = File.read(path)
+
+ if doc == contents
+ puts "Removals doc is up to date."
+ else
+ format_output('Removals doc is outdated! You (or your technical writer) can update it by running `bin/rake gitlab:docs:compile_removals`.')
+ abort
+ end
+ end
end
end
diff --git a/lib/tasks/gitlab/docs/redirect.rake b/lib/tasks/gitlab/docs/redirect.rake
index 123a4775605..e7ece9e0fdd 100644
--- a/lib/tasks/gitlab/docs/redirect.rake
+++ b/lib/tasks/gitlab/docs/redirect.rake
@@ -51,7 +51,7 @@ namespace :gitlab do
post.puts "remove_date: '#{date}'"
post.puts '---'
post.puts
- post.puts "This file was moved to [another location](#{new_path})."
+ post.puts "This document was moved to [another location](#{new_path})."
post.puts
post.puts "<!-- This redirect file can be deleted after <#{date}>. -->"
post.puts "<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->"
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index b01a7902bf2..18c68615637 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -2,41 +2,6 @@
namespace :gitlab do
namespace :gitaly do
- desc 'Installs gitaly for running tests within gitlab-development-kit'
- task :test_install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
- inside_gdk = Rails.env.test? && File.exist?(Rails.root.join('../GDK_ROOT'))
-
- if ENV['FORCE_GITALY_INSTALL'] || !inside_gdk
- Rake::Task["gitlab:gitaly:install"].invoke(*args)
-
- next
- end
-
- gdk_gitaly_dir = ENV.fetch('GDK_GITALY', Rails.root.join('../gitaly'))
-
- # Our test setup expects a git repo, so clone rather than copy
- clone_repo(gdk_gitaly_dir, args.dir, clone_opts: %w[--depth 1]) unless Dir.exist?(args.dir)
-
- # We assume the GDK gitaly already compiled binaries
- build_dir = File.join(gdk_gitaly_dir, '_build')
- FileUtils.cp_r(build_dir, args.dir)
-
- # We assume the GDK gitaly already ran bundle install
- bundle_dir = File.join(gdk_gitaly_dir, 'ruby', '.bundle')
- FileUtils.cp_r(bundle_dir, File.join(args.dir, 'ruby'))
-
- # For completeness we copy this for gitaly's make target
- ruby_bundle_file = File.join(gdk_gitaly_dir, '.ruby-bundle')
- FileUtils.cp_r(ruby_bundle_file, args.dir)
-
- gitaly_binary = File.join(build_dir, 'bin', 'gitaly')
- warn_gitaly_out_of_date!(gitaly_binary, Gitlab::GitalyClient.expected_server_version)
- rescue Errno::ENOENT => e
- puts "Could not copy files, did you run `gdk update`? Error: #{e.message}"
-
- raise
- end
-
desc 'GitLab | Gitaly | Clone and checkout gitaly'
task :clone, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab
@@ -60,9 +25,6 @@ Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]")
storage_paths = { 'default' => args.storage_path }
Gitlab::SetupHelper::Gitaly.create_configuration(args.dir, storage_paths)
- # In CI we run scripts/gitaly-test-build
- next if ENV['CI'].present?
-
Dir.chdir(args.dir) do
Bundler.with_original_env do
env = { "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil }
diff --git a/lib/tasks/gitlab/seed/group_seed.rake b/lib/tasks/gitlab/seed/group_seed.rake
index a9a350fb6c3..491cf782985 100644
--- a/lib/tasks/gitlab/seed/group_seed.rake
+++ b/lib/tasks/gitlab/seed/group_seed.rake
@@ -125,7 +125,7 @@ class GroupSeeder
name: FFaker::Name.name,
email: FFaker::Internet.email,
confirmed_at: DateTime.now,
- password: Devise.friendly_token
+ password: Gitlab::Password.test_default
)
end
diff --git a/lib/version_check.rb b/lib/version_check.rb
index e5a4c244c7a..2d132001f54 100644
--- a/lib/version_check.rb
+++ b/lib/version_check.rb
@@ -16,14 +16,6 @@ class VersionCheck
{ "REFERER": Gitlab.config.gitlab.url }
end
- # This is temporary and will be removed when the new UI is hooked up
- # to the version_check.json endpoint.
- def self.image_url
- encoded_data = Base64.urlsafe_encode64(data.to_json)
-
- "#{host}/check.svg?gitlab_info=#{encoded_data}"
- end
-
def self.url
encoded_data = Base64.urlsafe_encode64(data.to_json)