summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/appearance.rb1
-rw-r--r--lib/api/avatar.rb2
-rw-r--r--lib/api/badges.rb4
-rw-r--r--lib/api/broadcast_messages.rb1
-rw-r--r--lib/api/bulk_imports.rb2
-rw-r--r--lib/api/ci/job_artifacts.rb10
-rw-r--r--lib/api/ci/jobs.rb7
-rw-r--r--lib/api/ci/pipelines.rb4
-rw-r--r--lib/api/ci/runner.rb2
-rw-r--r--lib/api/ci/secure_files.rb2
-rw-r--r--lib/api/clusters/agent_tokens.rb12
-rw-r--r--lib/api/clusters/agents.rb8
-rw-r--r--lib/api/entities/ci/job_request/image.rb2
-rw-r--r--lib/api/entities/ci/job_request/service.rb5
-rw-r--r--lib/api/entities/hook.rb3
-rw-r--r--lib/api/entities/issue.rb10
-rw-r--r--lib/api/entities/personal_access_token_with_details.rb13
-rw-r--r--lib/api/entities/releases/link.rb11
-rw-r--r--lib/api/entities/wiki_page.rb10
-rw-r--r--lib/api/environments.rb11
-rw-r--r--lib/api/error_tracking/client_keys.rb1
-rw-r--r--lib/api/error_tracking/collector.rb1
-rw-r--r--lib/api/error_tracking/project_settings.rb1
-rw-r--r--lib/api/features.rb13
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/helpers.rb39
-rw-r--r--lib/api/helpers/project_stats_refresh_conflicts_helpers.rb15
-rw-r--r--lib/api/helpers/projects_helpers.rb1
-rw-r--r--lib/api/helpers/sse_helpers.rb16
-rw-r--r--lib/api/integrations/jira_connect/subscriptions.rb2
-rw-r--r--lib/api/integrations/slack/events.rb40
-rw-r--r--lib/api/integrations/slack/events/url_verification.rb22
-rw-r--r--lib/api/integrations/slack/request.rb51
-rw-r--r--lib/api/internal/base.rb12
-rw-r--r--lib/api/internal/mail_room.rb6
-rw-r--r--lib/api/internal/workhorse.rb37
-rw-r--r--lib/api/issue_links.rb16
-rw-r--r--lib/api/merge_requests.rb9
-rw-r--r--lib/api/metrics/dashboard/annotations.rb1
-rw-r--r--lib/api/metrics/user_starred_dashboards.rb1
-rw-r--r--lib/api/namespaces.rb5
-rw-r--r--lib/api/personal_access_tokens.rb8
-rw-r--r--lib/api/projects_relation_builder.rb2
-rw-r--r--lib/api/pypi_packages.rb114
-rw-r--r--lib/api/release/links.rb5
-rw-r--r--lib/api/releases.rb3
-rw-r--r--lib/api/terraform/modules/v1/packages.rb4
-rw-r--r--lib/api/terraform/state.rb8
-rw-r--r--lib/api/user_counts.rb1
-rw-r--r--lib/api/users.rb34
-rw-r--r--lib/api/wikis.rb15
-rw-r--r--lib/atlassian/jira_connect/client.rb2
-rw-r--r--lib/backup/manager.rb109
-rw-r--r--lib/backup/repositories.rb33
-rw-r--r--lib/banzai/filter/references/commit_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/issue_reference_filter.rb8
-rw-r--r--lib/banzai/filter/references/merge_request_reference_filter.rb16
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb1
-rw-r--r--lib/bulk_imports/groups/stage.rb18
-rw-r--r--lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb71
-rw-r--r--lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb50
-rw-r--r--lib/bulk_imports/projects/pipelines/releases_pipeline.rb16
-rw-r--r--lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb71
-rw-r--r--lib/bulk_imports/projects/stage.rb26
-rw-r--r--lib/bulk_imports/stage.rb3
-rw-r--r--lib/constraints/repository_redirect_url_constrainer.rb10
-rw-r--r--lib/container_registry/base_client.rb4
-rw-r--r--lib/container_registry/migration.rb15
-rw-r--r--lib/error_tracking/stacktrace_builder.rb61
-rw-r--r--lib/feature.rb30
-rw-r--r--lib/generators/gitlab/usage_metric/templates/numbers_instrumentation_class.rb.template19
-rw-r--r--lib/generators/gitlab/usage_metric_generator.rb11
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb6
-rw-r--r--lib/gitlab/analytics/unique_visits.rb35
-rw-r--r--lib/gitlab/application_context.rb23
-rw-r--r--lib/gitlab/application_rate_limiter.rb7
-rw-r--r--lib/gitlab/audit/unauthenticated_author.rb4
-rw-r--r--lib/gitlab/auth/o_auth/user.rb15
-rw-r--r--lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb43
-rw-r--r--lib/gitlab/background_migration/backfill_project_member_namespace_id.rb37
-rw-r--r--lib/gitlab/background_migration/cleanup_orphaned_routes.rb102
-rw-r--r--lib/gitlab/background_migration/delete_invalid_epic_issues.rb14
-rw-r--r--lib/gitlab/background_migration/fix_merge_request_diff_commit_users.rb137
-rw-r--r--lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb5
-rw-r--r--lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb10
-rw-r--r--lib/gitlab/background_migration/purge_stale_security_scans.rb32
-rw-r--r--lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects.rb24
-rw-r--r--lib/gitlab/base_doorkeeper_controller.rb3
-rw-r--r--lib/gitlab/bitbucket_server_import/project_creator.rb2
-rw-r--r--lib/gitlab/checks/changes_access.rb20
-rw-r--r--lib/gitlab/checks/single_change_access.rb3
-rw-r--r--lib/gitlab/checks/tag_check.rb15
-rw-r--r--lib/gitlab/ci/build/image.rb3
-rw-r--r--lib/gitlab/ci/config/entry/image.rb31
-rw-r--r--lib/gitlab/ci/config/entry/pull_policy.rb34
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb21
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule/changes.rb23
-rw-r--r--lib/gitlab/ci/config/external/file/local.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/project.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/remote.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/template.rb4
-rw-r--r--lib/gitlab/ci/jwt.rb6
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schema_validator.rb10
-rw-r--r--lib/gitlab/ci/pipeline/chain/limit/rate_limit.rb26
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/external.rb4
-rw-r--r--lib/gitlab/ci/reports/coverage_report.rb (renamed from lib/gitlab/ci/reports/coverage_reports.rb)6
-rw-r--r--lib/gitlab/ci/reports/coverage_report_generator.rb53
-rw-r--r--lib/gitlab/ci/runner_upgrade_check.rb24
-rw-r--r--lib/gitlab/ci/templates/Crystal.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Django.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Elixir.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Laravel.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/PHP.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Ruby.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Rust.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Security/DAST-On-Demand-API-Scan.gitlab-ci.yml12
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Terraform.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/trace.rb4
-rw-r--r--lib/gitlab/ci/trace/archive.rb4
-rw-r--r--lib/gitlab/config/entry/node.rb4
-rw-r--r--lib/gitlab/content_security_policy/config_loader.rb6
-rw-r--r--lib/gitlab/daemon.rb16
-rw-r--r--lib/gitlab/data_builder/pipeline.rb24
-rw-r--r--lib/gitlab/database.rb17
-rw-r--r--lib/gitlab/database/background_migration/batched_migration.rb27
-rw-r--r--lib/gitlab/database/background_migration/batched_migration_runner.rb5
-rw-r--r--lib/gitlab/database/batch_count.rb12
-rw-r--r--lib/gitlab/database/batch_counter.rb3
-rw-r--r--lib/gitlab/database/consistency_checker.rb6
-rw-r--r--lib/gitlab/database/gitlab_schema.rb8
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml14
-rw-r--r--lib/gitlab/database/load_balancing/configuration.rb59
-rw-r--r--lib/gitlab/database/load_balancing/connection_proxy.rb1
-rw-r--r--lib/gitlab/database/load_balancing/load_balancer.rb20
-rw-r--r--lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb15
-rw-r--r--lib/gitlab/database/migration.rb1
-rw-r--r--lib/gitlab/database/migration_helpers.rb25
-rw-r--r--lib/gitlab/database/migration_helpers/announce_database.rb23
-rw-r--r--lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb7
-rw-r--r--lib/gitlab/database/migrations/batched_background_migration_helpers.rb83
-rw-r--r--lib/gitlab/database/migrations/reestablished_connection_stack.rb6
-rw-r--r--lib/gitlab/database/partitioning/monthly_strategy.rb4
-rw-r--r--lib/gitlab/database/partitioning/partition_manager.rb13
-rw-r--r--lib/gitlab/database/partitioning/sliding_list_strategy.rb53
-rw-r--r--lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb8
-rw-r--r--lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb2
-rw-r--r--lib/gitlab/database/shared_model.rb9
-rw-r--r--lib/gitlab/devise_failure.rb2
-rw-r--r--lib/gitlab/diff/custom_diff.rb89
-rw-r--r--lib/gitlab/diff/file.rb10
-rw-r--r--lib/gitlab/diff/line.rb2
-rw-r--r--lib/gitlab/diff/rendered/notebook/diff_file.rb87
-rw-r--r--lib/gitlab/diff/rendered/notebook/diff_file_helper.rb108
-rw-r--r--lib/gitlab/email/receiver.rb20
-rw-r--r--lib/gitlab/email/reply_parser.rb22
-rw-r--r--lib/gitlab/error_tracking.rb35
-rw-r--r--lib/gitlab/error_tracking/logger.rb7
-rw-r--r--lib/gitlab/event_store/subscriber.rb1
-rw-r--r--lib/gitlab/event_store/subscription.rb3
-rw-r--r--lib/gitlab/fips.rb10
-rw-r--r--lib/gitlab/fogbugz_import/project_creator.rb13
-rw-r--r--lib/gitlab/form_builders/gitlab_ui_form_builder.rb80
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb21
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb16
-rw-r--r--lib/gitlab/git.rb7
-rw-r--r--lib/gitlab/git/cross_repo_comparer.rb2
-rw-r--r--lib/gitlab/git/diff.rb2
-rw-r--r--lib/gitlab/git/repository.rb4
-rw-r--r--lib/gitlab/git_access.rb58
-rw-r--r--lib/gitlab/git_access_project.rb2
-rw-r--r--lib/gitlab/git_access_snippet.rb13
-rw-r--r--lib/gitlab/git_access_wiki.rb30
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb53
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb40
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb8
-rw-r--r--lib/gitlab/github_import/importer/releases_importer.rb20
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/grape_logging/loggers/queue_duration_logger.rb15
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb16
-rw-r--r--lib/gitlab/graphql/board/issues_connection_extension.rb2
-rw-r--r--lib/gitlab/graphql/deprecation.rb28
-rw-r--r--lib/gitlab/graphql/generic_tracing.rb8
-rw-r--r--lib/gitlab/graphql/loaders/batch_model_loader.rb15
-rw-r--r--lib/gitlab/graphql/markdown_field.rb4
-rw-r--r--lib/gitlab/graphql/present/field_extension.rb1
-rw-r--r--lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb88
-rw-r--r--lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb78
-rw-r--r--lib/gitlab/graphql/query_analyzers/logger_analyzer.rb84
-rw-r--r--lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb62
-rw-r--r--lib/gitlab/hash_digest/facade.rb29
-rw-r--r--lib/gitlab/highlight.rb9
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb2
-rw-r--r--lib/gitlab/i18n.rb18
-rw-r--r--lib/gitlab/import_export/lfs_saver.rb6
-rw-r--r--lib/gitlab/import_export/project/import_export.yml2
-rw-r--r--lib/gitlab/import_sources.rb22
-rw-r--r--lib/gitlab/inactive_projects_deletion_warning_tracker.rb25
-rw-r--r--lib/gitlab/instrumentation_helper.rb5
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb4
-rw-r--r--lib/gitlab/mailgun/webhook_processors/base.rb20
-rw-r--r--lib/gitlab/mailgun/webhook_processors/failure_logger.rb36
-rw-r--r--lib/gitlab/mailgun/webhook_processors/member_invites.rb48
-rw-r--r--lib/gitlab/markdown_cache.rb2
-rw-r--r--lib/gitlab/memory/jemalloc.rb88
-rw-r--r--lib/gitlab/metrics/dashboard/stages/alerts_inserter.rb41
-rw-r--r--lib/gitlab/metrics/samplers/database_sampler.rb2
-rw-r--r--lib/gitlab/metrics/sli.rb12
-rw-r--r--lib/gitlab/middleware/compressed_json.rb3
-rw-r--r--lib/gitlab/object_hierarchy.rb28
-rw-r--r--lib/gitlab/pages_transfer.rb38
-rw-r--r--lib/gitlab/patch/database_config.rb66
-rw-r--r--lib/gitlab/project_stats_refresh_conflicts_logger.rb34
-rw-r--r--lib/gitlab/project_template.rb3
-rw-r--r--lib/gitlab/protocol_access.rb38
-rw-r--r--lib/gitlab/quick_actions/commit_actions.rb2
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb34
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb48
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb36
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb60
-rw-r--r--lib/gitlab/quick_actions/relate_actions.rb2
-rw-r--r--lib/gitlab/rack_attack.rb4
-rw-r--r--lib/gitlab/redis/duplicate_jobs.rb30
-rw-r--r--lib/gitlab/redis/multi_store.rb300
-rw-r--r--lib/gitlab/redis/sidekiq_status.rb24
-rw-r--r--lib/gitlab/regex.rb13
-rw-r--r--lib/gitlab/render_timeout.rb14
-rw-r--r--lib/gitlab/service_desk_email.rb6
-rw-r--r--lib/gitlab/sidekiq_logging/logs_jobs.rb6
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb17
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb25
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed.rb5
-rw-r--r--lib/gitlab/sidekiq_middleware/worker_context/client.rb17
-rw-r--r--lib/gitlab/sidekiq_middleware/worker_context/server.rb4
-rw-r--r--lib/gitlab/sidekiq_status.rb20
-rw-r--r--lib/gitlab/sql/cte.rb8
-rw-r--r--lib/gitlab/ssh/signature.rb74
-rw-r--r--lib/gitlab/ssh_public_key.rb27
-rw-r--r--lib/gitlab/static_site_editor/config/file_config.rb42
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/global.rb39
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb26
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/mount.rb39
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb33
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb26
-rw-r--r--lib/gitlab/static_site_editor/config/generated_config.rb70
-rw-r--r--lib/gitlab/themes.rb4
-rw-r--r--lib/gitlab/tracking/standard_context.rb14
-rw-r--r--lib/gitlab/updated_notes_paginator.rb74
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric.rb40
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb2
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/database_metric.rb2
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/issues_created_from_alerts_metric.rb30
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/issues_with_alert_management_alerts_metric.rb24
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/issues_with_prometheus_alert_events.rb24
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/issues_with_self_managed_prometheus_alert_events.rb24
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric.rb15
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/numbers_metric.rb60
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/unique_active_users_metric.rb29
-rw-r--r--lib/gitlab/usage/metrics/name_suggestion.rb2
-rw-r--r--lib/gitlab/usage/metrics/query.rb36
-rw-r--r--lib/gitlab/usage_data.rb78
-rw-r--r--lib/gitlab/usage_data_counters.rb1
-rw-r--r--lib/gitlab/usage_data_counters/editor_unique_counter.rb9
-rw-r--r--lib/gitlab/usage_data_counters/known_events/code_review_events.yml4
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml19
-rw-r--r--lib/gitlab/usage_data_counters/known_events/quickactions.yml4
-rw-r--r--lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb4
-rw-r--r--lib/gitlab/usage_data_counters/static_site_editor_counter.rb16
-rw-r--r--lib/gitlab/usage_data_queries.rb10
-rw-r--r--lib/gitlab/utils/usage_data.rb9
-rw-r--r--lib/gitlab/web_hooks/rate_limiter.rb70
-rw-r--r--lib/object_storage/direct_upload.rb5
-rw-r--r--lib/security/ci_configuration/sast_build_action.rb24
-rw-r--r--lib/service_ping/build_payload.rb2
-rw-r--r--lib/service_ping/permit_data_categories.rb2
-rw-r--r--lib/sidebars/groups/menus/customer_relations_menu.rb4
-rw-r--r--lib/sidebars/projects/menus/settings_menu.rb7
-rw-r--r--lib/support/systemd/gitlab-sidekiq.service1
-rw-r--r--lib/tasks/contracts.rake50
-rw-r--r--lib/tasks/gitlab/db.rake16
-rw-r--r--lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake68
-rw-r--r--lib/tasks/gitlab/db/lock_writes.rake119
-rw-r--r--lib/tasks/gitlab/db/validate_config.rake77
-rw-r--r--lib/tasks/gitlab/pages.rake54
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake27
-rw-r--r--lib/tasks/migrate/composite_primary_keys.rake17
-rw-r--r--lib/tasks/rubocop.rake2
296 files changed, 4313 insertions, 2090 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 0d74bc841b1..8cafde4fedb 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -131,7 +131,7 @@ module API
# This is a specific exception raised by `rack-timeout` gem when Puma
# requests surpass its timeout. Given it inherits from Exception, we
# should rescue it separately. For more info, see:
- # - https://github.com/sharpstone/rack-timeout/blob/master/doc/exceptions.md
+ # - https://github.com/zombocom/rack-timeout/blob/master/doc/exceptions.md
# - https://github.com/ruby-grape/grape#exception-handling
rescue_from Rack::Timeout::RequestTimeoutException do |exception|
handle_api_exception(exception)
@@ -229,6 +229,7 @@ module API
mount ::API::ImportGithub
mount ::API::Integrations
mount ::API::Integrations::JiraConnect::Subscriptions
+ mount ::API::Integrations::Slack::Events
mount ::API::Invitations
mount ::API::IssueLinks
mount ::API::Issues
@@ -314,6 +315,7 @@ module API
mount ::API::Internal::Kubernetes
mount ::API::Internal::MailRoom
mount ::API::Internal::ContainerRegistry::Migration
+ mount ::API::Internal::Workhorse
version 'v3', using: :path do
# Although the following endpoints are kept behind V3 namespace,
diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb
index 1eaa4167a7d..e599abf4aaf 100644
--- a/lib/api/appearance.rb
+++ b/lib/api/appearance.rb
@@ -5,6 +5,7 @@ module API
before { authenticated_as_admin! }
feature_category :navigation
+ urgency :low
helpers do
def current_appearance
diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb
index bd9fb37e18b..0fb7a4cd435 100644
--- a/lib/api/avatar.rb
+++ b/lib/api/avatar.rb
@@ -3,7 +3,7 @@
module API
class Avatar < ::API::Base
feature_category :users
- urgency :high
+ urgency :medium
resource :avatar do
desc 'Return avatar url for a user' do
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
index 68095fb2975..f969eec8431 100644
--- a/lib/api/badges.rb
+++ b/lib/api/badges.rb
@@ -32,7 +32,7 @@ module API
params do
use :pagination
end
- get ":id/badges", urgency: :default do
+ get ":id/badges", urgency: :low do
source = find_source(source_type, params[:id])
badges = source.badges
@@ -91,7 +91,7 @@ module API
requires :image_url, type: String, desc: 'URL of the badge image'
optional :name, type: String, desc: 'Name for the badge'
end
- post ":id/badges", urgency: :default do
+ post ":id/badges" do
source = find_source_if_admin(source_type)
badge = ::Badges::CreateService.new(declared_params(include_missing: false)).execute(source)
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index e081265b418..b5d68ca5de2 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -5,6 +5,7 @@ module API
include PaginationParams
feature_category :navigation
+ urgency :low
resource :broadcast_messages do
helpers do
diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb
index 766e05eca23..b1cb84c97cb 100644
--- a/lib/api/bulk_imports.rb
+++ b/lib/api/bulk_imports.rb
@@ -47,7 +47,7 @@ module API
requires :source_type, type: String, desc: 'Source entity type (only `group_entity` is supported)',
values: %w[group_entity]
requires :source_full_path, type: String, desc: 'Source full path of the entity to import'
- requires :destination_name, type: String, desc: 'Destination name for the entity'
+ requires :destination_name, type: String, desc: 'Destination slug for the entity'
requires :destination_namespace, type: String, desc: 'Destination namespace for the entity'
end
end
diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb
index 0800993602b..8b332f96be0 100644
--- a/lib/api/ci/job_artifacts.rb
+++ b/lib/api/ci/job_artifacts.rb
@@ -3,6 +3,8 @@
module API
module Ci
class JobArtifacts < ::API::Base
+ helpers ::API::Helpers::ProjectStatsRefreshConflictsHelpers
+
before { authenticate_non_get! }
feature_category :build_artifacts
@@ -35,7 +37,7 @@ module API
latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name])
authorize_read_job_artifacts!(latest_build)
- present_carrierwave_file!(latest_build.artifacts_file)
+ present_artifacts_file!(latest_build.artifacts_file)
end
desc 'Download a specific file from artifacts archive from a ref' do
@@ -76,7 +78,7 @@ module API
build = find_build!(params[:job_id])
authorize_read_job_artifacts!(build)
- present_carrierwave_file!(build.artifacts_file)
+ present_artifacts_file!(build.artifacts_file)
end
desc 'Download a specific file from artifacts archive' do
@@ -137,6 +139,8 @@ module API
build = find_build!(params[:job_id])
authorize!(:destroy_artifacts, build)
+ reject_if_build_artifacts_size_refreshing!(build.project)
+
build.erase_erasable_artifacts!
status :no_content
@@ -146,6 +150,8 @@ module API
delete ':id/artifacts' do
authorize_destroy_artifacts!
+ reject_if_build_artifacts_size_refreshing!(user_project)
+
::Ci::JobArtifacts::DeleteProjectArtifactsService.new(project: user_project).execute
accepted!
diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb
index 04999b5fb44..97471d3c96e 100644
--- a/lib/api/ci/jobs.rb
+++ b/lib/api/ci/jobs.rb
@@ -4,6 +4,9 @@ module API
module Ci
class Jobs < ::API::Base
include PaginationParams
+
+ helpers ::API::Helpers::ProjectStatsRefreshConflictsHelpers
+
before { authenticate! }
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
@@ -137,6 +140,8 @@ module API
authorize!(:erase_build, build)
break forbidden!('Job is not erasable!') unless build.erasable?
+ reject_if_build_artifacts_size_refreshing!(build.project)
+
build.erase(erased_by: current_user)
present build, with: Entities::Ci::Job
end
@@ -204,7 +209,7 @@ module API
.select { |_role, role_access_level| role_access_level <= user_access_level }
.map(&:first)
- environment = if environment_slug = current_authenticated_job.deployment&.environment&.slug
+ environment = if environment_slug = current_authenticated_job.persisted_environment&.slug
{ slug: environment_slug }
end
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 4253a9eb4d7..cd686a28dd2 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -5,6 +5,8 @@ module API
class Pipelines < ::API::Base
include PaginationParams
+ helpers ::API::Helpers::ProjectStatsRefreshConflictsHelpers
+
before { authenticate_non_get! }
params do
@@ -208,6 +210,8 @@ module API
delete ':id/pipelines/:pipeline_id', urgency: :low, feature_category: :continuous_integration do
authorize! :destroy_pipeline, pipeline
+ reject_if_build_artifacts_size_refreshing!(pipeline.project)
+
destroy_conditionally!(pipeline) do
::Ci::DestroyPipelineService.new(user_project, current_user).execute(pipeline)
end
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 4381309fb9e..65dc002e67d 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -330,7 +330,7 @@ module API
authenticate_job!(require_running: false)
end
- present_carrierwave_file!(current_job.artifacts_file, supports_direct_download: params[:direct_download])
+ present_artifacts_file!(current_job.artifacts_file, supports_direct_download: params[:direct_download])
end
end
end
diff --git a/lib/api/ci/secure_files.rb b/lib/api/ci/secure_files.rb
index 6c7f502b428..c1f47dd67ce 100644
--- a/lib/api/ci/secure_files.rb
+++ b/lib/api/ci/secure_files.rb
@@ -26,7 +26,7 @@ module API
end
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
get ':id/secure_files' do
- secure_files = user_project.secure_files
+ secure_files = user_project.secure_files.order_by_created_at
present paginate(secure_files), with: Entities::Ci::SecureFile
end
diff --git a/lib/api/clusters/agent_tokens.rb b/lib/api/clusters/agent_tokens.rb
index 1e52790f26b..1f9c8700d7a 100644
--- a/lib/api/clusters/agent_tokens.rb
+++ b/lib/api/clusters/agent_tokens.rb
@@ -26,9 +26,7 @@ module API
use :pagination
end
get do
- authorize! :read_cluster, user_project
-
- agent = user_project.cluster_agents.find(params[:agent_id])
+ agent = ::Clusters::AgentsFinder.new(user_project, current_user).find(params[:agent_id])
present paginate(agent.agent_tokens), with: Entities::Clusters::AgentTokenBasic
end
@@ -41,9 +39,8 @@ module API
requires :token_id, type: Integer, desc: 'The ID of the agent token'
end
get ':token_id' do
- authorize! :read_cluster, user_project
+ agent = ::Clusters::AgentsFinder.new(user_project, current_user).find(params[:agent_id])
- agent = user_project.cluster_agents.find(params[:agent_id])
token = agent.agent_tokens.find(params[:token_id])
present token, with: Entities::Clusters::AgentToken
@@ -62,7 +59,7 @@ module API
token_params = declared_params(include_missing: false)
- agent = user_project.cluster_agents.find(params[:agent_id])
+ agent = ::Clusters::AgentsFinder.new(user_project, current_user).find(params[:agent_id])
result = ::Clusters::AgentTokens::CreateService.new(
container: agent.project, current_user: current_user, params: token_params.merge(agent_id: agent.id)
@@ -82,7 +79,8 @@ module API
delete ':token_id' do
authorize! :admin_cluster, user_project
- agent = user_project.cluster_agents.find(params[:agent_id])
+ agent = ::Clusters::AgentsFinder.new(user_project, current_user).find(params[:agent_id])
+
token = agent.agent_tokens.find(params[:token_id])
# Skipping explicit error handling and relying on exceptions
diff --git a/lib/api/clusters/agents.rb b/lib/api/clusters/agents.rb
index 0fa556d2da9..2affd9680b6 100644
--- a/lib/api/clusters/agents.rb
+++ b/lib/api/clusters/agents.rb
@@ -22,7 +22,7 @@ module API
use :pagination
end
get ':id/cluster_agents' do
- authorize! :read_cluster, user_project
+ not_found!('ClusterAgents') unless can?(current_user, :read_cluster, user_project)
agents = ::Clusters::AgentsFinder.new(user_project, current_user).execute
@@ -37,9 +37,7 @@ module API
requires :agent_id, type: Integer, desc: 'The ID of an agent'
end
get ':id/cluster_agents/:agent_id' do
- authorize! :read_cluster, user_project
-
- agent = user_project.cluster_agents.find(params[:agent_id])
+ agent = ::Clusters::AgentsFinder.new(user_project, current_user).find(params[:agent_id])
present agent, with: Entities::Clusters::Agent
end
@@ -72,7 +70,7 @@ module API
delete ':id/cluster_agents/:agent_id' do
authorize! :admin_cluster, user_project
- agent = user_project.cluster_agents.find(params.delete(:agent_id))
+ agent = ::Clusters::AgentsFinder.new(user_project, current_user).find(params[:agent_id])
destroy_conditionally!(agent)
end
diff --git a/lib/api/entities/ci/job_request/image.rb b/lib/api/entities/ci/job_request/image.rb
index 8e404a8fa02..83f64da6050 100644
--- a/lib/api/entities/ci/job_request/image.rb
+++ b/lib/api/entities/ci/job_request/image.rb
@@ -7,6 +7,8 @@ module API
class Image < Grape::Entity
expose :name, :entrypoint
expose :ports, using: Entities::Ci::JobRequest::Port
+
+ expose :pull_policy, if: ->(_) { ::Feature.enabled?(:ci_docker_image_pull_policy) }
end
end
end
diff --git a/lib/api/entities/ci/job_request/service.rb b/lib/api/entities/ci/job_request/service.rb
index 0dae5d5a933..d9da2c92ec7 100644
--- a/lib/api/entities/ci/job_request/service.rb
+++ b/lib/api/entities/ci/job_request/service.rb
@@ -4,7 +4,10 @@ module API
module Entities
module Ci
module JobRequest
- class Service < Entities::Ci::JobRequest::Image
+ class Service < Grape::Entity
+ expose :name, :entrypoint
+ expose :ports, using: Entities::Ci::JobRequest::Port
+
expose :alias, :command
expose :variables
end
diff --git a/lib/api/entities/hook.rb b/lib/api/entities/hook.rb
index ac813bcac3f..d176e76b321 100644
--- a/lib/api/entities/hook.rb
+++ b/lib/api/entities/hook.rb
@@ -5,6 +5,9 @@ module API
class Hook < Grape::Entity
expose :id, :url, :created_at, :push_events, :tag_push_events, :merge_requests_events, :repository_update_events
expose :enable_ssl_verification
+
+ expose :alert_status
+ expose :disabled_until
end
end
end
diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb
index f87ef093cd8..1060b2c517a 100644
--- a/lib/api/entities/issue.rb
+++ b/lib/api/entities/issue.rb
@@ -29,6 +29,16 @@ module API
expose :project do |issue|
expose_url(api_v4_projects_path(id: issue.project_id))
end
+
+ expose :closed_as_duplicate_of do |issue|
+ if ::Feature.enabled?(:closed_as_duplicate_of_issues_api, issue.project) &&
+ issue.duplicated? &&
+ options[:current_user]&.can?(:read_issue, issue.duplicated_to)
+ expose_url(
+ api_v4_project_issue_path(id: issue.duplicated_to.project_id, issue_iid: issue.duplicated_to.iid)
+ )
+ end
+ end
end
expose :references, with: IssuableReferences do |issue|
diff --git a/lib/api/entities/personal_access_token_with_details.rb b/lib/api/entities/personal_access_token_with_details.rb
new file mode 100644
index 00000000000..5654bd4a1e1
--- /dev/null
+++ b/lib/api/entities/personal_access_token_with_details.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PersonalAccessTokenWithDetails < Entities::PersonalAccessToken
+ expose :expired?, as: :expired
+ expose :expires_soon?, as: :expires_soon
+ expose :revoke_path do |token|
+ Gitlab::Routing.url_helpers.revoke_profile_personal_access_token_path(token)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/releases/link.rb b/lib/api/entities/releases/link.rb
index c1d83a8924f..5157645af69 100644
--- a/lib/api/entities/releases/link.rb
+++ b/lib/api/entities/releases/link.rb
@@ -7,16 +7,11 @@ module API
expose :id
expose :name
expose :url
- expose :direct_asset_url
+ expose :direct_asset_url do |link|
+ ::Releases::LinkPresenter.new(link).direct_asset_url
+ end
expose :external?, as: :external
expose :link_type
-
- def direct_asset_url
- return object.url unless object.filepath
-
- release = object.release.present
- release.download_url(object.filepath)
- end
end
end
end
diff --git a/lib/api/entities/wiki_page.rb b/lib/api/entities/wiki_page.rb
index 43af6a336d2..5bba4271396 100644
--- a/lib/api/entities/wiki_page.rb
+++ b/lib/api/entities/wiki_page.rb
@@ -6,7 +6,15 @@ module API
include ::MarkupHelper
expose :content do |wiki_page, options|
- options[:render_html] ? render_wiki_content(wiki_page, ref: wiki_page.version.id) : wiki_page.content
+ if options[:render_html]
+ render_wiki_content(
+ wiki_page,
+ ref: wiki_page.version.id,
+ current_user: options[:current_user]
+ )
+ else
+ wiki_page.content
+ end
end
expose :encoding do |wiki_page|
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 11f1cab0c72..c4b67f83941 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -22,16 +22,15 @@ module API
use :pagination
optional :name, type: String, desc: 'Returns the environment with this name'
optional :search, type: String, desc: 'Returns list of environments matching the search criteria'
+ optional :states, type: String, values: Environment.valid_states.map(&:to_s), desc: 'List all environments that match a specific state'
mutually_exclusive :name, :search, message: 'cannot be used together'
end
get ':id/environments' do
authorize! :read_environment, user_project
- environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, params).execute
+ environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, declared_params(include_missing: false)).execute
present paginate(environments), with: Entities::Environment, current_user: current_user
- rescue ::Environments::EnvironmentsFinder::InvalidStatesError => exception
- bad_request!(exception.message)
end
desc 'Creates a new environment' do
@@ -129,14 +128,14 @@ module API
end
params do
requires :environment_id, type: Integer, desc: 'The environment ID'
+ optional :force, type: Boolean, default: false
end
post ':id/environments/:environment_id/stop' do
authorize! :read_environment, user_project
environment = user_project.environments.find(params[:environment_id])
- authorize! :stop_environment, environment
-
- environment.stop_with_actions!(current_user)
+ ::Environments::StopService.new(user_project, current_user, declared_params(include_missing: false))
+ .execute(environment)
status 200
present environment, with: Entities::Environment, current_user: current_user
diff --git a/lib/api/error_tracking/client_keys.rb b/lib/api/error_tracking/client_keys.rb
index d92cf220433..c1c378111a7 100644
--- a/lib/api/error_tracking/client_keys.rb
+++ b/lib/api/error_tracking/client_keys.rb
@@ -5,6 +5,7 @@ module API
before { authenticate! }
feature_category :error_tracking
+ urgency :low
params do
requires :id, type: String, desc: 'The ID of a project'
diff --git a/lib/api/error_tracking/collector.rb b/lib/api/error_tracking/collector.rb
index 29b213eaffb..eea0fd2bce9 100644
--- a/lib/api/error_tracking/collector.rb
+++ b/lib/api/error_tracking/collector.rb
@@ -6,6 +6,7 @@ module API
# sentry backend. For more details see https://gitlab.com/gitlab-org/gitlab/-/issues/329596.
class ErrorTracking::Collector < ::API::Base
feature_category :error_tracking
+ urgency :low
content_type :envelope, 'application/x-sentry-envelope'
content_type :json, 'application/json'
diff --git a/lib/api/error_tracking/project_settings.rb b/lib/api/error_tracking/project_settings.rb
index 74432d1eaec..fefc2098137 100644
--- a/lib/api/error_tracking/project_settings.rb
+++ b/lib/api/error_tracking/project_settings.rb
@@ -5,6 +5,7 @@ module API
before { authenticate! }
feature_category :error_tracking
+ urgency :low
helpers do
def project_setting
diff --git a/lib/api/features.rb b/lib/api/features.rb
index bff2817a2ec..13a6aedc2df 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -68,10 +68,13 @@ module API
requires :value, type: String, desc: '`true` or `false` to enable/disable, a float for percentage of time'
optional :key, type: String, desc: '`percentage_of_actors` or the default `percentage_of_time`'
optional :feature_group, type: String, desc: 'A Feature group name'
- optional :user, type: String, desc: 'A GitLab username'
- optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'"
- optional :namespace, type: String, desc: "A GitLab group or user namespace path, such as 'gitlab-org'"
- optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce'
+ optional :user, type: String, desc: 'A GitLab username or comma-separated multiple usernames'
+ optional :group, type: String,
+ desc: "A GitLab group's path, such as 'gitlab-org', or comma-separated multiple group paths"
+ optional :namespace, type: String,
+ desc: "A GitLab group or user namespace path, such as 'john-doe', or comma-separated multiple namespace paths"
+ optional :project, type: String,
+ desc: "A projects path, such as `gitlab-org/gitlab-ce`, or comma-separated multiple project paths"
optional :force, type: Boolean, desc: 'Skip feature flag validation checks, ie. YAML definition'
mutually_exclusive :key, :feature_group
@@ -110,6 +113,8 @@ module API
present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet
with: Entities::Feature, current_user: current_user
+ rescue Feature::Target::UnknowTargetError => e
+ bad_request!(e.message)
end
desc 'Remove the gate value for the given feature'
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 60bb51bf48f..c17bc432404 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -417,7 +417,7 @@ module API
requires :group_access, type: Integer, values: Gitlab::Access.all_values, desc: 'The group access level'
optional :expires_at, type: Date, desc: 'Share expiration date'
end
- post ":id/share", feature_category: :subgroups do
+ post ":id/share", feature_category: :subgroups, urgency: :low do
shared_with_group = find_group!(params[:group_id])
group_link_create_params = {
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a079c591519..fc1037131d8 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -394,8 +394,7 @@ module API
end
def order_options_with_tie_breaker
- order_by = if Feature.enabled?(:replace_order_by_created_at_with_id) &&
- params[:order_by] == 'created_at'
+ order_by = if params[:order_by] == 'created_at'
'id'
else
params[:order_by]
@@ -409,15 +408,11 @@ module API
# error helpers
def forbidden!(reason = nil)
- message = ['403 Forbidden']
- message << "- #{reason}" if reason
- render_api_error!(message.join(' '), 403)
+ render_api_error_with_reason!(403, '403 Forbidden', reason)
end
def bad_request!(reason = nil)
- message = ['400 Bad request']
- message << "- #{reason}" if reason
- render_api_error!(message.join(' '), 400)
+ render_api_error_with_reason!(400, '400 Bad request', reason)
end
def bad_request_missing_attribute!(attribute)
@@ -437,8 +432,8 @@ module API
end
end
- def unauthorized!
- render_api_error!('401 Unauthorized', 401)
+ def unauthorized!(reason = nil)
+ render_api_error_with_reason!(401, '401 Unauthorized', reason)
end
def not_allowed!(message = nil)
@@ -491,6 +486,12 @@ module API
model.errors.messages
end
+ def render_api_error_with_reason!(status, message, reason)
+ message = [message]
+ message << "- #{reason}" if reason
+ render_api_error!(message.join(' '), status)
+ end
+
def render_api_error!(message, status)
render_structured_api_error!({ 'message' => message }, status)
end
@@ -569,11 +570,19 @@ module API
end
end
+ def log_artifact_size(file)
+ Gitlab::ApplicationContext.push(artifact: file.model)
+ end
+
+ def present_artifacts_file!(file, **args)
+ log_artifact_size(file) if file
+
+ present_carrierwave_file!(file, **args)
+ end
+
def present_carrierwave_file!(file, supports_direct_download: true)
return not_found! unless file&.exists?
- log_artifact_size(file) if file.is_a?(JobArtifactUploader)
-
if file.file_storage?
present_disk_file!(file.path, file.filename)
elsif supports_direct_download && file.class.direct_download_enabled?
@@ -724,7 +733,6 @@ module API
# Deprecated. Use `send_artifacts_entry` instead.
def legacy_send_artifacts_entry(file, entry)
header(*Gitlab::Workhorse.send_artifacts_entry(file, entry))
- log_artifact_size(file)
body ''
end
@@ -732,15 +740,10 @@ module API
def send_artifacts_entry(file, entry)
header(*Gitlab::Workhorse.send_artifacts_entry(file, entry))
header(*Gitlab::Workhorse.detect_content_type)
- log_artifact_size(file)
body ''
end
- def log_artifact_size(file)
- Gitlab::ApplicationContext.push(artifact: file.model)
- end
-
# The Grape Error Middleware only has access to `env` but not `params` nor
# `request`. We workaround this by defining methods that returns the right
# values.
diff --git a/lib/api/helpers/project_stats_refresh_conflicts_helpers.rb b/lib/api/helpers/project_stats_refresh_conflicts_helpers.rb
new file mode 100644
index 00000000000..db464521033
--- /dev/null
+++ b/lib/api/helpers/project_stats_refresh_conflicts_helpers.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module ProjectStatsRefreshConflictsHelpers
+ def reject_if_build_artifacts_size_refreshing!(project)
+ return unless project.refreshing_build_artifacts_size?
+
+ Gitlab::ProjectStatsRefreshConflictsLogger.warn_request_rejected_during_stats_refresh(project.id)
+
+ conflict!('Action temporarily disabled. The project this pipeline belongs to is undergoing stats refresh.')
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 7a9dd78e4ed..52cb398d6bf 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -169,7 +169,6 @@ module API
:merge_commit_template,
:squash_commit_template,
:repository_storage,
- :compliance_framework_setting,
:packages_enabled,
:service_desk_enabled,
:keep_latest_artifact,
diff --git a/lib/api/helpers/sse_helpers.rb b/lib/api/helpers/sse_helpers.rb
deleted file mode 100644
index c354694f508..00000000000
--- a/lib/api/helpers/sse_helpers.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Helpers
- module SSEHelpers
- def request_from_sse?(project)
- return false if request.referer.blank?
-
- uri = URI.parse(request.referer)
- uri.path.starts_with?(::Gitlab::Routing.url_helpers.project_root_sse_path(project))
- rescue URI::InvalidURIError
- false
- end
- end
- end
-end
diff --git a/lib/api/integrations/jira_connect/subscriptions.rb b/lib/api/integrations/jira_connect/subscriptions.rb
index fa19dc2be3f..a6e931ba7bb 100644
--- a/lib/api/integrations/jira_connect/subscriptions.rb
+++ b/lib/api/integrations/jira_connect/subscriptions.rb
@@ -23,7 +23,7 @@ module API
installation = JiraConnectInstallation.find_by_client_key(jwt.iss_claim)
if !installation || !jwt.valid?(installation.shared_secret) || !jwt.verify_context_qsh_claim
- unauthorized!
+ unauthorized!('JWT authentication failed')
end
jira_user = installation.client.user_info(jwt.sub_claim)
diff --git a/lib/api/integrations/slack/events.rb b/lib/api/integrations/slack/events.rb
new file mode 100644
index 00000000000..6227b75a9d7
--- /dev/null
+++ b/lib/api/integrations/slack/events.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# This API endpoint handles all events sent from Slack once a Slack
+# workspace has installed the GitLab Slack app.
+#
+# See https://api.slack.com/apis/connections/events-api.
+module API
+ class Integrations
+ module Slack
+ class Events < ::API::Base
+ feature_category :integrations
+
+ before { verify_slack_request! }
+
+ helpers do
+ def verify_slack_request!
+ unauthorized! unless Request.verify!(request)
+ end
+ end
+
+ namespace 'integrations/slack' do
+ post :events do
+ type = params['type']
+ raise ArgumentError, "Unable to handle event type: '#{type}'" unless type == 'url_verification'
+
+ status :ok
+ UrlVerification.call(params)
+ rescue ArgumentError => e
+ # Track the error, but respond with a `2xx` because we don't want to risk
+ # Slack rate-limiting, or disabling our app, due to error responses.
+ # See https://api.slack.com/apis/connections/events-api.
+ Gitlab::ErrorTracking.track_exception(e)
+
+ no_content!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/integrations/slack/events/url_verification.rb b/lib/api/integrations/slack/events/url_verification.rb
new file mode 100644
index 00000000000..4628b93665d
--- /dev/null
+++ b/lib/api/integrations/slack/events/url_verification.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module API
+ class Integrations
+ module Slack
+ class Events
+ class UrlVerification
+ # When the GitLab Slack app is first configured to receive Slack events,
+ # Slack will issue a special request to the endpoint and expect it to respond
+ # with the `challenge` param.
+ #
+ # This must be done in-request, rather than on a queue.
+ #
+ # See https://api.slack.com/apis/connections/events-api.
+ def self.call(params)
+ { challenge: params[:challenge] }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/integrations/slack/request.rb b/lib/api/integrations/slack/request.rb
new file mode 100644
index 00000000000..df0109b07aa
--- /dev/null
+++ b/lib/api/integrations/slack/request.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module API
+ class Integrations
+ module Slack
+ module Request
+ VERIFICATION_VERSION = 'v0'
+ VERIFICATION_TIMESTAMP_HEADER = 'X-Slack-Request-Timestamp'
+ VERIFICATION_SIGNATURE_HEADER = 'X-Slack-Signature'
+ VERIFICATION_DELIMITER = ':'
+ VERIFICATION_HMAC_ALGORITHM = 'sha256'
+ VERIFICATION_TIMESTAMP_EXPIRY = 1.minute.to_i
+
+ # Verify the request by comparing the given request signature in the header
+ # with a signature value that we compute according to the steps in:
+ # https://api.slack.com/authentication/verifying-requests-from-slack.
+ def self.verify!(request)
+ return false unless Gitlab::CurrentSettings.slack_app_signing_secret
+
+ timestamp, signature = request.headers.values_at(
+ VERIFICATION_TIMESTAMP_HEADER,
+ VERIFICATION_SIGNATURE_HEADER
+ )
+
+ return false if timestamp.nil? || signature.nil?
+ return false if Time.current.to_i - timestamp.to_i >= VERIFICATION_TIMESTAMP_EXPIRY
+
+ request.body.rewind
+
+ basestring = [
+ VERIFICATION_VERSION,
+ timestamp,
+ request.body.read
+ ].join(VERIFICATION_DELIMITER)
+
+ hmac_digest = OpenSSL::HMAC.hexdigest(
+ VERIFICATION_HMAC_ALGORITHM,
+ Gitlab::CurrentSettings.slack_app_signing_secret,
+ basestring
+ )
+
+ # Signature will look like: 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503'
+ ActiveSupport::SecurityUtils.secure_compare(
+ signature,
+ "#{VERIFICATION_VERSION}=#{hmac_digest}"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index b53f855c3a2..3edd38a0108 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -164,6 +164,18 @@ module API
check_allowed(params)
end
+ post '/error_tracking_allowed', feature_category: :error_tracking do
+ public_key = params[:public_key]
+ project_id = params[:project_id]
+
+ unprocessable_entity! if public_key.blank? || project_id.blank?
+
+ enabled = ::ErrorTracking::ClientKey.enabled_key_for(project_id, public_key).exists?
+
+ status 200
+ { enabled: enabled }
+ end
+
post "/lfs_authenticate", feature_category: :source_code_management, urgency: :high do
not_found! unless container&.lfs_enabled?
diff --git a/lib/api/internal/mail_room.rb b/lib/api/internal/mail_room.rb
index 6e24cf6e7c5..1e5e8c4c4e2 100644
--- a/lib/api/internal/mail_room.rb
+++ b/lib/api/internal/mail_room.rb
@@ -12,6 +12,10 @@ module API
class MailRoom < ::API::Base
feature_category :service_desk
+ format :json
+ content_type :txt, 'text/plain'
+ default_format :txt
+
before do
authenticate_gitlab_mailroom_request!
end
@@ -30,7 +34,7 @@ module API
end
post "/*mailbox_type" do
worker = Gitlab::MailRoom.worker_for(params[:mailbox_type])
- raw = request.body.read
+ raw = Gitlab::EncodingHelper.encode_utf8(request.body.read)
begin
worker.perform_async(raw)
rescue Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
diff --git a/lib/api/internal/workhorse.rb b/lib/api/internal/workhorse.rb
new file mode 100644
index 00000000000..910cf52bc3b
--- /dev/null
+++ b/lib/api/internal/workhorse.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module API
+ module Internal
+ class Workhorse < ::API::Base
+ feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
+
+ before do
+ verify_workhorse_api!
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ end
+
+ helpers do
+ def request_authenticated?
+ authenticator = Gitlab::Auth::RequestAuthenticator.new(request)
+ return true if authenticator.find_authenticated_requester([:api])
+
+ # Look up user from warden, ignoring the absence of a CSRF token. For
+ # web users the CSRF token can be in the POST form data but Workhorse
+ # does not propagate the form data to us.
+ !!request.env['warden']&.authenticate
+ end
+ end
+
+ namespace 'internal' do
+ namespace 'workhorse' do
+ post 'authorize_upload' do
+ unauthorized! unless request_authenticated?
+
+ status 200
+ { TempPath: File.join(::Gitlab.config.uploads.storage_path, 'uploads/tmp') }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb
index cf075af8373..c07c2c1994e 100644
--- a/lib/api/issue_links.rb
+++ b/lib/api/issue_links.rb
@@ -61,6 +61,22 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'Get issues relation' do
+ detail 'This feature was introduced in GitLab 15.1.'
+ success Entities::IssueLink
+ end
+ params do
+ requires :issue_link_id, type: Integer, desc: 'The ID of an issue link'
+ end
+ get ':id/issues/:issue_iid/links/:issue_link_id' do
+ issue = find_project_issue(params[:issue_iid])
+ issue_link = IssueLink.for_source_or_target(issue).find(declared_params[:issue_link_id])
+
+ find_project_issue(issue_link.target.iid.to_s, issue_link.target.project_id.to_s)
+
+ present issue_link, with: Entities::IssueLink
+ end
+
desc 'Remove issues relation' do
success Entities::IssueLink
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 730baae63a2..156a92802b0 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -9,7 +9,6 @@ module API
before { authenticate_non_get! }
helpers Helpers::MergeRequestsHelpers
- helpers Helpers::SSEHelpers
# These endpoints are defined in `TimeTrackingEndpoints` and is shared by
# API::Issues. In order to be able to define the feature category of these
@@ -234,8 +233,6 @@ module API
handle_merge_request_errors!(merge_request)
- Gitlab::UsageDataCounters::EditorUniqueCounter.track_sse_edit_action(author: current_user) if request_from_sse?(user_project)
-
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
@@ -458,7 +455,11 @@ module API
not_allowed! if !immediately_mergeable && !automatically_mergeable
- render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?(skip_ci_check: automatically_mergeable)
+ if Feature.enabled?(:change_response_code_merge_status, user_project)
+ render_api_error!('Branch cannot be merged', 422) unless merge_request.mergeable?(skip_ci_check: automatically_mergeable)
+ else
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?(skip_ci_check: automatically_mergeable)
+ end
check_sha_param!(params, merge_request)
diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb
index c6406bf61df..6fc90da87d4 100644
--- a/lib/api/metrics/dashboard/annotations.rb
+++ b/lib/api/metrics/dashboard/annotations.rb
@@ -5,6 +5,7 @@ module API
module Dashboard
class Annotations < ::API::Base
feature_category :metrics
+ urgency :low
desc 'Create a new monitoring dashboard annotation' do
success Entities::Metrics::Dashboard::Annotation
diff --git a/lib/api/metrics/user_starred_dashboards.rb b/lib/api/metrics/user_starred_dashboards.rb
index 909f7f0405d..83d95f8b062 100644
--- a/lib/api/metrics/user_starred_dashboards.rb
+++ b/lib/api/metrics/user_starred_dashboards.rb
@@ -4,6 +4,7 @@ module API
module Metrics
class UserStarredDashboards < ::API::Base
feature_category :metrics
+ urgency :low
resource :projects do
desc 'Marks selected metrics dashboard as starred' do
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 4ff7096b5d9..a12fbbb9bb6 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -67,9 +67,10 @@ module API
end
get ':namespace/exists', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, feature_category: :subgroups, urgency: :low do
namespace_path = params[:namespace]
+ existing_namespaces_within_the_parent = Namespace.without_project_namespaces.by_parent(params[:parent_id])
- exists = Namespace.without_project_namespaces.by_parent(params[:parent_id]).filter_by_path(namespace_path).exists?
- suggestions = exists ? [Namespace.clean_path(namespace_path)] : []
+ exists = existing_namespaces_within_the_parent.filter_by_path(namespace_path).exists?
+ suggestions = exists ? [Namespace.clean_path(namespace_path, limited_to: existing_namespaces_within_the_parent)] : []
present :exists, exists
present :suggests, suggestions
diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb
index 40e6486dae9..f8b744bb14b 100644
--- a/lib/api/personal_access_tokens.rb
+++ b/lib/api/personal_access_tokens.rb
@@ -54,6 +54,14 @@ module API
present paginate(tokens), with: Entities::PersonalAccessToken
end
+ get ':id' do
+ token = PersonalAccessToken.find_by_id(params[:id])
+
+ unauthorized! unless token && Ability.allowed?(current_user, :read_user_personal_access_tokens, token.user)
+
+ present token, with: Entities::PersonalAccessToken
+ end
+
delete 'self' do
revoke_token(access_token)
end
diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb
index 35f555e16b5..fb782b49f02 100644
--- a/lib/api/projects_relation_builder.rb
+++ b/lib/api/projects_relation_builder.rb
@@ -45,8 +45,6 @@ module API
# For all projects except those in a user namespace, the `namespace`
# and `group` are identical. Preload the group when it's not a user namespace.
def preload_groups(projects_relation)
- return unless Feature.enabled?(:group_projects_api_preload_groups)
-
group_projects = projects_for_group_preload(projects_relation)
groups = group_projects.map(&:namespace)
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index f11270457c9..5bf3c3b8aac 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -39,6 +39,51 @@ module API
params :package_name do
requires :package_name, type: String, file_path: true, desc: 'The PyPi package name'
end
+
+ def present_simple_index(group_or_project)
+ authorize_read_package!(group_or_project)
+
+ packages = Packages::Pypi::PackagesFinder.new(current_user, group_or_project).execute
+ presenter = ::Packages::Pypi::SimpleIndexPresenter.new(packages, group_or_project)
+
+ present_html(presenter.body)
+ end
+
+ def present_simple_package(group_or_project)
+ authorize_read_package!(group_or_project)
+ track_simple_event(group_or_project, 'list_package')
+
+ packages = Packages::Pypi::PackagesFinder.new(current_user, group_or_project, { package_name: params[:package_name] }).execute
+ empty_packages = packages.empty?
+
+ redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do
+ not_found!('Package') if empty_packages
+ presenter = ::Packages::Pypi::SimplePackageVersionsPresenter.new(packages, group_or_project)
+
+ present_html(presenter.body)
+ end
+ end
+
+ def track_simple_event(group_or_project, event_name)
+ if group_or_project.is_a?(Project)
+ project = group_or_project
+ namespace = group_or_project.namespace
+ else
+ project = nil
+ namespace = group_or_project
+ end
+
+ track_package_event(event_name, :pypi, project: project, namespace: namespace)
+ end
+
+ def present_html(content)
+ # Adjusts grape output format
+ # to be HTML
+ content_type "text/html; charset=utf-8"
+ env['api.format'] = :binary
+
+ body content
+ end
end
params do
@@ -67,7 +112,18 @@ module API
present_carrierwave_file!(package_file.file, supports_direct_download: true)
end
- desc 'The PyPi Simple Endpoint' do
+ desc 'The PyPi Simple Group Index Endpoint' do
+ detail 'This feature was introduced in GitLab 15.1'
+ end
+
+ # An API entry point but returns an HTML file instead of JSON.
+ # PyPi simple API returns a list of packages as a simple HTML file.
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ get 'simple', format: :txt do
+ present_simple_index(find_authorized_group!)
+ end
+
+ desc 'The PyPi Simple Group Package Endpoint' do
detail 'This feature was introduced in GitLab 12.10'
end
@@ -75,29 +131,11 @@ module API
use :package_name
end
- # An Api entry point but returns an HTML file instead of JSON.
+ # An API entry point but returns an HTML file instead of JSON.
# PyPi simple API returns the package descriptor as a simple HTML file.
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get 'simple/*package_name', format: :txt do
- group = find_authorized_group!
- authorize_read_package!(group)
-
- track_package_event('list_package', :pypi)
-
- packages = Packages::Pypi::PackagesFinder.new(current_user, group, { package_name: params[:package_name] }).execute
- empty_packages = packages.empty?
-
- redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do
- not_found!('Package') if empty_packages
- presenter = ::Packages::Pypi::PackagePresenter.new(packages, group)
-
- # Adjusts grape output format
- # to be HTML
- content_type "text/html; charset=utf-8"
- env['api.format'] = :binary
-
- body presenter.body
- end
+ present_simple_package(find_authorized_group!)
end
end
end
@@ -133,7 +171,18 @@ module API
present_carrierwave_file!(package_file.file, supports_direct_download: true)
end
- desc 'The PyPi Simple Endpoint' do
+ desc 'The PyPi Simple Project Index Endpoint' do
+ detail 'This feature was introduced in GitLab 15.1'
+ end
+
+ # An API entry point but returns an HTML file instead of JSON.
+ # PyPi simple API returns a list of packages as a simple HTML file.
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ get 'simple', format: :txt do
+ present_simple_index(authorized_user_project)
+ end
+
+ desc 'The PyPi Simple Project Package Endpoint' do
detail 'This feature was introduced in GitLab 12.10'
end
@@ -141,28 +190,11 @@ module API
use :package_name
end
- # An Api entry point but returns an HTML file instead of JSON.
+ # An API entry point but returns an HTML file instead of JSON.
# PyPi simple API returns the package descriptor as a simple HTML file.
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get 'simple/*package_name', format: :txt do
- authorize_read_package!(authorized_user_project)
-
- track_package_event('list_package', :pypi, project: authorized_user_project, namespace: authorized_user_project.namespace)
-
- packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute
- empty_packages = packages.empty?
-
- redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do
- not_found!('Package') if empty_packages
- presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project)
-
- # Adjusts grape output format
- # to be HTML
- content_type "text/html; charset=utf-8"
- env['api.format'] = :binary
-
- body presenter.body
- end
+ present_simple_package(authorized_user_project)
end
desc 'The PyPi Package upload endpoint' do
diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb
index bc5ffe5b21f..8b9380b332e 100644
--- a/lib/api/release/links.rb
+++ b/lib/api/release/links.rb
@@ -29,6 +29,7 @@ module API
params do
use :pagination
end
+ route_setting :authentication, job_token_allowed: true
get 'links' do
authorize! :read_release, release
@@ -45,6 +46,7 @@ module API
optional :filepath, type: String, desc: 'The filepath of the link'
optional :link_type, type: String, desc: 'The link type, one of: "runbook", "image", "package" or "other"'
end
+ route_setting :authentication, job_token_allowed: true
post 'links' do
authorize! :create_release, release
@@ -65,6 +67,7 @@ module API
detail 'This feature was introduced in GitLab 11.7.'
success Entities::Releases::Link
end
+ route_setting :authentication, job_token_allowed: true
get do
authorize! :read_release, release
@@ -82,6 +85,7 @@ module API
optional :link_type, type: String, desc: 'The link type'
at_least_one_of :name, :url
end
+ route_setting :authentication, job_token_allowed: true
put do
authorize! :update_release, release
@@ -96,6 +100,7 @@ module API
detail 'This feature was introduced in GitLab 11.7.'
success Entities::Releases::Link
end
+ route_setting :authentication, job_token_allowed: true
delete do
authorize! :destroy_release, release
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index c69f45f1f38..aecd6f9eef8 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -107,9 +107,10 @@ module API
end
params do
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
+ optional :tag_message, type: String, desc: 'Message to use if creating a new annotated tag'
optional :name, type: String, desc: 'The name of the release'
optional :description, type: String, desc: 'The release notes'
- optional :ref, type: String, desc: 'The commit sha or branch name'
+ optional :ref, type: String, desc: 'Commit SHA or branch name to use if creating a new tag'
optional :assets, type: Hash do
optional :links, type: Array do
requires :name, type: String, desc: 'The name of the link'
diff --git a/lib/api/terraform/modules/v1/packages.rb b/lib/api/terraform/modules/v1/packages.rb
index 8da77ba18ae..f96cffb008c 100644
--- a/lib/api/terraform/modules/v1/packages.rb
+++ b/lib/api/terraform/modules/v1/packages.rb
@@ -114,7 +114,9 @@ module API
module_version: params[:module_version]
)
- jwt_token = Gitlab::TerraformRegistryToken.from_token(token_from_namespace_inheritable).encoded
+ if token_from_namespace_inheritable
+ jwt_token = Gitlab::TerraformRegistryToken.from_token(token_from_namespace_inheritable).encoded
+ end
header 'X-Terraform-Get', module_file_path.sub(%r{module_version/file$}, "#{params[:module_version]}/file?token=#{jwt_token}&archive=tgz")
status :no_content
diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb
index 7b111451b9f..b727fbd9f65 100644
--- a/lib/api/terraform/state.rb
+++ b/lib/api/terraform/state.rb
@@ -13,6 +13,7 @@ module API
default_format :json
rescue_from(
+ ::Terraform::RemoteStateHandler::StateDeletedError,
::ActiveRecord::RecordNotUnique,
::PG::UniqueViolation
) do |e|
@@ -24,6 +25,11 @@ module API
authorize! :read_terraform_state, user_project
increment_unique_values('p_terraform_state_api_unique_users', current_user.id)
+
+ if Feature.enabled?(:route_hll_to_snowplow_phase2, user_project&.namespace)
+ Gitlab::Tracking.event('API::Terraform::State', 'p_terraform_state_api_unique_users',
+ namespace: user_project&.namespace, user: current_user)
+ end
end
params do
@@ -76,7 +82,7 @@ module API
authorize! :admin_terraform_state, user_project
remote_state_handler.handle_with_lock do |state|
- state.destroy!
+ ::Terraform::States::TriggerDestroyService.new(state, current_user: current_user).execute
end
body false
diff --git a/lib/api/user_counts.rb b/lib/api/user_counts.rb
index 756901c5717..d0b1e458a27 100644
--- a/lib/api/user_counts.rb
+++ b/lib/api/user_counts.rb
@@ -3,6 +3,7 @@
module API
class UserCounts < ::API::Base
feature_category :navigation
+ urgency :low
resource :user_counts do
desc 'Return the user specific counts' do
diff --git a/lib/api/users.rb b/lib/api/users.rb
index b10458c4358..93df9413119 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -10,7 +10,7 @@ module API
feature_category :users, ['/users/:id/custom_attributes', '/users/:id/custom_attributes/:key']
- urgency :high, ['/users/:id/custom_attributes', '/users/:id/custom_attributes/:key']
+ urgency :medium, ['/users/:id/custom_attributes', '/users/:id/custom_attributes/:key']
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
include CustomAttributesEndpoints
@@ -145,7 +145,7 @@ module API
use :with_custom_attributes
end
# rubocop: disable CodeReuse/ActiveRecord
- get ":id", feature_category: :users, urgency: :medium do
+ get ":id", feature_category: :users, urgency: :low do
forbidden!('Not authorized!') unless current_user
unless current_user.admin?
@@ -170,7 +170,7 @@ module API
params do
requires :user_id, type: String, desc: 'The ID or username of the user'
end
- get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users, urgency: :high do
+ get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users, urgency: :default do
user = find_user(params[:user_id])
not_found!('User') unless user && can?(current_user, :read_user, user)
@@ -346,6 +346,30 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'Get the project-level Deploy keys that a specified user can access to.' do
+ success Entities::DeployKey
+ end
+ params do
+ requires :user_id, type: String, desc: 'The ID or username of the user'
+ use :pagination
+ end
+ get ':user_id/project_deploy_keys', requirements: API::USER_REQUIREMENTS, feature_category: :continuous_delivery do
+ user = find_user(params[:user_id])
+ not_found!('User') unless user && can?(current_user, :read_user, user)
+
+ project_ids = Project.visible_to_user_and_access_level(current_user, Gitlab::Access::MAINTAINER)
+
+ unless current_user == user
+ # Restrict to only common projects of both current_user and user.
+ project_ids = project_ids.visible_to_user_and_access_level(user, Gitlab::Access::MAINTAINER)
+ end
+
+ forbidden!('No common authorized project found') unless project_ids.present?
+
+ keys = DeployKey.in_projects(project_ids)
+ present paginate(keys), with: Entities::DeployKey
+ end
+
desc 'Add an SSH key to a specified user. Available only for admins.' do
success Entities::SSHKey
end
@@ -921,7 +945,7 @@ module API
desc 'Get the currently authenticated user' do
success Entities::UserPublic
end
- get feature_category: :users, urgency: :medium do
+ get feature_category: :users, urgency: :low do
entity =
if current_user.admin?
Entities::UserWithAdmin
@@ -1096,7 +1120,7 @@ module API
requires :credit_card_mask_number, type: String, desc: 'The last 4 digits of credit card number'
requires :credit_card_type, type: String, desc: 'The credit card network name'
end
- put ":user_id/credit_card_validation", feature_category: :purchase do
+ put ":user_id/credit_card_validation", urgency: :low, feature_category: :purchase do
authenticated_as_admin!
user = find_user(params[:user_id])
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 12dbf4792d6..082be1f7e11 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -37,7 +37,12 @@ module API
entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic
- present container.wiki.list_pages(load_content: params[:with_content]), with: entity
+ options = {
+ with: entity,
+ current_user: current_user
+ }
+
+ present container.wiki.list_pages(load_content: params[:with_content]), options
end
desc 'Get a wiki page' do
@@ -51,7 +56,13 @@ module API
get ':id/wikis/:slug', urgency: :low do
authorize! :read_wiki, container
- present wiki_page(params[:version]), with: Entities::WikiPage, render_html: params[:render_html]
+ options = {
+ with: Entities::WikiPage,
+ render_html: params[:render_html],
+ current_user: current_user
+ }
+
+ present wiki_page(params[:version]), options
end
desc 'Create a wiki page' do
diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb
index b8aa2cc8ea0..3c999920d39 100644
--- a/lib/atlassian/jira_connect/client.rb
+++ b/lib/atlassian/jira_connect/client.rb
@@ -30,6 +30,8 @@ module Atlassian
responses.compact
end
+ # Fetch user information for the given account.
+ # https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-get
def user_info(account_id)
r = get('/rest/api/3/user', { accountId: account_id, expand: 'groups' })
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 0991177d044..16b8f21c9e9 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -11,7 +11,8 @@ module Backup
LIST_ENVS = {
skipped: 'SKIP',
- repositories_storages: 'REPOSITORIES_STORAGES'
+ repositories_storages: 'REPOSITORIES_STORAGES',
+ repositories_paths: 'REPOSITORIES_PATHS'
}.freeze
TaskDefinition = Struct.new(
@@ -41,29 +42,8 @@ module Backup
end
def create
- if incremental?
- unpack(ENV.fetch('PREVIOUS_BACKUP', ENV['BACKUP']))
- read_backup_information
- verify_backup_version
- update_backup_information
- end
-
- build_backup_information
-
- definitions.keys.each do |task_name|
- run_create_task(task_name)
- end
-
- write_backup_information
-
- if skipped?('tar')
- upload
- else
- pack
- upload
- cleanup
- remove_old
- end
+ unpack(ENV.fetch('PREVIOUS_BACKUP', ENV['BACKUP'])) if incremental?
+ run_all_create_tasks
puts_time "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \
"and are not included in this backup. You will need these files to restore a backup.\n" \
@@ -95,22 +75,8 @@ module Backup
end
def restore
- cleanup_required = unpack(ENV['BACKUP'])
- read_backup_information
- verify_backup_version
-
- definitions.keys.each do |task_name|
- run_restore_task(task_name) if !skipped?(task_name) && enabled_task?(task_name)
- end
-
- Rake::Task['gitlab:shell:setup'].invoke
- Rake::Task['cache:clear'].invoke
-
- if cleanup_required
- cleanup
- end
-
- remove_tmp
+ unpack(ENV['BACKUP'])
+ run_all_restore_tasks
puts_time "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \
"and are not included in this backup. You will need to restore these files manually.".color(:red)
@@ -225,13 +191,59 @@ module Backup
max_storage_concurrency = ENV['GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY'].presence
strategy = Backup::GitalyBackup.new(progress, incremental: incremental?, max_parallelism: max_concurrency, storage_parallelism: max_storage_concurrency)
- Repositories.new(progress, strategy: strategy, storages: repositories_storages)
+ Repositories.new(progress,
+ strategy: strategy,
+ storages: list_env(:repositories_storages),
+ paths: list_env(:repositories_paths)
+ )
end
def build_files_task(app_files_dir, excludes: [])
Files.new(progress, app_files_dir, excludes: excludes)
end
+ def run_all_create_tasks
+ if incremental?
+ read_backup_information
+ verify_backup_version
+ update_backup_information
+ end
+
+ build_backup_information
+
+ definitions.keys.each do |task_name|
+ run_create_task(task_name)
+ end
+
+ write_backup_information
+
+ unless skipped?('tar')
+ pack
+ upload
+ remove_old
+ end
+
+ ensure
+ cleanup unless skipped?('tar')
+ remove_tmp
+ end
+
+ def run_all_restore_tasks
+ read_backup_information
+ verify_backup_version
+
+ definitions.keys.each do |task_name|
+ run_restore_task(task_name) if !skipped?(task_name) && enabled_task?(task_name)
+ end
+
+ Rake::Task['gitlab:shell:setup'].invoke
+ Rake::Task['cache:clear'].invoke
+
+ ensure
+ cleanup unless skipped?('tar')
+ remove_tmp
+ end
+
def incremental?
@incremental
end
@@ -259,7 +271,8 @@ module Backup
tar_version: tar_version,
installation_type: Gitlab::INSTALLATION_TYPE,
skipped: ENV['SKIP'],
- repositories_storages: ENV['REPOSITORIES_STORAGES']
+ repositories_storages: ENV['REPOSITORIES_STORAGES'],
+ repositories_paths: ENV['REPOSITORIES_PATHS']
}
end
@@ -272,7 +285,8 @@ module Backup
tar_version: tar_version,
installation_type: Gitlab::INSTALLATION_TYPE,
skipped: list_env(:skipped).join(','),
- repositories_storages: list_env(:repositories_storages).join(',')
+ repositories_storages: list_env(:repositories_storages).join(','),
+ repositories_paths: list_env(:repositories_paths).join(',')
)
end
@@ -299,7 +313,7 @@ module Backup
def upload
connection_settings = Gitlab.config.backup.upload.connection
- if connection_settings.blank? || skipped?('remote')
+ if connection_settings.blank? || skipped?('remote') || skipped?('tar')
puts_time "Uploading backup archive to remote storage #{remote_directory} ... ".color(:blue) + "[SKIPPED]".color(:cyan)
return
end
@@ -405,8 +419,7 @@ module Backup
def unpack(source_backup_id)
if source_backup_id.blank? && non_tarred_backup?
puts_time "Non tarred backup found in #{backup_path}, using that"
-
- return false
+ return
end
Dir.chdir(backup_path) do
@@ -466,10 +479,6 @@ module Backup
@skipped ||= list_env(:skipped)
end
- def repositories_storages
- @repositories_storages ||= list_env(:repositories_storages)
- end
-
def list_env(name)
list = ENV.fetch(LIST_ENVS[name], '').split(',')
list += backup_information[name].split(',') if backup_information[name]
diff --git a/lib/backup/repositories.rb b/lib/backup/repositories.rb
index 4a31e87b969..4f4a098f374 100644
--- a/lib/backup/repositories.rb
+++ b/lib/backup/repositories.rb
@@ -3,19 +3,25 @@
require 'yaml'
module Backup
+ # Backup and restores repositories by querying the database
class Repositories < Task
extend ::Gitlab::Utils::Override
- def initialize(progress, strategy:, storages: [])
+ # @param [IO] progress IO interface to output progress
+ # @param [Object] :strategy Fetches backups from gitaly
+ # @param [Array<String>] :storages Filter by specified storage names. Empty means all storages.
+ # @param [Array<String>] :paths Filter by specified project paths. Empty means all projects, groups and snippets.
+ def initialize(progress, strategy:, storages: [], paths: [])
super(progress)
@strategy = strategy
@storages = storages
+ @paths = paths
end
override :dump
- def dump(path, backup_id)
- strategy.start(:create, path, backup_id: backup_id)
+ def dump(destination_path, backup_id)
+ strategy.start(:create, destination_path, backup_id: backup_id)
enqueue_consecutive
ensure
@@ -23,8 +29,8 @@ module Backup
end
override :restore
- def restore(path)
- strategy.start(:restore, path)
+ def restore(destination_path)
+ strategy.start(:restore, destination_path)
enqueue_consecutive
ensure
@@ -36,7 +42,7 @@ module Backup
private
- attr_reader :strategy, :storages
+ attr_reader :strategy, :storages, :paths
def enqueue_consecutive
enqueue_consecutive_projects
@@ -66,12 +72,26 @@ module Backup
def project_relation
scope = Project.includes(:route, :group, namespace: :owner)
scope = scope.id_in(ProjectRepository.for_repository_storage(storages).select(:project_id)) if storages.any?
+ if paths.any?
+ scope = scope.where_full_path_in(paths).or(
+ Project.where(namespace_id: Namespace.where_full_path_in(paths).self_and_descendants)
+ )
+ end
+
scope
end
def snippet_relation
scope = Snippet.all
scope = scope.id_in(SnippetRepository.for_repository_storage(storages).select(:snippet_id)) if storages.any?
+ if paths.any?
+ scope = scope.joins(:project).merge(
+ Project.where_full_path_in(paths).or(
+ Project.where(namespace_id: Namespace.where_full_path_in(paths).self_and_descendants)
+ )
+ )
+ end
+
scope
end
@@ -79,7 +99,6 @@ module Backup
PoolRepository.includes(:source_project).find_each do |pool|
progress.puts " - Object pool #{pool.disk_path}..."
- pool.source_project ||= pool.member_projects.first&.root_of_fork_network
unless pool.source_project
progress.puts " - Object pool #{pool.disk_path}... " + "[SKIPPED]".color(:cyan)
next
diff --git a/lib/banzai/filter/references/commit_reference_filter.rb b/lib/banzai/filter/references/commit_reference_filter.rb
index 157dc696cc8..86ab8597cf5 100644
--- a/lib/banzai/filter/references/commit_reference_filter.rb
+++ b/lib/banzai/filter/references/commit_reference_filter.rb
@@ -19,7 +19,12 @@ module Banzai
def find_object(project, id)
return unless project.is_a?(Project) && project.valid_repo?
- _, record = reference_cache.records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
+ # Optimization: try exact commit hash match first
+ record = reference_cache.records_per_parent[project].fetch(id, nil)
+
+ unless record
+ _, record = reference_cache.records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
+ end
record
end
diff --git a/lib/banzai/filter/references/issue_reference_filter.rb b/lib/banzai/filter/references/issue_reference_filter.rb
index 337075b7ff8..b536d900a02 100644
--- a/lib/banzai/filter/references/issue_reference_filter.rb
+++ b/lib/banzai/filter/references/issue_reference_filter.rb
@@ -29,6 +29,14 @@ module Banzai
super + design_link_extras(issue, matches.named_captures['path'])
end
+ def reference_class(object_sym, tooltip: false)
+ super
+ end
+
+ def data_attributes_for(text, parent, object, **data)
+ super.merge(project_path: parent.full_path, iid: object.iid)
+ end
+
private
def additional_object_attributes(issue)
diff --git a/lib/banzai/filter/references/merge_request_reference_filter.rb b/lib/banzai/filter/references/merge_request_reference_filter.rb
index 6c5ad83d9ae..5bc18ee6985 100644
--- a/lib/banzai/filter/references/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/references/merge_request_reference_filter.rb
@@ -17,12 +17,6 @@ module Banzai
only_path: context[:only_path])
end
- def object_link_title(object, matches)
- # The method will return `nil` if object is not a commit
- # allowing for properly handling the extended MR Tooltip
- object_link_commit_title(object, matches)
- end
-
def object_link_text_extras(object, matches)
extras = super
@@ -53,20 +47,16 @@ module Banzai
.includes(target_project: :namespace)
end
- def reference_class(object_sym, options = {})
- super(object_sym, tooltip: false)
+ def reference_class(object_sym, tooltip: false)
+ super
end
def data_attributes_for(text, parent, object, **data)
- super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
+ super.merge(project_path: parent.full_path, iid: object.iid)
end
private
- def object_link_commit_title(object, matches)
- object_link_commit(object, matches)&.title
- end
-
def object_link_commit_ref(object, matches)
object_link_commit(object, matches)&.short_id
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index bcd9f39d1dc..7175e99f1c7 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -60,6 +60,7 @@ module Banzai
highlighted = %(<div class="gl-relative markdown-code-block js-markdown-code"><pre #{sourcepos_attr} class="#{css_classes}"
lang="#{language}"
+ #{lang != language ? "data-canonical-lang=\"#{escape_once(lang)}\"" : ""}
#{lang_params}
v-pre="true"><code>#{code}</code></pre><copy-code></copy-code></div>)
diff --git a/lib/bulk_imports/groups/stage.rb b/lib/bulk_imports/groups/stage.rb
index c4db53424fd..0378a9c605d 100644
--- a/lib/bulk_imports/groups/stage.rb
+++ b/lib/bulk_imports/groups/stage.rb
@@ -5,6 +5,21 @@ module BulkImports
class Stage < ::BulkImports::Stage
private
+ # To skip the execution of a pipeline in a specific source instance version, define the attributes
+ # `minimum_source_version` and `maximum_source_version`.
+ #
+ # Use the `minimum_source_version` to inform that the pipeline needs to run when importing from source instances
+ # version greater than or equal to the specified minimum source version. For example, if the
+ # `minimum_source_version` is equal to 15.1.0, the pipeline will be executed when importing from source instances
+ # running versions 15.1.0, 15.1.1, 15.2.0, 16.0.0, etc. And it won't be executed when the source instance version
+ # is 15.0.1, 15.0.0, 14.10.0, etc.
+ #
+ # Use the `maximum_source_version` to inform that the pipeline needs to run when importing from source instance
+ # versions less than or equal to the specified maximum source version. For example, if the
+ # `maximum_source_version` is equal to 15.1.0, the pipeline will be executed when importing from source instances
+ # running versions 15.1.1 (patch), 15.1.0, 15.0.1, 15.0.0, 14.10.0, etc. And it won't be executed when the source
+ # instance version is 15.2.0, 15.2.1, 16.0.0, etc.
+
def config
@config ||= {
group: {
@@ -21,7 +36,8 @@ module BulkImports
},
namespace_settings: {
pipeline: BulkImports::Groups::Pipelines::NamespaceSettingsPipeline,
- stage: 1
+ stage: 1,
+ minimum_source_version: '15.0.0'
},
members: {
pipeline: BulkImports::Common::Pipelines::MembersPipeline,
diff --git a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb
new file mode 100644
index 00000000000..2d5231b0541
--- /dev/null
+++ b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Projects
+ module Pipelines
+ class DesignBundlePipeline
+ include Pipeline
+
+ file_extraction_pipeline!
+ relation_name BulkImports::FileTransfer::ProjectConfig::DESIGN_BUNDLE_RELATION
+
+ def extract(_context)
+ download_service.execute
+ decompression_service.execute
+ extraction_service.execute
+
+ bundle_path = File.join(tmpdir, "#{self.class.relation}.bundle")
+
+ BulkImports::Pipeline::ExtractedData.new(data: bundle_path)
+ end
+
+ def load(_context, bundle_path)
+ Gitlab::Utils.check_path_traversal!(bundle_path)
+ Gitlab::Utils.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir])
+
+ return unless portable.lfs_enabled?
+ return unless File.exist?(bundle_path)
+ return if File.directory?(bundle_path)
+ return if File.lstat(bundle_path).symlink?
+
+ portable.design_repository.create_from_bundle(bundle_path)
+ end
+
+ def after_run(_)
+ FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
+ end
+
+ private
+
+ def download_service
+ BulkImports::FileDownloadService.new(
+ configuration: context.configuration,
+ relative_url: context.entity.relation_download_url_path(self.class.relation),
+ tmpdir: tmpdir,
+ filename: targz_filename
+ )
+ end
+
+ def decompression_service
+ BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename)
+ end
+
+ def extraction_service
+ BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename)
+ end
+
+ def tar_filename
+ "#{self.class.relation}.tar"
+ end
+
+ def targz_filename
+ "#{tar_filename}.gz"
+ end
+
+ def tmpdir
+ @tmpdir ||= Dir.mktmpdir('bulk_imports')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb b/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb
index 1754f27137c..d5886d7bae7 100644
--- a/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/project_attributes_pipeline.rb
@@ -10,16 +10,9 @@ module BulkImports
relation_name BulkImports::FileTransfer::BaseConfig::SELF_RELATION
- transformer ::BulkImports::Common::Transformers::ProhibitedAttributesTransformer
-
- def extract(_context)
- download_service.execute
- decompression_service.execute
-
- project_attributes = json_decode(json_attributes)
+ extractor ::BulkImports::Common::Extractors::JsonExtractor, relation: relation
- BulkImports::Pipeline::ExtractedData.new(data: project_attributes)
- end
+ transformer ::BulkImports::Common::Transformers::ProhibitedAttributesTransformer
def transform(_context, data)
subrelations = config.portable_relations_tree.keys.map(&:to_s)
@@ -39,51 +32,14 @@ module BulkImports
end
def after_run(_context)
- FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
- end
-
- def json_attributes
- @json_attributes ||= File.read(File.join(tmpdir, filename))
+ extractor.remove_tmpdir
end
private
- def tmpdir
- @tmpdir ||= Dir.mktmpdir('bulk_imports')
- end
-
def config
@config ||= BulkImports::FileTransfer.config_for(portable)
end
-
- def download_service
- @download_service ||= BulkImports::FileDownloadService.new(
- configuration: context.configuration,
- relative_url: context.entity.relation_download_url_path(self.class.relation),
- tmpdir: tmpdir,
- filename: compressed_filename
- )
- end
-
- def decompression_service
- @decompression_service ||= BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: compressed_filename)
- end
-
- def compressed_filename
- "#{filename}.gz"
- end
-
- def filename
- "#{self.class.relation}.json"
- end
-
- def json_decode(string)
- Gitlab::Json.parse(string)
- rescue JSON::ParserError => e
- Gitlab::ErrorTracking.log_exception(e)
-
- raise BulkImports::Error, 'Incorrect JSON format'
- end
end
end
end
diff --git a/lib/bulk_imports/projects/pipelines/releases_pipeline.rb b/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
index 8f9c6a5749f..c77e53b9aec 100644
--- a/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
@@ -9,6 +9,22 @@ module BulkImports
relation_name 'releases'
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
+
+ def after_run(_context)
+ super
+
+ portable.releases.find_each do |release|
+ create_release_evidence(release)
+ end
+ end
+
+ private
+
+ def create_release_evidence(release)
+ return if release.historical_release? || release.upcoming_release?
+
+ ::Releases::CreateEvidenceWorker.perform_async(release.id)
+ end
end
end
end
diff --git a/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb
new file mode 100644
index 00000000000..9a3c582642f
--- /dev/null
+++ b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Projects
+ module Pipelines
+ class RepositoryBundlePipeline
+ include Pipeline
+
+ abort_on_failure!
+ file_extraction_pipeline!
+ relation_name BulkImports::FileTransfer::ProjectConfig::REPOSITORY_BUNDLE_RELATION
+
+ def extract(_context)
+ download_service.execute
+ decompression_service.execute
+ extraction_service.execute
+
+ bundle_path = File.join(tmpdir, "#{self.class.relation}.bundle")
+
+ BulkImports::Pipeline::ExtractedData.new(data: bundle_path)
+ end
+
+ def load(_context, bundle_path)
+ Gitlab::Utils.check_path_traversal!(bundle_path)
+ Gitlab::Utils.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir])
+
+ return unless File.exist?(bundle_path)
+ return if File.directory?(bundle_path)
+ return if File.lstat(bundle_path).symlink?
+
+ portable.repository.create_from_bundle(bundle_path)
+ end
+
+ def after_run(_)
+ FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
+ end
+
+ private
+
+ def tar_filename
+ "#{self.class.relation}.tar"
+ end
+
+ def targz_filename
+ "#{tar_filename}.gz"
+ end
+
+ def download_service
+ BulkImports::FileDownloadService.new(
+ configuration: context.configuration,
+ relative_url: context.entity.relation_download_url_path(self.class.relation),
+ tmpdir: tmpdir,
+ filename: targz_filename
+ )
+ end
+
+ def decompression_service
+ BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename)
+ end
+
+ def extraction_service
+ BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename)
+ end
+
+ def tmpdir
+ @tmpdir ||= Dir.mktmpdir('bulk_imports')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/projects/stage.rb b/lib/bulk_imports/projects/stage.rb
index 229df9c410d..acfa9163eae 100644
--- a/lib/bulk_imports/projects/stage.rb
+++ b/lib/bulk_imports/projects/stage.rb
@@ -5,6 +5,21 @@ module BulkImports
class Stage < ::BulkImports::Stage
private
+ # To skip the execution of a pipeline in a specific source instance version, define the attributes
+ # `minimum_source_version` and `maximum_source_version`.
+ #
+ # Use the `minimum_source_version` to inform that the pipeline needs to run when importing from source instances
+ # version greater than or equal to the specified minimum source version. For example, if the
+ # `minimum_source_version` is equal to 15.1.0, the pipeline will be executed when importing from source instances
+ # running versions 15.1.0, 15.1.1, 15.2.0, 16.0.0, etc. And it won't be executed when the source instance version
+ # is 15.0.1, 15.0.0, 14.10.0, etc.
+ #
+ # Use the `maximum_source_version` to inform that the pipeline needs to run when importing from source instance
+ # versions less than or equal to the specified maximum source version. For example, if the
+ # `maximum_source_version` is equal to 15.1.0, the pipeline will be executed when importing from source instances
+ # running versions 15.1.1 (patch), 15.1.0, 15.0.1, 15.0.0, 14.10.0, etc. And it won't be executed when the source
+ # instance version is 15.2.0, 15.2.1, 16.0.0, etc.
+
def config
@config ||= {
project: {
@@ -13,6 +28,12 @@ module BulkImports
},
repository: {
pipeline: BulkImports::Projects::Pipelines::RepositoryPipeline,
+ maximum_source_version: '15.0.0',
+ stage: 1
+ },
+ repository_bundle: {
+ pipeline: BulkImports::Projects::Pipelines::RepositoryBundlePipeline,
+ minimum_source_version: '15.1.0',
stage: 1
},
project_attributes: {
@@ -95,6 +116,11 @@ module BulkImports
pipeline: BulkImports::Common::Pipelines::LfsObjectsPipeline,
stage: 5
},
+ design: {
+ pipeline: BulkImports::Projects::Pipelines::DesignBundlePipeline,
+ minimum_source_version: '15.1.0',
+ stage: 5
+ },
auto_devops: {
pipeline: BulkImports::Projects::Pipelines::AutoDevopsPipeline,
stage: 5
diff --git a/lib/bulk_imports/stage.rb b/lib/bulk_imports/stage.rb
index 6cf394c5df0..b45ac139385 100644
--- a/lib/bulk_imports/stage.rb
+++ b/lib/bulk_imports/stage.rb
@@ -15,9 +15,6 @@ module BulkImports
@pipelines ||= config
.values
.sort_by { |entry| entry[:stage] }
- .map do |entry|
- [entry[:stage], entry[:pipeline]]
- end
end
private
diff --git a/lib/constraints/repository_redirect_url_constrainer.rb b/lib/constraints/repository_redirect_url_constrainer.rb
index 44df670d8d3..046b3397152 100644
--- a/lib/constraints/repository_redirect_url_constrainer.rb
+++ b/lib/constraints/repository_redirect_url_constrainer.rb
@@ -18,11 +18,17 @@ module Constraints
end
# Check if the path matches any known repository containers.
- # These also cover wikis, since a `.wiki` suffix is valid in project/group paths too.
def container_path?(path)
- NamespacePathValidator.valid_path?(path) ||
+ wiki_path?(path) ||
ProjectPathValidator.valid_path?(path) ||
path =~ Gitlab::PathRegex.full_snippets_repository_path_regex
end
+
+ private
+
+ # These also cover wikis, since a `.wiki` suffix is valid in project/group paths too.
+ def wiki_path?(path)
+ NamespacePathValidator.valid_path?(path) && path.end_with?('.wiki')
+ end
end
end
diff --git a/lib/container_registry/base_client.rb b/lib/container_registry/base_client.rb
index 66bc934d1ef..0b24b31c4ae 100644
--- a/lib/container_registry/base_client.rb
+++ b/lib/container_registry/base_client.rb
@@ -8,11 +8,12 @@ module ContainerRegistry
class BaseClient
DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE = 'application/vnd.docker.distribution.manifest.list.v2+json'
+ OCI_DISTRIBUTION_INDEX_TYPE = 'application/vnd.oci.image.index.v1+json'
OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json'
CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json'
ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
- ACCEPTED_TYPES_RAW = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE, DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE].freeze
+ ACCEPTED_TYPES_RAW = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE, DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE, OCI_DISTRIBUTION_INDEX_TYPE].freeze
RETRY_EXCEPTIONS = [Faraday::Request::Retry::DEFAULT_EXCEPTIONS, Faraday::ConnectionFailed].flatten.freeze
RETRY_OPTIONS = {
@@ -107,6 +108,7 @@ module ContainerRegistry
conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+json'
conn.response :json, content_type: DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE
conn.response :json, content_type: OCI_MANIFEST_V1_TYPE
+ conn.response :json, content_type: OCI_DISTRIBUTION_INDEX_TYPE
end
def delete_if_exists(path)
diff --git a/lib/container_registry/migration.rb b/lib/container_registry/migration.rb
index 8377190c83c..92a001f9c24 100644
--- a/lib/container_registry/migration.rb
+++ b/lib/container_registry/migration.rb
@@ -22,6 +22,7 @@ module ContainerRegistry
delegate :container_registry_import_created_before, to: ::Gitlab::CurrentSettings
delegate :container_registry_pre_import_timeout, to: ::Gitlab::CurrentSettings
delegate :container_registry_import_timeout, to: ::Gitlab::CurrentSettings
+ delegate :container_registry_pre_import_tags_rate, to: ::Gitlab::CurrentSettings
alias_method :max_tags_count, :container_registry_import_max_tags_count
alias_method :max_retries, :container_registry_import_max_retries
@@ -31,6 +32,7 @@ module ContainerRegistry
alias_method :created_before, :container_registry_import_created_before
alias_method :pre_import_timeout, :container_registry_pre_import_timeout
alias_method :import_timeout, :container_registry_import_timeout
+ alias_method :pre_import_tags_rate, :container_registry_pre_import_tags_rate
end
def self.enabled?
@@ -41,6 +43,10 @@ module ContainerRegistry
Feature.enabled?(:container_registry_migration_limit_gitlab_org)
end
+ def self.delete_container_repository_worker_support?
+ Feature.enabled?(:container_registry_migration_phase2_delete_container_repository_worker_support)
+ end
+
def self.enqueue_waiting_time
return 0 if Feature.enabled?(:container_registry_migration_phase2_enqueue_speed_fast)
return 165.minutes if Feature.enabled?(:container_registry_migration_phase2_enqueue_speed_slow)
@@ -54,6 +60,7 @@ module ContainerRegistry
#
# TODO: See https://gitlab.com/gitlab-org/container-registry/-/issues/582
#
+ return 40 if Feature.enabled?(:container_registry_migration_phase2_capacity_40)
return 25 if Feature.enabled?(:container_registry_migration_phase2_capacity_25)
return 10 if Feature.enabled?(:container_registry_migration_phase2_capacity_10)
return 5 if Feature.enabled?(:container_registry_migration_phase2_capacity_5)
@@ -71,12 +78,8 @@ module ContainerRegistry
Feature.enabled?(:container_registry_migration_phase2_all_plans)
end
- def self.enqueue_twice?
- Feature.enabled?(:container_registry_migration_phase2_enqueue_twice)
- end
-
- def self.enqueuer_loop?
- Feature.enabled?(:container_registry_migration_phase2_enqueuer_loop)
+ def self.dynamic_pre_import_timeout_for(repository)
+ (repository.tags_count * pre_import_tags_rate).seconds
end
end
end
diff --git a/lib/error_tracking/stacktrace_builder.rb b/lib/error_tracking/stacktrace_builder.rb
new file mode 100644
index 00000000000..4f331bc4e06
--- /dev/null
+++ b/lib/error_tracking/stacktrace_builder.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class StacktraceBuilder
+ attr_reader :stacktrace
+
+ def initialize(payload)
+ @stacktrace = build_stacktrace(payload)
+ end
+
+ private
+
+ def build_stacktrace(payload)
+ raw_stacktrace = raw_stacktrace_from_payload(payload)
+ return [] unless raw_stacktrace
+
+ raw_stacktrace.map do |entry|
+ {
+ 'lineNo' => entry['lineno'],
+ 'context' => build_stacktrace_context(entry),
+ 'filename' => entry['filename'],
+ 'function' => entry['function'],
+ 'colNo' => 0 # we don't support colNo yet.
+ }
+ end
+ end
+
+ def raw_stacktrace_from_payload(payload)
+ exception_entry = payload['exception']
+ return unless exception_entry
+
+ exception_values = exception_entry['values']
+ stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
+ stack_trace_entry&.dig('stacktrace', 'frames')
+ end
+
+ def build_stacktrace_context(entry)
+ error_line = entry['context_line']
+ error_line_no = entry['lineno']
+ pre_context = entry['pre_context']
+ post_context = entry['post_context']
+
+ context = []
+ context.concat lines_with_position(pre_context, error_line_no - pre_context.size) if pre_context
+ context.concat lines_with_position([error_line], error_line_no)
+ context.concat lines_with_position(post_context, error_line_no + 1) if post_context
+
+ context.reject(&:blank?)
+ end
+
+ def lines_with_position(lines, position)
+ return [] if lines.blank?
+
+ lines.map.with_index do |line, index|
+ next unless line
+
+ [position + index, line]
+ end
+ end
+ end
+end
diff --git a/lib/feature.rb b/lib/feature.rb
index b5a97ee8f9b..3bba4be7514 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -281,6 +281,8 @@ class Feature
end
class Target
+ UnknowTargetError = Class.new(StandardError)
+
attr_reader :params
def initialize(params)
@@ -292,7 +294,7 @@ class Feature
end
def targets
- [feature_group, user, project, group, namespace].compact
+ [feature_group, users, projects, groups, namespaces].flatten.compact
end
private
@@ -305,29 +307,37 @@ class Feature
end
# rubocop: enable CodeReuse/ActiveRecord
- def user
+ def users
return unless params.key?(:user)
- UserFinder.new(params[:user]).find_by_username!
+ params[:user].split(',').map do |arg|
+ UserFinder.new(arg).find_by_username || (raise UnknowTargetError, "#{arg} is not found!")
+ end
end
- def project
+ def projects
return unless params.key?(:project)
- Project.find_by_full_path(params[:project])
+ params[:project].split(',').map do |arg|
+ Project.find_by_full_path(arg) || (raise UnknowTargetError, "#{arg} is not found!")
+ end
end
- def group
+ def groups
return unless params.key?(:group)
- Group.find_by_full_path(params[:group])
+ params[:group].split(',').map do |arg|
+ Group.find_by_full_path(arg) || (raise UnknowTargetError, "#{arg} is not found!")
+ end
end
- def namespace
+ def namespaces
return unless params.key?(:namespace)
- # We are interested in Group or UserNamespace
- Namespace.without_project_namespaces.find_by_full_path(params[:namespace])
+ params[:namespace].split(',').map do |arg|
+ # We are interested in Group or UserNamespace
+ Namespace.without_project_namespaces.find_by_full_path(arg) || (raise UnknowTargetError, "#{arg} is not found!")
+ end
end
end
end
diff --git a/lib/generators/gitlab/usage_metric/templates/numbers_instrumentation_class.rb.template b/lib/generators/gitlab/usage_metric/templates/numbers_instrumentation_class.rb.template
new file mode 100644
index 00000000000..ef9537f970e
--- /dev/null
+++ b/lib/generators/gitlab/usage_metric/templates/numbers_instrumentation_class.rb.template
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class <%= class_name %>Metric < NumbersMetric
+ operation :<%= operation%>
+
+ data do |time_frame|
+ [
+ # Insert numbers here
+ ]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/generators/gitlab/usage_metric_generator.rb b/lib/generators/gitlab/usage_metric_generator.rb
index 0656dfbc312..3624a6eb5a7 100644
--- a/lib/generators/gitlab/usage_metric_generator.rb
+++ b/lib/generators/gitlab/usage_metric_generator.rb
@@ -12,10 +12,13 @@ module Gitlab
ALLOWED_SUPERCLASSES = {
generic: 'Generic',
database: 'Database',
- redis: 'Redis'
+ redis: 'Redis',
+ numbers: 'Numbers'
}.freeze
- ALLOWED_OPERATIONS = %w(count distinct_count estimate_batch_distinct_count).freeze
+ ALLOWED_DATABASE_OPERATIONS = %w(count distinct_count estimate_batch_distinct_count sum average).freeze
+ ALLOWED_NUMBERS_OPERATIONS = %w(add).freeze
+ ALLOWED_OPERATIONS = ALLOWED_DATABASE_OPERATIONS | ALLOWED_NUMBERS_OPERATIONS
source_root File.expand_path('usage_metric/templates', __dir__)
@@ -29,6 +32,7 @@ module Gitlab
validate!
template "database_instrumentation_class.rb.template", file_path if type == 'database'
+ template "numbers_instrumentation_class.rb.template", file_path if type == 'numbers'
template "generic_instrumentation_class.rb.template", file_path if type == 'generic'
template "instrumentation_class_spec.rb.template", spec_file_path
@@ -39,7 +43,8 @@ module Gitlab
def validate!
raise ArgumentError, "Type is required, valid options are #{ALLOWED_SUPERCLASSES.keys.join(', ')}" unless type.present?
raise ArgumentError, "Unknown type '#{type}', valid options are #{ALLOWED_SUPERCLASSES.keys.join(', ')}" if metric_superclass.nil?
- raise ArgumentError, "Unknown operation '#{operation}' valid operations are #{ALLOWED_OPERATIONS.join(', ')}" if type == 'database' && !ALLOWED_OPERATIONS.include?(operation)
+ raise ArgumentError, "Unknown operation '#{operation}' valid operations for database are #{ALLOWED_DATABASE_OPERATIONS.join(', ')}" if type == 'database' && ALLOWED_DATABASE_OPERATIONS.exclude?(operation)
+ raise ArgumentError, "Unknown operation '#{operation}' valid operations for numbers are #{ALLOWED_NUMBERS_OPERATIONS.join(', ')}" if type == 'numbers' && ALLOWED_NUMBERS_OPERATIONS.exclude?(operation)
end
def ee?
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
index 55e421173d7..07dc4c02ba8 100644
--- a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
@@ -38,7 +38,7 @@ module Gitlab
strong_memoize(:serialized_records) do
# When RecordsFetcher is used with query sourced from
# InOperatorOptimization::QueryBuilder only columns
- # used in ORDER BY statement would be selected by Arel.start operation
+ # used in ORDER BY statement would be selected by Arel.star operation
selections = [stage_event_model.arel_table[Arel.star]]
selections << duration_in_seconds.as('total_time') if params[:sort] != :duration # duration sorting already exposes this data
@@ -55,7 +55,9 @@ module Gitlab
project_path: project.path,
namespace_path: project.namespace.route.path,
author: issuable.author,
- total_time: record.total_time
+ total_time: record.total_time,
+ start_event_timestamp: record.start_event_timestamp,
+ end_event_timestamp: record.end_event_timestamp
})
serializer.represent(attributes)
end
diff --git a/lib/gitlab/analytics/unique_visits.rb b/lib/gitlab/analytics/unique_visits.rb
deleted file mode 100644
index 3546a7e3ddb..00000000000
--- a/lib/gitlab/analytics/unique_visits.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Analytics
- class UniqueVisits
- # Returns number of unique visitors for given targets in given time frame
- #
- # @param [String, Array[<String>]] targets ids of targets to count visits on. Special case for :any
- # @param [ActiveSupport::TimeWithZone] start_date start of time frame
- # @param [ActiveSupport::TimeWithZone] end_date end of time frame
- # @return [Integer] number of unique visitors
- def unique_visits_for(targets:, start_date: 7.days.ago, end_date: start_date + 1.week)
- events = if targets == :analytics
- self.class.analytics_events
- elsif targets == :compliance
- self.class.compliance_events
- else
- Array(targets)
- end
-
- Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: start_date, end_date: end_date)
- end
-
- class << self
- def analytics_events
- Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('analytics')
- end
-
- def compliance_events
- Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('compliance')
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb
index 6ef5a1e2cd8..3e095585b18 100644
--- a/lib/gitlab/application_context.rb
+++ b/lib/gitlab/application_context.rb
@@ -20,7 +20,8 @@ module Gitlab
:pipeline_id,
:related_class,
:feature_category,
- :artifact_size
+ :artifact_size,
+ :root_caller_id
].freeze
private_constant :KNOWN_KEYS
@@ -34,7 +35,8 @@ module Gitlab
Attribute.new(:job, ::Ci::Build),
Attribute.new(:related_class, String),
Attribute.new(:feature_category, String),
- Attribute.new(:artifact, ::Ci::JobArtifact)
+ Attribute.new(:artifact, ::Ci::JobArtifact),
+ Attribute.new(:root_caller_id, String)
].freeze
def self.known_keys
@@ -84,10 +86,11 @@ module Gitlab
hash[:project] = -> { project_path } if include_project?
hash[:root_namespace] = -> { root_namespace_path } if include_namespace?
hash[:client_id] = -> { client } if include_client?
- hash[:caller_id] = caller_id if set_values.include?(:caller_id)
- hash[:remote_ip] = remote_ip if set_values.include?(:remote_ip)
- hash[:related_class] = related_class if set_values.include?(:related_class)
- hash[:feature_category] = feature_category if set_values.include?(:feature_category)
+ assign_hash_if_value(hash, :caller_id)
+ assign_hash_if_value(hash, :root_caller_id)
+ assign_hash_if_value(hash, :remote_ip)
+ assign_hash_if_value(hash, :related_class)
+ assign_hash_if_value(hash, :feature_category)
hash[:pipeline_id] = -> { job&.pipeline_id } if set_values.include?(:job)
hash[:job_id] = -> { job&.id } if set_values.include?(:job)
hash[:artifact_size] = -> { artifact&.size } if set_values.include?(:artifact)
@@ -108,6 +111,14 @@ module Gitlab
lazy_attr_reader attr.name, type: attr.type
end
+ def assign_hash_if_value(hash, attribute_name)
+ raise ArgumentError unless KNOWN_KEYS.include?(attribute_name)
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ hash[attribute_name] = public_send(attribute_name) if set_values.include?(attribute_name)
+ # rubocop:enable GitlabSecurity/PublicSend
+ end
+
def assign_attributes(values)
values.slice(*APPLICATION_ATTRIBUTES.map(&:name)).each do |name, value|
instance_variable_set("@#{name}", value)
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index 41a6cbc2543..722ee061eba 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -32,6 +32,8 @@ 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 },
+ web_hook_calls_mid: { interval: 1.minute },
+ web_hook_calls_low: { interval: 1.minute },
users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes },
username_exists: { threshold: 20, interval: 1.minute },
user_sign_up: { threshold: 20, interval: 1.minute },
@@ -42,7 +44,8 @@ module Gitlab
search_rate_limit: { threshold: -> { application_settings.search_rate_limit }, interval: 1.minute },
search_rate_limit_unauthenticated: { threshold: -> { application_settings.search_rate_limit_unauthenticated }, interval: 1.minute },
gitlab_shell_operation: { threshold: 600, interval: 1.minute },
- pipelines_create: { threshold: 25, interval: 1.minute }
+ pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute },
+ temporary_email_failure: { threshold: 50, interval: 1.day }
}.freeze
end
@@ -190,3 +193,5 @@ module Gitlab
end
end
end
+
+Gitlab::ApplicationRateLimiter.prepend_mod
diff --git a/lib/gitlab/audit/unauthenticated_author.rb b/lib/gitlab/audit/unauthenticated_author.rb
index 84c323c1950..b811f9f8ad0 100644
--- a/lib/gitlab/audit/unauthenticated_author.rb
+++ b/lib/gitlab/audit/unauthenticated_author.rb
@@ -12,6 +12,10 @@ module Gitlab
def name
@name || _('An unauthenticated user')
end
+
+ def impersonated?
+ false
+ end
end
end
end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index d9efb6b8d2d..7d9c4c0d7c1 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -217,7 +217,11 @@ module Gitlab
def build_new_user(skip_confirmation: true)
user_params = user_attributes.merge(skip_confirmation: skip_confirmation)
- Users::AuthorizedBuildService.new(nil, user_params).execute
+ new_user = Users::AuthorizedBuildService.new(nil, user_params).execute
+
+ persist_accepted_terms_if_required(new_user)
+
+ new_user
end
def user_attributes
@@ -245,6 +249,15 @@ module Gitlab
}
end
+ def persist_accepted_terms_if_required(new_user)
+ if Feature.enabled?(:update_oauth_registration_flow) &&
+ Gitlab::CurrentSettings.current_application_settings.enforce_terms?
+
+ terms = ApplicationSetting::Term.latest
+ Users::RespondToTermsService.new(new_user, terms).execute(accepted: true)
+ end
+ end
+
def sync_profile_from_provider?
Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
end
diff --git a/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb b/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb
new file mode 100644
index 00000000000..814f5a897a9
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Backfill projectfeatures.package_registry_access_level depending on projects.packages_enabled
+ class BackfillProjectFeaturePackageRegistryAccessLevel < ::Gitlab::BackgroundMigration::BatchedMigrationJob
+ FEATURE_DISABLED = 0 # ProjectFeature::DISABLED
+ FEATURE_PRIVATE = 10 # ProjectFeature::PRIVATE
+ FEATURE_ENABLED = 20 # ProjectFeature::ENABLED
+ FEATURE_PUBLIC = 30 # ProjectFeature::PUBLIC
+ PROJECT_PRIVATE = 0 # Gitlab::VisibilityLevel::PRIVATE
+ PROJECT_INTERNAL = 10 # Gitlab::VisibilityLevel::INTERNAL
+ PROJECT_PUBLIC = 20 # Gitlab::VisibilityLevel::PUBLIC
+
+ # Migration only version of ProjectFeature table
+ class ProjectFeature < ::ApplicationRecord
+ self.table_name = 'project_features'
+ end
+
+ def perform
+ each_sub_batch(operation_name: :update_all) do |sub_batch|
+ ProjectFeature.connection.execute(
+ <<~SQL
+ UPDATE project_features pf
+ SET package_registry_access_level = (CASE p.packages_enabled
+ WHEN true THEN (CASE p.visibility_level
+ WHEN #{PROJECT_PUBLIC} THEN #{FEATURE_PUBLIC}
+ WHEN #{PROJECT_INTERNAL} THEN #{FEATURE_ENABLED}
+ WHEN #{PROJECT_PRIVATE} THEN #{FEATURE_PRIVATE}
+ END)
+ WHEN false THEN #{FEATURE_DISABLED}
+ ELSE #{FEATURE_DISABLED}
+ END)
+ FROM projects p
+ WHERE pf.project_id = p.id AND
+ pf.project_id BETWEEN #{start_id} AND #{end_id}
+ SQL
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_project_member_namespace_id.rb b/lib/gitlab/background_migration/backfill_project_member_namespace_id.rb
new file mode 100644
index 00000000000..c2e37269b5e
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_project_member_namespace_id.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Backfills the `members.member_namespace_id` column for `type=ProjectMember`
+ class BackfillProjectMemberNamespaceId < Gitlab::BackgroundMigration::BatchedMigrationJob
+ def perform
+ parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id)
+
+ parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size, order_hint: :type) do |sub_batch|
+ batch_metrics.time_operation(:update_all) do
+ # rubocop:disable Layout/LineLength
+ sub_batch.update_all('member_namespace_id = (SELECT projects.project_namespace_id FROM projects WHERE projects.id = source_id)')
+ # rubocop:enable Layout/LineLength
+ end
+
+ pause_ms_value = [0, pause_ms].max
+ sleep(pause_ms_value * 0.001)
+ end
+ end
+
+ def batch_metrics
+ @batch_metrics ||= Gitlab::Database::BackgroundMigration::BatchMetrics.new
+ end
+
+ private
+
+ def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
+ define_batchable_model(source_table, connection: ApplicationRecord.connection)
+ .where(source_key_column => start_id..stop_id)
+ .joins('INNER JOIN projects ON members.source_id = projects.id')
+ .where(type: 'ProjectMember', source_type: 'Project')
+ .where(member_namespace_id: nil)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_orphaned_routes.rb b/lib/gitlab/background_migration/cleanup_orphaned_routes.rb
new file mode 100644
index 00000000000..0cd19dc5df9
--- /dev/null
+++ b/lib/gitlab/background_migration/cleanup_orphaned_routes.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Removes orphaned routes, i.e. routes that reference a namespace or project that no longer exists.
+ # This was possible since we were using a polymorphic association source_id, source_type. However since now
+ # we have project namespaces we can use a FK on routes#namespace_id to avoid orphaned records in routes.
+ class CleanupOrphanedRoutes < Gitlab::BackgroundMigration::BatchedMigrationJob
+ include Gitlab::Database::DynamicModelHelpers
+
+ def perform
+ # there should really be no records to fix, there is none gitlab.com, but taking the safer route, just in case.
+ fix_missing_namespace_id_routes
+ cleanup_orphaned_routes
+ end
+
+ private
+
+ def fix_missing_namespace_id_routes
+ non_orphaned_namespace_routes = non_orphaned_namespace_routes_scoped_to_range(batch_column, start_id, end_id)
+ non_orphaned_project_routes = non_orphaned_project_routes_scoped_to_range(batch_column, start_id, end_id)
+
+ update_namespace_id(batch_column, non_orphaned_namespace_routes, sub_batch_size)
+ update_namespace_id(batch_column, non_orphaned_project_routes, sub_batch_size)
+ end
+
+ def cleanup_orphaned_routes
+ orphaned_namespace_routes = orphaned_namespace_routes_scoped_to_range(batch_column, start_id, end_id)
+ orphaned_project_routes = orphaned_project_routes_scoped_to_range(batch_column, start_id, end_id)
+
+ cleanup_relations(batch_column, orphaned_namespace_routes, pause_ms, sub_batch_size)
+ cleanup_relations(batch_column, orphaned_project_routes, pause_ms, sub_batch_size)
+ end
+
+ def update_namespace_id(batch_column, non_orphaned_namespace_routes, sub_batch_size)
+ non_orphaned_namespace_routes.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
+ batch_metrics.time_operation(:fix_missing_namespace_id) do
+ ApplicationRecord.connection.execute <<~SQL
+ WITH route_and_ns(route_id, namespace_id) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
+ #{sub_batch.to_sql}
+ )
+ UPDATE routes
+ SET namespace_id = route_and_ns.namespace_id
+ FROM route_and_ns
+ WHERE id = route_and_ns.route_id
+ SQL
+ end
+ end
+ end
+
+ def cleanup_relations(batch_column, orphaned_namespace_routes, pause_ms, sub_batch_size)
+ orphaned_namespace_routes.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
+ batch_metrics.time_operation(:cleanup_orphaned_routes) do
+ sub_batch.delete_all
+ end
+ end
+ end
+
+ def orphaned_namespace_routes_scoped_to_range(source_key_column, start_id, stop_id)
+ Gitlab::BackgroundMigration::Route.joins("LEFT OUTER JOIN namespaces ON source_id = namespaces.id")
+ .where(source_key_column => start_id..stop_id)
+ .where(source_type: 'Namespace')
+ .where(namespace_id: nil)
+ .where(namespaces: { id: nil })
+ end
+
+ def orphaned_project_routes_scoped_to_range(source_key_column, start_id, stop_id)
+ Gitlab::BackgroundMigration::Route.joins("LEFT OUTER JOIN projects ON source_id = projects.id")
+ .where(source_key_column => start_id..stop_id)
+ .where(source_type: 'Project')
+ .where(namespace_id: nil)
+ .where(projects: { id: nil })
+ end
+
+ def non_orphaned_namespace_routes_scoped_to_range(source_key_column, start_id, stop_id)
+ Gitlab::BackgroundMigration::Route.joins("LEFT OUTER JOIN namespaces ON source_id = namespaces.id")
+ .where(source_key_column => start_id..stop_id)
+ .where(source_type: 'Namespace')
+ .where(namespace_id: nil)
+ .where.not(namespaces: { id: nil })
+ .select("routes.id, namespaces.id")
+ end
+
+ def non_orphaned_project_routes_scoped_to_range(source_key_column, start_id, stop_id)
+ Gitlab::BackgroundMigration::Route.joins("LEFT OUTER JOIN projects ON source_id = projects.id")
+ .where(source_key_column => start_id..stop_id)
+ .where(source_type: 'Project')
+ .where(namespace_id: nil)
+ .where.not(projects: { id: nil })
+ .select("routes.id, projects.project_namespace_id")
+ end
+ end
+
+ # Isolated route model for the migration
+ class Route < ApplicationRecord
+ include EachBatch
+
+ self.table_name = 'routes'
+ self.inheritance_column = :_type_disabled
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_invalid_epic_issues.rb b/lib/gitlab/background_migration/delete_invalid_epic_issues.rb
new file mode 100644
index 00000000000..3af59ab4931
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_epic_issues.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # rubocop: disable Style/Documentation
+ class DeleteInvalidEpicIssues < BatchedMigrationJob
+ def perform
+ end
+ end
+ end
+end
+
+# rubocop:disable Layout/LineLength
+Gitlab::BackgroundMigration::DeleteInvalidEpicIssues.prepend_mod_with('Gitlab::BackgroundMigration::DeleteInvalidEpicIssues')
diff --git a/lib/gitlab/background_migration/fix_merge_request_diff_commit_users.rb b/lib/gitlab/background_migration/fix_merge_request_diff_commit_users.rb
index ea3e56cb14a..4df55a7b02a 100644
--- a/lib/gitlab/background_migration/fix_merge_request_diff_commit_users.rb
+++ b/lib/gitlab/background_migration/fix_merge_request_diff_commit_users.rb
@@ -5,12 +5,6 @@ module Gitlab
# Background migration for fixing merge_request_diff_commit rows that don't
# have committer/author details due to
# https://gitlab.com/gitlab-org/gitlab/-/issues/344080.
- #
- # This migration acts on a single project and corrects its data. Because
- # this process needs Git/Gitaly access, and duplicating all that code is far
- # too much, this migration relies on global models such as Project,
- # MergeRequest, etc.
- # rubocop: disable Metrics/ClassLength
class FixMergeRequestDiffCommitUsers
BATCH_SIZE = 100
@@ -20,137 +14,8 @@ module Gitlab
end
def perform(project_id)
- if (project = ::Project.find_by_id(project_id))
- process(project)
- end
-
- ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- 'FixMergeRequestDiffCommitUsers',
- [project_id]
- )
-
- schedule_next_job
- end
-
- def process(project)
- # Loading everything using one big query may result in timeouts (e.g.
- # for projects the size of gitlab-org/gitlab). So instead we query
- # data on a per merge request basis.
- project.merge_requests.each_batch(column: :iid) do |mrs|
- mrs.ids.each do |mr_id|
- each_row_to_check(mr_id) do |commit|
- update_commit(project, commit)
- end
- end
- end
- end
-
- def each_row_to_check(merge_request_id, &block)
- columns = %w[merge_request_diff_id relative_order].map do |col|
- Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: col,
- order_expression: MergeRequestDiffCommit.arel_table[col.to_sym].asc,
- nullable: :not_nullable,
- distinct: false
- )
- end
-
- order = Pagination::Keyset::Order.build(columns)
- scope = MergeRequestDiffCommit
- .joins(:merge_request_diff)
- .where(merge_request_diffs: { merge_request_id: merge_request_id })
- .where('commit_author_id IS NULL OR committer_id IS NULL')
- .order(order)
-
- Pagination::Keyset::Iterator
- .new(scope: scope, use_union_optimization: true)
- .each_batch(of: BATCH_SIZE) do |rows|
- rows
- .select([
- :merge_request_diff_id,
- :relative_order,
- :sha,
- :committer_id,
- :commit_author_id
- ])
- .each(&block)
- end
- end
-
- # rubocop: disable Metrics/AbcSize
- def update_commit(project, row)
- commit = find_commit(project, row.sha)
- updates = []
-
- unless row.commit_author_id
- author_id = find_or_create_user(commit, :author_name, :author_email)
-
- updates << [arel_table[:commit_author_id], author_id] if author_id
- end
-
- unless row.committer_id
- committer_id =
- find_or_create_user(commit, :committer_name, :committer_email)
-
- updates << [arel_table[:committer_id], committer_id] if committer_id
- end
-
- return if updates.empty?
-
- update = Arel::UpdateManager
- .new
- .table(MergeRequestDiffCommit.arel_table)
- .where(matches_row(row))
- .set(updates)
- .to_sql
-
- MergeRequestDiffCommit.connection.execute(update)
- end
- # rubocop: enable Metrics/AbcSize
-
- def schedule_next_job
- job = Database::BackgroundMigrationJob
- .for_migration_class('FixMergeRequestDiffCommitUsers')
- .pending
- .first
-
- return unless job
-
- BackgroundMigrationWorker.perform_in(
- 2.minutes,
- 'FixMergeRequestDiffCommitUsers',
- job.arguments
- )
- end
-
- def find_commit(project, sha)
- @commits[sha] ||= (project.commit(sha)&.to_hash || {})
- end
-
- def find_or_create_user(commit, name_field, email_field)
- name = commit[name_field]
- email = commit[email_field]
-
- return unless name && email
-
- @users[[name, email]] ||=
- MergeRequest::DiffCommitUser.find_or_create(name, email).id
- end
-
- def matches_row(row)
- primary_key = Arel::Nodes::Grouping
- .new([arel_table[:merge_request_diff_id], arel_table[:relative_order]])
-
- primary_val = Arel::Nodes::Grouping
- .new([row.merge_request_diff_id, row.relative_order])
-
- primary_key.eq(primary_val)
- end
-
- def arel_table
- MergeRequestDiffCommit.arel_table
+ # No-op, see https://gitlab.com/gitlab-org/gitlab/-/issues/344540
end
end
- # rubocop: enable Metrics/ClassLength
end
end
diff --git a/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb b/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb
index b7a912da060..f53f2e8ee79 100644
--- a/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb
+++ b/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb
@@ -9,10 +9,7 @@ module Gitlab
# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54578 for discussion
class MigratePagesToZipStorage
def perform(start_id, stop_id)
- ::Pages::MigrateFromLegacyStorageService.new(Gitlab::AppLogger,
- ignore_invalid_entries: false,
- mark_projects_as_not_deployed: false)
- .execute_for_batch(start_id..stop_id)
+ # no-op
end
end
end
diff --git a/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb b/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb
index 36d4e649271..13b66b2e02e 100644
--- a/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb
+++ b/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb
@@ -10,9 +10,9 @@ module Gitlab
pause_ms = 0 if pause_ms < 0
batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id)
- batch_relation.each_batch(column: batch_column, of: sub_batch_size, order_hint: :type) do |sub_batch|
+ batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
batch_metrics.time_operation(:update_all) do
- sub_batch.update_all(runner_id: nil)
+ filtered_sub_batch(sub_batch).update_all(runner_id: nil)
end
sleep(pause_ms * 0.001)
@@ -31,9 +31,13 @@ module Gitlab
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
define_batchable_model(source_table, connection: connection)
+ .where(source_key_column => start_id..stop_id)
+ end
+
+ def filtered_sub_batch(sub_batch)
+ sub_batch
.joins('LEFT OUTER JOIN ci_runners ON ci_runners.id = ci_builds.runner_id')
.where('ci_builds.runner_id IS NOT NULL AND ci_runners.id IS NULL')
- .where(source_key_column => start_id..stop_id)
end
end
end
diff --git a/lib/gitlab/background_migration/purge_stale_security_scans.rb b/lib/gitlab/background_migration/purge_stale_security_scans.rb
new file mode 100644
index 00000000000..8b13a0382b4
--- /dev/null
+++ b/lib/gitlab/background_migration/purge_stale_security_scans.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # rubocop:disable Style/Documentation
+ class PurgeStaleSecurityScans # rubocop:disable Migration/BackgroundMigrationBaseClass
+ class SecurityScan < ::ApplicationRecord
+ include EachBatch
+
+ STALE_AFTER = 90.days
+
+ self.table_name = 'security_scans'
+
+ # Otherwise the schema_spec fails
+ validates :info, json_schema: { filename: 'security_scan_info', draft: 7 }
+
+ enum status: { succeeded: 1, purged: 6 }
+
+ scope :to_purge, -> { where('id <= ?', last_stale_record_id) }
+ scope :by_range, -> (range) { where(id: range) }
+
+ def self.last_stale_record_id
+ where('created_at < ?', STALE_AFTER.ago).order(created_at: :desc).first
+ end
+ end
+
+ def perform(_start_id, _end_id); end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::PurgeStaleSecurityScans.prepend_mod
diff --git a/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects.rb b/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects.rb
new file mode 100644
index 00000000000..e85b1bc402a
--- /dev/null
+++ b/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Set `project_settings.legacy_open_source_license_available` to false for non-public projects
+ class SetLegacyOpenSourceLicenseAvailableForNonPublicProjects < ::Gitlab::BackgroundMigration::BatchedMigrationJob
+ PUBLIC = 20
+
+ # Migration only version of `project_settings` table
+ class ProjectSetting < ApplicationRecord
+ self.table_name = 'project_settings'
+ end
+
+ def perform
+ each_sub_batch(
+ operation_name: :set_legacy_open_source_license_available,
+ batching_scope: ->(relation) { relation.where.not(visibility_level: PUBLIC) }
+ ) do |sub_batch|
+ ProjectSetting.where(project_id: sub_batch).update_all(legacy_open_source_license_available: false)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/base_doorkeeper_controller.rb b/lib/gitlab/base_doorkeeper_controller.rb
index 0f370850b5b..81b01395542 100644
--- a/lib/gitlab/base_doorkeeper_controller.rb
+++ b/lib/gitlab/base_doorkeeper_controller.rb
@@ -6,6 +6,9 @@ module Gitlab
class BaseDoorkeeperController < ActionController::Base
include Gitlab::Allowable
include EnforcesTwoFactorAuthentication
+ include SessionsHelper
+
+ before_action :limit_session_time, if: -> { !current_user }
helper_method :can?
end
diff --git a/lib/gitlab/bitbucket_server_import/project_creator.rb b/lib/gitlab/bitbucket_server_import/project_creator.rb
index 48ca4951957..ddc678abdd8 100644
--- a/lib/gitlab/bitbucket_server_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_server_import/project_creator.rb
@@ -19,7 +19,7 @@ module Gitlab
::Projects::CreateService.new(
current_user,
name: name,
- path: name,
+ path: repo_slug,
description: repo.description,
namespace_id: namespace.id,
visibility_level: repo.visibility_level,
diff --git a/lib/gitlab/checks/changes_access.rb b/lib/gitlab/checks/changes_access.rb
index 2e469aabeb2..99752dc6a01 100644
--- a/lib/gitlab/checks/changes_access.rb
+++ b/lib/gitlab/checks/changes_access.rb
@@ -36,35 +36,17 @@ module Gitlab
# any of the new revisions.
def commits
strong_memoize(:commits) do
- allow_quarantine = true
-
newrevs = @changes.map do |change|
- oldrev = change[:oldrev]
newrev = change[: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
next [] if newrevs.empty?
- # When filtering quarantined commits we can enable usage of the object
- # quarantine no matter whether we have an `oldrev` or not.
- if Feature.enabled?(:filter_quarantined_commits)
- allow_quarantine = true
- end
-
- project.repository.new_commits(newrevs, allow_quarantine: allow_quarantine)
+ project.repository.new_commits(newrevs)
end
end
diff --git a/lib/gitlab/checks/single_change_access.rb b/lib/gitlab/checks/single_change_access.rb
index 8e12801daee..2fd48dfbfe2 100644
--- a/lib/gitlab/checks/single_change_access.rb
+++ b/lib/gitlab/checks/single_change_access.rb
@@ -35,8 +35,7 @@ module Gitlab
end
def commits
- @commits ||= project.repository.new_commits(newrev,
- allow_quarantine: Feature.enabled?(:filter_quarantined_commits))
+ @commits ||= project.repository.new_commits(newrev)
end
protected
diff --git a/lib/gitlab/checks/tag_check.rb b/lib/gitlab/checks/tag_check.rb
index a45db85301a..5dd7720b67d 100644
--- a/lib/gitlab/checks/tag_check.rb
+++ b/lib/gitlab/checks/tag_check.rb
@@ -6,7 +6,9 @@ module Gitlab
ERROR_MESSAGES = {
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
- delete_protected_tag: 'Protected tags cannot be deleted.',
+ delete_protected_tag: 'You are not allowed to delete protected tags from this project. '\
+ 'Only a project maintainer or owner can delete a protected tag.',
+ delete_protected_tag_non_web: 'You can only delete protected tags using the web interface.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.'
}.freeze
@@ -34,7 +36,16 @@ module Gitlab
return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:update_protected_tag]) if update?
- raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
+
+ if deletion?
+ unless user_access.user.can?(:maintainer_access, project)
+ raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag])
+ end
+
+ unless updated_from_web?
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag_non_web]
+ end
+ end
unless user_access.can_create_tag?(tag_name)
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_protected_tag]
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
index 8ddcf1d523e..7dc375e05eb 100644
--- a/lib/gitlab/ci/build/image.rb
+++ b/lib/gitlab/ci/build/image.rb
@@ -4,7 +4,7 @@ module Gitlab
module Ci
module Build
class Image
- attr_reader :alias, :command, :entrypoint, :name, :ports, :variables
+ attr_reader :alias, :command, :entrypoint, :name, :ports, :variables, :pull_policy
class << self
def from_image(job)
@@ -34,6 +34,7 @@ module Gitlab
@name = image[:name]
@ports = build_ports(image).select(&:valid?)
@variables = build_variables(image)
+ @pull_policy = image[:pull_policy]
end
end
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 21c42857895..79443f69b03 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -12,11 +12,13 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Configurable
- ALLOWED_KEYS = %i[name entrypoint ports].freeze
+ ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
+ LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
validations do
validates :config, hash_or_string: true
- validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, allowed_keys: ALLOWED_KEYS, if: :ci_docker_image_pull_policy_enabled?
+ validates :config, allowed_keys: LEGACY_ALLOWED_KEYS, unless: :ci_docker_image_pull_policy_enabled?
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true
@@ -26,7 +28,10 @@ module Gitlab
entry :ports, Entry::Ports,
description: 'Ports used to expose the image'
- attributes :ports
+ entry :pull_policy, Entry::PullPolicy,
+ description: 'Pull policy for the image'
+
+ attributes :ports, :pull_policy
def name
value[:name]
@@ -37,16 +42,28 @@ module Gitlab
end
def value
- return { name: @config } if string?
- return @config if hash?
-
- {}
+ if string?
+ { name: @config }
+ elsif hash?
+ {
+ name: @config[:name],
+ entrypoint: @config[:entrypoint],
+ ports: ports_value,
+ pull_policy: (ci_docker_image_pull_policy_enabled? ? pull_policy_value : nil)
+ }.compact
+ else
+ {}
+ end
end
def with_image_ports?
opt(:with_image_ports)
end
+ def ci_docker_image_pull_policy_enabled?
+ ::Feature.enabled?(:ci_docker_image_pull_policy)
+ end
+
def skip_config_hash_validation?
true
end
diff --git a/lib/gitlab/ci/config/entry/pull_policy.rb b/lib/gitlab/ci/config/entry/pull_policy.rb
new file mode 100644
index 00000000000..f597134dd2c
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/pull_policy.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a configuration of the pull policies of an image.
+ #
+ class PullPolicy < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ ALLOWED_POLICIES = %w[always never if-not-present].freeze
+
+ validations do
+ validates :config, array_of_strings_or_string: true
+ validates :config,
+ allowed_array_values: { in: ALLOWED_POLICIES },
+ presence: true,
+ if: :array?
+ validates :config,
+ inclusion: { in: ALLOWED_POLICIES },
+ if: :string?
+ end
+
+ def value
+ # We either return an array with policies or nothing
+ Array(@config).presence
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
index 4722f2e9a61..63bf1b38ac6 100644
--- a/lib/gitlab/ci/config/entry/rules/rule.rb
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -9,11 +9,13 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
- CLAUSES = %i[if changes exists].freeze
- ALLOWED_KEYS = %i[if changes exists when start_in allow_failure variables].freeze
- ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze
+ ALLOWED_KEYS = %i[if changes exists when start_in allow_failure variables].freeze
+ ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
- attributes :if, :changes, :exists, :when, :start_in, :allow_failure
+ attributes :if, :exists, :when, :start_in, :allow_failure
+
+ entry :changes, Entry::Rules::Rule::Changes,
+ description: 'File change condition rule.'
entry :variables, Entry::Variables,
description: 'Environment variables to define for rule conditions.'
@@ -28,8 +30,8 @@ module Gitlab
with_options allow_nil: true do
validates :if, expression: true
- validates :changes, :exists, array_of_strings: true, length: { maximum: 50 }
- validates :when, allowed_values: { in: ALLOWABLE_WHEN }
+ validates :exists, array_of_strings: true, length: { maximum: 50 }
+ validates :when, allowed_values: { in: ALLOWED_WHEN }
validates :allow_failure, boolean: true
end
@@ -41,6 +43,13 @@ module Gitlab
end
end
+ def value
+ config.merge(
+ changes: (changes_value if changes_defined?),
+ variables: (variables_value if variables_defined?)
+ ).compact
+ end
+
def specifies_delay?
self.when == 'delayed'
end
diff --git a/lib/gitlab/ci/config/entry/rules/rule/changes.rb b/lib/gitlab/ci/config/entry/rules/rule/changes.rb
new file mode 100644
index 00000000000..be57e089f34
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules/rule/changes.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules
+ class Rule
+ class Changes < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config,
+ array_of_strings: true,
+ length: { maximum: 50, too_long: "has too many entries (maximum %{count})" }
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/external/file/local.rb b/lib/gitlab/ci/config/external/file/local.rb
index feb2cbb19ad..36fc5c656fc 100644
--- a/lib/gitlab/ci/config/external/file/local.rb
+++ b/lib/gitlab/ci/config/external/file/local.rb
@@ -42,7 +42,9 @@ module Gitlab
end
def fetch_local_content
- context.project.repository.blob_data_at(context.sha, location)
+ context.logger.instrument(:config_file_fetch_local_content) do
+ context.project.repository.blob_data_at(context.sha, location)
+ end
rescue GRPC::InvalidArgument
errors.push("Sha #{context.sha} is not valid!")
diff --git a/lib/gitlab/ci/config/external/file/project.rb b/lib/gitlab/ci/config/external/file/project.rb
index 09c36a1bcb6..b7fef081269 100644
--- a/lib/gitlab/ci/config/external/file/project.rb
+++ b/lib/gitlab/ci/config/external/file/project.rb
@@ -65,7 +65,9 @@ module Gitlab
return unless can_access_local_content?
return unless sha
- project.repository.blob_data_at(sha, location)
+ context.logger.instrument(:config_file_fetch_project_content) do
+ project.repository.blob_data_at(sha, location)
+ end
rescue GRPC::NotFound, GRPC::Internal
nil
end
diff --git a/lib/gitlab/ci/config/external/file/remote.rb b/lib/gitlab/ci/config/external/file/remote.rb
index 7d3a2362246..3984bf9e4f8 100644
--- a/lib/gitlab/ci/config/external/file/remote.rb
+++ b/lib/gitlab/ci/config/external/file/remote.rb
@@ -40,7 +40,9 @@ module Gitlab
def fetch_remote_content
begin
- response = Gitlab::HTTP.get(location)
+ response = context.logger.instrument(:config_file_fetch_remote_content) do
+ Gitlab::HTTP.get(location)
+ end
rescue SocketError
errors.push("Remote file `#{masked_location}` could not be fetched because of a socket error!")
rescue Timeout::Error
diff --git a/lib/gitlab/ci/config/external/file/template.rb b/lib/gitlab/ci/config/external/file/template.rb
index 58b81b259cb..5fcf7c71bdf 100644
--- a/lib/gitlab/ci/config/external/file/template.rb
+++ b/lib/gitlab/ci/config/external/file/template.rb
@@ -52,7 +52,9 @@ module Gitlab
end
def fetch_template_content
- Gitlab::Template::GitlabCiYmlTemplate.find(template_name, context.project)&.content
+ context.logger.instrument(:config_file_fetch_template_content) do
+ Gitlab::Template::GitlabCiYmlTemplate.find(template_name, context.project)&.content
+ end
end
def masked_raw
diff --git a/lib/gitlab/ci/jwt.rb b/lib/gitlab/ci/jwt.rb
index 97774bc5e13..19678def666 100644
--- a/lib/gitlab/ci/jwt.rb
+++ b/lib/gitlab/ci/jwt.rb
@@ -73,11 +73,7 @@ module Gitlab
def key
@key ||= begin
- key_data = if Feature.enabled?(:ci_jwt_signing_key, build.project)
- Gitlab::CurrentSettings.ci_jwt_signing_key
- else
- Rails.application.secrets.openid_connect_signing_key
- end
+ key_data = Gitlab::CurrentSettings.ci_jwt_signing_key
raise NoSigningKeyError unless key_data
diff --git a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
index 4460843545e..ee7733a081d 100644
--- a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
+++ b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
@@ -55,14 +55,8 @@ module Gitlab
end
def schema_path
- # We can't exactly error out here pre-15.0.
- # If the report itself doesn't specify the schema version,
- # it will be considered invalid post-15.0 but for now we will
- # validate against earliest supported version.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/335789#note_801479803
- # describes the indended behavior in detail
- # TODO: After 15.0 - pass report_type and report_data here and
- # error out if no version.
+ # The schema version selection logic here is described in the user documentation:
+ # https://docs.gitlab.com/ee/user/application_security/#security-report-validation
report_declared_version = File.join(root_path, report_version, file_name)
return report_declared_version if File.file?(report_declared_version)
diff --git a/lib/gitlab/ci/pipeline/chain/limit/rate_limit.rb b/lib/gitlab/ci/pipeline/chain/limit/rate_limit.rb
index 17ebf56985b..af5cc7fe523 100644
--- a/lib/gitlab/ci/pipeline/chain/limit/rate_limit.rb
+++ b/lib/gitlab/ci/pipeline/chain/limit/rate_limit.rb
@@ -7,10 +7,9 @@ module Gitlab
module Limit
class RateLimit < Chain::Base
include Chain::Helpers
+ include ::Gitlab::Utils::StrongMemoize
def perform!
- return unless throttle_enabled?
-
# We exclude child-pipelines from the rate limit because they represent
# sub-pipelines that would otherwise hit the rate limit due to having the
# same scope (project, user, sha).
@@ -19,7 +18,7 @@ module Gitlab
if rate_limit_throttled?
create_log_entry
- error(throttle_message) unless dry_run?
+ error(throttle_message) if enforce_throttle?
end
end
@@ -43,7 +42,9 @@ module Gitlab
commit_sha: command.sha,
current_user_id: current_user.id,
subscription_plan: project.actual_plan_name,
- message: 'Activated pipeline creation rate limit'
+ message: 'Activated pipeline creation rate limit',
+ throttled: enforce_throttle?,
+ throttle_override: throttle_override?
)
end
@@ -51,16 +52,17 @@ module Gitlab
'Too many pipelines created in the last minute. Try again later.'
end
- def throttle_enabled?
- ::Feature.enabled?(
- :ci_throttle_pipelines_creation,
- project)
+ def enforce_throttle?
+ strong_memoize(:enforce_throttle) do
+ ::Feature.enabled?(:ci_enforce_throttle_pipelines_creation, project) &&
+ !throttle_override?
+ end
end
- def dry_run?
- ::Feature.enabled?(
- :ci_throttle_pipelines_creation_dry_run,
- project)
+ def throttle_override?
+ strong_memoize(:throttle_override) do
+ ::Feature.enabled?(:ci_enforce_throttle_pipelines_creation_override, project)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb
index 85bd5f0a7c1..8177502be1d 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/external.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb
@@ -83,7 +83,9 @@ module Gitlab
project: {
id: project.id,
path: project.full_path,
- created_at: project.created_at&.iso8601
+ created_at: project.created_at&.iso8601,
+ shared_runners_enabled: project.shared_runners_enabled?,
+ group_runners_enabled: project.group_runners_enabled?
},
user: {
id: current_user.id,
diff --git a/lib/gitlab/ci/reports/coverage_reports.rb b/lib/gitlab/ci/reports/coverage_report.rb
index 31afb636d2f..cebbb9ae842 100644
--- a/lib/gitlab/ci/reports/coverage_reports.rb
+++ b/lib/gitlab/ci/reports/coverage_report.rb
@@ -3,13 +3,17 @@
module Gitlab
module Ci
module Reports
- class CoverageReports
+ class CoverageReport
attr_reader :files
def initialize
@files = {}
end
+ def empty?
+ @files.empty?
+ end
+
def pick(keys)
coverage_files = files.select do |key|
keys.include?(key)
diff --git a/lib/gitlab/ci/reports/coverage_report_generator.rb b/lib/gitlab/ci/reports/coverage_report_generator.rb
new file mode 100644
index 00000000000..fd73ed6fd25
--- /dev/null
+++ b/lib/gitlab/ci/reports/coverage_report_generator.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ class CoverageReportGenerator
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def report
+ coverage_report = Gitlab::Ci::Reports::CoverageReport.new
+
+ # Return an empty report if the pipeline is a child pipeline.
+ # Since the coverage report is used in a merge request report,
+ # we are only interested in the coverage report from the root pipeline.
+ return coverage_report if @pipeline.child?
+
+ coverage_report.tap do |coverage_report|
+ report_builds.find_each do |build|
+ build.each_report(::Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
+ blob,
+ coverage_report,
+ project_path: @pipeline.project.full_path,
+ worktree_paths: @pipeline.all_worktree_paths
+ )
+ end
+ end
+ end
+ end
+
+ private
+
+ def report_builds
+ if child_pipeline_feature_enabled?
+ @pipeline.latest_report_builds_in_self_and_descendants(::Ci::JobArtifact.coverage_reports)
+ else
+ @pipeline.latest_report_builds(::Ci::JobArtifact.coverage_reports)
+ end
+ end
+
+ def child_pipeline_feature_enabled?
+ strong_memoize(:feature_enabled) do
+ Feature.enabled?(:ci_child_pipeline_coverage_reports, @pipeline.project)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/runner_upgrade_check.rb b/lib/gitlab/ci/runner_upgrade_check.rb
index 46b41ed3c6c..0808290fe5b 100644
--- a/lib/gitlab/ci/runner_upgrade_check.rb
+++ b/lib/gitlab/ci/runner_upgrade_check.rb
@@ -20,15 +20,27 @@ module Gitlab
return :invalid unless runner_version
releases = RunnerReleases.instance.releases
- parsed_runner_version = runner_version.is_a?(::Gitlab::VersionInfo) ? runner_version : ::Gitlab::VersionInfo.parse(runner_version)
+ orig_runner_version = runner_version
+ runner_version = ::Gitlab::VersionInfo.parse(runner_version) unless runner_version.is_a?(::Gitlab::VersionInfo)
- raise ArgumentError, "'#{runner_version}' is not a valid version" unless parsed_runner_version.valid?
+ raise ArgumentError, "'#{orig_runner_version}' is not a valid version" unless runner_version.valid?
- available_releases = releases.reject { |release| release > @gitlab_version }
+ gitlab_minor_version = version_without_patch(@gitlab_version)
- return :recommended if available_releases.any? { |available_release| patch_update?(available_release, parsed_runner_version) }
- return :recommended if outside_backport_window?(parsed_runner_version, releases)
- return :available if available_releases.any? { |available_release| available_release > parsed_runner_version }
+ available_releases = releases
+ .reject { |release| release.major > @gitlab_version.major }
+ .reject do |release|
+ release_minor_version = version_without_patch(release)
+
+ # Do not reject a patch update, even if the runner is ahead of the instance version
+ next false if version_without_patch(runner_version) == release_minor_version
+
+ release_minor_version > gitlab_minor_version
+ end
+
+ return :recommended if available_releases.any? { |available_rel| patch_update?(available_rel, runner_version) }
+ return :recommended if outside_backport_window?(runner_version, releases)
+ return :available if available_releases.any? { |available_rel| available_rel > runner_version }
:not_available
end
diff --git a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
index 856a097e6e0..8886929646d 100644
--- a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
@@ -9,7 +9,7 @@ image: "crystallang/crystal:latest"
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: https://docs.gitlab.com/ee/ci/services/index.html
# services:
# - mysql:latest
# - redis:latest
diff --git a/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml b/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml
index c1815baf7e6..ab4c9b701d0 100644
--- a/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml
@@ -11,7 +11,7 @@
#
# --------------------
#
-# Documentation: https://docs.gitlab.com/ee/ci/cloud_deployment/#deploy-your-application-to-the-aws-elastic-container-service-ecs
+# Documentation: https://docs.gitlab.com/ee/ci/cloud_deployment/#deploy-your-application-to-ecs
stages:
- build
@@ -23,5 +23,5 @@ stages:
"error: Template has moved":
stage: deploy
script:
- - echo "Deploy-ECS.gitlab-ci.yml has been moved to AWS/Deploy-ECS.gitlab-ci.yml, see https://docs.gitlab.com/ee/ci/cloud_deployment/#deploy-your-application-to-the-aws-elastic-container-service-ecs for more details."
+ - echo "Deploy-ECS.gitlab-ci.yml has been moved to AWS/Deploy-ECS.gitlab-ci.yml, see https://docs.gitlab.com/ee/ci/cloud_deployment/#deploy-your-application-to-ecs for more details."
- exit 1
diff --git a/lib/gitlab/ci/templates/Django.gitlab-ci.yml b/lib/gitlab/ci/templates/Django.gitlab-ci.yml
index 426076c84a1..acc4a9d2917 100644
--- a/lib/gitlab/ci/templates/Django.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Django.gitlab-ci.yml
@@ -41,7 +41,7 @@ default:
#
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
- # Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+ # Check out: https://docs.gitlab.com/ee/ci/services/index.html
services:
- mysql:8.0
#
diff --git a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
index 1ceaf9fc86b..1eb920c7747 100644
--- a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
@@ -7,7 +7,7 @@ image: elixir:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: https://docs.gitlab.com/ee/ci/services/index.html
services:
- mysql:latest
- redis:latest
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index 6a6fc2cb702..8f1124373c4 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.28.2'
+ DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.30.0'
.dast-auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 98c4216679f..f9c0d4333ff 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.28.2'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.30.0'
.auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
index 603be5b1cdb..36f1b6981c4 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.28.2'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.30.0'
.auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
index ff7bac15017..0ec67526234 100644
--- a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
@@ -9,7 +9,7 @@ image: php:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: https://docs.gitlab.com/ee/ci/services/index.html
services:
- mysql:latest
diff --git a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
index 16bc0026aa8..44370f896a7 100644
--- a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
@@ -9,7 +9,7 @@ image: node:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: https://docs.gitlab.com/ee/ci/services/index.html
services:
- mysql:latest
- redis:latest
diff --git a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
index 281bf7e3dd9..4edc003a638 100644
--- a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
@@ -23,7 +23,7 @@ before_script:
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
-# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# Bring in any services we need https://docs.gitlab.com/ee/ci/services/index.html
# See http://docs.gitlab.com/ee/ci/services/README.html for examples.
services:
- mysql:5.7
diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
index 44f959468a8..690a5a291e1 100644
--- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
@@ -9,7 +9,7 @@ image: ruby:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: https://docs.gitlab.com/ee/ci/services/index.html
services:
- mysql:latest
- redis:latest
diff --git a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
index 869c1782352..390f0bb8061 100644
--- a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
@@ -9,7 +9,7 @@ image: "rust:latest"
# Optional: Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: https://docs.gitlab.com/ee/ci/services/index.html
# services:
# - mysql:latest
# - redis:latest
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 f7f016b5e57..d4b6a252b25 100644
--- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
@@ -12,8 +12,8 @@ variables:
# Which branch we want to run full fledged long running fuzzing jobs.
# All others will run fuzzing regression
COVFUZZ_BRANCH: "$CI_DEFAULT_BRANCH"
- # This is using semantic version and will always download latest v2 gitlab-cov-fuzz release
- COVFUZZ_VERSION: v2
+ # This is using semantic version and will always download latest v3 gitlab-cov-fuzz release
+ COVFUZZ_VERSION: v3
# This is for users who have an offline environment and will have to replicate gitlab-cov-fuzz release binaries
# to their own servers
COVFUZZ_URL_PREFIX: "https://gitlab.com/gitlab-org/security-products/analyzers/gitlab-cov-fuzz/-/raw"
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
index 3f9c87b7abf..4a72f5e72b1 100644
--- 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
@@ -1,3 +1,8 @@
+# To contribute improvements to CI/CD templates, please follow the Development guide at:
+# https://docs.gitlab.com/ee/development/cicd/templates.html
+# This specific template is located at:
+# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/DAST-On-Demand-API-Scan.gitlab-ci.yml
+
stages:
- build
- test
@@ -6,12 +11,13 @@ stages:
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
- DAST_API_VERSION: "1"
- DAST_API_IMAGE: $SECURE_ANALYZERS_PREFIX/api-fuzzing:$DAST_API_VERSION
+ DAST_API_VERSION: "2"
+ DAST_API_IMAGE_SUFFIX: ""
+ DAST_API_IMAGE: api-security
dast:
stage: dast
- image: $DAST_API_IMAGE
+ image: $SECURE_ANALYZERS_PREFIX/$DAST_API_IMAGE:$DAST_API_VERSION$DAST_API_IMAGE_SUFFIX
allow_failure: true
script:
- /peach/analyzer-dast-api
diff --git a/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
index e5ac5099546..10549b56856 100644
--- a/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
@@ -48,13 +48,10 @@ dast:
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME &&
- $REVIEW_DISABLED && $DAST_WEBSITE == null &&
- $DAST_API_SPECIFICATION == null
+ $REVIEW_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
($CI_KUBERNETES_ACTIVE || $KUBECONFIG) &&
$GITLAB_FEATURES =~ /\bdast\b/
- if: $CI_COMMIT_BRANCH &&
- $DAST_WEBSITE
- - if: $CI_COMMIT_BRANCH &&
- $DAST_API_SPECIFICATION
+ $GITLAB_FEATURES =~ /\bdast\b/
diff --git a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
index b34bfe2a53c..c414e70bfa3 100644
--- a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
@@ -20,7 +20,7 @@ variables:
SECURE_BINARIES_ANALYZERS: >-
bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, secrets, sobelow, pmd-apex, kics, kubesec, semgrep, gemnasium, gemnasium-maven, gemnasium-python,
license-finder,
- dast, dast-runner-validation, api-fuzzing
+ dast, dast-runner-validation, api-security
SECURE_BINARIES_DOWNLOAD_IMAGES: "true"
SECURE_BINARIES_PUSH_IMAGES: "true"
@@ -252,11 +252,11 @@ dast-runner-validation:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bdast-runner-validation\b/
-api-fuzzing:
+api-security:
extends: .download_images
variables:
- SECURE_BINARIES_ANALYZER_VERSION: "1"
+ SECURE_BINARIES_ANALYZER_VERSION: "2"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
- $SECURE_BINARIES_ANALYZERS =~ /\bapi-fuzzing\b/
+ $SECURE_BINARIES_ANALYZERS =~ /\bapi-security\b/
diff --git a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
index 56151a6bcdf..4d0259fe678 100644
--- a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
@@ -1,7 +1,7 @@
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
-# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
+# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
include:
- template: Terraform/Base.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
diff --git a/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
index 49bdd4b7713..6f9a9c5133c 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
@@ -4,7 +4,7 @@
# they are able to only include the jobs that they find interesting.
#
# Therefore, this template is not supposed to run any jobs. The idea is to only
-# create hidden jobs. See: https://docs.gitlab.com/ee/ci/yaml/#hide-jobs
+# create hidden jobs. See: https://docs.gitlab.com/ee/ci/jobs/#hide-jobs
#
# There is a more opinionated template which we suggest the users to abide,
# which is the lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index e93bd75a9fa..95a60b852b8 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -74,14 +74,14 @@ module Gitlab
end
def exist?
- archived? || live_trace_exist?
+ archived? || live?
end
def archived?
trace_artifact&.stored?
end
- def live_trace_exist?
+ def live?
job.trace_chunks.any? || current_path.present? || old_trace.present?
end
diff --git a/lib/gitlab/ci/trace/archive.rb b/lib/gitlab/ci/trace/archive.rb
index d4a451ca526..0cd8df2e2af 100644
--- a/lib/gitlab/ci/trace/archive.rb
+++ b/lib/gitlab/ci/trace/archive.rb
@@ -15,7 +15,7 @@ module Gitlab
def execute!(stream)
clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path|
- md5_checksum = self.class.md5_hexdigest(clone_path)
+ md5_checksum = self.class.md5_hexdigest(clone_path) unless Gitlab::FIPS.enabled?
sha256_checksum = self.class.sha256_hexdigest(clone_path)
job.transaction do
@@ -24,7 +24,7 @@ module Gitlab
end
end
- validate_archived_trace
+ validate_archived_trace unless Gitlab::FIPS.enabled?
end
private
diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb
index 6ce7046262b..40418a4c797 100644
--- a/lib/gitlab/config/entry/node.rb
+++ b/lib/gitlab/config/entry/node.rb
@@ -106,6 +106,10 @@ module Gitlab
@config.is_a?(Hash)
end
+ def array?
+ @config.is_a?(Array)
+ end
+
def string?
@config.is_a?(String)
end
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index 521dec110a8..574a7dceaa4 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -44,6 +44,7 @@ module Gitlab
allow_sentry(directives) if Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn
allow_framed_gitlab_paths(directives)
allow_customersdot(directives) if ENV['CUSTOMER_PORTAL_URL'].present?
+ allow_review_apps(directives) if ENV['REVIEW_APPS_ENABLED']
# The follow section contains workarounds to patch Safari's lack of support for CSP Level 3
# See https://gitlab.com/gitlab-org/gitlab/-/issues/343579
@@ -154,6 +155,11 @@ module Gitlab
append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path))
end
end
+
+ def self.allow_review_apps(directives)
+ # Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions
+ append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/')
+ end
end
end
end
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 5f579f90629..04d13778499 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -2,10 +2,20 @@
module Gitlab
class Daemon
- def self.initialize_instance(...)
- raise "#{name} singleton instance already initialized" if @instance
+ # Options:
+ # - recreate: We usually only allow a single instance per process to exist;
+ # this can be overridden with this switch, so that existing
+ # instances are stopped and recreated.
+ def self.initialize_instance(*args, recreate: false, **options)
+ if @instance
+ if recreate
+ @instance.stop
+ else
+ raise "#{name} singleton instance already initialized"
+ end
+ end
- @instance = new(...)
+ @instance = new(*args, **options)
Kernel.at_exit(&@instance.method(:stop))
@instance
end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 385f1e57705..c13bb1d6a9a 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -12,7 +12,7 @@ module Gitlab
def initialize(pipeline)
@pipeline = pipeline
- super(
+ attrs = {
object_kind: 'pipeline',
object_attributes: hook_attrs(pipeline),
merge_request: pipeline.merge_request && merge_request_attrs(pipeline.merge_request),
@@ -23,7 +23,13 @@ module Gitlab
preload_builds(pipeline, :latest_builds)
pipeline.latest_builds.map(&method(:build_hook_attrs))
end
- )
+ }
+
+ if pipeline.source_pipeline.present?
+ attrs[:source_pipeline] = source_pipeline_attrs(pipeline.source_pipeline)
+ end
+
+ super(attrs)
end
def with_retried_builds
@@ -72,6 +78,20 @@ module Gitlab
}
end
+ def source_pipeline_attrs(source_pipeline)
+ project = source_pipeline.source_project
+
+ {
+ project: {
+ id: project.id,
+ web_url: project.web_url,
+ path_with_namespace: project.full_path
+ },
+ job_id: source_pipeline.source_job_id,
+ pipeline_id: source_pipeline.source_pipeline_id
+ }
+ end
+
def merge_request_attrs(merge_request)
{
id: merge_request.id,
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 677b4485288..b42d164d9c4 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -68,7 +68,8 @@ module Gitlab
@schemas_to_base_models ||= {
gitlab_main: [self.database_base_models.fetch(:main)],
gitlab_ci: [self.database_base_models[:ci] || self.database_base_models.fetch(:main)], # use CI or fallback to main
- gitlab_shared: self.database_base_models.values # all models
+ gitlab_shared: self.database_base_models.values, # all models
+ gitlab_internal: self.database_base_models.values # all models
}.with_indifferent_access.freeze
end
@@ -203,8 +204,13 @@ module Gitlab
# This does not look at literal connection names, but rather compares
# models that are holders for a given db_config_name
def self.gitlab_schemas_for_connection(connection)
- db_name = self.db_config_name(connection)
- primary_model = self.database_base_models.fetch(db_name.to_sym)
+ db_config = self.db_config_for_connection(connection)
+
+ # connection might not be yet adopted (returning NullPool, and no connection_klass)
+ # in such cases it is fine to ignore such connections
+ return unless db_config
+
+ primary_model = self.database_base_models.fetch(db_config.name.to_sym)
self.schemas_to_base_models.select do |_, child_models|
child_models.any? do |child_model|
@@ -218,8 +224,11 @@ module Gitlab
def self.db_config_for_connection(connection)
return unless connection
+ # For a ConnectionProxy we want to avoid ambiguous db_config as it may
+ # sometimes default to replica so we always return the primary config
+ # instead.
if connection.is_a?(::Gitlab::Database::LoadBalancing::ConnectionProxy)
- return connection.load_balancer.configuration.primary_db_config
+ return connection.load_balancer.configuration.db_config
end
# During application init we might receive `NullPool`
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
index a90cae7aea2..d052d5adc4c 100644
--- a/lib/gitlab/database/background_migration/batched_migration.rb
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -30,9 +30,23 @@ module Gitlab
scope :created_after, ->(time) { where('created_at > ?', time) }
- scope :for_configuration, ->(job_class_name, table_name, column_name, job_arguments) do
- where(job_class_name: job_class_name, table_name: table_name, column_name: column_name)
+ scope :for_configuration, ->(gitlab_schema, job_class_name, table_name, column_name, job_arguments) do
+ relation = where(job_class_name: job_class_name, table_name: table_name, column_name: column_name)
.where("job_arguments = ?", job_arguments.to_json) # rubocop:disable Rails/WhereEquals
+
+ # This method is called from migrations older than the gitlab_schema column,
+ # check and add this filter only if the column exists.
+ relation = relation.for_gitlab_schema(gitlab_schema) if gitlab_schema_column_exists?
+
+ relation
+ end
+
+ def self.gitlab_schema_column_exists?
+ column_names.include?('gitlab_schema')
+ end
+
+ scope :for_gitlab_schema, ->(gitlab_schema) do
+ where(gitlab_schema: gitlab_schema)
end
state_machine :status, initial: :paused do
@@ -73,12 +87,13 @@ module Gitlab
state_machine.states.map(&:name)
end
- def self.find_for_configuration(job_class_name, table_name, column_name, job_arguments)
- for_configuration(job_class_name, table_name, column_name, job_arguments).first
+ def self.find_for_configuration(gitlab_schema, job_class_name, table_name, column_name, job_arguments)
+ for_configuration(gitlab_schema, job_class_name, table_name, column_name, job_arguments).first
end
- def self.active_migration
- executable.queue_order.first
+ def self.active_migration(connection:)
+ for_gitlab_schema(Gitlab::Database.gitlab_schemas_for_connection(connection))
+ .executable.queue_order.first
end
def self.successful_rows_counts(migrations)
diff --git a/lib/gitlab/database/background_migration/batched_migration_runner.rb b/lib/gitlab/database/background_migration/batched_migration_runner.rb
index 59ff9a9744f..388eb596ce2 100644
--- a/lib/gitlab/database/background_migration/batched_migration_runner.rb
+++ b/lib/gitlab/database/background_migration/batched_migration_runner.rb
@@ -54,7 +54,10 @@ module Gitlab
# in order to prevent it being picked up by the background worker. Perform all pending jobs,
# then keep running until migration is finished.
def finalize(job_class_name, table_name, column_name, job_arguments)
- migration = BatchedMigration.find_for_configuration(job_class_name, table_name, column_name, job_arguments)
+ migration = BatchedMigration.find_for_configuration(
+ Gitlab::Database.gitlab_schemas_for_connection(connection),
+ job_class_name, table_name, column_name, job_arguments
+ )
configuration = {
job_class_name: job_class_name,
diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb
index 49f56b5be97..92a41bb36ee 100644
--- a/lib/gitlab/database/batch_count.rb
+++ b/lib/gitlab/database/batch_count.rb
@@ -1,7 +1,11 @@
# frozen_string_literal: true
# For large tables, PostgreSQL can take a long time to count rows due to MVCC.
-# Implements a distinct and ordinary batch counter
+# Implements:
+# - distinct batch counter
+# - ordinary batch counter
+# - sum batch counter
+# - average batch counter
# Needs indexes on the column below to calculate max, min and range queries
# For larger tables just set use higher batch_size with index optimization
#
@@ -22,6 +26,8 @@
# batch_distinct_count(Project.group(:visibility_level), :creator_id)
# batch_sum(User, :sign_in_count)
# batch_sum(Issue.group(:state_id), :weight))
+# batch_average(Ci::Pipeline, :duration)
+# batch_average(MergeTrain.group(:status), :duration)
module Gitlab
module Database
module BatchCount
@@ -37,6 +43,10 @@ module Gitlab
BatchCounter.new(relation, column: nil, operation: :sum, operation_args: [column]).count(batch_size: batch_size, start: start, finish: finish)
end
+ def batch_average(relation, column, batch_size: nil, start: nil, finish: nil)
+ BatchCounter.new(relation, column: nil, operation: :average, operation_args: [column]).count(batch_size: batch_size, start: start, finish: finish)
+ end
+
class << self
include BatchCount
end
diff --git a/lib/gitlab/database/batch_counter.rb b/lib/gitlab/database/batch_counter.rb
index 417511618e4..522b598cd9d 100644
--- a/lib/gitlab/database/batch_counter.rb
+++ b/lib/gitlab/database/batch_counter.rb
@@ -6,6 +6,7 @@ module Gitlab
FALLBACK = -1
MIN_REQUIRED_BATCH_SIZE = 1_250
DEFAULT_SUM_BATCH_SIZE = 1_000
+ DEFAULT_AVERAGE_BATCH_SIZE = 1_000
MAX_ALLOWED_LOOPS = 10_000
SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep
ALLOWED_MODES = [:itself, :distinct].freeze
@@ -26,6 +27,7 @@ module Gitlab
def unwanted_configuration?(finish, batch_size, start)
(@operation == :count && batch_size <= MIN_REQUIRED_BATCH_SIZE) ||
(@operation == :sum && batch_size < DEFAULT_SUM_BATCH_SIZE) ||
+ (@operation == :average && batch_size < DEFAULT_AVERAGE_BATCH_SIZE) ||
(finish - start) / batch_size >= MAX_ALLOWED_LOOPS ||
start >= finish
end
@@ -92,6 +94,7 @@ module Gitlab
def batch_size_for_mode_and_operation(mode, operation)
return DEFAULT_SUM_BATCH_SIZE if operation == :sum
+ return DEFAULT_AVERAGE_BATCH_SIZE if operation == :average
mode == :distinct ? DEFAULT_DISTINCT_BATCH_SIZE : DEFAULT_BATCH_SIZE
end
diff --git a/lib/gitlab/database/consistency_checker.rb b/lib/gitlab/database/consistency_checker.rb
index e398fef744c..bf60c76a085 100644
--- a/lib/gitlab/database/consistency_checker.rb
+++ b/lib/gitlab/database/consistency_checker.rb
@@ -3,9 +3,9 @@
module Gitlab
module Database
class ConsistencyChecker
- BATCH_SIZE = 1000
- MAX_BATCHES = 25
- MAX_RUNTIME = 30.seconds # must be less than the scheduling frequency of the ConsistencyCheck jobs
+ BATCH_SIZE = 500
+ MAX_BATCHES = 20
+ MAX_RUNTIME = 5.seconds # must be less than the scheduling frequency of the ConsistencyCheck jobs
delegate :monotonic_time, to: :'Gitlab::Metrics::System'
diff --git a/lib/gitlab/database/gitlab_schema.rb b/lib/gitlab/database/gitlab_schema.rb
index d7ea614e2af..baf4cc48424 100644
--- a/lib/gitlab/database/gitlab_schema.rb
+++ b/lib/gitlab/database/gitlab_schema.rb
@@ -75,8 +75,8 @@ module Gitlab
return gitlab_schema
end
- # All tables from `information_schema.` are `:gitlab_shared`
- return :gitlab_shared if schema_name == 'information_schema'
+ # All tables from `information_schema.` are marked as `internal`
+ return :gitlab_internal if schema_name == 'information_schema'
return :gitlab_main if table_name.start_with?('_test_gitlab_main_')
@@ -85,8 +85,8 @@ module Gitlab
# All tables that start with `_test_` without a following schema are shared and ignored
return :gitlab_shared if table_name.start_with?('_test_')
- # All `pg_` tables are marked as `shared`
- return :gitlab_shared if table_name.start_with?('pg_')
+ # All `pg_` tables are marked as `internal`
+ return :gitlab_internal if table_name.start_with?('pg_')
# When undefined it's best to return a unique name so that we don't incorrectly assume that 2 undefined schemas belong on the same database
:"undefined_#{table_name}"
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index 036ce7d7631..71c323cb393 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -35,10 +35,11 @@ approval_project_rules_users: :gitlab_main
approvals: :gitlab_main
approver_groups: :gitlab_main
approvers: :gitlab_main
-ar_internal_metadata: :gitlab_shared
+ar_internal_metadata: :gitlab_internal
atlassian_identities: :gitlab_main
audit_events_external_audit_event_destinations: :gitlab_main
audit_events: :gitlab_main
+audit_events_streaming_headers: :gitlab_main
authentication_events: :gitlab_main
award_emoji: :gitlab_main
aws_roles: :gitlab_main
@@ -121,6 +122,7 @@ ci_unit_tests: :gitlab_ci
ci_variables: :gitlab_ci
cluster_agents: :gitlab_main
cluster_agent_tokens: :gitlab_main
+cluster_enabled_grants: :gitlab_main
cluster_groups: :gitlab_main
cluster_platforms_kubernetes: :gitlab_main
cluster_projects: :gitlab_main
@@ -217,7 +219,6 @@ geo_event_log: :gitlab_main
geo_events: :gitlab_main
geo_hashed_storage_attachments_events: :gitlab_main
geo_hashed_storage_migrated_events: :gitlab_main
-geo_lfs_object_deleted_events: :gitlab_main
geo_node_namespace_links: :gitlab_main
geo_nodes: :gitlab_main
geo_node_statuses: :gitlab_main
@@ -266,6 +267,7 @@ integrations: :gitlab_main
internal_ids: :gitlab_main
ip_restrictions: :gitlab_main
issuable_metric_images: :gitlab_main
+issuable_resource_links: :gitlab_main
issuable_severities: :gitlab_main
issuable_slas: :gitlab_main
issue_assignees: :gitlab_main
@@ -465,7 +467,7 @@ routes: :gitlab_main
saml_group_links: :gitlab_main
saml_providers: :gitlab_main
saved_replies: :gitlab_main
-schema_migrations: :gitlab_shared
+schema_migrations: :gitlab_internal
scim_identities: :gitlab_main
scim_oauth_access_tokens: :gitlab_main
security_findings: :gitlab_main
@@ -491,6 +493,7 @@ software_license_policies: :gitlab_main
software_licenses: :gitlab_main
spam_logs: :gitlab_main
sprints: :gitlab_main
+ssh_signatures: :gitlab_main
status_check_responses: :gitlab_main
status_page_published_incidents: :gitlab_main
status_page_settings: :gitlab_main
@@ -503,6 +506,7 @@ term_agreements: :gitlab_main
terraform_states: :gitlab_main
terraform_state_versions: :gitlab_main
timelogs: :gitlab_main
+timelog_categories: :gitlab_main
todos: :gitlab_main
token_with_ivs: :gitlab_main
topics: :gitlab_main
@@ -549,6 +553,7 @@ vulnerability_occurrences: :gitlab_main
vulnerability_reads: :gitlab_main
vulnerability_remediations: :gitlab_main
vulnerability_scanners: :gitlab_main
+vulnerability_state_transitions: :gitlab_main
vulnerability_statistics: :gitlab_main
vulnerability_user_mentions: :gitlab_main
webauthn_registrations: :gitlab_main
@@ -556,10 +561,13 @@ web_hook_logs: :gitlab_main
web_hooks: :gitlab_main
wiki_page_meta: :gitlab_main
wiki_page_slugs: :gitlab_main
+work_item_parent_links: :gitlab_main
work_item_types: :gitlab_main
x509_certificates: :gitlab_main
x509_commit_signatures: :gitlab_main
x509_issuers: :gitlab_main
zentao_tracker_data: :gitlab_main
+# dingtalk_tracker_data JiHu-specific, see https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/417
+dingtalk_tracker_data: :gitlab_main
zoom_meetings: :gitlab_main
batched_background_migration_job_transition_logs: :gitlab_shared
diff --git a/lib/gitlab/database/load_balancing/configuration.rb b/lib/gitlab/database/load_balancing/configuration.rb
index 0ddc745ebae..59b08fac7e9 100644
--- a/lib/gitlab/database/load_balancing/configuration.rb
+++ b/lib/gitlab/database/load_balancing/configuration.rb
@@ -41,8 +41,6 @@ module Gitlab
end
end
- config.reuse_primary_connection!
-
config
end
@@ -61,44 +59,17 @@ module Gitlab
disconnect_timeout: 120,
use_tcp: false
}
-
- # Temporary model for GITLAB_LOAD_BALANCING_REUSE_PRIMARY_
- # To be removed with FF
- @primary_model = nil
end
def db_config_name
@model.connection_db_config.name.to_sym
end
- # With connection re-use the primary connection can be overwritten
- # to be used from different model
- def primary_connection_specification_name
- primary_model_or_model_if_enabled.connection_specification_name
- end
-
- def primary_model_or_model_if_enabled
- if use_dedicated_connection?
- @model
- else
- @primary_model || @model
- end
- end
-
- def use_dedicated_connection?
- return true unless @primary_model # We can only use dedicated connection, if re-use of connections is disabled
- return false unless ::Gitlab::SafeRequestStore.active?
-
- ::Gitlab::SafeRequestStore.fetch(:force_no_sharing_primary_model) do
- ::Feature::FlipperFeature.table_exists? && ::Feature.enabled?(:force_no_sharing_primary_model)
- end
- end
-
- def primary_db_config
- primary_model_or_model_if_enabled.connection_db_config
+ def connection_specification_name
+ @model.connection_specification_name
end
- def replica_db_config
+ def db_config
@model.connection_db_config
end
@@ -131,30 +102,6 @@ module Gitlab
service_discovery[:record].present?
end
-
- # TODO: This is temporary code to allow re-use of primary connection
- # if the two connections are pointing to the same host. This is needed
- # to properly support transaction visibility.
- #
- # This behavior is required to support [Phase 3](https://gitlab.com/groups/gitlab-org/-/epics/6160#progress).
- # This method is meant to be removed as soon as it is finished.
- #
- # The remapping is done as-is:
- # export GITLAB_LOAD_BALANCING_REUSE_PRIMARY_<name-of-connection>=<new-name-of-connection>
- #
- # Ex.:
- # export GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main
- #
- def reuse_primary_connection!
- new_connection = ENV["GITLAB_LOAD_BALANCING_REUSE_PRIMARY_#{db_config_name}"]
- return unless new_connection.present?
-
- @primary_model = Gitlab::Database.database_base_models[new_connection.to_sym]
-
- unless @primary_model
- raise "Invalid value for 'GITLAB_LOAD_BALANCING_REUSE_PRIMARY_#{db_config_name}=#{new_connection}'"
- end
- end
end
end
end
diff --git a/lib/gitlab/database/load_balancing/connection_proxy.rb b/lib/gitlab/database/load_balancing/connection_proxy.rb
index 1be63da8896..8799f8d8af8 100644
--- a/lib/gitlab/database/load_balancing/connection_proxy.rb
+++ b/lib/gitlab/database/load_balancing/connection_proxy.rb
@@ -23,6 +23,7 @@ module Gitlab
insert
update
update_all
+ exec_insert_all
).freeze
NON_STICKY_READS = %i(
diff --git a/lib/gitlab/database/load_balancing/load_balancer.rb b/lib/gitlab/database/load_balancing/load_balancer.rb
index 191ebe18b8a..40b76a1c028 100644
--- a/lib/gitlab/database/load_balancing/load_balancer.rb
+++ b/lib/gitlab/database/load_balancing/load_balancer.rb
@@ -104,12 +104,24 @@ module Gitlab
# Yields a connection that can be used for both reads and writes.
def read_write
connection = nil
+ transaction_open = nil
# In the event of a failover the primary may be briefly unavailable.
# Instead of immediately grinding to a halt we'll retry the operation
# a few times.
retry_with_backoff do
connection = pool.connection
+ transaction_open = connection.transaction_open?
+
yield connection
+ rescue StandardError => e
+ if transaction_open && connection_error?(e)
+ ::Gitlab::Database::LoadBalancing::Logger.warn(
+ event: :transaction_leak,
+ message: 'A write transaction has leaked during database fail-over'
+ )
+ end
+
+ raise e
end
end
@@ -232,14 +244,14 @@ module Gitlab
# host - An optional host name to use instead of the default one.
# port - An optional port to connect to.
def create_replica_connection_pool(pool_size, host = nil, port = nil)
- db_config = @configuration.replica_db_config
+ db_config = @configuration.db_config
env_config = db_config.configuration_hash.dup
env_config[:pool] = pool_size
env_config[:host] = host if host
env_config[:port] = port if port
- replica_db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
+ db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
db_config.env_name,
db_config.name + REPLICA_SUFFIX,
env_config
@@ -249,7 +261,7 @@ module Gitlab
# as it will rewrite ActiveRecord::Base.connection
ActiveRecord::ConnectionAdapters::ConnectionHandler
.new
- .establish_connection(replica_db_config)
+ .establish_connection(db_config)
end
# ActiveRecord::ConnectionAdapters::ConnectionHandler handles fetching,
@@ -258,7 +270,7 @@ module Gitlab
# rubocop:disable Database/MultipleDatabases
def pool
ActiveRecord::Base.connection_handler.retrieve_connection_pool(
- @configuration.primary_connection_specification_name,
+ @configuration.connection_specification_name,
role: ActiveRecord::Base.writing_role,
shard: ActiveRecord::Base.default_shard
) || raise(::ActiveRecord::ConnectionNotEstablished)
diff --git a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
index 62dfe75a851..13afbd8fd37 100644
--- a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
+++ b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
@@ -39,18 +39,31 @@ module Gitlab
end
job['wal_locations'] = locations
+ job['wal_location_source'] = wal_location_source
+ end
+
+ def wal_location_source
+ if ::Gitlab::Database::LoadBalancing.primary_only? || uses_primary?
+ ::Gitlab::Database::LoadBalancing::ROLE_PRIMARY
+ else
+ ::Gitlab::Database::LoadBalancing::ROLE_REPLICA
+ end
end
def wal_location_for(load_balancer)
# When only using the primary there's no need for any WAL queries.
return if load_balancer.primary_only?
- if ::Gitlab::Database::LoadBalancing::Session.current.use_primary?
+ if uses_primary?
load_balancer.primary_write_location
else
load_balancer.host.database_replica_location
end
end
+
+ def uses_primary?
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary?
+ end
end
end
end
diff --git a/lib/gitlab/database/migration.rb b/lib/gitlab/database/migration.rb
index 038af570dbc..ab8b6988c3d 100644
--- a/lib/gitlab/database/migration.rb
+++ b/lib/gitlab/database/migration.rb
@@ -37,6 +37,7 @@ module Gitlab
class V1_0 < ActiveRecord::Migration[6.1] # rubocop:disable Naming/ClassAndModuleCamelCase
include LockRetriesConcern
include Gitlab::Database::MigrationHelpers::V2
+ include Gitlab::Database::MigrationHelpers::AnnounceDatabase
# When running migrations, the `db:migrate` switches connection of
# ActiveRecord::Base depending where the migration runs.
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 0453b81d67d..4bb1d71ce18 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -945,8 +945,13 @@ module Gitlab
end
def ensure_batched_background_migration_is_finished(job_class_name:, table_name:, column_name:, job_arguments:, finalize: true)
- migration = Gitlab::Database::BackgroundMigration::BatchedMigration
- .for_configuration(job_class_name, table_name, column_name, job_arguments).first
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_dml_mode!
+
+ Gitlab::Database::BackgroundMigration::BatchedMigration.reset_column_information
+ migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(
+ Gitlab::Database.gitlab_schemas_for_connection(connection),
+ job_class_name, table_name, column_name, job_arguments
+ )
configuration = {
job_class_name: job_class_name,
@@ -966,7 +971,7 @@ module Gitlab
"but it is '#{migration.status_name}':" \
"\t#{configuration}" \
"\n\n" \
- "Finalize it manually by running" \
+ "Finalize it manually by running the following command in a `bash` or `sh` shell:" \
"\n\n" \
"\tsudo gitlab-rake gitlab:background_migrations:finalize[#{job_class_name},#{table_name},#{column_name},'#{job_arguments.to_json.gsub(',', '\,')}']" \
"\n\n" \
@@ -1494,6 +1499,20 @@ into similar problems in the future (e.g. when new tables are created).
SQL
end
+ def drop_sequence(table_name, column_name, sequence_name)
+ execute <<~SQL
+ ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} DROP DEFAULT;
+ DROP SEQUENCE IF EXISTS #{quote_table_name(sequence_name)}
+ SQL
+ end
+
+ def add_sequence(table_name, column_name, sequence_name, start_value)
+ execute <<~SQL
+ CREATE SEQUENCE #{quote_table_name(sequence_name)} START #{start_value};
+ ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT nextval(#{quote(sequence_name)})
+ SQL
+ end
+
private
def create_temporary_columns_and_triggers(table, columns, primary_key: :id, data_type: :bigint)
diff --git a/lib/gitlab/database/migration_helpers/announce_database.rb b/lib/gitlab/database/migration_helpers/announce_database.rb
new file mode 100644
index 00000000000..28710aab717
--- /dev/null
+++ b/lib/gitlab/database/migration_helpers/announce_database.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module MigrationHelpers
+ module AnnounceDatabase
+ extend ActiveSupport::Concern
+
+ def write(text = "")
+ if text.present? # announce/say
+ super("#{db_config_name}: #{text}")
+ else
+ super(text)
+ end
+ end
+
+ def db_config_name
+ @db_config_name ||= Gitlab::Database.db_config_name(connection)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb
index d8d07fcaf2d..b8d1d21a0d2 100644
--- a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb
+++ b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb
@@ -21,7 +21,7 @@ module Gitlab
end
end
- def migrate(direction)
+ def exec_migration(conn, direction)
if unmatched_schemas.any?
migration_skipped
return
@@ -37,8 +37,9 @@ module Gitlab
private
def migration_skipped
- say "Current migration is skipped since it modifies "\
- "'#{self.class.allowed_gitlab_schemas}' which is outside of '#{allowed_schemas_for_connection}'"
+ say "The migration is skipped since it modifies the schemas: #{self.class.allowed_gitlab_schemas}."
+ say "This database can only apply migrations in one of the following schemas: " \
+ "#{allowed_schemas_for_connection}."
end
def validator_class
diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
index 7113c3686f1..4aaeaa7b365 100644
--- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
@@ -67,10 +67,22 @@ module Gitlab
batch_class_name: BATCH_CLASS_NAME,
batch_size: BATCH_SIZE,
max_batch_size: nil,
- sub_batch_size: SUB_BATCH_SIZE
+ sub_batch_size: SUB_BATCH_SIZE,
+ gitlab_schema: nil
)
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_dml_mode!
- if Gitlab::Database::BackgroundMigration::BatchedMigration.for_configuration(job_class_name, batch_table_name, batch_column_name, job_arguments).exists?
+ if transaction_open?
+ raise 'The `queue_batched_background_migration` cannot be run inside a transaction. ' \
+ 'You can disable transactions by calling `disable_ddl_transaction!` in the body of ' \
+ 'your migration class.'
+ end
+
+ gitlab_schema ||= gitlab_schema_from_context
+
+ Gitlab::Database::BackgroundMigration::BatchedMigration.reset_column_information
+
+ if Gitlab::Database::BackgroundMigration::BatchedMigration.for_configuration(gitlab_schema, job_class_name, batch_table_name, batch_column_name, job_arguments).exists?
Gitlab::AppLogger.warn "Batched background migration not enqueued because it already exists: " \
"job_class_name: #{job_class_name}, table_name: #{batch_table_name}, column_name: #{batch_column_name}, " \
"job_arguments: #{job_arguments.inspect}"
@@ -119,24 +131,77 @@ module Gitlab
end
end
+ if migration.respond_to?(:gitlab_schema)
+ migration.gitlab_schema = gitlab_schema
+ end
+
migration.save!
migration
end
def finalize_batched_background_migration(job_class_name:, table_name:, column_name:, job_arguments:)
- database_name = Gitlab::Database.db_config_name(connection)
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_dml_mode!
- unless ActiveRecord::Base.configurations.primary?(database_name)
- raise 'The `#finalize_background_migration` is currently not supported when running in decomposed database, ' \
- 'and this database is not `main:`. For more information visit: ' \
- 'https://docs.gitlab.com/ee/development/database/migrations_for_multiple_databases.html'
+ if transaction_open?
+ raise 'The `finalize_batched_background_migration` cannot be run inside a transaction. ' \
+ 'You can disable transactions by calling `disable_ddl_transaction!` in the body of ' \
+ 'your migration class.'
end
- migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(job_class_name, table_name, column_name, job_arguments)
+ Gitlab::Database::BackgroundMigration::BatchedMigration.reset_column_information
+
+ migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(
+ gitlab_schema_from_context, job_class_name, table_name, column_name, job_arguments)
raise 'Could not find batched background migration' if migration.nil?
- Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection)
+ with_restored_connection_stack do |restored_connection|
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do
+ Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(
+ job_class_name, table_name,
+ column_name, job_arguments,
+ connection: restored_connection)
+ end
+ end
+ end
+
+ # Deletes batched background migration for the given configuration.
+ #
+ # job_class_name - The background migration job class as a string
+ # table_name - The name of the table the migration iterates over
+ # column_name - The name of the column the migration will batch over
+ # job_arguments - Migration arguments
+ #
+ # Example:
+ #
+ # delete_batched_background_migration(
+ # 'CopyColumnUsingBackgroundMigrationJob',
+ # :events,
+ # :id,
+ # ['column1', 'column2'])
+ def delete_batched_background_migration(job_class_name, table_name, column_name, job_arguments)
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_dml_mode!
+
+ if transaction_open?
+ raise 'The `#delete_batched_background_migration` cannot be run inside a transaction. ' \
+ 'You can disable transactions by calling `disable_ddl_transaction!` in the body of ' \
+ 'your migration class.'
+ end
+
+ Gitlab::Database::BackgroundMigration::BatchedMigration.reset_column_information
+
+ Gitlab::Database::BackgroundMigration::BatchedMigration
+ .for_configuration(
+ gitlab_schema_from_context, job_class_name, table_name, column_name, job_arguments
+ ).delete_all
+ end
+
+ def gitlab_schema_from_context
+ if respond_to?(:allowed_gitlab_schemas) # Gitlab::Database::Migration::V2_0
+ Array(allowed_gitlab_schemas).first
+ else # Gitlab::Database::Migration::V1_0
+ :gitlab_main
+ end
end
end
end
diff --git a/lib/gitlab/database/migrations/reestablished_connection_stack.rb b/lib/gitlab/database/migrations/reestablished_connection_stack.rb
index d7cf482c32a..addc9d874af 100644
--- a/lib/gitlab/database/migrations/reestablished_connection_stack.rb
+++ b/lib/gitlab/database/migrations/reestablished_connection_stack.rb
@@ -17,7 +17,9 @@ module Gitlab
original_handler = ActiveRecord::Base.connection_handler
original_db_config = ActiveRecord::Base.connection_db_config
- return yield if ActiveRecord::Base.configurations.primary?(original_db_config.name)
+ if ActiveRecord::Base.configurations.primary?(original_db_config.name)
+ return yield(ActiveRecord::Base.connection)
+ end
# If the `ActiveRecord::Base` connection is different than `:main`
# re-establish and configure `SharedModel` context accordingly
@@ -43,7 +45,7 @@ module Gitlab
ActiveRecord::Base.establish_connection :main # rubocop:disable Database/EstablishConnection
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
- yield
+ yield(base_model.connection)
end
ensure
ActiveRecord::Base.connection_handler = original_handler
diff --git a/lib/gitlab/database/partitioning/monthly_strategy.rb b/lib/gitlab/database/partitioning/monthly_strategy.rb
index 9c8cccb3dc6..0f08a47d754 100644
--- a/lib/gitlab/database/partitioning/monthly_strategy.rb
+++ b/lib/gitlab/database/partitioning/monthly_strategy.rb
@@ -40,6 +40,10 @@ module Gitlab
# No-op, required by the partition manager
end
+ def validate_and_fix
+ # No-op, required by the partition manager
+ end
+
private
def desired_partitions
diff --git a/lib/gitlab/database/partitioning/partition_manager.rb b/lib/gitlab/database/partitioning/partition_manager.rb
index 3ee9a193b45..aac91eaadb1 100644
--- a/lib/gitlab/database/partitioning/partition_manager.rb
+++ b/lib/gitlab/database/partitioning/partition_manager.rb
@@ -22,15 +22,13 @@ module Gitlab
connection_name: @connection_name
)
- # Double-checking before getting the lease:
- # The prevailing situation is no missing partitions and no extra partitions
- return if missing_partitions.empty? && extra_partitions.empty?
-
only_with_exclusive_lease(model, lease_key: MANAGEMENT_LEASE_KEY) do
- partitions_to_create = missing_partitions
- create(partitions_to_create) unless partitions_to_create.empty?
+ model.partitioning_strategy.validate_and_fix
+ partitions_to_create = missing_partitions
partitions_to_detach = extra_partitions
+
+ create(partitions_to_create) unless partitions_to_create.empty?
detach(partitions_to_detach) unless partitions_to_detach.empty?
end
rescue StandardError => e
@@ -119,7 +117,8 @@ module Gitlab
parent_table_identifier = "#{connection.current_schema}.#{partition.table}"
if (example_fk = PostgresForeignKey.by_referenced_table_identifier(parent_table_identifier).first)
- raise UnsafeToDetachPartitionError, "Cannot detach #{partition.partition_name}, it would block while checking foreign key #{example_fk.name} on #{example_fk.constrained_table_identifier}"
+ raise UnsafeToDetachPartitionError, "Cannot detach #{partition.partition_name}, it would block while " \
+ "checking foreign key #{example_fk.name} on #{example_fk.constrained_table_identifier}"
end
end
diff --git a/lib/gitlab/database/partitioning/sliding_list_strategy.rb b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
index e9865fb91d6..5cf32d3272c 100644
--- a/lib/gitlab/database/partitioning/sliding_list_strategy.rb
+++ b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
@@ -48,9 +48,12 @@ module Gitlab
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)
+ 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
@@ -73,6 +76,42 @@ module Gitlab
current_partitions.empty?
end
+ def validate_and_fix
+ return unless Feature.enabled?(:fix_sliding_list_partitioning)
+ return if no_partitions_exist?
+
+ old_default_value = current_default_value
+ expected_default_value = active_partition.value
+
+ if old_default_value != expected_default_value
+ with_lock_retries do
+ model.connection.execute("LOCK TABLE #{model.table_name} IN ACCESS EXCLUSIVE MODE")
+
+ old_default_value = current_default_value
+ expected_default_value = active_partition.value
+
+ if old_default_value == expected_default_value
+ Gitlab::AppLogger.warn(
+ message: "Table partitions or partition key default value have been changed by another process",
+ table_name: table_name,
+ default_value: expected_default_value
+ )
+ raise ActiveRecord::Rollback
+ end
+
+ model.connection.change_column_default(model.table_name, partitioning_key, expected_default_value)
+ Gitlab::AppLogger.warn(
+ message: "Fixed default value of sliding_list_strategy partitioning_key",
+ column: partitioning_key,
+ table_name: table_name,
+ connection_name: model.connection.pool.db_config.name,
+ old_value: old_default_value,
+ new_value: expected_default_value
+ )
+ end
+ end
+ end
+
private
def current_default_value
@@ -95,6 +134,14 @@ module Gitlab
raise "Add #{partitioning_key} to #{model.name}.ignored_columns to use it with SlidingListStrategy"
end
end
+
+ def with_lock_retries(&block)
+ Gitlab::Database::WithLockRetries.new(
+ klass: self.class,
+ logger: Gitlab::AppLogger,
+ connection: model.connection
+ ).run(&block)
+ end
end
end
end
diff --git a/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb b/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb
index 391375d472f..06e2b114c91 100644
--- a/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb
+++ b/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb
@@ -27,15 +27,9 @@ module Gitlab
# to reduce amount of labels sort schemas used
gitlab_schemas = gitlab_schemas.to_a.sort.join(",")
- # Temporary feature to observe relation of `gitlab_schemas` to `db_config_name`
- # depending on primary model
- ci_dedicated_primary_connection = ::Ci::ApplicationRecord.connection_class? &&
- ::Ci::ApplicationRecord.load_balancer.configuration.use_dedicated_connection?
-
schemas_metrics.increment({
gitlab_schemas: gitlab_schemas,
- db_config_name: db_config_name,
- ci_dedicated_primary_connection: ci_dedicated_primary_connection
+ db_config_name: db_config_name
})
end
diff --git a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
index 3f0176cb654..c51282c9a55 100644
--- a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
+++ b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
@@ -9,7 +9,7 @@ module Gitlab
DMLNotAllowedError = Class.new(UnsupportedSchemaError)
DMLAccessDeniedError = Class.new(UnsupportedSchemaError)
- IGNORED_SCHEMAS = %i[gitlab_shared].freeze
+ IGNORED_SCHEMAS = %i[gitlab_shared gitlab_internal].freeze
class << self
def enabled?
diff --git a/lib/gitlab/database/shared_model.rb b/lib/gitlab/database/shared_model.rb
index f4c8fca8fa2..877866b9b23 100644
--- a/lib/gitlab/database/shared_model.rb
+++ b/lib/gitlab/database/shared_model.rb
@@ -20,6 +20,15 @@ module Gitlab
"to '#{Gitlab::Database.db_config_name(connection)}'"
end
+ # connection might not be yet adopted (returning nil, and no gitlab_schemas)
+ # in such cases it is fine to ignore such connections
+ gitlab_schemas = Gitlab::Database.gitlab_schemas_for_connection(connection)
+
+ unless gitlab_schemas.nil? || gitlab_schemas.include?(:gitlab_shared)
+ raise "Cannot set `SharedModel` to connection from `#{Gitlab::Database.db_config_name(connection)}` " \
+ "since this connection does not include `:gitlab_shared` schema."
+ end
+
self.overriding_connection = connection
yield
diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb
index 111ea697ec2..ffd057e1d33 100644
--- a/lib/gitlab/devise_failure.rb
+++ b/lib/gitlab/devise_failure.rb
@@ -9,3 +9,5 @@ module Gitlab
end
end
end
+
+Gitlab::DeviseFailure.prepend_mod
diff --git a/lib/gitlab/diff/custom_diff.rb b/lib/gitlab/diff/custom_diff.rb
deleted file mode 100644
index 860f87a28a3..00000000000
--- a/lib/gitlab/diff/custom_diff.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-module Gitlab
- module Diff
- module CustomDiff
- RENDERED_TIMEOUT_BACKGROUND = 20.seconds
- RENDERED_TIMEOUT_FOREGROUND = 1.5.seconds
- BACKGROUND_EXECUTION = 'background'
- FOREGROUND_EXECUTION = 'foreground'
- LOG_IPYNBDIFF_GENERATED = 'IPYNB_DIFF_GENERATED'
- LOG_IPYNBDIFF_TIMEOUT = 'IPYNB_DIFF_TIMEOUT'
- LOG_IPYNBDIFF_INVALID = 'IPYNB_DIFF_INVALID'
-
- class << self
- def preprocess_before_diff(path, old_blob, new_blob)
- return unless path.ends_with? '.ipynb'
-
- Timeout.timeout(timeout_time) do
- transformed_diff(old_blob&.data, new_blob&.data)&.tap do
- transformed_for_diff(new_blob, old_blob)
- log_event(LOG_IPYNBDIFF_GENERATED)
- end
- end
- rescue Timeout::Error => e
- rendered_timeout.increment(source: execution_source)
- log_event(LOG_IPYNBDIFF_TIMEOUT, e)
- rescue IpynbDiff::InvalidNotebookError, IpynbDiff::InvalidTokenError => e
- log_event(LOG_IPYNBDIFF_INVALID, e)
- end
-
- def transformed_diff(before, after)
- transformed_diff = IpynbDiff.diff(before, after,
- raise_if_invalid_nb: true,
- diffy_opts: { include_diff_info: true }).to_s(:text)
-
- strip_diff_frontmatter(transformed_diff)
- end
-
- def transformed_blob_language(blob)
- 'md' if transformed_for_diff?(blob)
- end
-
- def transformed_blob_data(blob)
- if transformed_for_diff?(blob)
- IpynbDiff.transform(blob.data, raise_errors: true, include_frontmatter: false)
- end
- end
-
- def strip_diff_frontmatter(diff_content)
- diff_content.scan(/.*\n/)[2..]&.join('') if diff_content.present?
- end
-
- def blobs_with_transformed_diffs
- @blobs_with_transformed_diffs ||= {}
- end
-
- def transformed_for_diff?(blob)
- blobs_with_transformed_diffs[blob]
- end
-
- def transformed_for_diff(*blobs)
- blobs.each do |b|
- blobs_with_transformed_diffs[b] = true if b
- end
- end
-
- def rendered_timeout
- @rendered_timeout ||= Gitlab::Metrics.counter(
- :ipynb_semantic_diff_timeouts_total,
- 'Counts the times notebook rendering timed out'
- )
- end
-
- def timeout_time
- Gitlab::Runtime.sidekiq? ? RENDERED_TIMEOUT_BACKGROUND : RENDERED_TIMEOUT_FOREGROUND
- end
-
- def execution_source
- Gitlab::Runtime.sidekiq? ? BACKGROUND_EXECUTION : FOREGROUND_EXECUTION
- end
-
- def log_event(message, error = nil)
- Gitlab::AppLogger.info({ message: message })
- Gitlab::ErrorTracking.track_exception(error) if error
- nil
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index d6ee21b93b6..8e039d32ef5 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -43,20 +43,12 @@ module Gitlab
# Ensure items are collected in the the batch
new_blob_lazy
old_blob_lazy
-
- if use_semantic_ipynb_diff? && !use_renderable_diff?
- diff.diff = Gitlab::Diff::CustomDiff.preprocess_before_diff(diff.new_path, old_blob_lazy, new_blob_lazy) || diff.diff
- end
end
def use_semantic_ipynb_diff?
strong_memoize(:_use_semantic_ipynb_diff) { Feature.enabled?(:ipynb_semantic_diff, repository.project) }
end
- def use_renderable_diff?
- strong_memoize(:_renderable_diff_enabled) { Feature.enabled?(:rendered_diffs_viewer, repository.project) }
- end
-
def has_renderable?
rendered&.has_renderable?
end
@@ -381,7 +373,7 @@ module Gitlab
end
def rendered
- return unless use_semantic_ipynb_diff? && use_renderable_diff? && ipynb? && modified_file? && !too_large?
+ return unless use_semantic_ipynb_diff? && ipynb? && modified_file? && !too_large?
strong_memoize(:rendered) { Rendered::Notebook::DiffFile.new(self) }
end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 316a0d2815a..75127098600 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -10,7 +10,7 @@ module Gitlab
attr_reader :marker_ranges
attr_writer :text, :rich_text
- attr_accessor :index, :old_pos, :new_pos, :line_code, :type
+ attr_accessor :index, :old_pos, :new_pos, :line_code, :type, :embedded_image
def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil)
@text = text
diff --git a/lib/gitlab/diff/rendered/notebook/diff_file.rb b/lib/gitlab/diff/rendered/notebook/diff_file.rb
index 1f064d8af50..0a5b2ec3890 100644
--- a/lib/gitlab/diff/rendered/notebook/diff_file.rb
+++ b/lib/gitlab/diff/rendered/notebook/diff_file.rb
@@ -3,11 +3,11 @@ module Gitlab
module Diff
module Rendered
module Notebook
- include Gitlab::Utils::StrongMemoize
-
class DiffFile < Gitlab::Diff::File
+ include Gitlab::Diff::Rendered::Notebook::DiffFileHelper
+ include Gitlab::Utils::StrongMemoize
+
RENDERED_TIMEOUT_BACKGROUND = 10.seconds
- RENDERED_TIMEOUT_FOREGROUND = 1.5.seconds
BACKGROUND_EXECUTION = 'background'
FOREGROUND_EXECUTION = 'foreground'
LOG_IPYNBDIFF_GENERATED = 'IPYNB_DIFF_GENERATED'
@@ -49,11 +49,14 @@ module Gitlab
end
def highlighted_diff_lines
- @highlighted_diff_lines ||= begin
- removal_line_maps, addition_line_maps = compute_end_start_map
- Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight.map do |line|
- mutate_line(line, addition_line_maps, removal_line_maps)
- end
+ strong_memoize(:highlighted_diff_lines) do
+ lines = Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
+ lines_in_source = lines_in_source_diff(
+ source_diff.highlighted_diff_lines, source_diff.deleted_file?, source_diff.new_file?
+ )
+
+ lines.zip(line_positions_at_source_diff(lines, transformed_blocks))
+ .map { |line, positions| mutate_line(line, positions, lines_in_source)}
end
end
@@ -66,10 +69,9 @@ module Gitlab
next
end
- Timeout.timeout(timeout_time) do
+ Gitlab::RenderTimeout.timeout(background: RENDERED_TIMEOUT_BACKGROUND) do
IpynbDiff.diff(source_diff.old_blob&.data, source_diff.new_blob&.data,
raise_if_invalid_nb: true,
- hide_images: true,
diffy_opts: { include_diff_info: true })&.tap do
log_event(LOG_IPYNBDIFF_GENERATED)
end
@@ -90,50 +92,8 @@ module Gitlab
diff
end
- def strip_diff_frontmatter(diff_content)
- diff_content.scan(/.*\n/)[2..]&.join('') if diff_content.present?
- end
-
- def transformed_line_to_source(transformed_line, transformed_blocks)
- transformed_blocks.empty? ? 0 : ( transformed_blocks[transformed_line - 1][:source_line] || -1 ) + 1
- end
-
- def mutate_line(line, addition_line_maps, removal_line_maps)
- line.new_pos = transformed_line_to_source(line.new_pos, notebook_diff.to.blocks)
- line.old_pos = transformed_line_to_source(line.old_pos, notebook_diff.from.blocks)
-
- line.old_pos = addition_line_maps[line.new_pos] if line.old_pos == 0 && line.new_pos != 0
- line.new_pos = removal_line_maps[line.old_pos] if line.new_pos == 0 && line.old_pos != 0
-
- # Lines that do not appear on the original diff should not be commentable
- line.type = "#{line.type || 'unchanged'}-nomappinginraw" unless addition_line_maps[line.new_pos] || removal_line_maps[line.old_pos]
-
- line.line_code = line_code(line)
- line
- end
-
- def compute_end_start_map
- # line_codes are used for assigning notes to diffs, and these depend on the line on the new version and the
- # line that would have been that one in the previous version. However, since we do a transformation on the
- # file, that map gets lost. To overcome this, we look at the original source lines and build two maps:
- # - For additions, we look at the latest line change for that line and pick the old line for that id
- # - For removals, we look at the first line in the old version, and pick the first line on the new version
- #
- #
- # The caveat here is that we can't have notes on lines that are not a translation of a line in the source
- # diff
- #
- # (gitlab/diff/file.rb:75)
-
- removals = {}
- additions = {}
-
- source_diff.highlighted_diff_lines.each do |line|
- removals[line.old_pos] = line.new_pos unless source_diff.new_file?
- additions[line.new_pos] = line.old_pos unless source_diff.deleted_file?
- end
-
- [removals, additions]
+ def transformed_blocks
+ { from: notebook_diff.from.blocks, to: notebook_diff.to.blocks }
end
def rendered_timeout
@@ -143,15 +103,26 @@ module Gitlab
)
end
- def timeout_time
- Gitlab::Runtime.sidekiq? ? RENDERED_TIMEOUT_BACKGROUND : RENDERED_TIMEOUT_FOREGROUND
- end
-
def log_event(message, error = nil)
Gitlab::AppLogger.info({ message: message })
Gitlab::ErrorTracking.log_exception(error) if error
nil
end
+
+ def mutate_line(line, mapped_positions, source_diff_lines)
+ line.old_pos, line.new_pos = mapped_positions
+
+ # Lines that do not appear on the original diff should not be commentable
+ unless source_diff_lines[:to].include?(line.new_pos) || source_diff_lines[:from].include?(line.old_pos)
+ line.type = "#{line.type || 'unchanged'}-nomappinginraw"
+ end
+
+ line.line_code = line_code(line)
+
+ line.rich_text = image_as_rich_text(line.text) || line.rich_text
+
+ line
+ end
end
end
end
diff --git a/lib/gitlab/diff/rendered/notebook/diff_file_helper.rb b/lib/gitlab/diff/rendered/notebook/diff_file_helper.rb
new file mode 100644
index 00000000000..2e1b5ea301d
--- /dev/null
+++ b/lib/gitlab/diff/rendered/notebook/diff_file_helper.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+module Gitlab
+ module Diff
+ module Rendered
+ module Notebook
+ module DiffFileHelper
+ require 'set'
+
+ EMBEDDED_IMAGE_PATTERN = ' ![](data:image'
+
+ def strip_diff_frontmatter(diff_content)
+ diff_content.scan(/.*\n/)[2..]&.join('') if diff_content.present?
+ end
+
+ # line_positions_at_source_diff: given the transformed lines,
+ # what are the correct values for old_pos and new_pos?
+ #
+ # Example:
+ #
+ # Original
+ # from | to
+ # A | A
+ # B | D
+ # C | E
+ # F | F
+ #
+ # Original Diff
+ # A A
+ # - B
+ # - C
+ # + D
+ # + E
+ # F F
+ #
+ # Transformed
+ # from | to
+ # A | A
+ # C | D
+ # B | J
+ # L | E
+ # K | K
+ # F | F
+ #
+ # Transformed diff | transf old, new | OG old_pos, new_pos |
+ # A A | 1, 1 | 1, 1 |
+ # -C | 2, 2 | 3, 2 |
+ # -B | 3, 2 | 2, 2 |
+ # -L | 4, 2 | 0, 0 |
+ # + D | 5, 2 | 4, 2 |
+ # + J | 5, 3 | 0, 0 |
+ # + E | 5, 4 | 4, 3 |
+ # K K | 5, 5 | 0, 0 |
+ # F F | 6, 6 | 4, 4 |
+ def line_positions_at_source_diff(lines, blocks)
+ last_mapped_old_pos = 0
+ last_mapped_new_pos = 0
+
+ lines.reverse_each.map do |line|
+ old_pos = source_line_from_block(line.old_pos, blocks[:from])
+ new_pos = source_line_from_block(line.new_pos, blocks[:to])
+
+ old_has_no_mapping = old_pos == 0
+ new_has_no_mapping = new_pos == 0
+
+ next [0, 0] if old_has_no_mapping && (new_has_no_mapping || line.type == 'old')
+ next [0, 0] if new_has_no_mapping && line.type == 'new'
+
+ new_pos = last_mapped_new_pos if new_has_no_mapping && line.type == 'old'
+ old_pos = last_mapped_old_pos if old_has_no_mapping && line.type == 'new'
+
+ last_mapped_old_pos = old_pos
+ last_mapped_new_pos = new_pos
+
+ [old_pos, new_pos]
+ end.reverse
+ end
+
+ def lines_in_source_diff(source_diff_lines, is_deleted_file, is_added_file)
+ {
+ from: is_added_file ? Set[] : source_diff_lines.map {|l| l.old_pos}.to_set,
+ to: is_deleted_file ? Set[] : source_diff_lines.map {|l| l.new_pos}.to_set
+ }
+ end
+
+ def source_line_from_block(transformed_line, transformed_blocks)
+ # Blocks are the lines returned from the library and are a hash with {text:, source_line:}
+ # Blocks source_line are 0 indexed
+ return 0 if transformed_blocks.empty?
+
+ line_in_source = transformed_blocks[transformed_line - 1][:source_line]
+
+ return 0 unless line_in_source.present?
+
+ line_in_source + 1
+ end
+
+ def image_as_rich_text(line_text)
+ return unless line_text[1..].starts_with?(EMBEDDED_IMAGE_PATTERN)
+
+ image_body = line_text[1..].delete_prefix(EMBEDDED_IMAGE_PATTERN).delete_suffix(')')
+
+ "<img src=\"data:image#{CGI.escapeHTML(image_body)}\">".html_safe
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 4da112bc5a0..ba84be6e8ca 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -32,8 +32,8 @@ module Gitlab
def mail_metadata
{
mail_uid: mail.message_id,
- from_address: mail.from,
- to_address: mail.to,
+ from_address: from,
+ to_address: to,
mail_key: mail_key,
references: Array(mail.references),
delivered_to: delivered_to.map(&:value),
@@ -42,7 +42,7 @@ module Gitlab
# reduced down to what looks like an email in the received headers
received_recipients: recipients_from_received_headers,
meta: {
- client_id: "email/#{mail.from.first}",
+ client_id: "email/#{from.first}",
project: handler&.project&.full_path
}
}
@@ -63,6 +63,8 @@ module Gitlab
end
def build_mail
+ # See https://github.com/mikel/mail/blob/641060598f8f4be14d79bad8d703e9f2967e1cdb/spec/mail/message_spec.rb#L569
+ # for mail structure
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
Encoding::InvalidByteSequenceError => e
@@ -76,7 +78,7 @@ module Gitlab
end
def key_from_to_header
- mail.to.find do |address|
+ to.find do |address|
key = email_class.key_from_address(address)
break key if key
end
@@ -110,6 +112,14 @@ module Gitlab
end
end
+ def from
+ Array(mail.from)
+ end
+
+ def to
+ Array(mail.to)
+ end
+
def delivered_to
Array(mail[:delivered_to])
end
@@ -148,8 +158,6 @@ module Gitlab
end
def find_first_key_from_received_headers
- return unless ::Feature.enabled?(:use_received_header_for_incoming_emails)
-
recipients_from_received_headers.find do |email|
key = email_class.key_from_address(email)
break key if key
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index d39fa139abb..c2d645138d7 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -33,10 +33,10 @@ module Gitlab
l.strip.empty? || (!allow_only_quotes && l.start_with?('>'))
end
- encoded_body = body.force_encoding(encoding).encode("UTF-8")
+ encoded_body = force_utf8(body.force_encoding(encoding))
return encoded_body unless @append_reply
- [encoded_body, stripped_text.force_encoding(encoding).encode("UTF-8")]
+ [encoded_body, force_utf8(stripped_text.force_encoding(encoding))]
end
private
@@ -70,13 +70,29 @@ module Gitlab
return if object.nil?
if object.charset
- object.body.decoded.force_encoding(object.charset.gsub(/utf8/i, "UTF-8")).encode("UTF-8").to_s
+ # A part of a multi-part may have a different encoding. Its encoding
+ # is denoted in its header. For example:
+ #
+ # ```
+ # ------=_Part_2192_32400445.1115745999735
+ # Content-Type: text/plain; charset=ISO-8859-1
+ # Content-Transfer-Encoding: 7bit
+ #
+ # Plain email.
+ # ```
+ # So, we had to force its part to corresponding encoding before able
+ # to convert it to UTF-8
+ force_utf8(object.body.decoded.force_encoding(object.charset.gsub(/utf8/i, "UTF-8")))
else
object.body.to_s
end
rescue StandardError
nil
end
+
+ def force_utf8(str)
+ Gitlab::EncodingHelper.encode_utf8(str).to_s
+ end
end
end
end
diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb
index f9959d5677b..35c1a1e73cf 100644
--- a/lib/gitlab/error_tracking.rb
+++ b/lib/gitlab/error_tracking.rb
@@ -67,7 +67,7 @@ module Gitlab
# `extra`. Exceptions can use this mechanism to provide structured data
# to sentry in addition to their message and back-trace.
def track_and_raise_exception(exception, extra = {})
- process_exception(exception, sentry: true, extra: extra)
+ process_exception(exception, extra: extra)
raise exception
end
@@ -87,7 +87,7 @@ module Gitlab
# Provide an issue URL for follow up.
# as `issue_url: 'http://gitlab.com/gitlab-org/gitlab/issues/111'`
def track_and_raise_for_dev_exception(exception, extra = {})
- process_exception(exception, sentry: true, extra: extra)
+ process_exception(exception, extra: extra)
raise exception if should_raise_for_dev?
end
@@ -99,7 +99,7 @@ module Gitlab
# `extra`. Exceptions can use this mechanism to provide structured data
# to sentry in addition to their message and back-trace.
def track_exception(exception, extra = {})
- process_exception(exception, sentry: true, extra: extra)
+ process_exception(exception, extra: extra)
end
# This should be used when you only want to log the exception,
@@ -110,7 +110,7 @@ module Gitlab
# `extra`. Exceptions can use this mechanism to provide structured data
# to sentry in addition to their message and back-trace.
def log_exception(exception, extra = {})
- process_exception(exception, extra: extra)
+ process_exception(exception, extra: extra, trackers: [Logger])
end
private
@@ -136,25 +136,22 @@ module Gitlab
end
end
- def process_exception(exception, sentry: false, logging: true, extra:)
+ def process_exception(exception, extra:, trackers: default_trackers)
context_payload = Gitlab::ErrorTracking::ContextPayloadGenerator.generate(exception, extra)
- if sentry && Raven.configuration.server
- Raven.capture_exception(exception, **context_payload)
+ trackers.each do |tracker|
+ tracker.capture_exception(exception, **context_payload)
end
+ end
- # There is a possibility that this method is called before Sentry is
- # configured. Since Sentry 4.0, some methods of Sentry are forwarded to
- # to `nil`, hence we have to check the client as well.
- if sentry && ::Sentry.get_current_client && ::Sentry.configuration.dsn
- ::Sentry.capture_exception(exception, **context_payload)
- end
-
- if logging
- formatter = Gitlab::ErrorTracking::LogFormatter.new
- log_hash = formatter.generate_log(exception, context_payload)
-
- Gitlab::ErrorTracking::Logger.error(log_hash)
+ def default_trackers
+ [].tap do |destinations|
+ destinations << Raven if Raven.configuration.server
+ # There is a possibility that this method is called before Sentry is
+ # configured. Since Sentry 4.0, some methods of Sentry are forwarded to
+ # to `nil`, hence we have to check the client as well.
+ destinations << ::Sentry if ::Sentry.get_current_client && ::Sentry.configuration.dsn
+ destinations << Logger
end
end
diff --git a/lib/gitlab/error_tracking/logger.rb b/lib/gitlab/error_tracking/logger.rb
index 1b081f943aa..f6b0a54fe22 100644
--- a/lib/gitlab/error_tracking/logger.rb
+++ b/lib/gitlab/error_tracking/logger.rb
@@ -3,6 +3,13 @@
module Gitlab
module ErrorTracking
class Logger < ::Gitlab::JsonLogger
+ def self.capture_exception(exception, **context_payload)
+ formatter = Gitlab::ErrorTracking::LogFormatter.new
+ log_hash = formatter.generate_log(exception, context_payload)
+
+ self.error(log_hash)
+ end
+
def self.file_name_noext
'exceptions_json'
end
diff --git a/lib/gitlab/event_store/subscriber.rb b/lib/gitlab/event_store/subscriber.rb
index 9f569059736..da95d3cfcfa 100644
--- a/lib/gitlab/event_store/subscriber.rb
+++ b/lib/gitlab/event_store/subscriber.rb
@@ -23,6 +23,7 @@ module Gitlab
include ApplicationWorker
loggable_arguments 0, 1
+ idempotent!
end
def perform(event_type, data)
diff --git a/lib/gitlab/event_store/subscription.rb b/lib/gitlab/event_store/subscription.rb
index e5c92ab969f..01986355d2d 100644
--- a/lib/gitlab/event_store/subscription.rb
+++ b/lib/gitlab/event_store/subscription.rb
@@ -13,8 +13,7 @@ module Gitlab
def consume_event(event)
return unless condition_met?(event)
- worker.perform_async(event.class.name, event.data)
- # TODO: Log dispatching of events to subscriber
+ worker.perform_async(event.class.name, event.data.deep_stringify_keys)
# We rescue and track any exceptions here because we don't want to
# impact other subscribers if one is faulty.
diff --git a/lib/gitlab/fips.rb b/lib/gitlab/fips.rb
index 97813f13a91..b2c22182d4b 100644
--- a/lib/gitlab/fips.rb
+++ b/lib/gitlab/fips.rb
@@ -16,18 +16,14 @@ module Gitlab
Technology.new(:ed25519_sk, SSHData::PublicKey::SKED25519, [256], %w(sk-ssh-ed25519@openssh.com))
].freeze
+ OPENSSL_DIGESTS = %i(SHA1 SHA256 SHA384 SHA512).freeze
+
class << self
# Returns whether we should be running in FIPS mode or not
#
# @return [Boolean]
def enabled?
- # Attempt to auto-detect FIPS mode from OpenSSL
- return true if OpenSSL.fips_mode
-
- # Otherwise allow it to be set manually via the env vars
- return true if ENV["FIPS_MODE"] == "true"
-
- false
+ ::Labkit::FIPS.enabled?
end
end
end
diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb
index 841f9de8d4a..a5e6356eb17 100644
--- a/lib/gitlab/fogbugz_import/project_creator.rb
+++ b/lib/gitlab/fogbugz_import/project_creator.rb
@@ -3,22 +3,23 @@
module Gitlab
module FogbugzImport
class ProjectCreator
- attr_reader :repo, :fb_session, :namespace, :current_user, :user_map
+ attr_reader :repo, :name, :fb_session, :namespace, :current_user, :user_map
- def initialize(repo, fb_session, namespace, current_user, user_map = nil)
+ def initialize(repo, name, namespace, current_user, fb_session, user_map = nil)
@repo = repo
- @fb_session = fb_session
+ @name = name
@namespace = namespace
@current_user = current_user
+ @fb_session = fb_session
@user_map = user_map
end
def execute
::Projects::CreateService.new(
current_user,
- name: repo.safe_name,
- path: repo.path,
- namespace: namespace,
+ name: name,
+ path: name,
+ namespace_id: namespace.id,
creator: current_user,
visibility_level: Gitlab::VisibilityLevel::PRIVATE,
import_type: 'fogbugz',
diff --git a/lib/gitlab/form_builders/gitlab_ui_form_builder.rb b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb
index e8e87a864cc..9174ca165cd 100644
--- a/lib/gitlab/form_builders/gitlab_ui_form_builder.rb
+++ b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb
@@ -5,76 +5,50 @@ module Gitlab
class GitlabUiFormBuilder < ActionView::Helpers::FormBuilder
def gitlab_ui_checkbox_component(
method,
- label,
+ label = nil,
help_text: nil,
checkbox_options: {},
checked_value: '1',
unchecked_value: '0',
- label_options: {}
+ label_options: {},
+ &block
)
- @template.content_tag(
- :div,
- class: 'gl-form-checkbox custom-control custom-checkbox'
- ) do
- value = checkbox_options[:multiple] ? checked_value : nil
-
- @template.check_box(
- @object_name,
- method,
- format_options(checkbox_options, ['custom-control-input']),
- checked_value,
- unchecked_value
- ) + generic_label(method, label, label_options, help_text: help_text, value: value)
- end
+ Pajamas::CheckboxComponent.new(
+ form: self,
+ method: method,
+ label: label,
+ help_text: help_text,
+ checkbox_options: format_options(checkbox_options),
+ checked_value: checked_value,
+ unchecked_value: unchecked_value,
+ label_options: format_options(label_options)
+ ).render_in(@template, &block)
end
def gitlab_ui_radio_component(
method,
value,
- label,
+ label = nil,
help_text: nil,
radio_options: {},
- label_options: {}
+ label_options: {},
+ &block
)
- @template.content_tag(
- :div,
- class: 'gl-form-radio custom-control custom-radio'
- ) do
- @template.radio_button(
- @object_name,
- method,
- value,
- format_options(radio_options, ['custom-control-input'])
- ) + generic_label(method, label, label_options, help_text: help_text, value: value)
- end
+ Pajamas::RadioComponent.new(
+ form: self,
+ method: method,
+ value: value,
+ label: label,
+ help_text: help_text,
+ radio_options: format_options(radio_options),
+ label_options: format_options(label_options)
+ ).render_in(@template, &block)
end
private
- def generic_label(method, label, label_options, help_text: nil, value: nil)
- @template.label(
- @object_name, method, format_options(label_options.merge({ value: value }), ['custom-control-label'])
- ) do
- if help_text
- @template.content_tag(
- :span,
- label
- ) +
- @template.content_tag(
- :p,
- help_text,
- class: 'help-text'
- )
- else
- label
- end
- end
- end
-
- def format_options(options, classes)
- classes << options[:class]
-
- objectify_options(options.merge({ class: classes.flatten.compact }))
+ def format_options(options)
+ objectify_options(options)
end
end
end
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 5d0a638f97a..40dcac5f46f 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -31,30 +31,41 @@ module Gitlab
# http://gitlab.com/some/link/#1234, and code `puts #1234`'
#
class ReferenceRewriter
+ include Gitlab::Utils::StrongMemoize
+
RewriteError = Class.new(StandardError)
- def initialize(text, source_parent, current_user)
+ def initialize(text, text_html, source_parent, current_user)
@text = text
+
+ # If for some reason cached html is not present it gets rendered here
+ @text_html = text_html || original_html
+
@source_parent = source_parent
@current_user = current_user
- @original_html = markdown(text)
@pattern = Gitlab::ReferenceExtractor.references_pattern
end
def rewrite(target_parent)
return @text unless needs_rewrite?
- @text.gsub(@pattern) do |reference|
+ @text.gsub!(@pattern) do |reference|
unfold_reference(reference, Regexp.last_match, target_parent)
end
end
def needs_rewrite?
- @text =~ @pattern
+ strong_memoize(:needs_rewrite) { @text_html.include?('data-reference-type=') }
end
private
+ def original_html
+ strong_memoize(:original_html) do
+ markdown(@text)
+ end
+ end
+
def unfold_reference(reference, match, target_parent)
before = @text[0...match.begin(0)]
after = @text[match.end(0)..]
@@ -89,7 +100,7 @@ module Gitlab
end
def substitution_valid?(substituted)
- @original_html == markdown(substituted)
+ original_html == markdown(substituted)
end
def markdown(text)
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index 82ef7eed56a..b0bf68f4204 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -12,7 +12,9 @@ module Gitlab
#
#
class UploadsRewriter
- def initialize(text, source_project, _current_user)
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(text, _text_html, source_project, _current_user)
@text = text
@source_project = source_project
@pattern = FileUploader::MARKDOWN_PATTERN
@@ -21,7 +23,7 @@ module Gitlab
def rewrite(target_parent)
return @text unless needs_rewrite?
- @text.gsub(@pattern) do |markdown|
+ @text.gsub!(@pattern) do |markdown|
file = find_file($~[:secret], $~[:file])
# No file will be returned for a path traversal
next if file.nil?
@@ -43,15 +45,9 @@ module Gitlab
end
def needs_rewrite?
- files.any?
- end
-
- def files
- referenced_files = @text.scan(@pattern).map do
- find_file($~[:secret], $~[:file])
+ strong_memoize(:needs_rewrite) do
+ FileUploader::MARKDOWN_PATTERN.match?(@text)
end
-
- referenced_files.compact.select(&:exists?)
end
def was_embedded?(markdown)
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 2da30b88d55..505d0b8d728 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -87,7 +87,12 @@ module Gitlab
length = [sha1.length, sha2.length].min
return false if length < Gitlab::Git::Commit::MIN_SHA_LENGTH
- sha1[0, length] == sha2[0, length]
+ # Optimization: prevent unnecessary substring creation
+ if sha1.length == sha2.length
+ sha1 == sha2
+ else
+ sha1[0, length] == sha2[0, length]
+ end
end
end
end
diff --git a/lib/gitlab/git/cross_repo_comparer.rb b/lib/gitlab/git/cross_repo_comparer.rb
index 3958373f7cb..d42b2a3bd98 100644
--- a/lib/gitlab/git/cross_repo_comparer.rb
+++ b/lib/gitlab/git/cross_repo_comparer.rb
@@ -45,7 +45,7 @@ module Gitlab
# name that will be deleted once the method completes. This is a no-op if
# fetching the source branch fails
def with_commit_in_source_tmp(commit_id, &blk)
- tmp_ref = "refs/tmp/#{SecureRandom.hex}"
+ tmp_ref = "refs/#{::Repository::REF_TMP}/#{SecureRandom.hex}"
yield commit_id if source_repo.fetch_source_branch!(target_repo, commit_id, tmp_ref)
ensure
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index c473fe6973d..003cc87d65a 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -44,7 +44,7 @@ module Gitlab
else
# Only show what is new in the source branch
# compared to the target branch, not the other way
- # around. The linex below with merge_base is
+ # around. The line below with merge_base is
# equivalent to diff with three dots (git diff
# branch1...branch2) From the git documentation:
# "git diff A...B" is equivalent to "git diff
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index ab365069adf..df744bd60b4 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -366,9 +366,9 @@ module Gitlab
end
end
- def new_commits(newrevs, allow_quarantine: false)
+ def new_commits(newrevs)
wrapped_gitaly_errors do
- gitaly_commit_client.list_new_commits(Array.wrap(newrevs), allow_quarantine: allow_quarantine)
+ gitaly_commit_client.list_new_commits(Array.wrap(newrevs))
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index cba63b3c6c7..66fd7aaedea 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
-# Check a user's access to perform a git action. All public methods in this
-# class return an instance of `GitlabAccessStatus`
+# Checks a user's access to perform a git action.
+# All public methods in this class return an instance of `GitlabAccessStatus`
+
module Gitlab
class GitAccess
include Gitlab::Utils::StrongMemoize
@@ -99,7 +100,7 @@ module Gitlab
@logger ||= Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER)
end
- def guest_can_download_code?
+ def guest_can_download?
Guest.can?(download_ability, container)
end
@@ -107,10 +108,10 @@ module Gitlab
authentication_abilities.include?(:download_code) &&
deploy_key? &&
deploy_key.has_access_to?(container) &&
- (project? && project&.repository_access_level != ::Featurable::DISABLED)
+ (project? && repository_access_level != ::Featurable::DISABLED)
end
- def user_can_download_code?
+ def user_can_download?
authentication_abilities.include?(:download_code) &&
user_access.can_do_action?(download_ability)
end
@@ -125,10 +126,6 @@ module Gitlab
raise NotImplementedError
end
- def build_can_download_code?
- authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
- end
-
def request_from_ci_build?
return false unless protocol == 'http'
@@ -136,11 +133,36 @@ module Gitlab
end
def protocol_allowed?
- Gitlab::ProtocolAccess.allowed?(protocol)
+ Gitlab::ProtocolAccess.allowed?(protocol, project: project)
end
private
+ # when accessing via the CI_JOB_TOKEN
+ def build_can_download_code?
+ authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
+ end
+
+ def build_can_download?
+ build_can_download_code?
+ end
+
+ def deploy_token_can_download?
+ deploy_token?
+ end
+
+ # When overriding this method, be careful using super
+ # as deploy_token_can_download? and build_can_download?
+ # do not consider the download_ability in the inheriting class
+ # for deploy tokens and builds
+ def can_download?
+ deploy_key_can_download_code? ||
+ deploy_token_can_download? ||
+ build_can_download? ||
+ user_can_download? ||
+ guest_can_download?
+ end
+
def check_container!
# Strict nil check, to avoid any surprises with Object#present?
# which can delegate to #empty?
@@ -273,15 +295,9 @@ module Gitlab
end
def check_download_access!
- passed = deploy_key_can_download_code? ||
- deploy_token? ||
- user_can_download_code? ||
- build_can_download_code? ||
- guest_can_download_code?
-
- unless passed
- raise ForbiddenError, download_forbidden_message
- end
+ return if can_download?
+
+ raise ForbiddenError, download_forbidden_message
end
def download_forbidden_message
@@ -517,6 +533,10 @@ module Gitlab
# overriden in EE
def check_additional_conditions!
end
+
+ def repository_access_level
+ project&.repository_access_level
+ end
end
end
diff --git a/lib/gitlab/git_access_project.rb b/lib/gitlab/git_access_project.rb
index 7e9bab4a8e6..732e0e14257 100644
--- a/lib/gitlab/git_access_project.rb
+++ b/lib/gitlab/git_access_project.rb
@@ -74,3 +74,5 @@ module Gitlab
end
end
end
+
+Gitlab::GitAccessProject.prepend_mod_with('Gitlab::GitAccessProject')
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index 5ae17dbbb91..8c291dd56ba 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -90,13 +90,14 @@ module Gitlab
super
end
- override :check_download_access!
- def check_download_access!
- passed = guest_can_download_code? || user_can_download_code?
+ override :can_download?
+ def can_download?
+ guest_can_download? || user_can_download?
+ end
- unless passed
- raise ForbiddenError, error_message(:read_snippet)
- end
+ override :download_forbidden_message
+ def download_forbidden_message
+ error_message(:read_snippet)
end
override :check_change_access!
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index fdd7e8a8c4a..34378ac8f46 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -27,12 +27,21 @@ module Gitlab
:create_wiki
end
- override :check_download_access!
- def check_download_access!
- super
+ private
+
+ override :build_can_download?
+ def build_can_download?
+ super && user_access.can_do_action?(download_ability)
+ end
- raise ForbiddenError, download_forbidden_message if build_cannot_download?
- raise ForbiddenError, download_forbidden_message if deploy_token_cannot_download?
+ override :deploy_token_can_download?
+ def deploy_token_can_download?
+ super && deploy_token.can?(download_ability, container)
+ end
+
+ override :repository_access_level
+ def repository_access_level
+ project&.wiki_access_level
end
override :check_change_access!
@@ -53,17 +62,6 @@ module Gitlab
def not_found_message
error_message(:not_found)
end
-
- private
-
- # when accessing via the CI_JOB_TOKEN
- def build_cannot_download?
- build_can_download_code? && !user_access.can_do_action?(download_ability)
- end
-
- def deploy_token_cannot_download?
- deploy_token && !deploy_token.can?(download_ability, container)
- end
end
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 5e1f92ae835..9fb34f74c82 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -258,9 +258,9 @@ module Gitlab
end
# List all commits which are new in the repository. If commits have been pushed into the repo
- def list_new_commits(revisions, allow_quarantine: false)
+ def list_new_commits(revisions)
git_env = Gitlab::Git::HookEnv.all(@gitaly_repo.gl_repository)
- if allow_quarantine && git_env['GIT_OBJECT_DIRECTORY_RELATIVE'].present?
+ if git_env['GIT_OBJECT_DIRECTORY_RELATIVE'].present?
# If we have a quarantine environment, then we can optimize the check
# by doing a ListAllCommitsRequest. Instead of walking through
# references, we just walk through all quarantined objects, which is
@@ -278,32 +278,29 @@ module Gitlab
response = GitalyClient.call(@repository.storage, :commit_service, :list_all_commits, request, timeout: GitalyClient.medium_timeout)
quarantined_commits = consume_commits_response(response)
-
- if Feature.enabled?(:filter_quarantined_commits)
- quarantined_commit_ids = quarantined_commits.map(&:id)
-
- # While in general the quarantine directory would only contain objects
- # which are actually new, this is not guaranteed by Git. In fact,
- # git-push(1) may sometimes push objects which already exist in the
- # target repository. We do not want to return those from this method
- # though given that they're not actually new.
- #
- # To fix this edge-case we thus have to filter commits down to those
- # which don't yet exist. To do so, we must check for object existence
- # in the main repository, but the object directory of our repository
- # points into the object quarantine. This can be fixed by unsetting
- # it, which will cause us to use the normal repository as indicated by
- # its relative path again.
- main_repo = @gitaly_repo.dup
- main_repo.git_object_directory = ""
-
- # Check object existence of all quarantined commits' IDs.
- quarantined_commit_existence = object_existence_map(quarantined_commit_ids, gitaly_repo: main_repo)
-
- # And then we reject all quarantined commits which exist in the main
- # repository already.
- quarantined_commits.reject! { |c| quarantined_commit_existence[c.id] }
- end
+ quarantined_commit_ids = quarantined_commits.map(&:id)
+
+ # While in general the quarantine directory would only contain objects
+ # which are actually new, this is not guaranteed by Git. In fact,
+ # git-push(1) may sometimes push objects which already exist in the
+ # target repository. We do not want to return those from this method
+ # though given that they're not actually new.
+ #
+ # To fix this edge-case we thus have to filter commits down to those
+ # which don't yet exist. To do so, we must check for object existence
+ # in the main repository, but the object directory of our repository
+ # points into the object quarantine. This can be fixed by unsetting
+ # it, which will cause us to use the normal repository as indicated by
+ # its relative path again.
+ main_repo = @gitaly_repo.dup
+ main_repo.git_object_directory = ""
+
+ # Check object existence of all quarantined commits' IDs.
+ quarantined_commit_existence = object_existence_map(quarantined_commit_ids, gitaly_repo: main_repo)
+
+ # And then we reject all quarantined commits which exist in the main
+ # repository already.
+ quarantined_commits.reject! { |c| quarantined_commit_existence[c.id] }
quarantined_commits
else
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 4637bf2e3ff..d575c0f470d 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -101,6 +101,16 @@ module Gitlab
if pre_receive_error = response.pre_receive_error.presence
raise Gitlab::Git::PreReceiveError, pre_receive_error
end
+ rescue GRPC::BadStatus => e
+ detailed_error = decode_detailed_error(e)
+
+ case detailed_error&.error
+ when :custom_hook
+ raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
+ fallback_message: e.details)
+ else
+ raise
+ end
end
def user_merge_to_ref(user, source_sha:, branch:, target_ref:, message:, first_parent_ref:, allow_conflicts: false)
@@ -163,6 +173,9 @@ module Gitlab
access_check_error = detailed_error.access_check
# These messages were returned from internal/allowed API calls
raise Gitlab::Git::PreReceiveError.new(fallback_message: access_check_error.error_message)
+ when :custom_hook
+ raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
+ fallback_message: e.details)
when :reference_update
# We simply ignore any reference update errors which are typically an
# indicator of multiple RPC calls trying to update the same reference
@@ -299,10 +312,6 @@ module Gitlab
timeout: GitalyClient.long_timeout
)
- if response.git_error.presence
- raise Gitlab::Git::Repository::GitError, response.git_error
- end
-
response.squash_sha
rescue GRPC::BadStatus => e
detailed_error = decode_detailed_error(e)
@@ -464,6 +473,21 @@ module Gitlab
)
handle_cherry_pick_or_revert_response(response)
+ rescue GRPC::BadStatus => e
+ detailed_error = decode_detailed_error(e)
+
+ case detailed_error&.error
+ when :access_check
+ access_check_error = detailed_error.access_check
+ # These messages were returned from internal/allowed API calls
+ raise Gitlab::Git::PreReceiveError.new(fallback_message: access_check_error.error_message)
+ when :cherry_pick_conflict
+ raise Gitlab::Git::Repository::CreateTreeError, 'CONFLICT'
+ when :target_branch_diverged
+ raise Gitlab::Git::CommitError, 'branch diverged'
+ else
+ raise e
+ end
end
def handle_cherry_pick_or_revert_response(response)
@@ -526,6 +550,14 @@ module Gitlab
# Error Class might not be known to ruby yet
nil
end
+
+ def custom_hook_error_message(custom_hook_error)
+ # Custom hooks may return messages via either stdout or stderr which have a specific prefix. If
+ # that prefix is present we'll want to print the hook's output, otherwise we'll want to print the
+ # Gitaly error as a fallback.
+ custom_hook_output = custom_hook_error.stderr.presence || custom_hook_error.stdout
+ EncodingHelper.encode!(custom_hook_output)
+ end
end
end
end
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index 7f46615f17e..35fd4bd88a0 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -31,6 +31,7 @@ module Gitlab
if (issue_id = create_issue)
create_assignees(issue_id)
issuable_finder.cache_database_id(issue_id)
+ update_search_data(issue_id) if Feature.enabled?(:issues_full_text_search)
end
end
end
@@ -77,6 +78,13 @@ module Gitlab
ApplicationRecord.legacy_bulk_insert(IssueAssignee.table_name, assignees) # rubocop:disable Gitlab/BulkInsert
end
+
+ # Adds search data to database (if full_text_search feature is enabled)
+ #
+ # issue_id - The ID of the created issue.
+ def update_search_data(issue_id)
+ project.issues.find(issue_id)&.update_search_data!
+ end
end
end
end
diff --git a/lib/gitlab/github_import/importer/releases_importer.rb b/lib/gitlab/github_import/importer/releases_importer.rb
index 64ec0251e54..7241e1ef703 100644
--- a/lib/gitlab/github_import/importer/releases_importer.rb
+++ b/lib/gitlab/github_import/importer/releases_importer.rb
@@ -27,7 +27,7 @@ module Gitlab
def build(release)
existing_tags.add(release.tag_name)
- {
+ build_hash = {
name: release.name,
tag: release.tag_name,
description: description_for(release),
@@ -37,6 +37,12 @@ module Gitlab
released_at: release.published_at || Time.current,
project_id: project.id
}
+
+ if Feature.enabled?(:import_release_authors_from_github, project)
+ build_hash[:author_id] = fetch_author_id(release)
+ end
+
+ build_hash
end
def each_release
@@ -50,6 +56,18 @@ module Gitlab
def object_type
:release
end
+
+ private
+
+ def fetch_author_id(release)
+ author_id, _author_found = user_finder.author_id_for(release)
+
+ author_id
+ end
+
+ def user_finder
+ @user_finder ||= GithubImport::UserFinder.new(project, client)
+ end
end
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 98570c02e3d..5f1802e323c 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -54,8 +54,6 @@ module Gitlab
push_frontend_feature_flag(:usage_data_api, type: :ops)
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:new_header_search)
- push_frontend_feature_flag(:bootstrap_confirmation_modals)
- push_frontend_feature_flag(:sandboxed_mermaid)
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:gl_avatar_for_all_user_avatars)
push_frontend_feature_flag(:mr_attention_requests, current_user)
diff --git a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
index fe741a5bbe8..dde2bdd855e 100644
--- a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
+++ b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
@@ -6,21 +6,12 @@ module Gitlab
module GrapeLogging
module Loggers
class QueueDurationLogger < ::GrapeLogging::Loggers::Base
- attr_accessor :start_time
-
- def before
- @start_time = Time.now
- end
-
def parameters(request, _)
- proxy_start = request.env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
-
- return {} unless proxy_start && start_time
+ duration_s = request.env[Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY].presence
- # Time in milliseconds since gitlab-workhorse started the request
- duration = start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000
+ return {} unless duration_s
- { 'queue_duration_s': Gitlab::Utils.ms_to_round_sec(duration) }
+ { 'queue_duration_s': duration_s }
end
end
end
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index dc49c806398..884fc85c4ec 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -15,11 +15,7 @@ module Gitlab
# If the `#authorize` call is used on multiple classes, we add the
# permissions specified on a subclass, to the ones that were specified
# on its superclass.
- @required_permissions ||= if respond_to?(:superclass) && superclass.respond_to?(:required_permissions)
- superclass.required_permissions.dup
- else
- []
- end
+ @required_permissions ||= call_superclass_method(:required_permissions, []).dup
end
def authorize(*permissions)
@@ -27,6 +23,8 @@ module Gitlab
end
def authorizes_object?
+ return true if call_superclass_method(:authorizes_object?, false)
+
defined?(@authorizes_object) ? @authorizes_object : false
end
@@ -37,6 +35,14 @@ module Gitlab
def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, msg
end
+
+ private
+
+ def call_superclass_method(method_name, or_else)
+ return or_else unless respond_to?(:superclass) && superclass.respond_to?(method_name)
+
+ superclass.send(method_name) # rubocop: disable GitlabSecurity/PublicSend
+ end
end
def find_object(*args)
diff --git a/lib/gitlab/graphql/board/issues_connection_extension.rb b/lib/gitlab/graphql/board/issues_connection_extension.rb
index 9dcd8c92592..b909cb021de 100644
--- a/lib/gitlab/graphql/board/issues_connection_extension.rb
+++ b/lib/gitlab/graphql/board/issues_connection_extension.rb
@@ -2,7 +2,7 @@
module Gitlab
module Graphql
module Board
- class IssuesConnectionExtension < GraphQL::Schema::Field::ConnectionExtension
+ class IssuesConnectionExtension < GraphQL::Schema::FieldExtension
def after_resolve(value:, object:, context:, **rest)
::Boards::Issues::ListService
.initialize_relative_positions(object.list.board, context[:current_user], value.nodes)
diff --git a/lib/gitlab/graphql/deprecation.rb b/lib/gitlab/graphql/deprecation.rb
index 3335e511714..d30751fe46e 100644
--- a/lib/gitlab/graphql/deprecation.rb
+++ b/lib/gitlab/graphql/deprecation.rb
@@ -3,9 +3,12 @@
module Gitlab
module Graphql
class Deprecation
+ REASON_RENAMED = :renamed
+ REASON_ALPHA = :alpha
+
REASONS = {
- renamed: 'This was renamed.',
- alpha: 'This feature is in Alpha, and can be removed or changed at any point.'
+ REASON_RENAMED => 'This was renamed.',
+ REASON_ALPHA => 'This feature is in Alpha. It can be changed or removed at any time.'
}.freeze
include ActiveModel::Validations
@@ -39,7 +42,7 @@ module Gitlab
def markdown(context: :inline)
parts = [
- "#{deprecated_in(format: :markdown)}.",
+ "#{changed_in_milestone(format: :markdown)}.",
reason_text,
replacement_markdown.then { |r| "Use: #{r}." if r }
].compact
@@ -77,7 +80,7 @@ module Gitlab
[
reason_text,
replacement && "Please use `#{replacement}`.",
- "#{deprecated_in}."
+ "#{changed_in_milestone}."
].compact.join(' ')
end
@@ -107,15 +110,24 @@ module Gitlab
end
def description_suffix
- " #{deprecated_in}: #{reason_text}"
+ " #{changed_in_milestone}: #{reason_text}"
end
- def deprecated_in(format: :plain)
+ # Returns 'Deprecated in <milestone>' for proper deprecations.
+ # Retruns 'Introduced in <milestone>' for :alpha deprecations.
+ # Formatted to markdown or plain format.
+ def changed_in_milestone(format: :plain)
+ verb = if reason == REASON_ALPHA
+ 'Introduced'
+ else
+ 'Deprecated'
+ end
+
case format
when :plain
- "Deprecated in #{milestone}"
+ "#{verb} in #{milestone}"
when :markdown
- "**Deprecated** in #{milestone}"
+ "**#{verb}** in #{milestone}"
end
end
end
diff --git a/lib/gitlab/graphql/generic_tracing.rb b/lib/gitlab/graphql/generic_tracing.rb
index 936b22d5afa..d3de9c714f4 100644
--- a/lib/gitlab/graphql/generic_tracing.rb
+++ b/lib/gitlab/graphql/generic_tracing.rb
@@ -23,6 +23,14 @@ module Gitlab
"#{type.name}.#{field.name}"
end
+ def platform_authorized_key(type)
+ "#{type.graphql_name}.authorized"
+ end
+
+ def platform_resolve_type_key(type)
+ "#{type.graphql_name}.resolve_type"
+ end
+
def platform_trace(platform_key, key, data, &block)
tags = { platform_key: platform_key, key: key }
start = Gitlab::Metrics::System.monotonic_time
diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb
index 805864cdd4c..41c3af33909 100644
--- a/lib/gitlab/graphql/loaders/batch_model_loader.rb
+++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb
@@ -4,20 +4,27 @@ module Gitlab
module Graphql
module Loaders
class BatchModelLoader
- attr_reader :model_class, :model_id
+ attr_reader :model_class, :model_id, :preloads
- def initialize(model_class, model_id)
+ def initialize(model_class, model_id, preloads = nil)
@model_class = model_class
@model_id = model_id
+ @preloads = preloads || []
end
# rubocop: disable CodeReuse/ActiveRecord
def find
- BatchLoader::GraphQL.for(model_id.to_i).batch(key: model_class) do |ids, loader, args|
+ BatchLoader::GraphQL.for([model_id.to_i, preloads]).batch(key: model_class) do |for_params, loader, args|
model = args[:key]
+ keys_by_id = for_params.group_by(&:first)
+ ids = for_params.map(&:first)
+ preloads = for_params.flat_map(&:second).uniq
results = model.where(id: ids)
+ results = results.preload(*preloads) unless preloads.empty?
- results.each { |record| loader.call(record.id, record) }
+ results.each do |record|
+ keys_by_id.fetch(record.id, []).each { |k| loader.call(k, record) }
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/graphql/markdown_field.rb b/lib/gitlab/graphql/markdown_field.rb
index 6188d860aba..43dddf4c4bc 100644
--- a/lib/gitlab/graphql/markdown_field.rb
+++ b/lib/gitlab/graphql/markdown_field.rb
@@ -22,8 +22,10 @@ module Gitlab
field name, GraphQL::Types::String, **kwargs
define_method resolver_method do
+ markdown_object = block_given? ? yield(object) : object
+
# We need to `dup` the context so the MarkdownHelper doesn't modify it
- ::MarkupHelper.markdown_field(object, method_name.to_sym, context.to_h.dup)
+ ::MarkupHelper.markdown_field(markdown_object, method_name.to_sym, context.to_h.dup)
end
end
end
diff --git a/lib/gitlab/graphql/present/field_extension.rb b/lib/gitlab/graphql/present/field_extension.rb
index 050a3a276ea..bc6d0c6fd35 100644
--- a/lib/gitlab/graphql/present/field_extension.rb
+++ b/lib/gitlab/graphql/present/field_extension.rb
@@ -21,6 +21,7 @@ module Gitlab
# TODO: remove this when resolve procs are removed from the
# graphql-ruby library, and all field instrumentation is removed.
# See: https://github.com/rmosolgo/graphql-ruby/issues/3385
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/363131
presented = field.owner.try(:present, object, attrs) || object
yield(presented, arguments)
end
diff --git a/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb
new file mode 100644
index 00000000000..9a7069249ec
--- /dev/null
+++ b/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module QueryAnalyzers
+ module AST
+ class LoggerAnalyzer < GraphQL::Analysis::AST::Analyzer
+ COMPLEXITY_ANALYZER = GraphQL::Analysis::AST::QueryComplexity
+ DEPTH_ANALYZER = GraphQL::Analysis::AST::QueryDepth
+ FIELD_USAGE_ANALYZER = GraphQL::Analysis::AST::FieldUsage
+ ALL_ANALYZERS = [COMPLEXITY_ANALYZER, DEPTH_ANALYZER, FIELD_USAGE_ANALYZER].freeze
+
+ def initialize(query)
+ super
+
+ @results = default_initial_values(query).merge({
+ time_started: Gitlab::Metrics::System.monotonic_time
+ })
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
+ @results = default_initial_values(query_or_multiplex)
+ end
+
+ def result
+ complexity, depth, field_usages =
+ GraphQL::Analysis::AST.analyze_query(@subject, ALL_ANALYZERS, multiplex_analyzers: [])
+
+ results[:depth] = depth
+ results[:complexity] = complexity
+ # This duration is not the execution time of the
+ # query but the execution time of the analyzer.
+ results[:duration_s] = duration(results[:time_started])
+ results[:used_fields] = field_usages[:used_fields]
+ results[:used_deprecated_fields] = field_usages[:used_deprecated_fields]
+
+ push_to_request_store(results)
+
+ # This gl_analysis is included in the tracer log
+ query.context[:gl_analysis] = results.except!(:time_started, :query)
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
+ end
+
+ private
+
+ attr_reader :results
+
+ def push_to_request_store(results)
+ query = @subject
+
+ # TODO: This RequestStore management is used to handle setting request wide metadata
+ # to improve preexisting logging. We should handle this either with ApplicationContext
+ # or in a separate tracer.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/343802
+
+ RequestStore.store[:graphql_logs] ||= []
+ RequestStore.store[:graphql_logs] << results.except(:time_started, :duration_s).merge({
+ variables: process_variables(query.provided_variables),
+ operation_name: query.operation_name
+ })
+ end
+
+ def process_variables(variables)
+ filtered_variables = filter_sensitive_variables(variables)
+ filtered_variables.try(:to_s) || filtered_variables
+ end
+
+ def filter_sensitive_variables(variables)
+ ActiveSupport::ParameterFilter
+ .new(::Rails.application.config.filter_parameters)
+ .filter(variables)
+ end
+
+ def duration(time_started)
+ Gitlab::Metrics::System.monotonic_time - time_started
+ end
+
+ def default_initial_values(query)
+ {
+ time_started: Gitlab::Metrics::System.monotonic_time,
+ duration_s: nil
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb b/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb
new file mode 100644
index 00000000000..4e90e4c912f
--- /dev/null
+++ b/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+# Recursive queries, with relatively low effort, can quickly spiral out of control exponentially
+# and may not be picked up by depth and complexity alone.
+module Gitlab
+ module Graphql
+ module QueryAnalyzers
+ module AST
+ class RecursionAnalyzer < GraphQL::Analysis::AST::Analyzer
+ IGNORED_FIELDS = %w(node edges nodes ofType).freeze
+ RECURSION_THRESHOLD = 2
+
+ def initialize(query)
+ super
+
+ @node_visits = {}
+ @recurring_fields = {}
+ end
+
+ def on_enter_field(node, _parent, visitor)
+ return if skip_node?(node, visitor)
+
+ node_name = node.name
+ node_visits[node_name] ||= 0
+ node_visits[node_name] += 1
+
+ times_encountered = @node_visits[node_name]
+ recurring_fields[node_name] = times_encountered if recursion_too_deep?(node_name, times_encountered)
+ end
+
+ # Visitors are all defined on the AST::Analyzer base class
+ # We override them for custom analyzers.
+ def on_leave_field(node, _parent, visitor)
+ return if skip_node?(node, visitor)
+
+ node_name = node.name
+ node_visits[node_name] ||= 0
+ node_visits[node_name] -= 1
+ end
+
+ def result
+ @recurring_fields = @recurring_fields.select { |k, v| recursion_too_deep?(k, v) }
+
+ if @recurring_fields.any?
+ GraphQL::AnalysisError.new(<<~MSG)
+ Recursive query - too many of fields '#{@recurring_fields}' detected
+ in single branch of the query")
+ MSG
+ end
+ end
+
+ private
+
+ attr_reader :node_visits, :recurring_fields
+
+ def recursion_too_deep?(node_name, times_encountered)
+ return if IGNORED_FIELDS.include?(node_name)
+
+ times_encountered > recursion_threshold
+ end
+
+ def skip_node?(node, visitor)
+ # We don't want to count skipped fields or fields
+ # inside fragment definitions
+ return false if visitor.skipping? || visitor.visiting_fragment_definition?
+
+ !node.is_a?(GraphQL::Language::Nodes::Field) || node.selections.empty?
+ end
+
+ # separated into a method for use in allow_high_graphql_recursion
+ def recursion_threshold
+ RECURSION_THRESHOLD
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
deleted file mode 100644
index 207324e73bd..00000000000
--- a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module QueryAnalyzers
- class LoggerAnalyzer
- COMPLEXITY_ANALYZER = GraphQL::Analysis::QueryComplexity.new { |query, complexity_value| complexity_value }
- DEPTH_ANALYZER = GraphQL::Analysis::QueryDepth.new { |query, depth_value| depth_value }
- FIELD_USAGE_ANALYZER = GraphQL::Analysis::FieldUsage.new { |query, used_fields, used_deprecated_fields| [used_fields, used_deprecated_fields] }
- ALL_ANALYZERS = [COMPLEXITY_ANALYZER, DEPTH_ANALYZER, FIELD_USAGE_ANALYZER].freeze
-
- def initial_value(query)
- {
- time_started: Gitlab::Metrics::System.monotonic_time,
- query: query
- }
- end
-
- def call(memo, *)
- memo
- end
-
- def final_value(memo)
- return if memo.nil?
-
- query = memo[:query]
- complexity, depth, field_usages = GraphQL::Analysis.analyze_query(query, ALL_ANALYZERS)
-
- memo[:depth] = depth
- memo[:complexity] = complexity
- # This duration is not the execution time of the
- # query but the execution time of the analyzer.
- memo[:duration_s] = duration(memo[:time_started])
- memo[:used_fields] = field_usages.first
- memo[:used_deprecated_fields] = field_usages.second
-
- push_to_request_store(memo)
-
- # This gl_analysis is included in the tracer log
- query.context[:gl_analysis] = memo.except!(:time_started, :query)
- rescue StandardError => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
- end
-
- private
-
- def push_to_request_store(memo)
- query = memo[:query]
-
- # TODO: This RequestStore management is used to handle setting request wide metadata
- # to improve preexisting logging. We should handle this either with ApplicationContext
- # or in a separate tracer.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/343802
-
- RequestStore.store[:graphql_logs] ||= []
- RequestStore.store[:graphql_logs] << memo.except(:time_started, :duration_s, :query).merge({
- variables: process_variables(query.provided_variables),
- operation_name: query.operation_name
- })
- end
-
- def process_variables(variables)
- filtered_variables = filter_sensitive_variables(variables)
-
- if filtered_variables.respond_to?(:to_s)
- filtered_variables.to_s
- else
- filtered_variables
- end
- end
-
- def filter_sensitive_variables(variables)
- ActiveSupport::ParameterFilter
- .new(::Rails.application.config.filter_parameters)
- .filter(variables)
- end
-
- def duration(time_started)
- Gitlab::Metrics::System.monotonic_time - time_started
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb b/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb
deleted file mode 100644
index 79a7104a2ff..00000000000
--- a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-# Recursive queries, with relatively low effort, can quickly spiral out of control exponentially
-# and may not be picked up by depth and complexity alone.
-module Gitlab
- module Graphql
- module QueryAnalyzers
- class RecursionAnalyzer
- IGNORED_FIELDS = %w(node edges nodes ofType).freeze
- RECURSION_THRESHOLD = 2
-
- def initial_value(query)
- {
- recurring_fields: {}
- }
- end
-
- def call(memo, visit_type, irep_node)
- return memo if skip_node?(irep_node)
-
- node_name = irep_node.ast_node.name
- times_encountered = memo[node_name] || 0
-
- if visit_type == :enter
- times_encountered += 1
- memo[:recurring_fields][node_name] = times_encountered if recursion_too_deep?(node_name, times_encountered)
- else
- times_encountered -= 1
- end
-
- memo[node_name] = times_encountered
- memo
- end
-
- def final_value(memo)
- recurring_fields = memo[:recurring_fields]
- recurring_fields = recurring_fields.select { |k, v| recursion_too_deep?(k, v) }
- if recurring_fields.any?
- GraphQL::AnalysisError.new("Recursive query - too many of fields '#{recurring_fields}' detected in single branch of the query")
- end
- end
-
- private
-
- def recursion_too_deep?(node_name, times_encountered)
- return if IGNORED_FIELDS.include?(node_name)
-
- times_encountered > recursion_threshold
- end
-
- def skip_node?(irep_node)
- ast_node = irep_node.ast_node
- !ast_node.is_a?(GraphQL::Language::Nodes::Field) || ast_node.selections.empty?
- end
-
- def recursion_threshold
- RECURSION_THRESHOLD
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/hash_digest/facade.rb b/lib/gitlab/hash_digest/facade.rb
new file mode 100644
index 00000000000..d8efef02893
--- /dev/null
+++ b/lib/gitlab/hash_digest/facade.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HashDigest
+ # Used for rolling out to use OpenSSL::Digest::SHA256
+ # for ActiveSupport::Digest
+ class Facade
+ class << self
+ def hexdigest(...)
+ hash_digest_class.hexdigest(...)
+ end
+
+ def hash_digest_class
+ if use_sha256?
+ ::OpenSSL::Digest::SHA256
+ else
+ ::Digest::MD5 # rubocop:disable Fips/MD5
+ end
+ end
+
+ def use_sha256?
+ return false unless Feature.feature_flags_available?
+
+ Feature.enabled?(:active_support_hash_digest_sha256)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 758a594036b..b71abe5c052 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -2,9 +2,6 @@
module Gitlab
class Highlight
- TIMEOUT_BACKGROUND = 30.seconds
- TIMEOUT_FOREGROUND = 1.5.seconds
-
def self.highlight(blob_name, blob_content, language: nil, plain: false)
new(blob_name, blob_content, language: language)
.highlight(blob_content, continue: false, plain: plain)
@@ -72,7 +69,7 @@ module Gitlab
def highlight_rich(text, continue: true)
tag = lexer.tag
tokens = lexer.lex(text, continue: continue)
- Timeout.timeout(timeout_time) { @formatter.format(tokens, **context, tag: tag).html_safe }
+ Gitlab::RenderTimeout.timeout { @formatter.format(tokens, **context, tag: tag).html_safe }
rescue Timeout::Error => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
highlight_plain(text)
@@ -80,10 +77,6 @@ module Gitlab
highlight_plain(text)
end
- def timeout_time
- Gitlab::Runtime.sidekiq? ? TIMEOUT_BACKGROUND : TIMEOUT_FOREGROUND
- end
-
def link_dependencies(text, highlighted_text)
Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
end
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
index 06ddd65d075..2b4bdbd48bd 100644
--- a/lib/gitlab/hook_data/merge_request_builder.rb
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -52,7 +52,7 @@ module Gitlab
source: merge_request.source_project.try(:hook_attrs),
target: merge_request.target_project.hook_attrs,
last_commit: merge_request.diff_head_commit&.hook_attrs,
- work_in_progress: merge_request.work_in_progress?,
+ work_in_progress: merge_request.draft?,
total_time_spent: merge_request.total_time_spent,
time_change: merge_request.time_change,
human_total_time_spent: merge_request.human_total_time_spent,
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index c8239c9e308..8d9f86d3232 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -43,11 +43,11 @@ module Gitlab
TRANSLATION_LEVELS = {
'bg' => 0,
'cs_CZ' => 0,
- 'da_DK' => 43,
+ 'da_DK' => 41,
'de' => 14,
'en' => 100,
'eo' => 0,
- 'es' => 39,
+ 'es' => 36,
'fil_PH' => 0,
'fr' => 10,
'gl_ES' => 0,
@@ -55,15 +55,15 @@ module Gitlab
'it' => 1,
'ja' => 33,
'ko' => 12,
- 'nb_NO' => 29,
+ 'nb_NO' => 27,
'nl_NL' => 0,
'pl_PL' => 4,
- 'pt_BR' => 50,
- 'ro_RO' => 58,
- 'ru' => 30,
- 'tr_TR' => 13,
- 'uk' => 47,
- 'zh_CN' => 96,
+ 'pt_BR' => 54,
+ 'ro_RO' => 79,
+ 'ru' => 29,
+ 'tr_TR' => 12,
+ 'uk' => 44,
+ 'zh_CN' => 94,
'zh_HK' => 2,
'zh_TW' => 2
}.freeze
diff --git a/lib/gitlab/import_export/lfs_saver.rb b/lib/gitlab/import_export/lfs_saver.rb
index 22a7a8dd7cd..6ad368a5d2f 100644
--- a/lib/gitlab/import_export/lfs_saver.rb
+++ b/lib/gitlab/import_export/lfs_saver.rb
@@ -56,7 +56,11 @@ module Gitlab
end
def copy_file_for_lfs_object(lfs_object)
- copy_files(lfs_object.file.path, destination_path_for_object(lfs_object))
+ file_path = lfs_object.file.path
+
+ return unless File.exist?(file_path)
+
+ copy_files(file_path, destination_path_for_object(lfs_object))
end
def append_lfs_json_for_batch(lfs_objects_batch)
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 1625c39595c..5a1787218f5 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -283,6 +283,7 @@ included_attributes:
- :analytics_access_level
- :security_and_compliance_access_level
- :container_registry_access_level
+ - :package_registry_access_level
prometheus_metrics:
- :created_at
- :updated_at
@@ -684,6 +685,7 @@ included_attributes:
- :operations_access_level
- :security_and_compliance_access_level
- :container_registry_access_level
+ - :package_registry_access_level
- :allow_merge_on_skipped_pipeline
- :auto_devops_deploy_strategy
- :auto_devops_enabled
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index c9f5005cede..5d3a6b0c6e1 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -11,17 +11,17 @@ module Gitlab
# We exclude `bare_repository` here as it has no import class associated
IMPORT_TABLE = [
- ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
- ImportSource.new('bitbucket', 'Bitbucket Cloud', Gitlab::BitbucketImport::Importer),
- ImportSource.new('bitbucket_server', 'Bitbucket Server', Gitlab::BitbucketServerImport::Importer),
- ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
- ImportSource.new('google_code', 'Google Code', nil),
- ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
- ImportSource.new('git', 'Repo by URL', nil),
- ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
- ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer),
- ImportSource.new('manifest', 'Manifest file', nil),
- ImportSource.new('phabricator', 'Phabricator', Gitlab::PhabricatorImport::Importer)
+ ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
+ ImportSource.new('bitbucket', 'Bitbucket Cloud', Gitlab::BitbucketImport::Importer),
+ ImportSource.new('bitbucket_server', 'Bitbucket Server', Gitlab::BitbucketServerImport::Importer),
+ ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
+ ImportSource.new('google_code', 'Google Code', nil),
+ ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
+ ImportSource.new('git', 'Repository by URL', nil),
+ ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
+ ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer),
+ ImportSource.new('manifest', 'Manifest file', nil),
+ ImportSource.new('phabricator', 'Phabricator', Gitlab::PhabricatorImport::Importer)
].freeze
class << self
diff --git a/lib/gitlab/inactive_projects_deletion_warning_tracker.rb b/lib/gitlab/inactive_projects_deletion_warning_tracker.rb
index f3f8e774b4b..3fdb34d42b7 100644
--- a/lib/gitlab/inactive_projects_deletion_warning_tracker.rb
+++ b/lib/gitlab/inactive_projects_deletion_warning_tracker.rb
@@ -2,6 +2,8 @@
module Gitlab
class InactiveProjectsDeletionWarningTracker
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :project_id
DELETION_TRACKING_REDIS_KEY = 'inactive_projects_deletion_warning_email_notified'
@@ -38,10 +40,33 @@ module Gitlab
end
end
+ def notification_date
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.hget(DELETION_TRACKING_REDIS_KEY, "project:#{project_id}")
+ end
+ end
+
+ def scheduled_deletion_date
+ if notification_date.present?
+ (notification_date.to_date + grace_period_after_notification).to_s
+ else
+ grace_period_after_notification.from_now.to_date.to_s
+ end
+ end
+
def reset
Gitlab::Redis::SharedState.with do |redis|
redis.hdel(DELETION_TRACKING_REDIS_KEY, "project:#{project_id}")
end
end
+
+ private
+
+ def grace_period_after_notification
+ strong_memoize(:grace_period_after_notification) do
+ (::Gitlab::CurrentSettings.inactive_projects_delete_after_months -
+ ::Gitlab::CurrentSettings.inactive_projects_send_warning_email_after_months).months
+ end
+ end
end
end
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index 379c27caeb7..b8d8deb3418 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -31,6 +31,7 @@ module Gitlab
instrument_thread_memory_allocations(payload)
instrument_load_balancing(payload)
instrument_pid(payload)
+ instrument_worker_id(payload)
instrument_uploads(payload)
instrument_rate_limiting_gates(payload)
end
@@ -106,6 +107,10 @@ module Gitlab
payload[:pid] = Process.pid
end
+ def instrument_worker_id(payload)
+ payload[:worker_id] = ::Prometheus::PidProvider.worker_id
+ end
+
def instrument_thread_memory_allocations(payload)
counters = ::Gitlab::Memory::Instrumentation.measure_thread_memory_allocations(
::Gitlab::RequestContext.instance.thread_memory_allocations)
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index fc5834613fd..4ddafbac4c6 100644
--- a/lib/gitlab/legacy_github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -63,6 +63,10 @@ module Gitlab
# Gitea doesn't have a Release API yet
# See https://github.com/go-gitea/gitea/issues/330
+ # On re-enabling care should be taken to include releases `author_id` field and enable corresponding tests.
+ # See:
+ # 1) https://gitlab.com/gitlab-org/gitlab/-/issues/343448#note_985979730
+ # 2) https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89694/diffs#dfc4a8141aa296465ea3c50b095a30292fb6ebc4_180_182
unless project.gitea_import?
import_releases
end
diff --git a/lib/gitlab/mailgun/webhook_processors/base.rb b/lib/gitlab/mailgun/webhook_processors/base.rb
new file mode 100644
index 00000000000..9402637a51d
--- /dev/null
+++ b/lib/gitlab/mailgun/webhook_processors/base.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Mailgun
+ module WebhookProcessors
+ class Base
+ def initialize(payload)
+ @payload = payload || {}
+ end
+
+ def execute
+ end
+
+ private
+
+ attr_reader :payload
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/mailgun/webhook_processors/failure_logger.rb b/lib/gitlab/mailgun/webhook_processors/failure_logger.rb
new file mode 100644
index 00000000000..a7a85bd1672
--- /dev/null
+++ b/lib/gitlab/mailgun/webhook_processors/failure_logger.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Mailgun
+ module WebhookProcessors
+ class FailureLogger < Base
+ def execute
+ log_failure if permanent_failure? || temporary_failure_over_threshold?
+ end
+
+ def permanent_failure?
+ payload['event'] == 'failed' && payload['severity'] == 'permanent'
+ end
+
+ def temporary_failure_over_threshold?
+ payload['event'] == 'failed' && payload['severity'] == 'temporary' &&
+ Gitlab::ApplicationRateLimiter.throttled?(:temporary_email_failure, scope: payload['recipient'])
+ end
+
+ private
+
+ def log_failure
+ Gitlab::ErrorTracking::Logger.error(
+ event: 'email_delivery_failure',
+ mailgun_event_id: payload['id'],
+ recipient: payload['recipient'],
+ failure_type: payload['severity'],
+ failure_reason: payload['reason'],
+ failure_code: payload.dig('delivery-status', 'code'),
+ failure_message: payload.dig('delivery-status', 'message')
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/mailgun/webhook_processors/member_invites.rb b/lib/gitlab/mailgun/webhook_processors/member_invites.rb
new file mode 100644
index 00000000000..f54c44381f0
--- /dev/null
+++ b/lib/gitlab/mailgun/webhook_processors/member_invites.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Mailgun
+ module WebhookProcessors
+ class MemberInvites < Base
+ ProcessWebhookServiceError = Class.new(StandardError)
+
+ def execute
+ return unless should_process?
+
+ @member = Member.find_by_invite_token(invite_token)
+ update_member_and_log if member
+ rescue ProcessWebhookServiceError => e
+ Gitlab::ErrorTracking.track_exception(e)
+ end
+
+ private
+
+ attr_reader :member
+
+ def should_process?
+ payload['event'] == 'failed' && payload['severity'] == 'permanent' &&
+ payload['tags']&.include?(::Members::Mailgun::INVITE_EMAIL_TAG)
+ end
+
+ def update_member_and_log
+ log_update_event if member.update(invite_email_success: false)
+ end
+
+ def log_update_event
+ Gitlab::AppLogger.info(
+ message: "UPDATED MEMBER INVITE_EMAIL_SUCCESS: member_id: #{member.id}",
+ event: 'updated_member_invite_email_success'
+ )
+ end
+
+ def invite_token
+ # may want to validate schema in some way using ::JSONSchemer.schema(SCHEMA_PATH).valid?(message) if this
+ # gets more complex
+ payload.dig('user-variables', ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY) ||
+ raise(ProcessWebhookServiceError, "Expected to receive #{::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY} " \
+ "in user-variables: #{payload}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb
index 283502d90c1..09ba95666de 100644
--- a/lib/gitlab/markdown_cache.rb
+++ b/lib/gitlab/markdown_cache.rb
@@ -11,7 +11,7 @@ module Gitlab
# this if the change to the renderer output is a new feature or a
# minor bug fix.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330313
- CACHE_COMMONMARK_VERSION = 30
+ CACHE_COMMONMARK_VERSION = 31
CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError)
diff --git a/lib/gitlab/memory/jemalloc.rb b/lib/gitlab/memory/jemalloc.rb
new file mode 100644
index 00000000000..454c54569de
--- /dev/null
+++ b/lib/gitlab/memory/jemalloc.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'fiddle'
+
+module Gitlab
+ module Memory
+ module Jemalloc
+ extend self
+
+ STATS_FORMATS = {
+ json: { options: 'J', extension: 'json' },
+ text: { options: '', extension: 'txt' }
+ }.freeze
+
+ STATS_DEFAULT_FORMAT = :json
+
+ # Return jemalloc stats as a string.
+ def stats(format: STATS_DEFAULT_FORMAT)
+ verify_format!(format)
+
+ with_malloc_stats_print do |stats_print|
+ StringIO.new.tap { |io| write_stats(stats_print, io, STATS_FORMATS[format]) }.string
+ end
+ end
+
+ # Write jemalloc stats to the given directory.
+ def dump_stats(path:, format: STATS_DEFAULT_FORMAT)
+ verify_format!(format)
+
+ with_malloc_stats_print do |stats_print|
+ format_settings = STATS_FORMATS[format]
+ File.open(File.join(path, file_name(format_settings[:extension])), 'wb') do |io|
+ write_stats(stats_print, io, format_settings)
+ end
+ end
+ end
+
+ private
+
+ def verify_format!(format)
+ raise "format must be one of #{STATS_FORMATS.keys}" unless STATS_FORMATS.key?(format)
+ end
+
+ def with_malloc_stats_print
+ fiddle_func = malloc_stats_print
+ return unless fiddle_func
+
+ yield fiddle_func
+ end
+
+ def malloc_stats_print
+ method = Fiddle::Handle.sym("malloc_stats_print")
+
+ Fiddle::Function.new(
+ method,
+ # C signature:
+ # void (write_cb_t *write_cb, void *cbopaque, const char *opts)
+ # arg1: callback function pointer (see below)
+ # arg2: pointer to cbopaque holding additional callback data; always NULL here
+ # arg3: options string, affects output format (text or JSON)
+ #
+ # Callback signature (write_cb_t):
+ # void (void *, const char *)
+ # arg1: pointer to cbopaque data (see above; unused)
+ # arg2: pointer to string buffer holding textual output
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
+ Fiddle::TYPE_VOID
+ )
+ rescue Fiddle::DLError
+ # This means the Fiddle::Handle to jemalloc was not open (jemalloc wasn't loaded)
+ # or already closed. Eiher way, return nil.
+ end
+
+ def write_stats(stats_print, io, format)
+ callback = Fiddle::Closure::BlockCaller.new(
+ Fiddle::TYPE_VOID, [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]) do |_, fragment|
+ io << fragment
+ end
+
+ stats_print.call(callback, nil, format[:options])
+ end
+
+ def file_name(extension)
+ "jemalloc_stats.#{$$}.#{Time.current.to_i}.#{extension}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/alerts_inserter.rb b/lib/gitlab/metrics/dashboard/stages/alerts_inserter.rb
deleted file mode 100644
index 38736158c3b..00000000000
--- a/lib/gitlab/metrics/dashboard/stages/alerts_inserter.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'set'
-
-module Gitlab
- module Metrics
- module Dashboard
- module Stages
- class AlertsInserter < BaseStage
- include ::Gitlab::Utils::StrongMemoize
-
- def transform!
- return if metrics_with_alerts.empty?
-
- for_metrics do |metric|
- next unless metrics_with_alerts.include?(metric[:metric_id])
-
- metric[:alert_path] = alert_path(metric[:metric_id], project, params[:environment])
- end
- end
-
- private
-
- def metrics_with_alerts
- strong_memoize(:metrics_with_alerts) do
- alerts = ::Projects::Prometheus::AlertsFinder
- .new(project: project, environment: params[:environment])
- .execute
-
- Set.new(alerts.map(&:prometheus_metric_id))
- end
- end
-
- def alert_path(metric_id, project, environment)
- ::Gitlab::Routing.url_helpers.project_prometheus_alert_path(project, metric_id, environment_id: environment.id, format: :json)
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/samplers/database_sampler.rb b/lib/gitlab/metrics/samplers/database_sampler.rb
index 965d85e20e5..86372973c82 100644
--- a/lib/gitlab/metrics/samplers/database_sampler.rb
+++ b/lib/gitlab/metrics/samplers/database_sampler.rb
@@ -72,7 +72,7 @@ module Gitlab
{
host: host.host,
port: host.port,
- class: load_balancer.configuration.primary_connection_specification_name,
+ class: load_balancer.configuration.connection_specification_name,
db_config_name: Gitlab::Database.db_config_name(host.connection)
}
end
diff --git a/lib/gitlab/metrics/sli.rb b/lib/gitlab/metrics/sli.rb
index fcd893b675f..2de19514354 100644
--- a/lib/gitlab/metrics/sli.rb
+++ b/lib/gitlab/metrics/sli.rb
@@ -68,10 +68,6 @@ module Gitlab
prometheus.counter(counter_name('total'), "Total number of measurements for #{name}")
end
- def counter_name(suffix)
- :"#{COUNTER_PREFIX}:#{name}_#{self.class.name.demodulize.underscore}:#{suffix}"
- end
-
def prometheus
Gitlab::Metrics
end
@@ -85,6 +81,10 @@ module Gitlab
private
+ def counter_name(suffix)
+ :"#{COUNTER_PREFIX}:#{name}_apdex:#{suffix}"
+ end
+
def numerator_counter
prometheus.counter(counter_name('success_total'), "Number of successful measurements for #{name}")
end
@@ -99,6 +99,10 @@ module Gitlab
private
+ def counter_name(suffix)
+ :"#{COUNTER_PREFIX}:#{name}:#{suffix}"
+ end
+
def numerator_counter
prometheus.counter(counter_name('error_total'), "Number of error measurements for #{name}")
end
diff --git a/lib/gitlab/middleware/compressed_json.rb b/lib/gitlab/middleware/compressed_json.rb
index ef6e0db5673..f66dfe44054 100644
--- a/lib/gitlab/middleware/compressed_json.rb
+++ b/lib/gitlab/middleware/compressed_json.rb
@@ -54,7 +54,8 @@ module Gitlab
end
def match_content_type?(env)
- env['CONTENT_TYPE'] == 'application/json' ||
+ env['CONTENT_TYPE'].nil? ||
+ env['CONTENT_TYPE'] == 'application/json' ||
env['CONTENT_TYPE'] == 'application/x-sentry-envelope'
end
diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb
index 693f1470d9d..9a850246221 100644
--- a/lib/gitlab/object_hierarchy.rb
+++ b/lib/gitlab/object_hierarchy.rb
@@ -92,6 +92,14 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
+ # Returns a relation that includes ID of the descendants_base set of objects
+ # and all their descendants IDs (recursively).
+ # rubocop: disable CodeReuse/ActiveRecord
+ def base_and_descendant_ids
+ read_only(base_and_descendant_ids_cte.apply_to(unscoped_model.select(objects_table[:id])))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
# Returns a relation that includes the base objects, their ancestors,
# and the descendants of the base objects.
#
@@ -214,6 +222,26 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
+ def base_and_descendant_ids_cte
+ cte = SQL::RecursiveCTE.new(:base_and_descendants)
+
+ base_query = descendants_base.except(:order).select(objects_table[:id])
+
+ cte << base_query
+
+ # Recursively get all the descendants of the base set.
+ descendants_query = unscoped_model
+ .select(objects_table[:id])
+ .from(from_tables(cte))
+ .where(descendant_conditions(cte))
+ .except(:order)
+
+ cte << descendants_query
+ cte
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def objects_table
model.arel_table
end
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
deleted file mode 100644
index ae5539c03b1..00000000000
--- a/lib/gitlab/pages_transfer.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-# To make a call happen in a new Sidekiq job, add `.async` before the call. For
-# instance:
-#
-# PagesTransfer.new.async.move_namespace(...)
-#
-module Gitlab
- class PagesTransfer < ProjectTransfer
- METHODS = %w[move_namespace move_project rename_project rename_namespace].freeze
-
- class Async
- METHODS.each do |meth|
- define_method meth do |*args|
- next unless Settings.pages.local_store.enabled
-
- PagesTransferWorker.perform_async(meth, args)
- end
- end
- end
-
- METHODS.each do |meth|
- define_method meth do |*args|
- next unless Settings.pages.local_store.enabled
-
- super(*args)
- end
- end
-
- def async
- @async ||= Async.new
- end
-
- def root_dir
- Gitlab.config.pages.path
- end
- end
-end
diff --git a/lib/gitlab/patch/database_config.rb b/lib/gitlab/patch/database_config.rb
index c5c73d50518..20d8f7be8fd 100644
--- a/lib/gitlab/patch/database_config.rb
+++ b/lib/gitlab/patch/database_config.rb
@@ -1,77 +1,15 @@
# frozen_string_literal: true
-# The purpose of this code is to transform legacy `database.yml`
-# into a `database.yml` containing `main:` as a name of a first database
-#
-# This should be removed once all places using legacy `database.yml`
-# are fixed. The likely moment to remove this check is the %14.0.
-#
-# This converts the following syntax:
-#
-# production:
-# adapter: postgresql
-# database: gitlabhq_production
-# username: git
-# password: "secure password"
-# host: localhost
-#
-# Into:
-#
-# production:
-# main:
-# adapter: postgresql
-# database: gitlabhq_production
-# username: git
-# password: "secure password"
-# host: localhost
-#
-
+# The purpose of this code is to set the migrations path
+# for the Geo tracking database.
module Gitlab
module Patch
module DatabaseConfig
extend ActiveSupport::Concern
- def load_database_yaml
- return super unless Gitlab.ee?
-
- super.deep_merge(load_geo_database_yaml)
- end
-
- # This method is taken from Rails to load a database YAML file without
- # evaluating ERB. This allows us to create the rake tasks for the Geo
- # tracking database without filling in the configuration values or
- # loading the environment. To be removed when we start configure Geo
- # tracking database in database.yml instead of custom database_geo.yml
- #
- # https://github.com/rails/rails/blob/v6.1.4/railties/lib/rails/application/configuration.rb#L255
- def load_geo_database_yaml
- path = Rails.root.join("config/database_geo.yml")
- return {} unless File.exist?(path)
-
- require "rails/application/dummy_erb_compiler"
-
- yaml = DummyERB.new(Pathname.new(path).read).result
- config = YAML.load(yaml) || {} # rubocop:disable Security/YAMLLoad
-
- config.to_h do |env, configs|
- # This check is taken from Rails where the transformation
- # of a flat database.yml is done into `primary:`
- # https://github.com/rails/rails/blob/v6.1.4/activerecord/lib/active_record/database_configurations.rb#L169
- if configs.is_a?(Hash) && !configs.all? { |_, v| v.is_a?(Hash) }
- configs = { "geo" => configs }
- end
-
- [env, configs]
- end
- end
-
def database_configuration
super.to_h do |env, configs|
if Gitlab.ee?
- if !configs.key?("geo") && File.exist?(Rails.root.join("config/database_geo.yml"))
- configs["geo"] = Rails.application.config_for(:database_geo).stringify_keys
- end
-
if configs.key?("geo")
migrations_paths = Array(configs["geo"]["migrations_paths"])
migrations_paths << "ee/db/geo/migrate" if migrations_paths.empty?
diff --git a/lib/gitlab/project_stats_refresh_conflicts_logger.rb b/lib/gitlab/project_stats_refresh_conflicts_logger.rb
new file mode 100644
index 00000000000..3e7eecce89c
--- /dev/null
+++ b/lib/gitlab/project_stats_refresh_conflicts_logger.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class ProjectStatsRefreshConflictsLogger # rubocop:disable Gitlab/NamespacedClass
+ def self.warn_artifact_deletion_during_stats_refresh(project_id:, method:)
+ payload = Gitlab::ApplicationContext.current.merge(
+ message: 'Deleted artifacts undergoing refresh',
+ method: method,
+ project_id: project_id
+ )
+
+ Gitlab::AppLogger.warn(payload)
+ end
+
+ def self.warn_request_rejected_during_stats_refresh(project_id)
+ payload = Gitlab::ApplicationContext.current.merge(
+ message: 'Rejected request due to project undergoing stats refresh',
+ project_id: project_id
+ )
+
+ Gitlab::AppLogger.warn(payload)
+ end
+
+ def self.warn_skipped_artifact_deletion_during_stats_refresh(project_ids:, method:)
+ payload = Gitlab::ApplicationContext.current.merge(
+ message: 'Skipped deleting artifacts undergoing refresh',
+ method: method,
+ project_ids: project_ids
+ )
+
+ Gitlab::AppLogger.warn(payload)
+ end
+ end
+end
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index e7a12edf763..0ab6055408f 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -42,6 +42,7 @@ module Gitlab
class << self
# TODO: Review child inheritance of this table (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430928221)
+ # rubocop:disable Metrics/AbcSize
def localized_templates_table
[
ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'),
@@ -53,6 +54,7 @@ module Gitlab
ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'),
ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby'), 'https://gitlab.com/pages/gatsby', 'illustrations/third-party-logos/gatsby.svg'),
ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'),
+ ProjectTemplate.new('pelican', 'Pages/Pelican', _('Everything you need to create a GitLab Pages site using Pelican'), 'https://gitlab.com/pages/pelican', 'illustrations/third-party-logos/pelican.svg'),
ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'),
ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML'), 'https://gitlab.com/pages/plain-html'),
ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'),
@@ -72,6 +74,7 @@ module Gitlab
ProjectTemplate.new('kotlin_native_linux', 'Kotlin Native Linux', _('A basic template for developing Linux programs using Kotlin Native'), 'https://gitlab.com/gitlab-org/project-templates/kotlin-native-linux')
].freeze
end
+ # rubocop:enable Metrics/AbcSize
def all
localized_templates_table
diff --git a/lib/gitlab/protocol_access.rb b/lib/gitlab/protocol_access.rb
index efeb1e07d49..5bcbf7b5cca 100644
--- a/lib/gitlab/protocol_access.rb
+++ b/lib/gitlab/protocol_access.rb
@@ -2,14 +2,42 @@
module Gitlab
module ProtocolAccess
- def self.allowed?(protocol)
- if protocol == 'web'
- true
- elsif Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+ class << self
+ def allowed?(protocol, project: nil)
+ # Web is always allowed
+ return true if protocol == "web"
+
+ # System settings
+ return false unless instance_allowed?(protocol)
+
+ # Group-level settings
+ return false unless namespace_allowed?(protocol, namespace: project&.root_namespace)
+
+ # Default to allowing all protocols
true
- else
+ end
+
+ private
+
+ def instance_allowed?(protocol)
+ # If admin hasn't configured this setting, default to true
+ return true if Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+
protocol == Gitlab::CurrentSettings.enabled_git_access_protocol
end
+
+ def namespace_allowed?(protocol, namespace: nil)
+ # If the namespace parameter was nil, we default to true here
+ return true if namespace.nil?
+
+ # Return immediately if all protocols are allowed
+ return true if namespace.enabled_git_access_protocol == "all"
+
+ # If the setting is somehow nil, such as in an unsaved state, we default to allow
+ return true if namespace.enabled_git_access_protocol.blank?
+
+ protocol == namespace.enabled_git_access_protocol
+ end
end
end
end
diff --git a/lib/gitlab/quick_actions/commit_actions.rb b/lib/gitlab/quick_actions/commit_actions.rb
index 49f5ddf24eb..661e768ffc4 100644
--- a/lib/gitlab/quick_actions/commit_actions.rb
+++ b/lib/gitlab/quick_actions/commit_actions.rb
@@ -8,7 +8,7 @@ module Gitlab
included do
# Commit only quick actions definitions
- desc _('Tag this commit.')
+ desc { _('Tag this commit.') }
explanation do |tag_name, message|
if message.present?
_("Tags this commit to %{tag_name} with \"%{message}\".") % { tag_name: tag_name, message: message }
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index 4bac0643a91..259d9e38d65 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -55,7 +55,7 @@ module Gitlab
@updates[:state_event] = 'reopen'
end
- desc _('Change title')
+ desc { _('Change title') }
explanation do |title_param|
_('Changes the title to "%{title_param}".') % { title_param: title_param }
end
@@ -72,7 +72,7 @@ module Gitlab
@updates[:title] = title_param
end
- desc _('Add label(s)')
+ desc { _('Add label(s)') }
explanation do |labels_param|
labels = find_label_references(labels_param)
@@ -91,7 +91,7 @@ module Gitlab
run_label_command(labels: find_labels(labels_param), command: :label, updates_key: :add_label_ids)
end
- desc _('Remove all or specific label(s)')
+ desc { _('Remove all or specific label(s)') }
explanation do |labels_param = nil|
label_references = labels_param.present? ? find_label_references(labels_param) : []
if label_references.any?
@@ -128,7 +128,7 @@ module Gitlab
@execution_message[:unlabel] = remove_label_message(label_references)
end
- desc _('Replace all label(s)')
+ desc { _('Replace all label(s)') }
explanation do |labels_param|
labels = find_label_references(labels_param)
"Replaces all labels with #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any?
@@ -144,9 +144,9 @@ module Gitlab
run_label_command(labels: find_labels(labels_param), command: :relabel, updates_key: :label_ids)
end
- desc _('Add a to do')
- explanation _('Adds a to do.')
- execution_message _('Added a to do.')
+ desc { _('Add a to do') }
+ explanation { _('Adds a to do.') }
+ execution_message { _('Added a to do.') }
types Issuable
condition do
quick_action_target.persisted? &&
@@ -156,9 +156,9 @@ module Gitlab
@updates[:todo_event] = 'add'
end
- desc _('Mark to do as done')
- explanation _('Marks to do as done.')
- execution_message _('Marked to do as done.')
+ desc { _('Mark to do as done') }
+ explanation { _('Marks to do as done.') }
+ execution_message { _('Marked to do as done.') }
types Issuable
condition do
quick_action_target.persisted? &&
@@ -168,7 +168,7 @@ module Gitlab
@updates[:todo_event] = 'done'
end
- desc _('Subscribe')
+ desc { _('Subscribe') }
explanation do
_('Subscribes to this %{quick_action_target}.') %
{ quick_action_target: quick_action_target.to_ability_name.humanize(capitalize: false) }
@@ -186,7 +186,7 @@ module Gitlab
@updates[:subscription_event] = 'subscribe'
end
- desc _('Unsubscribe')
+ desc { _('Unsubscribe') }
explanation do
_('Unsubscribes from this %{quick_action_target}.') %
{ quick_action_target: quick_action_target.to_ability_name.humanize(capitalize: false) }
@@ -204,7 +204,7 @@ module Gitlab
@updates[:subscription_event] = 'unsubscribe'
end
- desc _('Toggle emoji award')
+ desc { _('Toggle emoji award') }
explanation do |name|
_("Toggles :%{name}: emoji award.") % { name: name } if name
end
@@ -226,22 +226,22 @@ module Gitlab
end
end
- desc _("Append the comment with %{shrug}") % { shrug: SHRUG }
+ desc { _("Append the comment with %{shrug}") % { shrug: SHRUG } }
params '<Comment>'
types Issuable
substitution :shrug do |comment|
"#{comment} #{SHRUG}"
end
- desc _("Append the comment with %{tableflip}") % { tableflip: TABLEFLIP }
+ desc { _("Append the comment with %{tableflip}") % { tableflip: TABLEFLIP } }
params '<Comment>'
types Issuable
substitution :tableflip do |comment|
"#{comment} #{TABLEFLIP}"
end
- desc _('Set severity')
- explanation _('Sets the severity')
+ desc { _('Set severity') }
+ explanation { _('Sets the severity') }
params '1 / S1 / Critical'
types Issue
condition do
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
index 2f89774a257..189627506f3 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -8,7 +8,7 @@ module Gitlab
included do
# Issue only quick actions definition
- desc _('Set due date')
+ desc { _('Set due date') }
explanation do |due_date|
_("Sets the due date to %{due_date}.") % { due_date: due_date.strftime('%b %-d, %Y') } if due_date
end
@@ -32,9 +32,9 @@ module Gitlab
end
end
- desc _('Remove due date')
- explanation _('Removes the due date.')
- execution_message _('Removed the due date.')
+ desc { _('Remove due date') }
+ explanation { _('Removes the due date.') }
+ execution_message { _('Removed the due date.') }
types Issue
condition do
quick_action_target.persisted? &&
@@ -46,7 +46,7 @@ module Gitlab
@updates[:due_date] = nil
end
- desc _('Move issue from one column of the board to another')
+ desc { _('Move issue from one column of the board to another') }
explanation do |target_list_name|
label = find_label_references(target_list_name).first
_("Moves issue to %{label} column in the board.") % { label: label } if label
@@ -78,7 +78,7 @@ module Gitlab
@execution_message[:board_move] = message
end
- desc _('Mark this issue as a duplicate of another issue')
+ desc { _('Mark this issue as a duplicate of another issue') }
explanation do |duplicate_reference|
_("Marks this issue as a duplicate of %{duplicate_reference}.") % { duplicate_reference: duplicate_reference }
end
@@ -102,7 +102,7 @@ module Gitlab
@execution_message[:duplicate] = message
end
- desc _('Clone this issue')
+ desc { _('Clone this issue') }
explanation do |project = quick_action_target.project.full_path|
_("Clones this issue, without comments, to %{project}.") % { project: project }
end
@@ -137,7 +137,7 @@ module Gitlab
@execution_message[:clone] = message
end
- desc _('Move this issue to another project.')
+ desc { _('Move this issue to another project.') }
explanation do |path_to_project|
_("Moves this issue to %{path_to_project}.") % { path_to_project: path_to_project }
end
@@ -161,7 +161,7 @@ module Gitlab
@execution_message[:move] = message
end
- desc _('Make issue confidential')
+ desc { _('Make issue confidential') }
explanation do
_('Makes this issue confidential.')
end
@@ -178,7 +178,7 @@ module Gitlab
@updates[:confidential] = true
end
- desc _('Create a merge request')
+ desc { _('Create a merge request') }
explanation do |branch_name = nil|
if branch_name
_("Creates branch '%{branch_name}' and a merge request to resolve this issue.") % { branch_name: branch_name }
@@ -205,8 +205,8 @@ module Gitlab
}
end
- desc _('Add Zoom meeting')
- explanation _('Adds a Zoom meeting.')
+ desc { _('Add Zoom meeting') }
+ explanation { _('Adds a Zoom meeting.') }
params '<Zoom URL>'
types Issue
condition do
@@ -222,9 +222,9 @@ module Gitlab
@updates.merge!(result.payload) if result.payload
end
- desc _('Remove Zoom meeting')
- explanation _('Remove Zoom meeting.')
- execution_message _('Zoom meeting removed')
+ desc { _('Remove Zoom meeting') }
+ explanation { _('Remove Zoom meeting.') }
+ execution_message { _('Zoom meeting removed') }
types Issue
condition do
@zoom_service = zoom_link_service
@@ -235,8 +235,8 @@ module Gitlab
@execution_message[:remove_zoom] = result.message
end
- desc _('Add email participant(s)')
- explanation _('Adds email participant(s).')
+ desc { _('Add email participant(s)') }
+ explanation { _('Adds email participant(s).') }
params 'email1@example.com email2@example.com (up to 6 emails)'
types Issue
condition do
@@ -264,8 +264,8 @@ module Gitlab
end
end
- desc _('Promote issue to incident')
- explanation _('Promotes issue to incident')
+ desc { _('Promote issue to incident') }
+ explanation { _('Promotes issue to incident') }
types Issue
condition do
quick_action_target.persisted? &&
@@ -285,8 +285,8 @@ module Gitlab
end
end
- desc _('Add customer relation contacts')
- explanation _('Add customer relation contact(s).')
+ desc { _('Add customer relation contacts') }
+ explanation { _('Add customer relation contact(s).') }
params '[contact:contact@example.com] [contact:person@example.org]'
types Issue
condition do
@@ -300,13 +300,13 @@ module Gitlab
@updates[:add_contacts] = contact_emails.split(' ')
end
- desc _('Remove customer relation contacts')
- explanation _('Remove customer relation contact(s).')
+ desc { _('Remove customer relation contacts') }
+ explanation { _('Remove customer relation contact(s).') }
params '[contact:contact@example.com] [contact:person@example.org]'
types Issue
condition do
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
- CustomerRelations::Contact.exists_for_group?(quick_action_target.project.root_ancestor)
+ quick_action_target.customer_relations_contacts.exists?
end
execution_message do
_('One or more contacts were successfully removed.')
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index 4a75fa0a571..a0faf8dd460 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -8,7 +8,7 @@ module Gitlab
included do
# Issue, MergeRequest: quick actions definitions
- desc _('Assign')
+ desc { _('Assign') }
explanation do |users|
_('Assigns %{assignee_users_sentence}.') % { assignee_users_sentence: assignee_users_sentence(users) }
end
@@ -81,7 +81,7 @@ module Gitlab
end
end
- desc _('Set milestone')
+ desc { _('Set milestone') }
explanation do |milestone|
_("Sets the milestone to %{milestone_reference}.") % { milestone_reference: milestone.to_reference } if milestone
end
@@ -103,7 +103,7 @@ module Gitlab
@updates[:milestone_id] = milestone.id if milestone
end
- desc _('Remove milestone')
+ desc { _('Remove milestone') }
explanation do
_("Removes %{milestone_reference} milestone.") % { milestone_reference: quick_action_target.milestone.to_reference(format: :name) }
end
@@ -121,7 +121,7 @@ module Gitlab
@updates[:milestone_id] = nil
end
- desc _('Copy labels and milestone from other issue or merge request in this project')
+ desc { _('Copy labels and milestone from other issue or merge request in this project') }
explanation do |source_issuable|
_("Copy labels and milestone from %{source_issuable_reference}.") % { source_issuable_reference: source_issuable.to_reference }
end
@@ -143,7 +143,7 @@ module Gitlab
end
end
- desc _('Set time estimate')
+ desc { _('Set time estimate') }
explanation do |time_estimate|
formatted_time_estimate = format_time_estimate(time_estimate)
_("Sets time estimate to %{time_estimate}.") % { time_estimate: formatted_time_estimate } if formatted_time_estimate
@@ -167,7 +167,7 @@ module Gitlab
end
end
- desc _('Add or subtract spent time')
+ desc { _('Add or subtract spent time') }
explanation do |time_spent, time_spent_date|
spend_time_message(time_spent, time_spent_date, false)
end
@@ -194,9 +194,9 @@ module Gitlab
end
end
- desc _('Remove time estimate')
- explanation _('Removes time estimate.')
- execution_message _('Removed time estimate.')
+ desc { _('Remove time estimate') }
+ explanation { _('Removes time estimate.') }
+ execution_message { _('Removed time estimate.') }
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
@@ -206,9 +206,9 @@ module Gitlab
@updates[:time_estimate] = 0
end
- desc _('Remove spent time')
- explanation _('Removes spent time.')
- execution_message _('Removed spent time.')
+ desc { _('Remove spent time') }
+ explanation { _('Removes spent time.') }
+ execution_message { _('Removed spent time.') }
condition do
quick_action_target.persisted? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
@@ -218,9 +218,9 @@ module Gitlab
@updates[:spend_time] = { duration: :reset, user_id: current_user.id }
end
- desc _("Lock the discussion")
- explanation _("Locks the discussion.")
- execution_message _("Locked the discussion.")
+ desc { _("Lock the discussion") }
+ explanation { _("Locks the discussion.") }
+ execution_message { _("Locked the discussion.") }
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
@@ -231,9 +231,9 @@ module Gitlab
@updates[:discussion_locked] = true
end
- desc _("Unlock the discussion")
- explanation _("Unlocks the discussion.")
- execution_message _("Unlocked the discussion.")
+ desc { _("Unlock the discussion") }
+ explanation { _("Unlocks the discussion.") }
+ execution_message { _("Unlocked the discussion.") }
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index abf55f56c73..167e7ad67a9 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -88,19 +88,19 @@ module Gitlab
@execution_message[:rebase] = _('Scheduled a rebase of branch %{branch}.') % { branch: branch }
end
- desc 'Toggle the Draft status'
+ desc { _('Toggle the Draft status') }
explanation do
noun = quick_action_target.to_ability_name.humanize(capitalize: false)
- if quick_action_target.work_in_progress?
- _("Unmarks this %{noun} as a draft.")
+ if quick_action_target.draft?
+ _("Marks this %{noun} as ready.")
else
_("Marks this %{noun} as a draft.")
end % { noun: noun }
end
execution_message do
noun = quick_action_target.to_ability_name.humanize(capitalize: false)
- if quick_action_target.work_in_progress?
- _("Unmarked this %{noun} as a draft.")
+ if quick_action_target.draft?
+ _("Marked this %{noun} as ready.")
else
_("Marked this %{noun} as a draft.")
end % { noun: noun }
@@ -108,15 +108,45 @@ module Gitlab
types MergeRequest
condition do
- quick_action_target.respond_to?(:work_in_progress?) &&
- # Allow it to mark as WIP on MR creation page _or_ through MR notes.
+ quick_action_target.respond_to?(:draft?) &&
+ # Allow it to mark as draft on MR creation page or through MR notes
+ #
(quick_action_target.new_record? || current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target))
end
command :draft do
- @updates[:wip_event] = quick_action_target.work_in_progress? ? 'unwip' : 'wip'
+ @updates[:wip_event] = quick_action_target.draft? ? 'ready' : 'draft'
end
- desc _('Set target branch')
+ desc { _('Set the Ready status') }
+ explanation do
+ noun = quick_action_target.to_ability_name.humanize(capitalize: false)
+ if quick_action_target.draft?
+ _("Marks this %{noun} as ready.")
+ else
+ _("No change to this %{noun}'s draft status.")
+ end % { noun: noun }
+ end
+ execution_message do
+ noun = quick_action_target.to_ability_name.humanize(capitalize: false)
+ if quick_action_target.draft?
+ _("Marked this %{noun} as ready.")
+ else
+ _("No change to this %{noun}'s draft status.")
+ end % { noun: noun }
+ end
+
+ types MergeRequest
+ condition do
+ # Allow it to mark as draft on MR creation page or through MR notes
+ #
+ quick_action_target.respond_to?(:draft?) &&
+ (quick_action_target.new_record? || current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target))
+ end
+ command :ready do
+ @updates[:wip_event] = 'ready' if quick_action_target.draft?
+ end
+
+ desc { _('Set target branch') }
explanation do |branch_name|
_('Sets target branch to %{branch_name}.') % { branch_name: branch_name }
end
@@ -137,8 +167,8 @@ module Gitlab
@updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)
end
- desc _('Submit a review')
- explanation _('Submit the current review.')
+ desc { _('Submit a review') }
+ explanation { _('Submit the current review.') }
types MergeRequest
condition do
quick_action_target.persisted?
@@ -154,8 +184,8 @@ module Gitlab
end
end
- desc _('Approve a merge request')
- explanation _('Approve the current merge request.')
+ desc { _('Approve a merge request') }
+ explanation { _('Approve the current merge request.') }
types MergeRequest
condition do
quick_action_target.persisted? && quick_action_target.can_be_approved_by?(current_user)
@@ -168,8 +198,8 @@ module Gitlab
@execution_message[:approve] = _('Approved the current merge request.')
end
- desc _('Unapprove a merge request')
- explanation _('Unapprove the current merge request.')
+ desc { _('Unapprove a merge request') }
+ explanation { _('Unapprove the current merge request.') }
types MergeRequest
condition do
quick_action_target.persisted? && quick_action_target.can_be_unapproved_by?(current_user)
diff --git a/lib/gitlab/quick_actions/relate_actions.rb b/lib/gitlab/quick_actions/relate_actions.rb
index 1de23523f01..4c8035f192e 100644
--- a/lib/gitlab/quick_actions/relate_actions.rb
+++ b/lib/gitlab/quick_actions/relate_actions.rb
@@ -7,7 +7,7 @@ module Gitlab
include ::Gitlab::QuickActions::Dsl
included do
- desc _('Mark this issue as related to another issue')
+ desc { _('Mark this issue as related to another issue') }
explanation do |related_reference|
_('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
diff --git a/lib/gitlab/rack_attack.rb b/lib/gitlab/rack_attack.rb
index b2664f87306..f5fb6b5af3d 100644
--- a/lib/gitlab/rack_attack.rb
+++ b/lib/gitlab/rack_attack.rb
@@ -12,9 +12,9 @@ module Gitlab
rack_attack::Request.include(Gitlab::RackAttack::Request)
# This is Rack::Attack::DEFAULT_THROTTLED_RESPONSE, modified to allow a custom response
- rack_attack.throttled_response = lambda do |env|
+ rack_attack.throttled_responder = lambda do |request|
throttled_headers = Gitlab::RackAttack.throttled_response_headers(
- env['rack.attack.matched'], env['rack.attack.match_data']
+ request.env['rack.attack.matched'], request.env['rack.attack.match_data']
)
[429, { 'Content-Type' => 'text/plain' }.merge(throttled_headers), [Gitlab::Throttle.rate_limiting_response_text]]
end
diff --git a/lib/gitlab/redis/duplicate_jobs.rb b/lib/gitlab/redis/duplicate_jobs.rb
new file mode 100644
index 00000000000..beb3ba1abee
--- /dev/null
+++ b/lib/gitlab/redis/duplicate_jobs.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ # Pseudo-store to transition `Gitlab::SidekiqMiddleware::DuplicateJobs` from
+ # using `Sidekiq.redis` to using the `SharedState` redis store.
+ class DuplicateJobs < ::Gitlab::Redis::Wrapper
+ class << self
+ def store_name
+ 'SharedState'
+ end
+
+ private
+
+ def redis
+ primary_store = ::Redis.new(Gitlab::Redis::SharedState.params)
+
+ # `Sidekiq.redis` is a namespaced redis connection. This means keys are actually being stored under
+ # "resque:gitlab:resque:gitlab:duplicate:". For backwards compatibility, we make the secondary store
+ # namespaced in the same way, but omit it from the primary so keys have proper format there.
+ secondary_store = ::Redis::Namespace.new(
+ Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE, redis: ::Redis.new(Gitlab::Redis::Queues.params)
+ )
+
+ MultiStore.new(primary_store, secondary_store, name.demodulize)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
new file mode 100644
index 00000000000..24c540eea47
--- /dev/null
+++ b/lib/gitlab/redis/multi_store.rb
@@ -0,0 +1,300 @@
+# 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 PipelinedDiffError < StandardError
+ def message
+ 'Pipelined command executed on both stores successfully but results differ between them.'
+ 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.'
+ FAILED_TO_RUN_PIPELINE = 'Failed to execute pipeline on 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
+ flushdb
+ rpush
+ ).freeze
+
+ PIPELINED_COMMANDS = %i(
+ pipelined
+ multi
+ ).freeze
+
+ # To transition between two Redis store, `primary_store` should be the target store,
+ # and `secondary_store` should be the current store. Transition is controlled with feature flags:
+ #
+ # - At the default state, all read and write operations are executed in the secondary instance.
+ # - Turning use_primary_and_secondary_stores_for_<instance_name> on: The store writes to both instances.
+ # The read commands are executed in primary, but fallback to secondary.
+ # Other commands are executed in the the default instance (Secondary).
+ # - Turning use_primary_store_as_default_for_<instance_name> on: The behavior is the same as above,
+ # but other commands are executed in the primary now.
+ # - Turning use_primary_and_secondary_stores_for_<instance_name> off: commands are executed in the primary store.
+ 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, **kwargs, &block|
+ if use_primary_and_secondary_stores?
+ write_command(name, *args, **kwargs, &block)
+ else
+ default_store.send(name, *args, **kwargs, &block)
+ end
+ end
+ end
+
+ PIPELINED_COMMANDS.each do |name|
+ define_method(name) do |*args, **kwargs, &block|
+ if use_primary_and_secondary_stores?
+ pipelined_both(name, *args, **kwargs, &block)
+ else
+ default_store.send(name, *args, **kwargs, &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_enabled?("use_primary_and_secondary_stores_for")
+ end
+
+ def use_primary_store_as_default?
+ feature_enabled?("use_primary_store_as_default_for")
+ end
+
+ def increment_pipelined_command_error_count(command_name)
+ @pipelined_command_error ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_pipelined_diff_error_total,
+ 'Redis MultiStore pipelined command diff between stores')
+ @pipelined_command_error.increment(command: command_name, instance_name: instance_name)
+ 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 log_error(exception, command_name, extra = {})
+ Gitlab::ErrorTracking.log_exception(
+ exception,
+ extra.merge(command_name: command_name, instance_name: instance_name))
+ end
+
+ private
+
+ # @return [Boolean]
+ def feature_enabled?(prefix)
+ feature_table_exists? &&
+ Feature.enabled?("#{prefix}_#{instance_name.underscore}") &&
+ !same_redis_store?
+ end
+
+ # @return [Boolean]
+ def feature_table_exists?
+ Feature::FlipperFeature.table_exists?
+ rescue StandardError
+ false
+ end
+
+ 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, **kwargs, &block)
+ if @instance
+ send_command(@instance, command_name, *args, **kwargs, &block)
+ else
+ write_both(command_name, *args, **kwargs, &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)
+ 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, **kwargs, &block)
+ begin
+ send_command(primary_store, command_name, *args, **kwargs, &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, **kwargs, &block)
+ end
+
+ # Run the entire pipeline on both stores. We assume that `&block` is idempotent.
+ def pipelined_both(command_name, *args, **kwargs, &block)
+ begin
+ result_primary = send_command(primary_store, command_name, *args, **kwargs, &block)
+ rescue StandardError => e
+ log_error(e, command_name, multi_store_error_message: FAILED_TO_RUN_PIPELINE)
+ end
+
+ result_secondary = send_command(secondary_store, command_name, *args, **kwargs, &block)
+
+ # Pipelined commands return an array with all results. If they differ,
+ # log an error
+ if result_primary != result_secondary
+ log_error(PipelinedDiffError.new, command_name)
+ increment_pipelined_command_error_count(command_name)
+ end
+
+ result_secondary
+ 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, **kwargs, &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, **kwargs) do |*params|
+ with_instance(redis_instance, *params, &block)
+ end
+ else
+ redis_instance.send(command_name, *args, **kwargs)
+ end
+ end
+ # rubocop:enable GitlabSecurity/PublicSend
+
+ def with_instance(instance, *params)
+ @instance = instance
+
+ yield(*params)
+ ensure
+ @instance = nil
+ end
+
+ def redis_store?(store)
+ store.is_a?(::Redis) || store.is_a?(::Redis::Namespace)
+ 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 redis_store?(primary_store)
+ raise ArgumentError, 'invalid secondary_store' unless redis_store?(secondary_store)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/sidekiq_status.rb b/lib/gitlab/redis/sidekiq_status.rb
new file mode 100644
index 00000000000..d4362c7cad8
--- /dev/null
+++ b/lib/gitlab/redis/sidekiq_status.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ # Pseudo-store to transition `Gitlab::SidekiqStatus` from
+ # using `Sidekiq.redis` to using the `SharedState` redis store.
+ class SidekiqStatus < ::Gitlab::Redis::Wrapper
+ class << self
+ def store_name
+ 'SharedState'
+ end
+
+ private
+
+ def redis
+ primary_store = ::Redis.new(Gitlab::Redis::SharedState.params)
+ secondary_store = ::Redis.new(Gitlab::Redis::Queues.params)
+
+ MultiStore.new(primary_store, secondary_store, name.demodulize)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 205106afddb..b0f4194b7a0 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,7 +5,7 @@ module Gitlab
module Packages
CONAN_RECIPE_FILES = %w[conanfile.py conanmanifest.txt conan_sources.tgz conan_export.tgz].freeze
CONAN_PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
-
+ PYPI_NORMALIZED_NAME_REGEX_STRING = '[-_.]+'
API_PATH_REGEX = %r{^/api/v\d+/(projects/[^/]+/|groups?/[^/]+/-/)?packages/[A-Za-z]+}.freeze
def conan_package_reference_regex
@@ -119,9 +119,9 @@ module Gitlab
# See official parser: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n205
@debian_version_regex ||= %r{
\A(?:
- (?:([0-9]{1,9}):)? (?# epoch)
- ([0-9][0-9a-z\.+~-]*) (?# version)
- (?:(-[0-0a-z\.+~]+))? (?# revision)
+ (?:([0-9]{1,9}):)? (?# epoch)
+ ([0-9][0-9a-z\.+~]*-?){1,15} (?# version-revision)
+ (?<!-)
)\z}xi.freeze
end
@@ -481,6 +481,11 @@ module Gitlab
"can contain only lowercase letters, digits, '_' and '-'. " \
"Must start with a letter, and cannot end with '-' or '_'"
end
+
+ # One or more `part`s, separated by separator
+ def sep_by_1(separator, part)
+ %r(#{part} (#{separator} #{part})*)x
+ end
end
end
diff --git a/lib/gitlab/render_timeout.rb b/lib/gitlab/render_timeout.rb
new file mode 100644
index 00000000000..b3c2a5b4c2a
--- /dev/null
+++ b/lib/gitlab/render_timeout.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RenderTimeout
+ BACKGROUND = 30.seconds
+ FOREGROUND = 1.5.seconds
+
+ def self.timeout(background: BACKGROUND, foreground: FOREGROUND, &block)
+ period = Gitlab::Runtime.sidekiq? ? background : foreground
+
+ Timeout.timeout(period, &block)
+ end
+ end
+end
diff --git a/lib/gitlab/service_desk_email.rb b/lib/gitlab/service_desk_email.rb
index 52da10eff3e..14f07140825 100644
--- a/lib/gitlab/service_desk_email.rb
+++ b/lib/gitlab/service_desk_email.rb
@@ -23,6 +23,12 @@ module Gitlab
config.address.sub(Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER, key)
end
+
+ def key_from_fallback_message_id(mail_id)
+ message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
+
+ mail_id[message_id_regexp, 1]
+ end
end
end
end
diff --git a/lib/gitlab/sidekiq_logging/logs_jobs.rb b/lib/gitlab/sidekiq_logging/logs_jobs.rb
index cfe91b9a266..de08de6632b 100644
--- a/lib/gitlab/sidekiq_logging/logs_jobs.rb
+++ b/lib/gitlab/sidekiq_logging/logs_jobs.rb
@@ -11,7 +11,11 @@ module Gitlab
def parse_job(job)
# Error information from the previous try is in the payload for
# displaying in the Sidekiq UI, but is very confusing in logs!
- job = job.except('error_backtrace', 'error_class', 'error_message')
+ job = job.except(
+ 'error_backtrace', 'error_class', 'error_message',
+ 'exception.backtrace', 'exception.class', 'exception.message', 'exception.sql'
+ )
+
job['class'] = job.delete('wrapped') if job['wrapped'].present?
job['job_size_bytes'] = Sidekiq.dump_json(job['args']).bytesize
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index a9bfcce2e0a..6eb39981ef4 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -79,9 +79,14 @@ module Gitlab
if job_exception
payload['message'] = "#{message}: fail: #{payload['duration_s']} sec"
payload['job_status'] = 'fail'
- payload['error_message'] = job_exception.message
- payload['error_class'] = job_exception.class.name
- add_exception_backtrace!(job_exception, payload)
+
+ Gitlab::ExceptionLogFormatter.format!(job_exception, payload)
+
+ # Deprecated fields for compatibility
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/364241
+ payload['error_class'] = payload['exception.class']
+ payload['error_message'] = payload['exception.message']
+ payload['error_backtrace'] = payload['exception.backtrace']
else
payload['message'] = "#{message}: done: #{payload['duration_s']} sec"
payload['job_status'] = 'done'
@@ -98,12 +103,6 @@ module Gitlab
payload['completed_at'] = Time.now.utc.to_f
end
- def add_exception_backtrace!(job_exception, payload)
- return if job_exception.backtrace.blank?
-
- payload['error_backtrace'] = Rails.backtrace_cleaner.clean(job_exception.backtrace)
- end
-
def elapsed(t0)
t1 = get_time
{ duration: t1[:now] - t0[:now] }
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index 601c8d1c3cf..7533770e254 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -63,7 +63,7 @@ module Gitlab
read_jid = nil
read_wal_locations = {}
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.multi do |multi|
multi.set(idempotency_key, jid, ex: expiry, nx: true)
read_wal_locations = check_existing_wal_locations!(multi, expiry)
@@ -81,7 +81,7 @@ module Gitlab
def update_latest_wal_location!
return unless job_wal_locations.present?
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.multi do |multi|
job_wal_locations.each do |connection_name, location|
multi.eval(
@@ -100,20 +100,19 @@ module Gitlab
strong_memoize(:latest_wal_locations) do
read_wal_locations = {}
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.multi do |multi|
job_wal_locations.keys.each do |connection_name|
read_wal_locations[connection_name] = multi.lindex(wal_location_key(connection_name), 0)
end
end
end
-
read_wal_locations.transform_values(&:value).compact
end
end
def delete!
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.multi do |multi|
multi.del(idempotency_key, deduplicated_flag_key)
delete_wal_locations!(multi)
@@ -140,7 +139,7 @@ module Gitlab
def set_deduplicated_flag!(expiry = duplicate_key_ttl)
return unless reschedulable?
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.set(deduplicated_flag_key, DEDUPLICATED_FLAG_VALUE, ex: expiry, nx: true)
end
end
@@ -148,7 +147,7 @@ module Gitlab
def should_reschedule?
return false unless reschedulable?
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.get(deduplicated_flag_key).present?
end
end
@@ -272,6 +271,18 @@ module Gitlab
def reschedulable?
!scheduled? && options[:if_deduplicated] == :reschedule_once
end
+
+ def with_redis
+ if Feature.enabled?(:use_primary_and_secondary_stores_for_duplicate_jobs) ||
+ Feature.enabled?(:use_primary_store_as_default_for_duplicate_jobs)
+ # TODO: Swap for Gitlab::Redis::SharedState after store transition
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/923
+ Gitlab::Redis::DuplicateJobs.with { |redis| yield redis }
+ else
+ # Keep the old behavior intact if neither feature flag is turned on
+ Sidekiq.redis { |redis| yield redis }
+ end
+ end
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed.rb
index 8c7e15364f8..347f4e61d19 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed.rb
@@ -10,6 +10,8 @@ module Gitlab
class UntilExecuted < DeduplicatesWhenScheduling
override :perform
def perform(job)
+ job_deleted = false
+
super
yield
@@ -17,7 +19,10 @@ module Gitlab
should_reschedule = duplicate_job.should_reschedule?
# Deleting before rescheduling to make sure we don't deduplicate again.
duplicate_job.delete!
+ job_deleted = true
duplicate_job.reschedule if should_reschedule
+ ensure
+ duplicate_job.delete! unless job_deleted
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/worker_context/client.rb b/lib/gitlab/sidekiq_middleware/worker_context/client.rb
index 7d3925e9dec..d9797e9c7c7 100644
--- a/lib/gitlab/sidekiq_middleware/worker_context/client.rb
+++ b/lib/gitlab/sidekiq_middleware/worker_context/client.rb
@@ -19,14 +19,21 @@ module Gitlab
# This should be inside the context for the arguments so
# that we don't override the feature category on the worker
# with the one from the caller.
- #
+
+ root_caller_id = Gitlab::ApplicationContext.current_context_attribute(:root_caller_id) ||
+ Gitlab::ApplicationContext.current_context_attribute(:caller_id)
+
+ context = {
+ root_caller_id: root_caller_id
+ }
+
# We do not want to set anything explicitly in the context
# when the feature category is 'not_owned'.
- if worker_class.feature_category_not_owned?
- yield
- else
- Gitlab::ApplicationContext.with_context(feature_category: worker_class.get_feature_category.to_s, &block)
+ unless worker_class.feature_category_not_owned?
+ context[:feature_category] = worker_class.get_feature_category.to_s
end
+
+ Gitlab::ApplicationContext.with_context(**context, &block)
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/worker_context/server.rb b/lib/gitlab/sidekiq_middleware/worker_context/server.rb
index d026f4918c6..7c2dd80c17a 100644
--- a/lib/gitlab/sidekiq_middleware/worker_context/server.rb
+++ b/lib/gitlab/sidekiq_middleware/worker_context/server.rb
@@ -12,7 +12,9 @@ module Gitlab
# This is not a worker we know about, perhaps from a gem
return yield unless worker_class.respond_to?(:get_worker_context)
- Gitlab::ApplicationContext.with_context(feature_category: worker_class.get_feature_category.to_s) do
+ feature_category = worker_class.get_feature_category.to_s
+
+ Gitlab::ApplicationContext.with_context(feature_category: feature_category) do
# Use the context defined on the class level as the more specific context
wrap_in_optional_context(worker_class.get_worker_context, &block)
end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index 66417b3697e..9d08d236720 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -36,7 +36,7 @@ module Gitlab
def self.set(jid, expire = DEFAULT_EXPIRATION)
return unless expire
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.set(key_for(jid), 1, ex: expire)
end
end
@@ -45,7 +45,7 @@ module Gitlab
#
# jid - The Sidekiq job ID to remove.
def self.unset(jid)
- Sidekiq.redis do |redis|
+ with_redis do |redis|
redis.del(key_for(jid))
end
end
@@ -94,8 +94,7 @@ module Gitlab
keys = job_ids.map { |jid| key_for(jid) }
- Sidekiq
- .redis { |redis| redis.mget(*keys) }
+ with_redis { |redis| redis.mget(*keys) }
.map { |result| !result.nil? }
end
@@ -118,5 +117,18 @@ module Gitlab
def self.key_for(jid)
STATUS_KEY % jid
end
+
+ def self.with_redis
+ if Feature.enabled?(:use_primary_and_secondary_stores_for_sidekiq_status) ||
+ Feature.enabled?(:use_primary_store_as_default_for_sidekiq_status)
+ # TODO: Swap for Gitlab::Redis::SharedState after store transition
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/923
+ Gitlab::Redis::SidekiqStatus.with { |redis| yield redis }
+ else
+ # Keep the old behavior intact if neither feature flag is turned on
+ Sidekiq.redis { |redis| yield redis }
+ end
+ end
+ private_class_method :with_redis
end
end
diff --git a/lib/gitlab/sql/cte.rb b/lib/gitlab/sql/cte.rb
index 8f37602aeaa..67f61e0db40 100644
--- a/lib/gitlab/sql/cte.rb
+++ b/lib/gitlab/sql/cte.rb
@@ -33,7 +33,7 @@ module Gitlab
# Returns the Arel relation for this CTE.
def to_arel
- sql = Arel::Nodes::SqlLiteral.new("(#{query.to_sql})")
+ sql = Arel::Nodes::SqlLiteral.new("(#{query_as_sql})")
Gitlab::Database::AsWithMaterialized.new(table, sql, materialized: @materialized)
end
@@ -54,6 +54,12 @@ module Gitlab
.with(to_arel)
.from(alias_to(relation.model.arel_table))
end
+
+ private
+
+ def query_as_sql
+ query.is_a?(String) ? query : query.to_sql
+ end
end
end
end
diff --git a/lib/gitlab/ssh/signature.rb b/lib/gitlab/ssh/signature.rb
new file mode 100644
index 00000000000..1a236e1a70c
--- /dev/null
+++ b/lib/gitlab/ssh/signature.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+# Signature verification with ed25519 keys
+# requires this gem to be loaded.
+require 'ed25519'
+
+module Gitlab
+ module Ssh
+ class Signature
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(signature_text, signed_text, committer_email)
+ @signature_text = signature_text
+ @signed_text = signed_text
+ @committer_email = committer_email
+ end
+
+ def verification_status
+ strong_memoize(:verification_status) do
+ next :unverified unless all_attributes_present?
+ next :unverified unless valid_signature_blob? && committer
+ next :unknown_key unless signed_by_key
+ next :other_user unless signed_by_key.user == committer
+
+ :verified
+ end
+ end
+
+ private
+
+ def all_attributes_present?
+ # Signing an empty string is valid, but signature_text and committer_email
+ # must be non-empty.
+ @signed_text && @signature_text.present? && @committer_email.present?
+ end
+
+ # Verifies the signature using the public key embedded in the blob.
+ # This proves that the signed_text was signed by the private key
+ # of the public key identified by `key_fingerprint`. Afterwards, we
+ # still need to check that the key belongs to the committer.
+ def valid_signature_blob?
+ return false unless signature
+
+ signature.verify(@signed_text)
+ end
+
+ def committer
+ # Lookup by email because users can push verified commits that were made
+ # by someone else. For example: Doing a rebase.
+ strong_memoize(:committer) { User.find_by_any_email(@committer_email, confirmed: true) }
+ end
+
+ def signature
+ strong_memoize(:signature) do
+ ::SSHData::Signature.parse_pem(@signature_text)
+ rescue SSHData::DecodeError
+ nil
+ end
+ end
+
+ def key_fingerprint
+ strong_memoize(:key_fingerprint) { signature&.public_key&.fingerprint }
+ end
+
+ def signed_by_key
+ strong_memoize(:signed_by_key) do
+ next unless key_fingerprint
+
+ Key.find_by_fingerprint_sha256(key_fingerprint)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb
index 78682a89655..e9c8e816f18 100644
--- a/lib/gitlab/ssh_public_key.rb
+++ b/lib/gitlab/ssh_public_key.rb
@@ -15,6 +15,29 @@ module Gitlab
Technology.new(:ed25519_sk, SSHData::PublicKey::SKED25519, [256], %w(sk-ssh-ed25519@openssh.com))
].freeze
+ BANNED_SSH_KEY_FINGERPRINTS = [
+ # https://github.com/rapid7/ssh-badkeys/tree/master/authorized
+ # banned ssh rsa keys
+ "SHA256:Z+q4XhSwWY7q0BIDVPR1v/S306FjGBsid7tLq/8kIxM",
+ "SHA256:uy5wXyEgbRCGsk23+J6f85om7G55Cu3UIPwC7oMZhNQ",
+ "SHA256:9prMbqhS4QteoFQ1ZRJDqSBLWoHXPyKB0iWR05Ghro4",
+ "SHA256:1M4RzhMyWuFS/86uPY/ce2prh/dVTHW7iD2RhpquOZA",
+
+ # banned ssh dsa keys
+ "SHA256:/JLp6z6uGE3BPcs70RQob6QOdEWQ6nDC0xY7ejPOCc0",
+ "SHA256:whDP3xjKBEettbDuecxtGsfWBST+78gb6McdB9P7jCU",
+ "SHA256:MEc4HfsOlMqJ3/9QMTmrKn5Xj/yfnMITMW8EwfUfTww",
+ "SHA256:aPoYT2nPIfhqv6BIlbCCpbDjirBxaDFOtPfZ2K20uWw",
+ "SHA256:VtjqZ5fiaeoZ3mXOYi49Lk9aO31iT4pahKFP9JPiQPc",
+
+ # other banned ssh keys
+ # https://github.com/BenBE/kompromat/commit/c8d9a05ea155a1ed609c617d4516f0ac978e8559
+ "SHA256:Z+q4XhSwWY7q0BIDVPR1v/S306FjGBsid7tLq/8kIxM",
+
+ # https://www.ctrlu.net/vuln/0006.html
+ "SHA256:2ewGtK7Dc8XpnfNKShczdc8HSgoEGpoX+MiJkfH2p5I"
+ ].to_set.freeze
+
def self.technologies
if Gitlab::FIPS.enabled?
Gitlab::FIPS::SSH_KEY_TECHNOLOGIES
@@ -115,6 +138,10 @@ module Gitlab
end
end
+ def banned?
+ BANNED_SSH_KEY_FINGERPRINTS.include?(fingerprint_sha256)
+ end
+
private
def technology
diff --git a/lib/gitlab/static_site_editor/config/file_config.rb b/lib/gitlab/static_site_editor/config/file_config.rb
deleted file mode 100644
index 4180f6ccf00..00000000000
--- a/lib/gitlab/static_site_editor/config/file_config.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- #
- # Base GitLab Static Site Editor Configuration facade
- #
- class FileConfig
- ConfigError = Class.new(StandardError)
-
- def initialize(yaml)
- content_hash = content_hash(yaml)
- @global = Entry::Global.new(content_hash)
- @global.compose!
- rescue Gitlab::Config::Loader::FormatError => e
- raise FileConfig::ConfigError, e.message
- end
-
- def valid?
- @global.valid?
- end
-
- def errors
- @global.errors
- end
-
- def to_hash_with_defaults
- # NOTE: The current approach of simply mapping all the descendents' keys and values ('config')
- # into a flat hash may need to be enhanced as we add more complex, non-scalar entries.
- @global.descendants.to_h { |descendant| [descendant.key, descendant.config] }
- end
-
- private
-
- def content_hash(yaml)
- Gitlab::Config::Loader::Yaml.new(yaml).load!
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/global.rb b/lib/gitlab/static_site_editor/config/file_config/entry/global.rb
deleted file mode 100644
index c295ccf1d11..00000000000
--- a/lib/gitlab/static_site_editor/config/file_config/entry/global.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- class FileConfig
- module Entry
- ##
- # This class represents a global entry - root Entry for entire
- # GitLab StaticSiteEditor Configuration file.
- #
- class Global < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Configurable
- include ::Gitlab::Config::Entry::Attributable
-
- ALLOWED_KEYS = %i[
- image_upload_path
- mounts
- static_site_generator
- ].freeze
-
- attributes ALLOWED_KEYS
-
- validations do
- validates :config, allowed_keys: ALLOWED_KEYS
- end
-
- entry :image_upload_path, Entry::ImageUploadPath,
- description: 'Configuration of the Static Site Editor image upload path.'
- entry :mounts, Entry::Mounts,
- description: 'Configuration of the Static Site Editor mounts.'
- entry :static_site_generator, Entry::StaticSiteGenerator,
- description: 'Configuration of the Static Site Editor static site generator.'
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb b/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb
deleted file mode 100644
index 6a2b9e10d33..00000000000
--- a/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- class FileConfig
- module Entry
- ##
- # Entry that represents the path to which images will be uploaded
- #
- class ImageUploadPath < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
-
- validations do
- validates :config, type: String
- end
-
- def self.default
- 'source/images'
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/mount.rb b/lib/gitlab/static_site_editor/config/file_config/entry/mount.rb
deleted file mode 100644
index b10956e17a5..00000000000
--- a/lib/gitlab/static_site_editor/config/file_config/entry/mount.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- class FileConfig
- module Entry
- ##
- # Entry that represents the mappings of mounted source directories to target paths
- #
- class Mount < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
- include ::Gitlab::Config::Entry::Attributable
-
- ALLOWED_KEYS = %i[source target].freeze
-
- attributes ALLOWED_KEYS
-
- validations do
- validates :config, allowed_keys: ALLOWED_KEYS
-
- validates :source, type: String, presence: true
- validates :target, type: String, presence: true, allow_blank: true
- end
-
- def self.default
- # NOTE: This is the default for middleman projects. Ideally, this would be determined
- # based on the defaults for whatever `static_site_generator` is configured.
- {
- source: 'source',
- target: ''
- }
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb b/lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb
deleted file mode 100644
index 10bd377e419..00000000000
--- a/lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- class FileConfig
- module Entry
- ##
- # Entry that represents the mappings of mounted source directories to target paths
- #
- class Mounts < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Configurable
- include ::Gitlab::Config::Entry::Validatable
-
- entry :mount, Entry::Mount, description: 'Configuration of a Static Site Editor mount.'
-
- validations do
- validates :config, type: Array, presence: true
- end
-
- def skip_config_hash_validation?
- true
- end
-
- def self.default
- [Entry::Mount.default]
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb b/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb
deleted file mode 100644
index 593c0951f93..00000000000
--- a/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- class FileConfig
- module Entry
- ##
- # Entry that represents the static site generator tool/framework.
- #
- class StaticSiteGenerator < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
-
- validations do
- validates :config, type: String, inclusion: { in: %w[middleman], message: "should be 'middleman'" }
- end
-
- def self.default
- 'middleman'
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/generated_config.rb b/lib/gitlab/static_site_editor/config/generated_config.rb
deleted file mode 100644
index 1555c3469a5..00000000000
--- a/lib/gitlab/static_site_editor/config/generated_config.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- class GeneratedConfig
- def initialize(repository, ref, path, return_url)
- @repository = repository
- @ref = ref
- @path = path
- @return_url = return_url
- end
-
- def data
- merge_requests_illustration_path = ActionController::Base.helpers.image_path('illustrations/merge_requests.svg')
- {
- branch: ref,
- path: path,
- commit_id: commit_id,
- project_id: project.id,
- project: project.path,
- namespace: project.namespace.full_path,
- return_url: sanitize_url(return_url),
- is_supported_content: supported_content?,
- base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path),
- merge_requests_illustration_path: merge_requests_illustration_path
- }
- end
-
- private
-
- attr_reader :repository, :ref, :path, :return_url
-
- delegate :project, to: :repository
-
- def supported_extensions
- %w[.md .md.erb].freeze
- end
-
- def commit_id
- repository.commit(ref)&.id
- end
-
- def supported_content?
- branch_supported? && extension_supported? && file_exists?
- end
-
- def branch_supported?
- ref.in?(%w[master main])
- end
-
- def extension_supported?
- supported_extensions.any? { |ext| path.end_with?(ext) }
- end
-
- def file_exists?
- commit_id.present? && !repository.blob_at(commit_id, path).nil?
- end
-
- def full_path
- "#{ref}/#{path}"
- end
-
- def sanitize_url(url)
- url if Gitlab::UrlSanitizer.valid_web?(url)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index 228da9ee370..ec2ec0d801f 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -23,8 +23,8 @@ module Gitlab
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(2, s_('NavigationTheme|Gray'), 'ui-gray', 'theme_gray', '#303030'),
+ Theme.new(3, s_('NavigationTheme|Light Gray'), 'ui-light-gray', 'theme_light_gray', '#666'),
Theme.new(11, s_('NavigationTheme|Dark Mode (alpha)'), 'gl-dark', nil, '#303030')
]
end
diff --git a/lib/gitlab/tracking/standard_context.rb b/lib/gitlab/tracking/standard_context.rb
index 542dc476526..05ddc7e26cc 100644
--- a/lib/gitlab/tracking/standard_context.rb
+++ b/lib/gitlab/tracking/standard_context.rb
@@ -7,6 +7,12 @@ module Gitlab
GITLAB_RAILS_SOURCE = 'gitlab-rails'
def initialize(namespace: nil, project: nil, user: nil, **extra)
+ if Feature.enabled?(:standard_context_type_check)
+ check_argument_type(:namespace, namespace, [Namespace])
+ check_argument_type(:project, project, [Project, Integer])
+ check_argument_type(:user, user, [User, DeployToken])
+ end
+
@namespace = namespace
@plan = namespace&.actual_plan_name
@project = project
@@ -54,6 +60,14 @@ module Gitlab
def project_id
project.is_a?(Integer) ? project : project&.id
end
+
+ def check_argument_type(argument_name, argument_value, allowed_classes)
+ return if argument_value.nil? || allowed_classes.any? { |allowed_class| argument_value.is_a?(allowed_class) }
+
+ exception = "Invalid argument type passed for #{argument_name}." \
+ " Should be one of #{allowed_classes.map(&:to_s)}"
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new(exception))
+ end
end
end
end
diff --git a/lib/gitlab/updated_notes_paginator.rb b/lib/gitlab/updated_notes_paginator.rb
deleted file mode 100644
index d5c01bde6b3..00000000000
--- a/lib/gitlab/updated_notes_paginator.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- # UpdatedNotesPaginator implements a rudimentary form of keyset pagination on
- # top of a notes relation that has been initialized with a `last_fetched_at`
- # value. This class will attempt to limit the number of notes returned, and
- # specify a new value for `last_fetched_at` that will pick up where the last
- # page of notes left off.
- class UpdatedNotesPaginator
- LIMIT = 50
- MICROSECOND = 1_000_000
-
- attr_reader :next_fetched_at, :notes
-
- def initialize(relation, last_fetched_at:)
- @last_fetched_at = last_fetched_at
- @now = Time.current
-
- notes, more = fetch_page(relation)
- if more
- init_middle_page(notes)
- else
- init_final_page(notes)
- end
- end
-
- def metadata
- { last_fetched_at: next_fetched_at_microseconds, more: more }
- end
-
- private
-
- attr_reader :last_fetched_at, :more, :now
-
- def next_fetched_at_microseconds
- (next_fetched_at.to_i * MICROSECOND) + next_fetched_at.usec
- end
-
- def fetch_page(relation)
- relation = relation.order_updated_asc.with_order_id_asc
- notes = relation.limit(LIMIT + 1).to_a
-
- return [notes, false] unless notes.size > LIMIT
-
- marker = notes.pop # Remove the marker note
-
- # Although very unlikely, it is possible that more notes with the same
- # updated_at may exist, e.g., if created in bulk. Add them all to the page
- # if this is detected, so pagination won't get stuck indefinitely
- if notes.last.updated_at == marker.updated_at
- notes += relation
- .with_updated_at(marker.updated_at)
- .id_not_in(notes.map(&:id))
- .to_a
- end
-
- [notes, true]
- end
-
- def init_middle_page(notes)
- @more = true
-
- # The fetch overlap can be ignored if we're in an intermediate page.
- @next_fetched_at = notes.last.updated_at + NotesFinder::FETCH_OVERLAP
- @notes = notes
- end
-
- def init_final_page(notes)
- @more = false
- @next_fetched_at = now
- @notes = notes
- end
- end
-end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric.rb
new file mode 100644
index 00000000000..109d2245635
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountImportedProjectsTotalMetric < DatabaseMetric
+ # Relation and operation are not used, but are included to satisfy expectations
+ # of other metric generation logic.
+ relation { Project }
+ operation :count
+
+ IMPORT_TYPES = %w(gitlab_project gitlab github bitbucket bitbucket_server gitea git manifest
+ gitlab_migration).freeze
+
+ def value
+ count(project_relation) + count(entity_relation)
+ end
+
+ def to_sql
+ project_relation_sql = Gitlab::Usage::Metrics::Query.for(:count, project_relation)
+ entity_relation_sql = Gitlab::Usage::Metrics::Query.for(:count, entity_relation)
+
+ "SELECT (#{project_relation_sql}) + (#{entity_relation_sql})"
+ end
+
+ private
+
+ def project_relation
+ Project.imported_from(IMPORT_TYPES).where(time_constraints)
+ end
+
+ def entity_relation
+ BulkImports::Entity.where(source_type: :project_entity).where(time_constraints)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb
index 34247f4f6dd..07dfba70d92 100644
--- a/lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb
@@ -11,6 +11,8 @@ module Gitlab
finish { Issue.maximum(:id) }
relation { Issue }
+
+ cache_start_and_finish_as :issue
end
end
end
diff --git a/lib/gitlab/usage/metrics/instrumentations/database_metric.rb b/lib/gitlab/usage/metrics/instrumentations/database_metric.rb
index a000b4509c6..3b09100f3ff 100644
--- a/lib/gitlab/usage/metrics/instrumentations/database_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/database_metric.rb
@@ -18,7 +18,7 @@ module Gitlab
UnimplementedOperationError = Class.new(StandardError) # rubocop:disable UsageData/InstrumentationSuperclass
class << self
- IMPLEMENTED_OPERATIONS = %i(count distinct_count estimate_batch_distinct_count).freeze
+ IMPLEMENTED_OPERATIONS = %i(count distinct_count estimate_batch_distinct_count sum average).freeze
private_constant :IMPLEMENTED_OPERATIONS
diff --git a/lib/gitlab/usage/metrics/instrumentations/issues_created_from_alerts_metric.rb b/lib/gitlab/usage/metrics/instrumentations/issues_created_from_alerts_metric.rb
new file mode 100644
index 00000000000..e430bc8eb71
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/issues_created_from_alerts_metric.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class IssuesCreatedFromAlertsMetric < NumbersMetric
+ ISSUES_FROM_ALERTS_METRICS = [
+ IssuesWithAlertManagementAlertsMetric,
+ IssuesWithPrometheusAlertEvents,
+ IssuesWithSelfManagedPrometheusAlertEvents
+ ].freeze
+
+ operation :add
+
+ data do |time_frame|
+ ISSUES_FROM_ALERTS_METRICS.map { |metric| metric.new(time_frame: time_frame).value }
+ end
+
+ # overwriting instrumentation to generate the appropriate sql query
+ def instrumentation
+ 'SELECT ' + ISSUES_FROM_ALERTS_METRICS.map do |metric|
+ "(#{metric.new(time_frame: time_frame).instrumentation})"
+ end.join(' + ')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/issues_with_alert_management_alerts_metric.rb b/lib/gitlab/usage/metrics/instrumentations/issues_with_alert_management_alerts_metric.rb
new file mode 100644
index 00000000000..62b91e50e07
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/issues_with_alert_management_alerts_metric.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class IssuesWithAlertManagementAlertsMetric < DatabaseMetric
+ # this metric is used in IssuesCreatedFromAlertsMetric
+ # do not report metric directly in service ping
+ available? { false }
+
+ operation :count
+
+ start { Issue.minimum(:id) }
+ finish { Issue.maximum(:id) }
+
+ relation { Issue.with_alert_management_alerts }
+
+ cache_start_and_finish_as :issue
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/issues_with_prometheus_alert_events.rb b/lib/gitlab/usage/metrics/instrumentations/issues_with_prometheus_alert_events.rb
new file mode 100644
index 00000000000..2befec65ac2
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/issues_with_prometheus_alert_events.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class IssuesWithPrometheusAlertEvents < DatabaseMetric
+ # this metric is used in IssuesCreatedFromAlertsMetric
+ # do not report metric directly in service ping
+ available? { false }
+
+ operation :count
+
+ start { Issue.minimum(:id) }
+ finish { Issue.maximum(:id) }
+
+ relation { Issue.with_prometheus_alert_events }
+
+ cache_start_and_finish_as :issue
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/issues_with_self_managed_prometheus_alert_events.rb b/lib/gitlab/usage/metrics/instrumentations/issues_with_self_managed_prometheus_alert_events.rb
new file mode 100644
index 00000000000..fdbaa65bc68
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/issues_with_self_managed_prometheus_alert_events.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class IssuesWithSelfManagedPrometheusAlertEvents < DatabaseMetric
+ # this metric is used in IssuesCreatedFromAlertsMetric
+ # do not report metric directly in service ping
+ available? { false }
+
+ operation :count
+
+ start { Issue.minimum(:id) }
+ finish { Issue.maximum(:id) }
+
+ relation { Issue.with_self_managed_prometheus_alert_events }
+
+ cache_start_and_finish_as :issue
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric.rb b/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric.rb
new file mode 100644
index 00000000000..6ca57864b8a
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class JiraImportsTotalImportedIssuesCountMetric < DatabaseMetric
+ operation :sum, column: :imported_issues_count
+
+ relation { JiraImportState.finished }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/numbers_metric.rb b/lib/gitlab/usage/metrics/instrumentations/numbers_metric.rb
new file mode 100644
index 00000000000..8504ee368fc
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/numbers_metric.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class NumbersMetric < BaseMetric
+ # Usage Example
+ #
+ # class BoardsCountMetric < NumbersMetric
+ # operation :add
+ #
+ # data do |time_frame|
+ # [
+ # CountIssuesMetric.new(time_frame: time_frame).value,
+ # CountBoardsMetric.new(time_frame: time_frame).value,
+ # ]
+ # end
+ # end
+
+ UnimplementedOperationError = Class.new(StandardError) # rubocop:disable UsageData/InstrumentationSuperclass
+
+ class << self
+ IMPLEMENTED_OPERATIONS = %i(add).freeze
+
+ private_constant :IMPLEMENTED_OPERATIONS
+
+ def data(&block)
+ return @metric_data&.call unless block_given?
+
+ @metric_data = block
+ end
+
+ def operation(symbol)
+ raise UnimplementedOperationError unless symbol.in?(IMPLEMENTED_OPERATIONS)
+
+ @metric_operation = symbol
+ end
+
+ attr_reader :metric_operation, :metric_data
+ end
+
+ def value
+ method(self.class.metric_operation).call(*data)
+ end
+
+ def suggested_name
+ Gitlab::Usage::Metrics::NameSuggestion.for(:alt)
+ end
+
+ private
+
+ def data
+ self.class.metric_data.call(time_frame)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/unique_active_users_metric.rb b/lib/gitlab/usage/metrics/instrumentations/unique_active_users_metric.rb
new file mode 100644
index 00000000000..9da30db05dd
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/unique_active_users_metric.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class UniqueActiveUsersMetric < DatabaseMetric
+ operation :count
+ relation { ::User.active }
+
+ metric_options do
+ {
+ batch_size: 10_000
+ }
+ end
+
+ def time_constraints
+ case time_frame
+ when '28d'
+ monthly_time_range_db_params(column: :last_activity_on)
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/name_suggestion.rb b/lib/gitlab/usage/metrics/name_suggestion.rb
index 0728af9e2ca..238a7a51a20 100644
--- a/lib/gitlab/usage/metrics/name_suggestion.rb
+++ b/lib/gitlab/usage/metrics/name_suggestion.rb
@@ -19,6 +19,8 @@ module Gitlab
name_suggestion(column: column, relation: relation, prefix: 'estimate_distinct_count')
when :sum
name_suggestion(column: column, relation: relation, prefix: 'sum')
+ when :average
+ name_suggestion(column: column, relation: relation, prefix: 'average')
when :redis
REDIS_EVENT_METRIC_NAME
when :alt
diff --git a/lib/gitlab/usage/metrics/query.rb b/lib/gitlab/usage/metrics/query.rb
index 851aa7a50e8..e071b422c16 100644
--- a/lib/gitlab/usage/metrics/query.rb
+++ b/lib/gitlab/usage/metrics/query.rb
@@ -13,6 +13,8 @@ module Gitlab
distinct_count(relation, column)
when :sum
sum(relation, column)
+ when :average
+ average(relation, column)
when :estimate_batch_distinct_count
estimate_batch_distinct_count(relation, column)
when :histogram
@@ -25,19 +27,23 @@ module Gitlab
private
def count(relation, column = nil)
- raw_sql(relation, column)
+ raw_count_sql(relation, column)
end
def distinct_count(relation, column = nil)
- raw_sql(relation, column, true)
+ raw_count_sql(relation, column, true)
end
def sum(relation, column)
- relation.select(relation.all.table[column].sum).to_sql
+ raw_sum_sql(relation, column)
+ end
+
+ def average(relation, column)
+ raw_average_sql(relation, column)
end
def estimate_batch_distinct_count(relation, column = nil)
- raw_sql(relation, column, true)
+ raw_count_sql(relation, column, true)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -62,15 +68,31 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def raw_sql(relation, column, distinct = false)
+ def raw_count_sql(relation, column, distinct = false)
column ||= relation.primary_key
- node = node_to_count(relation, column)
+ node = node_to_operate(relation, column)
relation.unscope(:order).select(node.count(distinct)).to_sql
end
# rubocop: enable CodeReuse/ActiveRecord
- def node_to_count(relation, column)
+ # rubocop: disable CodeReuse/ActiveRecord
+ def raw_sum_sql(relation, column)
+ node = node_to_operate(relation, column)
+
+ relation.unscope(:order).select(node.sum).to_sql
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def raw_average_sql(relation, column)
+ node = node_to_operate(relation, column)
+
+ relation.unscope(:order).select(node.average).to_sql
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def node_to_operate(relation, column)
if join_relation?(relation) && joined_column?(column)
table_name, column_name = column.split(".")
Arel::Table.new(table_name)[column_name]
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 7a17288e5e5..604fa364aa2 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -18,7 +18,6 @@
module Gitlab
class UsageData
- DEPRECATED_VALUE = -1000
MAX_GENERATION_TIME_FOR_SAAS = 40.hours
CE_MEMOIZED_VALUES = %i(
@@ -28,7 +27,6 @@ module Gitlab
project_maximum_id
user_minimum_id
user_maximum_id
- unique_visit_service
deployment_minimum_id
deployment_maximum_id
auth_providers
@@ -68,13 +66,17 @@ module Gitlab
# rubocop: disable Metrics/AbcSize
# rubocop: disable CodeReuse/ActiveRecord
def system_usage_data
- issues_created_manually_from_alerts = count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: minimum_id(Issue), finish: maximum_id(Issue))
+ issues_created_manually_from_alerts = if Gitlab.com?
+ FALLBACK
+ else
+ count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: minimum_id(Issue), finish: maximum_id(Issue))
+ end
{
counts: {
assignee_lists: count(List.assignee),
ci_builds: count(::Ci::Build),
- ci_internal_pipelines: count(::Ci::Pipeline.internal),
+ ci_internal_pipelines: Gitlab.com? ? FALLBACK : count(::Ci::Pipeline.internal),
ci_external_pipelines: count(::Ci::Pipeline.external),
ci_pipeline_config_auto_devops: count(::Ci::Pipeline.auto_devops_source),
ci_pipeline_config_repository: count(::Ci::Pipeline.repository_source),
@@ -156,7 +158,7 @@ module Gitlab
notes: count(Note)
}.merge(
runners_usage,
- services_usage,
+ integrations_usage,
usage_counters,
user_preferences_usage,
container_expiration_policies_usage,
@@ -193,8 +195,7 @@ module Gitlab
packages: count(::Packages::Package.where(monthly_time_range_db_params)),
personal_snippets: count(PersonalSnippet.where(monthly_time_range_db_params)),
project_snippets: count(ProjectSnippet.where(monthly_time_range_db_params)),
- projects_with_alerts_created: distinct_count(::AlertManagement::Alert.where(monthly_time_range_db_params), :project_id),
- promoted_issues: DEPRECATED_VALUE
+ projects_with_alerts_created: distinct_count(::AlertManagement::Alert.where(monthly_time_range_db_params), :project_id)
}.tap do |data|
data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
end
@@ -367,7 +368,7 @@ module Gitlab
results
end
- def services_usage
+ def integrations_usage
# rubocop: disable UsageData/LargeTable:
Integration.available_integration_names(include_dev: false).each_with_object({}) do |name, response|
type = Integration.integration_name_to_type(name)
@@ -409,7 +410,7 @@ module Gitlab
{
jira_imports_total_imported_count: count(finished_jira_imports),
jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id),
- jira_imports_total_imported_issues_count: sum(JiraImportState.finished, :imported_issues_count)
+ jira_imports_total_imported_issues_count: add_metric('JiraImportsTotalImportedIssuesCountMetric')
}
# rubocop: enable UsageData/LargeTable
end
@@ -645,38 +646,6 @@ module Gitlab
}
end
- def analytics_unique_visits_data
- results = ::Gitlab::Analytics::UniqueVisits.analytics_events.each_with_object({}) do |target, hash|
- hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) }
- end
- results['analytics_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) }
- results['analytics_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics, **monthly_time_range) }
-
- { analytics_unique_visits: results }
- end
-
- def compliance_unique_visits_data
- results = ::Gitlab::Analytics::UniqueVisits.compliance_events.each_with_object({}) do |target, hash|
- hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) }
- end
- results['compliance_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance) }
- results['compliance_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance, **monthly_time_range) }
-
- { compliance_unique_visits: results }
- end
-
- def search_unique_visits_data
- events = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('search')
- results = events.each_with_object({}) do |event, hash|
- hash[event] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: event, **weekly_time_range) }
- end
-
- results['search_unique_visits_for_any_target_weekly'] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, **weekly_time_range) }
- results['search_unique_visits_for_any_target_monthly'] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, **monthly_time_range) }
-
- { search_unique_visits: results }
- end
-
def action_monthly_active_users(time_period)
date_range = { date_from: time_period[:created_at].first, date_to: time_period[:created_at].last }
@@ -724,9 +693,6 @@ module Gitlab
.merge(topology_usage_data)
.merge(usage_activity_by_stage)
.merge(usage_activity_by_stage(:usage_activity_by_stage_monthly, monthly_time_range_db_params))
- .merge(analytics_unique_visits_data)
- .merge(compliance_unique_visits_data)
- .merge(search_unique_visits_data)
.merge(redis_hll_counters)
.deep_merge(aggregated_metrics_data)
end
@@ -769,7 +735,6 @@ module Gitlab
action_monthly_active_users_web_ide_edit: redis_usage_data { counter.count_web_ide_edit_actions(**date_range) },
action_monthly_active_users_sfe_edit: redis_usage_data { counter.count_sfe_edit_actions(**date_range) },
action_monthly_active_users_snippet_editor_edit: redis_usage_data { counter.count_snippet_editor_edit_actions(**date_range) },
- action_monthly_active_users_sse_edit: redis_usage_data { counter.count_sse_edit_actions(**date_range) },
action_monthly_active_users_ide_edit: redis_usage_data { counter.count_edit_using_editor(**date_range) }
}
end
@@ -810,9 +775,8 @@ module Gitlab
# rubocop: enable UsageData/LargeTable:
0.upto(series_amount - 1).map do |series|
- # When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
- sent_count = sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails
- clicked_count = clicked_emails.is_a?(Hash) ? clicked_emails.fetch([track, series], 0) : clicked_emails
+ sent_count = sent_in_product_marketing_email_count(sent_emails, track, series)
+ clicked_count = clicked_in_product_marketing_email_count(clicked_emails, track, series)
result["in_product_marketing_email_#{track}_#{series}_sent"] = sent_count
result["in_product_marketing_email_#{track}_#{series}_cta_clicked"] = clicked_count unless track == 'experience'
@@ -821,18 +785,20 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- def unique_visit_service
- strong_memoize(:unique_visit_service) do
- ::Gitlab::Analytics::UniqueVisits.new
- end
+ def sent_in_product_marketing_email_count(sent_emails, track, series)
+ # When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
+ sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails
+ end
+
+ def clicked_in_product_marketing_email_count(clicked_emails, track, series)
+ # When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
+ clicked_emails.is_a?(Hash) ? clicked_emails.fetch([track, series], 0) : clicked_emails
end
def total_alert_issues
# Remove prometheus table queries once they are deprecated
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/217407.
- add count(Issue.with_alert_management_alerts, start: minimum_id(Issue), finish: maximum_id(Issue)),
- count(::Issue.with_self_managed_prometheus_alert_events, start: minimum_id(Issue), finish: maximum_id(Issue)),
- count(::Issue.with_prometheus_alert_events, start: minimum_id(Issue), finish: maximum_id(Issue))
+ add_metric('IssuesCreatedFromAlertsMetric')
end
def clear_memoized
@@ -879,7 +845,7 @@ module Gitlab
gitlab_migration: add_metric('CountBulkImportsEntitiesMetric', time_frame: time_frame, options: { source_type: :project_entity })
}
- counters[:total] = add(*counters.values)
+ counters[:total] = add_metric('CountImportedProjectsTotalMetric', time_frame: time_frame)
counters
end
diff --git a/lib/gitlab/usage_data_counters.rb b/lib/gitlab/usage_data_counters.rb
index cdcad8fdc7b..2a3dcf267c6 100644
--- a/lib/gitlab/usage_data_counters.rb
+++ b/lib/gitlab/usage_data_counters.rb
@@ -15,7 +15,6 @@ module Gitlab
MergeRequestCounter,
DesignsCounter,
KubernetesAgentCounter,
- StaticSiteEditorCounter,
DiffsCounter,
ServiceUsageDataCounter
].freeze
diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
index f97ebdccecf..8feb24e49ac 100644
--- a/lib/gitlab/usage_data_counters/editor_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
@@ -6,7 +6,6 @@ module Gitlab
EDIT_BY_SNIPPET_EDITOR = 'g_edit_by_snippet_ide'
EDIT_BY_SFE = 'g_edit_by_sfe'
EDIT_BY_WEB_IDE = 'g_edit_by_web_ide'
- EDIT_BY_SSE = 'g_edit_by_sse'
EDIT_CATEGORY = 'ide_edit'
EDIT_BY_LIVE_PREVIEW = 'g_edit_by_live_preview'
@@ -40,14 +39,6 @@ module Gitlab
count_unique(events, date_from, date_to)
end
- def track_sse_edit_action(author:, time: Time.zone.now)
- track_unique_action(EDIT_BY_SSE, author, time)
- end
-
- def count_sse_edit_actions(date_from:, date_to:)
- count_unique(EDIT_BY_SSE, date_from, date_to)
- end
-
def track_live_preview_edit_action(author:, time: Time.zone.now)
track_unique_action(EDIT_BY_LIVE_PREVIEW, author, time)
end
diff --git a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
index e3bb3f6fef3..267b7fe673d 100644
--- a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
@@ -3,6 +3,10 @@
redis_slot: code_review
category: code_review
aggregation: weekly
+- name: i_code_review_mr_with_invalid_approvers
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
- name: i_code_review_user_single_file_diffs
redis_slot: code_review
category: code_review
diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml
index 448ed4c66e1..0dcbaf59c9c 100644
--- a/lib/gitlab/usage_data_counters/known_events/common.yml
+++ b/lib/gitlab/usage_data_counters/known_events/common.yml
@@ -30,11 +30,6 @@
redis_slot: edit
expiry: 29
aggregation: daily
-- name: g_edit_by_sse
- category: ide_edit
- redis_slot: edit
- expiry: 29
- aggregation: daily
- name: g_edit_by_snippet_ide
category: ide_edit
redis_slot: edit
@@ -134,6 +129,19 @@
redis_slot: incident_management
category: incident_management
aggregation: weekly
+# Incident management timeline events
+- name: incident_management_timeline_event_created
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_timeline_event_edited
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_timeline_event_deleted
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
# Incident management alerts
- name: incident_management_alert_create_incident
redis_slot: incident_management
@@ -144,7 +152,6 @@
redis_slot: incident_management
category: incident_management_oncall
aggregation: weekly
- feature_flag: usage_data_i_incident_management_oncall_notification_sent
# Testing category
- name: i_testing_test_case_parsed
category: testing
diff --git a/lib/gitlab/usage_data_counters/known_events/quickactions.yml b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
index 4ba7ea2d407..f980503b4bf 100644
--- a/lib/gitlab/usage_data_counters/known_events/quickactions.yml
+++ b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
@@ -135,6 +135,10 @@
category: quickactions
redis_slot: quickactions
aggregation: weekly
+- name: i_quickactions_ready
+ category: quickactions
+ redis_slot: quickactions
+ aggregation: weekly
- name: i_quickactions_reassign
category: quickactions
redis_slot: quickactions
diff --git a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb
index 0fadd68aeab..9c0f8fe9a80 100644
--- a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb
@@ -250,3 +250,7 @@ module Gitlab
end
end
end
+
+# rubocop:disable Layout/LineLength
+Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.prepend_mod_with('Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter')
+# rubocop:enable Layout/LineLength
diff --git a/lib/gitlab/usage_data_counters/static_site_editor_counter.rb b/lib/gitlab/usage_data_counters/static_site_editor_counter.rb
deleted file mode 100644
index 3c5989d1e11..00000000000
--- a/lib/gitlab/usage_data_counters/static_site_editor_counter.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module UsageDataCounters
- class StaticSiteEditorCounter < BaseCounter
- KNOWN_EVENTS = %w[views commits merge_requests].freeze
- PREFIX = 'static_site_editor'
-
- class << self
- def increment_views_count
- count(:views)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb
index b2d74b1f0dd..fef5cd680cb 100644
--- a/lib/gitlab/usage_data_queries.rb
+++ b/lib/gitlab/usage_data_queries.rb
@@ -85,6 +85,16 @@ module Gitlab
failures: []
}
end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def sent_in_product_marketing_email_count(sent_emails, track, series)
+ count(Users::InProductMarketingEmail.where(track: track, series: series))
+ end
+
+ def clicked_in_product_marketing_email_count(clicked_emails, track, series)
+ count(Users::InProductMarketingEmail.where(track: track, series: series).where.not(cta_clicked_at: nil))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb
index 633f4683b6b..4d1b234ae54 100644
--- a/lib/gitlab/utils/usage_data.rb
+++ b/lib/gitlab/utils/usage_data.rb
@@ -104,6 +104,15 @@ module Gitlab
end
end
+ def average(relation, column, batch_size: nil, start: nil, finish: nil)
+ with_duration do
+ Gitlab::Database::BatchCount.batch_average(relation, column, batch_size: batch_size, start: start, finish: finish)
+ rescue ActiveRecord::StatementInvalid => error
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
+ FALLBACK
+ end
+ end
+
# We don't support batching with histograms.
# Please avoid using this method on large tables.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/323949.
diff --git a/lib/gitlab/web_hooks/rate_limiter.rb b/lib/gitlab/web_hooks/rate_limiter.rb
new file mode 100644
index 00000000000..73d59f6f786
--- /dev/null
+++ b/lib/gitlab/web_hooks/rate_limiter.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WebHooks
+ class RateLimiter
+ include Gitlab::Utils::StrongMemoize
+
+ LIMIT_NAME = :web_hook_calls
+ NO_LIMIT = 0
+ # SystemHooks (instance admin hooks) and ServiceHooks (integration hooks)
+ # are not rate-limited.
+ EXCLUDED_HOOK_TYPES = %w(SystemHook ServiceHook).freeze
+
+ def initialize(hook)
+ @hook = hook
+ @parent = hook.parent
+ end
+
+ # Increments the rate-limit counter.
+ # Returns true if the hook should be rate-limited.
+ def rate_limit!
+ return false if no_limit?
+
+ ::Gitlab::ApplicationRateLimiter.throttled?(
+ limit_name,
+ scope: [root_namespace],
+ threshold: limit
+ )
+ end
+
+ # Returns true if the hook is currently over its rate-limit.
+ # It does not increment the rate-limit counter.
+ def rate_limited?
+ return false if no_limit?
+
+ Gitlab::ApplicationRateLimiter.peek(
+ limit_name,
+ scope: [root_namespace],
+ threshold: limit
+ )
+ end
+
+ def limit
+ strong_memoize(:limit) do
+ next NO_LIMIT if hook.class.name.in?(EXCLUDED_HOOK_TYPES)
+
+ root_namespace.actual_limits.limit_for(limit_name) || NO_LIMIT
+ end
+ end
+
+ private
+
+ attr_reader :hook, :parent
+
+ def no_limit?
+ limit == NO_LIMIT
+ end
+
+ def root_namespace
+ @root_namespace ||= parent.root_ancestor
+ end
+
+ def limit_name
+ LIMIT_NAME
+ end
+ end
+ end
+end
+
+Gitlab::WebHooks::RateLimiter.prepend_mod
diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb
index 3dd4e5e27d4..a8b51a95e59 100644
--- a/lib/object_storage/direct_upload.rb
+++ b/lib/object_storage/direct_upload.rb
@@ -205,7 +205,10 @@ module ObjectStorage
end
def requires_multipart_upload?
- config.aws? && !has_length
+ return false unless config.aws?
+ return false if use_workhorse_s3_client? && Feature.enabled?(:s3_omit_multipart_urls)
+
+ !has_length
end
def upload_id
diff --git a/lib/security/ci_configuration/sast_build_action.rb b/lib/security/ci_configuration/sast_build_action.rb
index 63f16a1bebe..73298bcd070 100644
--- a/lib/security/ci_configuration/sast_build_action.rb
+++ b/lib/security/ci_configuration/sast_build_action.rb
@@ -13,16 +13,16 @@ module Security
private
def variables(params)
- collect_values(params, 'value')
+ collect_values(params, :value)
end
def default_sast_values(params)
- collect_values(params, 'defaultValue')
+ collect_values(params, :default_value)
end
def collect_values(config, key)
- global_variables = config['global']&.to_h { |k| [k['field'], k[key]] } || {}
- pipeline_variables = config['pipeline']&.to_h { |k| [k['field'], k[key]] } || {}
+ global_variables = config[:global]&.to_h { |k| [k[:field], k[key]] } || {}
+ pipeline_variables = config[:pipeline]&.to_h { |k| [k[:field], k[key]] } || {}
analyzer_variables = collect_analyzer_values(config, key)
@@ -31,10 +31,10 @@ module Security
def collect_analyzer_values(config, key)
analyzer_variables = analyzer_variables_for(config, key)
- analyzer_variables['SAST_EXCLUDED_ANALYZERS'] = if key == 'value'
- config['analyzers']
- &.reject {|a| a['enabled'] }
- &.collect {|a| a['name'] }
+ analyzer_variables['SAST_EXCLUDED_ANALYZERS'] = if key == :value
+ config[:analyzers]
+ &.reject {|a| a[:enabled] }
+ &.collect {|a| a[:name] }
&.sort
&.join(', ')
else
@@ -45,10 +45,10 @@ module Security
end
def analyzer_variables_for(config, key)
- config['analyzers']
- &.select {|a| a['enabled'] && a['variables'] }
- &.flat_map {|a| a['variables'] }
- &.collect {|v| [v['field'], v[key]] }.to_h
+ config[:analyzers]
+ &.select {|a| a[:enabled] && a[:variables] }
+ &.flat_map {|a| a[:variables] }
+ &.collect {|v| [v[:field], v[key]] }.to_h
end
def update_existing_content!
diff --git a/lib/service_ping/build_payload.rb b/lib/service_ping/build_payload.rb
index 4d3b32a1fc0..3553b624ae0 100644
--- a/lib/service_ping/build_payload.rb
+++ b/lib/service_ping/build_payload.rb
@@ -3,8 +3,6 @@
module ServicePing
class BuildPayload
def execute
- return {} unless ServicePingSettings.product_intelligence_enabled?
-
filtered_usage_data
end
diff --git a/lib/service_ping/permit_data_categories.rb b/lib/service_ping/permit_data_categories.rb
index 51eec0808cb..cf69f7503b7 100644
--- a/lib/service_ping/permit_data_categories.rb
+++ b/lib/service_ping/permit_data_categories.rb
@@ -14,8 +14,6 @@ module ServicePing
].to_set.freeze
def execute
- return [] unless ServicePingSettings.product_intelligence_enabled?
-
CATEGORIES
end
end
diff --git a/lib/sidebars/groups/menus/customer_relations_menu.rb b/lib/sidebars/groups/menus/customer_relations_menu.rb
index 0aaa6ec45f1..e0772cfe403 100644
--- a/lib/sidebars/groups/menus/customer_relations_menu.rb
+++ b/lib/sidebars/groups/menus/customer_relations_menu.rb
@@ -35,7 +35,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Contacts'),
link: group_crm_contacts_path(context.group),
- active_routes: { path: 'groups/crm#contacts' },
+ active_routes: { controller: 'groups/crm/contacts' },
item_id: :crm_contacts
)
end
@@ -44,7 +44,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Organizations'),
link: group_crm_organizations_path(context.group),
- active_routes: { path: 'groups/crm#organizations' },
+ active_routes: { controller: 'groups/crm/organizations' },
item_id: :crm_organizations
)
end
diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb
index 2b5b3cdbb22..85931e63ebc 100644
--- a/lib/sidebars/projects/menus/settings_menu.rb
+++ b/lib/sidebars/projects/menus/settings_menu.rb
@@ -54,7 +54,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Integrations'),
link: project_settings_integrations_path(context.project),
- active_routes: { path: %w[integrations#show services#edit] },
+ active_routes: { path: %w[integrations#index integrations#edit] },
item_id: :integrations
)
end
@@ -104,15 +104,14 @@ module Sidebars
end
def packages_and_registries_menu_item
- if !Gitlab.config.registry.enabled ||
- !can?(context.current_user, :destroy_container_image, context.project)
+ unless can?(context.current_user, :view_package_registry_project_settings, context.project)
return ::Sidebars::NilMenuItem.new(item_id: :packages_and_registries)
end
::Sidebars::MenuItem.new(
title: _('Packages & Registries'),
link: project_settings_packages_and_registries_path(context.project),
- active_routes: { path: 'packages_and_registries#index' },
+ active_routes: { path: 'packages_and_registries#show' },
item_id: :packages_and_registries
)
end
diff --git a/lib/support/systemd/gitlab-sidekiq.service b/lib/support/systemd/gitlab-sidekiq.service
index 7d09944c862..cab741010ed 100644
--- a/lib/support/systemd/gitlab-sidekiq.service
+++ b/lib/support/systemd/gitlab-sidekiq.service
@@ -11,7 +11,6 @@ User=git
WorkingDirectory=/home/git/gitlab
Environment=RAILS_ENV=production
ExecStart=/usr/local/bin/bundle exec sidekiq --config /home/git/gitlab/config/sidekiq_queues.yml --environment production
-ExecStop=/usr/local/bin/bundle exec sidekiqctl stop /run/gitlab/sidekiq.pid
PIDFile=/home/git/gitlab/tmp/pids/sidekiq.pid
Restart=on-failure
RestartSec=1
diff --git a/lib/tasks/contracts.rake b/lib/tasks/contracts.rake
new file mode 100644
index 00000000000..6bb7f30ad57
--- /dev/null
+++ b/lib/tasks/contracts.rake
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+return if Rails.env.production?
+
+require 'pact/tasks/verification_task'
+
+contracts = File.expand_path('../../spec/contracts', __dir__)
+provider = File.expand_path('provider', contracts)
+
+# rubocop:disable Rails/RakeEnvironment
+namespace :contracts do
+ namespace :mr do
+ Pact::VerificationTask.new(:diffs_batch) do |pact|
+ pact.uri(
+ "#{contracts}/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json",
+ pact_helper: "#{provider}/pact_helpers/project/merge_request/diffs_batch_helper.rb"
+ )
+ end
+
+ Pact::VerificationTask.new(:diffs_metadata) do |pact|
+ pact.uri(
+ "#{contracts}/contracts/project/merge_request/show/" \
+ "mergerequest#show-merge_request_diffs_metadata_endpoint.json",
+ pact_helper: "#{provider}/pact_helpers/project/merge_request/diffs_metadata_helper.rb"
+ )
+ end
+
+ Pact::VerificationTask.new(:discussions) do |pact|
+ pact.uri(
+ "#{contracts}/contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json",
+ pact_helper: "#{provider}/pact_helpers/project/merge_request/discussions_helper.rb"
+ )
+ end
+
+ desc 'Run all merge request contract tests'
+ task 'test:merge_request', :contract_mr do |_t, arg|
+ raise(ArgumentError, 'Merge request contract tests require contract_mr to be set') unless arg[:contract_mr]
+
+ ENV['CONTRACT_MR'] = arg[:contract_mr]
+ errors = %w[metadata discussions diffs].each_with_object([]) do |task, err|
+ Rake::Task["contracts:mr:pact:verify:#{task}"].execute
+ rescue StandardError, SystemExit
+ err << "contracts:mr:pact:verify:#{task}"
+ end
+
+ raise StandardError, "Errors in tasks #{errors.join(', ')}" unless errors.empty?
+ end
+ end
+end
+# rubocop:enable Rails/RakeEnvironment
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 068dc463d16..a446a17dfc3 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -131,14 +131,6 @@ namespace :gitlab do
end
end
- desc 'GitLab | DB | Sets up EE specific database functionality'
-
- if Gitlab.ee?
- task setup_ee: %w[db:drop:geo db:create:geo db:schema:load:geo db:migrate:geo]
- else
- task :setup_ee
- end
-
desc 'This adjusts and cleans db/structure.sql - it runs after db:structure:dump'
task :clean_structure_sql do |task_name|
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
@@ -356,7 +348,13 @@ namespace :gitlab do
Rake::Task['db:drop'].invoke
Rake::Task['db:create'].invoke
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
- ActiveRecord::Base.establish_connection(db_config.configuration_hash.merge(username: username)) # rubocop: disable Database/EstablishConnection
+ config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
+ db_config.env_name,
+ db_config.name,
+ db_config.configuration_hash.merge(username: username)
+ )
+
+ ActiveRecord::Base.establish_connection(config) # rubocop: disable Database/EstablishConnection
Gitlab::Database.check_for_non_superuser
Rake::Task['db:migrate'].invoke
end
diff --git a/lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake b/lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake
new file mode 100644
index 00000000000..4d78acb3011
--- /dev/null
+++ b/lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :db do
+ namespace :decomposition do
+ namespace :rollback do
+ SEQUENCE_NAME_MATCHER = /nextval\('([a-z_]+)'::regclass\)/.freeze
+
+ desc 'Bump all the CI tables sequences on the Main Database'
+ task :bump_ci_sequences, [:increase_by] => :environment do |_t, args|
+ increase_by = args.increase_by.to_i
+ if increase_by < 1
+ puts 'Please specify a positive integer `increase_by` value'.color(:red)
+ puts 'Example: rake gitlab:db:decomposition:rollback:bump_ci_sequences[100000]'.color(:green)
+ exit 1
+ end
+
+ sequences_by_gitlab_schema(ApplicationRecord, :gitlab_ci).each do |sequence_name|
+ increment_sequence_by(ApplicationRecord.connection, sequence_name, increase_by)
+ end
+ end
+ end
+ end
+ end
+end
+
+# base_model is to choose which connection to use to query the tables
+# gitlab_schema, can be 'gitlab_main', 'gitlab_ci', 'gitlab_shared'
+def sequences_by_gitlab_schema(base_model, gitlab_schema)
+ tables = Gitlab::Database::GitlabSchema.tables_to_schema.select do |_table_name, schema_name|
+ schema_name == gitlab_schema
+ end.keys
+
+ models = tables.map do |table|
+ model = Class.new(base_model)
+ model.table_name = table
+ model
+ end
+
+ sequences = []
+ models.each do |model|
+ model.columns.each do |column|
+ match_result = column.default_function&.match(SEQUENCE_NAME_MATCHER)
+ next unless match_result
+
+ sequences << match_result[1]
+ end
+ end
+
+ sequences
+end
+
+# This method is going to increase the sequence next_value by:
+# - increment_by + 1 if the sequence has the attribute is_called = True (which is the common case)
+# - increment_by if the sequence has the attribute is_called = False (for example, a newly created sequence)
+# It uses ALTER SEQUENCE as a safety mechanism to avoid that no concurrent insertions
+# will cause conflicts on the sequence.
+# This is because ALTER SEQUENCE blocks concurrent nextval, currval, lastval, and setval calls.
+def increment_sequence_by(connection, sequence_name, increment_by)
+ connection.transaction do
+ # The first call is to make sure that the sequence's is_called value is set to `true`
+ # This guarantees that the next call to `nextval` will increase the sequence by `increment_by`
+ connection.select_value("SELECT nextval($1)", nil, [sequence_name])
+ connection.execute("ALTER SEQUENCE #{sequence_name} INCREMENT BY #{increment_by}")
+ connection.select_value("select nextval($1)", nil, [sequence_name])
+ connection.execute("ALTER SEQUENCE #{sequence_name} INCREMENT BY 1")
+ end
+end
diff --git a/lib/tasks/gitlab/db/lock_writes.rake b/lib/tasks/gitlab/db/lock_writes.rake
new file mode 100644
index 00000000000..b57c2860fe3
--- /dev/null
+++ b/lib/tasks/gitlab/db/lock_writes.rake
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :db do
+ TRIGGER_FUNCTION_NAME = 'gitlab_schema_prevent_write'
+
+ desc "GitLab | DB | Install prevent write triggers on all databases"
+ task lock_writes: [:environment, 'gitlab:db:validate_config'] do
+ Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name|
+ create_write_trigger_function(connection)
+
+ schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
+ Gitlab::Database::GitlabSchema.tables_to_schema.each do |table_name, schema_name|
+ if schemas_for_connection.include?(schema_name.to_sym)
+ drop_write_trigger(database_name, connection, table_name)
+ else
+ create_write_trigger(database_name, connection, table_name)
+ end
+ end
+ end
+ end
+
+ desc "GitLab | DB | Remove all triggers that prevents writes from all databases"
+ task unlock_writes: :environment do
+ Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name|
+ Gitlab::Database::GitlabSchema.tables_to_schema.each do |table_name, schema_name|
+ drop_write_trigger(database_name, connection, table_name)
+ end
+ drop_write_trigger_function(connection)
+ end
+ end
+
+ def create_write_trigger_function(connection)
+ sql = <<-SQL
+ CREATE OR REPLACE FUNCTION #{TRIGGER_FUNCTION_NAME}()
+ RETURNS TRIGGER AS
+ $$
+ BEGIN
+ RAISE EXCEPTION 'Table: "%" is write protected within this Gitlab database.', TG_TABLE_NAME
+ USING ERRCODE = 'modifying_sql_data_not_permitted',
+ HINT = 'Make sure you are using the right database connection';
+ END
+ $$ LANGUAGE PLPGSQL
+ SQL
+
+ connection.execute(sql)
+ end
+
+ def drop_write_trigger_function(connection)
+ sql = <<-SQL
+ DROP FUNCTION IF EXISTS #{TRIGGER_FUNCTION_NAME}()
+ SQL
+
+ connection.execute(sql)
+ end
+
+ def create_write_trigger(database_name, connection, table_name)
+ puts "#{database_name}: '#{table_name}'... Lock Writes".color(:yellow)
+ sql = <<-SQL
+ DROP TRIGGER IF EXISTS #{write_trigger_name(table_name)} ON #{table_name};
+ CREATE TRIGGER #{write_trigger_name(table_name)}
+ BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE
+ ON #{table_name}
+ FOR EACH STATEMENT EXECUTE FUNCTION #{TRIGGER_FUNCTION_NAME}();
+ SQL
+
+ with_retries(connection) do
+ connection.execute(sql)
+ end
+ end
+
+ def drop_write_trigger(database_name, connection, table_name)
+ puts "#{database_name}: '#{table_name}'... Allow Writes".color(:green)
+ sql = <<-SQL
+ DROP TRIGGER IF EXISTS #{write_trigger_name(table_name)} ON #{table_name}
+ SQL
+
+ with_retries(connection) do
+ connection.execute(sql)
+ end
+ end
+
+ def with_retries(connection, &block)
+ with_statement_timeout_retries do
+ with_lock_retries(connection) do
+ yield
+ end
+ end
+ end
+
+ def with_statement_timeout_retries(times = 5)
+ current_iteration = 1
+ begin
+ yield
+ rescue ActiveRecord::QueryCanceled => err
+ puts "Retrying after #{err.message}"
+
+ if current_iteration <= times
+ current_iteration += 1
+ retry
+ else
+ raise err
+ end
+ end
+ end
+
+ def with_lock_retries(connection, &block)
+ Gitlab::Database::WithLockRetries.new(
+ klass: "gitlab:db:lock_writes",
+ logger: Gitlab::AppLogger,
+ connection: connection
+ ).run(&block)
+ end
+
+ def write_trigger_name(table_name)
+ "gitlab_schema_write_trigger_for_#{table_name}"
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/db/validate_config.rake b/lib/tasks/gitlab/db/validate_config.rake
index 66aa949cc94..2a3a54b5351 100644
--- a/lib/tasks/gitlab/db/validate_config.rake
+++ b/lib/tasks/gitlab/db/validate_config.rake
@@ -4,6 +4,23 @@ databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml
namespace :gitlab do
namespace :db do
+ DB_CONFIG_NAME_KEY = 'gitlab_db_config_name'
+
+ DB_IDENTIFIER_SQL = <<-SQL
+ SELECT system_identifier, current_database()
+ FROM pg_control_system()
+ SQL
+
+ # We fetch timestamp as a way to properly handle race conditions
+ # fail in such cases, which should not really happen in production environment
+ DB_IDENTIFIER_WITH_DB_CONFIG_NAME_SQL = <<-SQL
+ SELECT
+ system_identifier, current_database(),
+ value as db_config_name, created_at as timestamp
+ FROM pg_control_system()
+ LEFT JOIN ar_internal_metadata ON ar_internal_metadata.key=$1
+ SQL
+
desc 'Validates `config/database.yml` to ensure a correct behavior is configured'
task validate_config: :environment do
original_db_config = ActiveRecord::Base.connection_db_config # rubocop:disable Database/MultipleDatabases
@@ -14,26 +31,22 @@ namespace :gitlab do
db_configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, include_replicas: true)
db_configs = db_configs.reject(&:replica?)
+ # The `pg_control_system()` is not enough to properly discover matching database systems
+ # since in case of cluster promotion it will return the same identifier as main cluster
+ # We instead set an `ar_internal_metadata` information with configured database name
+ db_configs.reverse_each do |db_config|
+ insert_db_identifier(db_config)
+ end
+
# Map each database connection into unique identifier of system+database
- # rubocop:disable Database/MultipleDatabases
all_connections = db_configs.map do |db_config|
- identifier =
- begin
- ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
- ActiveRecord::Base.connection.select_one("SELECT system_identifier, current_database() FROM pg_control_system()")
- rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
- warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
- rescue ActiveRecord::NoDatabaseError
- end
-
{
name: db_config.name,
config: db_config,
database_tasks?: db_config.database_tasks?,
- identifier: identifier
+ identifier: get_db_identifier(db_config)
}
- end.compact
- # rubocop:enable Database/MultipleDatabases
+ end
unique_connections = all_connections.group_by { |connection| connection[:identifier] }
primary_connection = all_connections.find { |connection| ActiveRecord::Base.configurations.primary?(connection[:name]) }
@@ -111,5 +124,43 @@ namespace :gitlab do
Rake::Task["db:schema:load:#{name}"].enhance(['gitlab:db:validate_config'])
Rake::Task["db:schema:dump:#{name}"].enhance(['gitlab:db:validate_config'])
end
+
+ def insert_db_identifier(db_config)
+ ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
+
+ if ActiveRecord::InternalMetadata.table_exists?
+ ts = Time.zone.now
+
+ ActiveRecord::InternalMetadata.upsert(
+ { key: DB_CONFIG_NAME_KEY,
+ value: db_config.name,
+ created_at: ts,
+ updated_at: ts }
+ )
+ end
+ rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
+ warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
+ rescue ActiveRecord::NoDatabaseError
+ rescue ActiveRecord::StatementInvalid => err
+ raise unless err.cause.is_a?(PG::ReadOnlySqlTransaction)
+
+ warn "WARNING: Could not write to the database #{db_config.name}: #{err.message}"
+ end
+
+ def get_db_identifier(db_config)
+ ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
+
+ # rubocop:disable Database/MultipleDatabases
+ if ActiveRecord::InternalMetadata.table_exists?
+ ActiveRecord::Base.connection.select_one(
+ DB_IDENTIFIER_WITH_DB_CONFIG_NAME_SQL, nil, [DB_CONFIG_NAME_KEY])
+ else
+ ActiveRecord::Base.connection.select_one(DB_IDENTIFIER_SQL)
+ end
+ # rubocop:enable Database/MultipleDatabases
+ rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
+ warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
+ rescue ActiveRecord::NoDatabaseError
+ end
end
end
diff --git a/lib/tasks/gitlab/pages.rake b/lib/tasks/gitlab/pages.rake
index c3828e7eba4..e6fde28e38f 100644
--- a/lib/tasks/gitlab/pages.rake
+++ b/lib/tasks/gitlab/pages.rake
@@ -4,60 +4,6 @@ require 'logger'
namespace :gitlab do
namespace :pages do
- desc "GitLab | Pages | Migrate legacy storage to zip format"
- task migrate_legacy_storage: :gitlab_environment do
- logger.info('Starting to migrate legacy pages storage to zip deployments')
-
- result = ::Pages::MigrateFromLegacyStorageService.new(logger,
- ignore_invalid_entries: ignore_invalid_entries,
- mark_projects_as_not_deployed: mark_projects_as_not_deployed)
- .execute_with_threads(threads: migration_threads, batch_size: batch_size)
-
- logger.info("A total of #{result[:migrated] + result[:errored]} projects were processed.")
- logger.info("- The #{result[:migrated]} projects migrated successfully")
- logger.info("- The #{result[:errored]} projects failed to be migrated")
- end
-
- desc "GitLab | Pages | DANGER: Removes data which was migrated from legacy storage on zip storage. Can be used if some bugs in migration are discovered and migration needs to be restarted from scratch."
- task clean_migrated_zip_storage: :gitlab_environment do
- destroyed_deployments = 0
-
- logger.info("Starting to delete migrated pages deployments")
-
- ::PagesDeployment.migrated_from_legacy_storage.each_batch(of: batch_size) do |batch|
- destroyed_deployments += batch.count
-
- # we need to destroy associated files, so can't use delete_all
- batch.destroy_all # rubocop: disable Cop/DestroyAll
-
- logger.info("#{destroyed_deployments} deployments were deleted")
- end
- end
-
- def logger
- @logger ||= Logger.new($stdout)
- end
-
- def migration_threads
- ENV.fetch('PAGES_MIGRATION_THREADS', '3').to_i
- end
-
- def batch_size
- ENV.fetch('PAGES_MIGRATION_BATCH_SIZE', '10').to_i
- end
-
- def ignore_invalid_entries
- Gitlab::Utils.to_boolean(
- ENV.fetch('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'false')
- )
- end
-
- def mark_projects_as_not_deployed
- Gitlab::Utils.to_boolean(
- ENV.fetch('PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED', 'false')
- )
- end
-
namespace :deployments do
task migrate_to_object_storage: :gitlab_environment do
logger = Logger.new($stdout)
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index 6574bfd2549..40d88ea8a5b 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -6,6 +6,17 @@ namespace :tw do
desc 'Generates a list of codeowners for documentation pages.'
task :codeowners do
CodeOwnerRule = Struct.new(:category, :writer)
+ DocumentOwnerMapping = Struct.new(:path, :writer) do
+ def writer_owns_all_pages?(mappings)
+ mappings
+ .select { |mapping| mapping.directory == directory }
+ .all? { |mapping| mapping.writer == writer }
+ end
+
+ def directory
+ @directory ||= File.dirname(path)
+ end
+ end
CODE_OWNER_RULES = [
CodeOwnerRule.new('Activation', '@kpaizee'),
@@ -61,7 +72,6 @@ namespace :tw do
CodeOwnerRule.new('Sharding', '@sselhorn'),
CodeOwnerRule.new('Source Code', '@aqualls'),
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
- CodeOwnerRule.new('Static Site Editor', '@aqualls'),
CodeOwnerRule.new('Style Guide', '@sselhorn'),
CodeOwnerRule.new('Testing', '@eread'),
CodeOwnerRule.new('Threat Insights', '@claytoncornell'),
@@ -85,6 +95,7 @@ namespace :tw do
end
errors = []
+ mappings = []
path = Rails.root.join("doc/**/*.md")
Dir.glob(path) do |file|
@@ -99,9 +110,21 @@ namespace :tw do
writer = writer_for_group(document.group)
next unless writer
- puts "#{file.gsub(Dir.pwd, ".")} #{writer}" if document.has_a_valid_group?
+ mappings << DocumentOwnerMapping.new(file.delete_prefix(Dir.pwd), writer) if document.has_a_valid_group?
end
+ deduplicated_mappings = Set.new
+
+ mappings.each do |mapping|
+ if mapping.writer_owns_all_pages?(mappings)
+ deduplicated_mappings.add("#{mapping.directory}/ #{mapping.writer}")
+ else
+ deduplicated_mappings.add("#{mapping.path} #{mapping.writer}")
+ end
+ end
+
+ deduplicated_mappings.each { |mapping| puts mapping }
+
if errors.present?
puts "-----"
puts "ERRORS - the following files are missing the correct metadata:"
diff --git a/lib/tasks/migrate/composite_primary_keys.rake b/lib/tasks/migrate/composite_primary_keys.rake
deleted file mode 100644
index 68f7c4d6c4a..00000000000
--- a/lib/tasks/migrate/composite_primary_keys.rake
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-namespace :gitlab do
- namespace :db do
- desc 'GitLab | DB | Adds primary keys to tables that only have composite unique keys'
- task composite_primary_keys_add: :environment do
- require Rails.root.join('db/optional_migrations/composite_primary_keys')
- CompositePrimaryKeysMigration.new.up
- end
-
- desc 'GitLab | DB | Removes previously added composite primary keys'
- task composite_primary_keys_drop: :environment do
- require Rails.root.join('db/optional_migrations/composite_primary_keys')
- CompositePrimaryKeysMigration.new.down
- end
- end
-end
diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake
index 6eabdf51dcd..28c370e5ca6 100644
--- a/lib/tasks/rubocop.rake
+++ b/lib/tasks/rubocop.rake
@@ -36,7 +36,7 @@ unless Rails.env.production?
# expected.
cop_names = args.to_a
- todo_dir = RuboCop::TodoDir.new(RuboCop::TodoDir::DEFAULT_TODO_DIR)
+ todo_dir = RuboCop::TodoDir.new(RuboCop::Formatter::TodoFormatter.base_directory)
if cop_names.any?
# We are sorting the cop names to benefit from RuboCop cache which