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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-20 13:43:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-20 13:43:29 +0300
commit3b1af5cc7ed2666ff18b718ce5d30fa5a2756674 (patch)
tree3bc4a40e0ee51ec27eabf917c537033c0c5b14d4 /lib
parent9bba14be3f2c211bf79e15769cd9b77bc73a13bc (diff)
Add latest changes from gitlab-org/gitlab@16-1-stable-eev16.1.0-rc42
Diffstat (limited to 'lib')
-rw-r--r--lib/api/admin/batched_background_migrations.rb7
-rw-r--r--lib/api/admin/dictionary.rb61
-rw-r--r--lib/api/admin/migrations.rb62
-rw-r--r--lib/api/admin/plan_limits.rb10
-rw-r--r--lib/api/api.rb11
-rw-r--r--lib/api/api_guard.rb5
-rw-r--r--lib/api/badges.rb2
-rw-r--r--lib/api/ci/helpers/runner.rb6
-rw-r--r--lib/api/ci/pipelines.rb3
-rw-r--r--lib/api/ci/runner.rb2
-rw-r--r--lib/api/ci/secure_files.rb11
-rw-r--r--lib/api/concerns/packages/nuget/private_endpoints.rb153
-rw-r--r--lib/api/concerns/packages/nuget/public_endpoints.rb42
-rw-r--r--lib/api/concerns/packages/nuget_endpoints.rb159
-rw-r--r--lib/api/debian_project_packages.rb6
-rw-r--r--lib/api/deploy_keys.rb4
-rw-r--r--lib/api/discussions.rb4
-rw-r--r--lib/api/entities/deploy_key.rb1
-rw-r--r--lib/api/entities/dictionary/table.rb12
-rw-r--r--lib/api/entities/draft_note.rb2
-rw-r--r--lib/api/entities/error_tracking.rb2
-rw-r--r--lib/api/entities/merge_request_basic.rb1
-rw-r--r--lib/api/entities/namespace.rb8
-rw-r--r--lib/api/entities/note.rb2
-rw-r--r--lib/api/entities/nuget/metadatum.rb6
-rw-r--r--lib/api/entities/nuget/package_metadata_catalog_entry.rb3
-rw-r--r--lib/api/entities/nuget/search_result.rb2
-rw-r--r--lib/api/entities/package.rb2
-rw-r--r--lib/api/entities/package_version.rb2
-rw-r--r--lib/api/entities/plan_limit.rb2
-rw-r--r--lib/api/entities/project_integration_basic.rb2
-rw-r--r--lib/api/entities/project_scope_link.rb10
-rw-r--r--lib/api/error_tracking/collector.rb156
-rw-r--r--lib/api/group_avatar.rb2
-rw-r--r--lib/api/groups.rb30
-rw-r--r--lib/api/helpers.rb13
-rw-r--r--lib/api/helpers/integrations_helpers.rb90
-rw-r--r--lib/api/helpers/notes_helpers.rb2
-rw-r--r--lib/api/helpers/packages/basic_auth_helpers.rb13
-rw-r--r--lib/api/helpers/packages/conan/api_helpers.rb45
-rw-r--r--lib/api/helpers/packages/npm.rb108
-rw-r--r--lib/api/helpers/packages_helpers.rb18
-rw-r--r--lib/api/internal/error_tracking.rb2
-rw-r--r--lib/api/internal/kubernetes.rb15
-rw-r--r--lib/api/issues.rb7
-rw-r--r--lib/api/markdown.rb5
-rw-r--r--lib/api/maven_packages.rb10
-rw-r--r--lib/api/members.rb4
-rw-r--r--lib/api/ml/mlflow/entrypoint.rb2
-rw-r--r--lib/api/ml_model_packages.rb111
-rw-r--r--lib/api/namespaces.rb6
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/npm_group_packages.rb25
-rw-r--r--lib/api/npm_instance_packages.rb6
-rw-r--r--lib/api/npm_project_packages.rb6
-rw-r--r--lib/api/nuget_group_packages.rb39
-rw-r--r--lib/api/nuget_project_packages.rb280
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_job_token_scope.rb130
-rw-r--r--lib/api/project_packages.rb50
-rw-r--r--lib/api/project_snippets.rb8
-rw-r--r--lib/api/project_templates.rb8
-rw-r--r--lib/api/projects.rb40
-rw-r--r--lib/api/release/links.rb12
-rw-r--r--lib/api/releases.rb15
-rw-r--r--lib/api/resource_access_tokens.rb35
-rw-r--r--lib/api/settings.rb9
-rw-r--r--lib/api/snippets.rb8
-rw-r--r--lib/api/system_hooks.rb2
-rw-r--r--lib/api/topics.rb2
-rw-r--r--lib/api/users.rb21
-rw-r--r--lib/api/v3/github.rb10
-rw-r--r--lib/api/validations/validators/absence.rb2
-rw-r--r--lib/api/validations/validators/array_none_any.rb2
-rw-r--r--lib/api/validations/validators/bulk_imports.rb6
-rw-r--r--lib/api/validations/validators/check_assignees_count.rb2
-rw-r--r--lib/api/validations/validators/email_or_email_list.rb2
-rw-r--r--lib/api/validations/validators/file_path.rb4
-rw-r--r--lib/api/validations/validators/git_ref.rb2
-rw-r--r--lib/api/validations/validators/git_sha.rb2
-rw-r--r--lib/api/validations/validators/integer_or_custom_value.rb4
-rw-r--r--lib/api/validations/validators/limit.rb2
-rw-r--r--lib/api/validations/validators/project_portable.rb2
-rw-r--r--lib/api/validations/validators/untrusted_regexp.rb2
-rw-r--r--lib/atlassian/jira_issue_key_extractor.rb4
-rw-r--r--lib/backup/manager.rb17
-rw-r--r--lib/backup/repositories.rb20
-rw-r--r--lib/banzai/filter/autolink_filter.rb2
-rw-r--r--lib/banzai/filter/dollar_math_post_filter.rb4
-rw-r--r--lib/banzai/filter/inline_alert_metrics_filter.rb47
-rw-r--r--lib/banzai/filter/inline_cluster_metrics_filter.rb40
-rw-r--r--lib/banzai/filter/inline_diff_filter.rb4
-rw-r--r--lib/banzai/filter/inline_embeds_filter.rb93
-rw-r--r--lib/banzai/filter/inline_grafana_metrics_filter.rb79
-rw-r--r--lib/banzai/filter/inline_metrics_filter.rb36
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb154
-rw-r--r--lib/banzai/filter/references/reference_filter.rb4
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb2
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb2
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb4
-rw-r--r--lib/banzai/issuable_extractor.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb9
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb1
-rw-r--r--lib/bitbucket/representation/pull_request.rb10
-rw-r--r--lib/bulk_imports/clients/http.rb4
-rw-r--r--lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb4
-rw-r--r--lib/bulk_imports/common/pipelines/members_pipeline.rb2
-rw-r--r--lib/bulk_imports/common/pipelines/uploads_pipeline.rb2
-rw-r--r--lib/bulk_imports/common/transformers/member_attributes_transformer.rb (renamed from lib/bulk_imports/groups/transformers/member_attributes_transformer.rb)2
-rw-r--r--lib/bulk_imports/file_downloads/validations.rb2
-rw-r--r--lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb4
-rw-r--r--lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb4
-rw-r--r--lib/error_tracking/collector/payload_validator.rb13
-rw-r--r--lib/error_tracking/collector/sentry_auth_parser.rb25
-rw-r--r--lib/error_tracking/collector/sentry_request_parser.rb30
-rw-r--r--lib/error_tracking/stacktrace_builder.rb1
-rw-r--r--lib/extracts_ref.rb30
-rw-r--r--lib/feature/shared.rb11
-rw-r--r--lib/generators/gitlab/analytics/internal_events_generator.rb278
-rw-r--r--lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template2
-rw-r--r--lib/gitlab/access.rb6
-rw-r--r--lib/gitlab/access/branch_protection.rb4
-rw-r--r--lib/gitlab/alert_management/payload/base.rb1
-rw-r--r--lib/gitlab/alert_management/payload/prometheus.rb6
-rw-r--r--lib/gitlab/analytics/date_filler.rb2
-rw-r--r--lib/gitlab/api_authentication/token_locator.rb44
-rw-r--r--lib/gitlab/application_rate_limiter.rb2
-rw-r--r--lib/gitlab/asciidoc.rb3
-rw-r--r--lib/gitlab/asciidoc/include_processor.rb44
-rw-r--r--lib/gitlab/audit/auditor.rb10
-rw-r--r--lib/gitlab/audit/type/definition.rb10
-rw-r--r--lib/gitlab/auth.rb2
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/auth/saml/auth_hash.rb2
-rw-r--r--lib/gitlab/auth/saml/config.rb36
-rw-r--r--lib/gitlab/auth/saml/user.rb2
-rw-r--r--lib/gitlab/authorized_keys.rb2
-rw-r--r--lib/gitlab/avatar_cache.rb8
-rw-r--r--lib/gitlab/background_migration/backfill_ci_queuing_tables.rb153
-rw-r--r--lib/gitlab/background_migration/backfill_code_suggestions_namespace_settings.rb33
-rw-r--r--lib/gitlab/background_migration/backfill_group_features.rb35
-rw-r--r--lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb38
-rw-r--r--lib/gitlab/background_migration/backfill_resource_link_events.rb71
-rw-r--r--lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes.rb99
-rw-r--r--lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex.rb48
-rw-r--r--lib/gitlab/background_migration/encrypt_integration_properties.rb84
-rw-r--r--lib/gitlab/background_migration/encrypt_static_object_token.rb70
-rw-r--r--lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb82
-rw-r--r--lib/gitlab/background_migration/fix_incorrect_max_seats_used.rb13
-rw-r--r--lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb124
-rw-r--r--lib/gitlab/background_migration/mark_duplicate_npm_packages_for_destruction.rb48
-rw-r--r--lib/gitlab/background_migration/merge_topics_with_same_name.rb76
-rw-r--r--lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner.rb45
-rw-r--r--lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb27
-rw-r--r--lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects.rb2
-rw-r--r--lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb44
-rw-r--r--lib/gitlab/background_migration/populate_container_repository_migration_plan.rb51
-rw-r--r--lib/gitlab/background_migration/populate_namespace_statistics.rb47
-rw-r--r--lib/gitlab/background_migration/populate_test_reports_issue_id.rb14
-rw-r--r--lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb36
-rw-r--r--lib/gitlab/background_migration/populate_vulnerability_reads.rb84
-rw-r--r--lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb218
-rw-r--r--lib/gitlab/background_migration/recalculate_vulnerability_finding_signatures_for_findings.rb13
-rw-r--r--lib/gitlab/background_migration/remove_all_trace_expiration_dates.rb53
-rw-r--r--lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups.rb21
-rw-r--r--lib/gitlab/background_migration/remove_project_group_link_with_missing_groups.rb2
-rw-r--r--lib/gitlab/background_migration/remove_vulnerability_finding_links.rb19
-rw-r--r--lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb40
-rw-r--r--lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb40
-rw-r--r--lib/gitlab/background_migration/update_timelogs_null_spent_at.rb39
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb22
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb7
-rw-r--r--lib/gitlab/bitbucket_server_import/importers/repository_importer.rb8
-rw-r--r--lib/gitlab/bitbucket_server_import/user_finder.rb2
-rw-r--r--lib/gitlab/cache/import/caching.rb4
-rw-r--r--lib/gitlab/cache/json_cache.rb123
-rw-r--r--lib/gitlab/cache/json_caches/json_keyed.rb41
-rw-r--r--lib/gitlab/cache/json_caches/redis_keyed.rb31
-rw-r--r--lib/gitlab/checks/branch_check.rb7
-rw-r--r--lib/gitlab/checks/diff_check.rb5
-rw-r--r--lib/gitlab/ci/badge/release/latest_release.rb3
-rw-r--r--lib/gitlab/ci/build/rules.rb30
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb8
-rw-r--r--lib/gitlab/ci/config/entry/include/rules/rule.rb8
-rw-r--r--lib/gitlab/ci/config/external/file/base.rb3
-rw-r--r--lib/gitlab/ci/config/external/file/project.rb27
-rw-r--r--lib/gitlab/ci/config/external/mapper/verifier.rb28
-rw-r--r--lib/gitlab/ci/config/external/rules.rb27
-rw-r--r--lib/gitlab/ci/config/yaml.rb45
-rw-r--r--lib/gitlab/ci/config/yaml/interpolator.rb (renamed from lib/gitlab/ci/config/external/interpolator.rb)4
-rw-r--r--lib/gitlab/ci/config/yaml/loader.rb48
-rw-r--r--lib/gitlab/ci/config/yaml/result.rb2
-rw-r--r--lib/gitlab/ci/decompressed_gzip_size_validator.rb2
-rw-r--r--lib/gitlab/ci/jwt_v2.rb27
-rw-r--r--lib/gitlab/ci/parsers/security/common.rb1
-rw-r--r--lib/gitlab/ci/project_config.rb2
-rw-r--r--lib/gitlab/ci/project_config/repository.rb5
-rw-r--r--lib/gitlab/ci/project_config/source.rb5
-rw-r--r--lib/gitlab/ci/reports/security/finding.rb5
-rw-r--r--lib/gitlab/ci/runner_instructions.rb2
-rw-r--r--lib/gitlab/ci/secure_files/migration_helper.rb33
-rw-r--r--lib/gitlab/ci/status/build/waiting_for_approval.rb38
-rw-r--r--lib/gitlab/ci/status/scheduled.rb4
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/ci/templates/Android.gitlab-ci.yml36
-rw-r--r--lib/gitlab/ci/templates/Flutter.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.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/Kaniko.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml13
-rw-r--r--lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml13
-rw-r--r--lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml11
-rw-r--r--lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml21
-rw-r--r--lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml15
-rw-r--r--lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml43
-rw-r--r--lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml11
-rw-r--r--lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/SwaggerUI.gitlab-ci.yml13
-rw-r--r--lib/gitlab/ci/templates/Pages/Zola.gitlab-ci.yml30
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml15
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml15
-rw-r--r--lib/gitlab/ci/variables/builder.rb8
-rw-r--r--lib/gitlab/ci/variables/builder/pipeline.rb27
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb51
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_observer.rb22
-rw-r--r--lib/gitlab/counters/buffered_counter.rb12
-rw-r--r--lib/gitlab/data_builder/pipeline.rb4
-rw-r--r--lib/gitlab/database.rb121
-rw-r--r--lib/gitlab/database/async_indexes/index_base.rb3
-rw-r--r--lib/gitlab/database/async_indexes/postgres_async_index.rb24
-rw-r--r--lib/gitlab/database/background_migration/batched_job.rb2
-rw-r--r--lib/gitlab/database/background_migration/batched_migration.rb15
-rw-r--r--lib/gitlab/database/background_migration/batched_migration_runner.rb2
-rw-r--r--lib/gitlab/database/background_migration/health_status.rb45
-rw-r--r--lib/gitlab/database/background_migration/health_status/indicators.rb12
-rw-r--r--lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table.rb41
-rw-r--r--lib/gitlab/database/background_migration/health_status/indicators/patroni_apdex.rb90
-rw-r--r--lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log.rb74
-rw-r--r--lib/gitlab/database/background_migration/health_status/signals.rb71
-rw-r--r--lib/gitlab/database/convert_feature_category_to_group_label.rb37
-rw-r--r--lib/gitlab/database/database_connection_info.rb71
-rw-r--r--lib/gitlab/database/each_database.rb1
-rw-r--r--lib/gitlab/database/gitlab_schema.rb85
-rw-r--r--lib/gitlab/database/gitlab_schema_info.rb28
-rw-r--r--lib/gitlab/database/health_status.rb40
-rw-r--r--lib/gitlab/database/health_status/context.rb28
-rw-r--r--lib/gitlab/database/health_status/indicators.rb10
-rw-r--r--lib/gitlab/database/health_status/indicators/autovacuum_active_on_table.rb39
-rw-r--r--lib/gitlab/database/health_status/indicators/patroni_apdex.rb88
-rw-r--r--lib/gitlab/database/health_status/indicators/write_ahead_log.rb71
-rw-r--r--lib/gitlab/database/health_status/logger.rb15
-rw-r--r--lib/gitlab/database/health_status/signals.rb63
-rw-r--r--lib/gitlab/database/load_balancing/host.rb81
-rw-r--r--lib/gitlab/database/lock_writes_manager.rb9
-rw-r--r--lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb38
-rw-r--r--lib/gitlab/database/migrations/constraints_helpers.rb10
-rw-r--r--lib/gitlab/database/partitioning.rb4
-rw-r--r--lib/gitlab/database/partitioning/list/convert_table.rb50
-rw-r--r--lib/gitlab/database/partitioning/sliding_list_strategy.rb13
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb3
-rw-r--r--lib/gitlab/database/postgres_autovacuum_activity.rb7
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb50
-rw-r--r--lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb22
-rw-r--r--lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter.rb15
-rw-r--r--lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter.rb31
-rw-r--r--lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter.rb50
-rw-r--r--lib/gitlab/database/schema_validation/database.rb38
-rw-r--r--lib/gitlab/database/schema_validation/schema_inconsistency.rb4
-rw-r--r--lib/gitlab/database/schema_validation/schema_objects/foreign_key.rb34
-rw-r--r--lib/gitlab/database/schema_validation/structure_sql.rb39
-rw-r--r--lib/gitlab/database/schema_validation/track_inconsistency.rb58
-rw-r--r--lib/gitlab/database/schema_validation/validators/base_validator.rb5
-rw-r--r--lib/gitlab/database/schema_validation/validators/different_definition_foreign_keys.rb24
-rw-r--r--lib/gitlab/database/schema_validation/validators/extra_foreign_keys.rb21
-rw-r--r--lib/gitlab/database/schema_validation/validators/missing_foreign_keys.rb21
-rw-r--r--lib/gitlab/database/tables_locker.rb6
-rw-r--r--lib/gitlab/database/tables_truncate.rb2
-rw-r--r--lib/gitlab/database_importers/common_metrics.rb8
-rw-r--r--lib/gitlab/database_importers/common_metrics/importer.rb78
-rw-r--r--lib/gitlab/database_importers/common_metrics/prometheus_metric.rb12
-rw-r--r--lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb45
-rw-r--r--lib/gitlab/database_importers/default_organization_importer.rb19
-rw-r--r--lib/gitlab/dependency_linker/requirements_txt_linker.rb2
-rw-r--r--lib/gitlab/devise_failure.rb8
-rw-r--r--lib/gitlab/diff/formatters/base_formatter.rb1
-rw-r--r--lib/gitlab/diff/formatters/file_formatter.rb33
-rw-r--r--lib/gitlab/diff/formatters/image_formatter.rb1
-rw-r--r--lib/gitlab/diff/formatters/text_formatter.rb4
-rw-r--r--lib/gitlab/diff/position.rb15
-rw-r--r--lib/gitlab/diff/position_tracer.rb16
-rw-r--r--lib/gitlab/diff/position_tracer/file_strategy.rb47
-rw-r--r--lib/gitlab/diff/position_tracer/image_strategy.rb31
-rw-r--r--lib/gitlab/diff/position_tracer/line_strategy.rb5
-rw-r--r--lib/gitlab/discussions_diff/highlight_cache.rb12
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb2
-rw-r--r--lib/gitlab/email/handler/service_desk_handler.rb2
-rw-r--r--lib/gitlab/email/hook/silent_mode_interceptor.rb12
-rw-r--r--lib/gitlab/email/reply_parser.rb4
-rw-r--r--lib/gitlab/error_tracking/error_repository.rb2
-rw-r--r--lib/gitlab/error_tracking/error_repository/open_api_strategy.rb10
-rw-r--r--lib/gitlab/etag_caching/middleware.rb2
-rw-r--r--lib/gitlab/etag_caching/router/rails.rb2
-rw-r--r--lib/gitlab/front_matter.rb2
-rw-r--r--lib/gitlab/git/repository.rb16
-rw-r--r--lib/gitlab/git/tag.rb16
-rw-r--r--lib/gitlab/git/tree.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb87
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb15
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb4
-rw-r--r--lib/gitlab/github_gists_import/importer/gist_importer.rb51
-rw-r--r--lib/gitlab/github_gists_import/representation/gist.rb4
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb8
-rw-r--r--lib/gitlab/github_import/representation/diff_note.rb17
-rw-r--r--lib/gitlab/gl_repository.rb2
-rw-r--r--lib/gitlab/gl_repository/identifier.rb2
-rw-r--r--lib/gitlab/gon_helper.rb7
-rw-r--r--lib/gitlab/graphql/generic_tracing.rb6
-rw-r--r--lib/gitlab/hotlinking_detector.rb5
-rw-r--r--lib/gitlab/http.rb25
-rw-r--r--lib/gitlab/i18n.rb16
-rw-r--r--lib/gitlab/import_export/decompressed_archive_size_validator.rb2
-rw-r--r--lib/gitlab/import_export/group/import_export.yml1
-rw-r--r--lib/gitlab/import_export/group/tree_restorer.rb29
-rw-r--r--lib/gitlab/import_export/legacy_relation_tree_saver.rb23
-rw-r--r--lib/gitlab/import_export/project/exported_relations_merger.rb4
-rw-r--r--lib/gitlab/import_export/project/import_export.yml3
-rw-r--r--lib/gitlab/import_export/project/relation_factory.rb1
-rw-r--r--lib/gitlab/import_export/recursive_merge_folders.rb6
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb2
-rw-r--r--lib/gitlab/instrumentation/redis_base.rb4
-rw-r--r--lib/gitlab/instrumentation/redis_cluster_validator.rb11
-rw-r--r--lib/gitlab/internal_events.rb44
-rw-r--r--lib/gitlab/json_cache.rb118
-rw-r--r--lib/gitlab/markdown_cache/redis/store.rb2
-rw-r--r--lib/gitlab/merge_requests/message_generator.rb1
-rw-r--r--lib/gitlab/metrics/loose_foreign_keys_slis.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/rails_cache.rb34
-rw-r--r--lib/gitlab/middleware/compressed_json.rb4
-rw-r--r--lib/gitlab/pagination/cursor_based_keyset.rb3
-rw-r--r--lib/gitlab/patch/redis_cache_store.rb84
-rw-r--r--lib/gitlab/patch/redis_cluster.rb21
-rw-r--r--lib/gitlab/path_regex.rb16
-rw-r--r--lib/gitlab/path_traversal.rb48
-rw-r--r--lib/gitlab/project_authorizations.rb140
-rw-r--r--lib/gitlab/quick_actions/relate_actions.rb46
-rw-r--r--lib/gitlab/quick_actions/work_item_actions.rb90
-rw-r--r--lib/gitlab/reactive_cache_set_cache.rb10
-rw-r--r--lib/gitlab/redis.rb4
-rw-r--r--lib/gitlab/redis/cache.rb40
-rw-r--r--lib/gitlab/redis/chat.rb13
-rw-r--r--lib/gitlab/redis/cluster_cache.rb13
-rw-r--r--lib/gitlab/redis/cluster_util.rb32
-rw-r--r--lib/gitlab/redis/cross_slot.rb141
-rw-r--r--lib/gitlab/redis/multi_store.rb16
-rw-r--r--lib/gitlab/redis/rate_limiting.rb7
-rw-r--r--lib/gitlab/regex.rb16
-rw-r--r--lib/gitlab/repository_hash_cache.rb6
-rw-r--r--lib/gitlab/resource_events/assignment_event_recorder.rb2
-rw-r--r--lib/gitlab/search/abuse_detection.rb4
-rw-r--r--lib/gitlab/search/params.rb2
-rw-r--r--lib/gitlab/search_results.rb10
-rw-r--r--lib/gitlab/sentence.rb12
-rw-r--r--lib/gitlab/set_cache.rb6
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb22
-rw-r--r--lib/gitlab/sidekiq_middleware.rb5
-rw-r--r--lib/gitlab/sidekiq_middleware/defer_jobs.rb78
-rw-r--r--lib/gitlab/silent_mode.rb21
-rw-r--r--lib/gitlab/slash_commands/incident_management/incident_command.rb8
-rw-r--r--lib/gitlab/slash_commands/incident_management/incident_new.rb8
-rw-r--r--lib/gitlab/slash_commands/issue_new.rb2
-rw-r--r--lib/gitlab/spamcheck/client.rb1
-rw-r--r--lib/gitlab/template/finders/global_template_finder.rb2
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb2
-rw-r--r--lib/gitlab/template/metrics_dashboard_template.rb31
-rw-r--r--lib/gitlab/tracking.rb16
-rw-r--r--lib/gitlab/tracking/standard_context.rb2
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_all_ci_builds_metric.rb15
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_deployments_metric.rb40
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric.rb4
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_personal_snippets_metric.rb17
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_project_snippets_metric.rb (renamed from lib/gitlab/usage/metrics/instrumentations/installation_creation_date_metric.rb)8
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_projects_with_alerts_created_metric.rb17
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_snippets_metric.rb35
-rw-r--r--lib/gitlab/usage_data.rb57
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb80
-rw-r--r--lib/gitlab/usage_data_counters/jetbrains_bundled_plugin_activity_unique_counter.rb29
-rw-r--r--lib/gitlab/usage_data_counters/known_events/ci_templates.yml2
-rw-r--r--lib/gitlab/usage_data_counters/known_events/code_review_events.yml2
-rw-r--r--lib/gitlab/usage_data_counters/known_events/product_analytics.yml4
-rw-r--r--lib/gitlab/usage_data_counters/known_events/quickactions.yml4
-rw-r--r--lib/gitlab/usage_data_counters/known_events/workspaces.yml5
-rw-r--r--lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb2
-rw-r--r--lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb17
-rw-r--r--lib/gitlab/utils.rb46
-rw-r--r--lib/gitlab/utils/markdown.rb2
-rw-r--r--lib/gitlab/utils/sanitize_node_link.rb2
-rw-r--r--lib/gitlab/verify/ci_secure_files.rb39
-rw-r--r--lib/gitlab/x509/tag.rb2
-rw-r--r--lib/google_api/cloud_platform/client.rb4
-rw-r--r--lib/google_cloud/authentication.rb20
-rw-r--r--lib/google_cloud/logging_service/logger.rb41
-rw-r--r--lib/grafana/validator.rb1
-rw-r--r--lib/kramdown/parser/atlassian_document_format.rb4
-rw-r--r--lib/object_storage/fog_helpers.rb51
-rw-r--r--lib/object_storage/pending_direct_upload.rb88
-rw-r--r--lib/product_analytics/settings.rb41
-rw-r--r--lib/quality/seeders/issues.rb2
-rw-r--r--lib/sidebars/groups/menus/packages_registries_menu.rb2
-rw-r--r--lib/sidebars/groups/super_sidebar_menus/deploy_menu.rb26
-rw-r--r--lib/sidebars/groups/super_sidebar_menus/operations_menu.rb3
-rw-r--r--lib/sidebars/groups/super_sidebar_menus/secure_menu.rb1
-rw-r--r--lib/sidebars/groups/super_sidebar_panel.rb1
-rw-r--r--lib/sidebars/projects/menus/deployments_menu.rb6
-rw-r--r--lib/sidebars/projects/menus/monitor_menu.rb18
-rw-r--r--lib/sidebars/projects/menus/packages_registries_menu.rb8
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb3
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/build_menu.rb3
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/deploy_menu.rb30
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb1
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/operations_menu.rb5
-rw-r--r--lib/sidebars/projects/super_sidebar_panel.rb1
-rw-r--r--lib/sidebars/user_profile/panel.rb3
-rw-r--r--lib/tasks/cache.rake32
-rw-r--r--lib/tasks/frontend.rake18
-rw-r--r--lib/tasks/gitlab/assets.rake1
-rw-r--r--lib/tasks/gitlab/background_migrations.rake2
-rw-r--r--lib/tasks/gitlab/backup.rake10
-rw-r--r--lib/tasks/gitlab/ci_secure_files/check.rake10
-rw-r--r--lib/tasks/gitlab/ci_secure_files/migrate.rake23
-rw-r--r--lib/tasks/gitlab/db.rake23
-rw-r--r--lib/tasks/gitlab/packages/events.rake5
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake10
-rw-r--r--lib/tasks/gitlab/usage_data.rake9
-rw-r--r--lib/tasks/tanuki_emoji.rake6
-rw-r--r--lib/tasks/tokens.rake9
450 files changed, 5815 insertions, 4714 deletions
diff --git a/lib/api/admin/batched_background_migrations.rb b/lib/api/admin/batched_background_migrations.rb
index 7e612b5b66a..c0d1ce8767d 100644
--- a/lib/api/admin/batched_background_migrations.rb
+++ b/lib/api/admin/batched_background_migrations.rb
@@ -142,9 +142,12 @@ module API
@base_model ||= Gitlab::Database.database_base_models[database]
end
+ # Force progress evaluation to occur now while we're using the right connection
def present_entity(result)
- present result,
- with: ::API::Entities::BatchedBackgroundMigration
+ representation = entity_representation_for(::API::Entities::BatchedBackgroundMigration, result, {})
+ json_representation = Gitlab::Json.dump(representation)
+
+ body Gitlab::Json::PrecompiledJson.new(json_representation)
end
end
end
diff --git a/lib/api/admin/dictionary.rb b/lib/api/admin/dictionary.rb
new file mode 100644
index 00000000000..038c122c021
--- /dev/null
+++ b/lib/api/admin/dictionary.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module API
+ module Admin
+ class Dictionary < ::API::Base
+ feature_category :database
+ urgency :low
+
+ before do
+ authenticated_as_admin!
+ end
+
+ namespace 'admin' do
+ resources 'databases/:database_name/dictionary/tables/:table_name' do
+ desc 'Retrieve dictionary details' do
+ success ::API::Entities::Dictionary::Table
+ failure [
+ { code: 401, message: '401 Unauthorized' },
+ { code: 403, message: '403 Forbidden' },
+ { code: 404, message: '404 Not found' }
+ ]
+ end
+ params do
+ requires :database_name,
+ type: String,
+ values: %w[main ci],
+ desc: 'The database name'
+
+ requires :table_name,
+ type: String,
+ desc: 'The table name'
+ end
+ get do
+ not_found!('Table not found') unless File.exist?(safe_file_path!)
+
+ present table_dictionary, with: Entities::Dictionary::Table
+ end
+ end
+
+ helpers do
+ def table_name
+ params[:table_name]
+ end
+
+ def table_dictionary
+ YAML.load_file(safe_file_path!).with_indifferent_access
+ end
+
+ def safe_file_path!
+ dir = Gitlab::Database::GitlabSchema.dictionary_paths.first.to_s
+ path = Rails.root.join(dir, "#{table_name}.yml").to_s
+
+ Gitlab::PathTraversal.check_allowed_absolute_path_and_path_traversal!(path, [dir])
+
+ path
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/admin/migrations.rb b/lib/api/admin/migrations.rb
new file mode 100644
index 00000000000..d4dbdbbb021
--- /dev/null
+++ b/lib/api/admin/migrations.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module API
+ module Admin
+ class Migrations < ::API::Base
+ feature_category :database
+ urgency :low
+
+ before do
+ authenticated_as_admin!
+ end
+
+ namespace 'admin' do
+ resources 'migrations/:timestamp/mark' do
+ desc 'Mark the migration as successfully executed' do
+ success [
+ { code: 201, message: '201 Created' }
+ ]
+ failure [
+ { code: 401, message: '401 Unauthorized' },
+ { code: 403, message: '403 Forbidden' },
+ { code: 404, message: '404 Not found' },
+ { code: 422, message: 'You can mark only pending migrations' }
+ ]
+ tags %w[migrations]
+ end
+ params do
+ optional :database,
+ type: String,
+ values: Gitlab::Database.all_database_names,
+ desc: 'The name of the database',
+ default: 'main'
+ requires :timestamp,
+ type: Integer,
+ desc: 'The migration version timestamp'
+ end
+ post do
+ response = Database::MarkMigrationService.new(
+ connection: base_model.connection,
+ version: params[:timestamp]
+ ).execute
+
+ if response.success?
+ created!
+ elsif response.reason == :not_found
+ not_found!
+ else
+ render_api_error!('You can mark only pending migrations', 422)
+ end
+ end
+ end
+ end
+
+ helpers do
+ def base_model
+ database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
+ @base_model ||= Gitlab::Database.database_base_models[database]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/admin/plan_limits.rb b/lib/api/admin/plan_limits.rb
index f1d7b56ad92..017c27cec95 100644
--- a/lib/api/admin/plan_limits.rb
+++ b/lib/api/admin/plan_limits.rb
@@ -60,15 +60,19 @@ module API
optional :ci_registered_group_runners, type: Integer, desc: 'Maximum number of runners registered per group'
optional :ci_registered_project_runners, type: Integer, desc: 'Maximum number of runners registered per project'
optional :conan_max_file_size, type: Integer, desc: 'Maximum Conan package file size in bytes'
+ optional :enforcement_limit, type: Integer,
+ desc: 'Maximum storage size for the root namespace enforcement in MiB'
optional :generic_packages_max_file_size, type: Integer, desc: 'Maximum generic package file size in bytes'
optional :helm_max_file_size, type: Integer, desc: 'Maximum Helm chart file size in bytes'
optional :maven_max_file_size, type: Integer, desc: 'Maximum Maven package file size in bytes'
+ optional :notification_limit, type: Integer,
+ desc: 'Maximum storage size for the root namespace notifications in MiB'
optional :npm_max_file_size, type: Integer, desc: 'Maximum NPM package file size in bytes'
optional :nuget_max_file_size, type: Integer, desc: 'Maximum NuGet package file size in bytes'
optional :pypi_max_file_size, type: Integer, desc: 'Maximum PyPI package file size in bytes'
optional :terraform_module_max_file_size, type: Integer,
desc: 'Maximum Terraform Module package file size in bytes'
- optional :storage_size_limit, type: Integer, desc: 'Maximum storage size for the root namespace in megabytes'
+ optional :storage_size_limit, type: Integer, desc: 'Maximum storage size for the root namespace in MiB'
optional :pipeline_hierarchy_size, type: Integer,
desc: "Maximum number of downstream pipelines in a pipeline's hierarchy tree"
end
@@ -76,7 +80,9 @@ module API
params = declared_params(include_missing: false)
plan = current_plan(params.delete(:plan_name))
- if plan.actual_limits.update(params)
+ result = ::Admin::PlanLimits::UpdateService.new(params, current_user: current_user, plan: plan).execute
+
+ if result[:status] == :success
present plan.actual_limits, with: Entities::PlanLimit
else
render_validation_error!(plan.actual_limits)
diff --git a/lib/api/api.rb b/lib/api/api.rb
index f50c705c3ea..090fbaa7f93 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -91,6 +91,10 @@ module API
end
after do
+ Gitlab::UsageDataCounters::JetBrainsBundledPluginActivityUniqueCounter.track_api_request_when_trackable(user_agent: request&.user_agent, user: @current_user)
+ end
+
+ after do
Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter.track_api_request_when_trackable(user_agent: request&.user_agent, user: @current_user)
end
@@ -135,7 +139,7 @@ module API
end
rescue_from Gitlab::Git::ResourceExhaustedError do |exception|
- rack_response({ 'message' => exception.message }.to_json, 429, exception.headers)
+ rack_response({ 'message' => exception.message }.to_json, 503, exception.headers)
end
rescue_from :all do |exception|
@@ -177,7 +181,9 @@ module API
mount ::API::AccessRequests
mount ::API::Admin::BatchedBackgroundMigrations
mount ::API::Admin::Ci::Variables
+ mount ::API::Admin::Dictionary
mount ::API::Admin::InstanceClusters
+ mount ::API::Admin::Migrations
mount ::API::Admin::PlanLimits
mount ::API::AlertManagementAlerts
mount ::API::Appearance
@@ -255,7 +261,9 @@ module API
mount ::API::Metadata
mount ::API::Metrics::Dashboard::Annotations
mount ::API::Metrics::UserStarredDashboards
+ mount ::API::MlModelPackages
mount ::API::Namespaces
+ mount ::API::NpmGroupPackages
mount ::API::NpmInstancePackages
mount ::API::NpmProjectPackages
mount ::API::NugetGroupPackages
@@ -321,7 +329,6 @@ module API
mount ::API::Ci::PipelineSchedules
mount ::API::Ci::SecureFiles
mount ::API::Discussions
- mount ::API::ErrorTracking::Collector
mount ::API::GroupBoards
mount ::API::GroupLabels
mount ::API::GroupMilestones
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 81a640d9a93..0aee0c70203 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -83,10 +83,7 @@ module API
private
def bypass_session_for_admin_mode?(user)
- return user.is_a?(User) && Gitlab::CurrentSettings.admin_mode if Feature.disabled?(:admin_mode_for_api)
-
- return false unless Gitlab::CurrentSettings.admin_mode
- return false unless user.is_a?(User)
+ return false unless user.is_a?(User) && Gitlab::CurrentSettings.admin_mode
Gitlab::Session.with_session(current_request.session) { Gitlab::Auth::CurrentUserMode.new(user).admin_mode? } ||
Gitlab::Auth::RequestAuthenticator.new(current_request).valid_access_token?(scopes: [:admin_mode])
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
index 020ba53b9ee..84c9f780a53 100644
--- a/lib/api/badges.rb
+++ b/lib/api/badges.rb
@@ -8,7 +8,7 @@ module API
helpers ::API::Helpers::BadgesHelpers
- feature_category :projects
+ feature_category :groups_and_projects
helpers do
def find_source_if_admin(source_type)
diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
index 7ca8b2df3dd..94c1942a244 100644
--- a/lib/api/ci/helpers/runner.rb
+++ b/lib/api/ci/helpers/runner.rb
@@ -146,6 +146,12 @@ module API
# noop: overridden in EE
end
+ def check_if_backoff_required!
+ return unless Gitlab::Database::Migrations::RunnerBackoff::Communicator.backoff_runner?
+
+ too_many_requests!('Executing database migrations. Please retry later.', retry_after: 1.minute)
+ end
+
private
def get_runner_config_from_request
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 6416de6d2a9..809a9bd781b 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -329,7 +329,8 @@ module API
post ':id/pipelines/:pipeline_id/cancel', urgency: :low, feature_category: :continuous_integration do
authorize! :update_pipeline, pipeline
- pipeline.cancel_running
+ # TODO: inconsistent behavior: when pipeline is not cancelable we should return an error
+ ::Ci::CancelPipelineService.new(pipeline: pipeline, current_user: current_user).execute
status 200
present pipeline.reset, with: Entities::Ci::Pipeline
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 0247ce301e2..25ac1780a36 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -7,6 +7,8 @@ module API
content_type :txt, 'text/plain'
+ before { check_if_backoff_required! }
+
resource :runners do
desc 'Register a new runner' do
detail "Register a new runner for the instance"
diff --git a/lib/api/ci/secure_files.rb b/lib/api/ci/secure_files.rb
index 41faaf80c82..02f625f2130 100644
--- a/lib/api/ci/secure_files.rb
+++ b/lib/api/ci/secure_files.rb
@@ -6,6 +6,7 @@ module API
include PaginationParams
before do
+ check_api_enabled!
authenticate!
authorize! :read_secure_files, user_project
end
@@ -64,7 +65,7 @@ module API
resource do
before do
- read_only_feature_flag_enabled?
+ check_read_only_feature_flag_enabled!
authorize! :admin_secure_files, user_project
end
@@ -81,7 +82,7 @@ module API
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
post ':id/secure_files' do
secure_file = user_project.secure_files.new(
- name: Gitlab::Utils.check_path_traversal!(params[:name])
+ name: Gitlab::PathTraversal.check_path_traversal!(params[:name])
)
secure_file.file = params[:file]
@@ -112,7 +113,11 @@ module API
end
helpers do
- def read_only_feature_flag_enabled?
+ def check_api_enabled!
+ forbidden! unless Gitlab.config.ci_secure_files.enabled
+ end
+
+ def check_read_only_feature_flag_enabled!
service_unavailable! if Feature.enabled?(:ci_secure_files_read_only, user_project, type: :ops)
end
end
diff --git a/lib/api/concerns/packages/nuget/private_endpoints.rb b/lib/api/concerns/packages/nuget/private_endpoints.rb
new file mode 100644
index 00000000000..20c02f0a285
--- /dev/null
+++ b/lib/api/concerns/packages/nuget/private_endpoints.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+#
+# NuGet Package Manager Client API
+#
+# These API endpoints are not consumed directly by users, so there is no documentation for the
+# individual endpoints. They are called by the NuGet package manager client when users run commands
+# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here:
+# https://docs.gitlab.com/ee/user/packages/nuget_repository/
+#
+# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
+module API
+ module Concerns
+ module Packages
+ module Nuget
+ module PrivateEndpoints
+ extend ActiveSupport::Concern
+
+ POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}
+ NON_NEGATIVE_INTEGER_REGEX = %r{\A(0|[1-9]\d*)\z}
+
+ included do
+ helpers do
+ def find_packages(package_name)
+ packages = package_finder(package_name).execute
+
+ not_found!('Packages') unless packages.exists?
+
+ packages
+ end
+
+ def find_package(package_name, package_version)
+ package = package_finder(package_name, package_version).execute
+ .first
+
+ not_found!('Package') unless package
+
+ package
+ end
+
+ def package_finder(package_name, package_version = nil)
+ ::Packages::Nuget::PackageFinder.new(
+ current_user,
+ project_or_group,
+ package_name: package_name,
+ package_version: package_version
+ )
+ end
+
+ def search_packages(_search_term, search_options)
+ ::Packages::Nuget::SearchService
+ .new(current_user, project_or_group, params[:q], search_options)
+ .execute
+ end
+ end
+
+ # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
+ params do
+ requires :package_name, type: String, desc: 'The NuGet package name',
+ regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'MyNuGetPkg' }
+ end
+ namespace '/metadata/*package_name' do
+ after_validation do
+ authorize_packages_access!(project_or_group, required_permission)
+ end
+
+ desc 'The NuGet Metadata Service - Package name level' do
+ detail 'This feature was introduced in GitLab 12.8'
+ success code: 200, model: ::API::Entities::Nuget::PackagesMetadata
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
+ end
+ get 'index', format: :json, urgency: :low do
+ present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages(params[:package_name])),
+ with: ::API::Entities::Nuget::PackagesMetadata
+ end
+
+ desc 'The NuGet Metadata Service - Package name and version level' do
+ detail 'This feature was introduced in GitLab 12.8'
+ success code: 200, model: ::API::Entities::Nuget::PackageMetadata
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
+ end
+ params do
+ requires :package_version, type: String, desc: 'The NuGet package version',
+ regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: '1.0.0' }
+ end
+ get '*package_version', format: :json, urgency: :low do
+ present ::Packages::Nuget::PackageMetadataPresenter.new(
+ find_package(params[:package_name],
+ params[:package_version])
+ ),
+ with: ::API::Entities::Nuget::PackageMetadata
+ end
+ end
+
+ # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
+ params do
+ optional :q, type: String, desc: 'The search term', documentation: { example: 'MyNuGet' }
+ optional :skip, type: Integer, desc: 'The number of results to skip', default: 0,
+ regexp: NON_NEGATIVE_INTEGER_REGEX, documentation: { example: 1 }
+ optional :take, type: Integer, desc: 'The number of results to return',
+ default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX, documentation: { example: 1 }
+ optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true
+ end
+ namespace '/query' do
+ after_validation do
+ authorize_packages_access!(project_or_group, required_permission)
+ end
+
+ desc 'The NuGet Search Service' do
+ detail 'This feature was introduced in GitLab 12.8'
+ success code: 200, model: ::API::Entities::Nuget::SearchResults
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
+ end
+ get format: :json, urgency: :low do
+ search_options = {
+ include_prerelease_versions: params[:prerelease],
+ per_page: params[:take],
+ padding: params[:skip]
+ }
+
+ results = search_packages(params[:q], search_options)
+
+ track_package_event(
+ 'search_package',
+ :nuget,
+ **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages')
+ )
+
+ present ::Packages::Nuget::SearchResultsPresenter.new(results),
+ with: ::API::Entities::Nuget::SearchResults
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/concerns/packages/nuget/public_endpoints.rb b/lib/api/concerns/packages/nuget/public_endpoints.rb
new file mode 100644
index 00000000000..37b503212d9
--- /dev/null
+++ b/lib/api/concerns/packages/nuget/public_endpoints.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+# NuGet Package Manager Client API
+
+# These API endpoints are not consumed directly by users, so there is no documentation for the
+# individual endpoints. They are called by the NuGet package manager client when users run commands
+# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here:
+# https://docs.gitlab.com/ee/user/packages/nuget_repository/
+
+module API
+ module Concerns
+ module Packages
+ module Nuget
+ module PublicEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ # https://docs.microsoft.com/en-us/nuget/api/service-index
+ desc 'The NuGet Service Index' do
+ detail 'This feature was introduced in GitLab 12.6'
+ success code: 200, model: ::API::Entities::Nuget::ServiceIndex
+ failure [
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
+ end
+ get 'index', format: :json, urgency: :default do
+ track_package_event(
+ 'cli_metadata',
+ :nuget,
+ **snowplow_gitlab_standard_context_without_auth.merge(category: 'API::NugetPackages')
+ )
+
+ present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group_without_auth),
+ with: ::API::Entities::Nuget::ServiceIndex
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/concerns/packages/nuget_endpoints.rb b/lib/api/concerns/packages/nuget_endpoints.rb
deleted file mode 100644
index 5f32f0544f4..00000000000
--- a/lib/api/concerns/packages/nuget_endpoints.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-# frozen_string_literal: true
-#
-# NuGet Package Manager Client API
-#
-# These API endpoints are not consumed directly by users, so there is no documentation for the
-# individual endpoints. They are called by the NuGet package manager client when users run commands
-# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here:
-# https://docs.gitlab.com/ee/user/packages/nuget_repository/
-#
-# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
-module API
- module Concerns
- module Packages
- module NugetEndpoints
- extend ActiveSupport::Concern
-
- POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze
- NON_NEGATIVE_INTEGER_REGEX = %r{\A(0|[1-9]\d*)\z}.freeze
-
- included do
- helpers do
- def find_packages(package_name)
- packages = package_finder(package_name).execute
-
- not_found!('Packages') unless packages.exists?
-
- packages
- end
-
- def find_package(package_name, package_version)
- package = package_finder(package_name, package_version).execute
- .first
-
- not_found!('Package') unless package
-
- package
- end
-
- def package_finder(package_name, package_version = nil)
- ::Packages::Nuget::PackageFinder.new(
- current_user,
- project_or_group,
- package_name: package_name,
- package_version: package_version
- )
- end
-
- def search_packages(search_term, search_options)
- ::Packages::Nuget::SearchService
- .new(current_user, project_or_group, params[:q], search_options)
- .execute
- end
- end
-
- # https://docs.microsoft.com/en-us/nuget/api/service-index
- desc 'The NuGet Service Index' do
- detail 'This feature was introduced in GitLab 12.6'
- success code: 200, model: ::API::Entities::Nuget::ServiceIndex
- failure [
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
- end
- get 'index', format: :json, urgency: :default do
- authorize_packages_access!(project_or_group, required_permission)
-
- track_package_event('cli_metadata', :nuget, **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages'))
-
- present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group),
- with: ::API::Entities::Nuget::ServiceIndex
- end
-
- # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
- params do
- requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'MyNuGetPkg' }
- end
- namespace '/metadata/*package_name' do
- after_validation do
- authorize_packages_access!(project_or_group, required_permission)
- end
-
- desc 'The NuGet Metadata Service - Package name level' do
- detail 'This feature was introduced in GitLab 12.8'
- success code: 200, model: ::API::Entities::Nuget::PackagesMetadata
- failure [
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
- end
- get 'index', format: :json, urgency: :low do
- present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages(params[:package_name])),
- with: ::API::Entities::Nuget::PackagesMetadata
- end
-
- desc 'The NuGet Metadata Service - Package name and version level' do
- detail 'This feature was introduced in GitLab 12.8'
- success code: 200, model: ::API::Entities::Nuget::PackageMetadata
- failure [
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
- end
- params do
- requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: '1.0.0' }
- end
- get '*package_version', format: :json, urgency: :low do
- present ::Packages::Nuget::PackageMetadataPresenter.new(find_package(params[:package_name], params[:package_version])),
- with: ::API::Entities::Nuget::PackageMetadata
- end
- end
-
- # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
- params do
- optional :q, type: String, desc: 'The search term', documentation: { example: 'MyNuGet' }
- optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX, documentation: { example: 1 }
- optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX, documentation: { example: 1 }
- optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true
- end
- namespace '/query' do
- after_validation do
- authorize_packages_access!(project_or_group, required_permission)
- end
-
- desc 'The NuGet Search Service' do
- detail 'This feature was introduced in GitLab 12.8'
- success code: 200, model: ::API::Entities::Nuget::SearchResults
- failure [
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
- end
- get format: :json, urgency: :low do
- search_options = {
- include_prerelease_versions: params[:prerelease],
- per_page: params[:take],
- padding: params[:skip]
- }
-
- results = search_packages(params[:q], search_options)
-
- track_package_event('search_package', :nuget, **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages'))
-
- present ::Packages::Nuget::SearchResultsPresenter.new(results),
- with: ::API::Entities::Nuget::SearchResults
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb
index 4f78ac926d8..e1531847b87 100644
--- a/lib/api/debian_project_packages.rb
+++ b/lib/api/debian_project_packages.rb
@@ -19,6 +19,10 @@ module API
def project_or_group
authorized_user_project(action: :read_package)
end
+
+ def end_of_new_upload?
+ params[:distribution].present? || params[:file_name].end_with?('.changes')
+ end
end
after_validation do
@@ -97,7 +101,7 @@ module API
component: params['component']
}
- package = if params[:distribution].present?
+ package = if end_of_new_upload?
::Packages::CreateTemporaryPackageService.new(
project_or_group, current_user, declared_params.merge(build: current_authenticated_job)
).execute(:debian, name: ::Packages::Debian::TEMPORARY_PACKAGE_NAME)
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 634d6052b99..f8379392531 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -41,8 +41,10 @@ module API
authenticated_as_admin!
deploy_keys = params[:public] ? DeployKey.are_public : DeployKey.all
+ deploy_keys = deploy_keys.including_projects_with_write_access.including_projects_with_readonly_access
- present paginate(deploy_keys.including_projects_with_write_access), with: Entities::DeployKey, include_projects_with_write_access: true
+ present paginate(deploy_keys),
+ with: Entities::DeployKey, include_projects_with_write_access: true, include_projects_with_readonly_access: true
end
params do
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 768ffac41ce..45466a1894c 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -122,7 +122,7 @@ module API
note = create_note(noteable, opts)
- if note.valid?
+ if note.persisted?
present note.discussion, with: Entities::Discussion
else
bad_request!("Note #{note.errors.messages}")
@@ -175,7 +175,7 @@ module API
}
note = create_note(noteable, opts)
- if note.valid?
+ if note.persisted?
present note, with: Entities::Note
else
bad_request!("Note #{note.errors.messages}")
diff --git a/lib/api/entities/deploy_key.rb b/lib/api/entities/deploy_key.rb
index 1bcd06f2c88..0e82d9abb63 100644
--- a/lib/api/entities/deploy_key.rb
+++ b/lib/api/entities/deploy_key.rb
@@ -14,6 +14,7 @@ module API
documentation: { type: 'string', example: 'SHA256:Jrs3LD1Ji30xNLtTVf9NDCj7kkBgPBb2pjvTZ3HfIgU' }
expose :projects_with_write_access, using: Entities::ProjectIdentity, if: -> (_, options) { options[:include_projects_with_write_access] }
+ expose :projects_with_readonly_access, using: Entities::ProjectIdentity, if: -> (_, options) { options[:include_projects_with_readonly_access] }
end
end
end
diff --git a/lib/api/entities/dictionary/table.rb b/lib/api/entities/dictionary/table.rb
new file mode 100644
index 00000000000..8d4e3fb959d
--- /dev/null
+++ b/lib/api/entities/dictionary/table.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Dictionary
+ class Table < Grape::Entity
+ expose :table_name, documentation: { type: :string, example: 'users' }
+ expose :feature_categories, documentation: { type: :array, example: ['database'] }
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/draft_note.rb b/lib/api/entities/draft_note.rb
index 70b32bac502..13852513615 100644
--- a/lib/api/entities/draft_note.rb
+++ b/lib/api/entities/draft_note.rb
@@ -38,7 +38,7 @@ module API
}
}
} do |note|
- note.position.to_h
+ note.position.to_h.except(:ignore_whitespace_change)
end
end
end
diff --git a/lib/api/entities/error_tracking.rb b/lib/api/entities/error_tracking.rb
index 5e3b983c58c..180293a444d 100644
--- a/lib/api/entities/error_tracking.rb
+++ b/lib/api/entities/error_tracking.rb
@@ -21,7 +21,7 @@ module API
expose :id, documentation: { type: 'integer', example: 1 }
expose :active, documentation: { type: 'boolean' }
expose :public_key, documentation: { type: 'string', example: 'glet_aa77551d849c083f76d0bc545ed053a3' }
- expose :sentry_dsn, documentation: { type: 'string', example: 'https://glet_aa77551d849c083f76d0bc545ed053a3@gitlab.example.com/api/v4/error_tracking/collector/5' }
+ expose :sentry_dsn, documentation: { type: 'string', example: 'https://glet_aa77551d849c083f76d0bc545ed053a3@example.com/errortracking/api/v1/projects/5' }
end
end
end
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
index f796aeba17f..adff7f87cd3 100644
--- a/lib/api/entities/merge_request_basic.rb
+++ b/lib/api/entities/merge_request_basic.rb
@@ -68,6 +68,7 @@ module API
expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
+ expose :prepared_at
with_options if: -> (merge_request, _) { merge_request.for_fork? } do
expose :allow_collaboration
diff --git a/lib/api/entities/namespace.rb b/lib/api/entities/namespace.rb
index 15bc7d158c4..5e0630e0f7f 100644
--- a/lib/api/entities/namespace.rb
+++ b/lib/api/entities/namespace.rb
@@ -10,6 +10,14 @@ module API
def expose_members_count_with_descendants?(namespace, opts)
namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace)
end
+
+ expose :root_repository_size, documentation: { type: 'integer', example: 123 }, if: -> (namespace, opts) { expose_root_repository_size?(namespace, opts) } do |namespace, _|
+ namespace.root_storage_statistics&.repository_size
+ end
+
+ def expose_root_repository_size?(namespace, opts)
+ namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace)
+ end
end
end
end
diff --git a/lib/api/entities/note.rb b/lib/api/entities/note.rb
index cac4a8280e3..6ed5ca43fbb 100644
--- a/lib/api/entities/note.rb
+++ b/lib/api/entities/note.rb
@@ -18,7 +18,7 @@ module API
expose :commit_id, if: ->(note, options) { note.noteable_type == "MergeRequest" && note.is_a?(DiffNote) }
expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note|
- note.position.to_h
+ note.position.to_h.except(:ignore_whitespace_change)
end
expose :resolvable?, as: :resolvable
diff --git a/lib/api/entities/nuget/metadatum.rb b/lib/api/entities/nuget/metadatum.rb
index 256b916cb64..c316dfce740 100644
--- a/lib/api/entities/nuget/metadatum.rb
+++ b/lib/api/entities/nuget/metadatum.rb
@@ -4,6 +4,12 @@ module API
module Entities
module Nuget
class Metadatum < Grape::Entity
+ expose :authors, documentation: { type: 'string', example: 'Authors' } do |metadatum|
+ metadatum[:authors] || ''
+ end
+ expose :description, as: :summary, documentation: { type: 'string', example: 'Description' } do |metadatum|
+ metadatum[:description] || ''
+ end
expose :project_url, as: :projectUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/project' }
expose :license_url, as: :licenseUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/license' }
expose :icon_url, as: :iconUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/icon' }
diff --git a/lib/api/entities/nuget/package_metadata_catalog_entry.rb b/lib/api/entities/nuget/package_metadata_catalog_entry.rb
index ce328c5a5ca..b6e768e5083 100644
--- a/lib/api/entities/nuget/package_metadata_catalog_entry.rb
+++ b/lib/api/entities/nuget/package_metadata_catalog_entry.rb
@@ -5,16 +5,15 @@ module API
module Nuget
class PackageMetadataCatalogEntry < Grape::Entity
expose :json_url, as: :@id, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/1/packages/nuget/metadata/MyNuGetPkg/1.3.0.17.json' }
- expose :authors, documentation: { type: 'string', example: 'Author' }
expose :dependency_groups, as: :dependencyGroups, using: ::API::Entities::Nuget::DependencyGroup,
documentation: { is_array: true, type: 'API::Entities::Nuget::DependencyGroup' }
expose :package_name, as: :id, documentation: { type: 'string', example: 'MyNuGetPkg' }
expose :package_version, as: :version, documentation: { type: 'string', example: '1.3.0.17' }
expose :tags, documentation: { type: 'string', example: 'tag#1 tag#2' }
expose :archive_url, as: :packageContent, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg' }
- expose :summary, documentation: { type: 'string', example: 'Summary' }
expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true,
documentation: { type: 'API::Entities::Nuget::Metadatum' }
+ expose :published, documentation: { type: 'string', example: '2023-05-08T17:23:25Z' }
end
end
end
diff --git a/lib/api/entities/nuget/search_result.rb b/lib/api/entities/nuget/search_result.rb
index bb3698de30b..303efa7718e 100644
--- a/lib/api/entities/nuget/search_result.rb
+++ b/lib/api/entities/nuget/search_result.rb
@@ -5,10 +5,8 @@ module API
module Nuget
class SearchResult < Grape::Entity
expose :type, as: :@type, documentation: { type: 'string', example: 'Package' }
- expose :authors, documentation: { type: 'string', example: 'Author' }
expose :name, as: :id, documentation: { type: 'string', example: 'MyNuGetPkg' }
expose :name, as: :title, documentation: { type: 'string', example: 'MyNuGetPkg' }
- expose :summary, documentation: { type: 'string', example: 'Summary' }
expose :total_downloads, as: :totalDownloads, documentation: { type: 'integer', example: 1 }
expose :verified, documentation: { type: 'boolean' }
expose :version, documentation: { type: 'string', example: '1.3.0.17' }
diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb
index ab6cc0fcb0a..5831fe68a5d 100644
--- a/lib/api/entities/package.rb
+++ b/lib/api/entities/package.rb
@@ -43,7 +43,7 @@ module API
end
expose :tags
- expose :pipeline, if: ->(package) { package.original_build_info }, using: Package::Pipeline
+ expose :pipeline, if: ->(package) { package.last_build_info }, using: Package::Pipeline
expose :pipelines, if: ->(package) { package.pipelines.present? }, using: Package::Pipeline
expose :versions, using: ::API::Entities::PackageVersion, unless: ->(_, opts) { opts[:collection] }
diff --git a/lib/api/entities/package_version.rb b/lib/api/entities/package_version.rb
index 82522d3f423..417d8755144 100644
--- a/lib/api/entities/package_version.rb
+++ b/lib/api/entities/package_version.rb
@@ -8,7 +8,7 @@ module API
expose :created_at
expose :tags
- expose :pipeline, if: ->(package) { package.original_build_info }, using: Package::Pipeline
+ expose :pipeline, if: ->(package) { package.last_build_info }, using: Package::Pipeline
end
end
end
diff --git a/lib/api/entities/plan_limit.rb b/lib/api/entities/plan_limit.rb
index b5cff2bb73c..753c595d65f 100644
--- a/lib/api/entities/plan_limit.rb
+++ b/lib/api/entities/plan_limit.rb
@@ -11,9 +11,11 @@ module API
expose :ci_registered_group_runners, documentation: { type: 'integer', example: 1000 }
expose :ci_registered_project_runners, documentation: { type: 'integer', example: 1000 }
expose :conan_max_file_size, documentation: { type: 'integer', example: 3221225472 }
+ expose :enforcement_limit, documentation: { type: 'integer', example: 15000 }
expose :generic_packages_max_file_size, documentation: { type: 'integer', example: 5368709120 }
expose :helm_max_file_size, documentation: { type: 'integer', example: 5242880 }
expose :maven_max_file_size, documentation: { type: 'integer', example: 3221225472 }
+ expose :notification_limit, documentation: { type: 'integer', example: 15000 }
expose :npm_max_file_size, documentation: { type: 'integer', example: 524288000 }
expose :nuget_max_file_size, documentation: { type: 'integer', example: 524288000 }
expose :pipeline_hierarchy_size, documentation: { type: 'integer', example: 1000 }
diff --git a/lib/api/entities/project_integration_basic.rb b/lib/api/entities/project_integration_basic.rb
index b7c56d7cca1..d7e111b990e 100644
--- a/lib/api/entities/project_integration_basic.rb
+++ b/lib/api/entities/project_integration_basic.rb
@@ -15,9 +15,11 @@ module API
expose :push_events, documentation: { type: 'boolean' }
expose :issues_events, documentation: { type: 'boolean' }
expose :incident_events, documentation: { type: 'boolean' }
+ expose :alert_events, documentation: { type: 'boolean' }
expose :confidential_issues_events, documentation: { type: 'boolean' }
expose :merge_requests_events, documentation: { type: 'boolean' }
expose :tag_push_events, documentation: { type: 'boolean' }
+ expose :deployment_events, documentation: { type: 'boolean' }
expose :note_events, documentation: { type: 'boolean' }
expose :confidential_note_events, documentation: { type: 'boolean' }
expose :pipeline_events, documentation: { type: 'boolean' }
diff --git a/lib/api/entities/project_scope_link.rb b/lib/api/entities/project_scope_link.rb
new file mode 100644
index 00000000000..8a5466a0418
--- /dev/null
+++ b/lib/api/entities/project_scope_link.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectScopeLink < Grape::Entity
+ expose :source_project_id, documentation: { type: 'integer' }
+ expose :target_project_id, documentation: { type: 'integer' }
+ end
+ end
+end
diff --git a/lib/api/error_tracking/collector.rb b/lib/api/error_tracking/collector.rb
deleted file mode 100644
index e10125e02c6..00000000000
--- a/lib/api/error_tracking/collector.rb
+++ /dev/null
@@ -1,156 +0,0 @@
-# frozen_string_literal: true
-
-module API
- # This API is responsible for collecting error tracking information
- # from sentry client. It allows us to use GitLab as an alternative to
- # 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'
- content_type :txt, 'text/plain'
- default_format :envelope
-
- rescue_from Gitlab::ErrorTracking::ErrorRepository::DatabaseError do |e|
- render_api_error!(e.message, 400)
- end
-
- before do
- not_found!('Project') unless project
- not_found! unless feature_enabled?
- not_found! unless active_client_key?
- end
-
- helpers do
- def project
- @project ||= find_project(params[:id])
- end
-
- def feature_enabled?
- Feature.enabled?(:integrated_error_tracking, project) &&
- project.error_tracking_setting&.integrated_enabled?
- end
-
- def find_client_key(public_key)
- return unless public_key.present?
-
- project.error_tracking_client_keys.active.find_by_public_key(public_key)
- end
-
- def active_client_key?
- public_key = extract_public_key
-
- find_client_key(public_key)
- end
-
- def extract_public_key
- # Some SDK send public_key as a param. In this case we don't need to parse headers.
- return params[:sentry_key] if params[:sentry_key].present?
-
- begin
- ::ErrorTracking::Collector::SentryAuthParser.parse(request)[:public_key]
- rescue StandardError
- bad_request!('Failed to parse sentry request')
- end
- end
-
- def validate_payload(payload)
- unless ::ErrorTracking::Collector::PayloadValidator.new.valid?(payload)
- bad_request!('Unsupported sentry payload')
- end
- end
- end
-
- desc 'Submit error tracking event to the project as envelope' do
- detail 'This feature was introduced in GitLab 14.1.'
- end
- params do
- requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
- end
- post 'error_tracking/collector/api/:id/envelope' do
- # There is a reason why we have such uncommon path.
- # We depend on a client side error tracking software which
- # modifies URL for its own reasons.
- #
- # When we give user a URL like this
- # HOST/api/v4/error_tracking/collector/123
- #
- # Then error tracking software will convert it like this:
- # HOST/api/v4/error_tracking/collector/api/123/envelope/
-
- begin
- parsed_request = ::ErrorTracking::Collector::SentryRequestParser.parse(request)
- rescue StandardError
- bad_request!('Failed to parse sentry request')
- end
-
- type = parsed_request[:request_type]
-
- # Sentry sends 2 requests on each exception: transaction and event.
- # Everything else is not a desired behavior.
- unless type == 'transaction' || type == 'event'
- render_api_error!('400 Bad Request', 400)
-
- break
- end
-
- # We don't have use for transaction request yet,
- # so we record only event one.
- if type == 'event'
- validate_payload(parsed_request[:event])
-
- ::ErrorTracking::CollectErrorService
- .new(project, nil, event: parsed_request[:event])
- .execute
- end
-
- # Collector should never return any information back.
- # Because DSN and public key are designed for public use,
- # it is safe only for submission of new events.
- #
- # Some clients sdk require status 200 OK to work correctly.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/343531.
- status 200
- end
-
- desc 'Submit error tracking event to the project' do
- detail 'This feature was introduced in GitLab 14.1.'
- end
- params do
- requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
- end
- post 'error_tracking/collector/api/:id/store' do
- # There is a reason why we have such uncommon path.
- # We depend on a client side error tracking software which
- # modifies URL for its own reasons.
- #
- # When we give user a URL like this
- # HOST/api/v4/error_tracking/collector/123
- #
- # Then error tracking software will convert it like this:
- # HOST/api/v4/error_tracking/collector/api/123/store/
-
- begin
- parsed_body = Gitlab::Json.parse(request.body.read)
- rescue StandardError
- bad_request!('Failed to parse sentry request')
- end
-
- validate_payload(parsed_body)
-
- ::ErrorTracking::CollectErrorService
- .new(project, nil, event: parsed_body)
- .execute
-
- # Collector should never return any information back.
- # Because DSN and public key are designed for public use,
- # it is safe only for submission of new events.
- #
- # Some clients sdk require status 200 OK to work correctly.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/343531.
- status 200
- end
- end
-end
diff --git a/lib/api/group_avatar.rb b/lib/api/group_avatar.rb
index 0820011fd89..eeec23f27ab 100644
--- a/lib/api/group_avatar.rb
+++ b/lib/api/group_avatar.rb
@@ -4,7 +4,7 @@ module API
class GroupAvatar < ::API::Base
helpers Helpers::GroupsHelpers
- feature_category :subgroups
+ feature_category :groups_and_projects
params do
requires :id, type: String, desc: 'The ID of the group'
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index e13b661b357..1a2314d41f0 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -9,7 +9,7 @@ module API
helpers Helpers::GroupsHelpers
- feature_category :subgroups, ['/groups/:id/custom_attributes', '/groups/:id/custom_attributes/:key']
+ feature_category :groups_and_projects, ['/groups/:id/custom_attributes', '/groups/:id/custom_attributes/:key']
helpers do
params :statistics_params do
@@ -198,7 +198,7 @@ module API
use :group_list_params
use :with_custom_attributes
end
- get feature_category: :subgroups do
+ get feature_category: :groups_and_projects do
groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups_with_pagination_strategies params, groups
end
@@ -214,7 +214,7 @@ module API
use :optional_params
end
- post feature_category: :subgroups, urgency: :low do
+ post feature_category: :groups_and_projects, urgency: :low do
parent_group = find_group!(params[:parent_id]) if params[:parent_id].present?
if parent_group
authorize! :create_subgroup, parent_group
@@ -248,7 +248,7 @@ module API
use :optional_update_params
use :optional_update_params_ee
end
- put ':id', feature_category: :subgroups, urgency: :low do
+ put ':id', feature_category: :groups_and_projects, urgency: :low do
group = find_group!(params[:id])
group.preload_shared_group_links
@@ -272,7 +272,7 @@ module API
optional :with_projects, type: Boolean, default: true, desc: 'Omit project details'
end
# TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357841
- get ":id", feature_category: :subgroups, urgency: :low do
+ get ":id", feature_category: :groups_and_projects, urgency: :low do
group = find_group!(params[:id])
group.preload_shared_group_links
@@ -282,7 +282,7 @@ module API
desc 'Remove a group.' do
tags %w[groups]
end
- delete ":id", feature_category: :subgroups, urgency: :low do
+ delete ":id", feature_category: :groups_and_projects, urgency: :low do
group = find_group!(params[:id])
authorize! :admin_group, group
check_subscription! group
@@ -320,7 +320,7 @@ module API
use :optional_projects_params
end
# TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/211498
- get ":id/projects", feature_category: :subgroups, urgency: :low do
+ get ":id/projects", feature_category: :groups_and_projects, urgency: :low do
finder_options = {
only_owned: !params[:with_shared],
include_subgroups: params[:include_subgroups],
@@ -356,7 +356,7 @@ module API
use :pagination
use :with_custom_attributes
end
- get ":id/projects/shared", feature_category: :subgroups do
+ get ":id/projects/shared", feature_category: :groups_and_projects do
projects = find_group_projects(params, { only_shared: true })
present_projects(params, projects)
@@ -371,7 +371,7 @@ module API
use :group_list_params
use :with_custom_attributes
end
- get ":id/subgroups", feature_category: :subgroups, urgency: :low do
+ get ":id/subgroups", feature_category: :groups_and_projects, urgency: :low do
groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups params, groups
end
@@ -385,7 +385,7 @@ module API
use :group_list_params
use :with_custom_attributes
end
- get ":id/descendant_groups", feature_category: :subgroups, urgency: :low do
+ get ":id/descendant_groups", feature_category: :groups_and_projects, urgency: :low do
finder_params = declared_params(include_missing: false).merge(include_parent_descendants: true)
groups = find_groups(finder_params, params[:id])
present_groups params, groups
@@ -398,7 +398,7 @@ module API
params do
requires :project_id, type: String, desc: 'The ID or path of the project'
end
- post ":id/projects/:project_id", requirements: { project_id: /.+/ }, feature_category: :projects do
+ post ":id/projects/:project_id", requirements: { project_id: /.+/ }, feature_category: :groups_and_projects do
authenticated_as_admin!
group = find_group!(params[:id])
group.preload_shared_group_links
@@ -421,7 +421,7 @@ module API
optional :search, type: String, desc: 'Return list of namespaces matching the search criteria'
use :pagination
end
- get ':id/transfer_locations', feature_category: :subgroups do
+ get ':id/transfer_locations', feature_category: :groups_and_projects do
authorize! :admin_group, user_group
args = declared_params(include_missing: false)
@@ -440,7 +440,7 @@ module API
desc: 'The ID of the target group to which the group needs to be transferred to.'\
'If not provided, the source group will be promoted to a root group.'
end
- post ':id/transfer', feature_category: :subgroups do
+ post ':id/transfer', feature_category: :groups_and_projects do
group = find_group!(params[:id])
authorize! :admin_group, group
@@ -465,7 +465,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, urgency: :low do
+ post ":id/share", feature_category: :groups_and_projects, urgency: :low do
shared_with_group = find_group!(params[:group_id])
group_link_create_params = {
@@ -487,7 +487,7 @@ module API
requires :group_id, type: Integer, desc: 'The ID of the shared group'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete ":id/share/:group_id", feature_category: :subgroups do
+ delete ":id/share/:group_id", feature_category: :groups_and_projects do
shared_group = find_group!(params[:id])
link = shared_group.shared_with_group_links.find_by(shared_with_group_id: params[:group_id])
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 9fa0923d914..df080c8e666 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -20,6 +20,10 @@ module API
API_RESPONSE_STATUS_CODE = 'gitlab.api.response_status_code'
INTEGER_ID_REGEX = /^-?\d+$/.freeze
+ def logger
+ API.logger
+ end
+
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
declared(params, options).to_h.symbolize_keys
@@ -202,6 +206,7 @@ module API
not_found!('Namespace')
end
+ # find_namespace returns the namespace regardless of user access level on the namespace
# rubocop: disable CodeReuse/ActiveRecord
def find_namespace(id)
if id.to_s =~ INTEGER_ID_REGEX
@@ -212,6 +217,8 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ # find_namespace! returns the namespace if the current user can read the given namespace
+ # Otherwise, returns a not_found! error
def find_namespace!(id)
check_namespace_access(find_namespace(id))
end
@@ -486,6 +493,12 @@ module API
render_api_error!('413 Request Entity Too Large', 413)
end
+ def too_many_requests!(message = nil, retry_after: 1.minute)
+ header['Retry-After'] = retry_after.to_i if retry_after
+
+ render_api_error!(message || '429 Too Many Requests', 429)
+ end
+
def not_modified!
render_api_error!('304 Not Modified', 304)
end
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index 4c37a2a5aba..850cc61af2c 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -69,6 +69,12 @@ module API
},
{
required: false,
+ name: :alert_channel,
+ type: String,
+ desc: 'The name of the channel to receive alert_events notifications'
+ },
+ {
+ required: false,
name: :confidential_issue_channel,
type: String,
desc: 'The name of the channel to receive confidential_issues_events notifications'
@@ -87,12 +93,24 @@ module API
},
{
required: false,
+ name: :confidential_note_channel,
+ type: String,
+ desc: 'The name of the channel to receive confidential_note_events notifications'
+ },
+ {
+ required: false,
name: :tag_push_channel,
type: String,
desc: 'The name of the channel to receive tag_push_events notifications'
},
{
required: false,
+ name: :deployment_channel,
+ type: String,
+ desc: 'The name of the channel to receive deployment_events notifications'
+ },
+ {
+ required: false,
name: :pipeline_channel,
type: String,
desc: 'The name of the channel to receive pipeline_events notifications'
@@ -110,6 +128,12 @@ module API
[
{
required: false,
+ name: :commit_events,
+ type: Boolean,
+ desc: 'Enable notifications for commit_events'
+ },
+ {
+ required: false,
name: :push_events,
type: Boolean,
desc: 'Enable notifications for push_events'
@@ -128,6 +152,12 @@ module API
},
{
required: false,
+ name: :alert_events,
+ type: Boolean,
+ desc: 'Enable notifications for alert_events'
+ },
+ {
+ required: false,
name: :confidential_issues_events,
type: Boolean,
desc: 'Enable notifications for confidential_issues_events'
@@ -158,6 +188,18 @@ module API
},
{
required: false,
+ name: :deployment_events,
+ type: Boolean,
+ desc: 'Enable notifications for deployment_events'
+ },
+ {
+ required: false,
+ name: :job_events,
+ type: Boolean,
+ desc: 'Enable notifications for job_events'
+ },
+ {
+ required: false,
name: :pipeline_events,
type: Boolean,
desc: 'Enable notifications for pipeline_events'
@@ -197,6 +239,12 @@ module API
name: :app_store_private_key_file_name,
type: String,
desc: 'The Apple App Store Connect Private Key File Name'
+ },
+ {
+ required: false,
+ name: :app_store_protected_refs,
+ type: Boolean,
+ desc: 'Only enable for protected refs'
}
],
'asana' => [
@@ -397,8 +445,16 @@ module API
name: :webhook,
type: String,
desc: 'Discord webhook. For example, https://discord.com/api/webhooks/…'
- }
- ],
+ },
+ {
+ required: false,
+ name: :branches_to_be_notified,
+ type: String,
+ desc: 'Branches for which notifications are to be sent'
+ },
+ chat_notification_flags,
+ chat_notification_events
+ ].flatten,
'drone-ci' => [
{
required: true,
@@ -839,6 +895,20 @@ module API
desc: 'The issues URL'
}
],
+ 'clickup' => [
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ }
+ ],
'slack' => [
chat_notification_settings,
chat_notification_flags,
@@ -898,6 +968,21 @@ module API
desc: 'The password of the user'
}
],
+ 'telegram' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Telegram chat token. For example, 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'
+ },
+ {
+ required: true,
+ name: :room,
+ type: String,
+ desc: 'Unique identifier for the target chat or username of the target channel (in the format @channelusername)'
+ },
+ chat_notification_events
+ ].flatten,
'unify-circuit' => [
{
required: true,
@@ -968,6 +1053,7 @@ module API
::Integrations::Bugzilla,
::Integrations::Buildkite,
::Integrations::Campfire,
+ ::Integrations::Clickup,
::Integrations::Confluence,
::Integrations::CustomIssueTracker,
::Integrations::Datadog,
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index da499abe475..4b5335840f6 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -27,7 +27,7 @@ module API
note = ::Notes::UpdateService.new(project, current_user, opts).execute(note)
- if note.valid?
+ if note.errors.blank?
present note, with: Entities::Note
else
bad_request!("Failed to save note #{note.errors.messages}")
diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb
index a62bb1d4991..4f301d7038a 100644
--- a/lib/api/helpers/packages/basic_auth_helpers.rb
+++ b/lib/api/helpers/packages/basic_auth_helpers.rb
@@ -41,16 +41,15 @@ module API
end
def find_authorized_group!
- strong_memoize(:authorized_group) do
- group = find_group(params[:id])
+ group = find_group(params[:id])
- unless group && can?(current_user, :read_group, group)
- next unauthorized_or! { not_found! }
- end
-
- group
+ unless group && can?(current_user, :read_group, group)
+ return unauthorized_or! { not_found! }
end
+
+ group
end
+ strong_memoize_attr :find_authorized_group!
def authorize!(action, subject = :global, reason = nil)
return if can?(current_user, action, subject)
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
index b47bfbfb5aa..3873fe98a5f 100644
--- a/lib/api/helpers/packages/conan/api_helpers.rb
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -125,20 +125,18 @@ module API
end
def project
- strong_memoize(:project) do
- case package_scope
- when :project
- user_project(action: :read_package)
- when :instance
- full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
- find_project!(full_path)
- end
+ case package_scope
+ when :project
+ user_project(action: :read_package)
+ when :instance
+ full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
+ find_project!(full_path)
end
end
+ strong_memoize_attr :project
def package
- strong_memoize(:package) do
- project.packages
+ project.packages
.conan
.with_name(params[:package_name])
.with_version(params[:package_version])
@@ -147,18 +145,17 @@ module API
.order_created
.not_pending_destruction
.last
- end
end
+ strong_memoize_attr :package
def token
- strong_memoize(:token) do
- token = nil
- token = ::Gitlab::ConanToken.from_personal_access_token(find_personal_access_token.user_id, access_token_from_request) if find_personal_access_token
- token = ::Gitlab::ConanToken.from_deploy_token(deploy_token_from_request) if deploy_token_from_request
- token = ::Gitlab::ConanToken.from_job(find_job_from_token) if find_job_from_token
- token
- end
+ token = nil
+ token = ::Gitlab::ConanToken.from_personal_access_token(find_personal_access_token.user_id, access_token_from_request) if find_personal_access_token
+ token = ::Gitlab::ConanToken.from_deploy_token(deploy_token_from_request) if deploy_token_from_request
+ token = ::Gitlab::ConanToken.from_job(find_job_from_token) if find_job_from_token
+ token
end
+ strong_memoize_attr :token
def download_package_file(file_type)
authorize_read_package!(project)
@@ -227,17 +224,15 @@ module API
# We override this method from auth_finders because we need to
# extract the token from the Conan JWT which is specific to the Conan API
def find_personal_access_token
- strong_memoize(:find_personal_access_token) do
- PersonalAccessToken.find_by_token(access_token_from_request)
- end
+ PersonalAccessToken.find_by_token(access_token_from_request)
end
+ strong_memoize_attr :find_personal_access_token
def access_token_from_request
- strong_memoize(:access_token_from_request) do
- find_personal_access_token_from_conan_jwt ||
- find_password_from_basic_auth
- end
+ find_personal_access_token_from_conan_jwt ||
+ find_password_from_basic_auth
end
+ strong_memoize_attr :access_token_from_request
def find_password_from_basic_auth
return unless route_authentication_setting[:basic_auth_personal_access_token]
diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb
index 4eb6c39b7dc..be7f57fda0c 100644
--- a/lib/api/helpers/packages/npm.rb
+++ b/lib/api/helpers/packages/npm.rb
@@ -11,27 +11,22 @@ module API
package_name: API::NO_SLASH_URL_PART_REGEX
}.freeze
- def endpoint_scope
- params[:id].present? ? :project : :instance
- end
-
def project
- strong_memoize(:project) do
- case endpoint_scope
- when :project
- user_project(action: :read_package)
- when :instance
- # Simulate the same behavior as #user_project by re-using #find_project!
- # but take care if the project_id is nil as #find_project! is not designed
- # to handle it.
- project_id = project_id_or_nil
-
- not_found!('Project') unless project_id
-
- find_project!(project_id)
- end
+ case endpoint_scope
+ when :project
+ user_project(action: :read_package)
+ when :instance, :group
+ # Simulate the same behavior as #user_project by re-using #find_project!
+ # but take care if the project_id is nil as #find_project! is not designed
+ # to handle it.
+ project_id = project_id_or_nil
+
+ not_found!('Project') unless project_id
+
+ find_project!(project_id)
end
end
+ strong_memoize_attr :project
def finder_for_endpoint_scope(package_name)
case endpoint_scope
@@ -39,49 +34,57 @@ module API
::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil)
when :instance
::Packages::Npm::PackageFinder.new(package_name, namespace: top_namespace_from(package_name))
+ when :group
+ ::Packages::Npm::PackageFinder.new(package_name, namespace: group)
end
end
def project_or_nil
# mainly used by the metadata endpoint where we need to get a project
# and return nil if not found (no errors should be raised)
- strong_memoize(:project_or_nil) do
- next unless project_id_or_nil
+ return unless project_id_or_nil
- find_project(project_id_or_nil)
- end
+ find_project(project_id_or_nil)
end
+ strong_memoize_attr :project_or_nil
def project_id_or_nil
- strong_memoize(:project_id_or_nil) do
- case endpoint_scope
- when :project
- params[:id]
- when :instance
- package_name = params[:package_name]
-
- namespace =
- if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
- top_namespace_from(package_name)
- else
- namespace_path = ::Packages::Npm.scope_of(package_name)
- next unless namespace_path
-
- Namespace.top_most.by_path(namespace_path)
- end
-
- next unless namespace
-
- finder = ::Packages::Npm::PackageFinder.new(
- package_name,
- namespace: namespace,
- last_of_each_version: false
- )
-
- finder.last&.project_id
- end
+ case endpoint_scope
+ when :project
+ params[:id]
+ when :group
+ finder = ::Packages::Npm::PackageFinder.new(
+ params[:package_name],
+ namespace: group,
+ last_of_each_version: false
+ )
+
+ finder.last&.project_id
+ when :instance
+ package_name = params[:package_name]
+
+ namespace =
+ if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ top_namespace_from(package_name)
+ else
+ namespace_path = ::Packages::Npm.scope_of(package_name)
+ return unless namespace_path
+
+ Namespace.top_most.by_path(namespace_path)
+ end
+
+ return unless namespace
+
+ finder = ::Packages::Npm::PackageFinder.new(
+ package_name,
+ namespace: namespace,
+ last_of_each_version: false
+ )
+
+ finder.last&.project_id
end
end
+ strong_memoize_attr :project_id_or_nil
private
@@ -91,6 +94,13 @@ module API
Namespace.top_most.by_path(namespace_path)
end
+
+ def group
+ group = find_group(params[:id])
+ not_found!('Group') unless can?(current_user, :read_group, group)
+ group
+ end
+ strong_memoize_attr :group
end
end
end
diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb
index be2b73e2d48..f3b3a299204 100644
--- a/lib/api/helpers/packages_helpers.rb
+++ b/lib/api/helpers/packages_helpers.rb
@@ -4,6 +4,7 @@ module API
module Helpers
module PackagesHelpers
extend ::Gitlab::Utils::Override
+ include ::Gitlab::Utils::StrongMemoize
MAX_PACKAGE_FILE_SIZE = 50.megabytes.freeze
ALLOWED_REQUIRED_PERMISSIONS = %i[read_package read_group].freeze
@@ -71,19 +72,18 @@ module API
# This function is similar to the `find_project!` function, but it considers the `read_package` ability.
def user_project_with_read_package
- strong_memoize(:user_project_with_read_package) do
- project = find_project(params[:id])
+ project = find_project(params[:id])
- next forbidden! unless authorized_project_scope?(project)
+ return forbidden! unless authorized_project_scope?(project)
- next project if can?(current_user, :read_package, project&.packages_policy_subject)
- # guest users can have :read_project but not :read_package
- next forbidden! if can?(current_user, :read_project, project)
- next unauthorized! if authenticate_non_public?
+ return project if can?(current_user, :read_package, project&.packages_policy_subject)
+ # guest users can have :read_project but not :read_package
+ return forbidden! if can?(current_user, :read_project, project)
+ return unauthorized! if authenticate_non_public?
- not_found!('Project')
- end
+ not_found!('Project')
end
+ strong_memoize_attr :user_project_with_read_package
def track_package_event(action, scope, **args)
service = ::Packages::CreateEventService.new(nil, current_user, event_name: action, scope: scope)
diff --git a/lib/api/internal/error_tracking.rb b/lib/api/internal/error_tracking.rb
index 1680ac8afb5..e5047ba3e54 100644
--- a/lib/api/internal/error_tracking.rb
+++ b/lib/api/internal/error_tracking.rb
@@ -43,7 +43,7 @@ module API
project = Project.find(project_id)
enabled = error_tracking_enabled? &&
- Feature.enabled?(:use_click_house_database_for_error_tracking, project) &&
+ Feature.enabled?(:gitlab_error_tracking, project) &&
::ErrorTracking::ClientKey.enabled_key_for(project_id, public_key).exists?
status 200
diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb
index d340e097700..5592207c4b5 100644
--- a/lib/api/internal/kubernetes.rb
+++ b/lib/api/internal/kubernetes.rb
@@ -69,7 +69,7 @@ module API
end
def increment_count_events
- events = params[:counters]&.slice(:gitops_sync, :k8s_api_proxy_request)
+ events = params[:counters]&.slice(:gitops_sync, :k8s_api_proxy_request, :flux_git_push_notifications_total)
Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_event_counts(events)
end
@@ -121,6 +121,18 @@ module API
default_branch: project.default_branch_or_main
}
end
+
+ desc 'Verify agent access to a project' do
+ detail 'Verifies if the agent (owning the token) is authorized to access the given project'
+ end
+ route_setting :authentication, cluster_agent_token_allowed: true
+ get '/verify_project_access', feature_category: :deployment_management, urgency: :low do
+ project = find_project(params[:id])
+
+ not_found! unless agent_has_access_to_project?(project)
+
+ status 204
+ end
end
namespace 'kubernetes/agent_configuration' do
@@ -190,6 +202,7 @@ module API
optional :counters, type: Hash do
optional :gitops_sync, type: Integer, desc: 'The count to increment the gitops_sync metric by'
optional :k8s_api_proxy_request, type: Integer, desc: 'The count to increment the k8s_api_proxy_request metric by'
+ optional :flux_git_push_notifications_total, type: Integer, desc: 'The count to increment the flux_git_push_notifications_total metrics by'
end
optional :unique_counters, type: Hash do
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index d033913aa71..a0f7c5c9b21 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -271,11 +271,9 @@ module API
issue_params = convert_parameters_from_legacy_format(issue_params)
begin
- spam_params = ::Spam::SpamParams.new_from_request(request: request)
result = ::Issues::CreateService.new(container: user_project,
current_user: current_user,
- params: issue_params,
- spam_params: spam_params).execute
+ params: issue_params).execute
if result.success?
present result[:issue], with: Entities::Issue, current_user: current_user, project: user_project
@@ -318,11 +316,10 @@ module API
update_params = convert_parameters_from_legacy_format(update_params)
- spam_params = ::Spam::SpamParams.new_from_request(request: request)
issue = ::Issues::UpdateService.new(container: user_project,
current_user: current_user,
params: update_params,
- spam_params: spam_params).execute(issue)
+ perform_spam_check: true).execute(issue)
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb
index f348e20cc0b..5ef60ab0b94 100644
--- a/lib/api/markdown.rb
+++ b/lib/api/markdown.rb
@@ -2,6 +2,11 @@
module API
class Markdown < ::API::Base
+ include APIGuard
+
+ # Although this API endpoint responds to POST requests, it is a read-only operation
+ allow_access_with_scope :read_api
+
before { authenticate! if Feature.enabled?(:authenticate_markdown_api, type: :ops) }
feature_category :team_planning
diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb
index e075a917fa9..241cd93f380 100644
--- a/lib/api/maven_packages.rb
+++ b/lib/api/maven_packages.rb
@@ -60,16 +60,6 @@ module API
if stored_sha256 == expected_sha256
no_content!
else
- # Track sha1 conflicts.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/367356
- Gitlab::ErrorTracking.log_exception(
- ArgumentError.new,
- message: 'maven package file sha1 conflict',
- stored_sha1: package_file.file_sha1,
- received_sha256: uploaded_file.sha256,
- sha256_hexdigest_of_stored_sha1: stored_sha256
- )
-
conflict!
end
end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 9321b7ad8d5..337706f36e1 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -11,8 +11,8 @@ module API
helpers ::API::Helpers::MembersHelpers
{
- "group" => :subgroups,
- "project" => :projects
+ "group" => :groups_and_projects,
+ "project" => :groups_and_projects
}.each do |source_type, feature_category|
params do
requires :id, type: String, desc: "The #{source_type} ID"
diff --git a/lib/api/ml/mlflow/entrypoint.rb b/lib/api/ml/mlflow/entrypoint.rb
index 880b1efeb5a..048234eccd1 100644
--- a/lib/api/ml/mlflow/entrypoint.rb
+++ b/lib/api/ml/mlflow/entrypoint.rb
@@ -26,7 +26,7 @@ module API
authenticate!
- not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project)
+ not_found! unless can?(current_user, :read_model_experiments, user_project)
end
rescue_from ActiveRecord::ActiveRecordError do |e|
diff --git a/lib/api/ml_model_packages.rb b/lib/api/ml_model_packages.rb
new file mode 100644
index 00000000000..fec72b03ffd
--- /dev/null
+++ b/lib/api/ml_model_packages.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module API
+ class MlModelPackages < ::API::Base
+ include APIGuard
+ include ::API::Helpers::Authentication
+
+ ML_MODEL_PACKAGES_REQUIREMENTS = {
+ package_name: API::NO_SLASH_URL_PART_REGEX,
+ file_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ ALLOWED_STATUSES = %w[default hidden].freeze
+
+ feature_category :mlops
+ urgency :low
+
+ after_validation do
+ require_packages_enabled!
+ authenticate_non_get!
+
+ not_found! unless can?(current_user, :read_model_registry, user_project)
+ end
+
+ authenticate_with do |accept|
+ accept.token_types(:personal_access_token, :deploy_token, :job_token)
+ .sent_through(:http_token)
+ end
+
+ helpers do
+ include ::API::Helpers::PackagesHelpers
+ include ::API::Helpers::Packages::BasicAuthHelpers
+
+ def project
+ authorized_user_project
+ end
+
+ def max_file_size_exceeded?
+ project.actual_limits.exceeded?(:ml_model_max_file_size, params[:file].size)
+ end
+ end
+
+ params do
+ requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ namespace ':id/packages/ml_models' do
+ params do
+ requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.ml_model_name_regex,
+ file_path: true
+ requires :package_version, type: String, desc: 'Package version',
+ regexp: Gitlab::Regex.ml_model_version_regex
+ requires :file_name, type: String, desc: 'Package file name',
+ regexp: Gitlab::Regex.ml_model_file_name_regex, file_path: true
+ optional :status, type: String, values: ALLOWED_STATUSES, desc: 'Package status'
+ end
+ namespace ':package_name/*package_version/:file_name', requirements: ML_MODEL_PACKAGES_REQUIREMENTS do
+ desc 'Workhorse authorize model package file' do
+ detail 'Introduced in GitLab 16.1'
+ success code: 200
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[ml_model_registry]
+ end
+ put 'authorize' do
+ authorize_workhorse!(subject: project, maximum_size: project.actual_limits.ml_model_max_file_size)
+ end
+
+ desc 'Workhorse upload model package file' do
+ detail 'Introduced in GitLab 16.1'
+ success code: 201
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[ml_model_registry]
+ end
+ params do
+ requires :file,
+ type: ::API::Validations::Types::WorkhorseFile,
+ desc: 'The package file to be published (generated by Multipart middleware)',
+ documentation: { type: 'file' }
+ end
+ put do
+ authorize_upload!(project)
+
+ bad_request!('File is too large') if max_file_size_exceeded?
+
+ create_package_file_params = declared(params).merge(build: current_authenticated_job)
+ package_file = ::Packages::MlModel::CreatePackageFileService
+ .new(project, current_user, create_package_file_params)
+ .execute
+
+ bad_request!('Package creation failed') unless package_file
+
+ created!
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project.id })
+
+ forbidden!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index c971f73ccbb..750dc7fc2a1 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -38,7 +38,7 @@ module API
use :pagination
use :optional_list_params_ee
end
- get feature_category: :subgroups, urgency: :low do
+ get feature_category: :groups_and_projects, urgency: :low do
owned_only = params[:owned_only] == true
namespaces = current_user.admin ? Namespace.all : current_user.namespaces(owned_only: owned_only)
@@ -66,7 +66,7 @@ module API
params do
requires :id, types: [String, Integer], desc: 'ID or URL-encoded path of the namespace'
end
- get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, feature_category: :subgroups, urgency: :low do
+ get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, feature_category: :groups_and_projects, urgency: :low do
user_namespace = find_namespace!(params[:id])
present user_namespace, with: Entities::Namespace, current_user: current_user
@@ -84,7 +84,7 @@ module API
requires :id, type: String, desc: "Namespace’s path"
optional :parent_id, type: Integer, desc: 'The ID of the parent namespace. If no ID is specified, only top-level namespaces are considered.'
end
- get ':id/exists', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, feature_category: :subgroups, urgency: :low do
+ get ':id/exists', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, feature_category: :groups_and_projects, urgency: :low do
check_rate_limit!(:namespace_exists, scope: current_user)
namespace_path = params[:id]
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 8ce875cdc03..70b4a3735e3 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -98,7 +98,7 @@ module API
if note.errors.attribute_names == [:commands_only, :command_names]
status 202
present note, with: Entities::NoteCommands
- elsif note.valid?
+ elsif note.persisted?
present note, with: Entities.const_get(note.class.name, false)
else
note.errors.delete(:commands_only) if note.errors.has_key?(:commands)
diff --git a/lib/api/npm_group_packages.rb b/lib/api/npm_group_packages.rb
new file mode 100644
index 00000000000..1aa3135b186
--- /dev/null
+++ b/lib/api/npm_group_packages.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module API
+ class NpmGroupPackages < ::API::Base
+ helpers ::API::Helpers::Packages::Npm
+
+ feature_category :package_registry
+ urgency :low
+
+ helpers do
+ def endpoint_scope
+ :group
+ end
+ end
+
+ params do
+ requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the group'
+ end
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ namespace ':id/-/packages/npm' do
+ include ::API::Concerns::Packages::NpmEndpoints
+ end
+ end
+ end
+end
diff --git a/lib/api/npm_instance_packages.rb b/lib/api/npm_instance_packages.rb
index e387dd65e41..8215296b617 100644
--- a/lib/api/npm_instance_packages.rb
+++ b/lib/api/npm_instance_packages.rb
@@ -10,6 +10,12 @@ module API
render_api_error!(e.message, 400)
end
+ helpers do
+ def endpoint_scope
+ :instance
+ end
+ end
+
namespace 'packages/npm' do
include ::API::Concerns::Packages::NpmEndpoints
end
diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb
index 171a061bf97..61409909b06 100644
--- a/lib/api/npm_project_packages.rb
+++ b/lib/api/npm_project_packages.rb
@@ -10,6 +10,12 @@ module API
render_api_error!(e.message, 400)
end
+ helpers do
+ def endpoint_scope
+ :project
+ end
+ end
+
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
end
diff --git a/lib/api/nuget_group_packages.rb b/lib/api/nuget_group_packages.rb
index 2afcb915b06..229032f7a5a 100644
--- a/lib/api/nuget_group_packages.rb
+++ b/lib/api/nuget_group_packages.rb
@@ -17,11 +17,6 @@ module API
default_format :json
- authenticate_with do |accept|
- accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
- .sent_through(:http_basic_auth)
- end
-
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
@@ -31,10 +26,17 @@ module API
end
helpers do
+ include ::Gitlab::Utils::StrongMemoize
+
def project_or_group
find_authorized_group!
end
+ def project_or_group_without_auth
+ find_group(params[:id]).presence || not_found!
+ end
+ strong_memoize_attr :project_or_group_without_auth
+
def require_authenticated!
unauthorized! unless current_user
end
@@ -43,23 +45,38 @@ module API
{ namespace: find_authorized_group! }
end
+ def snowplow_gitlab_standard_context_without_auth
+ { namespace: project_or_group_without_auth }
+ end
+
def required_permission
:read_group
end
end
params do
- requires :id, types: [Integer, String], desc: 'The group ID or full group path.', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
+ requires :id, types: [Integer, String], desc: 'The group ID or full group path.', regexp: ::API::Concerns::Packages::Nuget::PrivateEndpoints::POSITIVE_INTEGER_REGEX
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- namespace ':id/-/packages/nuget' do
- after_validation do
- # This API can't be accessed anonymously
- require_authenticated!
+ namespace ':id/-/packages' do
+ namespace '/nuget' do
+ include ::API::Concerns::Packages::Nuget::PublicEndpoints
+ end
+
+ authenticate_with do |accept|
+ accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
+ .sent_through(:http_basic_auth)
end
- include ::API::Concerns::Packages::NugetEndpoints
+ namespace '/nuget' do
+ after_validation do
+ # This API can't be accessed anonymously
+ require_authenticated!
+ end
+
+ include ::API::Concerns::Packages::Nuget::PrivateEndpoints
+ end
end
end
end
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
index cd16aaf6b5f..2716d6f0b64 100644
--- a/lib/api/nuget_project_packages.rb
+++ b/lib/api/nuget_project_packages.rb
@@ -17,14 +17,10 @@ module API
PACKAGE_FILENAME = 'package.nupkg'
SYMBOL_PACKAGE_FILENAME = 'package.snupkg'
+ API_KEY_HEADER = 'X-Nuget-Apikey'
default_format :json
- authenticate_with do |accept|
- accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
- .sent_through(:http_basic_auth)
- end
-
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
@@ -34,6 +30,8 @@ module API
end
helpers do
+ include ::Gitlab::Utils::StrongMemoize
+
params :file_params do
requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)', documentation: { type: 'file' }
end
@@ -42,10 +40,19 @@ module API
authorized_user_project(action: :read_package)
end
+ def project_or_group_without_auth
+ find_project(params[:id]).presence || not_found!
+ end
+ strong_memoize_attr :project_or_group_without_auth
+
def snowplow_gitlab_standard_context
{ project: project_or_group, namespace: project_or_group.namespace }
end
+ def snowplow_gitlab_standard_context_without_auth
+ { project: project_or_group_without_auth, namespace: project_or_group_without_auth.namespace }
+ end
+
def authorize_nuget_upload
project = project_or_group
authorize_workhorse!(
@@ -97,115 +104,127 @@ module API
end
params do
- requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
+ requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project', regexp: ::API::Concerns::Packages::Nuget::PrivateEndpoints::POSITIVE_INTEGER_REGEX
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- namespace ':id/packages/nuget' do
- include ::API::Concerns::Packages::NugetEndpoints
-
- # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
- desc 'The NuGet Package Publish endpoint' do
- detail 'This feature was introduced in GitLab 12.6'
- success code: 201
- failure [
- { code: 400, message: 'Bad Request' },
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
+ namespace ':id/packages' do
+ namespace '/nuget' do
+ include ::API::Concerns::Packages::Nuget::PublicEndpoints
end
- params do
- use :file_params
+ authenticate_with do |accept|
+ accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
+ .sent_through(:http_basic_auth)
end
- put urgency: :low do
- upload_nuget_package_file do |package|
- track_package_event(
- 'push_package',
- :nuget,
- category: 'API::NugetPackages',
- project: package.project,
- namespace: package.project.namespace
- )
- end
- rescue ObjectStorage::RemoteStoreError => e
- Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
- forbidden!
- end
+ namespace '/nuget' do
+ include ::API::Concerns::Packages::Nuget::PrivateEndpoints
- desc 'The NuGet Package Authorize endpoint' do
- detail 'This feature was introduced in GitLab 14.1'
- success code: 200
- failure [
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
- end
- put 'authorize', urgency: :low do
- authorize_nuget_upload
- end
-
- # https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource
- desc 'The NuGet Symbol Package Publish endpoint' do
- detail 'This feature was introduced in GitLab 14.1'
- success code: 201
- failure [
- { code: 400, message: 'Bad Request' },
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
- end
- params do
- use :file_params
- end
- put 'symbolpackage', urgency: :low do
- upload_nuget_package_file(symbol_package: true) do |package|
- track_package_event(
- 'push_symbol_package',
- :nuget,
- category: 'API::NugetPackages',
- project: package.project,
- namespace: package.project.namespace
- )
+ # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
+ params do
+ requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'mynugetpkg.1.3.0.17.nupkg' }
+ end
+ namespace '/download/*package_name' do
+ after_validation do
+ authorize_read_package!(project_or_group)
+ end
+
+ desc 'The NuGet Content Service - index request' do
+ detail 'This feature was introduced in GitLab 12.8'
+ success code: 200, model: ::API::Entities::Nuget::PackagesVersions
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
+ end
+ get 'index', format: :json, urgency: :low do
+ present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages(params[:package_name])),
+ with: ::API::Entities::Nuget::PackagesVersions
+ end
+
+ desc 'The NuGet Content Service - content request' do
+ detail 'This feature was introduced in GitLab 12.8'
+ success code: 200
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
+ end
+ params do
+ requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: '1.3.0.17' }
+ requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'mynugetpkg.1.3.0.17.nupkg' }
+ end
+ get '*package_version/*package_filename', format: [:nupkg, :snupkg], urgency: :low do
+ filename = "#{params[:package_filename]}.#{params[:format]}"
+ package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true)
+ .execute
+
+ not_found!('Package') unless package_file
+
+ track_package_event(
+ params[:format] == 'snupkg' ? 'pull_symbol_package' : 'pull_package',
+ :nuget,
+ category: 'API::NugetPackages',
+ project: package_file.project,
+ namespace: package_file.project.namespace
+ )
+
+ # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
+ present_package_file!(package_file, supports_direct_download: false)
+ end
end
- rescue ObjectStorage::RemoteStoreError => e
- Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
-
- forbidden!
end
- desc 'The NuGet Symbol Package Authorize endpoint' do
- detail 'This feature was introduced in GitLab 14.1'
- success code: 200
- failure [
- { code: 401, message: 'Unauthorized' },
- { code: 403, message: 'Forbidden' },
- { code: 404, message: 'Not Found' }
- ]
- tags %w[nuget_packages]
- end
- put 'symbolpackage/authorize', urgency: :low do
- authorize_nuget_upload
+ # To support an additional authentication option for download endpoints,
+ # we redefine the `authenticate_with` method by combining the previous
+ # authentication option with the new one.
+ authenticate_with do |accept|
+ accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
+ .sent_through(:http_basic_auth)
+ accept.token_types(:personal_access_token, :deploy_token, :job_token)
+ .sent_through(http_header: API_KEY_HEADER)
end
- # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
- params do
- requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'mynugetpkg.1.3.0.17.nupkg' }
- end
- namespace '/download/*package_name' do
- after_validation do
- authorize_read_package!(project_or_group)
+ namespace '/nuget' do
+ # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
+ desc 'The NuGet Package Publish endpoint' do
+ detail 'This feature was introduced in GitLab 12.6'
+ success code: 201
+ failure [
+ { code: 400, message: 'Bad Request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
+ end
+
+ params do
+ use :file_params
+ end
+ put urgency: :low do
+ upload_nuget_package_file do |package|
+ track_package_event(
+ 'push_package',
+ :nuget,
+ category: 'API::NugetPackages',
+ project: package.project,
+ namespace: package.project.namespace
+ )
+ end
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
+
+ forbidden!
end
- desc 'The NuGet Content Service - index request' do
- detail 'This feature was introduced in GitLab 12.8'
- success code: 200, model: ::API::Entities::Nuget::PackagesVersions
+ desc 'The NuGet Package Authorize endpoint' do
+ detail 'This feature was introduced in GitLab 14.1'
+ success code: 200
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
@@ -213,15 +232,16 @@ module API
]
tags %w[nuget_packages]
end
- get 'index', format: :json, urgency: :low do
- present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages(params[:package_name])),
- with: ::API::Entities::Nuget::PackagesVersions
+ put 'authorize', urgency: :low do
+ authorize_nuget_upload
end
- desc 'The NuGet Content Service - content request' do
- detail 'This feature was introduced in GitLab 12.8'
- success code: 200
+ # https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource
+ desc 'The NuGet Symbol Package Publish endpoint' do
+ detail 'This feature was introduced in GitLab 14.1'
+ success code: 201
failure [
+ { code: 400, message: 'Bad Request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not Found' }
@@ -229,26 +249,36 @@ module API
tags %w[nuget_packages]
end
params do
- requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: '1.3.0.17' }
- requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'mynugetpkg.1.3.0.17.nupkg' }
+ use :file_params
+ end
+ put 'symbolpackage', urgency: :low do
+ upload_nuget_package_file(symbol_package: true) do |package|
+ track_package_event(
+ 'push_symbol_package',
+ :nuget,
+ category: 'API::NugetPackages',
+ project: package.project,
+ namespace: package.project.namespace
+ )
+ end
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
+
+ forbidden!
+ end
+
+ desc 'The NuGet Symbol Package Authorize endpoint' do
+ detail 'This feature was introduced in GitLab 14.1'
+ success code: 200
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[nuget_packages]
end
- get '*package_version/*package_filename', format: [:nupkg, :snupkg], urgency: :low do
- filename = "#{params[:package_filename]}.#{params[:format]}"
- package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true)
- .execute
-
- not_found!('Package') unless package_file
-
- track_package_event(
- params[:format] == 'snupkg' ? 'pull_symbol_package' : 'pull_package',
- :nuget,
- category: 'API::NugetPackages',
- project: package_file.project,
- namespace: package_file.project.namespace
- )
-
- # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
- present_package_file!(package_file, supports_direct_download: false)
+ put 'symbolpackage/authorize', urgency: :low do
+ authorize_nuget_upload
end
end
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index ced8ecec883..db97f4988e1 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -9,7 +9,7 @@ module API
before { authenticate! }
before { authorize_admin_project }
- feature_category :integrations
+ feature_category :webhooks
helpers ::API::Helpers::WebHooksHelpers
diff --git a/lib/api/project_job_token_scope.rb b/lib/api/project_job_token_scope.rb
index 7fd288491ef..79710bffeaf 100644
--- a/lib/api/project_job_token_scope.rb
+++ b/lib/api/project_job_token_scope.rb
@@ -2,6 +2,8 @@
module API
class ProjectJobTokenScope < ::API::Base
+ include PaginationParams
+
before { authenticate! }
feature_category :secrets_management
@@ -22,6 +24,134 @@ module API
present user_project, with: Entities::ProjectJobTokenScope
end
+
+ desc 'Patch CI_JOB_TOKEN access settings.' do
+ failure [
+ { code: 400, message: 'Bad Request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' }
+ ]
+ success code: 204
+ tags %w[projects_job_token_scope]
+ end
+ params do
+ requires :enabled,
+ type: Boolean,
+ as: :ci_inbound_job_token_scope_enabled,
+ allow_blank: false,
+ desc: "Indicates CI/CD job tokens generated in other projects have restricted access to this project."
+ end
+
+ patch ':id/job_token_scope' do
+ authorize_admin_project
+
+ job_token_scope_params = declared_params(include_missing: false)
+ result = ::Projects::UpdateService.new(user_project, current_user, job_token_scope_params).execute
+
+ break bad_request!(result[:message]) if result[:status] == :error
+
+ no_content!
+ end
+
+ desc 'Fetch project inbound allowlist for CI_JOB_TOKEN access settings.' do
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' }
+ ]
+ success status: 200, model: Entities::BasicProjectDetails
+ tags %w[projects_job_token_scope]
+ end
+ params do
+ use :pagination
+ end
+ get ':id/job_token_scope/allowlist' do
+ authorize_admin_project
+
+ inbound_projects = ::Ci::JobToken::Scope.new(user_project).inbound_projects
+
+ present paginate(inbound_projects), with: Entities::BasicProjectDetails
+ end
+
+ desc 'Add target project to allowlist.' do
+ failure [
+ { code: 400, message: 'Bad Request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' },
+ { code: 422, message: 'Unprocessable entity' }
+ ]
+ success status: 201, model: Entities::BasicProjectDetails
+ tags %w[projects_job_token_scope]
+ end
+ params do
+ requires :id,
+ allow_blank: false,
+ desc: 'ID of user project',
+ documentation: { example: 1 },
+ type: Integer
+
+ requires :target_project_id,
+ allow_blank: false,
+ desc: 'ID of target project',
+ documentation: { example: 2 },
+ type: Integer
+ end
+ post ':id/job_token_scope/allowlist' do
+ authorize_admin_project
+
+ target_project_id = declared_params(include_missing: false).fetch(:target_project_id)
+ target_project = Project.find_by_id(target_project_id)
+ break not_found!("target_project_id not found") if target_project.blank?
+
+ result = ::Ci::JobTokenScope::AddProjectService
+ .new(user_project, current_user)
+ .execute(target_project, direction: :inbound)
+
+ break bad_request!(result[:message]) if result.error?
+
+ present result.payload[:project_link], with: Entities::ProjectScopeLink
+ end
+
+ desc 'Delete project from allowlist.' do
+ failure [
+ { code: 400, message: 'Bad Request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' }
+ ]
+ success code: 204
+ tags %w[projects_job_token_scope]
+ end
+ params do
+ requires :id,
+ allow_blank: false,
+ desc: 'ID of user project',
+ documentation: { example: 1 },
+ type: Integer
+
+ requires :target_project_id,
+ allow_blank: false,
+ desc: 'ID of the project to be removed from the allowlist',
+ documentation: { example: 2 },
+ type: Integer
+ end
+ delete ':id/job_token_scope/allowlist/:target_project_id' do
+ target_project = find_project!(params[:target_project_id])
+
+ result = ::Ci::JobTokenScope::RemoveProjectService
+ .new(user_project, current_user)
+ .execute(target_project, :inbound)
+
+ if result.success?
+ no_content!
+ elsif result.reason == :insufficient_permissions
+ forbidden!(result.message)
+ else
+ bad_request!(result.message)
+ end
+ end
end
end
end
diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb
index 158ba7465f4..43bd15931ef 100644
--- a/lib/api/project_packages.rb
+++ b/lib/api/project_packages.rb
@@ -2,8 +2,11 @@
module API
class ProjectPackages < ::API::Base
+ include Gitlab::Utils::StrongMemoize
include PaginationParams
+ PIPELINE_COLUMNS = %i[id iid project_id sha ref status source created_at updated_at user_id].freeze
+
before do
authorize_packages_access!(user_project)
end
@@ -12,6 +15,13 @@ module API
urgency :low
helpers ::API::Helpers::PackagesHelpers
+ helpers do
+ def package
+ strong_memoize(:package) do # rubocop:disable Gitlab/StrongMemoizeAttr
+ ::Packages::PackageFinder.new(user_project, declared_params[:package_id]).execute
+ end
+ end
+ end
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
@@ -66,14 +76,45 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/packages/:package_id' do
- package = ::Packages::PackageFinder
- .new(user_project, params[:package_id]).execute
-
render_api_error!('Package not found', 404) unless package.default?
present package, with: ::API::Entities::Package, user: current_user, namespace: user_project.namespace
end
+ desc 'Get the pipelines for a single project package' do
+ detail 'This feature was introduced in GitLab 16.1'
+ success code: 200, model: ::API::Entities::Package::Pipeline
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[project_packages]
+ end
+ params do
+ use :pagination
+ requires :package_id, type: Integer, desc: 'The ID of a package'
+ optional :cursor, type: String, desc: 'Cursor for obtaining the next set of records'
+ # Overrides the original definition to add the `values: 1..20` restriction
+ optional :per_page, type: Integer, default: 20,
+ desc: 'Number of items per page', documentation: { example: 20 },
+ values: 1..20
+ end
+ route_setting :authentication, job_token_allowed: true
+ get ':id/packages/:package_id/pipelines' do
+ not_found!('Package not found') unless package.default?
+
+ params[:pagination] = 'keyset' # keyset is the only available pagination
+ pipelines = paginate_with_strategies(
+ package.build_infos.without_empty_pipelines,
+ paginator_params: { per_page: declared_params[:per_page], cursor: declared_params[:cursor] }
+ ) do |results|
+ ::Ci::Pipeline.id_in(results.map(&:pipeline_id)).select(PIPELINE_COLUMNS).order_id_desc
+ end
+
+ present pipelines, with: ::API::Entities::Package::Pipeline, user: current_user
+ end
+
desc 'Delete a project package' do
detail 'This feature was introduced in GitLab 11.9'
success code: 204
@@ -90,9 +131,6 @@ module API
delete ':id/packages/:package_id' do
authorize_destroy_package!(user_project)
- package = ::Packages::PackageFinder
- .new(user_project, params[:package_id]).execute
-
destroy_conditionally!(package) do |package|
::Packages::MarkPackageForDestructionService.new(container: package, current_user: current_user).execute
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 7ef722301ca..a503b941593 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -90,9 +90,7 @@ module API
authorize! :create_snippet, user_project
snippet_params = process_create_params(declared_params(include_missing: false))
-
- spam_params = ::Spam::SpamParams.new_from_request(request: request)
- service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute
+ service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params).execute
snippet = service_response.payload[:snippet]
if service_response.success?
@@ -138,9 +136,7 @@ module API
validate_params_for_multiple_files(snippet)
snippet_params = process_update_params(declared_params(include_missing: false))
-
- spam_params = ::Spam::SpamParams.new_from_request(request: request)
- service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute(snippet)
+ service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params, perform_spam_check: true).execute(snippet)
snippet = service_response.payload[:snippet]
if service_response.success?
diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb
index 2360a7e6b2a..49e2e4d8a91 100644
--- a/lib/api/project_templates.rb
+++ b/lib/api/project_templates.rb
@@ -4,7 +4,7 @@ module API
class ProjectTemplates < ::API::Base
include PaginationParams
- TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses metrics_dashboard_ymls issues merge_requests].freeze
+ TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses issues merge_requests].freeze
# The regex is needed to ensure a period (e.g. agpl-3.0)
# isn't confused with a format type. We also need to allow encoded
# values (e.g. C%2B%2B for C++), so allow % and + as well.
@@ -16,7 +16,7 @@ module API
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
- requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|licenses|metrics_dashboard_ymls|issues|merge_requests) of the template'
+ requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|licenses|issues|merge_requests) of the template'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of templates available to this project' do
@@ -32,8 +32,6 @@ module API
use :pagination
end
get ':id/templates/:type' do
- bad_request! if params[:type] == 'metrics_dashboard_ymls' && Feature.enabled?(:remove_monitor_metrics)
-
templates = TemplateFinder.all_template_names(user_project, params[:type]).values.flatten
present paginate(::Kaminari.paginate_array(templates)), with: Entities::TemplatesList
@@ -62,8 +60,6 @@ module API
end
get ':id/templates/:type/:name', requirements: TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS do
- bad_request! if params[:type] == 'metrics_dashboard_ymls' && Feature.enabled?(:remove_monitor_metrics)
-
begin
template = TemplateFinder.build(
params[:type],
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index d6863e4eba4..7ec9f72e0b2 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -9,7 +9,7 @@ module API
before { authenticate_non_get! }
- feature_category :projects, %w[
+ feature_category :groups_and_projects, %w[
/projects/:id/custom_attributes
/projects/:id/custom_attributes/:key
/projects/:id/share_locations
@@ -20,8 +20,6 @@ module API
helpers do
# EE::API::Projects would override this method
def apply_filters(projects)
- projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
- projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
projects = projects.joins(:statistics) if params[:order_by].include?('project_statistics') # rubocop: disable CodeReuse/ActiveRecord
projects = projects.created_by(current_user).imported.with_import_state if params[:imported]
@@ -244,7 +242,7 @@ module API
use :statistics_params
use :with_custom_attributes
end
- get ":user_id/projects", feature_category: :projects, urgency: :low do
+ get ":user_id/projects", feature_category: :groups_and_projects, urgency: :low do
user = find_user(params[:user_id])
not_found!('User') unless user
@@ -264,7 +262,7 @@ module API
use :collection_params
use :statistics_params
end
- get ":user_id/starred_projects", feature_category: :projects, urgency: :low do
+ get ":user_id/starred_projects", feature_category: :groups_and_projects, urgency: :low do
user = find_user(params[:user_id])
not_found!('User') unless user
@@ -290,7 +288,7 @@ module API
use :with_custom_attributes
end
# TODO: Set higher urgency https://gitlab.com/gitlab-org/gitlab/-/issues/211495
- get feature_category: :projects, urgency: :low do
+ get feature_category: :groups_and_projects, urgency: :low do
validate_projects_api_rate_limit_for_unauthenticated_users!
validate_updated_at_order_and_filter!
@@ -359,7 +357,7 @@ module API
use :create_params
end
# rubocop: disable CodeReuse/ActiveRecord
- post "user/:user_id", feature_category: :projects do
+ post "user/:user_id", feature_category: :groups_and_projects do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/issues/21139')
authenticated_as_admin!
user = User.find_by(id: params.delete(:user_id))
@@ -416,7 +414,7 @@ module API
desc: 'Include project license data'
end
# TODO: Set higher urgency https://gitlab.com/gitlab-org/gitlab/-/issues/357622
- get ":id", feature_category: :projects, urgency: :low do
+ get ":id", feature_category: :groups_and_projects, urgency: :low do
options = {
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
current_user: current_user,
@@ -527,7 +525,7 @@ module API
at_least_one_of(*Helpers::ProjectsHelpers.update_params_at_least_one_of)
end
- put ':id', feature_category: :projects do
+ put ':id', feature_category: :groups_and_projects do
authorize_admin_project
attrs = declared_params(include_missing: false)
authorize! :rename_project, user_project if attrs[:name].present?
@@ -559,7 +557,7 @@ module API
]
tags %w[projects]
end
- post ':id/archive', feature_category: :projects do
+ post ':id/archive', feature_category: :groups_and_projects do
authorize!(:archive_project, user_project)
::Projects::UpdateService.new(user_project, current_user, archived: true).execute
@@ -574,7 +572,7 @@ module API
]
tags %w[projects]
end
- post ':id/unarchive', feature_category: :projects, urgency: :default do
+ post ':id/unarchive', feature_category: :groups_and_projects, urgency: :default do
authorize!(:archive_project, user_project)
::Projects::UpdateService.new(user_project, current_user, archived: false).execute
@@ -590,7 +588,7 @@ module API
]
tags %w[projects]
end
- post ':id/star', feature_category: :projects do
+ post ':id/star', feature_category: :groups_and_projects do
if current_user.starred?(user_project)
not_modified!
else
@@ -609,7 +607,7 @@ module API
]
tags %w[projects]
end
- post ':id/unstar', feature_category: :projects do
+ post ':id/unstar', feature_category: :groups_and_projects do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
user_project.reset
@@ -633,7 +631,7 @@ module API
optional :search, type: String, desc: 'Return list of users matching the search criteria', documentation: { example: 'user' }
use :pagination
end
- get ':id/starrers', feature_category: :projects do
+ get ':id/starrers', feature_category: :groups_and_projects do
starrers = UsersStarProjectsFinder.new(user_project, params, current_user: current_user).execute
present paginate(starrers), with: Entities::UserStarsProject
@@ -661,7 +659,7 @@ module API
]
tags %w[projects]
end
- delete ":id", feature_category: :projects do
+ delete ":id", feature_category: :groups_and_projects do
authorize! :remove_project, user_project
delete_project(user_project)
@@ -729,7 +727,7 @@ module API
requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level'
optional :expires_at, type: Date, desc: 'Share expiration date'
end
- post ":id/share", feature_category: :projects do
+ post ":id/share", feature_category: :groups_and_projects do
authorize! :admin_project, user_project
shared_with_group = Group.find_by_id(params[:group_id])
@@ -759,7 +757,7 @@ module API
requires :group_id, type: Integer, desc: 'The ID of the group'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete ":id/share/:group_id", feature_category: :projects do
+ delete ":id/share/:group_id", feature_category: :groups_and_projects do
authorize! :admin_project, user_project
link = user_project.project_group_links.find_by(group_id: params[:group_id])
@@ -783,7 +781,7 @@ module API
params do
requires :project_id, type: Integer, desc: 'The ID of the source project to import the members from.'
end
- post ":id/import_project_members/:project_id", feature_category: :projects do
+ post ":id/import_project_members/:project_id", feature_category: :groups_and_projects do
::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/355916')
authorize! :admin_project, user_project
@@ -947,7 +945,7 @@ module API
params do
requires :namespace, type: String, desc: 'The ID or path of the new namespace', documentation: { example: 'gitlab' }
end
- put ":id/transfer", feature_category: :projects do
+ put ":id/transfer", feature_category: :groups_and_projects do
authorize! :change_namespace, user_project
namespace = find_namespace!(params[:namespace])
@@ -972,13 +970,13 @@ module API
optional :search, type: String, desc: 'Return list of namespaces matching the search criteria', documentation: { example: 'search' }
use :pagination
end
- get ":id/transfer_locations", feature_category: :projects do
+ get ":id/transfer_locations", feature_category: :groups_and_projects do
authorize! :change_namespace, user_project
args = declared_params(include_missing: false)
args[:permission_scope] = :transfer_projects
groups = ::Groups::UserGroupsFinder.new(current_user, current_user, args).execute
- groups = groups.with_route
+ groups = groups.excluding_groups(user_project.group).with_route
present_groups(groups)
end
diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb
index 311fcf9aba1..d0234439057 100644
--- a/lib/api/release/links.rb
+++ b/lib/api/release/links.rb
@@ -20,7 +20,7 @@ module API
end
resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
- requires :tag_name, type: String, desc: 'The tag associated with the release', as: :tag
+ requires :tag_name, type: String, desc: 'The tag associated with the release'
end
resource 'releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
resource :assets do
@@ -56,7 +56,8 @@ module API
params do
requires :name, type: String, desc: 'The name of the link. Link names must be unique in the release'
requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique in the release.'
- optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath
+ optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link'
+ optional :filepath, type: String, desc: 'Deprecated: optional path for a direct asset link'
optional :link_type,
type: String,
values: %w[other runbook image package],
@@ -110,7 +111,8 @@ module API
params do
optional :name, type: String, desc: 'The name of the link'
optional :url, type: String, desc: 'The URL of the link'
- optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath
+ optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link'
+ optional :filepath, type: String, desc: 'Deprecated: optional path for a direct asset link'
optional :link_type,
type: String,
values: %w[other runbook image package],
@@ -164,11 +166,11 @@ module API
helpers do
def release
- @release ||= user_project.releases.find_by_tag!(params[:tag])
+ @release ||= user_project.releases.find_by_tag!(declared_params(include_parent_namespaces: true)[:tag_name])
end
def link
- @link ||= release.links.find(params[:link_id])
+ @link ||= release.links.find(declared_params(include_parent_namespaces: true)[:link_id])
end
end
end
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 0b31a3e0309..5d056ade3da 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -109,7 +109,7 @@ module API
cache_context: -> (_) { "user:{#{current_user&.id}}" },
expires_in: 5.minutes,
current_user: current_user,
- include_html_description: params[:include_html_description]
+ include_html_description: declared_params[:include_html_description]
end
desc 'Get a release by a tag name' do
@@ -135,7 +135,7 @@ module API
not_found! unless release
- present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description]
+ present release, with: Entities::Release, current_user: current_user, include_html_description: declared_params[:include_html_description]
end
desc 'Download a project release asset file' do
@@ -162,8 +162,8 @@ module API
not_found! unless release
- link = release.links.find_by_filepath!("/#{params[:filepath]}")
-
+ filepath = declared_params(include_missing: false)[:filepath]
+ link = release.links.find_by_filepath!("/#{filepath}")
not_found! unless link
redirect link.url
@@ -196,7 +196,7 @@ module API
redirect_url = api_v4_projects_releases_path(id: user_project.id, tag_name: latest_release.tag)
# Include the additional suffix_path if present
- redirect_url += "/#{params[:suffix_path]}" if params[:suffix_path].present?
+ redirect_url += "/#{declared_params[:suffix_path]}" if declared_params[:suffix_path].present?
# Include any query parameter except `order_by` since we have plans to extend it in the future.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/352945 for reference.
@@ -238,7 +238,8 @@ module API
optional :links, type: Array do
requires :name, type: String, desc: 'The name of the link. Link names must be unique within the release'
requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique within the release'
- optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath
+ optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link'
+ optional :filepath, type: String, desc: 'Deprecated: optional path for a direct asset link'
optional :link_type, type: String, desc: 'The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`'
end
end
@@ -392,7 +393,7 @@ module API
end
def release
- @release ||= user_project.releases.find_by_tag(params[:tag])
+ @release ||= user_project.releases.find_by_tag(declared_params[:tag])
end
def find_latest_release
diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb
index b98ed5ec9ff..1ad5bc8d421 100644
--- a/lib/api/resource_access_tokens.rb
+++ b/lib/api/resource_access_tokens.rb
@@ -92,17 +92,30 @@ module API
success Entities::ResourceAccessTokenWithToken
end
params do
- requires :id, type: String, desc: "The #{source_type} ID", documentation: { example: 2 }
- requires :name, type: String, desc: "Resource access token name", documentation: { example: 'test' }
- requires :scopes, type: Array[String], values: ::Gitlab::Auth.resource_bot_scopes.map(&:to_s),
- desc: "The permissions of the token",
- documentation: { example: %w[api read_repository] }
- optional :access_level, type: Integer,
- values: ALLOWED_RESOURCE_ACCESS_LEVELS.values,
- default: Gitlab::Access::MAINTAINER,
- desc: "The access level of the token in the #{source_type}",
- documentation: { example: 40 }
- optional :expires_at, type: Date, desc: "The expiration date of the token", documentation: { example: '"2021-01-31' }
+ requires :id,
+ type: String,
+ desc: "The #{source_type} ID",
+ documentation: { example: 2 }
+ requires :name,
+ type: String,
+ desc: "Resource access token name",
+ documentation: { example: 'test' }
+ requires :scopes,
+ type: Array[String],
+ values: ::Gitlab::Auth.resource_bot_scopes.map(&:to_s),
+ desc: "The permissions of the token",
+ documentation: { example: %w[api read_repository] }
+ requires :expires_at,
+ type: Date,
+ desc: "The expiration date of the token",
+ default: PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now,
+ documentation: { example: '"2021-01-31' }
+ optional :access_level,
+ type: Integer,
+ values: ALLOWED_RESOURCE_ACCESS_LEVELS.values,
+ default: Gitlab::Access::MAINTAINER,
+ desc: "The access level of the token in the #{source_type}",
+ documentation: { example: 40 }
end
post ':id/access_tokens' do
resource = find_source(source_type, params[:id])
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 7d6e2ee4d4c..5d20444cb54 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -125,11 +125,15 @@ module API
given plantuml_enabled: ->(val) { val } do
requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
end
+ optional :diagramsnet_enabled, type: Boolean, desc: 'Enable Diagrams.net'
+ given diagramsnet_enabled: ->(val) { val } do
+ requires :diagramsnet_url, type: String, desc: 'The Diagrams.net server URL'
+ end
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
optional :project_export_enabled, type: Boolean, desc: 'Enable project export'
optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics'
- optional :push_event_hooks_limit, type: Integer, desc: "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
- optional :push_event_activities_limit, type: Integer, desc: 'Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.'
+ optional :push_event_hooks_limit, type: Integer, desc: "Maximum number of changes (branches or tags) in a single push above which webhooks and integrations are not triggered. Setting to `0` does not disable throttling."
+ optional :push_event_activities_limit, type: Integer, desc: 'Maximum number of changes (branches or tags) in a single push above which a bulk push event is created. Setting to `0` does not disable throttling.'
optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
given recaptcha_enabled: ->(val) { val } do
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
@@ -182,6 +186,7 @@ module API
optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute."
optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute."
optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes"
+ optional :wiki_asciidoc_allow_uri_includes, type: Boolean, desc: "Allow URI includes for AsciiDoc wiki pages"
optional :require_admin_approval_after_user_signup, type: Boolean, desc: 'Require explicit admin approval for new signups'
optional :whats_new_variant, type: String, values: ApplicationSetting.whats_new_variants.keys, desc: "What's new variant, possible values: `all_tiers`, `current_tier`, and `disabled`."
optional :floc_enabled, type: Grape::API::Boolean, desc: 'Enable FloC (Federated Learning of Cohorts)'
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 104848206a3..77872e7d13c 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -113,9 +113,7 @@ module API
authorize! :create_snippet
attrs = process_create_params(declared_params(include_missing: false))
-
- spam_params = ::Spam::SpamParams.new_from_request(request: request)
- service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute
+ service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs).execute
snippet = service_response.payload[:snippet]
if service_response.success?
@@ -162,9 +160,7 @@ module API
validate_params_for_multiple_files(snippet)
attrs = process_update_params(declared_params(include_missing: false))
-
- spam_params = ::Spam::SpamParams.new_from_request(request: request)
- service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute(snippet)
+ service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs, perform_spam_check: true).execute(snippet)
snippet = service_response.payload[:snippet]
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index f2019d785a0..3473b1922d6 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -6,7 +6,7 @@ module API
system_hooks_tags = %w[system_hooks]
- feature_category :integrations
+ feature_category :webhooks
before do
authenticate!
diff --git a/lib/api/topics.rb b/lib/api/topics.rb
index b16b40244d4..c3a753a5cac 100644
--- a/lib/api/topics.rb
+++ b/lib/api/topics.rb
@@ -4,7 +4,7 @@ module API
class Topics < ::API::Base
include PaginationParams
- feature_category :projects
+ feature_category :groups_and_projects
desc 'Get topics' do
detail 'This feature was introduced in GitLab 14.5.'
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3d9af536c3c..ff36a4cfe95 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -99,6 +99,7 @@ module API
optional :exclude_internal, as: :non_internal, type: Boolean, default: false, desc: 'Filters only non internal users'
optional :without_project_bots, type: Boolean, default: false, desc: 'Filters users without project bots'
optional :admins, type: Boolean, default: false, desc: 'Filters only admin users'
+ optional :two_factor, type: String, desc: 'Filter users by Two-factor authentication.'
all_or_none_of :extern_uid, :provider
use :sort_params
@@ -108,10 +109,12 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get feature_category: :user_profile, urgency: :low do
- authenticated_as_admin! if params[:extern_uid].present? && params[:provider].present?
+ index_params = declared_params(include_missing: false)
+
+ authenticated_as_admin! if index_params[:extern_uid].present? && index_params[:provider].present?
unless current_user&.can_read_all_resources?
- params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects)
+ index_params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects)
end
authorized = can?(current_user, :read_users_list)
@@ -121,11 +124,11 @@ module API
# a list of all the users on the GitLab instance. `UsersFinder` performs
# an exact match on the `username` parameter, so we are guaranteed to
# get either 0 or 1 `users` here.
- authorized &&= params[:username].present? if current_user.blank?
+ authorized &&= index_params[:username].present? if current_user.blank?
forbidden!("Not authorized to access /api/v4/users") unless authorized
- users = UsersFinder.new(current_user, params).execute
+ users = UsersFinder.new(current_user, index_params).execute
users = reorder_users(users)
entity = current_user&.can_read_all_resources? ? Entities::UserWithAdmin : Entities::UserBasic
@@ -707,9 +710,13 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- forbidden!('A blocked user must be unblocked to be activated') if user.blocked?
- user.activate
+ result = ::Users::ActivateService.new(current_user).execute(user)
+ if result[:status] == :success
+ true
+ else
+ render_api_error!(result[:message], result[:reason] || :bad_request)
+ end
end
desc 'Approve a pending user. Available only for admins.'
@@ -1239,7 +1246,7 @@ module API
params do
optional :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page'
optional :show_whitespace_in_diffs, type: Boolean, desc: 'Flag indicating the user sees whitespace changes in diffs'
- optional :pass_user_identities_to_ci_jwt, type: Boolean, desc: 'Flag indicating the user passes their external identities as CI information'
+ optional :pass_user_identities_to_ci_jwt, type: Boolean, desc: 'Flag indicating the user passes their external identities to a CI job as part of a JSON web token.'
at_least_one_of :view_diffs_file_by_file, :show_whitespace_in_diffs, :pass_user_identities_to_ci_jwt
end
put "preferences", feature_category: :user_profile, urgency: :high do
diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb
index 7d8c37cd39b..7348ed612fc 100644
--- a/lib/api/v3/github.rb
+++ b/lib/api/v3/github.rb
@@ -29,10 +29,9 @@ module API
feature_category :integrations
before do
- reversible_end_of_life!
-
authorize_jira_user_agent!(request)
authenticate!
+ reversible_end_of_life!
end
helpers do
@@ -50,6 +49,13 @@ module API
# TODO Make the breaking change irreversible https://gitlab.com/gitlab-org/gitlab/-/issues/408148.
def reversible_end_of_life!
not_found! unless Feature.enabled?(:jira_dvcs_end_of_life_amnesty)
+
+ Gitlab::IntegrationsLogger.info(
+ user_id: current_user&.id,
+ namespace: params[:namespace],
+ project: params[:project],
+ message: 'Deprecated Jira DVCS endpoint request'
+ )
end
def authorize_jira_user_agent!(request)
diff --git a/lib/api/validations/validators/absence.rb b/lib/api/validations/validators/absence.rb
index 7858ce7140b..49bf7f4a1c2 100644
--- a/lib/api/validations/validators/absence.rb
+++ b/lib/api/validations/validators/absence.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class Absence < Grape::Validations::Base
+ class Absence < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
return if params.respond_to?(:key?) && !params.key?(attr_name)
diff --git a/lib/api/validations/validators/array_none_any.rb b/lib/api/validations/validators/array_none_any.rb
index 8c064eefbf2..cc86bd1a535 100644
--- a/lib/api/validations/validators/array_none_any.rb
+++ b/lib/api/validations/validators/array_none_any.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class ArrayNoneAny < Grape::Validations::Base
+ class ArrayNoneAny < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
value = params[attr_name]
diff --git a/lib/api/validations/validators/bulk_imports.rb b/lib/api/validations/validators/bulk_imports.rb
index bff3424a0ac..f8ad5ed6d14 100644
--- a/lib/api/validations/validators/bulk_imports.rb
+++ b/lib/api/validations/validators/bulk_imports.rb
@@ -4,7 +4,7 @@ module API
module Validations
module Validators
module BulkImports
- class DestinationSlugPath < Grape::Validations::Base
+ class DestinationSlugPath < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
if Feature.disabled?(:restrict_special_characters_in_namespace_path)
return if params[attr_name] =~ Gitlab::Regex.group_path_regex
@@ -29,7 +29,7 @@ module API
end
end
- class DestinationNamespacePath < Grape::Validations::Base
+ class DestinationNamespacePath < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
return if params[attr_name].blank?
@@ -42,7 +42,7 @@ module API
end
end
- class SourceFullPath < Grape::Validations::Base
+ class SourceFullPath < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
return if params[attr_name] =~ Gitlab::Regex.bulk_import_source_full_path_regex
diff --git a/lib/api/validations/validators/check_assignees_count.rb b/lib/api/validations/validators/check_assignees_count.rb
index 15f48c09a4f..7d90f030f1a 100644
--- a/lib/api/validations/validators/check_assignees_count.rb
+++ b/lib/api/validations/validators/check_assignees_count.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class CheckAssigneesCount < Grape::Validations::Base
+ class CheckAssigneesCount < Grape::Validations::Validators::Base
def self.coerce
lambda do |value|
case value
diff --git a/lib/api/validations/validators/email_or_email_list.rb b/lib/api/validations/validators/email_or_email_list.rb
index 715a29c613d..8aa7c3bc32c 100644
--- a/lib/api/validations/validators/email_or_email_list.rb
+++ b/lib/api/validations/validators/email_or_email_list.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class EmailOrEmailList < Grape::Validations::Base
+ class EmailOrEmailList < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
value = params[attr_name]
diff --git a/lib/api/validations/validators/file_path.rb b/lib/api/validations/validators/file_path.rb
index 268ddc29d4e..7b32e1d71f8 100644
--- a/lib/api/validations/validators/file_path.rb
+++ b/lib/api/validations/validators/file_path.rb
@@ -3,12 +3,12 @@
module API
module Validations
module Validators
- class FilePath < Grape::Validations::Base
+ class FilePath < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
options = @option.is_a?(Hash) ? @option : {}
path_allowlist = options.fetch(:allowlist, [])
path = params[attr_name]
- Gitlab::Utils.check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
+ Gitlab::PathTraversal.check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
rescue StandardError
raise Grape::Exceptions::Validation.new(
params: [@scope.full_name(attr_name)],
diff --git a/lib/api/validations/validators/git_ref.rb b/lib/api/validations/validators/git_ref.rb
index dcb1db6ca33..711c272ab4e 100644
--- a/lib/api/validations/validators/git_ref.rb
+++ b/lib/api/validations/validators/git_ref.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class GitRef < Grape::Validations::Base
+ class GitRef < Grape::Validations::Validators::Base
# There are few checks that a Git reference should pass through to be valid reference.
# The link contains some rules that have been added to this validator.
# https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
diff --git a/lib/api/validations/validators/git_sha.rb b/lib/api/validations/validators/git_sha.rb
index 665d1878b4c..830137a0197 100644
--- a/lib/api/validations/validators/git_sha.rb
+++ b/lib/api/validations/validators/git_sha.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class GitSha < Grape::Validations::Base
+ class GitSha < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
sha = params[attr_name]
diff --git a/lib/api/validations/validators/integer_or_custom_value.rb b/lib/api/validations/validators/integer_or_custom_value.rb
index d2352495948..ff2df858a36 100644
--- a/lib/api/validations/validators/integer_or_custom_value.rb
+++ b/lib/api/validations/validators/integer_or_custom_value.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class IntegerOrCustomValue < Grape::Validations::Base
+ class IntegerOrCustomValue < Grape::Validations::Validators::Base
def initialize(attrs, options, required, scope, **opts)
@custom_values = extract_custom_values(options)
super
@@ -15,7 +15,7 @@ module API
return if value.is_a?(Integer)
return if @custom_values.map(&:downcase).include?(value.to_s.downcase)
- valid_options = Gitlab::Utils.to_exclusive_sentence(['an integer'] + @custom_values)
+ valid_options = Gitlab::Sentence.to_exclusive_sentence(['an integer'] + @custom_values)
raise Grape::Exceptions::Validation.new(
params: [@scope.full_name(attr_name)],
message: "should be #{valid_options}, however got #{value}"
diff --git a/lib/api/validations/validators/limit.rb b/lib/api/validations/validators/limit.rb
index 7e11f1d77cc..781bebac716 100644
--- a/lib/api/validations/validators/limit.rb
+++ b/lib/api/validations/validators/limit.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class Limit < Grape::Validations::Base
+ class Limit < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
value = params[attr_name]
diff --git a/lib/api/validations/validators/project_portable.rb b/lib/api/validations/validators/project_portable.rb
index 3a7ea5ea71e..50ab32b6b7a 100644
--- a/lib/api/validations/validators/project_portable.rb
+++ b/lib/api/validations/validators/project_portable.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class ProjectPortable < Grape::Validations::Base
+ class ProjectPortable < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
portable = params[attr_name]
diff --git a/lib/api/validations/validators/untrusted_regexp.rb b/lib/api/validations/validators/untrusted_regexp.rb
index 3ddea2bd9de..c5560be2e16 100644
--- a/lib/api/validations/validators/untrusted_regexp.rb
+++ b/lib/api/validations/validators/untrusted_regexp.rb
@@ -3,7 +3,7 @@
module API
module Validations
module Validators
- class UntrustedRegexp < Grape::Validations::Base
+ class UntrustedRegexp < Grape::Validations::Validators::Base
def validate_param!(attr_name, params)
value = params[attr_name]
return unless value
diff --git a/lib/atlassian/jira_issue_key_extractor.rb b/lib/atlassian/jira_issue_key_extractor.rb
index 881ba4544b2..17fa40e5676 100644
--- a/lib/atlassian/jira_issue_key_extractor.rb
+++ b/lib/atlassian/jira_issue_key_extractor.rb
@@ -12,7 +12,9 @@ module Atlassian
end
def issue_keys
- @text.scan(@match_regex).flatten.uniq
+ return @text.scan(@match_regex).flatten.uniq if @match_regex.is_a?(Regexp)
+
+ @match_regex.scan(@text).flatten.uniq
end
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index b5e1634004a..d56f852b23c 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -12,7 +12,8 @@ module Backup
LIST_ENVS = {
skipped: 'SKIP',
repositories_storages: 'REPOSITORIES_STORAGES',
- repositories_paths: 'REPOSITORIES_PATHS'
+ repositories_paths: 'REPOSITORIES_PATHS',
+ skip_repositories_paths: 'SKIP_REPOSITORIES_PATHS'
}.freeze
YAML_PERMITTED_CLASSES = [
@@ -176,6 +177,11 @@ module Backup
human_name: _('packages'),
destination_path: 'packages.tar.gz',
task: build_files_task(Settings.packages.storage_path, excludes: ['tmp'])
+ ),
+ 'ci_secure_files' => TaskDefinition.new(
+ human_name: _('ci secure files'),
+ destination_path: 'ci_secure_files.tar.gz',
+ task: build_files_task(Settings.ci_secure_files.storage_path, excludes: ['tmp'])
)
}.freeze
end
@@ -194,7 +200,8 @@ module Backup
Repositories.new(progress,
strategy: strategy,
storages: list_env(:repositories_storages),
- paths: list_env(:repositories_paths)
+ paths: list_env(:repositories_paths),
+ skip_paths: list_env(:skip_repositories_paths)
)
end
@@ -278,7 +285,8 @@ module Backup
installation_type: Gitlab::INSTALLATION_TYPE,
skipped: ENV['SKIP'],
repositories_storages: ENV['REPOSITORIES_STORAGES'],
- repositories_paths: ENV['REPOSITORIES_PATHS']
+ repositories_paths: ENV['REPOSITORIES_PATHS'],
+ skip_repositories_paths: ENV['SKIP_REPOSITORIES_PATHS']
}
end
@@ -292,7 +300,8 @@ module Backup
installation_type: Gitlab::INSTALLATION_TYPE,
skipped: list_env(:skipped).join(','),
repositories_storages: list_env(:repositories_storages).join(','),
- repositories_paths: list_env(:repositories_paths).join(',')
+ repositories_paths: list_env(:repositories_paths).join(','),
+ skip_repositories_paths: list_env(:skip_repositories_paths).join(',')
)
end
diff --git a/lib/backup/repositories.rb b/lib/backup/repositories.rb
index 218df3fcb6c..56726665d14 100644
--- a/lib/backup/repositories.rb
+++ b/lib/backup/repositories.rb
@@ -10,13 +10,15 @@ module Backup
# @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: [])
+ # @param [Array<String>] :paths Filter by specified project paths. Empty means all projects, groups, and snippets.
+ # @param [Array<String>] :skip_paths Skip specified project paths. Empty means all projects, groups, and snippets.
+ def initialize(progress, strategy:, storages: [], paths: [], skip_paths: [])
super(progress)
@strategy = strategy
@storages = storages
@paths = paths
+ @skip_paths = skip_paths
end
override :dump
@@ -42,7 +44,7 @@ module Backup
private
- attr_reader :strategy, :storages, :paths
+ attr_reader :strategy, :storages, :paths, :skip_paths
def remove_all_repositories
return if paths.present?
@@ -84,6 +86,7 @@ module Backup
)
end
+ scope = scope.and(skipped_path_relation) if skip_paths.any?
scope
end
@@ -98,9 +101,20 @@ module Backup
)
end
+ if skip_paths.any?
+ scope = scope.where(project: skipped_path_relation)
+ scope = scope.or(Snippet.where(project: nil)) if !paths.any? && !storages.any?
+ end
+
scope
end
+ def skipped_path_relation
+ Project.where.not(id: Project.where_full_path_in(skip_paths).or(
+ Project.where(namespace_id: Namespace.where_full_path_in(skip_paths).self_and_descendants)
+ ))
+ end
+
def restore_object_pools
PoolRepository.includes(:source_project).find_each do |pool|
progress.puts " - Object pool #{pool.disk_path}..."
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index a86c1bb2892..336d60055e2 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -40,7 +40,7 @@ module Banzai
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
# The XPath query to use for finding text nodes to parse.
- TEXT_QUERY = %Q(descendant-or-self::text()[
+ TEXT_QUERY = %(descendant-or-self::text()[
not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
and contains(., '://')
])
diff --git a/lib/banzai/filter/dollar_math_post_filter.rb b/lib/banzai/filter/dollar_math_post_filter.rb
index 76f69a66e8d..b3c230131e4 100644
--- a/lib/banzai/filter/dollar_math_post_filter.rb
+++ b/lib/banzai/filter/dollar_math_post_filter.rb
@@ -20,13 +20,13 @@ module Banzai
DOLLAR_INLINE_UNTRUSTED =
'(?P<matched>\$(?P<math>(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$)'
DOLLAR_INLINE_UNTRUSTED_REGEX =
- Gitlab::UntrustedRegexp.new(DOLLAR_INLINE_UNTRUSTED, multiline: false)
+ Gitlab::UntrustedRegexp.new(DOLLAR_INLINE_UNTRUSTED, multiline: false).freeze
# Corresponds to the "$$...$$" syntax
DOLLAR_DISPLAY_INLINE_UNTRUSTED =
'(?P<matched>\$\$\ *(?P<math>[^$\n]+?)\ *\$\$)'
DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX =
- Gitlab::UntrustedRegexp.new(DOLLAR_DISPLAY_INLINE_UNTRUSTED, multiline: false)
+ Gitlab::UntrustedRegexp.new(DOLLAR_DISPLAY_INLINE_UNTRUSTED, multiline: false).freeze
# Order dependent. Handle the `$$` syntax before the `$` syntax
DOLLAR_MATH_PIPELINE = [
diff --git a/lib/banzai/filter/inline_alert_metrics_filter.rb b/lib/banzai/filter/inline_alert_metrics_filter.rb
deleted file mode 100644
index a6140d1ac81..00000000000
--- a/lib/banzai/filter/inline_alert_metrics_filter.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that inserts a placeholder element for each
- # reference to an alert dashboard.
- class InlineAlertMetricsFilter < ::Banzai::Filter::InlineEmbedsFilter
- include ::Gitlab::Routing
- # Search params for selecting alert metrics links. A few
- # simple checks is enough to boost performance without
- # the cost of doing a full regex match.
- def xpath_search
- "descendant-or-self::a[contains(@href,'metrics_dashboard') and \
- contains(@href,'prometheus/alerts') and \
- starts-with(@href, '#{gitlab_domain}')]"
- end
-
- # Regular expression matching alert dashboard urls
- def link_pattern
- ::Gitlab::Metrics::Dashboard::Url.alert_regex
- end
-
- private
-
- # Endpoint FE should hit to collect the appropriate
- # chart information
- def metrics_dashboard_url(params)
- metrics_dashboard_namespace_project_prometheus_alert_url(
- params['namespace'],
- params['project'],
- params['alert'],
- embedded: true,
- format: :json,
- **query_params(params['url'])
- )
- end
-
- # Parses query params out from full url string into hash.
- #
- # Ex) 'https://<root>/<project>/metrics_dashboard?title=Title&group=Group'
- # --> { title: 'Title', group: 'Group' }
- def query_params(url)
- ::Gitlab::Metrics::Dashboard::Url.parse_query(url)
- end
- end
- end
-end
diff --git a/lib/banzai/filter/inline_cluster_metrics_filter.rb b/lib/banzai/filter/inline_cluster_metrics_filter.rb
deleted file mode 100644
index a696d3a6f9c..00000000000
--- a/lib/banzai/filter/inline_cluster_metrics_filter.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class InlineClusterMetricsFilter < ::Banzai::Filter::InlineEmbedsFilter
- def embed_params(node)
- url = node['href']
- @query_params = query_params(url)
- return unless [:group, :title, :y_label].all? do |param|
- @query_params.include?(param)
- end
-
- link_pattern.match(url) { |m| m.named_captures }.symbolize_keys
- end
-
- def xpath_search
- "descendant-or-self::a[contains(@href,'clusters') and \
- starts-with(@href, '#{gitlab_domain}')]"
- end
-
- def link_pattern
- ::Gitlab::Metrics::Dashboard::Url.clusters_regex
- end
-
- def metrics_dashboard_url(params)
- ::Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_cluster_url(
- params[:namespace],
- params[:project],
- params[:cluster_id],
- # Only Project clusters are supported for now
- # admin and group cluster types may be supported in the future
- cluster_type: :project,
- embedded: true,
- format: :json,
- **@query_params
- )
- end
- end
- end
-end
diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb
index 2a43540934c..fc77984f135 100644
--- a/lib/banzai/filter/inline_diff_filter.rb
+++ b/lib/banzai/filter/inline_diff_filter.rb
@@ -8,11 +8,11 @@ module Banzai
INLINE_DIFF_DELETION_UNTRUSTED = '(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})'
INLINE_DIFF_DELETION_UNTRUSTED_REGEX =
- Gitlab::UntrustedRegexp.new(INLINE_DIFF_DELETION_UNTRUSTED, multiline: false)
+ Gitlab::UntrustedRegexp.new(INLINE_DIFF_DELETION_UNTRUSTED, multiline: false).freeze
INLINE_DIFF_ADDITION_UNTRUSTED = '(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})'
INLINE_DIFF_ADDITION_UNTRUSTED_REGEX =
- Gitlab::UntrustedRegexp.new(INLINE_DIFF_ADDITION_UNTRUSTED, multiline: false)
+ Gitlab::UntrustedRegexp.new(INLINE_DIFF_ADDITION_UNTRUSTED, multiline: false).freeze
def call
doc.xpath('descendant-or-self::text()').each do |node|
diff --git a/lib/banzai/filter/inline_embeds_filter.rb b/lib/banzai/filter/inline_embeds_filter.rb
deleted file mode 100644
index a16166123f8..00000000000
--- a/lib/banzai/filter/inline_embeds_filter.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that inserts a node for each occurrence of
- # a given link format. To transform references to DB
- # resources in place, prefer to inherit from AbstractReferenceFilter.
- class InlineEmbedsFilter < HTML::Pipeline::Filter
- # Find every relevant link, create a new node based on
- # the link, and insert this node after any html content
- # surrounding the link.
- def call
- return doc if Feature.enabled?(:remove_monitor_metrics)
-
- doc.xpath(xpath_search).each do |node|
- next unless element = element_to_embed(node)
-
- # We want this to follow any surrounding content. For example,
- # if a link is inline in a paragraph.
- node.parent.children.last.add_next_sibling(element)
- end
-
- doc
- end
-
- # Child class must provide the metrics_dashboard_url.
- #
- # Return a Nokogiri::XML::Element to embed in the
- # markdown which provides a url to the metric_dashboard endpoint where
- # data can be requested through a prometheus proxy. InlineMetricsRedactorFilter
- # is responsible for permissions to see this div (and relies on the class 'js-render-metrics' ).
- def create_element(params)
- doc.document.create_element(
- 'div',
- class: 'js-render-metrics',
- 'data-dashboard-url': metrics_dashboard_url(params)
- )
- end
-
- # Implement in child class unless overriding #embed_params
- #
- # Returns the regex pattern used to filter
- # to only matching urls.
- def link_pattern
- end
-
- # Returns the xpath query string used to select nodes
- # from the html document on which the embed is based.
- #
- # Override to select nodes other than links.
- def xpath_search
- 'descendant-or-self::a[@href]'
- end
-
- # Creates a new element based on the parameters
- # obtained from the target link
- def element_to_embed(node)
- return unless params = embed_params(node)
-
- create_element(params)
- end
-
- # Returns a hash of named parameters based on the
- # provided regex with string keys.
- #
- # Override to select nodes other than links.
- def embed_params(node)
- url = node['href']
-
- link_pattern.match(url) { |m| m.named_captures }
- end
-
- # Parses query params out from full url string into hash.
- #
- # Ex) 'https://<root>/<project>/<environment>/metrics?title=Title&group=Group'
- # --> { title: 'Title', group: 'Group' }
- def query_params(url)
- Gitlab::Metrics::Dashboard::Url.parse_query(url)
- end
-
- # Implement in child class.
- #
- # Provides a full url to request the relevant panels of metric data.
- def metrics_dashboard_url
- raise NotImplementedError
- end
-
- def gitlab_domain
- ::Gitlab.config.gitlab.url
- end
- end
- end
-end
diff --git a/lib/banzai/filter/inline_grafana_metrics_filter.rb b/lib/banzai/filter/inline_grafana_metrics_filter.rb
deleted file mode 100644
index 07bde9858e8..00000000000
--- a/lib/banzai/filter/inline_grafana_metrics_filter.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that inserts a placeholder element for each
- # reference to a grafana dashboard.
- class InlineGrafanaMetricsFilter < Banzai::Filter::InlineEmbedsFilter
- # Placeholder element for the frontend to use as an
- # injection point for charts.
- def create_element(params)
- begin_loading_dashboard(params[:url])
-
- super
- end
-
- # @return [Hash<Symbol, String>] with keys :grafana_url, :start, and :end
- def embed_params(node)
- query_params = Gitlab::Metrics::Dashboard::Url.parse_query(node['href'])
-
- time_window = Grafana::TimeWindow.new(query_params[:from], query_params[:to])
- url = url_with_window(node['href'], query_params, time_window.in_milliseconds)
-
- { grafana_url: url }.merge(time_window.formatted)
- end
-
- # Selects any links with an href contains the configured
- # grafana domain for the project
- def xpath_search
- return unless grafana_url.present?
-
- %(descendant-or-self::a[starts-with(@href, '#{grafana_url}')])
- end
-
- private
-
- def project
- context[:project]
- end
-
- def grafana_url
- project&.grafana_integration&.grafana_url
- end
-
- def metrics_dashboard_url(params)
- Gitlab::Routing.url_helpers.project_grafana_api_metrics_dashboard_url(
- project,
- embedded: true,
- **params
- )
- end
-
- # If the provided url is missing time window parameters,
- # this inserts the default window into the url, allowing
- # the embed service to correctly format prometheus
- # queries during embed processing.
- #
- # @param url [String]
- # @param query_params [Hash<Symbol, String>]
- # @param time_window_params [Hash<Symbol, Integer>]
- # @return [String]
- def url_with_window(url, query_params, time_window_params)
- uri = URI(url)
- uri.query = time_window_params.merge(query_params).to_query
-
- uri.to_s
- end
-
- # Fetches a dashboard and caches the result for the
- # FE to fetch quickly while rendering charts
- def begin_loading_dashboard(url)
- ::Gitlab::Metrics::Dashboard::Finder.find(
- project,
- embedded: true,
- grafana_url: url
- )
- end
- end
- end
-end
diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb
deleted file mode 100644
index 2872ad7b632..00000000000
--- a/lib/banzai/filter/inline_metrics_filter.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that inserts a placeholder element for each
- # reference to a metrics dashboard.
- class InlineMetricsFilter < Banzai::Filter::InlineEmbedsFilter
- # Search params for selecting metrics links. A few
- # simple checks is enough to boost performance without
- # the cost of doing a full regex match.
- def xpath_search
- "descendant-or-self::a[contains(@href,'metrics') and \
- starts-with(@href, '#{gitlab_domain}')]"
- end
-
- # Regular expression matching metrics urls
- def link_pattern
- Gitlab::Metrics::Dashboard::Url.metrics_regex
- end
-
- private
-
- # Endpoint FE should hit to collect the appropriate
- # chart information
- def metrics_dashboard_url(params)
- Gitlab::Metrics::Dashboard::Url.build_dashboard_url(
- params['namespace'],
- params['project'],
- params['environment'],
- embedded: true,
- **query_params(params['url']).except(:environment)
- )
- end
- end
- end
-end
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
deleted file mode 100644
index b256815ae84..00000000000
--- a/lib/banzai/filter/inline_metrics_redactor_filter.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that removes embeded elements that the current user does
- # not have permission to view.
- class InlineMetricsRedactorFilter < HTML::Pipeline::Filter
- include Gitlab::Utils::StrongMemoize
-
- METRICS_CSS_CLASS = '.js-render-metrics'
- XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(METRICS_CSS_CLASS).freeze
- EMBED_LIMIT = 100
-
- Route = Struct.new(:regex, :permission)
- Embed = Struct.new(:project_path, :permission)
-
- # Finds all embeds based on the css class the FE
- # uses to identify the embedded content, removing
- # only unnecessary nodes.
- def call
- nodes.each do |node|
- embed = embeds_by_node[node]
- user_has_access = user_access_by_embed[embed]
-
- node.remove unless user_has_access
- end
-
- doc
- end
-
- private
-
- def user
- context[:current_user]
- end
-
- # Returns all nodes which the FE will identify as
- # a metrics embed placeholder element
- #
- # Removes any nodes beyond the first 100
- #
- # @return [Nokogiri::XML::NodeSet]
- def nodes
- strong_memoize(:nodes) do
- nodes = doc.xpath(XPATH)
- nodes.drop(EMBED_LIMIT).each(&:remove)
-
- nodes
- end
- end
-
- # Maps a node to key properties of an embed.
- # Memoized so we only need to run the regex to get
- # the project full path from the url once per node.
- #
- # @return [Hash<Nokogiri::XML::Node, Embed>]
- def embeds_by_node
- strong_memoize(:embeds_by_node) do
- nodes.each_with_object({}) do |node, embeds|
- embed = Embed.new
- url = node.attribute('data-dashboard-url').to_s
-
- permissions_by_route.each do |route|
- set_path_and_permission(embed, url, route.regex, route.permission) unless embed.permission
- end
-
- embeds[node] = embed if embed.permission
- end
- end
- end
-
- def permissions_by_route
- [
- Route.new(
- ::Gitlab::Metrics::Dashboard::Url.metrics_regex,
- :read_environment
- ),
- Route.new(
- ::Gitlab::Metrics::Dashboard::Url.grafana_regex,
- :read_project
- ),
- Route.new(
- ::Gitlab::Metrics::Dashboard::Url.clusters_regex,
- :read_cluster
- ),
- Route.new(
- ::Gitlab::Metrics::Dashboard::Url.alert_regex,
- :read_prometheus_alerts
- )
- ]
- end
-
- # Attempts to determine the path and permission attributes
- # of a url based on expected dashboard url formats and
- # sets the attributes on an Embed object
- #
- # @param embed [Embed]
- # @param url [String]
- # @param regex [RegExp]
- # @param permission [Symbol]
- def set_path_and_permission(embed, url, regex, permission)
- return unless path = regex.match(url) do |m|
- "#{$~[:namespace]}/#{$~[:project]}"
- end
-
- embed.project_path = path
- embed.permission = permission
- end
-
- # Returns a mapping representing whether the current user
- # has permission to view the embed for the project.
- # Determined in a batch
- #
- # @return [Hash<Embed, Boolean>]
- def user_access_by_embed
- strong_memoize(:user_access_by_embed) do
- unique_embeds.each_with_object({}) do |embed, access|
- project = projects_by_path[embed.project_path]
-
- access[embed] = Ability.allowed?(user, embed.permission, project)
- end
- end
- end
-
- # Returns a unique list of embeds
- #
- # @return [Array<Embed>]
- def unique_embeds
- embeds_by_node.values.uniq
- end
-
- # Maps a project's full path to a Project object.
- # Contains all of the Projects referenced in the
- # metrics placeholder elements of the current document
- #
- # @return [Hash<String, Project>]
- def projects_by_path
- strong_memoize(:projects_by_path) do
- Project.eager_load(:route, namespace: [:route])
- .where_full_path_in(unique_project_paths)
- .index_by(&:full_path)
- end
- end
-
- # Returns a list of the full_paths of every project which
- # has an embed in the doc
- #
- # @return [Array<String>]
- def unique_project_paths
- embeds_by_node.values.map(&:project_path).uniq
- end
- end
- end
-end
diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb
index 37734f6a45a..a687ae2882e 100644
--- a/lib/banzai/filter/references/reference_filter.rb
+++ b/lib/banzai/filter/references/reference_filter.rb
@@ -143,7 +143,7 @@ module Banzai
attributes.delete(:original) if context[:no_original_data]
attributes.map do |key, value|
- %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
+ %(data-#{key.to_s.dasherize}="#{escape_once(value)}")
end
.join(' ')
.prepend(reference_type_attribute)
@@ -251,7 +251,7 @@ module Banzai
end
def query
- @query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
+ @query ||= %{descendant-or-self::text()[not(#{ignore_ancestor_query})]
| descendant-or-self::a[
not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
]}
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
index 5983036a8e5..d6b6fdb7149 100644
--- a/lib/banzai/filter/references/user_reference_filter.rb
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -45,7 +45,7 @@ module Banzai
# have `gfm` and `gfm-project_member` class names attached for styling.
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, username|
- if username == 'all' && !skip_project_check?
+ if Feature.disabled?(:disable_all_mention) && username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
else
cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index b119c2ffccf..c2cad237d6f 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -34,7 +34,7 @@ module Banzai
# Allow section elements with data-footnotes attribute
allowlist[:elements].push('section')
allowlist[:attributes]['section'] = %w(data-footnotes)
- allowlist[:attributes]['a'].push('data-footnote-ref', 'data-footnote-backref')
+ allowlist[:attributes]['a'].push('data-footnote-ref', 'data-footnote-backref', 'data-footnote-backref-idx')
allowlist
end
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index f8d03fd6e50..d370a585271 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -42,7 +42,7 @@ module Banzai
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
# The XPath query to use for finding text nodes to parse.
- TEXT_QUERY = %Q(descendant-or-self::text()[
+ TEXT_QUERY = %(descendant-or-self::text()[
not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
and contains(., ']\(')
])
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index d76009d08e1..de3e978b320 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -55,7 +55,7 @@ module Banzai
def anchor_tag(href)
escaped_href = CGI.escape(href) # account for non-ASCII characters
- %Q{<a id="user-content-#{href}" class="anchor" href="##{escaped_href}" aria-hidden="true"></a>}
+ %{<a id="user-content-#{href}" class="anchor" href="##{escaped_href}" aria-hidden="true"></a>}
end
def push_toc(children, root: false)
@@ -69,7 +69,7 @@ module Banzai
end
def push_anchor(header_node)
- result[:toc] << %Q{<li><a href="##{header_node.href}">#{header_node.text}</a>}
+ result[:toc] << %{<li><a href="##{header_node.href}">#{header_node.text}</a>}
push_toc(header_node.children)
result[:toc] << '</li>'
end
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index 6428f71eb8f..c0eb159d517 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -48,7 +48,7 @@ module Banzai
end
def query
- %Q(
+ %(
descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")]
[#{reference_types.join(' or ')}]
)
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 53f938c044f..1d6269c704d 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -26,7 +26,6 @@ module Banzai
Filter::AudioLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
- *metrics_filters,
Filter::TableOfContentsFilter,
Filter::TableOfContentsTagFilter,
Filter::AutolinkFilter,
@@ -44,14 +43,6 @@ module Banzai
]
end
- def self.metrics_filters
- [
- Filter::InlineMetricsFilter,
- Filter::InlineGrafanaMetricsFilter,
- Filter::InlineClusterMetricsFilter
- ]
- end
-
def self.reference_filters
[
Filter::References::UserReferenceFilter,
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index f8035698b9b..86c8d98494d 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -15,7 +15,6 @@ module Banzai
def self.internal_link_filters
[
Filter::ReferenceRedactorFilter,
- Filter::InlineMetricsRedactorFilter,
# UploadLinkFilter must come before RepositoryLinkFilter to
# prevent unnecessary Gitaly calls from being made.
Filter::UploadLinkFilter,
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
index 8d9de2dbc7d..75183b1df95 100644
--- a/lib/bitbucket/representation/pull_request.rb
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -4,7 +4,7 @@ module Bitbucket
module Representation
class PullRequest < Representation::Base
def author
- raw.fetch('author', {}).fetch('nickname', nil)
+ raw.dig('author', 'nickname')
end
def description
@@ -39,19 +39,19 @@ module Bitbucket
end
def source_branch_name
- source_branch.fetch('branch', {}).fetch('name', nil)
+ source_branch.dig('branch', 'name')
end
def source_branch_sha
- source_branch.fetch('commit', {}).fetch('hash', nil)
+ source_branch.dig('commit', 'hash')
end
def target_branch_name
- target_branch.fetch('branch', {}).fetch('name', nil)
+ target_branch.dig('branch', 'name')
end
def target_branch_sha
- target_branch.fetch('commit', {}).fetch('hash', nil)
+ target_branch.dig('commit', 'hash')
end
private
diff --git a/lib/bulk_imports/clients/http.rb b/lib/bulk_imports/clients/http.rb
index 616ab8754b4..c9ed75e663e 100644
--- a/lib/bulk_imports/clients/http.rb
+++ b/lib/bulk_imports/clients/http.rb
@@ -158,15 +158,13 @@ module BulkImports
{ timeout: SIDEKIQ_REQUEST_TIMEOUT } if Gitlab::Runtime.sidekiq?
end
+ # @raise [BulkImports::NetworkError] when unsuccessful
def with_error_handling
response = yield
return response if response.success?
raise ::BulkImports::NetworkError.new("Unsuccessful response #{response.code} from #{response.request.path.path}. Body: #{response.parsed_response}", response: response)
-
- rescue Gitlab::HTTP::BlockedUrlError => e
- raise e
rescue *Gitlab::HTTP::HTTP_ERRORS => e
raise ::BulkImports::NetworkError, e
end
diff --git a/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb b/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb
index 2e6a29f4738..68bd64dc2ff 100644
--- a/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb
+++ b/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb
@@ -18,8 +18,8 @@ module BulkImports
# rubocop: disable CodeReuse/ActiveRecord
def load(_context, file_path)
- Gitlab::Utils.check_path_traversal!(file_path)
- Gitlab::Utils.check_allowed_absolute_path!(file_path, [Dir.tmpdir])
+ Gitlab::PathTraversal.check_path_traversal!(file_path)
+ Gitlab::PathTraversal.check_allowed_absolute_path!(file_path, [Dir.tmpdir])
return if tar_filepath?(file_path)
return if lfs_json_filepath?(file_path)
diff --git a/lib/bulk_imports/common/pipelines/members_pipeline.rb b/lib/bulk_imports/common/pipelines/members_pipeline.rb
index f35eb5ccf5e..548b191dc25 100644
--- a/lib/bulk_imports/common/pipelines/members_pipeline.rb
+++ b/lib/bulk_imports/common/pipelines/members_pipeline.rb
@@ -7,7 +7,7 @@ module BulkImports
include Pipeline
transformer Common::Transformers::ProhibitedAttributesTransformer
- transformer BulkImports::Groups::Transformers::MemberAttributesTransformer
+ transformer Common::Transformers::MemberAttributesTransformer
def extract(context)
graphql_extractor.extract(context)
diff --git a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb
index a1b338aeb9f..06132791ea6 100644
--- a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb
+++ b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb
@@ -22,7 +22,7 @@ module BulkImports
def load(context, file_path)
# Validate that the path is OK to load
- Gitlab::Utils.check_allowed_absolute_path_and_path_traversal!(file_path, [Dir.tmpdir])
+ Gitlab::PathTraversal.check_allowed_absolute_path_and_path_traversal!(file_path, [Dir.tmpdir])
return if File.directory?(file_path)
return if File.lstat(file_path).symlink?
diff --git a/lib/bulk_imports/groups/transformers/member_attributes_transformer.rb b/lib/bulk_imports/common/transformers/member_attributes_transformer.rb
index da50a19ee62..382e6a51a73 100644
--- a/lib/bulk_imports/groups/transformers/member_attributes_transformer.rb
+++ b/lib/bulk_imports/common/transformers/member_attributes_transformer.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module BulkImports
- module Groups
+ module Common
module Transformers
class MemberAttributesTransformer
def transform(context, data)
diff --git a/lib/bulk_imports/file_downloads/validations.rb b/lib/bulk_imports/file_downloads/validations.rb
index ae94267a6e8..b852a50c888 100644
--- a/lib/bulk_imports/file_downloads/validations.rb
+++ b/lib/bulk_imports/file_downloads/validations.rb
@@ -22,7 +22,7 @@ module BulkImports
private
def validate_filepath
- Gitlab::Utils.check_path_traversal!(filepath)
+ Gitlab::PathTraversal.check_path_traversal!(filepath)
end
def validate_content_type
diff --git a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb
index 2d5231b0541..373cd2bd75a 100644
--- a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb
@@ -20,8 +20,8 @@ module BulkImports
end
def load(_context, bundle_path)
- Gitlab::Utils.check_path_traversal!(bundle_path)
- Gitlab::Utils.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir])
+ Gitlab::PathTraversal.check_path_traversal!(bundle_path)
+ Gitlab::PathTraversal.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir])
return unless portable.lfs_enabled?
return unless File.exist?(bundle_path)
diff --git a/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb
index 9a3c582642f..f19d8931f4a 100644
--- a/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb
@@ -21,8 +21,8 @@ module BulkImports
end
def load(_context, bundle_path)
- Gitlab::Utils.check_path_traversal!(bundle_path)
- Gitlab::Utils.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir])
+ Gitlab::PathTraversal.check_path_traversal!(bundle_path)
+ Gitlab::PathTraversal.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir])
return unless File.exist?(bundle_path)
return if File.directory?(bundle_path)
diff --git a/lib/error_tracking/collector/payload_validator.rb b/lib/error_tracking/collector/payload_validator.rb
deleted file mode 100644
index aae19a3635a..00000000000
--- a/lib/error_tracking/collector/payload_validator.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module ErrorTracking
- module Collector
- class PayloadValidator
- PAYLOAD_SCHEMA_PATH = Rails.root.join('app', 'validators', 'json_schemas', 'error_tracking_event_payload.json').to_s
-
- def valid?(payload)
- JSONSchemer.schema(Pathname.new(PAYLOAD_SCHEMA_PATH)).valid?(payload)
- end
- end
- end
-end
diff --git a/lib/error_tracking/collector/sentry_auth_parser.rb b/lib/error_tracking/collector/sentry_auth_parser.rb
deleted file mode 100644
index 4945b8f73e1..00000000000
--- a/lib/error_tracking/collector/sentry_auth_parser.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module ErrorTracking
- module Collector
- class SentryAuthParser
- def self.parse(request)
- # Sentry client sends auth in X-Sentry-Auth header
- #
- # Example of content:
- # "Sentry sentry_version=7, sentry_client=sentry-ruby/4.5.1, sentry_timestamp=1623923398,
- # sentry_key=afadk312..., sentry_secret=123456asd32131..."
- auth = request.headers['X-Sentry-Auth']
-
- # Sentry DSN contains key and secret.
- # The key is required while secret is optional.
- # We are going to use only the key since secret is deprecated.
- public_key = auth[/sentry_key=(\w+)/, 1]
-
- {
- public_key: public_key
- }
- end
- end
- end
-end
diff --git a/lib/error_tracking/collector/sentry_request_parser.rb b/lib/error_tracking/collector/sentry_request_parser.rb
deleted file mode 100644
index ae632ebd518..00000000000
--- a/lib/error_tracking/collector/sentry_request_parser.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module ErrorTracking
- module Collector
- class SentryRequestParser
- def self.parse(request)
- body = request.body.read
-
- # Request body contains 3 json objects merged together in one StringIO.
- # We need to separate and parse them into array of hash objects.
- json_objects = []
- parser = Yajl::Parser.new
-
- parser.parse(body) do |json_object|
- json_objects << json_object
- end
-
- # The request contains 3 objects: sentry metadata, type data and event data.
- # We need only last two. Type to decide what to do with the request.
- # And event data as it contains all information about the exception.
- _, type, event = json_objects
-
- {
- request_type: type['type'],
- event: event
- }
- end
- end
- end
-end
diff --git a/lib/error_tracking/stacktrace_builder.rb b/lib/error_tracking/stacktrace_builder.rb
index 024587e8683..a2d7091a62a 100644
--- a/lib/error_tracking/stacktrace_builder.rb
+++ b/lib/error_tracking/stacktrace_builder.rb
@@ -19,6 +19,7 @@ module ErrorTracking
'lineNo' => entry['lineno'],
'context' => build_stacktrace_context(entry),
'filename' => entry['filename'],
+ 'abs_path' => entry['abs_path'],
'function' => entry['function'],
'colNo' => 0 # we don't support colNo yet.
}
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index 5f73b474956..49ec564eb8d 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -7,6 +7,28 @@ module ExtractsRef
InvalidPathError = Class.new(StandardError)
BRANCH_REF_TYPE = 'heads'
TAG_REF_TYPE = 'tags'
+ REF_TYPES = [BRANCH_REF_TYPE, TAG_REF_TYPE].freeze
+
+ def self.ref_type(type)
+ return unless REF_TYPES.include?(type)
+
+ type
+ end
+
+ def self.qualify_ref(ref, type)
+ validated_type = ref_type(type)
+ return ref unless validated_type
+
+ %(refs/#{validated_type}/#{ref})
+ end
+
+ def self.unqualify_ref(ref, type)
+ validated_type = ref_type(type)
+ return ref unless validated_type
+
+ ref.sub(%r{^refs/#{validated_type}/}, '')
+ end
+
# Given a string containing both a Git tree-ish, such as a branch or tag, and
# a filesystem path joined by forward slashes, attempts to separate the two.
#
@@ -60,7 +82,6 @@ module ExtractsRef
#
# If the :id parameter appears to be requesting a specific response format,
# that will be handled as well.
- #
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def assign_ref_vars
@id, @ref, @path = extract_ref_path
@@ -70,7 +91,7 @@ module ExtractsRef
return unless @ref.present?
@commit = if ref_type
- @fully_qualified_ref = %(refs/#{ref_type}/#{@ref})
+ @fully_qualified_ref = ExtractsRef.qualify_ref(@ref, ref_type)
@repo.commit(@fully_qualified_ref)
else
@repo.commit(@ref)
@@ -90,9 +111,7 @@ module ExtractsRef
end
def ref_type
- return unless params[:ref_type].present?
-
- params[:ref_type] == TAG_REF_TYPE ? TAG_REF_TYPE : BRANCH_REF_TYPE
+ ExtractsRef.ref_type(params[:ref_type])
end
private
@@ -156,6 +175,7 @@ module ExtractsRef
raise NotImplementedError
end
+ # deprecated in favor of ExtractsRef::RequestedRef
def ambiguous_ref?(project, ref)
return false unless ref
return true if project.repository.ambiguous_ref?(ref)
diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb
index 6af24451322..d801070ff1a 100644
--- a/lib/feature/shared.rb
+++ b/lib/feature/shared.rb
@@ -54,6 +54,17 @@ module Feature
example: <<-EOS
experiment(:my_experiment, project: project, actor: current_user) { ...variant code... }
EOS
+ },
+ worker: {
+ description: "Feature flags for controlling Sidekiq workers behavior (e.g. deferring jobs)",
+ optional: true,
+ rollout_issue: false,
+ ee_only: false,
+ default_enabled: false,
+ example: '<<-EOS
+ Feature.enabled?(:"defer_sidekiq_jobs:AuthorizedProjectsWorker", type: :worker,
+ default_enabled_if_undefined: false)
+ EOS'
}
}.freeze
diff --git a/lib/generators/gitlab/analytics/internal_events_generator.rb b/lib/generators/gitlab/analytics/internal_events_generator.rb
new file mode 100644
index 00000000000..a85cdd352d5
--- /dev/null
+++ b/lib/generators/gitlab/analytics/internal_events_generator.rb
@@ -0,0 +1,278 @@
+# frozen_string_literal: true
+
+require 'rails/generators'
+
+module Gitlab
+ module Analytics
+ class InternalEventsGenerator < Rails::Generators::Base
+ TIME_FRAME_DIRS = {
+ '7d' => 'counts_7d',
+ '28d' => 'counts_28d'
+ }.freeze
+
+ TIME_FRAMES_DEFAULT = TIME_FRAME_DIRS.keys.tap do |time_frame_defaults|
+ time_frame_defaults.class_eval do
+ def to_s
+ join(", ")
+ end
+ end
+ end.freeze
+
+ ALLOWED_TIERS = %w[free premium ultimate].dup.tap do |tiers_default|
+ tiers_default.class_eval do
+ def to_s
+ join(", ")
+ end
+ end
+ end.freeze
+
+ NEGATIVE_ANSWERS = %w[no n].freeze
+ POSITIVE_ANSWERS = %w[yes y].freeze
+ TOP_LEVEL_DIR = 'config'
+ TOP_LEVEL_DIR_EE = 'ee'
+ DESCRIPTION_MIN_LENGTH = 50
+ KNOWN_EVENTS_PATH = 'lib/gitlab/usage_data_counters/known_events/common.yml'
+ KNOWN_EVENTS_PATH_EE = 'ee/lib/ee/gitlab/usage_data_counters/known_events/common.yml'
+
+ DESCRIPTION_INQUIRY = %(
+ Please describe in at least #{DESCRIPTION_MIN_LENGTH} characters
+ what %{entity} %{entity_type} represents,
+ consider mentioning: %{considerations}.
+ Your answer will be processed by a full-text search tool and help others find and reuse this %{entity_type}.
+ ).freeze
+
+ source_root File.expand_path('../../../../generator_templates/gitlab_internal_events', __dir__)
+
+ desc 'Generates metric definitions, event definition yml files and known events entries'
+
+ class_option :skip_namespace,
+ hide: true
+ class_option :skip_collision_check,
+ hide: true
+ class_option :time_frames,
+ optional: true,
+ default: TIME_FRAMES_DEFAULT,
+ type: :array,
+ banner: TIME_FRAMES_DEFAULT,
+ desc: "Indicates the metrics time frames. Please select one or more from: #{TIME_FRAMES_DEFAULT}"
+ class_option :tiers,
+ optional: true,
+ default: ALLOWED_TIERS,
+ type: :array,
+ banner: ALLOWED_TIERS,
+ desc: "Indicates the metric's GitLab subscription tiers. Please select one or more from: #{ALLOWED_TIERS}"
+ class_option :group,
+ type: :string,
+ optional: false,
+ desc: 'Name of group that added this metric'
+ class_option :stage,
+ type: :string,
+ optional: false,
+ desc: 'Name of stage that added this metric'
+ class_option :section,
+ type: :string,
+ optional: false,
+ desc: 'Name of section that added this metric'
+ class_option :mr,
+ type: :string,
+ optional: false,
+ desc: 'Merge Request that adds this metric'
+ class_option :event,
+ type: :string,
+ optional: false,
+ desc: 'Name of the event that this metric counts'
+ class_option :unique_on,
+ type: :string,
+ optional: false,
+ desc: 'Name of the event property that this metric counts'
+
+ def create_metric_file
+ validate!
+
+ template "event_definition.yml",
+ event_file_path(event),
+ ask_description(event, "event", "what the event is supposed to track, where, and when")
+
+ time_frames.each do |time_frame|
+ template "metric_definition.yml",
+ metric_file_path(time_frame),
+ key_path(time_frame),
+ time_frame,
+ ask_description(
+ key_path(time_frame),
+ "metric",
+ "events, and event attributes in the description"
+ )
+ end
+
+ # ToDo: Delete during https://gitlab.com/groups/gitlab-org/-/epics/9542 cleanup
+ append_file known_events_file_name, known_event_entry
+ end
+
+ private
+
+ def known_event_entry
+ <<~YML
+ - name: #{event}
+ YML
+ end
+
+ def event_identifiers
+ return unless include_default_event_properties?
+
+ "\n- project\n- user\n- namespace"
+ end
+
+ def include_default_event_properties?
+ question = <<~DESC
+ By convention all events automatically include the following properties:
+ * environment: string,
+ * source: string (eg: ruby, javascript)
+ * user_id: number
+ * project_id: number
+ * namespace_id: number
+ * plan: string (eg: free, premium, ultimate)
+ Would you like to add default properties to the event? Y(es)/N(o)
+ DESC
+
+ answer = Gitlab::TaskHelpers.prompt(question, POSITIVE_ANSWERS + NEGATIVE_ANSWERS)
+ POSITIVE_ANSWERS.include?(answer)
+ end
+
+ def event_file_path(event)
+ path = File.join(TOP_LEVEL_DIR, 'events', "#{event}.yml")
+ path = File.join(TOP_LEVEL_DIR_EE, path) unless free?
+ path
+ end
+
+ def event
+ options[:event]
+ end
+
+ def ask_description(entity, type, considerations)
+ say("")
+ desc = ask(format(DESCRIPTION_INQUIRY, entity: entity, entity_type: type, considerations: considerations))
+
+ while desc.length < DESCRIPTION_MIN_LENGTH
+ error_msg = <<~ERROR
+ Provided description is too short: #{desc.length} of required #{DESCRIPTION_MIN_LENGTH} characters
+ ERROR
+
+ say(set_color(error_msg, :red))
+
+ desc = ask("Please provide description that is #{DESCRIPTION_MIN_LENGTH} characters long.\n")
+ end
+ desc
+ end
+
+ def distributions
+ dist = "\n"
+ dist += "- ce\n" if free?
+
+ "#{dist}- ee"
+ end
+
+ def tiers
+ "\n- #{options[:tiers].join("\n- ")}"
+ end
+
+ def milestone
+ Gitlab::VERSION.match('(\d+\.\d+)').captures.first
+ end
+
+ def class_name
+ 'RedisHLLMetric'
+ end
+
+ def key_path(time_frame)
+ "count_distinct_#{options[:unique_on]}_from_#{event}_#{time_frame}"
+ end
+
+ def metric_file_path(time_frame)
+ path = File.join(TOP_LEVEL_DIR, 'metrics', TIME_FRAME_DIRS[time_frame], "#{key_path(time_frame)}.yml")
+ path = File.join(TOP_LEVEL_DIR_EE, path) unless free?
+ path
+ end
+
+ def known_events_file_name
+ (free? ? KNOWN_EVENTS_PATH : KNOWN_EVENTS_PATH_EE)
+ end
+
+ def validate!
+ raise "Required file: #{known_events_file_name} does not exists." unless File.exist?(known_events_file_name)
+ raise "An event '#{event}' already exists" if event_exists?
+
+ validate_tiers!
+
+ %i[unique_on event mr section stage group].each do |option|
+ raise "The option: --#{option} is missing" unless options.key? option
+ end
+
+ time_frames.each do |time_frame|
+ validate_time_frame!(time_frame)
+ validate_key_path!(time_frame)
+ end
+ end
+
+ def validate_time_frame!(time_frame)
+ return if TIME_FRAME_DIRS.key?(time_frame)
+
+ raise "Invalid time frame: #{time_frame}, allowed options are: #{TIME_FRAMES_DEFAULT}"
+ end
+
+ def validate_tiers!
+ wrong_tiers = options[:tiers] - ALLOWED_TIERS
+ unless wrong_tiers.empty?
+ raise "Tiers option included not allowed values: #{wrong_tiers}. Only allowed values are: #{ALLOWED_TIERS}"
+ end
+
+ return unless options[:tiers].empty?
+
+ raise "At least one tier must be present. Please set --tiers option"
+ end
+
+ def validate_key_path!(time_frame)
+ return unless metric_definition_exists?(time_frame)
+
+ raise "Metric definition with key path '#{key_path(time_frame)}' already exists"
+ end
+
+ def event_exists?
+ return true if ::Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event)
+
+ existing_events_from_definitions.include?(event)
+ end
+
+ def existing_events_from_definitions
+ events_glob_path = File.join(TOP_LEVEL_DIR, 'events', "*.yml")
+ ee_events_glob_path = File.join(TOP_LEVEL_DIR_EE, events_glob_path)
+
+ [ee_events_glob_path, events_glob_path].flat_map do |glob_path|
+ Dir.glob(glob_path).map do |path|
+ YAML.safe_load(File.read(path))["action"]
+ end
+ end
+ end
+
+ def free?
+ options[:tiers].include? "free"
+ end
+
+ def time_frames
+ options[:time_frames]
+ end
+
+ def directory
+ @directory ||= TIME_FRAME_DIRS.find { |d| d.match?(input_dir) }
+ end
+
+ def metric_definitions
+ @definitions ||= Gitlab::Usage::MetricDefinition.definitions(skip_validation: true)
+ end
+
+ def metric_definition_exists?(time_frame)
+ metric_definitions[key_path(time_frame)].present?
+ end
+ end
+ end
+end
diff --git a/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template b/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template
index f8bd502ab77..915b91e43da 100644
--- a/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template
+++ b/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::<%= class_name %>Metric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::<%= class_name %>Metric, feature_category: :service_ping do
let(:expected_value) { 1 }
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index bafda11170a..f1777e059ed 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -23,6 +23,7 @@ module Gitlab
PROTECTION_DEV_CAN_PUSH = 1
PROTECTION_FULL = 2
PROTECTION_DEV_CAN_MERGE = 3
+ PROTECTION_DEV_CAN_INITIAL_PUSH = 4
# Default project creation level
NO_ONE_PROJECT_ACCESS = 0
@@ -95,6 +96,11 @@ module Gitlab
label: s_('DefaultBranchProtection|Fully protected'),
help_text: s_('DefaultBranchProtection|Developers cannot push new commits, but maintainers can. No one can force push.'),
value: PROTECTION_FULL
+ },
+ {
+ label: s_('DefaultBranchProtection|Fully protected after initial push'),
+ help_text: s_('DefaultBranchProtection|Developers can push the initial commit to a repository, but none afterward. Maintainers can always push. No one can force push.'),
+ value: PROTECTION_DEV_CAN_INITIAL_PUSH
}
]
end
diff --git a/lib/gitlab/access/branch_protection.rb b/lib/gitlab/access/branch_protection.rb
index 339a99eb068..6ac8de407b0 100644
--- a/lib/gitlab/access/branch_protection.rb
+++ b/lib/gitlab/access/branch_protection.rb
@@ -34,6 +34,10 @@ module Gitlab
level == PROTECTION_DEV_CAN_PUSH
end
+ def developer_can_initial_push?
+ level == PROTECTION_DEV_CAN_INITIAL_PUSH
+ end
+
def developer_can_merge?
level == PROTECTION_DEV_CAN_MERGE
end
diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb
index 01dcb95eab5..5b136431ce7 100644
--- a/lib/gitlab/alert_management/payload/base.rb
+++ b/lib/gitlab/alert_management/payload/base.rb
@@ -181,7 +181,6 @@ module Gitlab
end
end
- # Overriden in EE::Gitlab::AlertManagement::Payload::Generic
def value_for_paths(paths)
target_path = paths.find { |path| payload&.dig(*path) }
diff --git a/lib/gitlab/alert_management/payload/prometheus.rb b/lib/gitlab/alert_management/payload/prometheus.rb
index 4c36ebbf3aa..76f3da8366b 100644
--- a/lib/gitlab/alert_management/payload/prometheus.rb
+++ b/lib/gitlab/alert_management/payload/prometheus.rb
@@ -94,6 +94,10 @@ module Gitlab
project && title && starts_at_raw
end
+ def source
+ integration&.name || monitoring_tool
+ end
+
private
override :severity_mapping
@@ -131,3 +135,5 @@ module Gitlab
end
end
end
+
+Gitlab::AlertManagement::Payload::Prometheus.prepend_mod
diff --git a/lib/gitlab/analytics/date_filler.rb b/lib/gitlab/analytics/date_filler.rb
index aa3db9f3635..33ebe269f26 100644
--- a/lib/gitlab/analytics/date_filler.rb
+++ b/lib/gitlab/analytics/date_filler.rb
@@ -32,7 +32,7 @@ module Gitlab
# End date of the range
#
# **period**
- # Specifies the period in wich the dates should be generated. Options:
+ # Specifies the period in which the dates should be generated. Options:
#
# - :day, generate date-value pair for each day in the given period
# - :week, generate date-value pair for each week (beginning of the week date)
diff --git a/lib/gitlab/api_authentication/token_locator.rb b/lib/gitlab/api_authentication/token_locator.rb
index df342905d2e..5656ea0d120 100644
--- a/lib/gitlab/api_authentication/token_locator.rb
+++ b/lib/gitlab/api_authentication/token_locator.rb
@@ -8,22 +8,23 @@ module Gitlab
include ActiveModel::Validations
include ActionController::HttpAuthentication::Basic
+ VALID_LOCATIONS = %i[
+ http_basic_auth
+ http_token
+ http_bearer_token
+ http_deploy_token_header
+ http_job_token_header
+ http_private_token_header
+ http_header
+ token_param
+ ].freeze
+
attr_reader :location
- validates :location, inclusion: {
- in: %i[
- http_basic_auth
- http_token
- http_bearer_token
- http_deploy_token_header
- http_job_token_header
- http_private_token_header
- token_param
- ]
- }
+ validates :location, inclusion: { in: VALID_LOCATIONS }
def initialize(location)
- @location = location
+ @location = extract_location(location)
validate!
end
@@ -41,6 +42,8 @@ module Gitlab
extract_from_http_job_token_header request
when :http_private_token_header
extract_from_http_private_token_header request
+ when :http_header
+ extract_from_http_header request
when :token_param
extract_from_token_param request
end
@@ -48,6 +51,16 @@ module Gitlab
private
+ def extract_location(location)
+ case location
+ when Symbol
+ location
+ when Hash
+ result, @token_identifier = location.detect { |k, _v| VALID_LOCATIONS.include?(k) }
+ result
+ end
+ end
+
def extract_from_http_basic_auth(request)
username, password = user_name_and_password(request)
return unless username.present? && password.present?
@@ -96,6 +109,13 @@ module Gitlab
UsernameAndPassword.new(nil, password)
end
+
+ def extract_from_http_header(request)
+ password = request.headers[@token_identifier]
+ return unless password.present?
+
+ UsernameAndPassword.new(nil, password)
+ end
end
end
end
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index a8e74cbd7e6..8d7712951e1 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -45,7 +45,7 @@ module Gitlab
auto_rollback_deployment: { threshold: 1, interval: 3.minutes },
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 },
+ gitlab_shell_operation: { threshold: application_settings.gitlab_shell_operation_limit, interval: 1.minute },
pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute },
temporary_email_failure: { threshold: 300, interval: 1.day },
permanent_email_failure: { threshold: 5, interval: 1.day },
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index d55f2bc8ac9..955cb14594f 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -70,7 +70,8 @@ module Gitlab
.merge({
# Define the Kroki server URL from the settings.
# This attribute cannot be overridden from the AsciiDoc document.
- 'kroki-server-url' => Gitlab::CurrentSettings.kroki_url
+ 'kroki-server-url' => Gitlab::CurrentSettings.kroki_url,
+ 'allow-uri-read' => Gitlab::CurrentSettings.wiki_asciidoc_allow_uri_includes
}),
extensions: extensions }
diff --git a/lib/gitlab/asciidoc/include_processor.rb b/lib/gitlab/asciidoc/include_processor.rb
index 6c4ecc04cdc..ae83dbedf04 100644
--- a/lib/gitlab/asciidoc/include_processor.rb
+++ b/lib/gitlab/asciidoc/include_processor.rb
@@ -9,6 +9,8 @@ module Gitlab
class IncludeProcessor < Asciidoctor::IncludeExt::IncludeProcessor
extend ::Gitlab::Utils::Override
+ NoData = Class.new(StandardError)
+
def initialize(context)
super(logger: Gitlab::AppLogger)
@@ -16,6 +18,7 @@ module Gitlab
@repository = context[:repository] || context[:project].try(:repository)
@max_includes = context[:max_includes].to_i
@included = []
+ @included_content = {}
# Note: Asciidoctor calls #freeze on extensions, so we can't set new
# instance variables after initialization.
@@ -31,9 +34,10 @@ module Gitlab
doc = reader.document
max_include_depth = doc.attributes.fetch('max-include-depth').to_i
+ allow_uri_read = doc.attributes.fetch('allow-uri-read', false)
return false if max_include_depth < 1
- return false if target_http?(target)
+ return false if target_http?(target) && !allow_uri_read
return false if included.size >= max_includes
true
@@ -42,6 +46,7 @@ module Gitlab
override :resolve_target_path
def resolve_target_path(target, reader)
return unless repository.try(:exists?)
+ return target if target_http?(target)
base_path = reader.include_stack.empty? ? requested_path : reader.file
path = resolve_relative_path(target, base_path)
@@ -51,12 +56,15 @@ module Gitlab
override :read_lines
def read_lines(filename, selector)
- blob = read_blob(ref, filename)
+ content = read_content(filename)
+ raise NoData, filename if content.nil?
+
+ included << filename
if selector
- blob.data.each_line.select.with_index(1, &selector)
+ content.each_line.select.with_index(1, &selector)
else
- blob.data
+ content.lines
end
end
@@ -67,7 +75,17 @@ module Gitlab
private
- attr_reader :context, :repository, :cache, :max_includes, :included
+ attr_reader :context, :repository, :cache, :max_includes, :included, :included_content
+
+ def read_content(filename)
+ return included_content[filename] if included_content.key?(filename)
+
+ included_content[filename] = if target_http?(filename)
+ read_uri(filename)
+ else
+ read_blob(ref, filename)
+ end
+ end
# Gets a Blob at a path for a specific revision.
# This method will check that the Blob exists and contains readable text.
@@ -75,16 +93,22 @@ module Gitlab
# revision - The String SHA1.
# path - The String file path.
#
- # Returns a Blob
+ # Returns a string containing the blob content
def read_blob(ref, filename)
blob = repository&.blob_at(ref, filename)
- raise 'Blob not found' unless blob
- raise 'File is not readable' unless blob.readable_text?
+ raise NoData, 'Blob not found' unless blob
+ raise NoData, 'File is not readable' unless blob.readable_text?
- included << filename
+ blob.data
+ end
+
+ def read_uri(uri)
+ r = Gitlab::HTTP.get(uri)
+
+ raise NoData, uri unless r.success?
- blob
+ r.body
end
# Resolves the given relative path of file in repository into canonical
diff --git a/lib/gitlab/audit/auditor.rb b/lib/gitlab/audit/auditor.rb
index e3d2b394404..a59237fbb1f 100644
--- a/lib/gitlab/audit/auditor.rb
+++ b/lib/gitlab/audit/auditor.rb
@@ -77,12 +77,12 @@ module Gitlab
@authentication_provider = @context[:authentication_provider]
# TODO: Remove this code once we close https://gitlab.com/gitlab-org/gitlab/-/issues/367870
- return unless @is_audit_event_yaml_defined
+ return if @is_audit_event_yaml_defined
- # rubocop:disable Gitlab/RailsLogger
- Rails.logger.warn('WARNING: Logging audit events without an event type definition will be deprecated soon.')
- Rails.logger.warn('See https://docs.gitlab.com/ee/development/audit_event_guide/#event-type-definitions')
- # rubocop:enable Gitlab/RailsLogger
+ message = 'Logging audit events without an event type definition will be deprecated soon ' \
+ '(https://docs.gitlab.com/ee/development/audit_event_guide/#event-type-definitions)'
+
+ Gitlab::AppLogger.warn(message: message, event_type: @name)
end
def single_audit
diff --git a/lib/gitlab/audit/type/definition.rb b/lib/gitlab/audit/type/definition.rb
index 81c88a3a0ae..772023616b8 100644
--- a/lib/gitlab/audit/type/definition.rb
+++ b/lib/gitlab/audit/type/definition.rb
@@ -13,6 +13,10 @@ module Gitlab
validate :validate_schema
validate :validate_file_name
+ def self.declarative_policy_class
+ 'AuditEvents::DefinitionPolicy'
+ end
+
InvalidAuditEventTypeError = Class.new(StandardError)
AUDIT_EVENT_TYPE_SCHEMA_PATH = Rails.root.join('config', 'audit_events', 'types', 'type_schema.json')
@@ -78,6 +82,12 @@ module Gitlab
definitions.keys.map(&:to_s)
end
+ def names_with_category
+ definitions.map do |event_name, value|
+ { event_name: event_name, feature_category: value.attributes[:feature_category] }
+ end
+ end
+
def defined?(key)
get(key).present?
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 9268fdd8519..83d94d168a0 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -401,7 +401,7 @@ module Gitlab
scopes = non_admin_available_scopes
if resource.admin? # rubocop: disable Cop/UserAdmin
- scopes += Feature.enabled?(:admin_mode_for_api) ? ADMIN_SCOPES : [SUDO_SCOPE]
+ scopes += ADMIN_SCOPES
end
scopes
diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb
index d1eede65f0c..cce08750296 100644
--- a/lib/gitlab/auth/o_auth/auth_hash.rb
+++ b/lib/gitlab/auth/o_auth/auth_hash.rb
@@ -78,7 +78,7 @@ module Gitlab
def get_from_auth_hash_or_info(key)
if auth_hash.key?(key)
coerce_utf8(auth_hash[key])
- elsif auth_hash.key?(:extra) && auth_hash.extra.key?(:raw_info) && !auth_hash.extra.raw_info[key].nil?
+ elsif auth_hash.key?(:extra) && auth_hash.extra.key?(:raw_info) && !auth_hash.extra.raw_info[key].blank?
coerce_utf8(auth_hash.extra.raw_info[key])
else
get_info(key)
diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb
index a2b0dfd5c66..592d88264e9 100644
--- a/lib/gitlab/auth/saml/auth_hash.rb
+++ b/lib/gitlab/auth/saml/auth_hash.rb
@@ -5,7 +5,7 @@ module Gitlab
module Saml
class AuthHash < Gitlab::Auth::OAuth::AuthHash
def groups
- Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
+ Array.wrap(get_raw(Gitlab::Auth::Saml::Config.new(auth_hash.provider).groups))
end
def authn_context
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 815130aeee2..7524d8b9f85 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -8,26 +8,32 @@ module Gitlab
def enabled?
::AuthHelper.saml_providers.any?
end
+ end
- def options
- Gitlab::Auth::OAuth::Provider.config_for('saml')
- end
+ DEFAULT_PROVIDER_NAME = 'saml'
- def upstream_two_factor_authn_contexts
- options.args[:upstream_two_factor_authn_contexts]
- end
+ def initialize(provider = DEFAULT_PROVIDER_NAME)
+ @provider = provider
+ end
- def groups
- options[:groups_attribute]
- end
+ def options
+ Gitlab::Auth::OAuth::Provider.config_for(@provider)
+ end
- def external_groups
- options[:external_groups]
- end
+ def upstream_two_factor_authn_contexts
+ options.args[:upstream_two_factor_authn_contexts]
+ end
- def admin_groups
- options[:admin_groups]
- end
+ def groups
+ options[:groups_attribute]
+ end
+
+ def external_groups
+ options[:external_groups]
+ end
+
+ def admin_groups
+ options[:admin_groups]
end
end
end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index d14da41deb6..6f72f185c8d 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -43,7 +43,7 @@ module Gitlab
protected
def saml_config
- Gitlab::Auth::Saml::Config
+ Gitlab::Auth::Saml::Config.new(auth_hash.provider)
end
def auto_link_saml_user?
diff --git a/lib/gitlab/authorized_keys.rb b/lib/gitlab/authorized_keys.rb
index e7eba65bea8..3e529a0d2f3 100644
--- a/lib/gitlab/authorized_keys.rb
+++ b/lib/gitlab/authorized_keys.rb
@@ -149,7 +149,7 @@ module Gitlab
raise KeyError, "Invalid public_key: #{key.inspect}"
end
- %Q(command="#{command(id)}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{strip(key)})
+ %(command="#{command(id)}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{strip(key)})
end
def command(id)
diff --git a/lib/gitlab/avatar_cache.rb b/lib/gitlab/avatar_cache.rb
index ed00a279299..f4dcd6f7910 100644
--- a/lib/gitlab/avatar_cache.rb
+++ b/lib/gitlab/avatar_cache.rb
@@ -7,7 +7,7 @@ module Gitlab
# immediate cache expiry of all avatar caches.
#
# @return [Integer]
- VERSION = 1
+ VERSION = 2
# @return [Symbol]
BASE_KEY = :avatar_cache
@@ -65,10 +65,8 @@ module Gitlab
keys = emails.map { |email| email_key(email) }
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- if ::Feature.enabled?(:use_pipeline_over_multikey)
- redis.pipelined do |pipeline|
- keys.each { |key| pipeline.unlink(key) }
- end.sum
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis)
else
redis.unlink(*keys)
end
diff --git a/lib/gitlab/background_migration/backfill_ci_queuing_tables.rb b/lib/gitlab/background_migration/backfill_ci_queuing_tables.rb
deleted file mode 100644
index 63112b52584..00000000000
--- a/lib/gitlab/background_migration/backfill_ci_queuing_tables.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Ensure queuing entries are present even if admins skip upgrades.
- class BackfillCiQueuingTables
- class Namespace < ActiveRecord::Base # rubocop:disable Style/Documentation
- self.table_name = 'namespaces'
- self.inheritance_column = :_type_disabled
- end
-
- class Project < ActiveRecord::Base # rubocop:disable Style/Documentation
- self.table_name = 'projects'
-
- belongs_to :namespace
- has_one :ci_cd_settings, class_name: 'Gitlab::BackgroundMigration::BackfillCiQueuingTables::ProjectCiCdSetting'
-
- def group_runners_enabled?
- return false unless ci_cd_settings
-
- ci_cd_settings.group_runners_enabled?
- end
- end
-
- class ProjectCiCdSetting < ActiveRecord::Base # rubocop:disable Style/Documentation
- self.table_name = 'project_ci_cd_settings'
- end
-
- class Taggings < ActiveRecord::Base # rubocop:disable Style/Documentation
- self.table_name = 'taggings'
- end
-
- module Ci
- class Build < ActiveRecord::Base # rubocop:disable Style/Documentation
- include EachBatch
-
- self.table_name = 'ci_builds'
- self.inheritance_column = :_type_disabled
-
- belongs_to :project
-
- scope :pending, -> do
- where(status: :pending, type: 'Ci::Build', runner_id: nil)
- end
-
- def self.each_batch(of: 1000, column: :id, order: { runner_id: :asc, id: :asc }, order_hint: nil)
- start = except(:select).select(column).reorder(order)
- start = start.take
- return unless start
-
- start_id = start[column]
- arel_table = self.arel_table
-
- 1.step do |index|
- start_cond = arel_table[column].gteq(start_id)
- stop = except(:select).select(column).where(start_cond).reorder(order)
- stop = stop.offset(of).limit(1).take
- relation = where(start_cond)
-
- if stop
- stop_id = stop[column]
- start_id = stop_id
- stop_cond = arel_table[column].lt(stop_id)
- relation = relation.where(stop_cond)
- end
-
- # Any ORDER BYs are useless for this relation and can lead to less
- # efficient UPDATE queries, hence we get rid of it.
- relation = relation.except(:order)
-
- # Using unscoped is necessary to prevent leaking the current scope used by
- # ActiveRecord to chain `each_batch` method.
- unscoped { yield relation, index }
-
- break unless stop
- end
- end
-
- def tags_ids
- BackfillCiQueuingTables::Taggings
- .where(taggable_id: id, taggable_type: 'CommitStatus')
- .pluck(:tag_id)
- end
- end
-
- class PendingBuild < ActiveRecord::Base # rubocop:disable Style/Documentation
- self.table_name = 'ci_pending_builds'
-
- class << self
- def upsert_from_build!(build)
- entry = self.new(args_from_build(build))
-
- self.upsert(
- entry.attributes.compact,
- returning: %w[build_id],
- unique_by: :build_id)
- end
-
- def args_from_build(build)
- project = build.project
-
- {
- build_id: build.id,
- project_id: build.project_id,
- protected: build.protected?,
- namespace_id: project.namespace_id,
- tag_ids: build.tags_ids,
- instance_runners_enabled: project.shared_runners_enabled?,
- namespace_traversal_ids: namespace_traversal_ids(project)
- }
- end
-
- def namespace_traversal_ids(project)
- if project.group_runners_enabled?
- project.namespace.traversal_ids
- else
- []
- end
- end
- end
- end
- end
-
- BATCH_SIZE = 100
-
- def perform(start_id, end_id)
- scope = BackfillCiQueuingTables::Ci::Build.pending.where(id: start_id..end_id)
- pending_builds_query = BackfillCiQueuingTables::Ci::PendingBuild
- .where('ci_builds.id = ci_pending_builds.build_id')
- .select(1)
-
- scope.each_batch(of: BATCH_SIZE) do |builds|
- builds = builds.where('NOT EXISTS (?)', pending_builds_query)
- builds = builds.includes(:project, project: [:namespace, :ci_cd_settings])
-
- builds.each do |build|
- BackfillCiQueuingTables::Ci::PendingBuild.upsert_from_build!(build)
- end
- end
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments)
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/backfill_code_suggestions_namespace_settings.rb b/lib/gitlab/background_migration/backfill_code_suggestions_namespace_settings.rb
new file mode 100644
index 00000000000..2d3bb4bbafa
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_code_suggestions_namespace_settings.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class sets default `code_suggestions` values on the namespace_settings table.
+ # For group namespace, set this to enabled.
+ # For user namespace, set this to disabled.
+ class BackfillCodeSuggestionsNamespaceSettings < BatchedMigrationJob
+ feature_category :code_suggestions
+ operation_name :update_all
+
+ TYPE_VALUE_PAIRS = [
+ { type: 'Group', value: true },
+ { type: 'User', value: false }
+ ].freeze
+
+ NAMESPACES_JOIN = <<~SQL
+ INNER JOIN namespaces
+ ON namespaces.id = namespace_settings.namespace_id
+ SQL
+
+ def perform
+ TYPE_VALUE_PAIRS.each do |pair|
+ each_sub_batch do |sub_batch|
+ sub_batch.joins(NAMESPACES_JOIN)
+ .where(namespaces: { type: pair[:type] })
+ .update_all(code_suggestions: pair[:value])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_group_features.rb b/lib/gitlab/background_migration/backfill_group_features.rb
deleted file mode 100644
index c45dcad5b2d..00000000000
--- a/lib/gitlab/background_migration/backfill_group_features.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Backfill group_features for an array of groups
- class BackfillGroupFeatures < ::Gitlab::BackgroundMigration::BatchedMigrationJob
- job_arguments :batch_size
- operation_name :upsert_group_features
- feature_category :database
-
- def perform
- each_sub_batch(
- batching_arguments: { order_hint: :type },
- batching_scope: ->(relation) { relation.where(type: 'Group') }
- ) do |sub_batch|
- upsert_group_features(sub_batch)
- end
- end
-
- private
-
- def upsert_group_features(relation)
- connection.execute(
- <<~SQL
- INSERT INTO group_features (group_id, created_at, updated_at)
- SELECT namespaces.id as group_id, now(), now()
- FROM namespaces
- WHERE namespaces.type = 'Group' AND namespaces.id IN(#{relation.select(:id).limit(batch_size).to_sql})
- ON CONFLICT (group_id) DO NOTHING;
- SQL
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb b/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb
deleted file mode 100644
index 0585924cb7b..00000000000
--- a/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Backfills the `routes.namespace_id` column, by copying source_id value
- # (for groups and user namespaces source_id == namespace_id)
- class BackfillNamespaceIdForNamespaceRoute
- include Gitlab::Database::DynamicModelHelpers
-
- def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms)
- 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) do |sub_batch|
- batch_metrics.time_operation(:update_all) do
- sub_batch.update_all('namespace_id=source_id')
- end
-
- pause_ms = [0, pause_ms].max
- sleep(pause_ms * 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)
- .joins('inner join namespaces on routes.source_id = namespaces.id')
- .where(source_key_column => start_id..stop_id)
- .where(namespace_id: nil)
- .where(source_type: 'Namespace')
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/backfill_resource_link_events.rb b/lib/gitlab/background_migration/backfill_resource_link_events.rb
new file mode 100644
index 00000000000..a2499e90e1f
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_resource_link_events.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Backfills resource_link_events from system_note_metadata and notes records
+ class BackfillResourceLinkEvents < BatchedMigrationJob
+ operation_name :backfill_resource_link_events
+ feature_category :team_planning
+
+ # AR model for resource_link_events inlined
+ class ResourceLinkEvent < ApplicationRecord
+ self.table_name = 'resource_link_events'
+
+ enum action: {
+ add: 1,
+ remove: 2
+ }
+ end
+
+ scope_to ->(relation) { relation.where("action='relate_to_parent' OR action='unrelate_from_parent'") }
+
+ def perform
+ each_sub_batch do |sub_batch|
+ values_subquery = resource_link_event_values_query(sub_batch.select(:id).to_sql)
+
+ connection.execute(<<~SQL)
+ INSERT INTO resource_link_events (action, issue_id, child_work_item_id, user_id, created_at, system_note_metadata_id)
+ #{values_subquery}
+ ON CONFLICT (system_note_metadata_id) DO NOTHING;
+ SQL
+ end
+ end
+
+ def resource_link_event_values_query(ids_subquery)
+ <<~SQL
+ SELECT
+ CASE WHEN system_note_metadata.action='relate_to_parent' THEN #{ResourceLinkEvent.actions[:add]}
+ ELSE #{ResourceLinkEvent.actions[:remove]}
+ END AS action,
+ parent_issues.id AS issue_id,
+ notes.noteable_id AS child_work_item_id,
+ notes.author_id AS user_id,
+ system_note_metadata.created_at AS created_at,
+ system_note_metadata.id AS system_note_metadata_id
+ FROM system_note_metadata
+ INNER JOIN notes ON system_note_metadata.note_id = notes.id
+ INNER JOIN issues as work_items ON work_items.id = notes.noteable_id,
+ LATERAL (
+ -- This lateral join searches for the id of the parent issue.
+ --
+ -- When a child work item is added to its parent,
+ -- "relate_to_parent" is recorded as `system_note_metadata.action`
+ -- and a note records to which parent the child work item is added e.g, "added #1 (iid) as parent".
+ --
+ -- Based on the iid of the parent extracted from the note and using the child work item's project id,
+ -- we can find out the id of the parent issue.
+ SELECT issues.id
+ FROM issues
+ WHERE
+ issues.project_id = work_items.project_id
+ AND issues.iid = CASE WHEN system_note_metadata.action='relate_to_parent' THEN substring(notes.note from 'added #(\\d+) as parent')::bigint
+ ELSE substring(notes.note from 'removed parent \\S+ #(\\d+)')::bigint
+ END
+ ) parent_issues
+ WHERE
+ system_note_metadata.id IN (#{ids_subquery})
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes.rb b/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes.rb
new file mode 100644
index 00000000000..23c510720c0
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Backfill the following columns on the namespace_root_storage_statistics table:
+ # - public_forks_storage_size
+ # - internal_forks_storage_size
+ # - private_forks_storage_size
+ class BackfillRootStorageStatisticsForkStorageSizes < BatchedMigrationJob
+ operation_name :backfill_root_storage_statistics_fork_sizes
+ feature_category :consumables_cost_management
+
+ VISIBILITY_LEVELS_TO_STORAGE_SIZE_COLUMNS = {
+ 0 => :private_forks_storage_size,
+ 10 => :internal_forks_storage_size,
+ 20 => :public_forks_storage_size
+ }.freeze
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch.each do |root_storage_statistics|
+ next if has_fork_data?(root_storage_statistics)
+
+ namespace_id = root_storage_statistics.namespace_id
+
+ namespace_type = execute("SELECT type FROM namespaces WHERE id = #{namespace_id}").first&.fetch('type')
+
+ next if namespace_type.nil?
+
+ sql = if user_namespace?(namespace_type)
+ user_namespace_sql(namespace_id)
+ else
+ group_namespace_sql(namespace_id)
+ end
+
+ stats = execute(sql)
+ .map { |h| { h['projects_visibility_level'] => h['sum_project_statistics_storage_size'] } }
+ .reduce({}) { |memo, h| memo.merge(h) }
+ .transform_keys { |k| VISIBILITY_LEVELS_TO_STORAGE_SIZE_COLUMNS[k] }
+
+ root_storage_statistics.update!(stats)
+ end
+ end
+ end
+
+ def has_fork_data?(root_storage_statistics)
+ root_storage_statistics.public_forks_storage_size != 0 ||
+ root_storage_statistics.internal_forks_storage_size != 0 ||
+ root_storage_statistics.private_forks_storage_size != 0
+ end
+
+ def user_namespace?(type)
+ type.nil? || type == 'User' || !(type == 'Group' || type == 'Project')
+ end
+
+ def execute(sql)
+ ::ApplicationRecord.connection.execute(sql)
+ end
+
+ def user_namespace_sql(namespace_id)
+ <<~SQL
+ SELECT
+ SUM("project_statistics"."storage_size") AS sum_project_statistics_storage_size,
+ "projects"."visibility_level" AS projects_visibility_level
+ FROM
+ "projects"
+ INNER JOIN "project_statistics" ON "project_statistics"."project_id" = "projects"."id"
+ INNER JOIN "fork_network_members" ON "fork_network_members"."project_id" = "projects"."id"
+ INNER JOIN "fork_networks" ON "fork_networks"."id" = "fork_network_members"."fork_network_id"
+ WHERE
+ "projects"."namespace_id" = #{namespace_id}
+ AND (fork_networks.root_project_id != projects.id)
+ GROUP BY "projects"."visibility_level"
+ SQL
+ end
+
+ def group_namespace_sql(namespace_id)
+ <<~SQL
+ SELECT
+ SUM("project_statistics"."storage_size") AS sum_project_statistics_storage_size,
+ "projects"."visibility_level" AS projects_visibility_level
+ FROM
+ "projects"
+ INNER JOIN "project_statistics" ON "project_statistics"."project_id" = "projects"."id"
+ INNER JOIN "fork_network_members" ON "fork_network_members"."project_id" = "projects"."id"
+ INNER JOIN "fork_networks" ON "fork_networks"."id" = "fork_network_members"."fork_network_id"
+ WHERE
+ "projects"."namespace_id" IN (
+ SELECT namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id
+ FROM "namespaces"
+ WHERE "namespaces"."type" = 'Group' AND (traversal_ids @> ('{#{namespace_id}}'))
+ )
+ AND (fork_networks.root_project_id != projects.id)
+ GROUP BY "projects"."visibility_level"
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex.rb b/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex.rb
deleted file mode 100644
index b703faf6a6c..00000000000
--- a/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Cleanup draft column data inserted by a faulty regex
- #
- class CleanupDraftDataFromFaultyRegex
- # Migration only version of MergeRequest table
- ##
- class MergeRequest < ActiveRecord::Base
- LEAKY_REGEXP_STR = "^\\[draft\\]|\\(draft\\)|draft:|draft|\\[WIP\\]|WIP:|WIP"
- CORRECTED_REGEXP_STR = "^(\\[draft\\]|\\(draft\\)|draft:|draft|\\[WIP\\]|WIP:|WIP)"
-
- include EachBatch
-
- self.table_name = 'merge_requests'
-
- def self.eligible
- where(state_id: 1)
- .where(draft: true)
- .where("title ~* ?", LEAKY_REGEXP_STR)
- .where("title !~* ?", CORRECTED_REGEXP_STR)
- end
- end
-
- def perform(start_id, end_id)
- eligible_mrs = MergeRequest.eligible.where(id: start_id..end_id).pluck(:id)
-
- return if eligible_mrs.empty?
-
- eligible_mrs.each_slice(10) do |slice|
- MergeRequest.where(id: slice).update_all(draft: false)
- end
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- 'CleanupDraftDataFromFaultyRegex',
- arguments
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/encrypt_integration_properties.rb b/lib/gitlab/background_migration/encrypt_integration_properties.rb
deleted file mode 100644
index 28c28ae48eb..00000000000
--- a/lib/gitlab/background_migration/encrypt_integration_properties.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Migrates the integration.properties column from plaintext to encrypted text.
- class EncryptIntegrationProperties
- # The Integration model, with just the relevant bits.
- class Integration < ActiveRecord::Base
- include EachBatch
-
- ALGORITHM = 'aes-256-gcm'
-
- self.table_name = 'integrations'
- self.inheritance_column = :_type_disabled
-
- scope :with_properties, -> { where.not(properties: nil) }
- scope :not_already_encrypted, -> { where(encrypted_properties: nil) }
- scope :for_batch, ->(range) { where(id: range) }
-
- attr_encrypted :encrypted_properties_tmp,
- attribute: :encrypted_properties,
- mode: :per_attribute_iv,
- key: ::Settings.attr_encrypted_db_key_base_32,
- algorithm: ALGORITHM,
- marshal: true,
- marshaler: ::Gitlab::Json,
- encode: false,
- encode_iv: false
-
- # See 'Integration#reencrypt_properties'
- def encrypt_properties
- data = ::Gitlab::Json.parse(properties)
- iv = generate_iv(ALGORITHM)
- ep = self.class.attr_encrypt(:encrypted_properties_tmp, data, { iv: iv })
-
- [ep, iv]
- end
- end
-
- def perform(start_id, stop_id)
- batch_query = Integration.with_properties.not_already_encrypted.for_batch(start_id..stop_id)
- encrypt_batch(batch_query)
- mark_job_as_succeeded(start_id, stop_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments
- )
- end
-
- # represent binary string as a PSQL binary literal:
- # https://www.postgresql.org/docs/9.4/datatype-binary.html
- def bytea(value)
- "'\\x#{value.unpack1('H*')}'::bytea"
- end
-
- def encrypt_batch(batch_query)
- values = batch_query.select(:id, :properties).map do |record|
- encrypted_properties, encrypted_properties_iv = record.encrypt_properties
- "(#{record.id}, #{bytea(encrypted_properties)}, #{bytea(encrypted_properties_iv)})"
- end
-
- return if values.empty?
-
- Integration.connection.execute(<<~SQL.squish)
- WITH cte(cte_id, cte_encrypted_properties, cte_encrypted_properties_iv)
- AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
- SELECT *
- FROM (VALUES #{values.join(',')}) AS t (id, encrypted_properties, encrypted_properties_iv)
- )
- UPDATE #{Integration.table_name}
- SET encrypted_properties = cte_encrypted_properties
- , encrypted_properties_iv = cte_encrypted_properties_iv
- FROM cte
- WHERE cte_id = id
- SQL
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/encrypt_static_object_token.rb b/lib/gitlab/background_migration/encrypt_static_object_token.rb
deleted file mode 100644
index 961dea028c9..00000000000
--- a/lib/gitlab/background_migration/encrypt_static_object_token.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Populates "static_object_token_encrypted" field with encrypted versions
- # of values from "static_object_token" field
- class EncryptStaticObjectToken
- # rubocop:disable Style/Documentation
- class User < ActiveRecord::Base
- include ::EachBatch
- self.table_name = 'users'
- scope :with_static_object_token, -> { where.not(static_object_token: nil) }
- scope :without_static_object_token_encrypted, -> { where(static_object_token_encrypted: nil) }
- end
- # rubocop:enable Style/Documentation
-
- BATCH_SIZE = 100
-
- def perform(start_id, end_id)
- ranged_query = User
- .where(id: start_id..end_id)
- .with_static_object_token
- .without_static_object_token_encrypted
-
- ranged_query.each_batch(of: BATCH_SIZE) do |sub_batch|
- first, last = sub_batch.pick(Arel.sql('min(id), max(id)'))
-
- batch_query = User.unscoped
- .where(id: first..last)
- .with_static_object_token
- .without_static_object_token_encrypted
-
- user_tokens = batch_query.pluck(:id, :static_object_token)
-
- user_encrypted_tokens = user_tokens.map do |(id, plaintext_token)|
- next if plaintext_token.blank?
-
- [id, Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext_token)]
- end
-
- encrypted_tokens_sql = user_encrypted_tokens.compact.map { |(id, token)| "(#{id}, '#{token}')" }.join(',')
-
- next unless user_encrypted_tokens.present?
-
- User.connection.execute(<<~SQL)
- WITH cte(cte_id, cte_token) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
- SELECT *
- FROM (VALUES #{encrypted_tokens_sql}) AS t (id, token)
- )
- UPDATE #{User.table_name}
- SET static_object_token_encrypted = cte_token
- FROM cte
- WHERE cte_id = id
- SQL
- end
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb b/lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb
deleted file mode 100644
index 3772430d0b7..00000000000
--- a/lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Fix project name duplicates and backfill missing project namespace ids
- class FixDuplicateProjectNameAndPath
- SUB_BATCH_SIZE = 10
- # isolated project active record
- class Project < ActiveRecord::Base
- include ::EachBatch
-
- self.table_name = 'projects'
-
- scope :without_project_namespace, -> { where(project_namespace_id: nil) }
- scope :id_in, ->(ids) { where(id: ids) }
- end
-
- def perform(start_id, end_id)
- @project_ids = fetch_project_ids(start_id, end_id)
- backfill_project_namespaces_service = init_backfill_service(project_ids)
- backfill_project_namespaces_service.cleanup_gin_index('projects')
-
- project_ids.each_slice(SUB_BATCH_SIZE) do |ids|
- ApplicationRecord.connection.execute(update_projects_name_and_path_sql(ids))
- end
-
- backfill_project_namespaces_service.backfill_project_namespaces
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- attr_accessor :project_ids
-
- def fetch_project_ids(start_id, end_id)
- Project.without_project_namespace.where(id: start_id..end_id)
- end
-
- def init_backfill_service(project_ids)
- service = Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNamespaces.new
- service.project_ids = project_ids
- service.sub_batch_size = SUB_BATCH_SIZE
-
- service
- end
-
- def update_projects_name_and_path_sql(project_ids)
- <<~SQL
- WITH cte (project_id, path_from_route ) AS (
- #{path_from_route_sql(project_ids).to_sql}
- )
- UPDATE
- projects
- SET
- name = concat(projects.name, '-', id),
- path = CASE
- WHEN projects.path <> cte.path_from_route THEN path_from_route
- ELSE projects.path
- END
- FROM
- cte
- WHERE
- projects.id = cte.project_id;
- SQL
- end
-
- def path_from_route_sql(project_ids)
- Project.without_project_namespace.id_in(project_ids)
- .joins("INNER JOIN routes ON routes.source_id = projects.id AND routes.source_type = 'Project'")
- .select("projects.id, SUBSTRING(routes.path FROM '[^/]+(?=/$|$)') AS path_from_route")
- end
-
- def mark_job_as_succeeded(*arguments)
- ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- 'FixDuplicateProjectNameAndPath',
- arguments
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/fix_incorrect_max_seats_used.rb b/lib/gitlab/background_migration/fix_incorrect_max_seats_used.rb
deleted file mode 100644
index 2c09b8c0b24..00000000000
--- a/lib/gitlab/background_migration/fix_incorrect_max_seats_used.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # rubocop: disable Style/Documentation
- class FixIncorrectMaxSeatsUsed
- def perform(batch = nil)
- end
- end
- end
-end
-
-Gitlab::BackgroundMigration::FixIncorrectMaxSeatsUsed.prepend_mod_with('Gitlab::BackgroundMigration::FixIncorrectMaxSeatsUsed')
diff --git a/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb b/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb
deleted file mode 100644
index db3f98bc2ba..00000000000
--- a/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-# frozen_string_literal: true
-
-require 'parser/ruby27'
-
-module Gitlab
- module BackgroundMigration
- # This migration fixes raw_metadata entries which have incorrectly been passed a Ruby Hash instead of JSON data.
- class FixVulnerabilityOccurrencesWithHashesAsRawMetadata
- CLUSTER_IMAGE_SCANNING_REPORT_TYPE = 7
- GENERIC_REPORT_TYPE = 99
-
- # Type error is used to handle unexpected types when parsing stringified hashes.
- class TypeError < ::StandardError
- attr_reader :message, :type
-
- def initialize(message, type)
- @message = message
- @type = type
- end
- end
-
- # Migration model namespace isolated from application code.
- class Finding < ActiveRecord::Base
- include EachBatch
-
- self.table_name = 'vulnerability_occurrences'
-
- scope :by_api_report_types, -> { where(report_type: [CLUSTER_IMAGE_SCANNING_REPORT_TYPE, GENERIC_REPORT_TYPE]) }
- end
-
- def perform(start_id, end_id)
- Finding.by_api_report_types.where(id: start_id..end_id).each do |finding|
- next if valid_json?(finding.raw_metadata)
-
- metadata = hash_from_s(finding.raw_metadata)
-
- finding.update(raw_metadata: metadata.to_json) if metadata
- end
- mark_job_as_succeeded(start_id, end_id)
- end
-
- def hash_from_s(str_hash)
- ast = Parser::Ruby27.parse(str_hash)
-
- unless ast.type == :hash
- ::Gitlab::AppLogger.error(message: "expected raw_metadata to be a hash", type: ast.type)
- return
- end
-
- parse_hash(ast)
- rescue Parser::SyntaxError => e
- ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message)
- nil
- rescue TypeError => e
- ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message, type: e.type)
- nil
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- 'FixVulnerabilityOccurrencesWithHashesAsRawMetadata',
- arguments
- )
- end
-
- def valid_json?(metadata)
- Oj.load(metadata)
- true
- rescue Oj::ParseError, EncodingError, JSON::ParserError, JSON::GeneratorError, Encoding::UndefinedConversionError
- false
- end
-
- def parse_hash(hash)
- out = {}
- hash.children.each do |node|
- unless node.type == :pair
- raise TypeError.new("expected child of hash to be a `pair`", node.type)
- end
-
- key, value = node.children
-
- key = parse_key(key)
- value = parse_value(value)
-
- out[key] = value
- end
-
- out
- end
-
- def parse_key(key)
- case key.type
- when :sym, :str, :int
- key.children.first
- else
- raise TypeError.new("expected key to be either symbol, string, or integer", key.type)
- end
- end
-
- def parse_value(value)
- case value.type
- when :sym, :str, :int
- value.children.first
- # rubocop:disable Lint/BooleanSymbol
- when :true
- true
- when :false
- false
- # rubocop:enable Lint/BooleanSymbol
- when :nil
- nil
- when :array
- value.children.map { |c| parse_value(c) }
- when :hash
- parse_hash(value)
- else
- raise TypeError.new("value of a pair was an unexpected type", value.type)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/mark_duplicate_npm_packages_for_destruction.rb b/lib/gitlab/background_migration/mark_duplicate_npm_packages_for_destruction.rb
new file mode 100644
index 00000000000..68cc650d130
--- /dev/null
+++ b/lib/gitlab/background_migration/mark_duplicate_npm_packages_for_destruction.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # It seeks duplicate npm packages and mark them for destruction
+ class MarkDuplicateNpmPackagesForDestruction < BatchedMigrationJob
+ NPM_PACKAGE_TYPE = 2
+ PENDING_DESTRUCTION_STATUS = 4
+
+ operation_name :update_all
+ feature_category :package_registry
+
+ # Temporary class to link AR model to the `packages_packages` table
+ class Package < ::ApplicationRecord
+ include EachBatch
+
+ self.table_name = 'packages_packages'
+ end
+
+ def perform
+ distinct_each_batch do |batch|
+ project_ids = batch.pluck(:project_id)
+
+ subquery = Package
+ .where(project_id: project_ids, package_type: NPM_PACKAGE_TYPE)
+ .where.not(status: PENDING_DESTRUCTION_STATUS)
+ .select('project_id, name, version, MAX(id) AS max_id')
+ .group(:project_id, :name, :version)
+ .having('COUNT(*) > 1')
+
+ join_query = <<~SQL.squish
+ INNER JOIN (#{subquery.to_sql}) AS duplicates
+ ON packages_packages.project_id = duplicates.project_id
+ AND packages_packages.name = duplicates.name
+ AND packages_packages.version = duplicates.version
+ SQL
+
+ Package
+ .joins(join_query)
+ .where.not('packages_packages.id = duplicates.max_id')
+ .each_batch do |batch_to_update|
+ batch_to_update.update_all(status: PENDING_DESTRUCTION_STATUS)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/merge_topics_with_same_name.rb b/lib/gitlab/background_migration/merge_topics_with_same_name.rb
deleted file mode 100644
index 07231098a5f..00000000000
--- a/lib/gitlab/background_migration/merge_topics_with_same_name.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # The class to merge project topics with the same case insensitive name
- class MergeTopicsWithSameName
- # Temporary AR model for topics
- class Topic < ActiveRecord::Base
- self.table_name = 'topics'
- end
-
- # Temporary AR model for project topic assignment
- class ProjectTopic < ActiveRecord::Base
- self.table_name = 'project_topics'
- end
-
- def perform(topic_names)
- topic_names.each do |topic_name|
- topics = Topic.where('LOWER(name) = ?', topic_name)
- .order(total_projects_count: :desc, non_private_projects_count: :desc, id: :asc)
- .to_a
- topic_to_keep = topics.shift
- merge_topics(topic_to_keep, topics) if topics.any?
- end
- end
-
- private
-
- def merge_topics(topic_to_keep, topics_to_remove)
- description = topic_to_keep.description
-
- topics_to_remove.each do |topic|
- description ||= topic.description if topic.description.present?
- process_avatar(topic_to_keep, topic) if topic.avatar.present?
-
- ProjectTopic.transaction do
- ProjectTopic.where(topic_id: topic.id)
- .where.not(project_id: ProjectTopic.where(topic_id: topic_to_keep).select(:project_id))
- .update_all(topic_id: topic_to_keep.id)
- ProjectTopic.where(topic_id: topic.id).delete_all
- end
- end
-
- Topic.where(id: topics_to_remove).delete_all
-
- topic_to_keep.update(
- description: description,
- total_projects_count: total_projects_count(topic_to_keep.id),
- non_private_projects_count: non_private_projects_count(topic_to_keep.id)
- )
- end
-
- # We intentionally use application code here because we need to copy/remove avatar files
- def process_avatar(topic_to_keep, topic_to_remove)
- topic_to_remove = ::Projects::Topic.find(topic_to_remove.id)
- topic_to_keep = ::Projects::Topic.find(topic_to_keep.id)
- unless topic_to_keep.avatar.present?
- topic_to_keep.avatar = topic_to_remove.avatar
- topic_to_keep.save!
- end
-
- topic_to_remove.remove_avatar!
- topic_to_remove.save!
- end
-
- def total_projects_count(topic_id)
- ProjectTopic.where(topic_id: topic_id).count
- end
-
- def non_private_projects_count(topic_id)
- ProjectTopic.joins('INNER JOIN projects ON project_topics.project_id = projects.id')
- .where(project_topics: { topic_id: topic_id }).where('projects.visibility_level in (10, 20)').count
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner.rb b/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner.rb
deleted file mode 100644
index 49eff6e2771..00000000000
--- a/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Migrates personal namespace project `maintainer` memberships (for the associated user only) to OWNER
- # Does not create any missing records, simply migrates existing ones
- class MigratePersonalNamespaceProjectMaintainerToOwner
- include Gitlab::Database::DynamicModelHelpers
-
- def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms)
- 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) do |sub_batch|
- batch_metrics.time_operation(:update_all) do
- sub_batch.update_all('access_level = 50')
- end
-
- pause_ms = 0 if pause_ms < 0
- sleep(pause_ms * 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)
- # members of projects within their own personal namespace
-
- # rubocop: disable CodeReuse/ActiveRecord
- define_batchable_model(:members, connection: ApplicationRecord.connection)
- .where(source_key_column => start_id..stop_id)
- .joins("INNER JOIN projects ON members.source_id = projects.id")
- .joins("INNER JOIN namespaces ON projects.namespace_id = namespaces.id")
- .where(type: 'ProjectMember')
- .where("namespaces.type = 'User'")
- .where('members.access_level < 50')
- .where('namespaces.owner_id = members.user_id')
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-end
diff --git a/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb b/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb
deleted file mode 100644
index d7d24960a41..00000000000
--- a/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # The class to migrate category of integrations to third_party_wiki for confluence and shimo
- class MigrateShimoConfluenceIntegrationCategory
- include Gitlab::Database::DynamicModelHelpers
-
- def perform(start_id, end_id)
- define_batchable_model('integrations', connection: ApplicationRecord.connection)
- .where(id: start_id..end_id, type_new: %w[Integrations::Confluence Integrations::Shimo])
- .update_all(category: 'third_party_wiki')
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects.rb b/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects.rb
index 592ef3220ff..74f5bc3f725 100644
--- a/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects.rb
+++ b/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects.rb
@@ -12,7 +12,7 @@ module Gitlab
end
operation_name :update_all
- feature_category :projects
+ feature_category :groups_and_projects
def perform
each_sub_batch do |sub_batch|
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
deleted file mode 100644
index 13b66b2e02e..00000000000
--- a/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # A job to nullify orphan runner_id on ci_builds table
- class NullifyOrphanRunnerIdOnCiBuilds
- include Gitlab::Database::DynamicModelHelpers
-
- def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms)
- 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) do |sub_batch|
- batch_metrics.time_operation(:update_all) do
- filtered_sub_batch(sub_batch).update_all(runner_id: nil)
- end
-
- sleep(pause_ms * 0.001)
- end
- end
-
- def batch_metrics
- @batch_metrics ||= Gitlab::Database::BackgroundMigration::BatchMetrics.new
- end
-
- private
-
- def connection
- ::Ci::ApplicationRecord.connection
- end
-
- 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')
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/populate_container_repository_migration_plan.rb b/lib/gitlab/background_migration/populate_container_repository_migration_plan.rb
deleted file mode 100644
index a9611e9814c..00000000000
--- a/lib/gitlab/background_migration/populate_container_repository_migration_plan.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # The class to populates the migration_plan column of container_repositories
- # with the current plan of the namespaces that owns the container_repository
- #
- # The plan can be NULL, in which case no UPDATE
- # will be executed.
- class PopulateContainerRepositoryMigrationPlan
- def perform(start_id, end_id)
- (start_id..end_id).each do |id|
- execute(<<~SQL)
- WITH selected_plan AS (
- SELECT "plans"."name"
- FROM "container_repositories"
- INNER JOIN "projects" ON "projects"."id" = "container_repositories"."project_id"
- INNER JOIN "namespaces" ON "namespaces"."id" = "projects"."namespace_id"
- INNER JOIN "gitlab_subscriptions" ON "gitlab_subscriptions"."namespace_id" = "namespaces"."traversal_ids"[1]
- INNER JOIN "plans" ON "plans"."id" = "gitlab_subscriptions"."hosted_plan_id"
- WHERE "container_repositories"."id" = #{id}
- )
- UPDATE container_repositories
- SET migration_plan = selected_plan.name
- FROM selected_plan
- WHERE container_repositories.id = #{id};
- SQL
- end
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def connection
- @connection ||= ApplicationRecord.connection
- end
-
- def execute(sql)
- connection.execute(sql)
- end
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/populate_namespace_statistics.rb b/lib/gitlab/background_migration/populate_namespace_statistics.rb
deleted file mode 100644
index 97927ef48c2..00000000000
--- a/lib/gitlab/background_migration/populate_namespace_statistics.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # This class creates/updates those namespace statistics
- # that haven't been created nor initialized.
- # It also updates the related namespace statistics
- class PopulateNamespaceStatistics
- def perform(group_ids, statistics)
- # Updating group statistics might involve calling Gitaly.
- # For example, when calculating `wiki_size`, we will need
- # to perform the request to check if the repo exists and
- # also the repository size.
- #
- # The `allow_n_plus_1_calls` method is only intended for
- # dev and test. It won't be raised in prod.
- ::Gitlab::GitalyClient.allow_n_plus_1_calls do
- relation(group_ids).each do |group|
- upsert_namespace_statistics(group, statistics)
- end
- end
- end
-
- private
-
- def upsert_namespace_statistics(group, statistics)
- response = ::Groups::UpdateStatisticsService.new(group, statistics: statistics).execute
-
- error_message("#{response.message} group: #{group.id}") if response.error?
- end
-
- def logger
- @logger ||= ::Gitlab::BackgroundMigration::Logger.build
- end
-
- def error_message(message)
- logger.error(message: "Namespace Statistics Migration: #{message}")
- end
-
- def relation(group_ids)
- Group.includes(:namespace_statistics).where(id: group_ids)
- end
- end
- end
-end
-
-Gitlab::BackgroundMigration::PopulateNamespaceStatistics.prepend_mod_with('Gitlab::BackgroundMigration::PopulateNamespaceStatistics')
diff --git a/lib/gitlab/background_migration/populate_test_reports_issue_id.rb b/lib/gitlab/background_migration/populate_test_reports_issue_id.rb
deleted file mode 100644
index 301efd0c943..00000000000
--- a/lib/gitlab/background_migration/populate_test_reports_issue_id.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-# rubocop: disable Style/Documentation
-
-module Gitlab
- module BackgroundMigration
- class PopulateTestReportsIssueId
- def perform(start_id, stop_id)
- # NO OP
- end
- end
- end
-end
-
-Gitlab::BackgroundMigration::PopulateTestReportsIssueId.prepend_mod
diff --git a/lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb b/lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb
deleted file mode 100644
index 1f2b55004e4..00000000000
--- a/lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # The class to populates the non private projects counter of topics
- class PopulateTopicsNonPrivateProjectsCount
- SUB_BATCH_SIZE = 100
-
- # Temporary AR model for topics
- class Topic < ActiveRecord::Base
- include EachBatch
-
- self.table_name = 'topics'
- end
-
- def perform(start_id, stop_id)
- Topic.where(id: start_id..stop_id).each_batch(of: SUB_BATCH_SIZE) do |batch|
- ApplicationRecord.connection.execute(<<~SQL)
- WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{batch.select(:id).limit(SUB_BATCH_SIZE).to_sql})
- UPDATE topics
- SET non_private_projects_count = (
- SELECT COUNT(*)
- FROM project_topics
- INNER JOIN projects
- ON project_topics.project_id = projects.id
- WHERE project_topics.topic_id = batched_relation.id
- AND projects.visibility_level > 0
- )
- FROM batched_relation
- WHERE topics.id = batched_relation.id
- SQL
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/populate_vulnerability_reads.rb b/lib/gitlab/background_migration/populate_vulnerability_reads.rb
deleted file mode 100644
index 656c62d9ee5..00000000000
--- a/lib/gitlab/background_migration/populate_vulnerability_reads.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # rubocop:disable Style/Documentation
- class PopulateVulnerabilityReads
- include Gitlab::Database::DynamicModelHelpers
-
- PAUSE_SECONDS = 0.1
-
- def perform(start_id, end_id, sub_batch_size)
- vulnerability_model.where(id: start_id..end_id).each_batch(of: sub_batch_size) do |sub_batch|
- first, last = sub_batch.pick(Arel.sql('min(id), max(id)'))
- connection.execute(insert_query(first, last))
-
- sleep PAUSE_SECONDS
- end
-
- mark_job_as_succeeded(start_id, end_id, sub_batch_size)
- end
-
- private
-
- def vulnerability_model
- define_batchable_model('vulnerabilities', connection: connection)
- end
-
- def connection
- ApplicationRecord.connection
- end
-
- def insert_query(start_id, end_id)
- <<~SQL
- INSERT INTO vulnerability_reads (
- vulnerability_id,
- project_id,
- scanner_id,
- report_type,
- severity,
- state,
- has_issues,
- resolved_on_default_branch,
- uuid,
- location_image
- )
- SELECT
- vulnerabilities.id,
- vulnerabilities.project_id,
- vulnerability_scanners.id,
- vulnerabilities.report_type,
- vulnerabilities.severity,
- vulnerabilities.state,
- CASE
- WHEN
- vulnerability_issue_links.vulnerability_id IS NOT NULL
- THEN
- true
- ELSE
- false
- END
- has_issues,
- vulnerabilities.resolved_on_default_branch,
- vulnerability_occurrences.uuid::uuid,
- vulnerability_occurrences.location ->> 'image'
- FROM
- vulnerabilities
- INNER JOIN vulnerability_occurrences ON vulnerability_occurrences.vulnerability_id = vulnerabilities.id
- INNER JOIN vulnerability_scanners ON vulnerability_scanners.id = vulnerability_occurrences.scanner_id
- LEFT JOIN vulnerability_issue_links ON vulnerability_issue_links.vulnerability_id = vulnerabilities.id
- WHERE vulnerabilities.id BETWEEN #{start_id} AND #{end_id}
- ON CONFLICT(vulnerability_id) DO NOTHING;
- SQL
- end
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments
- )
- end
- end
- # rubocop:enable Style/Documentation
- end
-end
diff --git a/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb b/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
deleted file mode 100644
index 9a42d035285..00000000000
--- a/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
+++ /dev/null
@@ -1,218 +0,0 @@
-# frozen_string_literal: true
-
-# rubocop: disable Style/Documentation
-class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid # rubocop:disable Metrics/ClassLength
- # rubocop: disable Gitlab/NamespacedClass
- class VulnerabilitiesIdentifier < ActiveRecord::Base
- self.table_name = "vulnerability_identifiers"
- has_many :primary_findings, class_name: 'VulnerabilitiesFinding', inverse_of: :primary_identifier, foreign_key: 'primary_identifier_id'
- end
-
- class VulnerabilitiesFinding < ActiveRecord::Base
- include EachBatch
- include ShaAttribute
-
- self.table_name = "vulnerability_occurrences"
-
- has_many :signatures, foreign_key: 'finding_id', class_name: 'VulnerabilityFindingSignature', inverse_of: :finding
- belongs_to :primary_identifier, class_name: 'VulnerabilitiesIdentifier', inverse_of: :primary_findings, foreign_key: 'primary_identifier_id'
-
- REPORT_TYPES = {
- sast: 0,
- dependency_scanning: 1,
- container_scanning: 2,
- dast: 3,
- secret_detection: 4,
- coverage_fuzzing: 5,
- api_fuzzing: 6,
- cluster_image_scanning: 7,
- generic: 99
- }.with_indifferent_access.freeze
- enum report_type: REPORT_TYPES
-
- sha_attribute :fingerprint
- sha_attribute :location_fingerprint
- end
-
- class VulnerabilityFindingSignature < ActiveRecord::Base
- include ShaAttribute
-
- self.table_name = 'vulnerability_finding_signatures'
- belongs_to :finding, foreign_key: 'finding_id', inverse_of: :signatures, class_name: 'VulnerabilitiesFinding'
-
- sha_attribute :signature_sha
- end
-
- class VulnerabilitiesFindingPipeline < ActiveRecord::Base
- include EachBatch
- self.table_name = "vulnerability_occurrence_pipelines"
- end
-
- class Vulnerability < ActiveRecord::Base
- include EachBatch
- self.table_name = "vulnerabilities"
- end
-
- class CalculateFindingUUID
- FINDING_NAMESPACES_IDS = {
- development: "a143e9e2-41b3-47bc-9a19-081d089229f4",
- test: "a143e9e2-41b3-47bc-9a19-081d089229f4",
- staging: "a6930898-a1b2-4365-ab18-12aa474d9b26",
- production: "58dc0f06-936c-43b3-93bb-71693f1b6570"
- }.freeze
-
- NAMESPACE_REGEX = /(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})/.freeze
- PACK_PATTERN = "NnnnnN"
-
- def self.call(value)
- Digest::UUID.uuid_v5(namespace_id, value)
- end
-
- def self.namespace_id
- namespace_uuid = FINDING_NAMESPACES_IDS.fetch(Rails.env.to_sym)
- # Digest::UUID is broken when using an UUID in namespace_id
- # https://github.com/rails/rails/issues/37681#issue-520718028
- namespace_uuid.scan(NAMESPACE_REGEX).flatten.map { |s| s.to_i(16) }.pack(PACK_PATTERN)
- end
- end
- # rubocop: enable Gitlab/NamespacedClass
-
- # rubocop: disable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength
- def perform(start_id, end_id)
- log_info('Migration started', start_id: start_id, end_id: end_id)
-
- VulnerabilitiesFinding
- .joins(:primary_identifier)
- .includes(:signatures)
- .select(:id, :report_type, :primary_identifier_id, :fingerprint, :location_fingerprint, :project_id, :created_at, :vulnerability_id, :uuid)
- .where(id: start_id..end_id)
- .each_batch(of: 50) do |relation|
- duplicates = find_duplicates(relation)
- remove_findings(ids: duplicates) if duplicates.present?
-
- to_update = relation.reject { |finding| duplicates.include?(finding.id) }
-
- begin
- known_uuids = Set.new
- to_be_deleted = []
-
- mappings = to_update.each_with_object({}) do |finding, hash|
- uuid = calculate_uuid_v5_for_finding(finding)
-
- if known_uuids.add?(uuid)
- hash[finding] = { uuid: uuid }
- else
- to_be_deleted << finding.id
- end
- end
-
- # It is technically still possible to have duplicate uuids
- # if the data integrity is broken somehow and the primary identifiers of
- # the findings are pointing to different projects with the same fingerprint values.
- if to_be_deleted.present?
- log_info('Conflicting UUIDs found within the batch', finding_ids: to_be_deleted)
-
- remove_findings(ids: to_be_deleted)
- end
-
- ::Gitlab::Database::BulkUpdate.execute(%i[uuid], mappings) if mappings.present?
-
- log_info('Recalculation is done', finding_ids: mappings.keys.pluck(:id))
- rescue ActiveRecord::RecordNotUnique => error
- log_info('RecordNotUnique error received')
-
- match_data = /\(uuid\)=\((?<uuid>\S{36})\)/.match(error.message)
-
- # This exception returns the **correct** UUIDv5 which probably comes from a later record
- # and it's the one we can drop in the easiest way before retrying the UPDATE query
- if match_data
- uuid = match_data[:uuid]
- log_info('Conflicting UUID found', uuid: uuid)
-
- id = VulnerabilitiesFinding.find_by(uuid: uuid)&.id
- remove_findings(ids: id) if id
- retry
- else
- log_error('Couldnt find conflicting uuid')
-
- Gitlab::ErrorTracking.track_and_raise_exception(error)
- end
- end
- end
-
- mark_job_as_succeeded(start_id, end_id)
- rescue StandardError => error
- log_error('An exception happened')
-
- Gitlab::ErrorTracking.track_and_raise_exception(error)
- end
- # rubocop: disable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength
-
- private
-
- def find_duplicates(relation)
- to_exclude = []
- relation.flat_map do |record|
- # Assuming we're scanning id 31 and the duplicate is id 40
- # first we'd process 31 and add 40 to the list of ids to remove
- # then we would process record 40 and add 31 to the list of removals
- # so we would drop both records
- to_exclude << record.id
-
- VulnerabilitiesFinding.where(
- report_type: record.report_type,
- location_fingerprint: record.location_fingerprint,
- primary_identifier_id: record.primary_identifier_id,
- project_id: record.project_id
- ).where.not(id: to_exclude).pluck(:id)
- end
- end
-
- def remove_findings(ids:)
- ids = Array(ids)
- log_info('Removing Findings and associated records', ids: ids)
-
- vulnerability_ids = VulnerabilitiesFinding.where(id: ids).pluck(:vulnerability_id).uniq.compact
-
- VulnerabilitiesFindingPipeline.where(occurrence_id: ids).each_batch { |batch| batch.delete_all }
- Vulnerability.where(id: vulnerability_ids).each_batch { |batch| batch.delete_all }
- VulnerabilitiesFinding.where(id: ids).delete_all
- end
-
- def calculate_uuid_v5_for_finding(vulnerability_finding)
- return unless vulnerability_finding
-
- signatures = vulnerability_finding.signatures.sort_by { |signature| signature.algorithm_type_before_type_cast }
- location_fingerprint = signatures.last&.signature_sha || vulnerability_finding.location_fingerprint
-
- uuid_v5_name_components = {
- report_type: vulnerability_finding.report_type,
- primary_identifier_fingerprint: vulnerability_finding.fingerprint,
- location_fingerprint: location_fingerprint,
- project_id: vulnerability_finding.project_id
- }
-
- name = uuid_v5_name_components.values.join('-')
-
- CalculateFindingUUID.call(name)
- end
-
- def log_info(message, **extra)
- logger.info(migrator: 'RecalculateVulnerabilitiesOccurrencesUuid', message: message, **extra)
- end
-
- def log_error(message, **extra)
- logger.error(migrator: 'RecalculateVulnerabilitiesOccurrencesUuid', message: message, **extra)
- end
-
- def logger
- @logger ||= Gitlab::BackgroundMigration::Logger.build
- end
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- 'RecalculateVulnerabilitiesOccurrencesUuid',
- arguments
- )
- end
-end
diff --git a/lib/gitlab/background_migration/recalculate_vulnerability_finding_signatures_for_findings.rb b/lib/gitlab/background_migration/recalculate_vulnerability_finding_signatures_for_findings.rb
deleted file mode 100644
index 20200a1d508..00000000000
--- a/lib/gitlab/background_migration/recalculate_vulnerability_finding_signatures_for_findings.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # rubocop: disable Style/Documentation
- class RecalculateVulnerabilityFindingSignaturesForFindings
- def perform(start_id, stop_id)
- end
- end
- end
-end
-
-Gitlab::BackgroundMigration::RecalculateVulnerabilityFindingSignaturesForFindings.prepend_mod
diff --git a/lib/gitlab/background_migration/remove_all_trace_expiration_dates.rb b/lib/gitlab/background_migration/remove_all_trace_expiration_dates.rb
deleted file mode 100644
index d47aa76f24b..00000000000
--- a/lib/gitlab/background_migration/remove_all_trace_expiration_dates.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Removing expire_at timestamps that shouldn't have
- # been written to traces on gitlab.com.
- class RemoveAllTraceExpirationDates
- include Gitlab::Database::MigrationHelpers
-
- BATCH_SIZE = 1_000
-
- # Stubbed class to connect to the CI database
- # connects_to has to be called in abstract classes.
- class MultiDbAdaptableClass < ActiveRecord::Base
- self.abstract_class = true
-
- if Gitlab::Database.has_config?(:ci)
- connects_to database: { writing: :ci, reading: :ci }
- end
- end
-
- # Stubbed class to access the ci_job_artifacts table
- class JobArtifact < MultiDbAdaptableClass
- include EachBatch
-
- self.table_name = 'ci_job_artifacts'
-
- TARGET_TIMESTAMPS = [
- Date.new(2021, 04, 22).midnight.utc,
- Date.new(2021, 05, 22).midnight.utc,
- Date.new(2021, 06, 22).midnight.utc,
- Date.new(2022, 01, 22).midnight.utc,
- Date.new(2022, 02, 22).midnight.utc,
- Date.new(2022, 03, 22).midnight.utc,
- Date.new(2022, 04, 22).midnight.utc
- ].freeze
-
- scope :traces, -> { where(file_type: 3) }
- scope :between, -> (start_id, end_id) { where(id: start_id..end_id) }
- scope :in_targeted_timestamps, -> { where(expire_at: TARGET_TIMESTAMPS) }
- end
-
- def perform(start_id, end_id)
- return unless Gitlab.com?
-
- JobArtifact.traces
- .between(start_id, end_id)
- .in_targeted_timestamps
- .each_batch(of: BATCH_SIZE) { |batch| batch.update_all(expire_at: nil) }
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups.rb b/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups.rb
new file mode 100644
index 00000000000..0a107a136b0
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class removes invalid `protected_environment_deploy_access_levels.group_id` records.
+ class RemoveInvalidDeployAccessLevelGroups < BatchedMigrationJob
+ operation_name :remove_invalid_deploy_access_level_groups
+ feature_category :database
+
+ scope_to ->(relation) do
+ relation.joins('INNER JOIN namespaces ON namespaces.id = protected_environment_deploy_access_levels.group_id')
+ .where.not(protected_environment_deploy_access_levels: { group_id: nil })
+ .where("namespaces.type = 'User'")
+ end
+
+ def perform
+ each_sub_batch(&:delete_all)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups.rb b/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups.rb
index 879e52c96bf..713131edd30 100644
--- a/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups.rb
+++ b/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups.rb
@@ -7,7 +7,7 @@ module Gitlab
class RemoveProjectGroupLinkWithMissingGroups < Gitlab::BackgroundMigration::BatchedMigrationJob
scope_to ->(relation) { relation }
operation_name :delete_all
- feature_category :subgroups
+ feature_category :groups_and_projects
def perform
each_sub_batch do |sub_batch|
diff --git a/lib/gitlab/background_migration/remove_vulnerability_finding_links.rb b/lib/gitlab/background_migration/remove_vulnerability_finding_links.rb
deleted file mode 100644
index 4acef9029f9..00000000000
--- a/lib/gitlab/background_migration/remove_vulnerability_finding_links.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Remove vulnerability finding link records
- # The records will be repopulated from the `raw_metadata`
- # column of `vulnerability_occurrences` once the unique
- # index is in place.
- class RemoveVulnerabilityFindingLinks
- include Gitlab::Database::DynamicModelHelpers
-
- def perform(start_id, stop_id)
- define_batchable_model('vulnerability_finding_links', connection: ApplicationRecord.connection)
- .where(id: start_id..stop_id)
- .delete_all
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb b/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb
deleted file mode 100644
index 190e2fc22fb..00000000000
--- a/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # A job to nullify duplicate runners_token_encrypted values in projects table in batches
- class ResetDuplicateCiRunnersTokenEncryptedValuesOnProjects
- class Project < ActiveRecord::Base # rubocop:disable Style/Documentation
- include EachBatch
-
- self.table_name = 'projects'
-
- scope :base_query, -> { where.not(runners_token_encrypted: nil) }
- end
-
- def perform(start_id, end_id)
- # Reset duplicate runner tokens that would prevent creating an unique index.
- batch_records = Project.base_query.where(id: start_id..end_id)
-
- duplicate_tokens = Project.base_query
- .where(runners_token_encrypted: batch_records.select(:runners_token_encrypted).distinct)
- .group(:runners_token_encrypted)
- .having('COUNT(*) > 1')
- .pluck(:runners_token_encrypted)
-
- batch_records.where(runners_token_encrypted: duplicate_tokens).update_all(runners_token_encrypted: nil) if duplicate_tokens.any?
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb b/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb
deleted file mode 100644
index b58eefa0ab3..00000000000
--- a/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # A job to nullify duplicate ci_runners_token values in projects table in batches
- class ResetDuplicateCiRunnersTokenValuesOnProjects
- class Project < ActiveRecord::Base # rubocop:disable Style/Documentation
- include EachBatch
-
- self.table_name = 'projects'
-
- scope :base_query, -> { where.not(runners_token: nil) }
- end
-
- def perform(start_id, end_id)
- # Reset duplicate runner tokens that would prevent creating an unique index.
- batch_records = Project.base_query.where(id: start_id..end_id)
-
- duplicate_tokens = Project.base_query
- .where(runners_token: batch_records.select(:runners_token).distinct)
- .group(:runners_token)
- .having('COUNT(*) > 1')
- .pluck(:runners_token)
-
- batch_records.where(runners_token: duplicate_tokens).update_all(runners_token: nil) if duplicate_tokens.any?
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- self.class.name.demodulize,
- arguments
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb b/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb
deleted file mode 100644
index b61f2ee7f4c..00000000000
--- a/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Class to populate spent_at for timelogs
- class UpdateTimelogsNullSpentAt
- include Gitlab::Database::DynamicModelHelpers
-
- BATCH_SIZE = 100
-
- def perform(start_id, stop_id)
- define_batchable_model('timelogs', connection: connection)
- .where(spent_at: nil, id: start_id..stop_id)
- .each_batch(of: 100) do |subbatch|
- batch_start, batch_end = subbatch.pick('min(id), max(id)')
-
- update_timelogs(batch_start, batch_end)
- end
- end
-
- def update_timelogs(batch_start, batch_stop)
- execute(<<~SQL)
- UPDATE timelogs
- SET spent_at = created_at
- WHERE spent_at IS NULL
- AND timelogs.id BETWEEN #{batch_start} AND #{batch_stop};
- SQL
- end
-
- def connection
- @connection ||= ApplicationRecord.connection
- end
-
- def execute(sql)
- connection.execute(sql)
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 592e75b1430..e785ce558db 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -10,6 +10,8 @@ module Gitlab
attr_reader :project, :client, :errors, :users
+ ALREADY_IMPORTED_CACHE_KEY = 'bitbucket_cloud-importer/already-imported/%{project}/%{collection}'
+
def initialize(project)
@project = project
@client = Bitbucket::Client.new(project.import_data.credentials)
@@ -31,6 +33,18 @@ module Gitlab
private
+ def already_imported?(collection, iid)
+ Gitlab::Cache::Import::Caching.set_includes?(cache_key(collection), iid)
+ end
+
+ def mark_as_imported(collection, iid)
+ Gitlab::Cache::Import::Caching.set_add(cache_key(collection), iid)
+ end
+
+ def cache_key(collection)
+ format(ALREADY_IMPORTED_CACHE_KEY, project: project.id, collection: collection)
+ end
+
def handle_errors
return unless errors.any?
@@ -97,6 +111,8 @@ module Gitlab
issue_type_id = ::WorkItems::Type.default_issue_type.id
client.issues(repo).each_with_index do |issue, index|
+ next if already_imported?(:issues, issue.iid)
+
# If a user creates an issue while the import is in progress, this can lead to an import failure.
# The workaround is to allocate IIDs before starting the importer.
allocate_issues_internal_id!(project, client) if index == 0
@@ -127,6 +143,8 @@ module Gitlab
updated_at: issue.updated_at
)
+ mark_as_imported(:issues, issue.iid)
+
metrics.issues_counter.increment
gitlab_issue.labels << @labels[label_name]
@@ -179,6 +197,8 @@ module Gitlab
pull_requests = client.pull_requests(repo)
pull_requests.each do |pull_request|
+ next if already_imported?(:pull_requests, pull_request.iid)
+
import_pull_request(pull_request)
end
end
@@ -209,6 +229,8 @@ module Gitlab
updated_at: pull_request.updated_at
)
+ mark_as_imported(:pull_requests, pull_request.iid)
+
metrics.merge_requests_counter.increment
import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
index 6b163cd1b2d..f3253027d57 100644
--- a/lib/gitlab/bitbucket_server_import/importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -442,10 +442,9 @@ module Gitlab
end
def uid(rep_object)
- # We want this explicit to only be username on the FF
- # Otherwise, match email.
- # There should be no default fall-through on username. Fall-through to import user
- if Feature.enabled?(:bitbucket_server_user_mapping_by_username)
+ # We want this to only match either username or email depending on the flag state.
+ # There should be no fall-through.
+ if Feature.enabled?(:bitbucket_server_user_mapping_by_username, type: :ops)
find_user_id(by: :username, value: rep_object.author_username)
else
find_user_id(by: :email, value: rep_object.author_email)
diff --git a/lib/gitlab/bitbucket_server_import/importers/repository_importer.rb b/lib/gitlab/bitbucket_server_import/importers/repository_importer.rb
index cd09ac40e9f..e7a9adf2beb 100644
--- a/lib/gitlab/bitbucket_server_import/importers/repository_importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importers/repository_importer.rb
@@ -17,6 +17,8 @@ module Gitlab
project.repository.import_repository(project.import_url)
project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
+ validate_repository_size!
+
update_clone_time
end
@@ -48,7 +50,13 @@ module Gitlab
def update_clone_time
project.touch(:last_repository_updated_at)
end
+
+ def validate_repository_size!
+ # Defined in EE
+ end
end
end
end
end
+
+Gitlab::BitbucketServerImport::Importers::RepositoryImporter.prepend_mod
diff --git a/lib/gitlab/bitbucket_server_import/user_finder.rb b/lib/gitlab/bitbucket_server_import/user_finder.rb
index f96454eb2cc..68bd2d4851a 100644
--- a/lib/gitlab/bitbucket_server_import/user_finder.rb
+++ b/lib/gitlab/bitbucket_server_import/user_finder.rb
@@ -24,7 +24,7 @@ module Gitlab
def uid(object)
# We want this to only match either username or email depending on the flag state.
# There should be no fall-through.
- if Feature.enabled?(:bitbucket_server_user_mapping_by_username)
+ if Feature.enabled?(:bitbucket_server_user_mapping_by_username, type: :ops)
find_user_id(by: :username, value: object.is_a?(Hash) ? object[:author_username] : object.author_username)
else
find_user_id(by: :email, value: object.is_a?(Hash) ? object[:author_email] : object.author_email)
diff --git a/lib/gitlab/cache/import/caching.rb b/lib/gitlab/cache/import/caching.rb
index 7fec6584ba3..8f2df29c320 100644
--- a/lib/gitlab/cache/import/caching.rb
+++ b/lib/gitlab/cache/import/caching.rb
@@ -162,13 +162,13 @@ module Gitlab
def self.write_multiple(mapping, key_prefix: nil, timeout: TIMEOUT)
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |multi|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
mapping.each do |raw_key, value|
key = cache_key_for("#{key_prefix}#{raw_key}")
validate_redis_value!(value)
- multi.set(key, value, ex: timeout)
+ pipeline.set(key, value, ex: timeout)
end
end
end
diff --git a/lib/gitlab/cache/json_cache.rb b/lib/gitlab/cache/json_cache.rb
new file mode 100644
index 00000000000..7450c7e540b
--- /dev/null
+++ b/lib/gitlab/cache/json_cache.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cache
+ class JsonCache
+ STRATEGY_KEY_COMPONENTS = {
+ revision: Gitlab.revision,
+ version: [Gitlab::VERSION, Rails.version]
+ }.freeze
+
+ def initialize(options = {})
+ @backend = options.fetch(:backend, Rails.cache)
+ @namespace = options.fetch(:namespace, nil)
+ @cache_key_strategy = options.fetch(:cache_key_strategy, :revision)
+ end
+
+ def active?
+ if backend.respond_to?(:active?)
+ backend.active?
+ else
+ true
+ end
+ end
+
+ def expire(key)
+ backend.delete(cache_key(key))
+ end
+
+ def read(key, klass = nil)
+ value = read_raw(key)
+ value = parse_value(value, klass) unless value.nil?
+ value
+ end
+
+ def write(key, value, options = nil)
+ write_raw(key, value, options)
+ end
+
+ def fetch(key, options = {})
+ klass = options.delete(:as)
+ value = read(key, klass)
+
+ return value unless value.nil?
+
+ value = yield
+
+ write(key, value, options)
+
+ value
+ end
+
+ private
+
+ attr_reader :backend, :namespace, :cache_key_strategy
+
+ def cache_key(key)
+ expanded_cache_key(key).compact.join(':').freeze
+ end
+
+ def write_raw(_key, _value, _options)
+ raise NoMethodError
+ end
+
+ def expanded_cache_key(_key)
+ raise NoMethodError
+ end
+
+ def read_raw(_key)
+ raise NoMethodError
+ end
+
+ def parse_value(value, klass)
+ case value
+ when Hash then parse_entry(value, klass)
+ when Array then parse_entries(value, klass)
+ else
+ value
+ end
+ end
+
+ def parse_entry(raw, klass)
+ return unless valid_entry?(raw, klass)
+ return klass.new(raw) unless klass.ancestors.include?(ActiveRecord::Base)
+
+ # When the cached value is a persisted instance of ActiveRecord::Base in
+ # some cases a relation can return an empty collection because scope.none!
+ # is being applied on ActiveRecord::Associations::CollectionAssociation#scope
+ # when the new_record? method incorrectly returns false.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/issues/9903#note_145329964
+ klass.allocate.init_with(encode_for(klass, raw))
+ end
+
+ def encode_for(klass, raw)
+ # We have models that leave out some fields from the JSON export for
+ # security reasons, e.g. models that include the CacheMarkdownField.
+ # The ActiveRecord::AttributeSet we build from raw does know about
+ # these columns so we need manually set them.
+ missing_attributes = (klass.columns.map(&:name) - raw.keys)
+ missing_attributes.each { |column| raw[column] = nil }
+
+ coder = {}
+ klass.new(raw).encode_with(coder)
+ coder["new_record"] = new_record?(raw, klass)
+ coder
+ end
+
+ def new_record?(raw, klass)
+ raw.fetch(klass.primary_key, nil).blank?
+ end
+
+ def valid_entry?(raw, klass)
+ return false unless klass && raw.is_a?(Hash)
+
+ (raw.keys - klass.attribute_names).empty?
+ end
+
+ def parse_entries(values, klass)
+ values.filter_map { |value| parse_entry(value, klass) }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cache/json_caches/json_keyed.rb b/lib/gitlab/cache/json_caches/json_keyed.rb
new file mode 100644
index 00000000000..701a49c23de
--- /dev/null
+++ b/lib/gitlab/cache/json_caches/json_keyed.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cache
+ module JsonCaches
+ class JsonKeyed < JsonCache
+ private
+
+ def expanded_cache_key(key)
+ [namespace, key]
+ end
+
+ def write_raw(key, value, options = nil)
+ raw_value = {}
+
+ begin
+ read_value = backend.read(cache_key(key))
+ read_value = Gitlab::Json.parse(read_value.to_s) unless read_value.nil?
+ raw_value = read_value if read_value.is_a?(Hash)
+ rescue JSON::ParserError
+ end
+
+ raw_value[strategy_key_component] = value
+ backend.write(cache_key(key), raw_value.to_json, options)
+ end
+
+ def read_raw(key)
+ value = backend.read(cache_key(key))
+ value = Gitlab::Json.parse(value.to_s) unless value.nil?
+ value[strategy_key_component] if value.is_a?(Hash)
+ rescue JSON::ParserError
+ nil
+ end
+
+ def strategy_key_component
+ Array.wrap(STRATEGY_KEY_COMPONENTS.fetch(cache_key_strategy)).compact.join(':').freeze
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cache/json_caches/redis_keyed.rb b/lib/gitlab/cache/json_caches/redis_keyed.rb
new file mode 100644
index 00000000000..92709adef63
--- /dev/null
+++ b/lib/gitlab/cache/json_caches/redis_keyed.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cache
+ module JsonCaches
+ class RedisKeyed < JsonCache
+ private
+
+ def expanded_cache_key(key)
+ [namespace, key, *strategy_key_component]
+ end
+
+ def write_raw(key, value, options)
+ backend.write(cache_key(key), value.to_json, options)
+ end
+
+ def read_raw(key)
+ value = backend.read(cache_key(key))
+ value = Gitlab::Json.parse(value.to_s) unless value.nil?
+ value
+ rescue JSON::ParserError
+ nil
+ end
+
+ def strategy_key_component
+ STRATEGY_KEY_COMPONENTS.fetch(cache_key_strategy)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb
index fa7c4972c91..8be1e1716ec 100644
--- a/lib/gitlab/checks/branch_check.rb
+++ b/lib/gitlab/checks/branch_check.rb
@@ -13,7 +13,8 @@ module Gitlab
create_protected_branch: 'You are not allowed to create protected branches on this project.',
invalid_commit_create_protected_branch: 'You can only use an existing protected branch ref as the basis of a new protected branch.',
non_web_create_protected_branch: 'You can only create protected branches using the web interface and API.',
- prohibited_hex_branch_name: 'You cannot create a branch with a 40-character hexadecimal branch name.'
+ prohibited_hex_branch_name: 'You cannot create a branch with a 40-character hexadecimal branch name.',
+ invalid_branch_name: 'You cannot create a branch with an invalid name.'
}.freeze
LOG_MESSAGES = {
@@ -45,6 +46,10 @@ module Gitlab
if branch_name =~ %r{\A\h{40}(/|\z)}
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name]
end
+
+ unless Gitlab::GitRefValidator.validate(branch_name)
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:invalid_branch_name]
+ end
end
def protected_branch_checks
diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb
index 083c2448a0a..1186b532baf 100644
--- a/lib/gitlab/checks/diff_check.rb
+++ b/lib/gitlab/checks/diff_check.rb
@@ -18,7 +18,10 @@ module Gitlab
return unless should_run_validations?
return if commits.empty?
- paths = project.repository.find_changed_paths(commits.map(&:sha))
+ paths = project.repository.find_changed_paths(
+ commits.map(&:sha), merge_commit_diff_mode: :all_parents
+ )
+
paths.each do |path|
validate_path(path)
end
diff --git a/lib/gitlab/ci/badge/release/latest_release.rb b/lib/gitlab/ci/badge/release/latest_release.rb
index 8d84a54787b..8f247006f1a 100644
--- a/lib/gitlab/ci/badge/release/latest_release.rb
+++ b/lib/gitlab/ci/badge/release/latest_release.rb
@@ -19,7 +19,8 @@ module Gitlab::Ci
@release = ::ReleasesFinder.new(
project,
current_user,
- order_by: opts[:order_by]).execute.first
+ order_by_for_latest: opts[:order_by],
+ latest: true).execute.first
end
def entity
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index bc7aad1b186..17b9f30db33 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -6,14 +6,15 @@ module Gitlab
class Rules
include ::Gitlab::Utils::StrongMemoize
- Result = Struct.new(:when, :start_in, :allow_failure, :variables, :needs, :errors) do
+ Result = Struct.new(:when, :start_in, :allow_failure, :variables, :needs, :errors, keyword_init: true) do
def build_attributes
+ needs_job = needs&.dig(:job)
{
when: self.when,
options: { start_in: start_in }.compact,
allow_failure: allow_failure,
- scheduling_type: (:dag if needs),
- needs_attributes: needs&.[](:job)
+ scheduling_type: (:dag if needs_job.present?),
+ needs_attributes: needs_job
}.compact
end
@@ -29,20 +30,25 @@ module Gitlab
def evaluate(pipeline, context)
if @rule_list.nil?
- Result.new(@default_when)
+ Result.new(when: @default_when)
elsif matched_rule = match_rule(pipeline, context)
- Result.new(
- matched_rule.attributes[:when] || @default_when,
- matched_rule.attributes[:start_in],
- matched_rule.attributes[:allow_failure],
- matched_rule.attributes[:variables],
- (matched_rule.attributes[:needs] if Feature.enabled?(:introduce_rules_with_needs, pipeline.project))
+ result = Result.new(
+ when: matched_rule.attributes[:when] || @default_when,
+ start_in: matched_rule.attributes[:start_in],
+ allow_failure: matched_rule.attributes[:allow_failure],
+ variables: matched_rule.attributes[:variables]
)
+
+ if Feature.enabled?(:introduce_rules_with_needs, pipeline.project)
+ result.needs = matched_rule.attributes[:needs]
+ end
+
+ result
else
- Result.new('never')
+ Result.new(when: 'never')
end
rescue Rule::Clause::ParseError => e
- Result.new('never', nil, nil, nil, nil, [e.message])
+ Result.new(when: 'never', errors: [e.message])
end
private
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index b3ff74c14da..b3862b3f186 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -10,7 +10,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[key untracked paths when policy unprotect fallback_keys].freeze
- ALLOWED_POLICY = %w[pull-push push pull].freeze
+ ALLOWED_POLICY = /pull-push|push|pull|\$\w{1,255}*/
DEFAULT_POLICY = 'pull-push'
ALLOWED_WHEN = %w[on_success on_failure always].freeze
DEFAULT_WHEN = 'on_success'
@@ -18,9 +18,9 @@ module Gitlab
validations do
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
- validates :policy, type: String, allow_blank: true, inclusion: {
- in: ALLOWED_POLICY,
- message: "should be one of: #{ALLOWED_POLICY.join(', ')}"
+ validates :policy, type: String, allow_blank: true, format: {
+ with: ALLOWED_POLICY,
+ message: "should be a variable or one of: pull-push, push, pull"
}
with_options allow_nil: true do
diff --git a/lib/gitlab/ci/config/entry/include/rules/rule.rb b/lib/gitlab/ci/config/entry/include/rules/rule.rb
index fa99a7204d6..9cdbd8cd037 100644
--- a/lib/gitlab/ci/config/entry/include/rules/rule.rb
+++ b/lib/gitlab/ci/config/entry/include/rules/rule.rb
@@ -9,10 +9,14 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[if exists].freeze
+ ALLOWED_KEYS = %i[if exists when].freeze
+ ALLOWED_WHEN = %w[never always].freeze
- attributes :if, :exists
+ attributes :if, :exists, :when
+ # Include rules are validated before Entry validations. This is because
+ # the include files are expanded before `compose!` runs in Ci::Config.
+ # The actual validation logic is in lib/gitlab/ci/config/external/rules.rb.
validations do
validates :config, presence: true
validates :config, type: { with: Hash }
diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb
index 6b635cdf33b..61d95c8d4e6 100644
--- a/lib/gitlab/ci/config/external/file/base.rb
+++ b/lib/gitlab/ci/config/external/file/base.rb
@@ -130,8 +130,7 @@ module Gitlab
strong_memoize_attr :content_hash
def interpolator
- External::Interpolator
- .new(content_result, content_inputs, context)
+ Yaml::Interpolator.new(content_result, content_inputs, context)
end
strong_memoize_attr :interpolator
diff --git a/lib/gitlab/ci/config/external/file/project.rb b/lib/gitlab/ci/config/external/file/project.rb
index 16a6bc8a692..de726b57053 100644
--- a/lib/gitlab/ci/config/external/file/project.rb
+++ b/lib/gitlab/ci/config/external/file/project.rb
@@ -68,8 +68,6 @@ module Gitlab
private
def project
- return legacy_project if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
-
# Although we use `where_full_path_in`, this BatchLoader does not reduce the number of queries to 1.
# That's because we use it in the `can_access_local_content?` and `sha` BatchLoaders
# as the `for` parameter. And this loads the project immediately.
@@ -83,10 +81,6 @@ module Gitlab
end
def can_access_local_content?
- if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
- return legacy_can_access_local_content?
- end
-
return if project.nil?
# We are force-loading the project with the `itself` method
@@ -103,7 +97,6 @@ module Gitlab
end
def sha
- return legacy_sha if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
return if project.nil?
# with `itself`, we are force-loading the project
@@ -128,26 +121,6 @@ module Gitlab
end
end
- def legacy_project
- strong_memoize(:legacy_project) do
- ::Project.find_by_full_path(project_name)
- end
- end
-
- def legacy_can_access_local_content?
- strong_memoize(:legacy_can_access_local_content) do
- context.logger.instrument(:config_file_project_validate_access) do
- Ability.allowed?(context.user, :download_code, project)
- end
- end
- end
-
- def legacy_sha
- strong_memoize(:legacy_sha) do
- project.commit(ref_name).try(:sha)
- end
- end
-
override :expand_context_attrs
def expand_context_attrs
{
diff --git a/lib/gitlab/ci/config/external/mapper/verifier.rb b/lib/gitlab/ci/config/external/mapper/verifier.rb
index 3472f2c581a..95975e4661b 100644
--- a/lib/gitlab/ci/config/external/mapper/verifier.rb
+++ b/lib/gitlab/ci/config/external/mapper/verifier.rb
@@ -11,10 +11,6 @@ module Gitlab
# rubocop: disable Metrics/CyclomaticComplexity
def process_without_instrumentation(files)
- if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
- return legacy_process_without_instrumentation(files)
- end
-
files.each do |file|
# When running a pipeline, some Ci::ProjectConfig sources prepend the config content with an
# "internal" `include`. We use this condition to exclude that `include` from the included file set.
@@ -45,30 +41,6 @@ module Gitlab
end
# rubocop: enable Metrics/CyclomaticComplexity
- def legacy_process_without_instrumentation(files)
- files.each do |file|
- # When running a pipeline, some Ci::ProjectConfig sources prepend the config content with an
- # "internal" `include`. We use this condition to exclude that `include` from the included file set.
- context.expandset << file unless context.internal_include?
- verify_max_includes!
-
- verify_execution_time!
-
- file.validate_location!
- file.validate_context! if file.valid?
- file.content if file.valid?
- end
-
- # We do not combine the loops because we need to load the content of all files before continuing
- # to call `BatchLoader` for all locations.
- files.each do |file| # rubocop:disable Style/CombinableLoops
- verify_execution_time!
-
- file.validate_content! if file.valid?
- file.load_and_validate_expanded_hash! if file.valid?
- end
- end
-
def verify_max_includes!
return if context.expandset.count <= context.max_includes
diff --git a/lib/gitlab/ci/config/external/rules.rb b/lib/gitlab/ci/config/external/rules.rb
index 95470537de3..134306332e6 100644
--- a/lib/gitlab/ci/config/external/rules.rb
+++ b/lib/gitlab/ci/config/external/rules.rb
@@ -6,6 +6,7 @@ module Gitlab
module External
class Rules
ALLOWED_KEYS = Entry::Include::Rules::Rule::ALLOWED_KEYS
+ ALLOWED_WHEN = Entry::Include::Rules::Rule::ALLOWED_WHEN
InvalidIncludeRulesError = Class.new(Mapper::Error)
@@ -16,7 +17,17 @@ module Gitlab
end
def evaluate(context)
- Result.new(@rule_list.nil? || match_rule(context))
+ if Feature.enabled?(:ci_support_include_rules_when_never, context.project)
+ if @rule_list.nil?
+ Result.new('always')
+ elsif matched_rule = match_rule(context)
+ Result.new(matched_rule.attributes[:when])
+ else
+ Result.new('never')
+ end
+ else
+ LegacyResult.new(@rule_list.nil? || match_rule(context))
+ end
end
private
@@ -29,13 +40,23 @@ module Gitlab
return unless rule_hashes.is_a?(Array)
rule_hashes.each do |rule_hash|
- next if (rule_hash.keys - ALLOWED_KEYS).empty?
+ next if (rule_hash.keys - ALLOWED_KEYS).empty? && valid_when?(rule_hash)
raise InvalidIncludeRulesError, "invalid include rule: #{rule_hash}"
end
end
- Result = Struct.new(:result) do
+ def valid_when?(rule_hash)
+ rule_hash[:when].nil? || rule_hash[:when].in?(ALLOWED_WHEN)
+ end
+
+ Result = Struct.new(:when) do
+ def pass?
+ self.when != 'never'
+ end
+ end
+
+ LegacyResult = Struct.new(:result) do
def pass?
!!result
end
diff --git a/lib/gitlab/ci/config/yaml.rb b/lib/gitlab/ci/config/yaml.rb
index 729e7e3ac05..f74ef95a832 100644
--- a/lib/gitlab/ci/config/yaml.rb
+++ b/lib/gitlab/ci/config/yaml.rb
@@ -4,51 +4,6 @@ module Gitlab
module Ci
class Config
module Yaml
- AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze
- MAX_DOCUMENTS = 2
-
- class Loader
- def initialize(content, project: nil)
- @content = content
- @project = project
- end
-
- def load!
- ensure_custom_tags
-
- if project.present? && ::Feature.enabled?(:ci_multi_doc_yaml, project)
- ::Gitlab::Config::Loader::MultiDocYaml.new(
- content,
- max_documents: MAX_DOCUMENTS,
- additional_permitted_classes: AVAILABLE_TAGS,
- reject_empty: true
- ).load!
- else
- ::Gitlab::Config::Loader::Yaml
- .new(content, additional_permitted_classes: AVAILABLE_TAGS)
- .load!
- end
- end
-
- def to_result
- Yaml::Result.new(config: load!, error: nil)
- rescue ::Gitlab::Config::Loader::FormatError => e
- Yaml::Result.new(error: e)
- end
-
- private
-
- attr_reader :content, :project
-
- def ensure_custom_tags
- @ensure_custom_tags ||= begin
- AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
-
- true
- end
- end
- end
-
class << self
def load!(content, project: nil)
Loader.new(content, project: project).to_result.then do |result|
diff --git a/lib/gitlab/ci/config/external/interpolator.rb b/lib/gitlab/ci/config/yaml/interpolator.rb
index f8af77fb246..4ae191dfedf 100644
--- a/lib/gitlab/ci/config/external/interpolator.rb
+++ b/lib/gitlab/ci/config/yaml/interpolator.rb
@@ -3,9 +3,9 @@
module Gitlab
module Ci
class Config
- module External
+ module Yaml
##
- # Config::External::Interpolation perform includable file interpolation, and surfaces all possible interpolation
+ # Config::Yaml::Interpolation performs includable file interpolation, and surfaces all possible interpolation
# errors. It is designed to provide an external file's validation context too.
#
class Interpolator
diff --git a/lib/gitlab/ci/config/yaml/loader.rb b/lib/gitlab/ci/config/yaml/loader.rb
new file mode 100644
index 00000000000..924a1f2e46b
--- /dev/null
+++ b/lib/gitlab/ci/config/yaml/loader.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Yaml
+ class Loader
+ AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze
+ MAX_DOCUMENTS = 2
+
+ def initialize(content, project: nil)
+ @content = content
+ @project = project
+ end
+
+ def to_result
+ Yaml::Result.new(config: load!, error: nil)
+ rescue ::Gitlab::Config::Loader::FormatError => e
+ Yaml::Result.new(error: e)
+ end
+
+ private
+
+ attr_reader :content, :project
+
+ def ensure_custom_tags
+ @ensure_custom_tags ||= begin
+ AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
+
+ true
+ end
+ end
+
+ def load!
+ ensure_custom_tags
+
+ ::Gitlab::Config::Loader::MultiDocYaml.new(
+ content,
+ max_documents: MAX_DOCUMENTS,
+ additional_permitted_classes: AVAILABLE_TAGS,
+ reject_empty: true
+ ).load!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/yaml/result.rb b/lib/gitlab/ci/config/yaml/result.rb
index 33f9a454106..6b53adc3a57 100644
--- a/lib/gitlab/ci/config/yaml/result.rb
+++ b/lib/gitlab/ci/config/yaml/result.rb
@@ -31,7 +31,7 @@ module Gitlab
def content
return @config.last if has_header?
- @config.first
+ @config.first || {}
end
end
end
diff --git a/lib/gitlab/ci/decompressed_gzip_size_validator.rb b/lib/gitlab/ci/decompressed_gzip_size_validator.rb
index a92f3007671..9b7b5f0dd66 100644
--- a/lib/gitlab/ci/decompressed_gzip_size_validator.rb
+++ b/lib/gitlab/ci/decompressed_gzip_size_validator.rb
@@ -63,7 +63,7 @@ module Gitlab
end
def validate_archive_path
- Gitlab::Utils.check_path_traversal!(archive_path)
+ Gitlab::PathTraversal.check_path_traversal!(archive_path)
raise(ServiceError, 'Archive path is a symlink') if File.lstat(archive_path).symlink?
raise(ServiceError, 'Archive path is not a file') unless File.file?(archive_path)
diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb
index aff30455d09..9e71a9e8e91 100644
--- a/lib/gitlab/ci/jwt_v2.rb
+++ b/lib/gitlab/ci/jwt_v2.rb
@@ -42,11 +42,36 @@ module Gitlab
end
def custom_claims
- super.merge(
+ additional_claims = {
runner_id: runner&.id,
runner_environment: runner_environment,
sha: pipeline.sha
+ }
+
+ if Feature.enabled?(:ci_jwt_v2_ref_uri_claim, pipeline.project)
+ additional_claims[:ci_config_ref_uri] = ci_config_ref_uri
+ end
+
+ super.merge(additional_claims)
+ end
+
+ def ci_config_ref_uri
+ project_config = Gitlab::Ci::ProjectConfig.new(
+ project: project,
+ sha: pipeline.sha,
+ pipeline_source: pipeline.source&.to_sym,
+ pipeline_source_bridge: pipeline.source_bridge
)
+
+ return unless project_config&.source == :repository_source
+
+ "#{project_config.url}@#{pipeline.source_ref_path}"
+
+ # Errors are rescued to mitigate risk. This can be removed if no errors are observed.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117923#note_1387660746 for context.
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, pipeline_id: pipeline.id)
+ nil
end
def runner_environment
diff --git a/lib/gitlab/ci/parsers/security/common.rb b/lib/gitlab/ci/parsers/security/common.rb
index 447136df81f..21408beb8cb 100644
--- a/lib/gitlab/ci/parsers/security/common.rb
+++ b/lib/gitlab/ci/parsers/security/common.rb
@@ -279,7 +279,6 @@ module Gitlab
end
def finding_name(data, identifiers, location)
- return data['message'] if data['message'].present?
return data['name'] if data['name'].present?
identifier = identifiers.find(&:cve?) || identifiers.find(&:cwe?) || identifiers.first
diff --git a/lib/gitlab/ci/project_config.rb b/lib/gitlab/ci/project_config.rb
index 00b2ad58428..ffff2da9d7a 100644
--- a/lib/gitlab/ci/project_config.rb
+++ b/lib/gitlab/ci/project_config.rb
@@ -25,7 +25,7 @@ module Gitlab
@config = find_config(project, sha, custom_content, pipeline_source, pipeline_source_bridge)
end
- delegate :content, :source, to: :@config, allow_nil: true
+ delegate :content, :source, :url, to: :@config, allow_nil: true
delegate :internal_include_prepended?, to: :@config
def exists?
diff --git a/lib/gitlab/ci/project_config/repository.rb b/lib/gitlab/ci/project_config/repository.rb
index 272425fd546..7dfd528fd6f 100644
--- a/lib/gitlab/ci/project_config/repository.rb
+++ b/lib/gitlab/ci/project_config/repository.rb
@@ -20,6 +20,11 @@ module Gitlab
:repository_source
end
+ override :url
+ def url
+ File.join(Settings.build_ci_component_fqdn, project.full_path, '//', ci_config_path)
+ end
+
private
def file_in_repository?
diff --git a/lib/gitlab/ci/project_config/source.rb b/lib/gitlab/ci/project_config/source.rb
index 9a4a6394fa1..68853ca8296 100644
--- a/lib/gitlab/ci/project_config/source.rb
+++ b/lib/gitlab/ci/project_config/source.rb
@@ -5,6 +5,7 @@ module Gitlab
class ProjectConfig
class Source
include Gitlab::Utils::StrongMemoize
+ extend ::Gitlab::Utils::Override
def initialize(project, sha, custom_content, pipeline_source, pipeline_source_bridge)
@project = project
@@ -33,6 +34,10 @@ module Gitlab
raise NotImplementedError
end
+ def url
+ nil
+ end
+
private
attr_reader :project, :sha, :custom_content, :pipeline_source, :pipeline_source_bridge
diff --git a/lib/gitlab/ci/reports/security/finding.rb b/lib/gitlab/ci/reports/security/finding.rb
index bf48c7d0bb7..d439149158a 100644
--- a/lib/gitlab/ci/reports/security/finding.rb
+++ b/lib/gitlab/ci/reports/security/finding.rb
@@ -82,7 +82,6 @@ module Gitlab
details
signatures
description
- message
cve
solution
].index_with do |key|
@@ -174,10 +173,6 @@ module Gitlab
original_data['description']
end
- def message
- original_data['message']
- end
-
def solution
original_data['solution']
end
diff --git a/lib/gitlab/ci/runner_instructions.rb b/lib/gitlab/ci/runner_instructions.rb
index 1bf015a0aa0..3040acc1eb4 100644
--- a/lib/gitlab/ci/runner_instructions.rb
+++ b/lib/gitlab/ci/runner_instructions.rb
@@ -112,3 +112,5 @@ module Gitlab
end
end
end
+
+::Gitlab::Ci::RunnerInstructions.prepend_mod
diff --git a/lib/gitlab/ci/secure_files/migration_helper.rb b/lib/gitlab/ci/secure_files/migration_helper.rb
new file mode 100644
index 00000000000..13796f2476e
--- /dev/null
+++ b/lib/gitlab/ci/secure_files/migration_helper.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module SecureFiles
+ class MigrationHelper
+ class << self
+ def migrate_to_remote_storage(&block)
+ migrate_in_batches(
+ ::Ci::SecureFile.with_files_stored_locally,
+ ::Ci::SecureFileUploader::Store::REMOTE,
+ &block
+ )
+ end
+
+ private
+
+ def batch_size
+ ENV.fetch('MIGRATION_BATCH_SIZE', 10).to_i
+ end
+
+ def migrate_in_batches(files, store, &block)
+ files.find_each(batch_size: batch_size) do |file| # rubocop:disable CodeReuse/ActiveRecord
+ file.file.migrate!(store)
+
+ yield file if block
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/waiting_for_approval.rb b/lib/gitlab/ci/status/build/waiting_for_approval.rb
index ac3f5838d26..c5fb32034ce 100644
--- a/lib/gitlab/ci/status/build/waiting_for_approval.rb
+++ b/lib/gitlab/ci/status/build/waiting_for_approval.rb
@@ -5,44 +5,14 @@ module Gitlab
module Status
module Build
class WaitingForApproval < Status::Extended
- def illustration
- {
- image: 'illustrations/manual_action.svg',
- size: 'svg-394',
- title: _('Waiting for approval'),
- content: _("This job deploys to the protected environment \"%{environment}\" which requires approvals.") % { environment: subject.deployment&.environment&.name }
- }
- end
-
- def has_action?
- true
- end
-
- def action_icon
- nil
- end
-
- def action_title
- nil
- end
-
- def action_button_title
- _('Go to environments page to approve or reject')
- end
-
- def action_path
- project_environments_path(subject.project)
- end
-
- def action_method
- :get
- end
-
+ ## Extended in EE
def self.matches?(build, user)
- build.waiting_for_deployment_approval?
+ false
end
end
end
end
end
end
+
+Gitlab::Ci::Status::Build::WaitingForApproval.prepend_mod_with('Gitlab::Ci::Status::Build::WaitingForApproval')
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
index e9068c326cf..8526becfef9 100644
--- a/lib/gitlab/ci/status/scheduled.rb
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -5,11 +5,11 @@ module Gitlab
module Status
class Scheduled < Status::Core
def text
- s_('CiStatusText|delayed')
+ s_('CiStatusText|scheduled')
end
def label
- s_('CiStatusLabel|delayed')
+ s_('CiStatusLabel|scheduled')
end
def icon
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 47623ad945f..84a0e52f518 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -9,7 +9,7 @@ module Gitlab
#
class SuccessWarning < Status::Extended
def text
- s_('CiStatusText|passed')
+ s_('CiStatusText|warning')
end
def label
diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
index b8a4c59c233..95cdf9b9953 100644
--- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
@@ -6,53 +6,51 @@
# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny
# If you are interested in using Android with FastLane for publishing take a look at the Android-Fastlane template.
-image: openjdk:8-jdk
+image: eclipse-temurin:17-jdk-jammy
variables:
# ANDROID_COMPILE_SDK is the version of Android you're compiling with.
# It should match compileSdkVersion.
- ANDROID_COMPILE_SDK: "29"
+ ANDROID_COMPILE_SDK: "33"
# ANDROID_BUILD_TOOLS is the version of the Android build tools you are using.
# It should match buildToolsVersion.
- ANDROID_BUILD_TOOLS: "29.0.3"
+ ANDROID_BUILD_TOOLS: "33.0.2"
# It's what version of the command line tools we're going to download from the official site.
# Official Site-> https://developer.android.com/studio/index.html
# There, look down below at the cli tools only, sdk tools package is of format:
# commandlinetools-os_type-ANDROID_SDK_TOOLS_latest.zip
# when the script was last modified for latest compileSdkVersion, it was which is written down below
- ANDROID_SDK_TOOLS: "6514223"
+ ANDROID_SDK_TOOLS: "9477386"
# Packages installation before running script
before_script:
- apt-get --quiet update --yes
- - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
+ - apt-get --quiet install --yes wget unzip
# Setup path as android_home for moving/exporting the downloaded sdk into it
- - export ANDROID_HOME="${PWD}/android-home"
+ - export ANDROID_HOME="${PWD}/android-sdk-root"
# Create a new directory at specified location
- install -d $ANDROID_HOME
# Here we are installing androidSDK tools from official source,
# (the key thing here is the url from where you are downloading these sdk tool for command line, so please do note this url pattern there and here as well)
# after that unzipping those tools and
# then running a series of SDK manager commands to install necessary android SDK packages that'll allow the app to build
- - wget --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
- # move to the archive at ANDROID_HOME
- - pushd $ANDROID_HOME
- - unzip -d cmdline-tools cmdline-tools.zip
- - popd
- - export PATH=$PATH:${ANDROID_HOME}/cmdline-tools/tools/bin/
+ - wget --no-verbose --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
+ - unzip -q -d "$ANDROID_HOME/cmdline-tools" "$ANDROID_HOME/cmdline-tools.zip"
+ - mv -T "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools"
+ - export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin
# Nothing fancy here, just checking sdkManager version
- sdkmanager --version
# use yes to accept all licenses
- - yes | sdkmanager --sdk_root=${ANDROID_HOME} --licenses || true
- - sdkmanager --sdk_root=${ANDROID_HOME} "platforms;android-${ANDROID_COMPILE_SDK}"
- - sdkmanager --sdk_root=${ANDROID_HOME} "platform-tools"
- - sdkmanager --sdk_root=${ANDROID_HOME} "build-tools;${ANDROID_BUILD_TOOLS}"
+ - yes | sdkmanager --licenses > /dev/null || true
+ - sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}"
+ - sdkmanager "platform-tools"
+ - sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}"
# Not necessary, but just for surity
- chmod +x ./gradlew
@@ -64,6 +62,11 @@ lintDebug:
stage: build
script:
- ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint
+ artifacts:
+ paths:
+ - app/lint/reports/lint-results-debug.html
+ expose_as: "lint-report"
+ when: always
# Make Project
assembleDebug:
@@ -77,6 +80,7 @@ assembleDebug:
# Run all tests, if any fails, interrupt the pipeline(fail it)
debugTests:
+ needs: [lintDebug, assembleDebug]
interruptible: true
stage: test
script:
diff --git a/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml b/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml
index 7f81755348c..3d053d4d78c 100644
--- a/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml
@@ -8,9 +8,9 @@
code_quality:
stage: test
- image: "cirrusci/flutter:1.22.5"
+ image: "ghcr.io/cirruslabs/flutter:3.10.3"
before_script:
- - pub global activate dart_code_metrics
+ - flutter pub global activate dart_code_metrics
- export PATH="$PATH:$HOME/.pub-cache/bin"
script:
- metrics lib -r codeclimate > gl-code-quality-report.json
@@ -20,9 +20,9 @@ code_quality:
test:
stage: test
- image: "cirrusci/flutter:1.22.5"
+ image: "ghcr.io/cirruslabs/flutter:3.10.3"
before_script:
- - pub global activate junitreport
+ - flutter pub global activate junitreport
- export PATH="$PATH:$HOME/.pub-cache/bin"
script:
- flutter test --machine --coverage | tojunit -o report.xml
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 7a4c65f8c5b..49d3c270bac 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.32.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.34.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
index 7a4c65f8c5b..49d3c270bac 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.32.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.34.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml
index 6e8cf15204a..de3f688bdb6 100644
--- a/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml
@@ -7,7 +7,7 @@ cloud_formation:
script:
- gl-cloudformation create-stack
rules:
- - if: '($AUTO_DEVOPS_PLATFORM_TARGET != "EC2") || ($AUTO_DEVOPS_PLATFORM_TARGET != "ECS")'
+ - if: '($AUTO_DEVOPS_PLATFORM_TARGET != "EC2") && ($AUTO_DEVOPS_PLATFORM_TARGET != "ECS")'
when: never
- if: '$CI_KUBERNETES_ACTIVE || $KUBECONFIG'
when: never
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index b2ab6704e35..7c9aa82b1ae 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -8,7 +8,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
- CODE_QUALITY_IMAGE_TAG: "0.94.0"
+ CODE_QUALITY_IMAGE_TAG: "0.96.0"
CODE_QUALITY_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/ci-cd/codequality:$CODE_QUALITY_IMAGE_TAG"
needs: []
script:
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 4ee5fa74df9..f4a13d61ba2 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.48.2'
+ DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.50.0'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/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 622b44d78ad..c1a3daa7f5b 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.48.2'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.50.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/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 2954ddf8a35..a3c7c6baf02 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.48.2'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.50.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml b/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
index d46ac97ad1b..d7a6104082d 100644
--- a/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
@@ -32,11 +32,11 @@ kaniko-build:
VERSION="latest"
elif [ -n "$CI_COMMIT_TAG" ];then
NOSLASH=$(echo "$CI_COMMIT_TAG" | tr -s / - )
- SANITIZED="${NOSLASH//[^a-zA-Z0-9\-\.]/}"
+ SANITIZED="${NOSLASH//[^a-zA-Z0-9.-]/}"
VERSION="$SANITIZED"
else \
NOSLASH=$(echo "$CI_COMMIT_REF_NAME" | tr -s / - )
- SANITIZED="${NOSLASH//[^a-zA-Z0-9\-]/}"
+ SANITIZED="${NOSLASH//[^a-zA-Z0-9-]/}"
VERSION="branch-$SANITIZED"
fi
export IMAGE_TAG=$CI_REGISTRY_IMAGE:$VERSION
diff --git a/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml
index 51d2273d41d..eb1c920e11b 100644
--- a/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/brunch
-image: node:4.2.2
+default:
+ # Full project: https://gitlab.com/pages/brunch
+ image: node:4.2.2
pages:
cache:
diff --git a/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml
index e577a489c55..95eab65e629 100644
--- a/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/doxygen
-image: alpine
+default:
+ # Full project: https://gitlab.com/pages/doxygen
+ image: alpine
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
index 88ed73b41e7..c58db066d61 100644
--- a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
@@ -3,13 +3,14 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
-image: node:latest
+default:
+ image: node:latest
-# This folder is cached between builds
-# https://docs.gitlab.com/ee/ci/yaml/index.html#cache
-cache:
- paths:
- - node_modules/
+ # This folder is cached between builds
+ # https://docs.gitlab.com/ee/ci/yaml/index.html#cache
+ cache:
+ paths:
+ - node_modules/
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml
index aa86ad2a6ad..313cb99a841 100644
--- a/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/harp
-image: node:4.2.2
+default:
+ # Full project: https://gitlab.com/pages/harp
+ image: node:4.2.2
pages:
cache:
diff --git a/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
index b1617e9239c..f27228f2e3c 100644
--- a/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/hexo
-image: node:10.15.3
+default:
+ # Full project: https://gitlab.com/pages/hexo
+ image: node:10.15.3
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml
index d6f6e94526e..6a364959db2 100644
--- a/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml
@@ -6,7 +6,8 @@
---
# All available Hugo versions are listed here:
# https://gitlab.com/pages/hugo/container_registry
-image: "${CI_TEMPLATE_REGISTRY_HOST}/pages/hugo:latest"
+default:
+ image: "${CI_TEMPLATE_REGISTRY_HOST}/pages/hugo:latest"
variables:
GIT_SUBMODULE_STRATEGY: recursive
@@ -14,9 +15,8 @@ variables:
test:
script:
- hugo
- except:
- variables:
- - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+ rules:
+ - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
pages:
script:
@@ -24,7 +24,6 @@ pages:
artifacts:
paths:
- public
- only:
- variables:
- - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
environment: production
diff --git a/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml
index fba4afca9ed..c3ee4d62359 100644
--- a/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml
@@ -3,12 +3,13 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/hyde
-image: python:2.7
+default:
+ # Full project: https://gitlab.com/pages/hyde
+ image: python:2.7
-cache:
- paths:
- - vendor/
+ cache:
+ paths:
+ - vendor/
test:
stage: test
diff --git a/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml
index 57e3ced4dc2..ba8eb81ca22 100644
--- a/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml
@@ -13,20 +13,21 @@
#
# HowTo at: https://jorge.aguilera.gitlab.io/howtojbake/
-image: java:8
+default:
+ image: java:8
+
+ # We use SDKMan as tool for managing versions
+ before_script:
+ - apt-get update -qq && apt-get install -y -qq unzip zip
+ - curl -sSL https://get.sdkman.io | bash
+ - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
+ - source /root/.sdkman/bin/sdkman-init.sh
+ - sdk install jbake $JBAKE_VERSION < /dev/null
+ - sdk use jbake $JBAKE_VERSION
variables:
JBAKE_VERSION: 2.5.1
-# We use SDKMan as tool for managing versions
-before_script:
- - apt-get update -qq && apt-get install -y -qq unzip zip
- - curl -sSL https://get.sdkman.io | bash
- - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
- - source /root/.sdkman/bin/sdkman-init.sh
- - sdk install jbake $JBAKE_VERSION < /dev/null
- - sdk use jbake $JBAKE_VERSION
-
# This build job produced the output directory of your site
pages:
environment: production
diff --git a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
index 8b07454af24..812a08c33fb 100644
--- a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
@@ -3,18 +3,19 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
-# Template project: https://gitlab.com/pages/jekyll
-# Docs: https://docs.gitlab.com/ee/pages/
-image: ruby:2.6
+default:
+ # Template project: https://gitlab.com/pages/jekyll
+ # Docs: https://docs.gitlab.com/ee/pages/
+ image: ruby:2.6
+
+ before_script:
+ - gem install bundler
+ - bundle install
variables:
JEKYLL_ENV: production
LC_ALL: C.UTF-8
-before_script:
- - gem install bundler
- - bundle install
-
test:
stage: test
script:
diff --git a/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml
index ad083fcc5db..291c03c0002 100644
--- a/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml
@@ -7,29 +7,30 @@
#
# Full project: https://github.com/tightenco/jigsaw
-image: php:7.2
+default:
+ image: php:7.2
-# These folders are cached between builds
-cache:
- paths:
- - vendor/
- - node_modules/
+ # These folders are cached between builds
+ cache:
+ paths:
+ - vendor/
+ - node_modules/
-before_script:
- # Update packages
- - apt-get update -yqq
- # Install dependencies
- - apt-get install -yqq gnupg zlib1g-dev libpng-dev
- # Install Node 8
- - curl -sL https://deb.nodesource.com/setup_8.x | bash -
- - apt-get install -yqq nodejs
- # Install php extensions
- - docker-php-ext-install zip
- # Install Composer and project dependencies
- - curl -sS https://getcomposer.org/installer | php
- - php composer.phar install
- # Install Node dependencies
- - npm install
+ before_script:
+ # Update packages
+ - apt-get update -yqq
+ # Install dependencies
+ - apt-get install -yqq gnupg zlib1g-dev libpng-dev
+ # Install Node 8
+ - curl -sL https://deb.nodesource.com/setup_8.x | bash -
+ - apt-get install -yqq nodejs
+ # Install php extensions
+ - docker-php-ext-install zip
+ # Install Composer and project dependencies
+ - curl -sS https://getcomposer.org/installer | php
+ - php composer.phar install
+ # Install Node dependencies
+ - npm install
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml
index e86337ae23c..e83cf30e999 100644
--- a/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/hyde
-image: python:2.7
+default:
+ # Full project: https://gitlab.com/pages/hyde
+ image: python:2.7
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml
index a49e95b62c8..bbbffa7c682 100644
--- a/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/metalsmith
-image: node:4.2.2
+default:
+ # Full project: https://gitlab.com/pages/metalsmith
+ image: node:4.2.2
pages:
cache:
diff --git a/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml
index d8f036ab4ed..f0a7f88eaf2 100644
--- a/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml
@@ -3,12 +3,13 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/middleman
-image: ruby:2.6
+default:
+ # Full project: https://gitlab.com/pages/middleman
+ image: ruby:2.6
-cache:
- paths:
- - vendor
+ cache:
+ paths:
+ - vendor
test:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml
index b0511abd109..3160948ec35 100644
--- a/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/nanoc
-image: ruby:2.6
+default:
+ # Full project: https://gitlab.com/pages/nanoc
+ image: ruby:2.6
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml
index c89050eede7..395842c4cd1 100644
--- a/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/octopress
-image: ruby:2.6
+default:
+ # Full project: https://gitlab.com/pages/octopress
+ image: ruby:2.6
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml
index 3721344b21e..7921a71a89a 100644
--- a/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml
@@ -3,8 +3,9 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml
-# Full project: https://gitlab.com/pages/pelican
-image: python:2.7-alpine
+default:
+ # Full project: https://gitlab.com/pages/pelican
+ image: python:2.7-alpine
pages:
script:
diff --git a/lib/gitlab/ci/templates/Pages/SwaggerUI.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/SwaggerUI.gitlab-ci.yml
index 00efcfa1b32..d75ecc3da01 100644
--- a/lib/gitlab/ci/templates/Pages/SwaggerUI.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/SwaggerUI.gitlab-ci.yml
@@ -3,7 +3,13 @@
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/SwaggerUI.gitlab-ci.yml
-image: node:10-alpine
+default:
+ image: node:10-alpine
+
+ # These folders are cached between builds
+ cache:
+ paths:
+ - ./node_modules
# specify the location of the Open API Specification files within your project
# and the filename of the specification that you would like to display by default
@@ -11,11 +17,6 @@ variables:
DOCS_FOLDER: "api-docs"
SPEC_TO_DISPLAY: "my-project_specification_0.0.1.json"
-# These folders are cached between builds
-cache:
- paths:
- - ./node_modules
-
# publishes all files from the $DOCS_FOLDER together with the static version of SwaggerUI
# sets the specification file named in $SPEC_TO_DISPLAY to be displayed by default
pages:
diff --git a/lib/gitlab/ci/templates/Pages/Zola.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Zola.gitlab-ci.yml
new file mode 100644
index 00000000000..c2c890846b9
--- /dev/null
+++ b/lib/gitlab/ci/templates/Pages/Zola.gitlab-ci.yml
@@ -0,0 +1,30 @@
+# 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/Pages/Zola.gitlab-ci.yml
+
+# Prefer to copy-paste this template instead of include it to ensure forward compatibility
+
+---
+# From: https://www.getzola.org/documentation/deployment/gitlab-pages/
+# Source template is slightly modified to be self-contained
+
+pages:
+ image: alpine:latest
+ variables:
+ # This variable will ensure that the CI runner pulls in your theme from the submodule
+ GIT_SUBMODULE_STRATEGY: recursive
+ before_script:
+ # Install the zola package from the alpine community repositories
+ - apk add --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ zola
+ script:
+ # Execute zola build
+ - zola build --base-url "$CI_PAGES_URL"
+ artifacts:
+ paths:
+ # Path of our artifacts
+ - public
+ # This config will only publish changes that are pushed on the default branch
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+ environment: production
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 792bd7f666b..f10011ab23b 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -26,11 +26,12 @@ variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
+ DAST_IMAGE_SUFFIX: ""
dast:
stage: dast
image:
- name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
+ name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION$DAST_IMAGE_SUFFIX"
variables:
GIT_STRATEGY: none
allow_failure: true
@@ -57,6 +58,11 @@ dast:
$REVIEW_DISABLED == '1'
when: never
- if: $CI_COMMIT_BRANCH &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $GITLAB_FEATURES =~ /\bdast\b/
+ variables:
+ DAST_IMAGE_SUFFIX: "-fips"
+ - if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdast\b/
after_script:
# Remove any debug.log files because they might contain secrets.
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 d1d1c4d7e52..989f9caf601 100644
--- a/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
@@ -26,11 +26,12 @@ variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
+ DAST_IMAGE_SUFFIX: ""
dast:
stage: dast
image:
- name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
+ name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION$DAST_IMAGE_SUFFIX"
variables:
GIT_STRATEGY: none
allow_failure: true
@@ -59,6 +60,12 @@ dast:
$REVIEW_DISABLED == '1'
when: never
+ # Add the job to merge request pipelines if there's an open merge request. (FIPS)
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $GITLAB_FEATURES =~ /\bdast\b/
+ variables:
+ DAST_IMAGE_SUFFIX: "-fips"
# Add the job to merge request pipelines if there's an open merge request.
- if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
$GITLAB_FEATURES =~ /\bdast\b/
@@ -67,6 +74,12 @@ dast:
- if: $CI_OPEN_MERGE_REQUESTS
when: never
+ # Add the job to branch pipelines. (FIPS)
+ - if: $CI_COMMIT_BRANCH &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $GITLAB_FEATURES =~ /\bdast\b/
+ variables:
+ DAST_IMAGE_SUFFIX: "-fips"
# Add the job to branch pipelines.
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdast\b/
diff --git a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
index 88fe55a44ab..793030d302a 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
@@ -9,18 +9,19 @@
# There is a more opinionated template which we suggest the users to abide,
# which is the lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
-image:
- name: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/terraform-images/stable:latest"
+default:
+ image:
+ name: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/terraform-images/stable:latest"
+
+ cache:
+ key: "${TF_ROOT}"
+ paths:
+ - ${TF_ROOT}/.terraform/
variables:
TF_ROOT: ${CI_PROJECT_DIR} # The relative path to the root directory of the Terraform project
TF_STATE_NAME: default # The name of the state file used by the GitLab Managed Terraform state backend
-cache:
- key: "${TF_ROOT}"
- paths:
- - ${TF_ROOT}/.terraform/
-
.terraform:fmt: &terraform_fmt
stage: validate
script:
diff --git a/lib/gitlab/ci/variables/builder.rb b/lib/gitlab/ci/variables/builder.rb
index 86e54fdfcdf..cae3a966bc6 100644
--- a/lib/gitlab/ci/variables/builder.rb
+++ b/lib/gitlab/ci/variables/builder.rb
@@ -139,14 +139,6 @@ module Gitlab
# Set environment name here so we can access it when evaluating the job's rules
variables.append(key: 'CI_ENVIRONMENT_NAME', value: job.environment) if job.environment
-
- if Feature.disabled?(:ci_remove_legacy_predefined_variables, project)
- # legacy variables
- variables.append(key: 'CI_BUILD_NAME', value: job.name)
- variables.append(key: 'CI_BUILD_STAGE', value: job.stage_name)
- variables.append(key: 'CI_BUILD_TRIGGERED', value: 'true') if job.trigger_request
- variables.append(key: 'CI_BUILD_MANUAL', value: 'true') if job.action?
- end
end
end
diff --git a/lib/gitlab/ci/variables/builder/pipeline.rb b/lib/gitlab/ci/variables/builder/pipeline.rb
index 1e7a18d70b0..c3b0cb856ba 100644
--- a/lib/gitlab/ci/variables/builder/pipeline.rb
+++ b/lib/gitlab/ci/variables/builder/pipeline.rb
@@ -40,7 +40,7 @@ module Gitlab
attr_reader :pipeline
- def predefined_commit_variables # rubocop:disable Metrics/AbcSize - Remove this rubocop:disable when FF `ci_remove_legacy_predefined_variables` is removed.
+ def predefined_commit_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
next variables unless pipeline.sha.present?
@@ -56,24 +56,10 @@ module Gitlab
variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!pipeline.protected_ref?).to_s)
variables.append(key: 'CI_COMMIT_TIMESTAMP', value: pipeline.git_commit_timestamp.to_s)
variables.append(key: 'CI_COMMIT_AUTHOR', value: pipeline.git_author_full_text.to_s)
-
- if Feature.disabled?(:ci_remove_legacy_predefined_variables, pipeline.project)
- variables.concat(legacy_predefined_commit_variables)
- end
end
end
strong_memoize_attr :predefined_commit_variables
- def legacy_predefined_commit_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_BUILD_REF', value: pipeline.sha)
- variables.append(key: 'CI_BUILD_BEFORE_SHA', value: pipeline.before_sha)
- variables.append(key: 'CI_BUILD_REF_NAME', value: pipeline.source_ref)
- variables.append(key: 'CI_BUILD_REF_SLUG', value: pipeline.source_ref_slug)
- end
- end
- strong_memoize_attr :legacy_predefined_commit_variables
-
def predefined_commit_tag_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
git_tag = pipeline.project.repository.find_tag(pipeline.ref)
@@ -82,21 +68,10 @@ module Gitlab
variables.append(key: 'CI_COMMIT_TAG', value: pipeline.ref)
variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: git_tag.message)
-
- if Feature.disabled?(:ci_remove_legacy_predefined_variables, pipeline.project)
- variables.concat(legacy_predefined_commit_tag_variables)
- end
end
end
strong_memoize_attr :predefined_commit_tag_variables
- def legacy_predefined_commit_tag_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_BUILD_TAG', value: pipeline.ref)
- end
- end
- strong_memoize_attr :legacy_predefined_commit_tag_variables
-
def predefined_merge_request_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: pipeline.merge_request_event_type.to_s)
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
deleted file mode 100644
index 957faf797b5..00000000000
--- a/lib/gitlab/cluster/puma_worker_killer_initializer.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Cluster
- class PumaWorkerKillerInitializer
- def self.start(
- puma_options,
- puma_per_worker_max_memory_mb: 1200,
- puma_master_max_memory_mb: 950,
- additional_puma_dev_max_memory_mb: 200)
-
- # We are replacing PWK with Watchdog by using backward compatible RssMemoryLimit monitor by default.
- # https://gitlab.com/groups/gitlab-org/-/epics/9119
- return if Gitlab::Utils.to_boolean(ENV.fetch('GITLAB_MEMORY_WATCHDOG_ENABLED', true))
-
- require 'puma_worker_killer'
-
- PumaWorkerKiller.config do |config|
- worker_count = puma_options[:workers] || 1
- # The Puma Worker Killer checks the total memory used by the cluster,
- # i.e. both primary and worker processes.
- # https://github.com/schneems/puma_worker_killer/blob/v0.1.0/lib/puma_worker_killer/puma_memory.rb#L57
- #
- # Additional memory is added when running in `development`
- config.ram = puma_master_max_memory_mb +
- (worker_count * puma_per_worker_max_memory_mb) +
- (Rails.env.development? ? (1 + worker_count) * additional_puma_dev_max_memory_mb : 0)
-
- config.frequency = 20 # seconds
-
- # We just want to limit to a fixed maximum, unrelated to the total amount
- # of available RAM.
- config.percent_usage = 0.98
-
- # Ideally we'll never hit the maximum amount of memory. Restart the workers
- # regularly rather than rely on OOM behavior for periodic restarting.
- config.rolling_restart_frequency = 43200 # 12 hours in seconds.
-
- # Spread the rolling restarts out over 1 hour to avoid too many simultaneous
- # process startups.
- config.rolling_restart_splay_seconds = 0.0..3600.0 # 0 to 1 hour in seconds.
-
- observer = Gitlab::Cluster::PumaWorkerKillerObserver.new
- config.pre_term = observer.callback
- end
-
- PumaWorkerKiller.start
- end
- end
- end
-end
diff --git a/lib/gitlab/cluster/puma_worker_killer_observer.rb b/lib/gitlab/cluster/puma_worker_killer_observer.rb
deleted file mode 100644
index f53051c32ff..00000000000
--- a/lib/gitlab/cluster/puma_worker_killer_observer.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Cluster
- class PumaWorkerKillerObserver
- def initialize
- @counter = Gitlab::Metrics.counter(:puma_killer_terminations_total, 'Number of workers terminated by PumaWorkerKiller')
- end
-
- # returns the Proc to be used as the observer callback block
- def callback
- method(:log_termination)
- end
-
- private
-
- def log_termination(worker)
- @counter.increment
- end
- end
- end
-end
diff --git a/lib/gitlab/counters/buffered_counter.rb b/lib/gitlab/counters/buffered_counter.rb
index 3e232c78e45..258ada864c8 100644
--- a/lib/gitlab/counters/buffered_counter.rb
+++ b/lib/gitlab/counters/buffered_counter.rb
@@ -78,11 +78,7 @@ module Gitlab
def increment(increment)
result = redis_state do |redis|
- if Feature.enabled?(:project_statistics_bulk_increment, type: :development)
- redis.eval(LUA_INCREMENT_WITH_DEDUPLICATION_SCRIPT, **increment_args(increment)).to_i
- else
- redis.incrby(key, increment.amount)
- end
+ redis.eval(LUA_INCREMENT_WITH_DEDUPLICATION_SCRIPT, **increment_args(increment)).to_i
end
FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, counter_record.class.name, counter_record.id, attribute)
@@ -94,11 +90,7 @@ module Gitlab
result = redis_state do |redis|
redis.pipelined do |pipeline|
increments.each do |increment|
- if Feature.enabled?(:project_statistics_bulk_increment, type: :development)
- pipeline.eval(LUA_INCREMENT_WITH_DEDUPLICATION_SCRIPT, **increment_args(increment))
- else
- pipeline.incrby(key, increment.amount)
- end
+ pipeline.eval(LUA_INCREMENT_WITH_DEDUPLICATION_SCRIPT, **increment_args(increment))
end
end
end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index ecb0cc20a64..2fa0b40df14 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -65,6 +65,7 @@ module Gitlab
{
id: pipeline.id,
iid: pipeline.iid,
+ name: pipeline.name,
ref: pipeline.source_ref,
tag: pipeline.tag,
sha: pipeline.sha,
@@ -77,7 +78,8 @@ module Gitlab
finished_at: pipeline.finished_at,
duration: pipeline.duration,
queued_duration: pipeline.queued_duration,
- variables: pipeline.variables.map(&:hook_attrs)
+ variables: pipeline.variables.map(&:hook_attrs),
+ url: Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
}
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 4197c87f51f..da9ebf4ab0f 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -2,8 +2,6 @@
module Gitlab
module Database
- DATABASE_NAMES = %w[main ci main_clusterwide].freeze
-
MAIN_DATABASE_NAME = 'main'
CI_DATABASE_NAME = 'ci'
DEFAULT_POOL_HEADROOM = 10
@@ -56,67 +54,78 @@ module Gitlab
MODE_SINGLE_DATABASE_CI_CONNECTION = "single-database-ci-connection"
MODE_MULTIPLE_DATABASES = "multiple-databases"
+ def self.all_database_connection_files
+ Dir.glob(Rails.root.join("db/database_connections/*.yaml"))
+ end
+
+ def self.all_gitlab_schema_files
+ Dir.glob(Rails.root.join("db/gitlab_schemas/*.yaml"))
+ end
+
+ def self.all_database_connections
+ @all_database_connections ||=
+ all_database_connection_files
+ .map { |file| DatabaseConnectionInfo.load_file(file) }
+ .sort_by(&:order)
+ .index_by(&:name)
+ .with_indifferent_access.freeze
+ end
+
+ def self.all_database_names
+ all_database_connections.keys.map(&:to_s)
+ end
+
+ def self.all_gitlab_schemas
+ @all_gitlab_schemas ||=
+ all_gitlab_schema_files
+ .map { |file| GitlabSchemaInfo.load_file(file) }
+ .index_by(&:name)
+ .with_indifferent_access.freeze
+ end
+
def self.database_base_models
- @database_base_models ||= {
- # Note that we use ActiveRecord::Base here and not ApplicationRecord.
- # This is deliberate, as we also use these classes to apply load
- # balancing to, and the load balancer must be enabled for _all_ models
- # that inherit from ActiveRecord::Base; not just our own models that
- # inherit from ApplicationRecord.
- main: ::ActiveRecord::Base,
- main_clusterwide: ::MainClusterwide::ApplicationRecord.connection_class? ? ::MainClusterwide::ApplicationRecord : nil,
- ci: ::Ci::ApplicationRecord.connection_class? ? ::Ci::ApplicationRecord : nil
- }.compact.with_indifferent_access.freeze
+ # Note that we use ActiveRecord::Base here and not ApplicationRecord.
+ # This is deliberate, as we also use these classes to apply load
+ # balancing to, and the load balancer must be enabled for _all_ models
+ # that inherit from ActiveRecord::Base; not just our own models that
+ # inherit from ApplicationRecord.
+ @database_base_models ||=
+ all_database_connections
+ .transform_values(&:connection_class)
+ .compact.with_indifferent_access.freeze
end
# This returns a list of databases that contains all the gitlab_shared schema
- # tables. We can't reuse database_base_models because Geo does not support
- # the gitlab_shared tables yet.
+ # tables.
def self.database_base_models_with_gitlab_shared
- @database_base_models_with_gitlab_shared ||= {
- # Note that we use ActiveRecord::Base here and not ApplicationRecord.
- # This is deliberate, as we also use these classes to apply load
- # balancing to, and the load balancer must be enabled for _all_ models
- # that inher from ActiveRecord::Base; not just our own models that
- # inherit from ApplicationRecord.
- main: ::ActiveRecord::Base,
- main_clusterwide: ::MainClusterwide::ApplicationRecord.connection_class? ? ::MainClusterwide::ApplicationRecord : nil,
- ci: ::Ci::ApplicationRecord.connection_class? ? ::Ci::ApplicationRecord : nil
- }.compact.with_indifferent_access.freeze
+ @database_base_models_with_gitlab_shared ||=
+ all_database_connections
+ .select { |_, db| db.has_gitlab_shared? }
+ .transform_values(&:connection_class)
+ .compact.with_indifferent_access.freeze
end
# This returns a list of databases whose connection supports database load
- # balancing. We can't reuse the database_base_models method because the Geo
- # database does not support load balancing yet.
- #
- # TODO: https://gitlab.com/gitlab-org/geo-team/discussions/-/issues/5032
+ # balancing. We can't reuse the database_base_models since not all connections
+ # do support load balancing.
def self.database_base_models_using_load_balancing
- @database_base_models_using_load_balancing ||= {
- # Note that we use ActiveRecord::Base here and not ApplicationRecord.
- # This is deliberate, as we also use these classes to apply load
- # balancing to, and the load balancer must be enabled for _all_ models
- # that inher from ActiveRecord::Base; not just our own models that
- # inherit from ApplicationRecord.
- main: ::ActiveRecord::Base,
- main_clusterwide: ::MainClusterwide::ApplicationRecord.connection_class? ? ::MainClusterwide::ApplicationRecord : nil,
- ci: ::Ci::ApplicationRecord.connection_class? ? ::Ci::ApplicationRecord : nil
- }.compact.with_indifferent_access.freeze
+ @database_base_models_using_load_balancing ||=
+ all_database_connections
+ .select { |_, db| db.uses_load_balancing? }
+ .transform_values(&:connection_class)
+ .compact.with_indifferent_access.freeze
end
# This returns a list of base models with connection associated for a given gitlab_schema
def self.schemas_to_base_models
- @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: database_base_models_with_gitlab_shared.values, # all models
- gitlab_internal: database_base_models.values, # all models
- gitlab_pm: [self.database_base_models.fetch(:main)], # package metadata models
- gitlab_main_clusterwide: [self.database_base_models[:main_clusterwide] || self.database_base_models.fetch(:main)]
- }.with_indifferent_access.freeze
- end
-
- def self.all_database_names
- DATABASE_NAMES
+ @schemas_to_base_models ||=
+ all_gitlab_schemas.transform_values do |schema|
+ all_database_connections
+ .values
+ .select { |db| db.gitlab_schemas.include?(schema.name) }
+ .filter_map { |db| db.connection_class_or_fallback(all_database_connections) }
+ .uniq
+ end.compact.with_indifferent_access.freeze
end
# We configure the database connection pool size automatically based on the
@@ -255,8 +264,16 @@ module Gitlab
end
end
- def self.db_config_names
- ::ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).map(&:name) - ['geo']
+ def self.db_config_names(with_schema:)
+ db_config_names = ::ActiveRecord::Base.configurations
+ .configs_for(env_name: Rails.env).map(&:name)
+ return db_config_names unless with_schema
+
+ schema_models = schemas_to_base_models.fetch(with_schema)
+ db_config_names.select do |db_config_name|
+ db_info = all_database_connections.fetch(db_config_name)
+ schema_models.include?(db_info.connection_class)
+ end
end
# This returns all matching schemas that a given connection can use
diff --git a/lib/gitlab/database/async_indexes/index_base.rb b/lib/gitlab/database/async_indexes/index_base.rb
index bde75e12295..93f3ba88345 100644
--- a/lib/gitlab/database/async_indexes/index_base.rb
+++ b/lib/gitlab/database/async_indexes/index_base.rb
@@ -81,7 +81,8 @@ module Gitlab
{
table_name: async_index.table_name,
index_name: async_index.name,
- class: self.class.name.to_s
+ class: self.class.name.to_s,
+ connection_name: database_config_name
}
end
end
diff --git a/lib/gitlab/database/async_indexes/postgres_async_index.rb b/lib/gitlab/database/async_indexes/postgres_async_index.rb
index 9f5f39613ed..a3c600a4519 100644
--- a/lib/gitlab/database/async_indexes/postgres_async_index.rb
+++ b/lib/gitlab/database/async_indexes/postgres_async_index.rb
@@ -8,13 +8,17 @@ module Gitlab
self.table_name = 'postgres_async_indexes'
+ # schema_name + . + table_name
+ MAX_TABLE_NAME_LENGTH = (Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH * 2) + 1
MAX_IDENTIFIER_LENGTH = Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH
MAX_DEFINITION_LENGTH = 2048
validates :name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH }
- validates :table_name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH }
+ validates :table_name, presence: true, length: { maximum: MAX_TABLE_NAME_LENGTH }
validates :definition, presence: true, length: { maximum: MAX_DEFINITION_LENGTH }
+ validate :ensure_correct_schema_and_table_name
+
scope :to_create, -> { where("definition ILIKE 'CREATE%'") }
scope :to_drop, -> { where("definition ILIKE 'DROP%'") }
scope :ordered, -> { order(attempts: :asc, id: :asc) }
@@ -22,6 +26,24 @@ module Gitlab
def to_s
definition
end
+
+ private
+
+ def ensure_correct_schema_and_table_name
+ return unless table_name
+
+ schema, table, *rest = table_name.split('.')
+
+ too_long = (table.nil? && schema.length > MAX_DEFINITION_LENGTH) || # no schema given
+ # both schema and table given
+ (schema.length > MAX_IDENTIFIER_LENGTH || (table && table.length > MAX_IDENTIFIER_LENGTH))
+
+ if too_long
+ errors.add(:table_name, :too_long)
+ elsif rest.any?
+ errors.add(:table_name, :invalid)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb
index 523ab2a9f27..458b099924b 100644
--- a/lib/gitlab/database/background_migration/batched_job.rb
+++ b/lib/gitlab/database/background_migration/batched_job.rb
@@ -139,7 +139,7 @@ module Gitlab
new_batch_size = batch_size / 2
- break update!(attempts: 0) if new_batch_size < 1
+ next update!(attempts: 0) if new_batch_size < 1
batching_strategy = batched_migration.batch_class.new(connection: self.class.connection)
next_batch_bounds = batching_strategy.next_batch(
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
index a883996a5c5..83beee091f1 100644
--- a/lib/gitlab/database/background_migration/batched_migration.rb
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -79,6 +79,10 @@ module Gitlab
transition any => :finalizing
end
+ before_transition any => :finished do |migration|
+ migration.finished_at = Time.current if migration.respond_to?(:finished_at)
+ end
+
before_transition any => :active do |migration|
migration.started_at = Time.current if migration.respond_to?(:started_at)
end
@@ -92,10 +96,6 @@ module Gitlab
for_configuration(gitlab_schema, job_class_name, table_name, column_name, job_arguments).first
end
- def self.active_migration(connection:)
- active_migrations_distinct_on_table(connection: connection, limit: 1).first
- end
-
def self.find_executable(id, connection:)
for_gitlab_schema(Gitlab::Database.gitlab_schemas_for_connection(connection))
.executable.find_by_id(id)
@@ -220,7 +220,12 @@ module Gitlab
end
def health_context
- HealthStatus::Context.new(connection, [table_name], gitlab_schema.to_sym)
+ @health_context ||= Gitlab::Database::HealthStatus::Context.new(
+ self,
+ connection,
+ [table_name],
+ gitlab_schema.to_sym
+ )
end
def hold!(until_time: 10.minutes.from_now)
diff --git a/lib/gitlab/database/background_migration/batched_migration_runner.rb b/lib/gitlab/database/background_migration/batched_migration_runner.rb
index 7224ff2b517..e3e8754c758 100644
--- a/lib/gitlab/database/background_migration/batched_migration_runner.rb
+++ b/lib/gitlab/database/background_migration/batched_migration_runner.rb
@@ -144,7 +144,7 @@ module Gitlab
end
def adjust_migration(active_migration)
- signals = HealthStatus.evaluate(active_migration)
+ signals = Gitlab::Database::HealthStatus.evaluate(active_migration.health_context)
if signals.any?(&:stop?)
active_migration.hold!
diff --git a/lib/gitlab/database/background_migration/health_status.rb b/lib/gitlab/database/background_migration/health_status.rb
deleted file mode 100644
index 96905fd0bc5..00000000000
--- a/lib/gitlab/database/background_migration/health_status.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module BackgroundMigration
- module HealthStatus
- DEFAULT_INIDICATORS = [
- Indicators::AutovacuumActiveOnTable,
- Indicators::WriteAheadLog,
- Indicators::PatroniApdex
- ].freeze
-
- # Rather than passing along the migration, we use a more explicitly defined context
- Context = Struct.new(:connection, :tables, :gitlab_schema)
-
- def self.evaluate(migration, indicators = DEFAULT_INIDICATORS)
- indicators.map do |indicator|
- signal = begin
- indicator.new(migration.health_context).evaluate
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, migration_id: migration.id,
- job_class_name: migration.job_class_name)
-
- Signals::Unknown.new(indicator, reason: "unexpected error: #{e.message} (#{e.class})")
- end
-
- log_signal(signal, migration) if signal.log_info?
-
- signal
- end
- end
-
- def self.log_signal(signal, migration)
- Gitlab::BackgroundMigration::Logger.info(
- migration_id: migration.id,
- health_status_indicator: signal.indicator_class.to_s,
- indicator_signal: signal.short_name,
- signal_reason: signal.reason,
- message: "#{migration} signaled: #{signal}"
- )
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/background_migration/health_status/indicators.rb b/lib/gitlab/database/background_migration/health_status/indicators.rb
deleted file mode 100644
index 69503e5b61f..00000000000
--- a/lib/gitlab/database/background_migration/health_status/indicators.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module BackgroundMigration
- module HealthStatus
- module Indicators
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table.rb b/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table.rb
deleted file mode 100644
index 48e12609a13..00000000000
--- a/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module BackgroundMigration
- module HealthStatus
- module Indicators
- class AutovacuumActiveOnTable
- def initialize(context)
- @context = context
- end
-
- def evaluate
- return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
-
- autovacuum_active_on = active_autovacuums_for(context.tables)
-
- if autovacuum_active_on.empty?
- Signals::Normal.new(self.class, reason: 'no autovacuum running on any relevant tables')
- else
- Signals::Stop.new(self.class, reason: "autovacuum running on: #{autovacuum_active_on.join(', ')}")
- end
- end
-
- private
-
- attr_reader :context
-
- def enabled?
- Feature.enabled?(:batched_migrations_health_status_autovacuum, type: :ops)
- end
-
- def active_autovacuums_for(tables)
- Gitlab::Database::PostgresAutovacuumActivity.for_tables(tables)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/background_migration/health_status/indicators/patroni_apdex.rb b/lib/gitlab/database/background_migration/health_status/indicators/patroni_apdex.rb
deleted file mode 100644
index 0dd6dd5c2a4..00000000000
--- a/lib/gitlab/database/background_migration/health_status/indicators/patroni_apdex.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module BackgroundMigration
- module HealthStatus
- module Indicators
- class PatroniApdex
- include Gitlab::Utils::StrongMemoize
-
- def initialize(context)
- @context = context
- end
-
- def evaluate
- return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
-
- connection_error_message = fetch_connection_error_message
- return unknown_signal(connection_error_message) if connection_error_message.present?
-
- apdex_sli = fetch_sli(apdex_sli_query)
- return unknown_signal('Patroni service apdex can not be calculated') unless apdex_sli.present?
-
- if apdex_sli.to_f > apdex_slo.to_f
- Signals::Normal.new(self.class, reason: 'Patroni service apdex is above SLO')
- else
- Signals::Stop.new(self.class, reason: 'Patroni service apdex is below SLO')
- end
- end
-
- private
-
- attr_reader :context
-
- def enabled?
- Feature.enabled?(:batched_migrations_health_status_patroni_apdex, type: :ops)
- end
-
- def unknown_signal(reason)
- Signals::Unknown.new(self.class, reason: reason)
- end
-
- def fetch_connection_error_message
- return 'Patroni Apdex Settings not configured' unless database_apdex_settings.present?
- return 'Prometheus client is not ready' unless client.ready?
- return 'Apdex SLI query is not configured' unless apdex_sli_query
- return 'Apdex SLO is not configured' unless apdex_slo
- end
-
- def client
- @client ||= Gitlab::PrometheusClient.new(
- database_apdex_settings[:prometheus_api_url],
- allow_local_requests: true,
- verify: true
- )
- end
-
- def database_apdex_settings
- @database_apdex_settings ||= Gitlab::CurrentSettings.database_apdex_settings&.with_indifferent_access
- end
-
- def apdex_sli_query
- {
- gitlab_main: database_apdex_settings[:apdex_sli_query][:main],
- gitlab_ci: database_apdex_settings[:apdex_sli_query][:ci]
- }.fetch(context.gitlab_schema.to_sym)
- end
- strong_memoize_attr :apdex_sli_query
-
- def apdex_slo
- {
- gitlab_main: database_apdex_settings[:apdex_slo][:main],
- gitlab_ci: database_apdex_settings[:apdex_slo][:ci]
- }.fetch(context.gitlab_schema.to_sym)
- end
- strong_memoize_attr :apdex_slo
-
- def fetch_sli(query)
- response = client.query(query)
- metric = response&.first || {}
- value = metric.fetch('value', [])
-
- Array.wrap(value).second
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log.rb b/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log.rb
deleted file mode 100644
index d2fb0a8b751..00000000000
--- a/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module BackgroundMigration
- module HealthStatus
- module Indicators
- class WriteAheadLog
- include Gitlab::Utils::StrongMemoize
-
- LIMIT = 42
- PENDING_WAL_COUNT_SQL = <<~SQL
- WITH
- current_wal_file AS (
- SELECT pg_walfile_name(pg_current_wal_insert_lsn()) AS pg_walfile_name
- ),
- current_wal AS (
- SELECT
- ('x' || substring(pg_walfile_name, 9, 8))::bit(32)::int AS log,
- ('x' || substring(pg_walfile_name, 17, 8))::bit(32)::int AS seg,
- pg_walfile_name
- FROM current_wal_file
- ),
- archive_wal AS (
- SELECT
- ('x' || substring(last_archived_wal, 9, 8))::bit(32)::int AS log,
- ('x' || substring(last_archived_wal, 17, 8))::bit(32)::int AS seg,
- last_archived_wal
- FROM pg_stat_archiver
- )
- SELECT ((current_wal.log - archive_wal.log) * 256) + (current_wal.seg - archive_wal.seg) AS pending_wal_count
- FROM current_wal, archive_wal
- SQL
-
- def initialize(context)
- @connection = context.connection
- end
-
- def evaluate
- return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
-
- unless pending_wal_count
- return Signals::NotAvailable.new(self.class, reason: 'WAL archive queue can not be calculated')
- end
-
- if pending_wal_count > LIMIT
- Signals::Stop.new(self.class, reason: "WAL archive queue is too big")
- else
- Signals::Normal.new(self.class, reason: 'WAL archive queue is within limit')
- end
- end
-
- private
-
- attr_reader :connection
-
- def enabled?
- Feature.enabled?(:batched_migrations_health_status_wal, type: :ops)
- end
-
- # Returns number of WAL segments pending archival
- def pending_wal_count
- strong_memoize(:pending_wal_count) do
- Gitlab::Database::LoadBalancing::Session.current.use_primary do
- connection.execute(PENDING_WAL_COUNT_SQL).to_a.first&.fetch('pending_wal_count')
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/background_migration/health_status/signals.rb b/lib/gitlab/database/background_migration/health_status/signals.rb
deleted file mode 100644
index 534c4330cf2..00000000000
--- a/lib/gitlab/database/background_migration/health_status/signals.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module BackgroundMigration
- module HealthStatus
- module Signals
- # Base class for a signal
- class Base
- attr_reader :indicator_class, :reason
-
- def initialize(indicator_class, reason:)
- @indicator_class = indicator_class
- @reason = reason
- end
-
- def to_s
- "#{short_name} (indicator: #{indicator_class}; reason: #{reason})"
- end
-
- # :nocov:
- def log_info?
- false
- end
-
- def stop?
- false
- end
- # :nocov:
-
- def short_name
- self.class.name.demodulize
- end
- end
-
- # A Signals::Stop is an indication to put a migration on hold or stop it entirely:
- # In general, we want to slow down or pause the migration.
- class Stop < Base
- # :nocov:
- def log_info?
- true
- end
-
- def stop?
- true
- end
- # :nocov:
- end
-
- # A Signals::Normal indicates normal system state: We carry on with the migration
- # and may even attempt to optimize its throughput etc.
- class Normal < Base; end
-
- # When given an Signals::Unknown, something unexpected happened while
- # we evaluated system indicators.
- class Unknown < Base
- # :nocov:
- def log_info?
- true
- end
- # :nocov:
- end
-
- # No signal could be determined, e.g. because the indicator
- # was disabled.
- class NotAvailable < Base; end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/convert_feature_category_to_group_label.rb b/lib/gitlab/database/convert_feature_category_to_group_label.rb
new file mode 100644
index 00000000000..5a4599312ba
--- /dev/null
+++ b/lib/gitlab/database/convert_feature_category_to_group_label.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class ConvertFeatureCategoryToGroupLabel
+ STAGES_URL = 'https://gitlab.com/gitlab-com/www-gitlab-com/-/raw/master/data/stages.yml'
+
+ def initialize(feature_category)
+ @feature_category = feature_category
+ end
+
+ def execute
+ feature_categories_map[feature_category]
+ end
+
+ private
+
+ attr_reader :feature_category
+
+ def stages
+ response = Gitlab::HTTP.get(STAGES_URL)
+
+ YAML.safe_load(response) if response.success?
+ end
+
+ def feature_categories_map
+ stages['stages'].each_with_object({}) do |(_, stage), result|
+ stage['groups'].each do |group_name, group|
+ group['categories'].each do |category|
+ result[category] = "group::#{group_name.sub('_', ' ')}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/database_connection_info.rb b/lib/gitlab/database/database_connection_info.rb
new file mode 100644
index 00000000000..57ecbcd64ae
--- /dev/null
+++ b/lib/gitlab/database/database_connection_info.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ DatabaseConnectionInfo = Struct.new(
+ :name,
+ :description,
+ :gitlab_schemas,
+ :klass,
+ :fallback_database,
+ :db_dir,
+ :uses_load_balancing,
+ :file_path,
+ keyword_init: true
+ ) do
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(*)
+ super
+ self.name = name.to_sym
+ self.gitlab_schemas = gitlab_schemas.map(&:to_sym)
+ self.klass = klass.constantize
+ self.fallback_database = fallback_database&.to_sym
+ self.db_dir = Rails.root.join(db_dir || 'db')
+ end
+
+ def self.load_file(yaml_file)
+ content = YAML.load_file(yaml_file)
+ new(**content.deep_symbolize_keys.merge(file_path: yaml_file))
+ end
+
+ def active_record_base?
+ klass == ActiveRecord::Base
+ end
+ private :active_record_base?
+
+ strong_memoize_attr def connection_class
+ klass.connection_class || active_record_base? ? klass : nil
+ end
+
+ strong_memoize_attr def order
+ # Retain order of configurations as they are defined in `config/database.yml`
+ ActiveRecord::Base.configurations
+ .configs_for(env_name: Rails.env)
+ .map(&:name)
+ .index(name.to_s) || 1_000
+ end
+
+ def connection_class_or_fallback(all_databases)
+ if connection_class
+ connection_class
+ elsif fallback_database
+ all_databases.fetch(fallback_database)
+ .connection_class_or_fallback(all_databases)
+ end
+ end
+
+ def has_gitlab_shared?
+ gitlab_schemas.include?(:gitlab_shared)
+ end
+
+ def uses_load_balancing?
+ !!uses_load_balancing
+ end
+
+ def db_docs_dir
+ db_dir.join('docs')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/each_database.rb b/lib/gitlab/database/each_database.rb
index 02f008abf85..b1af62e4875 100644
--- a/lib/gitlab/database/each_database.rb
+++ b/lib/gitlab/database/each_database.rb
@@ -18,6 +18,7 @@ module Gitlab
end
end
end
+ alias_method :each_db_connection, :each_database_connection
def each_model_connection(models, only_on: nil, &blk)
selected_databases = Array.wrap(only_on).map(&:to_sym)
diff --git a/lib/gitlab/database/gitlab_schema.rb b/lib/gitlab/database/gitlab_schema.rb
index 4394c089b22..9b58284b389 100644
--- a/lib/gitlab/database/gitlab_schema.rb
+++ b/lib/gitlab/database/gitlab_schema.rb
@@ -3,13 +3,15 @@
# This module gathers information about table to schema mapping
# to understand table affinity
#
-# Each table / view needs to have assigned gitlab_schema. Names supported today:
+# Each table / view needs to have assigned gitlab_schema. For example:
#
# - gitlab_shared - defines a set of tables that are found on all databases (data accessed is dependent on connection)
# - gitlab_main / gitlab_ci - defines a set of tables that can only exist on a given application database
# - gitlab_geo - defines a set of tables that can only exist on the geo database
# - gitlab_internal - defines all internal tables of Rails and PostgreSQL
#
+# All supported GitLab schemas can be viewed in `db/gitlab_schemas/` and `ee/db/gitlab_schemas/`
+#
# Tables for the purpose of tests should be prefixed with `_test_my_table_name`
module Gitlab
@@ -17,8 +19,6 @@ module Gitlab
module GitlabSchema
UnknownSchemaError = Class.new(StandardError)
- DICTIONARY_PATH = 'db/docs/'
-
def self.table_schemas!(tables)
tables.map { |table| table_schema!(table) }.to_set
end
@@ -67,44 +67,50 @@ module Gitlab
# All `pg_` tables are marked as `internal`
return :gitlab_internal if table_name.start_with?('pg_')
-
- # Sometimes the name of an index can be interpreted as a table's name.
- # For eg, if we execute "ALTER INDEX my_index..", my_index is interpreted as a table name.
- # In such cases, we should return the schema of the database table actually
- # holding that index.
- index_name = table_name
- derive_schema_from_index(index_name)
end
# rubocop:enable Metrics/CyclomaticComplexity
- def self.dictionary_path_globs
- [Rails.root.join(DICTIONARY_PATH, '*.yml')]
+ def self.table_schema!(name)
+ # rubocop:disable Gitlab/DocUrl
+ self.table_schema(name) || raise(
+ UnknownSchemaError,
+ "Could not find gitlab schema for table #{name}: Any new or deleted tables must be added to the database dictionary " \
+ "See https://docs.gitlab.com/ee/development/database/database_dictionary.html"
+ )
+ # rubocop:enable Gitlab/DocUrl
end
- def self.view_path_globs
- [Rails.root.join(DICTIONARY_PATH, 'views', '*.yml')]
+ private_class_method def self.cross_access_allowed?(type, table_schemas)
+ table_schemas.any? do |schema|
+ extra_schemas = table_schemas - [schema]
+ extra_schemas -= Gitlab::Database.all_gitlab_schemas[schema]&.public_send(type) || [] # rubocop:disable GitlabSecurity/PublicSend
+ extra_schemas.empty?
+ end
end
- def self.deleted_views_path_globs
- [Rails.root.join(DICTIONARY_PATH, 'deleted_views', '*.yml')]
+ def self.cross_joins_allowed?(table_schemas)
+ table_schemas.empty? || self.cross_access_allowed?(:allow_cross_joins, table_schemas)
end
- def self.deleted_tables_path_globs
- [Rails.root.join(DICTIONARY_PATH, 'deleted_tables', '*.yml')]
+ def self.cross_transactions_allowed?(table_schemas)
+ table_schemas.empty? || self.cross_access_allowed?(:allow_cross_transactions, table_schemas)
end
- def self.views_and_tables_to_schema
- @views_and_tables_to_schema ||= self.tables_to_schema.merge(self.views_to_schema)
+ def self.cross_foreign_key_allowed?(table_schemas)
+ self.cross_access_allowed?(:allow_cross_foreign_keys, table_schemas)
end
- def self.table_schema!(name)
- # rubocop:disable Gitlab/DocUrl
- self.table_schema(name) || raise(
- UnknownSchemaError,
- "Could not find gitlab schema for table #{name}: Any new or deleted tables must be added to the database dictionary " \
- "See https://docs.gitlab.com/ee/development/database/database_dictionary.html"
- )
- # rubocop:enable Gitlab/DocUrl
+ def self.dictionary_paths
+ Gitlab::Database.all_database_connections
+ .values.map(&:db_docs_dir).uniq
+ end
+
+ def self.dictionary_path_globs(scope)
+ self.dictionary_paths.map { |path| Rails.root.join(path, scope, '*.yml') }
+ end
+
+ def self.views_and_tables_to_schema
+ @views_and_tables_to_schema ||= self.tables_to_schema.merge(self.views_to_schema)
end
def self.deleted_views_and_tables_to_schema
@@ -112,36 +118,27 @@ module Gitlab
end
def self.deleted_tables_to_schema
- @deleted_tables_to_schema ||= self.build_dictionary(self.deleted_tables_path_globs)
+ @deleted_tables_to_schema ||= self.build_dictionary('deleted_tables').to_h
end
def self.deleted_views_to_schema
- @deleted_views_to_schema ||= self.build_dictionary(self.deleted_views_path_globs)
+ @deleted_views_to_schema ||= self.build_dictionary('deleted_views').to_h
end
def self.tables_to_schema
- @tables_to_schema ||= self.build_dictionary(self.dictionary_path_globs)
+ @tables_to_schema ||= self.build_dictionary('').to_h
end
def self.views_to_schema
- @views_to_schema ||= self.build_dictionary(self.view_path_globs)
+ @views_to_schema ||= self.build_dictionary('views').to_h
end
def self.schema_names
@schema_names ||= self.views_and_tables_to_schema.values.to_set
end
- private_class_method def self.derive_schema_from_index(index_name)
- index = Gitlab::Database::PostgresIndex.find_by(name: index_name,
- schema: ApplicationRecord.connection.current_schema)
-
- return unless index
-
- table_schema(index.tablename)
- end
-
- private_class_method def self.build_dictionary(path_globs)
- Dir.glob(path_globs).each_with_object({}) do |file_path, dic|
+ def self.build_dictionary(scope)
+ Dir.glob(dictionary_path_globs(scope)).map do |file_path|
data = YAML.load_file(file_path)
key_name = data['table_name'] || data['view_name']
@@ -156,7 +153,7 @@ module Gitlab
end
# rubocop:enable Gitlab/DocUrl
- dic[key_name] = data['gitlab_schema'].to_sym
+ [key_name, data['gitlab_schema'].to_sym]
end
end
end
diff --git a/lib/gitlab/database/gitlab_schema_info.rb b/lib/gitlab/database/gitlab_schema_info.rb
new file mode 100644
index 00000000000..34b89cb9006
--- /dev/null
+++ b/lib/gitlab/database/gitlab_schema_info.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ GitlabSchemaInfo = Struct.new(
+ :name,
+ :description,
+ :allow_cross_joins,
+ :allow_cross_transactions,
+ :allow_cross_foreign_keys,
+ :file_path,
+ keyword_init: true
+ ) do
+ def initialize(*)
+ super
+ self.name = name.to_sym
+ self.allow_cross_joins = allow_cross_joins&.map(&:to_sym)&.freeze
+ self.allow_cross_transactions = allow_cross_transactions&.map(&:to_sym)&.freeze
+ self.allow_cross_foreign_keys = allow_cross_foreign_keys&.map(&:to_sym)&.freeze
+ end
+
+ def self.load_file(yaml_file)
+ content = YAML.load_file(yaml_file)
+ new(**content.deep_symbolize_keys.merge(file_path: yaml_file))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status.rb b/lib/gitlab/database/health_status.rb
new file mode 100644
index 00000000000..69bb8a70afd
--- /dev/null
+++ b/lib/gitlab/database/health_status.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ DEFAULT_INIDICATORS = [
+ Indicators::AutovacuumActiveOnTable,
+ Indicators::WriteAheadLog,
+ Indicators::PatroniApdex
+ ].freeze
+
+ class << self
+ def evaluate(context, indicators = DEFAULT_INIDICATORS)
+ indicators.map do |indicator|
+ signal = begin
+ indicator.new(context).evaluate
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e, **context.status_checker_info)
+
+ Signals::Unknown.new(indicator, reason: "unexpected error: #{e.message} (#{e.class})")
+ end
+
+ log_signal(signal, context) if signal.log_info?
+
+ signal
+ end
+ end
+
+ def log_signal(signal, context)
+ Gitlab::Database::HealthStatus::Logger.info(**context.status_checker_info.merge(
+ health_status_indicator: signal.indicator_class.to_s,
+ indicator_signal: signal.short_name,
+ signal_reason: signal.reason,
+ message: "#{context.status_checker} signaled: #{signal}"
+ ))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status/context.rb b/lib/gitlab/database/health_status/context.rb
new file mode 100644
index 00000000000..717257a84ad
--- /dev/null
+++ b/lib/gitlab/database/health_status/context.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ class Context
+ attr_reader :status_checker, :connection, :tables, :gitlab_schema
+
+ # status_checker: the caller object which checks for database health status
+ # eg: BackgroundMigration::BatchedMigration or DeferJobs::DatabaseHealthStatusChecker
+ def initialize(status_checker, connection, tables, gitlab_schema)
+ @status_checker = status_checker
+ @connection = connection
+ @tables = tables
+ @gitlab_schema = gitlab_schema
+ end
+
+ def status_checker_info
+ {
+ status_checker_id: status_checker.id,
+ status_checker_type: status_checker.class.name,
+ job_class_name: status_checker.job_class_name
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status/indicators.rb b/lib/gitlab/database/health_status/indicators.rb
new file mode 100644
index 00000000000..a149c36aae4
--- /dev/null
+++ b/lib/gitlab/database/health_status/indicators.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ module Indicators
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status/indicators/autovacuum_active_on_table.rb b/lib/gitlab/database/health_status/indicators/autovacuum_active_on_table.rb
new file mode 100644
index 00000000000..6bf2bbf0c70
--- /dev/null
+++ b/lib/gitlab/database/health_status/indicators/autovacuum_active_on_table.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ module Indicators
+ class AutovacuumActiveOnTable
+ def initialize(context)
+ @tables = context.tables
+ end
+
+ def evaluate
+ return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
+
+ autovacuum_active_on = active_autovacuums_for(tables)
+
+ if autovacuum_active_on.empty?
+ Signals::Normal.new(self.class, reason: 'no autovacuum running on any relevant tables')
+ else
+ Signals::Stop.new(self.class, reason: "autovacuum running on: #{autovacuum_active_on.join(', ')}")
+ end
+ end
+
+ private
+
+ attr_reader :tables
+
+ def enabled?
+ Feature.enabled?(:batched_migrations_health_status_autovacuum, type: :ops)
+ end
+
+ def active_autovacuums_for(tables)
+ Gitlab::Database::PostgresAutovacuumActivity.for_tables(tables)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status/indicators/patroni_apdex.rb b/lib/gitlab/database/health_status/indicators/patroni_apdex.rb
new file mode 100644
index 00000000000..680c86cf7b2
--- /dev/null
+++ b/lib/gitlab/database/health_status/indicators/patroni_apdex.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ module Indicators
+ class PatroniApdex
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(context)
+ @gitlab_schema = context.gitlab_schema.to_sym
+ end
+
+ def evaluate
+ return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
+
+ connection_error_message = fetch_connection_error_message
+ return unknown_signal(connection_error_message) if connection_error_message.present?
+
+ apdex_sli = fetch_sli(apdex_sli_query)
+ return unknown_signal('Patroni service apdex can not be calculated') unless apdex_sli.present?
+
+ if apdex_sli.to_f > apdex_slo.to_f
+ Signals::Normal.new(self.class, reason: 'Patroni service apdex is above SLO')
+ else
+ Signals::Stop.new(self.class, reason: 'Patroni service apdex is below SLO')
+ end
+ end
+
+ private
+
+ attr_reader :gitlab_schema
+
+ def enabled?
+ Feature.enabled?(:batched_migrations_health_status_patroni_apdex, type: :ops)
+ end
+
+ def unknown_signal(reason)
+ Signals::Unknown.new(self.class, reason: reason)
+ end
+
+ def fetch_connection_error_message
+ return 'Patroni Apdex Settings not configured' unless database_apdex_settings.present?
+ return 'Prometheus client is not ready' unless client.ready?
+ return 'Apdex SLI query is not configured' unless apdex_sli_query
+ return 'Apdex SLO is not configured' unless apdex_slo
+ end
+
+ def client
+ @client ||= Gitlab::PrometheusClient.new(
+ database_apdex_settings[:prometheus_api_url],
+ allow_local_requests: true,
+ verify: true
+ )
+ end
+
+ def database_apdex_settings
+ @database_apdex_settings ||= Gitlab::CurrentSettings.database_apdex_settings&.with_indifferent_access
+ end
+
+ def apdex_sli_query
+ {
+ gitlab_main: database_apdex_settings[:apdex_sli_query][:main],
+ gitlab_ci: database_apdex_settings[:apdex_sli_query][:ci]
+ }.fetch(gitlab_schema)
+ end
+ strong_memoize_attr :apdex_sli_query
+
+ def apdex_slo
+ {
+ gitlab_main: database_apdex_settings[:apdex_slo][:main],
+ gitlab_ci: database_apdex_settings[:apdex_slo][:ci]
+ }.fetch(gitlab_schema)
+ end
+ strong_memoize_attr :apdex_slo
+
+ def fetch_sli(query)
+ response = client.query(query)
+ metric = response&.first || {}
+ value = metric.fetch('value', [])
+
+ Array.wrap(value).second
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status/indicators/write_ahead_log.rb b/lib/gitlab/database/health_status/indicators/write_ahead_log.rb
new file mode 100644
index 00000000000..1614b17df48
--- /dev/null
+++ b/lib/gitlab/database/health_status/indicators/write_ahead_log.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ module Indicators
+ class WriteAheadLog
+ include Gitlab::Utils::StrongMemoize
+
+ LIMIT = 42
+ PENDING_WAL_COUNT_SQL = <<~SQL
+ WITH
+ current_wal_file AS (
+ SELECT pg_walfile_name(pg_current_wal_insert_lsn()) AS pg_walfile_name
+ ),
+ current_wal AS (
+ SELECT
+ ('x' || substring(pg_walfile_name, 9, 8))::bit(32)::int AS log,
+ ('x' || substring(pg_walfile_name, 17, 8))::bit(32)::int AS seg,
+ pg_walfile_name
+ FROM current_wal_file
+ ),
+ archive_wal AS (
+ SELECT
+ ('x' || substring(last_archived_wal, 9, 8))::bit(32)::int AS log,
+ ('x' || substring(last_archived_wal, 17, 8))::bit(32)::int AS seg,
+ last_archived_wal
+ FROM pg_stat_archiver
+ )
+ SELECT ((current_wal.log - archive_wal.log) * 256) + (current_wal.seg - archive_wal.seg) AS pending_wal_count
+ FROM current_wal, archive_wal
+ SQL
+
+ def initialize(context)
+ @connection = context.connection
+ end
+
+ def evaluate
+ return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
+
+ unless pending_wal_count
+ return Signals::NotAvailable.new(self.class, reason: 'WAL archive queue can not be calculated')
+ end
+
+ if pending_wal_count > LIMIT
+ Signals::Stop.new(self.class, reason: "WAL archive queue is too big")
+ else
+ Signals::Normal.new(self.class, reason: 'WAL archive queue is within limit')
+ end
+ end
+
+ private
+
+ attr_reader :connection
+
+ def enabled?
+ Feature.enabled?(:batched_migrations_health_status_wal, type: :ops)
+ end
+
+ # Returns number of WAL segments pending archival
+ def pending_wal_count
+ Gitlab::Database::LoadBalancing::Session.current.use_primary do
+ connection.execute(PENDING_WAL_COUNT_SQL).to_a.first&.fetch('pending_wal_count')
+ end
+ end
+ strong_memoize_attr :pending_wal_count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status/logger.rb b/lib/gitlab/database/health_status/logger.rb
new file mode 100644
index 00000000000..820c1aeb695
--- /dev/null
+++ b/lib/gitlab/database/health_status/logger.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ class Logger < ::Gitlab::JsonLogger
+ exclude_context!
+
+ def self.file_name_noext
+ 'database_health_status'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/health_status/signals.rb b/lib/gitlab/database/health_status/signals.rb
new file mode 100644
index 00000000000..e1bcdcae9c7
--- /dev/null
+++ b/lib/gitlab/database/health_status/signals.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module HealthStatus
+ module Signals
+ # Base class for a signal
+ class Base
+ attr_reader :indicator_class, :reason
+
+ def initialize(indicator_class, reason:)
+ @indicator_class = indicator_class
+ @reason = reason
+ end
+
+ def to_s
+ "#{short_name} (indicator: #{indicator_class}; reason: #{reason})"
+ end
+
+ def log_info?
+ false
+ end
+
+ def stop?
+ false
+ end
+
+ def short_name
+ self.class.name.demodulize
+ end
+ end
+
+ # A Signals::Stop is an indication to put a migration on hold or stop it entirely:
+ # In general, we want to slow down or pause the migration.
+ class Stop < Base
+ def log_info?
+ true
+ end
+
+ def stop?
+ true
+ end
+ end
+
+ # A Signals::Normal indicates normal system state: We carry on with the migration
+ # and may even attempt to optimize its throughput etc.
+ class Normal < Base; end
+
+ # When given an Signals::Unknown, something unexpected happened while
+ # we evaluated system indicators.
+ class Unknown < Base
+ def log_info?
+ true
+ end
+ end
+
+ # No signal could be determined, e.g. because the indicator
+ # was disabled.
+ class NotAvailable < Base; end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/load_balancing/host.rb b/lib/gitlab/database/load_balancing/host.rb
index bdbb80d6f31..f8ed5fcd4cc 100644
--- a/lib/gitlab/database/load_balancing/host.rb
+++ b/lib/gitlab/database/load_balancing/host.rb
@@ -16,6 +16,43 @@ module Gitlab
PG::Error
].freeze
+ # This query checks that the current user has permissions before we try and query logical replication status. We
+ # also only allow >= PG14 because these views are only accessible to superuser before PG14 even if the
+ # has_table_privilege says otherwise.
+ CAN_TRACK_LOGICAL_LSN_QUERY = <<~SQL.squish.freeze
+ SELECT
+ has_table_privilege('pg_replication_origin_status', 'select')
+ AND
+ has_function_privilege('pg_show_replication_origin_status()', 'execute')
+ AND current_setting('server_version_num', true)::int >= 140000
+ AS allowed
+ SQL
+
+ # The following is necessary to handle a mix of logical and physical replicas. We assume that if they have
+ # pg_replication_origin_status then they are a logical replica. In a logical replica we need to use
+ # `remote_lsn` rather than `pg_last_wal_replay_lsn` in order for our LSN to be comparable to the source
+ # cluster. This logic would be broken if we have 2 logical subscriptions or if we have a logical subscription
+ # in the source primary cluster. Read more at https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121621
+ LATEST_LSN_WITH_LOGICAL_QUERY = <<~SQL.squish.freeze
+ CASE
+ WHEN (SELECT TRUE FROM pg_replication_origin_status) THEN
+ (SELECT remote_lsn FROM pg_replication_origin_status)
+ WHEN pg_is_in_recovery() THEN
+ pg_last_wal_replay_lsn()
+ ELSE
+ pg_current_wal_insert_lsn()
+ END
+ SQL
+
+ LATEST_LSN_WITHOUT_LOGICAL_QUERY = <<~SQL.squish.freeze
+ CASE
+ WHEN pg_is_in_recovery() THEN
+ pg_last_wal_replay_lsn()
+ ELSE
+ pg_current_wal_insert_lsn()
+ END
+ SQL
+
# host - The address of the database.
# load_balancer - The LoadBalancer that manages this Host.
def initialize(host, load_balancer, port: nil)
@@ -30,6 +67,7 @@ module Gitlab
@online = true
@last_checked_at = Time.zone.now
+ # Randomly somewhere in between interval and 2*interval we'll refresh the status of the host
interval = load_balancer.configuration.replica_check_interval
@intervals = (interval..(interval * 2)).step(0.5).to_a
end
@@ -91,6 +129,7 @@ module Gitlab
end
def refresh_status
+ @latest_lsn_query = nil # Periodically clear the cached @latest_lsn_query value in case permissions change
@online = replica_is_up_to_date?
@last_checked_at = Time.zone.now
end
@@ -142,11 +181,11 @@ module Gitlab
# primary.
#
# This method will return nil if no lag size could be calculated.
- def replication_lag_size
- location = connection.quote(primary_write_location)
+ def replication_lag_size(location = primary_write_location)
+ location = connection.quote(location)
+
row = query_and_release(<<-SQL.squish)
- SELECT pg_wal_lsn_diff(#{location}, pg_last_wal_replay_lsn())::float
- AS diff
+ SELECT pg_wal_lsn_diff(#{location}, (#{latest_lsn_query}))::float AS diff
SQL
row['diff'].to_i if row.any?
@@ -173,22 +212,8 @@ module Gitlab
#
# location - The transaction write location as reported by a primary.
def caught_up?(location)
- string = connection.quote(location)
-
- # In case the host is a primary pg_last_wal_replay_lsn/pg_last_xlog_replay_location() returns
- # NULL. The recovery check ensures we treat the host as up-to-date in
- # such a case.
- query = <<-SQL.squish
- SELECT NOT pg_is_in_recovery()
- OR pg_wal_lsn_diff(pg_last_wal_replay_lsn(), #{string}) >= 0
- AS result
- SQL
-
- row = query_and_release(query)
-
- ::Gitlab::Utils.to_boolean(row['result'])
- rescue *CONNECTION_ERRORS
- false
+ lag = replication_lag_size(location)
+ lag.present? && lag.to_i <= 0
end
def query_and_release(sql)
@@ -198,6 +223,22 @@ module Gitlab
ensure
release_connection
end
+
+ private
+
+ def can_track_logical_lsn?
+ row = query_and_release(CAN_TRACK_LOGICAL_LSN_QUERY)
+
+ ::Gitlab::Utils.to_boolean(row['allowed'])
+ rescue *CONNECTION_ERRORS
+ false
+ end
+
+ # The LATEST_LSN_WITH_LOGICAL query requires permissions that may not be present in self-managed configurations.
+ # We fallback gracefully to the query that does not correctly handle logical replicas for such configurations.
+ def latest_lsn_query
+ @latest_lsn_query ||= can_track_logical_lsn? ? LATEST_LSN_WITH_LOGICAL_QUERY : LATEST_LSN_WITHOUT_LOGICAL_QUERY
+ end
end
end
end
diff --git a/lib/gitlab/database/lock_writes_manager.rb b/lib/gitlab/database/lock_writes_manager.rb
index 43e71e6bda2..8ddd871f93c 100644
--- a/lib/gitlab/database/lock_writes_manager.rb
+++ b/lib/gitlab/database/lock_writes_manager.rb
@@ -51,10 +51,15 @@ module Gitlab
execute_sql_statement(sql_statement)
- result_hash(action: 'locked')
+ result_hash(action: dry_run ? 'needs_lock' : 'locked')
end
def unlock_writes
+ unless table_locked_for_writes?
+ logger&.info "Skipping unlock_writes, because #{table_name} is already unlocked for writes"
+ return result_hash(action: 'skipped')
+ end
+
logger&.info "Database: '#{database_name}', Table: '#{table_name}': Allow Writes".color(:green)
sql_statement = <<~SQL
DROP TRIGGER IF EXISTS #{write_trigger_name} ON #{table_name};
@@ -62,7 +67,7 @@ module Gitlab
execute_sql_statement(sql_statement)
- result_hash(action: 'unlocked')
+ result_hash(action: dry_run ? 'needs_unlock' : 'unlocked')
end
private
diff --git a/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb b/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
new file mode 100644
index 00000000000..555efb58606
--- /dev/null
+++ b/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module MigrationHelpers
+ module WraparoundAutovacuum
+ # This is used for partitioning CI tables because the autovacuum for wraparound
+ # prevention can take many hours to complete on some of the tables and this in
+ # turn blocks the post deployment migrations pipeline.
+ # Intended workflow for this helper:
+ # 1. Introduce a migration that is guarded with this helper
+ # 2. Check that the migration was successfully executed on .com
+ # 3. Introduce the migration again for self-managed.
+ #
+ def can_execute_on?(*tables)
+ return false unless Gitlab.com? || Gitlab.dev_or_test_env?
+
+ if wraparound_prevention_on_tables?(tables)
+ Gitlab::AppLogger.info(message: "Wraparound prevention vacuum detected", class: self.class)
+ say "Wraparound prevention vacuum detected, skipping migration"
+ return false
+ end
+
+ true
+ end
+
+ def wraparound_prevention_on_tables?(tables)
+ Gitlab::Database::PostgresAutovacuumActivity.reset_column_information
+
+ Gitlab::Database::PostgresAutovacuumActivity
+ .wraparound_prevention
+ .for_tables(tables)
+ .any?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migrations/constraints_helpers.rb b/lib/gitlab/database/migrations/constraints_helpers.rb
index 5aafc9f1444..1771f1f0fa7 100644
--- a/lib/gitlab/database/migrations/constraints_helpers.rb
+++ b/lib/gitlab/database/migrations/constraints_helpers.rb
@@ -262,6 +262,16 @@ module Gitlab
SQL
end
+ def switch_constraint_names(table_name, constraint_a, constraint_b)
+ validate_not_in_transaction!(:switch_constraint_names)
+
+ with_lock_retries do
+ rename_constraint(table_name, constraint_a, :temp_name_for_renaming)
+ rename_constraint(table_name, constraint_b, constraint_a)
+ rename_constraint(table_name, :temp_name_for_renaming, constraint_b)
+ end
+ end
+
def validate_check_constraint_name!(constraint_name)
return unless constraint_name.to_s.length > MAX_IDENTIFIER_NAME_LENGTH
diff --git a/lib/gitlab/database/partitioning.rb b/lib/gitlab/database/partitioning.rb
index 7222f148b3f..9895a68ec8d 100644
--- a/lib/gitlab/database/partitioning.rb
+++ b/lib/gitlab/database/partitioning.rb
@@ -40,7 +40,7 @@ module Gitlab
next if model < ::Gitlab::Database::SharedModel && !(model < TableWithoutModel)
model_connection_name = model.connection_db_config.name
- Gitlab::Database::EachDatabase.each_database_connection do |connection, connection_name|
+ Gitlab::Database::EachDatabase.each_db_connection(include_shared: false) do |connection, connection_name|
if connection_name != model_connection_name
PartitionManager.new(model, connection: connection).sync_partitions
end
@@ -60,6 +60,8 @@ module Gitlab
end
def drop_detached_partitions
+ return unless Feature.enabled?(:partition_manager_sync_partitions, type: :ops)
+
Gitlab::AppLogger.info(message: 'Dropping detached postgres partitions')
Gitlab::Database::EachDatabase.each_database_connection do
diff --git a/lib/gitlab/database/partitioning/list/convert_table.rb b/lib/gitlab/database/partitioning/list/convert_table.rb
index d4fb150d956..9889d01be76 100644
--- a/lib/gitlab/database/partitioning/list/convert_table.rb
+++ b/lib/gitlab/database/partitioning/list/convert_table.rb
@@ -11,12 +11,11 @@ module Gitlab
PARTITIONING_CONSTRAINT_NAME = 'partitioning_constraint'
- attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value,
- :locking_configuration
+ attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value
def initialize(
migration_context:, table_name:, parent_table_name:, partitioning_column:,
- zero_partition_value:, lock_tables: [])
+ zero_partition_value:)
@migration_context = migration_context
@connection = migration_context.connection
@@ -24,7 +23,6 @@ module Gitlab
@parent_table_name = parent_table_name
@partitioning_column = partitioning_column
@zero_partition_value = zero_partition_value
- @locking_configuration = LockingConfiguration.new(migration_context, table_locking_order: lock_tables)
end
def prepare_for_partitioning(async: false)
@@ -38,22 +36,24 @@ module Gitlab
end
def partition
- assert_existing_constraints_partitionable
- assert_partitioning_constraint_present
-
- create_parent_table
- attach_foreign_keys_to_parent
- locking_sql = locking_configuration.locking_statement_for(tables_that_will_lock_during_partitioning)
-
- locking_configuration.with_lock_retries do
- # Loose FKs trigger will exclusively lock the table and it might
- # not follow the locking order needed to attach the partition.
- migration_context.execute(locking_sql) if locking_sql.present?
-
- redefine_loose_foreign_key_triggers do
- migration_context.execute(sql_to_convert_table)
+ # If already partitioned, the table is no longer partitionable. Thus we skip checks leading up
+ # to partitioning if the partitioning transaction has already succeeded.
+ unless already_partitioned?
+ assert_existing_constraints_partitionable
+ assert_partitioning_constraint_present
+
+ create_parent_table
+
+ migration_context.with_lock_retries do
+ redefine_loose_foreign_key_triggers do
+ migration_context.execute(sql_to_convert_table)
+ end
end
end
+
+ # Attaching foreign keys handles cases where one or more foreign keys already exists, so it doesn't
+ # need a check similar to the rest of this method
+ attach_foreign_keys_to_parent
end
def revert_partitioning
@@ -194,6 +194,8 @@ module Gitlab
# At this point no other connection knows about the parent table.
# Thus the only contended lock in the following transaction is on fk.to_table.
# So a deadlock is impossible.
+ # (We also take a share update exclusive lock against the recently attached child table,
+ # but that only blocks vacuum and other schema modifications, not reads or writes)
# If we're rerunning this migration after a failure to acquire a lock, the foreign key might already exist
# Don't try to recreate it in that case
@@ -299,16 +301,8 @@ module Gitlab
end
end
- def tables_that_will_lock_during_partitioning
- # Locks are taken against the table + all tables that reference it by foreign key
- # postgres_foreign_keys.referenced_table_name gives the table name that we need here directly, but that
- # column did not exist yet during the migration 20221021145820_create_routing_table_for_builds_metadata_v2
- # To ensure compatibility with that migration if it is run with this code, use referenced_table_identifier
- # here.
- referenced_tables = Gitlab::Database::PostgresForeignKey
- .by_constrained_table_identifier(table_identifier)
- .map { |fk| table_name_for_identifier(fk.referenced_table_identifier) }
- referenced_tables + [table_name]
+ def already_partitioned?
+ Gitlab::Database::PostgresPartition.for_identifier(table_identifier).exists?
end
end
end
diff --git a/lib/gitlab/database/partitioning/sliding_list_strategy.rb b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
index 5bb34a86d43..8f8afdfc551 100644
--- a/lib/gitlab/database/partitioning/sliding_list_strategy.rb
+++ b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
@@ -77,6 +77,19 @@ module Gitlab
end
def validate_and_fix
+ unless model.connection_db_config.name ==
+ Gitlab::Database.db_config_name(Gitlab::Database::SharedModel.connection)
+
+ Gitlab::AppLogger.warn(
+ message: 'Skipping fixing column default because connections mismatch',
+ event: :partition_manager_validate_and_fix_connection_mismatch,
+ model_connection_name: Gitlab::Database.db_config_name(model.connection),
+ shared_connection_name: Gitlab::Database.db_config_name(Gitlab::Database::SharedModel.connection)
+ )
+
+ return
+ end
+
return if no_partitions_exist?
old_default_value = current_default_value
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index 61e95dbe1a4..1ce0a44e37f 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -287,8 +287,7 @@ module Gitlab
table_name: table_name,
parent_table_name: parent_table_name,
partitioning_column: partitioning_column,
- zero_partition_value: initial_partitioning_value,
- lock_tables: lock_tables
+ zero_partition_value: initial_partitioning_value
).partition
end
diff --git a/lib/gitlab/database/postgres_autovacuum_activity.rb b/lib/gitlab/database/postgres_autovacuum_activity.rb
index a4dc199c259..6a80c631abb 100644
--- a/lib/gitlab/database/postgres_autovacuum_activity.rb
+++ b/lib/gitlab/database/postgres_autovacuum_activity.rb
@@ -6,9 +6,14 @@ module Gitlab
self.table_name = 'postgres_autovacuum_activity'
self.primary_key = 'table_identifier'
+ scope :wraparound_prevention, -> { where(wraparound_prevention: true) }
+
def self.for_tables(tables)
Gitlab::Database::LoadBalancing::Session.current.use_primary do
- where('schema = current_schema()').where(table: tables)
+ # calling `.to_a` here to execute the query in the primary's scope
+ # and to avoid having the scope chained and re-executed
+ #
+ where('schema = current_schema()').where(table: tables).to_a
end
end
diff --git a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
index d15a0eaa44c..b5a45eed3a4 100644
--- a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
+++ b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
@@ -5,6 +5,7 @@ module Gitlab
module QueryAnalyzers
class PreventCrossDatabaseModification < Database::QueryAnalyzers::Base
CrossDatabaseModificationAcrossUnsupportedTablesError = Class.new(QueryAnalyzerError)
+ QUERY_LIMIT = 10
# This method will allow cross database modifications within the block
# Example:
@@ -42,7 +43,8 @@ module Gitlab
context.merge!({
transaction_depth_by_db: Hash.new { |h, k| h[k] = 0 },
modified_tables_by_db: Hash.new { |h, k| h[k] = Set.new },
- ignored_tables: []
+ ignored_tables: [],
+ queries: []
})
end
@@ -71,6 +73,7 @@ module Gitlab
context[:transaction_depth_by_db][database] -= 1
if context[:transaction_depth_by_db][database] == 0
context[:modified_tables_by_db][database].clear
+ clear_queries
# Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/351531
::CrossDatabaseModification::TransactionStackTrackRecord.log_gitlab_transactions_stack(action: :end_of_transaction)
@@ -104,22 +107,25 @@ module Gitlab
# databases
return if tables == ['schema_migrations']
+ add_to_queries(sql)
context[:modified_tables_by_db][database].merge(tables)
all_tables = context[:modified_tables_by_db].values.flat_map(&:to_a)
schemas = ::Gitlab::Database::GitlabSchema.table_schemas!(all_tables)
-
schemas += ApplicationRecord.gitlab_transactions_stack
- if schemas.many?
- message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
- "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \
- "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."
+ unless ::Gitlab::Database::GitlabSchema.cross_transactions_allowed?(schemas)
+ messages = []
+ messages << "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
+ "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. "
+ messages << "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions " \
+ "for details on how to resolve this exception."
+ messages += cleaned_queries
- raise CrossDatabaseModificationAcrossUnsupportedTablesError, message
+ raise CrossDatabaseModificationAcrossUnsupportedTablesError, messages.join("\n\n")
end
rescue CrossDatabaseModificationAcrossUnsupportedTablesError => e
::Gitlab::ErrorTracking.track_exception(e, { gitlab_schemas: schemas, tables: all_tables, query: parsed.sql })
- raise if raise_exception?
+ raise if dev_or_test_env?
end
# rubocop:enable Metrics/AbcSize
@@ -159,12 +165,28 @@ module Gitlab
end
end
- # We only raise in tests for now otherwise some features will be broken
- # in development. For now we've mostly only added allowlist based on
- # spec names. Until we have allowed all the violations inline we don't
- # want to raise in development.
- def self.raise_exception?
- Rails.env.test?
+ def self.dev_or_test_env?
+ Gitlab.dev_or_test_env?
+ end
+
+ def self.clear_queries
+ return unless dev_or_test_env?
+
+ context[:queries].clear
+ end
+
+ def self.add_to_queries(sql)
+ return unless dev_or_test_env?
+
+ context[:queries].push(sql)
+ end
+
+ def self.cleaned_queries
+ return [] unless dev_or_test_env?
+
+ context[:queries].last(QUERY_LIMIT).each_with_index.map do |sql, i|
+ "#{i}: #{sql}"
+ end
end
def self.in_transaction?
diff --git a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
index f3e0fc26946..4e1ab700542 100644
--- a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
+++ b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
@@ -19,7 +19,8 @@ module Gitlab
gitlab_internal: nil,
# Pods specific changes
- gitlab_main_clusterwide: :gitlab_main
+ gitlab_main_clusterwide: :gitlab_main,
+ gitlab_main_cell: :gitlab_main
}.freeze
class << self
@@ -61,6 +62,7 @@ module Gitlab
def restrict_to_ddl_only(parsed)
tables = self.dml_tables(parsed)
schemas = self.dml_schemas(tables)
+ schemas = self.map_schemas(schemas)
if schemas.any?
self.raise_dml_not_allowed_error("Modifying of '#{tables}' (#{schemas.to_a}) with '#{parsed.sql}'")
@@ -78,8 +80,10 @@ module Gitlab
tables = self.dml_tables(parsed)
schemas = self.dml_schemas(tables)
+ schemas = self.map_schemas(schemas)
+ allowed_schemas = self.map_schemas(self.allowed_gitlab_schemas)
- if (schemas - self.allowed_gitlab_schemas).any?
+ if (schemas - allowed_schemas).any?
raise DMLAccessDeniedError, \
"Select/DML queries (SELECT/UPDATE/DELETE) do access '#{tables}' (#{schemas.to_a}) " \
"which is outside of list of allowed schemas: '#{self.allowed_gitlab_schemas}'. " \
@@ -100,15 +104,19 @@ module Gitlab
end
def dml_schemas(tables)
- extra_schemas = ::Gitlab::Database::GitlabSchema.table_schemas!(tables)
+ ::Gitlab::Database::GitlabSchema.table_schemas!(tables)
+ end
+
+ def map_schemas(schemas)
+ schemas = schemas.to_set
- SCHEMA_MAPPING.each do |schema, mapped_schema|
- next unless extra_schemas.delete?(schema)
+ SCHEMA_MAPPING.each do |in_schema, mapped_schema|
+ next unless schemas.delete?(in_schema)
- extra_schemas.add(mapped_schema) if mapped_schema
+ schemas.add(mapped_schema) if mapped_schema
end
- extra_schemas
+ schemas
end
def raise_dml_not_allowed_error(message)
diff --git a/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter.rb b/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter.rb
index 420195d89dd..20814b098c1 100644
--- a/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter.rb
+++ b/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter.rb
@@ -104,7 +104,7 @@ module Gitlab
when :func_call
"#{parse_node(node.func_call.funcname.first)}()"
when :a_const
- parse_node(node.a_const.val)
+ parse_a_const(node.a_const)
when :type_cast
value = parse_node(node.type_cast.arg)
type = type(node.type_cast.type_name)
@@ -112,10 +112,21 @@ module Gitlab
[MAPPINGS.fetch(value, "'#{value}'"), separator].compact.join('')
else
- node.to_h[node.node].values.last
+ get_value_from_key(node, key: node.node)
end
end
+ def parse_a_const(a_const)
+ return unless a_const
+
+ type = a_const.val
+ get_value_from_key(a_const, key: type)
+ end
+
+ def get_value_from_key(node, key:)
+ node.to_h[key].values.last
+ end
+
def partition_keys
return [] unless partitioning_stmt
diff --git a/lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter.rb b/lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter.rb
new file mode 100644
index 00000000000..3b45f5c77ca
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ module Adapters
+ class ForeignKeyDatabaseAdapter
+ def initialize(query_result)
+ @query_result = query_result
+ end
+
+ def name
+ "#{query_result['schema']}.#{query_result['foreign_key_name']}"
+ end
+
+ def table_name
+ query_result['table_name']
+ end
+
+ def statement
+ query_result['foreign_key_definition']
+ end
+
+ private
+
+ attr_reader :query_result
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter.rb b/lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter.rb
new file mode 100644
index 00000000000..e4c1e1adab3
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ module Adapters
+ class ForeignKeyStructureSqlAdapter
+ STATEMENT_REGEX = /\bREFERENCES\s\K\S+\K\s\(/
+ EXTRACT_REGEX = /\bFOREIGN KEY.*/
+
+ def initialize(parsed_stmt)
+ @parsed_stmt = parsed_stmt
+ end
+
+ def name
+ "#{schema_name}.#{foreign_key_name}"
+ end
+
+ def table_name
+ parsed_stmt.relation.relname
+ end
+
+ # PgQuery parses FK statements with an extra space in the referenced table column.
+ # This extra space needs to be removed.
+ #
+ # @example REFERENCES ci_pipelines (id) => REFERENCES ci_pipelines(id)
+ def statement
+ deparse_stmt[EXTRACT_REGEX].gsub(STATEMENT_REGEX, '(')
+ end
+
+ private
+
+ attr_reader :parsed_stmt
+
+ def schema_name
+ parsed_stmt.relation.schemaname
+ end
+
+ def foreign_key_name
+ parsed_stmt.cmds.first.alter_table_cmd.def.constraint.conname
+ end
+
+ def deparse_stmt
+ PgQuery.deparse_stmt(parsed_stmt)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_validation/database.rb b/lib/gitlab/database/schema_validation/database.rb
index 7a0e348a27b..858bf618f44 100644
--- a/lib/gitlab/database/schema_validation/database.rb
+++ b/lib/gitlab/database/schema_validation/database.rb
@@ -18,6 +18,10 @@ module Gitlab
trigger_map[trigger_name]
end
+ def fetch_foreign_key_by_name(foreign_key_name)
+ foreign_key_map[foreign_key_name]
+ end
+
def fetch_table_by_name(table_name)
table_map[table_name]
end
@@ -30,6 +34,10 @@ module Gitlab
trigger_map[trigger_name].present?
end
+ def foreign_key_exists?(foreign_key_name)
+ fetch_foreign_key_by_name(foreign_key_name).present?
+ end
+
def table_exists?(table_name)
fetch_table_by_name(table_name).present?
end
@@ -42,6 +50,10 @@ module Gitlab
trigger_map.values
end
+ def foreign_keys
+ foreign_key_map.values
+ end
+
def tables
table_map.values
end
@@ -68,6 +80,14 @@ module Gitlab
end
end
+ def foreign_key_map
+ @foreign_key_map ||= fetch_fks.each_with_object({}) do |stmt, result|
+ adapter = Adapters::ForeignKeyDatabaseAdapter.new(stmt)
+
+ result[adapter.name] = SchemaObjects::ForeignKey.new(adapter)
+ end
+ end
+
def table_map
@table_map ||= fetch_tables.transform_values! do |stmt|
columns = stmt.map { |column| SchemaObjects::Column.new(Adapters::ColumnDatabaseAdapter.new(column)) }
@@ -122,6 +142,24 @@ module Gitlab
connection.exec_query(sql, nil, schemas).group_by { |row| row['table_name'] }
end
+
+ def fetch_fks
+ sql = <<~SQL
+ SELECT
+ pg_namespace.nspname::text AS schema,
+ pg_class.relname::text AS table_name,
+ pg_constraint.conname AS foreign_key_name,
+ pg_get_constraintdef(pg_constraint.oid) AS foreign_key_definition
+ FROM pg_constraint
+ INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid
+ INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
+ WHERE contype = 'f'
+ AND pg_namespace.nspname = $1
+ AND pg_constraint.conparentid = 0
+ SQL
+
+ connection.exec_query(sql, nil, [connection.current_schema])
+ end
end
end
end
diff --git a/lib/gitlab/database/schema_validation/schema_inconsistency.rb b/lib/gitlab/database/schema_validation/schema_inconsistency.rb
index 6f50603e784..9f39db5b4c0 100644
--- a/lib/gitlab/database/schema_validation/schema_inconsistency.rb
+++ b/lib/gitlab/database/schema_validation/schema_inconsistency.rb
@@ -8,7 +8,9 @@ module Gitlab
belongs_to :issue
- validates :object_name, :valitador_name, :table_name, presence: true
+ validates :object_name, :valitador_name, :table_name, :diff, presence: true
+
+ scope :with_open_issues, -> { joins(:issue).where('issue.state_id': Issue.available_states[:opened]) }
end
end
end
diff --git a/lib/gitlab/database/schema_validation/schema_objects/foreign_key.rb b/lib/gitlab/database/schema_validation/schema_objects/foreign_key.rb
new file mode 100644
index 00000000000..b616b1a72b7
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/schema_objects/foreign_key.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ module SchemaObjects
+ class ForeignKey
+ def initialize(adapter)
+ @adapter = adapter
+ end
+
+ # Foreign key name should include the schema, as the same name could be used across different schemas
+ #
+ # @example public.foreign_key_name
+ def name
+ @name ||= adapter.name
+ end
+
+ def table_name
+ @table_name ||= adapter.table_name
+ end
+
+ def statement
+ @statement ||= adapter.statement
+ end
+
+ private
+
+ attr_reader :adapter
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_validation/structure_sql.rb b/lib/gitlab/database/schema_validation/structure_sql.rb
index 299db1c2a7e..4d6fa17f0fc 100644
--- a/lib/gitlab/database/schema_validation/structure_sql.rb
+++ b/lib/gitlab/database/schema_validation/structure_sql.rb
@@ -19,6 +19,10 @@ module Gitlab
triggers.find { |trigger| trigger.name == trigger_name }.present?
end
+ def foreign_key_exists?(foreign_key_name)
+ foreign_keys.find { |fk| fk.name == foreign_key_name }.present?
+ end
+
def fetch_table_by_name(table_name)
tables.find { |table| table.name == table_name }
end
@@ -35,6 +39,14 @@ module Gitlab
@triggers ||= map_with_default_schema(trigger_statements, SchemaObjects::Trigger)
end
+ def foreign_keys
+ @foreign_keys ||= foreign_key_statements.map do |stmt|
+ stmt.relation.schemaname = schema_name if stmt.relation.schemaname == ''
+
+ SchemaObjects::ForeignKey.new(Adapters::ForeignKeyStructureSqlAdapter.new(stmt))
+ end
+ end
+
def tables
@tables ||= table_statements.map do |stmt|
table_name = stmt.relation.relname
@@ -65,6 +77,33 @@ module Gitlab
statements.filter_map { |s| s.stmt.create_stmt }
end
+ def foreign_key_statements
+ constraint_statements(:CONSTR_FOREIGN)
+ end
+
+ # Filter constraint statement nodes
+ #
+ # @param constraint_type [Symbol] node type. One of CONSTR_PRIMARY, CONSTR_CHECK, CONSTR_EXCLUSION,
+ # CONSTR_UNIQUE or CONSTR_FOREIGN.
+ def constraint_statements(constraint_type)
+ alter_table_statements(:AT_AddConstraint).filter do |stmt|
+ stmt.cmds.first.alter_table_cmd.def.constraint.contype == constraint_type
+ end
+ end
+
+ # Filter alter table statement nodes
+ #
+ # @param subtype [Symbol] node subtype +AT_AttachPartition+, +AT_ColumnDefault+ or +AT_AddConstraint+
+ def alter_table_statements(subtype)
+ statements.filter_map do |statement|
+ node = statement.stmt.alter_table_stmt
+
+ next unless node
+
+ node if node.cmds.first.alter_table_cmd.subtype == subtype
+ end
+ end
+
def statements
@statements ||= parsed_structure_file.tree.stmts
end
diff --git a/lib/gitlab/database/schema_validation/track_inconsistency.rb b/lib/gitlab/database/schema_validation/track_inconsistency.rb
index 47c3492971c..6e167653d32 100644
--- a/lib/gitlab/database/schema_validation/track_inconsistency.rb
+++ b/lib/gitlab/database/schema_validation/track_inconsistency.rb
@@ -4,6 +4,8 @@ module Gitlab
module Database
module SchemaValidation
class TrackInconsistency
+ COLUMN_TEXT_LIMIT = 6144
+
def initialize(inconsistency, project, user)
@inconsistency = inconsistency
@project = project
@@ -12,10 +14,13 @@ module Gitlab
def execute
return unless Gitlab.com?
- return if inconsistency_record.present?
+ return refresh_issue if inconsistency_record.present?
- result = ::Issues::CreateService.new(container: project, current_user: user, params: params,
- spam_params: nil).execute
+ result = ::Issues::CreateService.new(
+ container: project,
+ current_user: user,
+ params: params,
+ perform_spam_check: false).execute
track_inconsistency(result[:issue]) if result.success?
end
@@ -25,20 +30,21 @@ module Gitlab
attr_reader :inconsistency, :project, :user
def track_inconsistency(issue)
- schema_inconsistency_model.create(
+ schema_inconsistency_model.create!(
issue: issue,
object_name: inconsistency.object_name,
table_name: inconsistency.table_name,
- valitador_name: inconsistency.type
+ valitador_name: inconsistency.type,
+ diff: inconsistency_diff
)
end
def params
{
title: issue_title,
- description: issue_description,
+ description: description,
issue_type: 'issue',
- labels: %w[database database-inconsistency-report]
+ labels: default_labels + group_labels
}
end
@@ -46,7 +52,7 @@ module Gitlab
"New schema inconsistency: #{inconsistency.object_name}"
end
- def issue_description
+ def description
<<~MSG
We have detected a new schema inconsistency.
@@ -81,12 +87,46 @@ module Gitlab
MSG
end
+ def group_labels
+ dictionary = YAML.safe_load(File.read(table_file_path))
+
+ dictionary['feature_categories'].to_a.filter_map do |feature_category|
+ Gitlab::Database::ConvertFeatureCategoryToGroupLabel.new(feature_category).execute
+ end
+ rescue Errno::ENOENT
+ []
+ end
+
+ def default_labels
+ %w[database database-inconsistency-report type::maintenance severity::4]
+ end
+
+ def table_file_path
+ Rails.root.join(Gitlab::Database::GitlabSchema.dictionary_paths.first, "#{inconsistency.table_name}.yml")
+ end
+
def schema_inconsistency_model
Gitlab::Database::SchemaValidation::SchemaInconsistency
end
+ def refresh_issue
+ return if inconsistency_diff == inconsistency_record.diff # Nothing to refresh
+
+ note = ::Notes::CreateService.new(
+ inconsistency_record.issue.project,
+ user,
+ { noteable_type: 'Issue', noteable: inconsistency_record.issue, note: description }
+ ).execute
+
+ inconsistency_record.update!(diff: inconsistency_diff) if note.persisted?
+ end
+
+ def inconsistency_diff
+ @inconsistency_diff ||= inconsistency.diff.to_s.first(COLUMN_TEXT_LIMIT)
+ end
+
def inconsistency_record
- schema_inconsistency_model.find_by(
+ @inconsistency_record ||= schema_inconsistency_model.with_open_issues.find_by(
object_name: inconsistency.object_name,
table_name: inconsistency.table_name,
valitador_name: inconsistency.type
diff --git a/lib/gitlab/database/schema_validation/validators/base_validator.rb b/lib/gitlab/database/schema_validation/validators/base_validator.rb
index 58e0bf5292b..ee322e50a2c 100644
--- a/lib/gitlab/database/schema_validation/validators/base_validator.rb
+++ b/lib/gitlab/database/schema_validation/validators/base_validator.rb
@@ -18,13 +18,16 @@ module Gitlab
ExtraTableColumns,
ExtraIndexes,
ExtraTriggers,
+ ExtraForeignKeys,
MissingTables,
MissingTableColumns,
MissingIndexes,
MissingTriggers,
+ MissingForeignKeys,
DifferentDefinitionTables,
DifferentDefinitionIndexes,
- DifferentDefinitionTriggers
+ DifferentDefinitionTriggers,
+ DifferentDefinitionForeignKeys
]
end
diff --git a/lib/gitlab/database/schema_validation/validators/different_definition_foreign_keys.rb b/lib/gitlab/database/schema_validation/validators/different_definition_foreign_keys.rb
new file mode 100644
index 00000000000..8969fa76cd8
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/validators/different_definition_foreign_keys.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ module Validators
+ class DifferentDefinitionForeignKeys < BaseValidator
+ ERROR_MESSAGE = "The %s foreign key has a different statement between structure.sql and database"
+
+ def execute
+ structure_sql.foreign_keys.filter_map do |structure_sql_fk|
+ database_fk = database.fetch_foreign_key_by_name(structure_sql_fk.name)
+
+ next if database_fk.nil?
+ next if database_fk.statement == structure_sql_fk.statement
+
+ build_inconsistency(self.class, structure_sql_fk, database_fk)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_validation/validators/extra_foreign_keys.rb b/lib/gitlab/database/schema_validation/validators/extra_foreign_keys.rb
new file mode 100644
index 00000000000..887e86c7bfd
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/validators/extra_foreign_keys.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ module Validators
+ class ExtraForeignKeys < BaseValidator
+ ERROR_MESSAGE = "The foreign key %s is present in the database, but not in the structure.sql file"
+
+ def execute
+ database.foreign_keys.filter_map do |database_fk|
+ next if structure_sql.foreign_key_exists?(database_fk.name)
+
+ build_inconsistency(self.class, nil, database_fk)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_validation/validators/missing_foreign_keys.rb b/lib/gitlab/database/schema_validation/validators/missing_foreign_keys.rb
new file mode 100644
index 00000000000..b20f8474426
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/validators/missing_foreign_keys.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ module Validators
+ class MissingForeignKeys < BaseValidator
+ ERROR_MESSAGE = "The foreign key %s is missing from the database"
+
+ def execute
+ structure_sql.foreign_keys.filter_map do |structure_sql_fk|
+ next if database.foreign_key_exists?(structure_sql_fk.name)
+
+ build_inconsistency(self.class, structure_sql_fk, nil)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/tables_locker.rb b/lib/gitlab/database/tables_locker.rb
index 0b0d46f4b0e..02e0da022f9 100644
--- a/lib/gitlab/database/tables_locker.rb
+++ b/lib/gitlab/database/tables_locker.rb
@@ -5,10 +5,11 @@ module Gitlab
class TablesLocker
GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_embedding gitlab_geo].freeze
- def initialize(logger: nil, dry_run: false)
+ def initialize(logger: nil, dry_run: false, include_partitions: true)
@logger = logger
@dry_run = dry_run
@result = []
+ @include_partitions = include_partitions
end
def unlock_writes
@@ -50,6 +51,7 @@ module Gitlab
# Unlocks the writes on the table and its partitions
def unlock_writes_on_table(table_name, connection, database_name)
@result << lock_writes_manager(table_name, connection, database_name).unlock_writes
+ return unless @include_partitions
table_attached_partitions(table_name, connection) do |postgres_partition|
@result << lock_writes_manager(postgres_partition.identifier, connection, database_name).unlock_writes
@@ -59,6 +61,7 @@ module Gitlab
# It locks the writes on the table and its partitions
def lock_writes_on_table(table_name, connection, database_name)
@result << lock_writes_manager(table_name, connection, database_name).lock_writes
+ return unless @include_partitions
table_attached_partitions(table_name, connection) do |postgres_partition|
@result << lock_writes_manager(postgres_partition.identifier, connection, database_name).lock_writes
@@ -67,6 +70,7 @@ module Gitlab
def tables_to_lock(connection, &block)
Gitlab::Database::GitlabSchema.tables_to_schema.each(&block)
+ return unless @include_partitions
Gitlab::Database::SharedModel.using_connection(connection) do
Postgresql::DetachedPartition.find_each do |detached_partition|
diff --git a/lib/gitlab/database/tables_truncate.rb b/lib/gitlab/database/tables_truncate.rb
index a6430d1758b..7ae1981fa2b 100644
--- a/lib/gitlab/database/tables_truncate.rb
+++ b/lib/gitlab/database/tables_truncate.rb
@@ -3,7 +3,7 @@
module Gitlab
module Database
class TablesTruncate
- GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo].freeze
+ GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo gitlab_embedding].freeze
def initialize(database_name:, min_batch_size:, logger: nil, until_table: nil, dry_run: false)
@database_name = database_name
diff --git a/lib/gitlab/database_importers/common_metrics.rb b/lib/gitlab/database_importers/common_metrics.rb
deleted file mode 100644
index f964ae8a275..00000000000
--- a/lib/gitlab/database_importers/common_metrics.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module DatabaseImporters
- module CommonMetrics
- end
- end
-end
diff --git a/lib/gitlab/database_importers/common_metrics/importer.rb b/lib/gitlab/database_importers/common_metrics/importer.rb
deleted file mode 100644
index 6c61e05674e..00000000000
--- a/lib/gitlab/database_importers/common_metrics/importer.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module DatabaseImporters
- module CommonMetrics
- class Importer
- MissingQueryId = Class.new(StandardError)
-
- attr_reader :content
-
- def initialize(filename = 'common_metrics.yml')
- @content = YAML.load_file(Rails.root.join('config', 'prometheus', filename))
- end
-
- def execute
- CommonMetrics::PrometheusMetric.reset_column_information
-
- process_content do |id, attributes|
- find_or_build_metric!(id)
- .update!(**attributes)
- end
- end
-
- private
-
- def process_content(&blk)
- content['panel_groups'].map do |group|
- process_group(group, &blk)
- end
- end
-
- def process_group(group, &blk)
- attributes = {
- group: find_group_title_key(group['group'])
- }
-
- group['panels'].map do |panel|
- process_panel(panel, attributes, &blk)
- end
- end
-
- def process_panel(panel, attributes, &blk)
- attributes = attributes.merge(
- title: panel['title'],
- y_label: panel['y_label'])
-
- panel['metrics'].map do |metric_details|
- process_metric_details(metric_details, attributes, &blk)
- end
- end
-
- def process_metric_details(metric_details, attributes, &blk)
- attributes = attributes.merge(
- legend: metric_details['label'],
- query: metric_details['query_range'],
- unit: metric_details['unit'])
-
- yield(metric_details['id'], attributes)
- end
-
- def find_or_build_metric!(id)
- raise MissingQueryId unless id
-
- CommonMetrics::PrometheusMetric.common.find_by(identifier: id) ||
- CommonMetrics::PrometheusMetric.new(common: true, identifier: id)
- end
-
- def find_group_title_key(title)
- CommonMetrics::PrometheusMetricEnums.groups[find_group_title(title)]
- end
-
- def find_group_title(title)
- CommonMetrics::PrometheusMetricEnums.group_titles.invert[title]
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database_importers/common_metrics/prometheus_metric.rb b/lib/gitlab/database_importers/common_metrics/prometheus_metric.rb
deleted file mode 100644
index b4a392cbea9..00000000000
--- a/lib/gitlab/database_importers/common_metrics/prometheus_metric.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module DatabaseImporters
- module CommonMetrics
- class PrometheusMetric < ApplicationRecord
- enum group: PrometheusMetricEnums.groups
- scope :common, -> { where(common: true) }
- end
- end
- end
-end
diff --git a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
deleted file mode 100644
index 8a5f53be20f..00000000000
--- a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module DatabaseImporters
- module CommonMetrics
- module PrometheusMetricEnums
- def self.groups
- {
- # built-in groups
- nginx_ingress_vts: -1,
- ha_proxy: -2,
- aws_elb: -3,
- nginx: -4,
- kubernetes: -5,
- nginx_ingress: -6,
-
- # custom groups
- business: 0,
- response: 1,
- system: 2,
- custom: 3,
-
- cluster_health: -100
- }
- end
-
- def self.group_titles
- {
- business: _('Business metrics (Custom)'),
- response: _('Response metrics (Custom)'),
- system: _('System metrics (Custom)'),
- nginx_ingress_vts: _('Response metrics (NGINX Ingress VTS)'),
- nginx_ingress: _('Response metrics (NGINX Ingress)'),
- ha_proxy: _('Response metrics (HA Proxy)'),
- aws_elb: _('Response metrics (AWS ELB)'),
- nginx: _('Response metrics (NGINX)'),
- kubernetes: _('System metrics (Kubernetes)'),
- cluster_health: _('Cluster Health'),
- custom: _('Custom metrics')
- }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database_importers/default_organization_importer.rb b/lib/gitlab/database_importers/default_organization_importer.rb
new file mode 100644
index 00000000000..147c0d19b01
--- /dev/null
+++ b/lib/gitlab/database_importers/default_organization_importer.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DatabaseImporters
+ module DefaultOrganizationImporter
+ def self.create_default_organization
+ return if Organizations::Organization.default_organization
+
+ # When adding or changing attributes, consider changing the factory for Organization model as well
+ # spec/factories/organizations/organizations.rb
+ Organizations::Organization.create!(
+ id: Organizations::Organization::DEFAULT_ORGANIZATION_ID,
+ name: 'Default',
+ path: 'default'
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb
index f630c13b760..dc654964e0b 100644
--- a/lib/gitlab/dependency_linker/requirements_txt_linker.rb
+++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb
@@ -9,7 +9,7 @@ module Gitlab
def link_dependencies
link_regex(/^(?<name>(?![a-z+]+:)[^#.-][^ ><=~!;\[]+)/) do |name|
- "https://pypi.python.org/pypi/#{name}"
+ "https://pypi.org/project/#{name}/"
end
link_regex(%r{^(?<name>https?://[^ ]+)}, &:itself)
diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb
index ffd057e1d33..2c48ad3b46b 100644
--- a/lib/gitlab/devise_failure.rb
+++ b/lib/gitlab/devise_failure.rb
@@ -7,6 +7,14 @@ module Gitlab
def http_auth?
request_format && super
end
+
+ def respond
+ if warden_options[:reason] == :too_many_requests
+ self.status = 403
+ else
+ super
+ end
+ end
end
end
diff --git a/lib/gitlab/diff/formatters/base_formatter.rb b/lib/gitlab/diff/formatters/base_formatter.rb
index 19fc028594c..807ce3682ab 100644
--- a/lib/gitlab/diff/formatters/base_formatter.rb
+++ b/lib/gitlab/diff/formatters/base_formatter.rb
@@ -9,6 +9,7 @@ module Gitlab
attr_reader :base_sha
attr_reader :start_sha
attr_reader :head_sha
+ attr_reader :ignore_whitespace_change
def initialize(attrs)
if diff_file = attrs[:diff_file]
diff --git a/lib/gitlab/diff/formatters/file_formatter.rb b/lib/gitlab/diff/formatters/file_formatter.rb
new file mode 100644
index 00000000000..37b9ad85ef8
--- /dev/null
+++ b/lib/gitlab/diff/formatters/file_formatter.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ module Formatters
+ class FileFormatter < BaseFormatter
+ def initialize(attrs)
+ @ignore_whitespace_change = false
+
+ super(attrs)
+ end
+
+ def key
+ @key ||= super.push(new_path, old_path)
+ end
+
+ def position_type
+ "file"
+ end
+
+ def complete?
+ [new_path, old_path].all?(&:present?)
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ old_path == other.old_path &&
+ new_path == other.new_path
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/formatters/image_formatter.rb b/lib/gitlab/diff/formatters/image_formatter.rb
index d0c13dee1aa..f0d25885387 100644
--- a/lib/gitlab/diff/formatters/image_formatter.rb
+++ b/lib/gitlab/diff/formatters/image_formatter.rb
@@ -14,6 +14,7 @@ module Gitlab
@y = attrs[:y]
@width = attrs[:width]
@height = attrs[:height]
+ @ignore_whitespace_change = false
super(attrs)
end
diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb
index 9ea9bdfdf15..bd083720165 100644
--- a/lib/gitlab/diff/formatters/text_formatter.rb
+++ b/lib/gitlab/diff/formatters/text_formatter.rb
@@ -12,6 +12,7 @@ module Gitlab
@old_line = attrs[:old_line]
@new_line = attrs[:new_line]
@line_range = attrs[:line_range]
+ @ignore_whitespace_change = !!attrs[:ignore_whitespace_change]
super(attrs)
end
@@ -25,7 +26,8 @@ module Gitlab
end
def to_h
- super.merge(old_line: old_line, new_line: new_line, line_range: line_range)
+ super.merge(old_line: old_line, new_line: new_line, line_range: line_range,
+ ignore_whitespace_change: ignore_whitespace_change)
end
def line_age
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 40b6ae2f14e..feee4bcc7f9 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -19,7 +19,8 @@ module Gitlab
:x,
:y,
:line_range,
- :position_type, to: :formatter
+ :position_type,
+ :ignore_whitespace_change, to: :formatter
# A position can belong to a text line or to an image coordinate
# it depends of the position_type argument.
@@ -69,11 +70,11 @@ module Gitlab
end
def to_json(opts = nil)
- Gitlab::Json.generate(formatter.to_h, opts)
+ Gitlab::Json.generate(to_h.except(:ignore_whitespace_change), opts)
end
def as_json(opts = nil)
- to_h.as_json(opts)
+ to_h.except(:ignore_whitespace_change).as_json(opts)
end
def type
@@ -134,7 +135,7 @@ module Gitlab
end
def diff_options
- { paths: paths, expanded: true, include_stats: false }
+ { paths: paths, expanded: true, include_stats: false, ignore_whitespace_change: ignore_whitespace_change }
end
def diff_line(repository)
@@ -149,6 +150,10 @@ module Gitlab
@file_hash ||= Digest::SHA1.hexdigest(file_path)
end
+ def on_file?
+ position_type == 'file'
+ end
+
def on_image?
position_type == 'image'
end
@@ -184,6 +189,8 @@ module Gitlab
case type
when 'image'
Gitlab::Diff::Formatters::ImageFormatter
+ when 'file'
+ Gitlab::Diff::Formatters::FileFormatter
else
Gitlab::Diff::Formatters::TextFormatter
end
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
index 1c21c35fa60..a8c0108fa34 100644
--- a/lib/gitlab/diff/position_tracer.rb
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -21,9 +21,9 @@ module Gitlab
return unless old_diff_refs&.complete? && new_diff_refs&.complete?
return unless old_position.diff_refs == old_diff_refs
- strategy = old_position.on_text? ? LineStrategy : ImageStrategy
+ @ignore_whitespace_change = old_position.ignore_whitespace_change
- strategy.new(self).trace(old_position)
+ strategy(old_position).new(self).trace(old_position)
end
def ac_diffs
@@ -48,9 +48,19 @@ module Gitlab
private
+ def strategy(old_position)
+ if old_position.on_text?
+ LineStrategy
+ elsif old_position.on_file?
+ FileStrategy
+ else
+ ImageStrategy
+ end
+ end
+
def compare(start_sha, head_sha, straight: false)
compare = CompareService.new(project, head_sha).execute(project, start_sha, straight: straight)
- compare.diffs(paths: paths, expanded: true)
+ compare.diffs(paths: paths, expanded: true, ignore_whitespace_change: @ignore_whitespace_change)
end
end
end
diff --git a/lib/gitlab/diff/position_tracer/file_strategy.rb b/lib/gitlab/diff/position_tracer/file_strategy.rb
new file mode 100644
index 00000000000..171d78bf46f
--- /dev/null
+++ b/lib/gitlab/diff/position_tracer/file_strategy.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ class PositionTracer
+ class FileStrategy < BaseStrategy
+ def trace(position)
+ a_path = position.old_path
+ b_path = position.new_path
+
+ # If file exists in B->D (e.g. updated, renamed, removed), let the
+ # note become outdated.
+ bd_diff = bd_diffs.diff_file_with_old_path(b_path)
+
+ return { position: new_position(position, bd_diff), outdated: true } if bd_diff
+
+ # If file still exists in the new diff, update the position.
+ cd_diff = cd_diffs.diff_file_with_new_path(b_path)
+
+ return { position: new_position(position, cd_diff), outdated: false } if cd_diff
+
+ # If file exists in A->C (e.g. rebased and same changes were present
+ # in target branch), let the note become outdated.
+ ac_diff = ac_diffs.diff_file_with_old_path(a_path)
+
+ return { position: new_position(position, ac_diff), outdated: true } if ac_diff
+
+ # If ever there's a case that the file no longer exists in any diff,
+ # don't set a change position and let the note become outdated.
+ #
+ # This should never happen given the file should exist in one of the
+ # diffs above.
+ { outdated: true }
+ end
+
+ private
+
+ def new_position(position, diff_file)
+ Position.new(
+ diff_file: diff_file,
+ position_type: position.position_type
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/position_tracer/image_strategy.rb b/lib/gitlab/diff/position_tracer/image_strategy.rb
index aac52b536f7..172eba4acd3 100644
--- a/lib/gitlab/diff/position_tracer/image_strategy.rb
+++ b/lib/gitlab/diff/position_tracer/image_strategy.rb
@@ -3,36 +3,7 @@
module Gitlab
module Diff
class PositionTracer
- class ImageStrategy < BaseStrategy
- def trace(position)
- a_path = position.old_path
- b_path = position.new_path
-
- # If file exists in B->D (e.g. updated, renamed, removed), let the
- # note become outdated.
- bd_diff = bd_diffs.diff_file_with_old_path(b_path)
-
- return { position: new_position(position, bd_diff), outdated: true } if bd_diff
-
- # If file still exists in the new diff, update the position.
- cd_diff = cd_diffs.diff_file_with_new_path(b_path)
-
- return { position: new_position(position, cd_diff), outdated: false } if cd_diff
-
- # If file exists in A->C (e.g. rebased and same changes were present
- # in target branch), let the note become outdated.
- ac_diff = ac_diffs.diff_file_with_old_path(a_path)
-
- return { position: new_position(position, ac_diff), outdated: true } if ac_diff
-
- # If ever there's a case that the file no longer exists in any diff,
- # don't set a change position and let the note become outdated.
- #
- # This should never happen given the file should exist in one of the
- # diffs above.
- { outdated: true }
- end
-
+ class ImageStrategy < FileStrategy
private
def new_position(position, diff_file)
diff --git a/lib/gitlab/diff/position_tracer/line_strategy.rb b/lib/gitlab/diff/position_tracer/line_strategy.rb
index d7a7e3f5425..0de9aa22008 100644
--- a/lib/gitlab/diff/position_tracer/line_strategy.rb
+++ b/lib/gitlab/diff/position_tracer/line_strategy.rb
@@ -62,6 +62,8 @@ module Gitlab
# The line number as of D can be found by using the LineMapper on diff C->D
# and providing the line number as of C.
+ @ignore_whitespace_change = position.ignore_whitespace_change
+
if position.added?
trace_added_line(position)
elsif position.removed?
@@ -189,7 +191,8 @@ module Gitlab
diff_file: diff_file,
old_line: old_line,
new_line: new_line,
- line_range: line_range
+ line_range: line_range,
+ ignore_whitespace_change: @ignore_whitespace_change
}.compact
Position.new(**params)
diff --git a/lib/gitlab/discussions_diff/highlight_cache.rb b/lib/gitlab/discussions_diff/highlight_cache.rb
index df93e6e91b4..18ff7c28e17 100644
--- a/lib/gitlab/discussions_diff/highlight_cache.rb
+++ b/lib/gitlab/discussions_diff/highlight_cache.rb
@@ -16,7 +16,7 @@ module Gitlab
def write_multiple(mapping)
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipelined|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipelined|
mapping.each do |raw_key, value|
key = cache_key_for(raw_key)
@@ -41,8 +41,8 @@ module Gitlab
content =
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- if ::Feature.enabled?(:use_pipeline_over_multikey)
- redis.pipelined do |pipeline|
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
keys.each { |key| pipeline.get(key) }
end
else
@@ -72,10 +72,8 @@ module Gitlab
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- if ::Feature.enabled?(:use_pipeline_over_multikey)
- redis.pipelined do |pipeline|
- keys.each { |key| pipeline.del(key) }
- end.sum
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis)
else
redis.del(keys)
end
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index c325112b673..869bcc6e2be 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -68,7 +68,7 @@ module Gitlab
title: mail.subject,
description: message_including_reply_or_only_quotes
},
- spam_params: nil
+ perform_spam_check: false
).execute
end
diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb
index 076ba42daac..215ba77db13 100644
--- a/lib/gitlab/email/handler/service_desk_handler.rb
+++ b/lib/gitlab/email/handler/service_desk_handler.rb
@@ -103,7 +103,7 @@ module Gitlab
cc: mail.cc
}
},
- spam_params: nil
+ perform_spam_check: false
).execute
raise InvalidIssueError if result.error?
diff --git a/lib/gitlab/email/hook/silent_mode_interceptor.rb b/lib/gitlab/email/hook/silent_mode_interceptor.rb
index 56f94119472..774d4ac1f45 100644
--- a/lib/gitlab/email/hook/silent_mode_interceptor.rb
+++ b/lib/gitlab/email/hook/silent_mode_interceptor.rb
@@ -5,19 +5,17 @@ module Gitlab
module Hook
class SilentModeInterceptor
def self.delivering_email(message)
- if Gitlab::CurrentSettings.silent_mode_enabled?
+ if ::Gitlab::SilentMode.enabled?
message.perform_deliveries = false
- Gitlab::AppJsonLogger.info(
+ ::Gitlab::SilentMode.log_info(
message: "SilentModeInterceptor prevented sending mail",
- mail_subject: message.subject,
- silent_mode_enabled: true
+ mail_subject: message.subject
)
else
- Gitlab::AppJsonLogger.debug(
+ ::Gitlab::SilentMode.log_debug(
message: "SilentModeInterceptor did nothing",
- mail_subject: message.subject,
- silent_mode_enabled: false
+ mail_subject: message.subject
)
end
end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index c2d645138d7..e7462b711f1 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -80,9 +80,7 @@ module Gitlab
#
# 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")))
+ object.body.decoded.force_encoding(object.charset.gsub(/utf8/i, "UTF-8")).encode("UTF-8").to_s
else
object.body.to_s
end
diff --git a/lib/gitlab/error_tracking/error_repository.rb b/lib/gitlab/error_tracking/error_repository.rb
index fd2467add20..3871305c9c5 100644
--- a/lib/gitlab/error_tracking/error_repository.rb
+++ b/lib/gitlab/error_tracking/error_repository.rb
@@ -16,7 +16,7 @@ module Gitlab
# @return [self]
def self.build(project)
strategy =
- if Feature.enabled?(:use_click_house_database_for_error_tracking, project)
+ if Feature.enabled?(:gitlab_error_tracking, project)
OpenApiStrategy.new(project)
else
ActiveRecordStrategy.new(project)
diff --git a/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb b/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb
index e168fa10630..398ddebd355 100644
--- a/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb
+++ b/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb
@@ -127,21 +127,22 @@ module Gitlab
def to_sentry_error(error)
Gitlab::ErrorTracking::Error.new(
id: error.fingerprint.to_s,
- title: error.name,
+ title: "#{error.name}: #{error.description}",
message: error.description,
culprit: error.actor,
first_seen: error.first_seen_at,
last_seen: error.last_seen_at,
status: error.status,
count: error.event_count,
- user_count: error.approximated_user_count
+ user_count: error.approximated_user_count,
+ frequency: error.stats&.frequency&.dig(:'24h') || []
)
end
def to_sentry_detailed_error(error)
Gitlab::ErrorTracking::DetailedError.new(
id: error.fingerprint.to_s,
- title: error.name,
+ title: "#{error.name}: #{error.description}",
message: error.description,
culprit: error.actor,
first_seen: error.first_seen_at.to_s,
@@ -155,7 +156,8 @@ module Gitlab
external_base_url: external_base_url,
integrated: true,
first_release_version: release_from(oldest_event_for(error.fingerprint)),
- last_release_version: release_from(newest_event_for(error.fingerprint))
+ last_release_version: release_from(newest_event_for(error.fingerprint)),
+ frequency: error.stats&.frequency&.dig(:'24h') || []
)
end
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index 7aabf699a59..786a68c86f2 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -57,7 +57,7 @@ module Gitlab
end
def weak_etag_format(value)
- %Q{W/"#{value}"}
+ %{W/"#{value}"}
end
def handle_cache_hit(etag, route, request)
diff --git a/lib/gitlab/etag_caching/router/rails.rb b/lib/gitlab/etag_caching/router/rails.rb
index 2924370f494..5fd592c43e4 100644
--- a/lib/gitlab/etag_caching/router/rails.rb
+++ b/lib/gitlab/etag_caching/router/rails.rb
@@ -17,7 +17,7 @@ module Gitlab
new environments].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
- RESERVED_WORDS_PREFIX = %Q(^(?!.*\/(#{RESERVED_WORDS_REGEX})\/).*)
+ RESERVED_WORDS_PREFIX = %(^(?!.*\/(#{RESERVED_WORDS_REGEX})\/).*)
ROUTES = [
[
diff --git a/lib/gitlab/front_matter.rb b/lib/gitlab/front_matter.rb
index 2a759434b81..d215a77f4d7 100644
--- a/lib/gitlab/front_matter.rb
+++ b/lib/gitlab/front_matter.rb
@@ -37,6 +37,6 @@ module Gitlab
# rubocop:enable Style/StringConcatenation
PATTERN_UNTRUSTED_REGEX =
- Gitlab::UntrustedRegexp.new(PATTERN_UNTRUSTED, multiline: true)
+ Gitlab::UntrustedRegexp.new(PATTERN_UNTRUSTED, multiline: true).freeze
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 80d0fd17568..ed45d3eb030 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -91,12 +91,10 @@ module Gitlab
end
# Default branch in the repository
- def root_ref
- gitaly_ref_client.default_branch_name
- rescue GRPC::NotFound => e
- raise NoRepository, e.message
- rescue GRPC::Unknown => e
- raise Gitlab::Git::CommandError, e.message
+ def root_ref(head_only: false)
+ wrapped_gitaly_errors do
+ gitaly_ref_client.default_branch_name(head_only: head_only)
+ end
end
def exists?
@@ -520,13 +518,15 @@ module Gitlab
empty_diff_stats
end
- def find_changed_paths(commits)
+ def find_changed_paths(commits, merge_commit_diff_mode: nil)
processed_commits = commits.reject { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) }
return [] if processed_commits.empty?
wrapped_gitaly_errors do
- gitaly_commit_client.find_changed_paths(processed_commits)
+ gitaly_commit_client.find_changed_paths(
+ processed_commits, merge_commit_diff_mode: merge_commit_diff_mode
+ )
end
rescue CommandError, TypeError, NoRepository
[]
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index 37977a1dfb6..5b54ba472d9 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -76,8 +76,16 @@ module Gitlab
encode! @message
end
- def tagger
- @raw_tag.tagger
+ def user_name
+ encode! tagger.name if tagger
+ end
+
+ def user_email
+ encode! tagger.email if tagger
+ end
+
+ def date
+ Time.at(tagger.date.seconds).utc if tagger
end
def has_signature?
@@ -105,6 +113,10 @@ module Gitlab
private
+ def tagger
+ @raw_tag.tagger
+ end
+
def message_from_gitaly_tag
return @raw_tag.message.dup if full_message_fetched_from_gitaly?
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index e437f99dab3..df3d8165ef2 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -6,7 +6,7 @@ module Gitlab
include Gitlab::EncodingHelper
extend Gitlab::Git::WrapsGitalyErrors
- attr_accessor :id, :type, :mode, :commit_id, :submodule_url
+ attr_accessor :id, :type, :mode, :commit_id, :submodule_url, :ref_type
attr_writer :name, :path, :flat_path
class << self
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 0c67b9fa078..aa25fd3589a 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -12,6 +12,11 @@ module Gitlab
'unspecified' => Gitaly::CommitDiffRequest::WhitespaceChanges::WHITESPACE_CHANGES_UNSPECIFIED
}.freeze
+ MERGE_COMMIT_DIFF_MODES = {
+ all_parents: Gitaly::FindChangedPathsRequest::MergeCommitDiffMode::MERGE_COMMIT_DIFF_MODE_ALL_PARENTS,
+ include_merges: Gitaly::FindChangedPathsRequest::MergeCommitDiffMode::MERGE_COMMIT_DIFF_MODE_INCLUDE_MERGES
+ }.freeze
+
TREE_ENTRIES_DEFAULT_LIMIT = 100_000
def initialize(repository)
@@ -123,8 +128,10 @@ module Gitlab
end
def tree_entries(repository, revision, path, recursive, skip_flat_paths, pagination_params)
- pagination_params ||= {}
- pagination_params[:limit] ||= TREE_ENTRIES_DEFAULT_LIMIT
+ unless pagination_params.nil? && recursive
+ pagination_params ||= {}
+ pagination_params[:limit] ||= TREE_ENTRIES_DEFAULT_LIMIT
+ end
request = Gitaly::GetTreeEntriesRequest.new(
repository: @gitaly_repo,
@@ -157,6 +164,17 @@ module Gitlab
end
[entries, cursor]
+ rescue GRPC::BadStatus => e
+ detailed_error = GitalyClient.decode_detailed_error(e)
+
+ case detailed_error.try(:error)
+ when :path
+ raise Gitlab::Git::Index::IndexError, path_error_message(detailed_error.path)
+ when :resolve_tree
+ raise Gitlab::Git::Index::IndexError, e.details
+ else
+ raise e
+ end
end
def commit_count(ref, options = {})
@@ -229,11 +247,35 @@ module Gitlab
response.flat_map { |rsp| rsp.stats.to_a }
end
- def find_changed_paths(commits)
- request = Gitaly::FindChangedPathsRequest.new(
- repository: @gitaly_repo,
- commits: commits
- )
+ # When finding changed paths and passing a sha for a merge commit we can
+ # specify how to diff the commit.
+ #
+ # When diffing a merge commit and merge_commit_diff_mode is :all_parents
+ # file paths are only returned if changed in both parents (or all parents
+ # if diffing an octopus merge)
+ #
+ # This means if we create a merge request that includes a merge commit
+ # of changes already existing in the target branch, we can omit those
+ # changes when looking up the changed paths.
+ #
+ # e.g.
+ # 1. User branches from master to new branch named feature/foo_bar
+ # 2. User changes ./foo_bar.rb and commits change to feature/foo_bar
+ # 3. Another user merges a change to ./bar_baz.rb to master
+ # 4. User merges master into feature/foo_bar
+ # 5. User pushes to GitLab
+ # 6. GitLab checks which files have changed
+ #
+ # case merge_commit_diff_mode
+ # when :all_parents
+ # ['foo_bar.rb']
+ # when :include_merges
+ # ['foo_bar.rb', 'bar_baz.rb'],
+ # else # defaults to :include_merges behavior
+ # ['foo_bar.rb', 'bar_baz.rb'],
+ #
+ def find_changed_paths(commits, merge_commit_diff_mode: nil)
+ request = find_changed_paths_request(commits, merge_commit_diff_mode)
response = gitaly_client_call(@repository.storage, :diff_service, :find_changed_paths, request, timeout: GitalyClient.medium_timeout)
response.flat_map do |msg|
@@ -595,6 +637,37 @@ module Gitlab
response.commit
end
+
+ def find_changed_paths_request(commits, merge_commit_diff_mode)
+ diff_mode = MERGE_COMMIT_DIFF_MODES[merge_commit_diff_mode] if Feature.enabled?(:merge_commit_diff_modes)
+
+ if Feature.disabled?(:find_changed_paths_new_format)
+ return Gitaly::FindChangedPathsRequest.new(repository: @gitaly_repo, commits: commits, merge_commit_diff_mode: diff_mode)
+ end
+
+ commit_requests = commits.map do |commit|
+ Gitaly::FindChangedPathsRequest::Request.new(
+ commit_request: Gitaly::FindChangedPathsRequest::Request::CommitRequest.new(commit_revision: commit)
+ )
+ end
+
+ Gitaly::FindChangedPathsRequest.new(repository: @gitaly_repo, requests: commit_requests, merge_commit_diff_mode: diff_mode)
+ end
+
+ def path_error_message(path_error)
+ case path_error.error_type
+ when :ERROR_TYPE_EMPTY_PATH
+ "You must provide a file path"
+ when :ERROR_TYPE_RELATIVE_PATH_ESCAPES_REPOSITORY
+ "Path cannot include traversal syntax"
+ when :ERROR_TYPE_ABSOLUTE_PATH
+ "Only relative path is accepted"
+ when :ERROR_TYPE_LONG_PATH
+ "Path is too long"
+ else
+ "Unknown path error"
+ end
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 1af06cc7490..bd6cc9105d9 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -8,6 +8,8 @@ module Gitlab
MAX_MSG_SIZE = 128.kilobytes.freeze
+ CUSTOM_HOOK_FALLBACK_MESSAGE = 'Prevented by server hooks'
+
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@repository = repository
@@ -52,7 +54,7 @@ module Gitlab
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)
+ fallback_message: CUSTOM_HOOK_FALLBACK_MESSAGE)
when :reference_exists
raise Gitlab::Git::Repository::TagExistsError
else
@@ -85,7 +87,7 @@ module Gitlab
case detailed_error.try(:error)
when :custom_hook
raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
- fallback_message: e.details)
+ fallback_message: CUSTOM_HOOK_FALLBACK_MESSAGE)
else
if e.code == GRPC::Core::StatusCodes::FAILED_PRECONDITION
raise Gitlab::Git::Repository::InvalidRef, e
@@ -127,7 +129,7 @@ module Gitlab
case detailed_error.try(:error)
when :custom_hook
raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
- fallback_message: e.details)
+ fallback_message: CUSTOM_HOOK_FALLBACK_MESSAGE)
else
raise
end
@@ -195,7 +197,7 @@ module Gitlab
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)
+ fallback_message: CUSTOM_HOOK_FALLBACK_MESSAGE)
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
@@ -465,7 +467,7 @@ module Gitlab
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)
+ fallback_message: CUSTOM_HOOK_FALLBACK_MESSAGE)
when :index_update
raise Gitlab::Git::Index::IndexError, index_error_message(detailed_error.index_update)
else
@@ -583,8 +585,7 @@ module Gitlab
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.
+ # that prefix is present we'll want to print the hook's output.
custom_hook_output = custom_hook_error.stderr.presence || custom_hook_error.stdout
EncodingHelper.encode!(custom_hook_output)
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 88c79eb8954..45edfd4cbbf 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -50,8 +50,8 @@ module Gitlab
consume_find_all_branches_response(response)
end
- def default_branch_name
- request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo)
+ def default_branch_name(head_only: false)
+ request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo, head_only: head_only)
response = gitaly_client_call(@storage, :ref_service, :find_default_branch_name, request, timeout: GitalyClient.fast_timeout)
Gitlab::Git.branch_name(response.name)
end
diff --git a/lib/gitlab/github_gists_import/importer/gist_importer.rb b/lib/gitlab/github_gists_import/importer/gist_importer.rb
index 4018f425e7c..71dfe5e2aa5 100644
--- a/lib/gitlab/github_gists_import/importer/gist_importer.rb
+++ b/lib/gitlab/github_gists_import/importer/gist_importer.rb
@@ -4,10 +4,13 @@ module Gitlab
module GithubGistsImport
module Importer
class GistImporter
- attr_reader :gist, :user
+ attr_reader :gist, :user, :snippet
FileCountLimitError = Class.new(StandardError)
+ RepoSizeLimitError = Class.new(StandardError)
+ SnippetRepositoryError = Class.new(StandardError)
FILE_COUNT_LIMIT_MESSAGE = 'Snippet maximum file count exceeded'
+ REPO_SIZE_LIMIT_MESSAGE = 'Snippet repository size exceeded'
# gist - An instance of `Gitlab::GithubGistsImport::Representation::Gist`.
def initialize(gist, user_id)
@@ -16,12 +19,15 @@ module Gitlab
end
def execute
- snippet = build_snippet
- import_repository(snippet) if snippet.save!
+ validate_gist!
- return ServiceResponse.success unless max_snippet_files_count_exceeded?(snippet)
+ @snippet = build_snippet
+ import_repository if snippet.save!
+ validate_repository!
- fail_and_track(snippet)
+ ServiceResponse.success
+ rescue FileCountLimitError, RepoSizeLimitError, SnippetRepositoryError => exception
+ fail_and_track(snippet, exception)
end
private
@@ -40,13 +46,13 @@ module Gitlab
PersonalSnippet.new(attrs)
end
- def import_repository(snippet)
+ def import_repository
resolved_address = get_resolved_address
snippet.create_repository
snippet.repository.fetch_as_mirror(gist.git_pull_url, forced: true, resolved_address: resolved_address)
rescue StandardError
- remove_snippet_and_repository(snippet)
+ remove_snippet_and_repository
raise
end
@@ -61,11 +67,19 @@ module Gitlab
host.present? ? validated_pull_url.host.to_s : ''
end
- def max_snippet_files_count_exceeded?(snippet)
- snippet.all_files.size > Snippet.max_file_limit
+ def check_gist_files_count!
+ return if gist.files.count <= Snippet.max_file_limit
+
+ raise FileCountLimitError, FILE_COUNT_LIMIT_MESSAGE
end
- def remove_snippet_and_repository(snippet)
+ def check_gist_repo_size!
+ return if gist.total_files_size <= Gitlab::CurrentSettings.snippet_size_limit
+
+ raise RepoSizeLimitError, REPO_SIZE_LIMIT_MESSAGE
+ end
+
+ def remove_snippet_and_repository
snippet.repository.remove if snippet.repository_exists?
snippet.destroy
end
@@ -74,10 +88,21 @@ module Gitlab
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
- def fail_and_track(snippet)
- remove_snippet_and_repository(snippet)
+ def fail_and_track(snippet, exception)
+ remove_snippet_and_repository if snippet
+
+ ServiceResponse.error(message: exception.message).track_exception(as: exception.class)
+ end
+
+ def validate_gist!
+ check_gist_files_count!
+ check_gist_repo_size!
+ end
+
+ def validate_repository!
+ result = Snippets::RepositoryValidationService.new(user, snippet).execute
- ServiceResponse.error(message: FILE_COUNT_LIMIT_MESSAGE).track_exception(as: FileCountLimitError)
+ raise SnippetRepositoryError, result.message if result.error?
end
end
end
diff --git a/lib/gitlab/github_gists_import/representation/gist.rb b/lib/gitlab/github_gists_import/representation/gist.rb
index 0d309a98f38..674da4f3400 100644
--- a/lib/gitlab/github_gists_import/representation/gist.rb
+++ b/lib/gitlab/github_gists_import/representation/gist.rb
@@ -65,6 +65,10 @@ module Gitlab
def github_identifiers
{ id: id }
end
+
+ def total_files_size
+ files.values.sum { |f| f[:size].to_i }
+ end
end
end
end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index 2654812b64a..d37942aa8a3 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -54,6 +54,8 @@ module Gitlab
project.change_head(default_branch) if default_branch
+ validate_repository_size!
+
# The initial fetch can bring in lots of loose refs and objects.
# Running a `git gc` will make importing pull requests faster.
Repositories::HousekeepingService.new(project, :gc).execute
@@ -89,7 +91,13 @@ module Gitlab
strong_memoize_attr def client_repository
client.repository(project.import_source)
end
+
+ def validate_repository_size!
+ # Defined in EE
+ end
end
end
end
end
+
+Gitlab::GithubImport::Importer::RepositoryImporter.prepend_mod
diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb
index 0408b34bb02..191e15962a6 100644
--- a/lib/gitlab/github_import/representation/diff_note.rb
+++ b/lib/gitlab/github_import/representation/diff_note.rb
@@ -12,7 +12,7 @@ module Gitlab
expose_attribute :noteable_id, :commit_id, :file_path,
:diff_hunk, :author, :created_at, :updated_at,
:original_commit_id, :note_id, :end_line, :start_line,
- :side, :in_reply_to_id, :discussion_id
+ :side, :in_reply_to_id, :discussion_id, :subject_type
# Builds a diff note from a GitHub API response.
#
@@ -43,7 +43,8 @@ module Gitlab
start_line: note[:start_line],
side: note[:side],
in_reply_to_id: note[:in_reply_to_id],
- discussion_id: DiffNotes::DiscussionId.new(note).find_or_generate
+ discussion_id: DiffNotes::DiscussionId.new(note).find_or_generate,
+ subject_type: note[:subject_type]
}
new(hash)
@@ -84,8 +85,14 @@ module Gitlab
end
def line_code
- diff_line = Gitlab::Diff::Parser.new.parse(diff_hunk.lines).to_a.last
+ # on the GitHub side it is possible to leave a comment on a file
+ # or on a line. When the comment is left on a file there is no
+ # diff hunk, but LegacyDiffNote requires line_code to be always present
+ # and DiffFile requires it for text files
+ # so it is set as the first line for any type of file (image, binary, text)
+ return Gitlab::Git.diff_line_code(file_path, 1, 1) if on_file?
+ diff_line = Gitlab::Diff::Parser.new.parse(diff_hunk.lines).to_a.last
Gitlab::Git.diff_line_code(file_path, diff_line.new_pos, diff_line.old_pos)
end
@@ -141,6 +148,10 @@ module Gitlab
def addition?
side == 'RIGHT'
end
+
+ def on_file?
+ subject_type == 'file'
+ end
end
end
end
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index 268d5d3e564..9161a1a138f 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -34,7 +34,7 @@ module Gitlab
DESIGN = ::Gitlab::GlRepository::RepoType.new(
name: :design,
access_checker_class: ::Gitlab::GitAccessDesign,
- repository_resolver: -> (project) { project.design_management_repository.repository },
+ repository_resolver: -> (project) { project.find_or_create_design_management_repository.repository },
suffix: :design,
container_class: DesignManagement::Repository
).freeze
diff --git a/lib/gitlab/gl_repository/identifier.rb b/lib/gitlab/gl_repository/identifier.rb
index f521a14ea19..787e80fb763 100644
--- a/lib/gitlab/gl_repository/identifier.rb
+++ b/lib/gitlab/gl_repository/identifier.rb
@@ -23,7 +23,7 @@ module Gitlab
return identifier if identifier&.valid?
- raise InvalidIdentifier, %Q(Invalid GL Repository "#{gl_repository}")
+ raise InvalidIdentifier, %(Invalid GL Repository "#{gl_repository}")
end
# The older 2-segment format, where the container is implied.
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 904a2ccc79b..9eeea7336b5 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -50,8 +50,12 @@ module Gitlab
gon.jh = Gitlab.jh?
gon.dot_com = Gitlab.com?
gon.uf_error_prefix = ::Gitlab::Utils::ErrorMessage::UF_ERROR_PREFIX
+ gon.pat_prefix = Gitlab::CurrentSettings.current_application_settings.personal_access_token_prefix
+
+ gon.diagramsnet_url = Gitlab::CurrentSettings.diagramsnet_url if Gitlab::CurrentSettings.diagramsnet_enabled
if current_user
+ gon.version = Gitlab::VERSION # publish version only for logged in users
gon.current_user_id = current_user.id
gon.current_username = current_user.username
gon.current_user_fullname = current_user.name
@@ -66,10 +70,11 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:vscode_web_ide, current_user)
- push_frontend_feature_flag(:super_sidebar_peek, current_user)
push_frontend_feature_flag(:unbatch_graphql_queries, current_user)
+ push_frontend_feature_flag(:command_palette, current_user)
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
+ push_frontend_feature_flag(:gitlab_duo, current_user)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/graphql/generic_tracing.rb b/lib/gitlab/graphql/generic_tracing.rb
index d3de9c714f4..dc3f6574631 100644
--- a/lib/gitlab/graphql/generic_tracing.rb
+++ b/lib/gitlab/graphql/generic_tracing.rb
@@ -39,11 +39,15 @@ module Gitlab
ensure
duration = Gitlab::Metrics::System.monotonic_time - start
- graphql_duration_seconds.observe(tags, duration)
+ graphql_duration_seconds.observe(tags, duration) unless deactivated?
end
private
+ def deactivated?
+ Feature.enabled?(:graphql_generic_tracing_metrics_deactivate)
+ end
+
def with_labkit_tracing(tags, &block)
return yield unless Labkit::Tracing.enabled?
diff --git a/lib/gitlab/hotlinking_detector.rb b/lib/gitlab/hotlinking_detector.rb
index b5000777010..e81983cd014 100644
--- a/lib/gitlab/hotlinking_detector.rb
+++ b/lib/gitlab/hotlinking_detector.rb
@@ -25,6 +25,11 @@ module Gitlab
return true if INVALID_FORMATS.include?(request_accepts.first)
false
+
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType, Mime::Type::InvalidMimeType
+ # Malformed requests with invalid MIME types prevent the checks from
+ # being executed correctly, so we should intercept those requests.
+ true
end
private
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index c6cd5fbfced..8b19611e5c0 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -10,6 +10,7 @@ module Gitlab
RedirectionTooDeep = Class.new(StandardError)
ReadTotalTimeout = Class.new(Net::ReadTimeout)
HeaderReadTimeout = Class.new(Net::ReadTimeout)
+ SilentModeBlockedError = Class.new(StandardError)
HTTP_TIMEOUT_ERRORS = [
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout
@@ -28,6 +29,13 @@ module Gitlab
}.freeze
DEFAULT_READ_TOTAL_TIMEOUT = 30.seconds
+ SILENT_MODE_ALLOWED_METHODS = [
+ Net::HTTP::Get,
+ Net::HTTP::Head,
+ Net::HTTP::Options,
+ Net::HTTP::Trace
+ ].freeze
+
include HTTParty # rubocop:disable Gitlab/HTTParty
class << self
@@ -37,6 +45,8 @@ module Gitlab
connection_adapter HTTPConnectionAdapter
def self.perform_request(http_method, path, options, &block)
+ raise_if_blocked_by_silent_mode(http_method)
+
log_info = options.delete(:extra_log_info)
options_with_timeouts =
if !options.has_key?(:timeout)
@@ -76,5 +86,20 @@ module Gitlab
rescue *HTTP_ERRORS
nil
end
+
+ def self.raise_if_blocked_by_silent_mode(http_method)
+ return unless blocked_by_silent_mode?(http_method)
+
+ ::Gitlab::SilentMode.log_info(
+ message: 'Outbound HTTP request blocked',
+ outbound_http_request_method: http_method.to_s
+ )
+
+ raise SilentModeBlockedError, 'only get, head, options, and trace methods are allowed in silent mode'
+ end
+
+ def self.blocked_by_silent_mode?(http_method)
+ ::Gitlab::SilentMode.enabled? && SILENT_MODE_ALLOWED_METHODS.exclude?(http_method)
+ end
end
end
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index fa85d839927..180ccf21264 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -45,27 +45,27 @@ module Gitlab
'bg' => 0,
'cs_CZ' => 0,
'da_DK' => 31,
- 'de' => 15,
+ 'de' => 97,
'en' => 100,
'eo' => 0,
- 'es' => 31,
+ 'es' => 30,
'fil_PH' => 0,
'fr' => 98,
'gl_ES' => 0,
'id_ID' => 0,
'it' => 1,
- 'ja' => 77,
+ 'ja' => 98,
'ko' => 18,
- 'nb_NO' => 23,
+ 'nb_NO' => 22,
'nl_NL' => 0,
'pl_PL' => 3,
'pt_BR' => 56,
- 'ro_RO' => 84,
- 'ru' => 24,
+ 'ro_RO' => 82,
+ 'ru' => 23,
'si_LK' => 10,
'tr_TR' => 9,
- 'uk' => 54,
- 'zh_CN' => 99,
+ 'uk' => 53,
+ 'zh_CN' => 98,
'zh_HK' => 1,
'zh_TW' => 99
}.freeze
diff --git a/lib/gitlab/import_export/decompressed_archive_size_validator.rb b/lib/gitlab/import_export/decompressed_archive_size_validator.rb
index 564008e7a73..104c9e6c456 100644
--- a/lib/gitlab/import_export/decompressed_archive_size_validator.rb
+++ b/lib/gitlab/import_export/decompressed_archive_size_validator.rb
@@ -85,7 +85,7 @@ module Gitlab
end
def validate_archive_path
- Gitlab::Utils.check_path_traversal!(@archive_path)
+ Gitlab::PathTraversal.check_path_traversal!(@archive_path)
raise(ServiceError, 'Archive path is a symlink') if File.lstat(@archive_path).symlink?
raise(ServiceError, 'Archive path is not a file') unless File.file?(@archive_path)
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
index e30414265be..c2a1a1f8575 100644
--- a/lib/gitlab/import_export/group/import_export.yml
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -56,7 +56,6 @@ excluded_attributes:
- :runners_token
- :runners_token_encrypted
- :saml_discovery_token
- - :visibility_level
- :trial_ends_on
- :shared_runners_minute_limit
- :extra_shared_runners_minutes_limit
diff --git a/lib/gitlab/import_export/group/tree_restorer.rb b/lib/gitlab/import_export/group/tree_restorer.rb
index 19d707aaca5..0b92942eb8a 100644
--- a/lib/gitlab/import_export/group/tree_restorer.rb
+++ b/lib/gitlab/import_export/group/tree_restorer.rb
@@ -65,6 +65,15 @@ module Gitlab
# with existing groups name and/or path.
group_attributes.delete_attributes('name', 'path')
+ if @top_level_group.has_parent?
+ group_attributes.attributes['visibility_level'] = sub_group_visibility_level(
+ group_attributes.attributes['visibility_level'],
+ @top_level_group.parent
+ )
+ elsif Gitlab::VisibilityLevel.restricted_level?(group_attributes.attributes['visibility_level'])
+ group_attributes.delete_attribute('visibility_level')
+ end
+
restore_group(@top_level_group, group_attributes)
end
@@ -86,6 +95,7 @@ module Gitlab
parent_id = group_attributes.delete_attribute('parent_id')
name = group_attributes.delete_attribute('name')
path = group_attributes.delete_attribute('path')
+ visibility_level = group_attributes.delete_attribute('visibility_level')
parent_group = @groups_mapping.fetch(parent_id) { raise(ArgumentError, 'Parent group not found') }
@@ -94,7 +104,7 @@ module Gitlab
name: name,
path: path,
parent_id: parent_group.id,
- visibility_level: sub_group_visibility_level(group_attributes.attributes, parent_group)
+ visibility_level: sub_group_visibility_level(visibility_level, parent_group)
).execute
group.validate!
@@ -124,16 +134,23 @@ module Gitlab
end
end
- def sub_group_visibility_level(group_hash, parent_group)
- original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE
+ def sub_group_visibility_level(visibility_level, parent_group)
+ parent_visibility_level = parent_group.visibility_level
- if parent_group && parent_group.visibility_level < original_visibility_level
- Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
+ original_visibility_level = visibility_level ||
+ closest_allowed_level(parent_visibility_level)
+
+ if parent_visibility_level < original_visibility_level
+ closest_allowed_level(parent_visibility_level)
else
- original_visibility_level
+ closest_allowed_level(original_visibility_level)
end
end
+ def closest_allowed_level(visibility_level)
+ Gitlab::VisibilityLevel.closest_allowed_level(visibility_level)
+ end
+
def reader
strong_memoize(:reader) do
Gitlab::ImportExport::Reader.new(
diff --git a/lib/gitlab/import_export/legacy_relation_tree_saver.rb b/lib/gitlab/import_export/legacy_relation_tree_saver.rb
deleted file mode 100644
index cf75a2c7fa8..00000000000
--- a/lib/gitlab/import_export/legacy_relation_tree_saver.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ImportExport
- class LegacyRelationTreeSaver
- include Gitlab::ImportExport::CommandLineUtil
-
- def serialize(exportable, relations_tree)
- Gitlab::ImportExport::FastHashSerializer
- .new(exportable, relations_tree)
- .execute
- end
-
- def save(tree, dir_path, filename)
- mkdir_p(dir_path)
-
- tree_json = ::JSON.generate(tree)
-
- File.write(File.join(dir_path, filename), tree_json)
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/project/exported_relations_merger.rb b/lib/gitlab/import_export/project/exported_relations_merger.rb
index dda3d00d608..b5eba768e56 100644
--- a/lib/gitlab/import_export/project/exported_relations_merger.rb
+++ b/lib/gitlab/import_export/project/exported_relations_merger.rb
@@ -20,8 +20,8 @@ module Gitlab
tar_gz_full_path = File.join(dirpath, filename)
decompress_path = File.join(dirpath, relation)
- Gitlab::Utils.check_path_traversal!(tar_gz_full_path)
- Gitlab::Utils.check_path_traversal!(decompress_path)
+ Gitlab::PathTraversal.check_path_traversal!(tar_gz_full_path)
+ Gitlab::PathTraversal.check_path_traversal!(decompress_path)
# Download tar.gz
download_or_copy_upload(
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 36a3c73271b..410e918649b 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -121,7 +121,6 @@ tree:
- label:
- :priorities
- :service_desk_setting
- - :design_management_repository
group_members:
- :user
@@ -322,6 +321,7 @@ included_attributes:
- :feature_flags_access_level
- :releases_access_level
- :infrastructure_access_level
+ - :model_experiments_access_level
prometheus_metrics:
- :created_at
- :updated_at
@@ -742,6 +742,7 @@ included_attributes:
- :feature_flags_access_level
- :releases_access_level
- :infrastructure_access_level
+ - :model_experiments_access_level
- :auto_devops_deploy_strategy
- :auto_devops_enabled
- :container_registry_enabled
diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb
index 5d7e3ea9ed7..8c673acdd1a 100644
--- a/lib/gitlab/import_export/project/relation_factory.rb
+++ b/lib/gitlab/import_export/project/relation_factory.rb
@@ -23,6 +23,7 @@ module Gitlab
design: 'DesignManagement::Design',
designs: 'DesignManagement::Design',
design_management_repository: 'DesignManagement::Repository',
+ design_management_repository_state: 'Geo::DesignManagementRepositoryState',
design_versions: 'DesignManagement::Version',
actions: 'DesignManagement::Action',
labels: :project_labels,
diff --git a/lib/gitlab/import_export/recursive_merge_folders.rb b/lib/gitlab/import_export/recursive_merge_folders.rb
index 982358699bd..827385d4daf 100644
--- a/lib/gitlab/import_export/recursive_merge_folders.rb
+++ b/lib/gitlab/import_export/recursive_merge_folders.rb
@@ -45,9 +45,9 @@ module Gitlab
DEFAULT_DIR_MODE = 0o700
def self.merge(source_path, target_path)
- Gitlab::Utils.check_path_traversal!(source_path)
- Gitlab::Utils.check_path_traversal!(target_path)
- Gitlab::Utils.check_allowed_absolute_path!(source_path, [Dir.tmpdir])
+ Gitlab::PathTraversal.check_path_traversal!(source_path)
+ Gitlab::PathTraversal.check_path_traversal!(target_path)
+ Gitlab::PathTraversal.check_allowed_absolute_path!(source_path, [Dir.tmpdir])
recursive_merge(source_path, target_path)
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index cc214d730fe..d7d262501de 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -42,7 +42,7 @@ module Gitlab
def ensure_repository_does_not_exist!
if repository.exists?
shared.logger.info(
- message: %Q{Deleting existing "#{repository.disk_path}" to re-import it.}
+ message: %{Deleting existing "#{repository.disk_path}" to re-import it.}
)
Repositories::DestroyService.new(repository).execute
diff --git a/lib/gitlab/instrumentation/redis_base.rb b/lib/gitlab/instrumentation/redis_base.rb
index 00a7387afe2..70aaa59f912 100644
--- a/lib/gitlab/instrumentation/redis_base.rb
+++ b/lib/gitlab/instrumentation/redis_base.rb
@@ -94,9 +94,9 @@ module Gitlab
raise RedisClusterValidator::CrossSlotError, "Redis command #{result[:command_name]} arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands"
end
- increment_allowed_cross_slot_request_count if result[:allowed]
+ increment_allowed_cross_slot_request_count if result[:allowed] && !result[:valid]
- result[:valid]
+ result[:valid] || result[:allowed]
end
def enable_redis_cluster_validation
diff --git a/lib/gitlab/instrumentation/redis_cluster_validator.rb b/lib/gitlab/instrumentation/redis_cluster_validator.rb
index 1567e54d8da..948132e6edd 100644
--- a/lib/gitlab/instrumentation/redis_cluster_validator.rb
+++ b/lib/gitlab/instrumentation/redis_cluster_validator.rb
@@ -193,8 +193,7 @@ module Gitlab
keys = commands.map { |command| extract_keys(command) }.flatten
{
- # calculate key-slots only if not allowed
- valid: allow_cross_slot_commands? || !has_cross_slot_keys?(keys),
+ valid: !has_cross_slot_keys?(keys),
command_name: command_name,
key_count: keys.size,
allowed: allow_cross_slot_commands?
@@ -211,6 +210,10 @@ module Gitlab
Thread.current[:allow_cross_slot_commands] -= 1
end
+ def allow_cross_slot_commands?
+ Thread.current[:allow_cross_slot_commands].to_i > 0
+ end
+
private
def extract_keys(command)
@@ -226,10 +229,6 @@ module Gitlab
keys.map { |key| key_slot(key) }.uniq.many? # rubocop: disable CodeReuse/ActiveRecord
end
- def allow_cross_slot_commands?
- Thread.current[:allow_cross_slot_commands].to_i > 0
- end
-
def key_slot(key)
::Redis::Cluster::KeySlotConverter.convert(extract_hash_tag(key))
end
diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb
new file mode 100644
index 00000000000..cde83068de1
--- /dev/null
+++ b/lib/gitlab/internal_events.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module InternalEvents
+ class << self
+ include Gitlab::Tracking::Helpers
+
+ def track_event(event_name, **kwargs)
+ user_id = kwargs.delete(:user_id)
+ UsageDataCounters::HLLRedisCounter.track_event(event_name, values: user_id)
+
+ project_id = kwargs.delete(:project_id)
+ namespace_id = kwargs.delete(:namespace_id)
+
+ namespace = Namespace.find(namespace_id) if namespace_id
+
+ standard_context = Tracking::StandardContext.new(
+ project_id: project_id,
+ user_id: user_id,
+ namespace_id: namespace&.id,
+ plan_name: namespace&.actual_plan_name
+ ).to_context
+
+ service_ping_context = Tracking::ServicePingContext.new(
+ data_source: :redis_hll,
+ event: event_name
+ ).to_context
+
+ track_struct_event(event_name, contexts: [standard_context, service_ping_context])
+ end
+
+ private
+
+ def track_struct_event(event_name, contexts:)
+ category = 'InternalEventTracking'
+ tracker = Gitlab::Tracking.tracker
+ tracker.event(category, event_name, context: contexts)
+ rescue StandardError => error
+ Gitlab::ErrorTracking
+ .track_and_raise_for_dev_exception(error, snowplow_category: category, snowplow_action: event_name)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb
deleted file mode 100644
index 0087c2accc3..00000000000
--- a/lib/gitlab/json_cache.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- class JsonCache
- attr_reader :backend, :namespace
-
- STRATEGY_KEY_COMPONENTS = {
- revision: Gitlab.revision,
- version: [Gitlab::VERSION, Rails.version]
- }.freeze
-
- def initialize(options = {})
- @backend = options.fetch(:backend, Rails.cache)
- @namespace = options.fetch(:namespace, nil)
- @cache_key_strategy = options.fetch(:cache_key_strategy, :revision)
- end
-
- def active?
- if backend.respond_to?(:active?)
- backend.active?
- else
- true
- end
- end
-
- def cache_key(key)
- expanded_cache_key = [namespace, key, *strategy_key_component].compact
- expanded_cache_key.join(':').freeze
- end
-
- def strategy_key_component
- STRATEGY_KEY_COMPONENTS.fetch(@cache_key_strategy)
- end
-
- def expire(key)
- backend.delete(cache_key(key))
- end
-
- def read(key, klass = nil)
- value = backend.read(cache_key(key))
- value = parse_value(value, klass) unless value.nil?
- value
- end
-
- def write(key, value, options = nil)
- backend.write(cache_key(key), value.to_json, options)
- end
-
- def fetch(key, options = {}, &block)
- klass = options.delete(:as)
- value = read(key, klass)
-
- return value unless value.nil?
-
- value = yield
-
- write(key, value, options)
-
- value
- end
-
- private
-
- def parse_value(raw, klass)
- value = Gitlab::Json.parse(raw.to_s)
-
- case value
- when Hash then parse_entry(value, klass)
- when Array then parse_entries(value, klass)
- else
- value
- end
- rescue JSON::ParserError
- nil
- end
-
- def parse_entry(raw, klass)
- return unless valid_entry?(raw, klass)
- return klass.new(raw) unless klass.ancestors.include?(ActiveRecord::Base)
-
- # When the cached value is a persisted instance of ActiveRecord::Base in
- # some cases a relation can return an empty collection becauses scope.none!
- # is being applied on ActiveRecord::Associations::CollectionAssociation#scope
- # when the new_record? method incorrectly returns false.
- #
- # See https://gitlab.com/gitlab-org/gitlab/issues/9903#note_145329964
- klass.allocate.init_with(encode_for(klass, raw))
- end
-
- def encode_for(klass, raw)
- # We have models that leave out some fields from the JSON export for
- # security reasons, e.g. models that include the CacheMarkdownField.
- # The ActiveRecord::AttributeSet we build from raw does know about
- # these columns so we need manually set them.
- missing_attributes = (klass.columns.map(&:name) - raw.keys)
- missing_attributes.each { |column| raw[column] = nil }
-
- coder = {}
- klass.new(raw).encode_with(coder)
- coder["new_record"] = new_record?(raw, klass)
- coder
- end
-
- def new_record?(raw, klass)
- raw.fetch(klass.primary_key, nil).blank?
- end
-
- def valid_entry?(raw, klass)
- return false unless klass && raw.is_a?(Hash)
-
- (raw.keys - klass.attribute_names).empty?
- end
-
- def parse_entries(values, klass)
- values.filter_map { |value| parse_entry(value, klass) }
- end
- end
-end
diff --git a/lib/gitlab/markdown_cache/redis/store.rb b/lib/gitlab/markdown_cache/redis/store.rb
index 8cab069e1bf..f742cb82b8d 100644
--- a/lib/gitlab/markdown_cache/redis/store.rb
+++ b/lib/gitlab/markdown_cache/redis/store.rb
@@ -11,7 +11,7 @@ module Gitlab
Gitlab::Redis::Cache.with do |r|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- r.pipelined do |pipeline|
+ Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
subjects.each do |subject|
results[subject.cache_key] = new(subject).read(pipeline)
end
diff --git a/lib/gitlab/merge_requests/message_generator.rb b/lib/gitlab/merge_requests/message_generator.rb
index 5113fbdcd7b..523e0e665dc 100644
--- a/lib/gitlab/merge_requests/message_generator.rb
+++ b/lib/gitlab/merge_requests/message_generator.rb
@@ -51,6 +51,7 @@ module Gitlab
end,
'description' => ->(merge_request, _, _) { merge_request.description },
'reference' => ->(merge_request, _, _) { merge_request.to_reference(full: true) },
+ 'local_reference' => ->(merge_request, _, _) { merge_request.to_reference(full: false) },
'first_commit' => -> (merge_request, _, _) {
return unless merge_request.persisted? || merge_request.compare_commits.present?
diff --git a/lib/gitlab/metrics/loose_foreign_keys_slis.rb b/lib/gitlab/metrics/loose_foreign_keys_slis.rb
index 5d8245aa609..c0db709fe13 100644
--- a/lib/gitlab/metrics/loose_foreign_keys_slis.rb
+++ b/lib/gitlab/metrics/loose_foreign_keys_slis.rb
@@ -26,7 +26,7 @@ module Gitlab
private
def possible_labels
- ::Gitlab::Database.db_config_names.map do |db_config_name|
+ ::Gitlab::Database.db_config_names(with_schema: :gitlab_shared).map do |db_config_name|
{
db_config_name: db_config_name,
feature_category: :database
diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb
index b4e9e85a012..e2cdd6c5358 100644
--- a/lib/gitlab/metrics/subscribers/rails_cache.rb
+++ b/lib/gitlab/metrics/subscribers/rails_cache.rb
@@ -13,7 +13,7 @@ module Gitlab
return unless current_transaction
- labels = { store: event.payload[:store].split('::').last }
+ labels = { store: extract_store_name(event) }
current_transaction.observe(:gitlab_cache_read_multikey_count, event.payload[:key].size, labels) do
buckets [10, 50, 100, 1000]
docstring 'Number of keys for mget in read_multi/fetch_multi'
@@ -26,11 +26,7 @@ module Gitlab
return unless current_transaction
return if event.payload[:super_operation] == :fetch
- unless event.payload[:hit]
- current_transaction.increment(:gitlab_cache_misses_total, 1) do
- docstring 'Cache read miss'
- end
- end
+ track_cache_miss(event) unless event.payload[:hit]
end
def cache_write(event)
@@ -48,23 +44,23 @@ module Gitlab
def cache_fetch_hit(event)
return unless current_transaction
- current_transaction.increment(:gitlab_transaction_cache_read_hit_count_total, 1)
+ labels = { store: extract_store_name(event) }
+ current_transaction.increment(:gitlab_transaction_cache_read_hit_count_total, 1, labels)
end
def cache_generate(event)
return unless current_transaction
- current_transaction.increment(:gitlab_cache_misses_total, 1) do
- docstring 'Cache read miss'
- end
+ track_cache_miss(event)
- current_transaction.increment(:gitlab_transaction_cache_read_miss_count_total, 1)
+ labels = { store: extract_store_name(event) }
+ current_transaction.increment(:gitlab_transaction_cache_read_miss_count_total, 1, labels)
end
def observe(key, event)
return unless current_transaction
- labels = { operation: key, store: event.payload[:store].split('::').last }
+ labels = { operation: key, store: extract_store_name(event) }
current_transaction.increment(:gitlab_cache_operations_total, 1, labels) do
docstring 'Cache operations'
@@ -76,6 +72,20 @@ module Gitlab
private
+ def track_cache_miss(event)
+ # avoid passing in labels to ensure metric has consistent set of labels
+ labels = { store: extract_store_name(event) }
+
+ current_transaction.increment(:gitlab_cache_misses_total, 1, labels) do
+ docstring 'Cache read miss'
+ end
+ end
+
+ def extract_store_name(event)
+ # see payload documentation in https://guides.rubyonrails.org/active_support_instrumentation.html#active-support
+ event.payload[:store].to_s.split('::').last
+ end
+
def current_transaction
::Gitlab::Metrics::WebTransaction.current
end
diff --git a/lib/gitlab/middleware/compressed_json.rb b/lib/gitlab/middleware/compressed_json.rb
index cc485d8a5db..1f15f1d5857 100644
--- a/lib/gitlab/middleware/compressed_json.rb
+++ b/lib/gitlab/middleware/compressed_json.rb
@@ -3,7 +3,6 @@
module Gitlab
module Middleware
class CompressedJson
- COLLECTOR_PATH = '/api/v4/error_tracking/collector'
INSTANCE_PACKAGES_PATH = %r{
\A/api/v4/packages/npm/-/npm/v1/security/
(?:(?:advisories/bulk)|(?:audits/quick))\z (?# end)
@@ -79,8 +78,7 @@ module Gitlab
end
def match_path?(env)
- env['PATH_INFO'].start_with?((File.join(relative_url, COLLECTOR_PATH))) ||
- match_packages_path?(env)
+ match_packages_path?(env)
end
def match_packages_path?(env)
diff --git a/lib/gitlab/pagination/cursor_based_keyset.rb b/lib/gitlab/pagination/cursor_based_keyset.rb
index a21d0228082..ee8259cc671 100644
--- a/lib/gitlab/pagination/cursor_based_keyset.rb
+++ b/lib/gitlab/pagination/cursor_based_keyset.rb
@@ -6,7 +6,8 @@ module Gitlab
SUPPORTED_ORDERING = {
Group => { name: :asc },
AuditEvent => { id: :desc },
- ::Ci::Build => { id: :desc }
+ ::Ci::Build => { id: :desc },
+ ::Packages::BuildInfo => { id: :desc }
}.freeze
# Relation types that are enforced in this list
diff --git a/lib/gitlab/patch/redis_cache_store.rb b/lib/gitlab/patch/redis_cache_store.rb
new file mode 100644
index 00000000000..5279c4081b2
--- /dev/null
+++ b/lib/gitlab/patch/redis_cache_store.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Patch
+ module RedisCacheStore
+ PIPELINE_BATCH_SIZE = 100
+
+ # We will try keep patched code explicit and matching the original signature in
+ # https://github.com/rails/rails/blob/v6.1.7.2/activesupport/lib/active_support/cache/redis_cache_store.rb#L361
+ def read_multi_mget(*names) # rubocop:disable Style/ArgumentsForwarding
+ return super unless enable_rails_cache_pipeline_patch?
+ return super unless use_patched_mget?
+
+ patched_read_multi_mget(*names) # rubocop:disable Style/ArgumentsForwarding
+ end
+
+ # `delete_multi_entries` in Rails runs a multi-key `del` command
+ # patch will run pipelined single-key `del` for Redis Cluster compatibility
+ def delete_multi_entries(entries, **options)
+ return super unless enable_rails_cache_pipeline_patch?
+
+ delete_count = 0
+ redis.with do |conn|
+ entries.each_slice(PIPELINE_BATCH_SIZE) do |subset|
+ delete_count += Gitlab::Redis::CrossSlot::Pipeline.new(conn).pipelined do |pipeline|
+ subset.each { |entry| pipeline.del(entry) }
+ end.sum
+ end
+ end
+ delete_count
+ end
+
+ # Copied from https://github.com/rails/rails/blob/v6.1.6.1/activesupport/lib/active_support/cache/redis_cache_store.rb
+ # re-implements `read_multi_mget` using a pipeline of `get`s rather than an `mget`
+ #
+ def patched_read_multi_mget(*names)
+ options = names.extract_options!
+ options = merged_options(options)
+ return {} if names == []
+
+ raw = options&.fetch(:raw, false)
+
+ keys = names.map { |name| normalize_key(name, options) }
+
+ values = failsafe(:patched_read_multi_mget, returning: {}) do
+ redis.with { |c| pipeline_mget(c, keys) }
+ end
+
+ names.zip(values).each_with_object({}) do |(name, value), results|
+ if value # rubocop:disable Style/Next
+ entry = deserialize_entry(value, raw: raw)
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
+ results[name] = entry.value
+ end
+ end
+ end
+ end
+
+ def pipeline_mget(conn, keys)
+ keys.each_slice(PIPELINE_BATCH_SIZE).flat_map do |subset|
+ Gitlab::Redis::CrossSlot::Pipeline.new(conn).pipelined do |p|
+ subset.each { |key| p.get(key) }
+ end
+ end
+ end
+
+ private
+
+ def enable_rails_cache_pipeline_patch?
+ redis.with { |c| ::Gitlab::Redis::ClusterUtil.cluster?(c) }
+ end
+
+ # MultiStore reads ONLY from the default store (no fallback), hence we can use `mget`
+ # if the default store is not a Redis::Cluster. We should do that as pipelining gets on a single redis is slow
+ def use_patched_mget?
+ redis.with do |conn|
+ next true unless conn.is_a?(Gitlab::Redis::MultiStore)
+
+ ::Gitlab::Redis::ClusterUtil.cluster?(conn.default_store)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/patch/redis_cluster.rb b/lib/gitlab/patch/redis_cluster.rb
new file mode 100644
index 00000000000..145ce35a317
--- /dev/null
+++ b/lib/gitlab/patch/redis_cluster.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# Patch to expose `find_node_key` method for cross-slot pipelining
+# In redis v5.0.x, cross-slot pipelining is implemented via redis-cluster-client.
+# This patch should be removed since there is no need for it.
+# Gitlab::Redis::CrossSlot and its usage should be removed as well.
+if Gem::Version.new(Redis::VERSION) != Gem::Version.new('4.8.0')
+ raise 'New version of redis detected, please remove or update this patch'
+end
+
+module Gitlab
+ module Patch
+ module RedisCluster
+ # _find_node_key exposes a private function of the same name in Redis::Cluster.
+ # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/cluster.rb#L282
+ def _find_node_key(command)
+ find_node_key(command)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index b0804c2ff66..e112423f167 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -122,6 +122,7 @@ module Gitlab
ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES
ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze
+ ILLEGAL_ORGANIZATION_PATH_WORDS = (TOP_LEVEL_ROUTES | PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze
# The namespace regex is used in JavaScript to validate usernames in the "Register" form. However, Javascript
# does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
@@ -138,6 +139,17 @@ module Gitlab
PROJECT_PATH_FORMAT_REGEX = /(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX}/.freeze
FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/){,#{Namespace::NUMBER_OF_ANCESTORS_ALLOWED}}#{NAMESPACE_FORMAT_REGEX}}.freeze
+ def organization_route_regex
+ @organization_route_regex ||= begin
+ illegal_words = Regexp.new(Regexp.union(ILLEGAL_ORGANIZATION_PATH_WORDS).source, Regexp::IGNORECASE)
+
+ single_line_regexp %r{
+ (?!(#{illegal_words})/)
+ #{NAMESPACE_FORMAT_REGEX}
+ }x
+ end
+ end
+
def root_namespace_route_regex
@root_namespace_route_regex ||= begin
illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE)
@@ -195,6 +207,10 @@ module Gitlab
@full_namespace_path_regex ||= %r{\A#{full_namespace_route_regex}/\z}
end
+ def organization_path_regex
+ @organization_path_regex ||= %r{\A#{organization_route_regex}/\z}
+ end
+
def full_project_path_regex
@full_project_path_regex ||= %r{\A#{full_namespace_route_regex}/#{project_route_regex}/\z}
end
diff --git a/lib/gitlab/path_traversal.rb b/lib/gitlab/path_traversal.rb
new file mode 100644
index 00000000000..1123ff73136
--- /dev/null
+++ b/lib/gitlab/path_traversal.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PathTraversal
+ extend self
+ PathTraversalAttackError = Class.new(StandardError)
+
+ private_class_method def logger
+ @logger ||= Gitlab::AppLogger
+ end
+
+ PATH_TRAVERSAL_REGEX = %r{(\A(\.{1,2})\z|\A\.\.[/\\]|[/\\]\.\.\z|[/\\]\.\.[/\\]|\n)}
+
+ # Ensure that the relative path will not traverse outside the base directory
+ # We url decode the path to avoid passing invalid paths forward in url encoded format.
+ # Also see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24223#note_284122580
+ # It also checks for ALT_SEPARATOR aka '\' (forward slash)
+ def check_path_traversal!(path)
+ return unless path
+
+ path = path.to_s if path.is_a?(Gitlab::HashedPath)
+ raise PathTraversalAttackError, 'Invalid path' unless path.is_a?(String)
+
+ path = ::Gitlab::Utils.decode_path(path)
+
+ if path.match?(PATH_TRAVERSAL_REGEX)
+ logger.warn(message: "Potential path traversal attempt detected", path: path.to_s)
+ raise PathTraversalAttackError, 'Invalid path'
+ end
+
+ path
+ end
+
+ def check_allowed_absolute_path!(path, allowlist)
+ return unless Pathname.new(path).absolute?
+ return if ::Gitlab::Utils.allowlisted?(path, allowlist)
+
+ raise StandardError, "path #{path} is not allowed"
+ end
+
+ def check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
+ traversal_path = check_path_traversal!(path)
+ raise StandardError, "path is not a string!" unless traversal_path.is_a?(String)
+
+ check_allowed_absolute_path!(traversal_path, path_allowlist)
+ end
+ end
+end
diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb
index c770260a66e..0fcb8321dae 100644
--- a/lib/gitlab/project_authorizations.rb
+++ b/lib/gitlab/project_authorizations.rb
@@ -12,62 +12,56 @@ module Gitlab
end
def calculate
- cte = if Feature.enabled?(:linear_project_authorization, user)
- linear_cte
- else
- recursive_cte
- end
-
- cte_alias = cte.table.alias(Group.table_name)
- projects = Project.arel_table
- links = ProjectGroupLink.arel_table
-
- relations = [
- # The project a user has direct access to.
- user.projects_with_active_memberships.select_for_project_authorization,
-
- # The personal projects of the user.
- user.personal_projects.select_project_owner_for_project_authorization,
+ if Feature.enabled?(:compare_project_authorization_linear_cte, user)
+ linear_relation = calculate_with_linear_query
+ recursive_relation = calculate_with_recursive_query
+ recursive_set = Set.new(recursive_relation.to_a.pluck(:project_id, :access_level))
+ linear_set = Set.new(linear_relation.to_a.pluck(:project_id, :access_level))
+ if linear_set == recursive_set
+ Gitlab::AppJsonLogger.info(event: 'linear_authorized_projects_check',
+ user_id: user.id,
+ matching_results: true)
+ return calculate_with_linear_query
+ else
+ Gitlab::AppJsonLogger.warn(event: 'linear_authorized_projects_check',
+ user_id: user.id,
+ matching_results: false)
+ end
+ end
- # Projects that belong directly to any of the groups the user has
- # access to.
- Namespace
- .unscoped
- .select([alias_as_column(projects[:id], 'project_id'),
- cte_alias[:access_level]])
- .from(cte_alias)
- .joins(:projects),
-
- # Projects shared with any of the namespaces the user has access to.
- Namespace
- .unscoped
- .select([
- links[:project_id],
- least(cte_alias[:access_level], links[:group_access], 'access_level')
- ])
- .from(cte_alias)
- .joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id')
- .joins('INNER JOIN projects ON projects.id = project_group_links.project_id')
- .joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id')
- .where('p_ns.share_with_group_lock IS FALSE')
- ]
+ Gitlab::AppJsonLogger.info(event: 'linear_authorized_projects_check_with_flag',
+ feature_flag_status: Feature.enabled?(:linear_project_authorization, user))
if Feature.enabled?(:linear_project_authorization, user)
- ProjectAuthorization
- .unscoped
- .with(cte.to_arel)
- .select_from_union(relations)
+ calculate_with_linear_query
else
- ProjectAuthorization
- .unscoped
- .with
- .recursive(cte.to_arel)
- .select_from_union(relations)
+ calculate_with_recursive_query
end
end
private
+ def calculate_with_linear_query
+ cte = linear_cte
+ cte_alias = cte.table.alias(Group.table_name)
+
+ ProjectAuthorization
+ .unscoped
+ .with(cte.to_arel)
+ .select_from_union(relations(cte_alias: cte_alias))
+ end
+
+ def calculate_with_recursive_query
+ cte = recursive_cte
+ cte_alias = cte.table.alias(Group.table_name)
+
+ ProjectAuthorization
+ .unscoped
+ .with
+ .recursive(cte.to_arel)
+ .select_from_union(relations(cte_alias: cte_alias))
+ end
+
# Builds a recursive CTE that gets all the groups the current user has
# access to, including any nested groups and any shared groups.
def recursive_cte
@@ -83,9 +77,11 @@ module Gitlab
# Namespaces shared with any of the group
cte << Group.select([namespaces[:id],
- least(members[:access_level],
- group_group_links[:group_access],
- 'access_level')])
+ least(
+ members[:access_level],
+ group_group_links[:group_access],
+ 'access_level'
+ )])
.joins(join_group_group_links)
.joins(join_members_on_group_group_links)
@@ -120,7 +116,9 @@ module Gitlab
.merge(user.group_members)
.merge(GroupMember.active_state)
- union = Namespace.from_union([shared_groups, member_groups_with_ancestors])
+ union = Namespace
+ .select("namespaces.id, access_level")
+ .from_union([shared_groups, member_groups_with_ancestors])
Gitlab::SQL::CTE.new(:linear_namespaces_cte, union)
end
@@ -185,5 +183,45 @@ module Gitlab
def alias_as_column(value, alias_to)
Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to))
end
+
+ def relations(cte_alias:)
+ [
+ user.projects_with_active_memberships.select_for_project_authorization,
+ user.personal_projects.select_project_owner_for_project_authorization,
+ projects_belonging_directy_to_any_groups_user_has_access_to(cte_alias: cte_alias),
+ projects_shared_with_namespaces_user_has_access_to(cte_alias: cte_alias)
+ ]
+ end
+
+ def projects_shared_with_namespaces_user_has_access_to(cte_alias:)
+ Namespace
+ .unscoped
+ .select([
+ links[:project_id],
+ least(cte_alias[:access_level], links[:group_access], 'access_level')
+ ])
+ .from(cte_alias)
+ .joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id')
+ .joins('INNER JOIN projects ON projects.id = project_group_links.project_id')
+ .joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id')
+ .where('p_ns.share_with_group_lock IS FALSE')
+ end
+
+ def projects_belonging_directy_to_any_groups_user_has_access_to(cte_alias:)
+ Namespace
+ .unscoped
+ .select([alias_as_column(projects[:id], 'project_id'),
+ cte_alias[:access_level]])
+ .from(cte_alias)
+ .joins(:projects)
+ end
+
+ def projects
+ Project.arel_table
+ end
+
+ def links
+ ProjectGroupLink.arel_table
+ end
end
end
diff --git a/lib/gitlab/quick_actions/relate_actions.rb b/lib/gitlab/quick_actions/relate_actions.rb
index b8cbfdefda1..058c1e7e9bf 100644
--- a/lib/gitlab/quick_actions/relate_actions.rb
+++ b/lib/gitlab/quick_actions/relate_actions.rb
@@ -16,32 +16,60 @@ module Gitlab
end
params '<#issue | group/project#issue | issue URL>'
types Issue
- condition { can_relate_issues? }
+ condition { can_admin_link? }
parse_params { |issues| format_params(issues) }
command :relate do |target_issues|
create_links(target_issues)
end
+ desc { _("Remove link with another issue") }
+ explanation do |issue|
+ _('Removes link with %{issue_ref}.') % { issue_ref: issue.to_reference(quick_action_target) }
+ end
+ execution_message do |issue|
+ _('Removed link with %{issue_ref}.') % { issue_ref: issue.to_reference(quick_action_target) }
+ end
+ params '<#issue | group/project#issue | issue URL>'
+ types Issue
+ condition { can_admin_link? }
+ parse_params do |issue_param|
+ extract_references(issue_param, :issue).first
+ end
+ command :unlink do |issue|
+ link = IssueLink.for_issues(quick_action_target, issue).first
+
+ if link
+ call_link_service(IssueLinks::DestroyService.new(link, current_user))
+ else
+ @execution_message[:unlink] = _('No linked issue matches the provided parameter.')
+ end
+ end
+
private
+ def can_admin_link?
+ current_user.can?(:admin_issue_link, quick_action_target)
+ end
+
def create_links(references, type: 'relates_to')
- service = IssueLinks::CreateService.new(
+ create_service_instance = IssueLinks::CreateService.new(
quick_action_target,
current_user, { issuable_references: references, link_type: type }
)
- create_issue_link = proc { service.execute }
+
+ call_link_service(create_service_instance)
+ end
+
+ def call_link_service(service_instance)
+ execute_service = proc { service_instance.execute }
if quick_action_target.persisted?
- create_issue_link.call
+ execute_service.call
else
- quick_action_target.run_after_commit(&create_issue_link)
+ quick_action_target.run_after_commit(&execute_service)
end
end
- def can_relate_issues?
- current_user.can?(:admin_issue_link, quick_action_target)
- end
-
def format_params(issue_references)
issue_references.split(' ')
end
diff --git a/lib/gitlab/quick_actions/work_item_actions.rb b/lib/gitlab/quick_actions/work_item_actions.rb
index fa43308c9e2..5664410f3ca 100644
--- a/lib/gitlab/quick_actions/work_item_actions.rb
+++ b/lib/gitlab/quick_actions/work_item_actions.rb
@@ -12,42 +12,94 @@ module Gitlab
format(_("Converts work item to %{type}. Widgets not supported in new type are removed."), type: target_type)
end
types WorkItem
- condition do
- quick_action_target&.project&.work_items_mvc_2_feature_flag_enabled?
- end
params 'Task | Objective | Key Result | Issue'
command :type do |type_name|
- work_item_type = ::WorkItems::Type.find_by_name(type_name)
- errors = validate_type(work_item_type)
-
- if errors.present?
- @execution_message[:type] = errors
- else
- @updates[:issue_type] = work_item_type.base_type
- @updates[:work_item_type] = work_item_type
- @execution_message[:type] = _('Type changed successfully.')
- end
+ @execution_message[:type] = update_type(type_name, :type)
+ end
+
+ desc { _('Promote work item') }
+ explanation do |type_name|
+ format(_("Promotes work item to %{type}."), type: type_name)
+ end
+ types WorkItem
+ params 'issue | objective'
+ condition { supports_promotion? }
+ command :promote_to do |type_name|
+ @execution_message[:promote_to] = update_type(type_name, :promote_to)
end
end
private
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def update_type(type_name, command)
+ new_type = ::WorkItems::Type.find_by_name(type_name.titleize)
+ error_message = command == :type ? validate_type(new_type) : validate_promote_to(new_type)
+ return error_message if error_message.present?
+
+ @updates[:issue_type] = new_type.base_type
+ @updates[:work_item_type] = new_type
+
+ success_msg[command]
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
def validate_type(type)
- return type_error(:not_found) unless type.present?
- return type_error(:same_type) if quick_action_target.work_item_type == type
- return type_error(:forbidden) unless current_user.can?(:"create_#{type.base_type}", quick_action_target)
+ return error_msg(:not_found) unless type.present?
+ return error_msg(:same_type) if quick_action_target.work_item_type == type
+ return error_msg(:forbidden) unless current_user.can?(:"create_#{type.base_type}", quick_action_target)
nil
end
- def type_error(reason)
+ def validate_promote_to(type)
+ return error_msg(:not_found, action: 'promote') unless type && supports_promote_to?(type.name)
+
+ unless current_user.can?(:"create_#{type.base_type}", quick_action_target)
+ return error_msg(:forbidden, action: 'promote')
+ end
+
+ validate_hierarchy
+ end
+
+ def validate_hierarchy
+ return unless current_type.task? && quick_action_target.parent_link
+
+ error_msg(:hierarchy, action: 'promote')
+ end
+
+ def current_type
+ quick_action_target.work_item_type
+ end
+
+ def supports_promotion?
+ current_type.base_type.in?(promote_to_map.keys)
+ end
+
+ def supports_promote_to?(type_name)
+ type_name == promote_to_map[current_type.base_type]
+ end
+
+ def promote_to_map
+ { issue: 'Incident', task: 'Issue' }.with_indifferent_access
+ end
+
+ def error_msg(reason, action: 'convert')
message = {
not_found: 'Provided type is not supported',
same_type: 'Types are the same',
- forbidden: 'You have insufficient permissions'
+ forbidden: 'You have insufficient permissions',
+ hierarchy: 'A task cannot be promoted when a parent issue is present'
}.freeze
- format(_("Failed to convert this work item: %{reason}."), { reason: message[reason] })
+ format(_("Failed to %{action} this work item: %{reason}."), { action: action, reason: message[reason] })
+ end
+
+ def success_msg
+ {
+ type: _('Type changed successfully.'),
+ promote_to: _("Work Item promoted successfully.")
+ }
end
end
end
diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb
index dc13bb927e6..9f5769a5c97 100644
--- a/lib/gitlab/reactive_cache_set_cache.rb
+++ b/lib/gitlab/reactive_cache_set_cache.rb
@@ -11,17 +11,15 @@ module Gitlab
end
def clear_cache!(key)
- use_pipeline = ::Feature.enabled?(:use_pipeline_over_multikey)
-
with do |redis|
keys = read(key).map { |value| "#{cache_namespace}:#{value}" }
keys << cache_key(key)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- if use_pipeline
- keys.each { |key| pipeline.unlink(key) }
- else
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis)
+ else
+ redis.pipelined do |pipeline|
keys.each_slice(1000) { |subset| pipeline.unlink(*subset) }
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 64ca89c6bff..06bce7649bf 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -9,6 +9,7 @@ module Gitlab
# config/initializers/7_redis.rb, instrumented, and used in health- & readiness checks.
ALL_CLASSES = [
Gitlab::Redis::Cache,
+ Gitlab::Redis::ClusterCache,
Gitlab::Redis::DbLoadBalancing,
Gitlab::Redis::FeatureFlag,
Gitlab::Redis::Queues,
@@ -16,7 +17,8 @@ module Gitlab
Gitlab::Redis::RepositoryCache,
Gitlab::Redis::Sessions,
Gitlab::Redis::SharedState,
- Gitlab::Redis::TraceChunks
+ Gitlab::Redis::TraceChunks,
+ Gitlab::Redis::Chat
].freeze
end
end
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index ba3af3e7a6f..60944268f91 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -5,19 +5,35 @@ module Gitlab
class Cache < ::Gitlab::Redis::Wrapper
CACHE_NAMESPACE = 'cache:gitlab'
- # Full list of options:
- # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
- def self.active_support_config
- {
- redis: pool,
- compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
- namespace: CACHE_NAMESPACE,
- expires_in: default_ttl_seconds
- }
- end
+ class << self
+ # Full list of options:
+ # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
+ def active_support_config
+ {
+ redis: pool,
+ compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
+ namespace: CACHE_NAMESPACE,
+ expires_in: default_ttl_seconds
+ }
+ end
+
+ def default_ttl_seconds
+ ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i
+ end
+
+ # Exposes redis for Peek adapter. To be removed after ClusterCache migration.
+ def multistore_redis
+ redis
+ end
+
+ private
+
+ def redis
+ primary_store = ::Redis.new(Gitlab::Redis::ClusterCache.params)
+ secondary_store = ::Redis.new(params)
- def self.default_ttl_seconds
- ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i
+ MultiStore.new(primary_store, secondary_store, store_name)
+ end
end
end
end
diff --git a/lib/gitlab/redis/chat.rb b/lib/gitlab/redis/chat.rb
new file mode 100644
index 00000000000..6f320fa6fc6
--- /dev/null
+++ b/lib/gitlab/redis/chat.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ class Chat < ::Gitlab::Redis::Wrapper
+ class << self
+ def config_fallback
+ Cache
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/cluster_cache.rb b/lib/gitlab/redis/cluster_cache.rb
new file mode 100644
index 00000000000..15a87739c6d
--- /dev/null
+++ b/lib/gitlab/redis/cluster_cache.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ class ClusterCache < ::Gitlab::Redis::Wrapper
+ class << self
+ def config_fallback
+ Cache
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/cluster_util.rb b/lib/gitlab/redis/cluster_util.rb
new file mode 100644
index 00000000000..5f1f39b5237
--- /dev/null
+++ b/lib/gitlab/redis/cluster_util.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ module ClusterUtil
+ class << self
+ # clusters? is used to select Redis command types, on `true`, the subsequent
+ # commands should be compatible with Redis Cluster.
+ #
+ # When working with MultiStore, if even 1 of 2 stores is a Redis::Cluster,
+ # we should err on the side of caution and return `true `,
+ def cluster?(obj)
+ if obj.is_a?(MultiStore)
+ cluster?(obj.primary_store) || cluster?(obj.secondary_store)
+ else
+ obj.respond_to?(:_client) && obj._client.is_a?(::Redis::Cluster)
+ end
+ end
+
+ def batch_unlink(keys, redis)
+ expired_count = 0
+ keys.each_slice(1000) do |subset|
+ expired_count += Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+ subset.each { |key| pipeline.unlink(key) }
+ end.sum
+ end
+ expired_count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/cross_slot.rb b/lib/gitlab/redis/cross_slot.rb
new file mode 100644
index 00000000000..e5aa6d9ce72
--- /dev/null
+++ b/lib/gitlab/redis/cross_slot.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ module CrossSlot
+ class Router
+ attr_reader :node_mapping, :futures, :node_sequence, :cmd_queue
+
+ delegate :respond_to_missing?, to: :@redis
+
+ # This map contains redis-rb methods which does not map directly
+ # to a standard Redis command. It is used transform unsupported commands to standard commands
+ # to find the node key for unsupported commands.
+ #
+ # Redis::Cluster::Command only contains details of commands which the Redis Server
+ # returns. Hence, commands like mapped_hmget and hscan_each internally will call the
+ # base command, hmget and hscan respectively.
+ #
+ # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/cluster/command.rb
+ UNSUPPORTED_CMD_MAPPING = {
+ # Internally, redis-rb calls the supported Redis command and transforms the output.
+ # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/commands/hashes.rb#L104
+ mapped_hmget: :hmget
+ }.freeze
+
+ # Initializes the CrossSlot::Router
+ # @param {::Redis}
+ def initialize(redis)
+ @redis = redis
+ @node_mapping = {}
+ @futures = {}
+ @node_sequence = []
+ @cmd_queue = []
+ end
+
+ # For now we intercept every redis.call and return a Gitlab-Future object.
+ # This method groups every commands to a node for fan-out. Commands are grouped using the first key.
+ #
+ # rubocop:disable Style/MissingRespondToMissing
+ def method_missing(cmd, *args, **kwargs, &blk)
+ # Note that we can re-map the command without affecting execution as it is
+ # solely for finding the node key. The original cmd will be executed.
+ node = @redis._client._find_node_key([UNSUPPORTED_CMD_MAPPING.fetch(cmd, cmd)] + args)
+
+ @node_mapping[node] ||= []
+ @futures[node] ||= []
+
+ @node_sequence << node
+ @node_mapping[node] << [cmd, args, kwargs || {}, blk]
+ f = Future.new
+ @futures[node] << f
+ @cmd_queue << [f, cmd, args, kwargs || {}, blk]
+ f
+ end
+ # rubocop:enable Style/MissingRespondToMissing
+ end
+
+ # Wraps over redis-rb's Future in
+ # https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/pipeline.rb#L244
+ class Future
+ def set(future, is_val = false)
+ @redis_future = future
+ @is_val = is_val
+ end
+
+ def value
+ return @redis_val if @is_val
+
+ @redis_future.value
+ end
+ end
+
+ # Pipeline allows cross-slot pipelined to be called. The fan-out logic is implemented in
+ # https://github.com/redis-rb/redis-cluster-client/blob/master/lib/redis_client/cluster/pipeline.rb
+ # which is available in redis-rb v5.0.
+ #
+ # This file can be deprecated after redis-rb v4.8.0 is upgraded to v5.0
+ class Pipeline
+ # Initializes the CrossSlot::Pipeline
+ # @param {::Redis}
+ def initialize(redis)
+ @redis = redis
+ end
+
+ # pipelined is used in place of ::Redis `.pipelined` when running in a cluster context
+ # where cross-slot operations may happen.
+ def pipelined(&block)
+ # Directly call .pipelined and defer the pipeline execution to MultiStore.
+ # MultiStore could wrap over 0, 1, or 2 Redis Cluster clients, handling it here
+ # will not work for 2 clients since the key-slot topology can differ.
+ if use_cross_slot_pipelining?
+ router = Router.new(@redis)
+ yield router
+ execute_commands(router)
+ else
+ # use redis-rb's pipelined method
+ @redis.pipelined(&block)
+ end
+ end
+
+ private
+
+ def use_cross_slot_pipelining?
+ !@redis.instance_of?(::Gitlab::Redis::MultiStore) && @redis._client.instance_of?(::Redis::Cluster)
+ end
+
+ def execute_commands(router)
+ router.node_mapping.each do |node_key, commands|
+ # TODO possibly use Threads to speed up but for now `n` is 3-5 which is small.
+ @redis.pipelined do |p|
+ commands.each_with_index do |command, idx|
+ future = router.futures[node_key][idx]
+ cmd, args, kwargs, blk = command
+ future.set(p.public_send(cmd, *args, **kwargs, &blk)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+
+ router.node_sequence.map do |node_key|
+ router.futures[node_key].shift.value
+ end
+ rescue ::Redis::CommandError => err
+ if err.message.start_with?('MOVED', 'ASK')
+ Gitlab::ErrorTracking.log_exception(err)
+ return execute_commands_sequentially(router)
+ end
+
+ raise
+ end
+
+ def execute_commands_sequentially(router)
+ router.cmd_queue.map do |command|
+ future, cmd, args, kwargs, blk = command
+ future.set(@redis.public_send(cmd, *args, **kwargs, &blk), true) # rubocop:disable GitlabSecurity/PublicSend
+ future.value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
index 9571e2f92e6..d36ef6b99ee 100644
--- a/lib/gitlab/redis/multi_store.rb
+++ b/lib/gitlab/redis/multi_store.rb
@@ -44,6 +44,7 @@ module Gitlab
hscan_each
mapped_hmget
mget
+ scan
scan_each
scard
sismember
@@ -66,11 +67,14 @@ module Gitlab
mapped_hmset
rpush
sadd
+ sadd?
set
setex
setnx
srem
unlink
+
+ memory
].freeze
PIPELINED_COMMANDS = %i[
@@ -122,7 +126,7 @@ module Gitlab
if use_primary_and_secondary_stores?
pipelined_both(name, *args, **kwargs, &block)
else
- default_store.send(name, *args, **kwargs, &block)
+ send_command(default_store, name, *args, **kwargs, &block)
end
end
end
@@ -289,6 +293,16 @@ module Gitlab
# rubocop:disable GitlabSecurity/PublicSend
def send_command(redis_instance, command_name, *args, **kwargs, &block)
+ # Run wrapped pipeline for each instance individually so that the fan-out is distinct.
+ # If both primary and secondary are Redis Clusters, the slot-node distribution could
+ # be different.
+ #
+ # We ignore args and kwargs since `pipelined` does not accept arguments
+ # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis.rb#L164
+ if command_name.to_s == 'pipelined' && redis_instance._client.instance_of?(::Redis::Cluster)
+ return Gitlab::Redis::CrossSlot::Pipeline.new(redis_instance).pipelined(&block)
+ end
+
if block
# 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|
diff --git a/lib/gitlab/redis/rate_limiting.rb b/lib/gitlab/redis/rate_limiting.rb
index 74b4ca12d18..30ec44b748d 100644
--- a/lib/gitlab/redis/rate_limiting.rb
+++ b/lib/gitlab/redis/rate_limiting.rb
@@ -3,18 +3,11 @@
module Gitlab
module Redis
class RateLimiting < ::Gitlab::Redis::Wrapper
- # We create a subclass only for the purpose of differentiating between different stores in cache metrics
- RateLimitingStore = Class.new(ActiveSupport::Cache::RedisCacheStore)
-
class << self
# The data we store on RateLimiting used to be stored on Cache.
def config_fallback
Cache
end
-
- def cache_store
- @cache_store ||= RateLimitingStore.new(redis: pool, namespace: Cache::CACHE_NAMESPACE)
- end
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index eb99805e2e8..26ca9d2547c 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -548,8 +548,8 @@ module Gitlab
# https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html
# Avoids linking CVE IDs (https://cve.mitre.org/cve/identifiers/syntaxchange.html#new) as Jira issues.
# CVE IDs use the format of CVE-YYYY-NNNNNNN
- def jira_issue_key_regex
- @jira_issue_key_regex ||= /(?!CVE-\d+-\d+)[A-Z][A-Z_0-9]+-\d+/
+ def jira_issue_key_regex(expression_escape: '\b')
+ /#{expression_escape}(?!CVE-\d+-\d+)[A-Z][A-Z_0-9]+-\d+/
end
def jira_issue_key_project_key_extraction_regex
@@ -623,6 +623,18 @@ module Gitlab
def x509_subject_key_identifier_regex
@x509_subject_key_identifier_regex ||= /\A(?:\h{2}:)*\h{2}\z/.freeze
end
+
+ def ml_model_version_regex
+ maven_version_regex
+ end
+
+ def ml_model_name_regex
+ package_name_regex
+ end
+
+ def ml_model_file_name_regex
+ maven_file_name_regex
+ end
end
end
diff --git a/lib/gitlab/repository_hash_cache.rb b/lib/gitlab/repository_hash_cache.rb
index 1f3c084e194..fab0e9e09e8 100644
--- a/lib/gitlab/repository_hash_cache.rb
+++ b/lib/gitlab/repository_hash_cache.rb
@@ -40,7 +40,11 @@ module Gitlab
keys = keys.map { |key| cache_key(key) }
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.unlink(*keys)
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis)
+ else
+ redis.unlink(*keys)
+ end
end
end
end
diff --git a/lib/gitlab/resource_events/assignment_event_recorder.rb b/lib/gitlab/resource_events/assignment_event_recorder.rb
index 94bd05a17ba..0f1ceeb2a66 100644
--- a/lib/gitlab/resource_events/assignment_event_recorder.rb
+++ b/lib/gitlab/resource_events/assignment_event_recorder.rb
@@ -11,8 +11,6 @@ module Gitlab
end
def record
- return if Feature.disabled?(:record_issue_and_mr_assignee_events, parent.project)
-
case parent
when Issue
record_for_parent(
diff --git a/lib/gitlab/search/abuse_detection.rb b/lib/gitlab/search/abuse_detection.rb
index 7b5377bce88..8711d078ea9 100644
--- a/lib/gitlab/search/abuse_detection.rb
+++ b/lib/gitlab/search/abuse_detection.rb
@@ -8,7 +8,6 @@ module Gitlab
ABUSIVE_TERM_SIZE = 100
ALLOWED_CHARS_REGEX = %r{\A[[:alnum:]_\-\/\.!]+\z}.freeze
- MINIMUM_SEARCH_CHARS = 2
ALLOWED_SCOPES = %w(
blobs
@@ -50,7 +49,8 @@ module Gitlab
exclusion: { in: STOP_WORDS, message: 'stopword only abusive search detected' }, allow_blank: true
validates :query_string,
- length: { minimum: MINIMUM_SEARCH_CHARS, message: 'abusive tiny search detected' }, unless: :skip_tiny_search_validation?, allow_blank: true
+ length: { minimum: Params::MIN_TERM_LENGTH, message: 'abusive tiny search detected' },
+ unless: :skip_tiny_search_validation?, allow_blank: true
validates :query_string,
no_abusive_term_length: { maximum: ABUSIVE_TERM_SIZE, maximum_for_url: ABUSIVE_TERM_SIZE * 2 }
diff --git a/lib/gitlab/search/params.rb b/lib/gitlab/search/params.rb
index 1ae14e5e618..6eb24a92be6 100644
--- a/lib/gitlab/search/params.rb
+++ b/lib/gitlab/search/params.rb
@@ -7,7 +7,7 @@ module Gitlab
SEARCH_CHAR_LIMIT = 4096
SEARCH_TERM_LIMIT = 64
- MIN_TERM_LENGTH = 3
+ MIN_TERM_LENGTH = 2
# Generic validation
validates :query_string, length: { maximum: SEARCH_CHAR_LIMIT }
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 93befc2df57..a733dca6a56 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -107,7 +107,11 @@ module Gitlab
def users
return User.none unless Ability.allowed?(current_user, :read_users_list)
- UsersFinder.new(current_user, search: query).execute
+ if Feature.enabled?(:autocomplete_users_use_search_service)
+ UsersFinder.new(current_user, { search: query, use_minimum_char_limit: false }).execute
+ else
+ UsersFinder.new(current_user, search: query).execute
+ end
end
# highlighting is only performed by Elasticsearch backed results
@@ -174,7 +178,9 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def projects
- limit_projects.search(query)
+ scope = limit_projects
+ scope = scope.non_archived if Feature.enabled?(:search_projects_hide_archived) && !filters[:include_archived]
+ scope.search(query)
end
def issues(finder_params = {})
diff --git a/lib/gitlab/sentence.rb b/lib/gitlab/sentence.rb
new file mode 100644
index 00000000000..963459e31a3
--- /dev/null
+++ b/lib/gitlab/sentence.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Sentence
+ extend self
+ # Wraps ActiveSupport's Array#to_sentence to convert the given array to a
+ # comma-separated sentence joined with localized 'or' Strings instead of 'and'.
+ def to_exclusive_sentence(array)
+ array.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or '))
+ end
+ end
+end
diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb
index 623b254c4e0..eb73a0a3d31 100644
--- a/lib/gitlab/set_cache.rb
+++ b/lib/gitlab/set_cache.rb
@@ -22,10 +22,8 @@ module Gitlab
keys_to_expire = keys.map { |key| cache_key(key) }
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- if ::Feature.enabled?(:use_pipeline_over_multikey)
- redis.pipelined do |pipeline|
- keys_to_expire.each { |key| pipeline.unlink(key) }
- end.sum
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys_to_expire, redis)
else
redis.unlink(*keys_to_expire)
end
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index 7ce3f6b5ccb..c4566a6dc2a 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -76,15 +76,19 @@ module Gitlab
payload['load_balancing_strategy'] = job['load_balancing_strategy'] if job['load_balancing_strategy']
payload['dedup_wal_locations'] = job['dedup_wal_locations'] if job['dedup_wal_locations'].present?
- if job_exception
- payload['message'] = "#{message}: fail: #{payload['duration_s']} sec"
- payload['job_status'] = 'fail'
-
- Gitlab::ExceptionLogFormatter.format!(job_exception, payload)
- else
- payload['message'] = "#{message}: done: #{payload['duration_s']} sec"
- payload['job_status'] = 'done'
- end
+ job_status = if job_exception
+ 'fail'
+ elsif job['deferred']
+ 'deferred'
+ else
+ 'done'
+ end
+
+ payload['message'] = "#{message}: #{job_status}: #{payload['duration_s']} sec"
+ payload['job_status'] = job_status
+ payload['job_deferred_by'] = job['deferred_by'] if job['deferred']
+
+ Gitlab::ExceptionLogFormatter.format!(job_exception, payload) if job_exception
db_duration = ActiveRecord::LogSubscriber.runtime
payload['db_duration_s'] = Gitlab::Utils.ms_to_round_sec(db_duration)
diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb
index b20f639ce85..ec2a6472809 100644
--- a/lib/gitlab/sidekiq_middleware.rb
+++ b/lib/gitlab/sidekiq_middleware.rb
@@ -7,7 +7,7 @@ module Gitlab
# The result of this method should be passed to
# Sidekiq's `config.server_middleware` method
# eg: `config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator)`
- def self.server_configurator(metrics: true, arguments_logger: true)
+ def self.server_configurator(metrics: true, arguments_logger: true, defer_jobs: true)
lambda do |chain|
# Size limiter should be placed at the top
chain.add ::Gitlab::SidekiqMiddleware::SizeLimiter::Server
@@ -36,10 +36,11 @@ module Gitlab
chain.add ::Gitlab::SidekiqVersioning::Middleware
chain.add ::Gitlab::SidekiqStatus::ServerMiddleware
chain.add ::Gitlab::SidekiqMiddleware::WorkerContext::Server
- # DuplicateJobs::Server should be placed at the bottom, but before the SidekiqServerMiddleware,
+ # DuplicateJobs::Server should be placed at the bottom, but before the SidekiqServerMiddleware,
# so we can compare the latest WAL location against replica
chain.add ::Gitlab::SidekiqMiddleware::DuplicateJobs::Server
chain.add ::Gitlab::Database::LoadBalancing::SidekiqServerMiddleware
+ chain.add ::Gitlab::SidekiqMiddleware::DeferJobs if defer_jobs
end
end
diff --git a/lib/gitlab/sidekiq_middleware/defer_jobs.rb b/lib/gitlab/sidekiq_middleware/defer_jobs.rb
new file mode 100644
index 00000000000..0a12667865c
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/defer_jobs.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class DeferJobs
+ DELAY = ENV.fetch("SIDEKIQ_DEFER_JOBS_DELAY", 5.minutes)
+ FEATURE_FLAG_PREFIX = "defer_sidekiq_jobs"
+
+ DatabaseHealthStatusChecker = Struct.new(:id, :job_class_name)
+
+ # There are 2 scenarios under which this middleware defers a job
+ # 1. defer_sidekiq_jobs_#{worker_name} FF, jobs are deferred indefinitely until this feature flag
+ # is turned off or when Feature.enabled? returns false by chance while using `percentage of time` value.
+ # 2. Gitlab::Database::HealthStatus, on evaluating the db health status if it returns any indicator
+ # with stop signal, the jobs will be delayed by 'x' seconds (set in worker).
+ def call(worker, job, _queue)
+ # ActiveJobs have wrapped class stored in 'wrapped' key
+ resolved_class = job['wrapped']&.safe_constantize || worker.class
+ defer_job, delay, deferred_by = defer_job_info(resolved_class, job)
+
+ if !!defer_job
+ # Referred in job_logger's 'log_job_done' method to compute proper 'job_status'
+ job['deferred'] = true
+ job['deferred_by'] = deferred_by
+
+ worker.class.perform_in(delay, *job['args'])
+ counter.increment({ worker: worker.class.name })
+
+ # This breaks the middleware chain and return
+ return
+ end
+
+ yield
+ end
+
+ private
+
+ def defer_job_info(worker_class, job)
+ if defer_job_by_ff?(worker_class)
+ [true, DELAY, :feature_flag]
+ elsif defer_job_by_database_health_signal?(job, worker_class)
+ [true, worker_class.database_health_check_attrs[:delay_by], :database_health_check]
+ end
+ end
+
+ def defer_job_by_ff?(worker_class)
+ Feature.enabled?(
+ :"#{FEATURE_FLAG_PREFIX}_#{worker_class.name}",
+ type: :worker,
+ default_enabled_if_undefined: false
+ )
+ end
+
+ def defer_job_by_database_health_signal?(job, worker_class)
+ unless worker_class.respond_to?(:defer_on_database_health_signal?) &&
+ worker_class.defer_on_database_health_signal?
+ return false
+ end
+
+ health_check_attrs = worker_class.database_health_check_attrs
+ job_base_model = Gitlab::Database.schemas_to_base_models[health_check_attrs[:gitlab_schema]].first
+
+ health_context = Gitlab::Database::HealthStatus::Context.new(
+ DatabaseHealthStatusChecker.new(job['jid'], worker_class.name),
+ job_base_model.connection,
+ health_check_attrs[:gitlab_schema],
+ health_check_attrs[:tables]
+ )
+
+ Gitlab::Database::HealthStatus.evaluate(health_context).any?(&:stop?)
+ end
+
+ def counter
+ @counter ||= Gitlab::Metrics.counter(:sidekiq_jobs_deferred_total, 'The number of jobs deferred')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/silent_mode.rb b/lib/gitlab/silent_mode.rb
new file mode 100644
index 00000000000..7c7cbf8f1d9
--- /dev/null
+++ b/lib/gitlab/silent_mode.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SilentMode
+ def self.enabled?
+ Gitlab::CurrentSettings.silent_mode_enabled?
+ end
+
+ def self.log_info(data)
+ Gitlab::AppJsonLogger.info(**add_silent_mode_log_data(data))
+ end
+
+ def self.log_debug(data)
+ Gitlab::AppJsonLogger.debug(**add_silent_mode_log_data(data))
+ end
+
+ def self.add_silent_mode_log_data(data)
+ data.merge!({ silent_mode_enabled: enabled? })
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/incident_management/incident_command.rb b/lib/gitlab/slash_commands/incident_management/incident_command.rb
index 3fa08621777..1c870efe8b1 100644
--- a/lib/gitlab/slash_commands/incident_management/incident_command.rb
+++ b/lib/gitlab/slash_commands/incident_management/incident_command.rb
@@ -11,9 +11,13 @@ module Gitlab
def collection
IssuesFinder.new(current_user, project_id: project.id, issue_types: :incident).execute
end
+
+ def slack_installation
+ slack_workspace_id = params[:team_id]
+
+ SlackIntegration.with_bot.find_by_team_id(slack_workspace_id)
+ end
end
end
end
end
-
-Gitlab::SlashCommands::IncidentManagement::IncidentCommand.prepend_mod
diff --git a/lib/gitlab/slash_commands/incident_management/incident_new.rb b/lib/gitlab/slash_commands/incident_management/incident_new.rb
index a43235bdeb6..b5c43873355 100644
--- a/lib/gitlab/slash_commands/incident_management/incident_new.rb
+++ b/lib/gitlab/slash_commands/incident_management/incident_new.rb
@@ -16,6 +16,14 @@ module Gitlab
text == 'incident declare'
end
+ def execute(_match)
+ response = ::Integrations::SlackInteractions::IncidentManagement::IncidentModalOpenedService
+ .new(slack_installation, current_user, params)
+ .execute
+
+ presenter.present(response.message)
+ end
+
private
def presenter
diff --git a/lib/gitlab/slash_commands/issue_new.rb b/lib/gitlab/slash_commands/issue_new.rb
index 508526ac500..dd2bf632e2c 100644
--- a/lib/gitlab/slash_commands/issue_new.rb
+++ b/lib/gitlab/slash_commands/issue_new.rb
@@ -37,7 +37,7 @@ module Gitlab
private
def create_issue(title:, description:)
- ::Issues::CreateService.new(container: project, current_user: current_user, params: { title: title, description: description }, spam_params: nil).execute
+ ::Issues::CreateService.new(container: project, current_user: current_user, params: { title: title, description: description }, perform_spam_check: false).execute
end
def presenter(issue)
diff --git a/lib/gitlab/spamcheck/client.rb b/lib/gitlab/spamcheck/client.rb
index 222dd54b7b4..0afaf46fa9b 100644
--- a/lib/gitlab/spamcheck/client.rb
+++ b/lib/gitlab/spamcheck/client.rb
@@ -58,6 +58,7 @@ module Gitlab
pb.title = spammable.spam_title || '' if pb.respond_to?(:title)
pb.description = spammable.spam_description || '' if pb.respond_to?(:description)
pb.text = spammable.spammable_text || '' if pb.respond_to?(:text)
+ pb.type = spammable.spammable_entity_type if pb.respond_to?(:type)
pb.created_at = convert_to_pb_timestamp(spammable.created_at) if spammable.created_at
pb.updated_at = convert_to_pb_timestamp(spammable.updated_at) if spammable.updated_at
pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb
index 6d2677175e6..a6ab36e6cd2 100644
--- a/lib/gitlab/template/finders/global_template_finder.rb
+++ b/lib/gitlab/template/finders/global_template_finder.rb
@@ -25,7 +25,7 @@ module Gitlab
# The key is untrusted input, so ensure we can't be directed outside
# of base_dir
- Gitlab::Utils.check_path_traversal!(file_name)
+ Gitlab::PathTraversal.check_path_traversal!(file_name)
directory = select_directory(file_name)
directory ? File.join(category_directory(directory), file_name) : nil
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index 9f0ba97bcdf..8343750e04a 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -29,7 +29,7 @@ module Gitlab
# The key is untrusted input, so ensure we can't be directed outside
# of base_dir inside the repository
- Gitlab::Utils.check_path_traversal!(file_name)
+ Gitlab::PathTraversal.check_path_traversal!(file_name)
directory = select_directory(file_name)
raise FileNotFoundError if directory.nil?
diff --git a/lib/gitlab/template/metrics_dashboard_template.rb b/lib/gitlab/template/metrics_dashboard_template.rb
deleted file mode 100644
index 469f97d7cb1..00000000000
--- a/lib/gitlab/template/metrics_dashboard_template.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Template
- class MetricsDashboardTemplate < BaseTemplate
- def description
- "# This file is a template, and might need editing before it works on your project."
- end
-
- class << self
- def extension
- '.metrics-dashboard.yml'
- end
-
- def categories
- {
- "General" => ''
- }
- end
-
- def base_dir
- Rails.root.join('lib/gitlab/metrics/templates')
- end
-
- def finder(project = nil)
- Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index 52aee4d2d45..f127e14243c 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -68,6 +68,14 @@ module Gitlab
false
end
+ def tracker
+ @tracker ||= if snowplow_micro_enabled?
+ Gitlab::Tracking::Destinations::SnowplowMicro.new
+ else
+ Gitlab::Tracking::Destinations::Snowplow.new
+ end
+ end
+
private
def track_struct_event(destination, category, action, label:, property:, value:, contexts:) # rubocop:disable Metrics/ParameterLists
@@ -76,14 +84,6 @@ module Gitlab
rescue StandardError => error
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error, snowplow_category: category, snowplow_action: action)
end
-
- def tracker
- @tracker ||= if snowplow_micro_enabled?
- Gitlab::Tracking::Destinations::SnowplowMicro.new
- else
- Gitlab::Tracking::Destinations::Snowplow.new
- end
- end
end
end
end
diff --git a/lib/gitlab/tracking/standard_context.rb b/lib/gitlab/tracking/standard_context.rb
index 62c45368410..61d6fdc6dca 100644
--- a/lib/gitlab/tracking/standard_context.rb
+++ b/lib/gitlab/tracking/standard_context.rb
@@ -3,7 +3,7 @@
module Gitlab
module Tracking
class StandardContext
- GITLAB_STANDARD_SCHEMA_URL = 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-8'
+ GITLAB_STANDARD_SCHEMA_URL = 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-9'
GITLAB_RAILS_SOURCE = 'gitlab-rails'
def initialize(namespace_id: nil, plan_name: nil, project_id: nil, user_id: nil, **extra)
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_all_ci_builds_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_all_ci_builds_metric.rb
new file mode 100644
index 00000000000..2a3cbaf5d03
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_all_ci_builds_metric.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountAllCiBuildsMetric < DatabaseMetric
+ operation :count
+
+ relation { ::Ci::Build }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric.rb
new file mode 100644
index 00000000000..97813fbb5c0
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountDeploymentsMetric < DatabaseMetric
+ operation :count
+
+ start { Deployment.minimum(:id) }
+ finish { Deployment.maximum(:id) }
+
+ def initialize(metric_definition)
+ super
+
+ raise ArgumentError, 'Missing Deployment type' unless type
+ raise ArgumentError, "Invalid Deployment type: #{type}" unless type.in?(%i[all success failed])
+ end
+
+ private
+
+ def type
+ options[:type].to_sym
+ end
+
+ def relation
+ @metric_relation = case type
+ when :all
+ Deployment
+ when :success
+ Deployment.success
+ when :failed
+ Deployment.failed
+ end.where(time_constraints)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric.rb
index d485e8b4f72..05e29f2d885 100644
--- a/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric.rb
@@ -23,7 +23,7 @@ module Gitlab
::Project
.select(:id)
.where(Project.arel_table[:created_at].gteq(start)) # rubocop:disable UsageData/LargeTable
- .order(created_at: :asc).limit(1).first&.id
+ .order(created_at: :asc).order(id: :asc).limit(1).first&.id
end
end
end
@@ -36,7 +36,7 @@ module Gitlab
::Project
.select(:id)
.where(Project.arel_table[:created_at].lteq(finish)) # rubocop:disable UsageData/LargeTable
- .order(created_at: :desc).limit(1).first&.id
+ .order(created_at: :desc).order(id: :desc).limit(1).first&.id
end
end
end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_personal_snippets_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_personal_snippets_metric.rb
new file mode 100644
index 00000000000..9a34c535676
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_personal_snippets_metric.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountPersonalSnippetsMetric < DatabaseMetric
+ operation :count
+
+ relation do
+ PersonalSnippet
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_project_snippets_metric.rb
index c2ca62f9eba..af25a32592c 100644
--- a/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/count_project_snippets_metric.rb
@@ -4,9 +4,11 @@ module Gitlab
module Usage
module Metrics
module Instrumentations
- class InstallationCreationDateMetric < GenericMetric
- value do
- User.where(id: 1).pick(:created_at)
+ class CountProjectSnippetsMetric < DatabaseMetric
+ operation :count
+
+ relation do
+ ProjectSnippet
end
end
end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_projects_with_alerts_created_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_projects_with_alerts_created_metric.rb
new file mode 100644
index 00000000000..8ae4000b802
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_projects_with_alerts_created_metric.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountProjectsWithAlertsCreatedMetric < DatabaseMetric
+ operation :distinct_count, column: :project_id
+
+ relation do
+ ::AlertManagement::Alert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_snippets_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_snippets_metric.rb
new file mode 100644
index 00000000000..342ba802fd8
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_snippets_metric.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountSnippetsMetric < DatabaseMetric
+ operation :count
+ # Relation and operation are not used, but are included to satisfy expectations
+ # of other metric generation logic.
+ relation { Snippet }
+
+ def value
+ count(project_snippet_relation) + count(personal_snippet_relation)
+ end
+
+ def project_snippet_relation
+ ProjectSnippet.where(time_constraints)
+ end
+
+ def personal_snippet_relation
+ PersonalSnippet.where(time_constraints)
+ end
+
+ def to_sql
+ project_snippet_relation_sql = Gitlab::Usage::Metrics::Query.for(:count, project_snippet_relation)
+ personal_snippet_relation_sql = Gitlab::Usage::Metrics::Query.for(:count, personal_snippet_relation)
+
+ "SELECT (#{project_snippet_relation_sql}) + (#{personal_snippet_relation_sql})"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 846bb934a3d..72168bce782 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -42,14 +42,11 @@ module Gitlab
clear_memoized
with_finished_at(:recording_ce_finished_at) do
- usage_data_metrics
+ { recorded_at: recorded_at }
+ .merge(usage_data_metrics)
end
end
- def license_usage_data
- { recorded_at: recorded_at }
- end
-
def recorded_at
@recorded_at ||= Time.current
end
@@ -70,9 +67,6 @@ module Gitlab
auto_devops_disabled: count(::ProjectAutoDevops.disabled),
deploy_keys: count(DeployKey),
# rubocop: disable UsageData/LargeTable:
- deployments: deployment_count(Deployment),
- successful_deployments: deployment_count(Deployment.success),
- failed_deployments: deployment_count(Deployment.failed),
feature_flags: count(Operations::FeatureFlag),
# rubocop: enable UsageData/LargeTable:
environments: count(::Environment),
@@ -101,7 +95,7 @@ module Gitlab
issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
issues_with_embedded_grafana_charts_approx: grafana_embed_usage_data,
issues_created_from_alerts: total_alert_issues,
- incident_issues: count(::Issue.incident, start: minimum_id(Issue), finish: maximum_id(Issue)),
+ incident_issues: count(::Issue.with_issue_type(:incident), start: minimum_id(Issue), finish: maximum_id(Issue)),
alert_bot_incident_issues: count(::Issue.authored(::User.alert_bot), start: minimum_id(Issue), finish: maximum_id(Issue)),
keys: count(Key),
label_lists: count(List.label),
@@ -113,11 +107,10 @@ module Gitlab
pages_domains: count(PagesDomain),
pool_repositories: count(PoolRepository),
projects: count(Project),
- projects_creating_incidents: distinct_count(Issue.incident, :project_id),
+ projects_creating_incidents: distinct_count(Issue.with_issue_type(:incident), :project_id),
projects_imported_from_github: count(Project.where(import_type: 'github')),
projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
- projects_with_alerts_created: distinct_count(::AlertManagement::Alert, :project_id),
projects_with_enabled_alert_integrations: distinct_count(::AlertManagement::HttpIntegration.active, :project_id),
projects_with_terraform_reports: distinct_count(::Ci::JobArtifact.of_report_type(:terraform), :project_id),
projects_with_terraform_states: distinct_count(::Terraform::State, :project_id),
@@ -125,8 +118,6 @@ module Gitlab
protected_branches_except_default: count(ProtectedBranch.where.not(name: ['main', 'master', Gitlab::CurrentSettings.default_branch_name])),
releases: count(Release),
remote_mirrors: count(RemoteMirror),
- personal_snippets: count(PersonalSnippet),
- project_snippets: count(ProjectSnippet),
suggestions: count(Suggestion),
terraform_reports: count(::Ci::JobArtifact.of_report_type(:terraform)),
terraform_states: count(::Terraform::State),
@@ -140,9 +131,7 @@ module Gitlab
integrations_usage,
user_preferences_usage,
service_desk_counts
- ).tap do |data|
- data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
- end
+ )
}
end
# rubocop: enable Metrics/AbcSize
@@ -150,19 +139,9 @@ module Gitlab
def system_usage_data_monthly
{
counts_monthly: {
- # rubocop: disable UsageData/LargeTable:
- deployments: deployment_count(Deployment.where(monthly_time_range_db_params)),
- successful_deployments: deployment_count(Deployment.success.where(monthly_time_range_db_params)),
- failed_deployments: deployment_count(Deployment.failed.where(monthly_time_range_db_params)),
- # rubocop: enable UsageData/LargeTable:
projects: count(Project.where(monthly_time_range_db_params), start: minimum_id(Project), finish: maximum_id(Project)),
- 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)
- }.tap do |data|
- data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
- end
+ packages: count(::Packages::Package.where(monthly_time_range_db_params))
+ }
}
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -372,7 +351,6 @@ module Gitlab
package: usage_activity_by_stage_package(time_period),
plan: usage_activity_by_stage_plan(time_period),
release: usage_activity_by_stage_release(time_period),
- secure: usage_activity_by_stage_secure(time_period),
verify: usage_activity_by_stage_verify(time_period)
}
}
@@ -450,8 +428,11 @@ module Gitlab
start: minimum_id(User),
finish: maximum_id(User)),
projects_with_error_tracking_enabled: distinct_count(::Project.with_enabled_error_tracking.where(time_period), :creator_id),
- projects_with_incidents: distinct_count(::Issue.incident.where(time_period), :project_id),
- projects_with_alert_incidents: distinct_count(::Issue.incident.with_alert_management_alerts.where(time_period), :project_id),
+ projects_with_incidents: distinct_count(::Issue.with_issue_type(:incident).where(time_period), :project_id),
+ # We are making an assumption here that all alert_management_alerts are associated with an issue of type
+ # incident. In reality this is very close to the truth and allows more efficient queries.
+ # More info in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121297#note_1416999956
+ projects_with_alert_incidents: distinct_count(::AlertManagement::Alert.where(time_period).where.not(issue_id: nil), :project_id),
projects_with_enabled_alert_integrations_histogram: integrations_histogram
}.compact
end
@@ -514,13 +495,6 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- # Currently too complicated and to get reliable counts for these stats:
- # container_scanning_jobs, dast_jobs, dependency_scanning_jobs, license_management_jobs, sast_jobs, secret_detection_jobs
- # Once https://gitlab.com/gitlab-org/gitlab/merge_requests/17568 is merged, this might be doable
- def usage_activity_by_stage_secure(time_period)
- {}
- end
-
def with_metadata
result = nil
error = nil
@@ -552,8 +526,7 @@ module Gitlab
end
def usage_data_metrics
- license_usage_data
- .merge(system_usage_data_license)
+ system_usage_data_license
.merge(system_usage_data_settings)
.merge(system_usage_data)
.merge(system_usage_data_monthly)
@@ -637,10 +610,6 @@ module Gitlab
omniauth_provider_names.reject { |name| name.starts_with?('ldap') }
end
- def deployment_count(relation)
- count relation, start: minimum_id(Deployment), finish: maximum_id(Deployment)
- end
-
def project_imports(time_period)
time_frame = metric_time_period(time_period)
counters = {
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index badcda1def0..eaa4bf15fe1 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -3,18 +3,14 @@
module Gitlab
module UsageDataCounters
module HLLRedisCounter
- DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH = 6.weeks
- DEFAULT_DAILY_KEY_EXPIRY_LENGTH = 29.days
+ KEY_EXPIRY_LENGTH = 6.weeks
REDIS_SLOT = 'hll_counters'
EventError = Class.new(StandardError)
UnknownEvent = Class.new(EventError)
- UnknownAggregation = Class.new(EventError)
- AggregationMismatch = Class.new(EventError)
InvalidContext = Class.new(EventError)
KNOWN_EVENTS_PATH = File.expand_path('known_events/*.yml', __dir__)
- ALLOWED_AGGREGATIONS = %i(daily weekly).freeze
# Track event on entity_id
# Increment a Redis HLL counter for unique event_name and entity_id
@@ -24,7 +20,6 @@ module Gitlab
# Event example:
#
# - name: g_compliance_dashboard # Unique event name
- # aggregation: weekly # Aggregation level, keys are stored weekly
#
# Usage:
#
@@ -63,8 +58,7 @@ module Gitlab
# end_date - The end date of the time range.
# context - Event context, plan level tracking. Available if set when tracking.
def unique_events(event_names:, start_date:, end_date:, context: '')
- count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date, context: context) do |events|
- raise AggregationMismatch, events unless events_same_aggregation?(events)
+ count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date, context: context) do
raise InvalidContext if context.present? && !context.in?(valid_context_list)
end
end
@@ -78,9 +72,7 @@ module Gitlab
end
def calculate_events_union(event_names:, start_date:, end_date:)
- count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date) do |events|
- raise AggregationMismatch, events unless events_same_aggregation?(events)
- end
+ count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date)
end
private
@@ -94,12 +86,7 @@ module Gitlab
return if event.blank?
return unless Feature.enabled?(:redis_hll_tracking, type: :ops)
- if event[:aggregation].to_sym == :daily
- weekly_event = event.dup.tap { |e| e['aggregation'] = 'weekly' }
- Gitlab::Redis::HLL.add(key: redis_key(weekly_event, time, context), value: values, expiry: expiry(weekly_event))
- end
-
- Gitlab::Redis::HLL.add(key: redis_key(event, time, context), value: values, expiry: expiry(event))
+ Gitlab::Redis::HLL.add(key: redis_key(event, time, context), value: values, expiry: KEY_EXPIRY_LENGTH)
rescue StandardError => e
# Ignore any exceptions unless is dev or test env
@@ -117,25 +104,18 @@ module Gitlab
yield events if block_given?
- aggregation = events.first[:aggregation]
-
- if Feature.disabled?(:revert_daily_hll_events_to_weekly_aggregation)
- aggregation = 'weekly'
- events = events.map { |e| e.merge(aggregation: 'weekly') }
- end
+ keys = keys_for_aggregation(events: events, start_date: start_date, end_date: end_date, context: context)
- keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date, context: context)
return FALLBACK unless keys.any?
redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) }
end
- def keys_for_aggregation(aggregation, events:, start_date:, end_date:, context: '')
- if aggregation.to_sym == :daily
- daily_redis_keys(events: events, start_date: start_date, end_date: end_date, context: context)
- else
- weekly_redis_keys(events: events, start_date: start_date, end_date: end_date, context: context)
- end
+ def keys_for_aggregation(events:, start_date:, end_date:, context: '')
+ end_date = end_date.end_of_week - 1.week
+ (start_date.to_date..end_date.to_date).map do |date|
+ events.map { |event| redis_key(event, date, context) }
+ end.flatten.uniq
end
def load_events(wildcard)
@@ -152,15 +132,6 @@ module Gitlab
known_events.map { |event| event[:name] }
end
- def events_same_aggregation?(events)
- aggregation = events.first[:aggregation]
- events.all? { |event| event[:aggregation] == aggregation }
- end
-
- def expiry(event)
- event[:aggregation].to_sym == :daily ? DEFAULT_DAILY_KEY_EXPIRY_LENGTH : DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH
- end
-
def event_for(event_name)
known_events.find { |event| event[:name] == event_name.to_s }
end
@@ -173,36 +144,13 @@ module Gitlab
def redis_key(event, time, context = '')
raise UnknownEvent, "Unknown event #{event[:name]}" unless known_events_names.include?(event[:name].to_s)
- # ToDo: remove during https://gitlab.com/groups/gitlab-org/-/epics/9542 cleanup
- raise UnknownAggregation, "Use :daily or :weekly aggregation" unless ALLOWED_AGGREGATIONS.include?(event[:aggregation].to_sym)
-
key = "{#{REDIS_SLOT}}_#{event[:name]}"
- key = apply_time_aggregation(key, time, event)
- key = "#{context}_#{key}" if context.present?
- key
- end
- def apply_time_aggregation(key, time, event)
- if event[:aggregation].to_sym == :daily
- year_day = time.strftime('%G-%j')
- "#{year_day}-#{key}"
- else
- year_week = time.strftime('%G-%V')
- "#{key}-#{year_week}"
- end
- end
+ year_week = time.strftime('%G-%V')
+ key = "#{key}-#{year_week}"
- def daily_redis_keys(events:, start_date:, end_date:, context: '')
- (start_date.to_date..end_date.to_date).map do |date|
- events.map { |event| redis_key(event, date, context) }
- end.flatten
- end
-
- def weekly_redis_keys(events:, start_date:, end_date:, context: '')
- end_date = end_date.end_of_week - 1.week
- (start_date.to_date..end_date.to_date).map do |date|
- events.map { |event| redis_key(event, date, context) }
- end.flatten.uniq
+ key = "#{context}_#{key}" if context.present?
+ key
end
end
end
diff --git a/lib/gitlab/usage_data_counters/jetbrains_bundled_plugin_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/jetbrains_bundled_plugin_activity_unique_counter.rb
new file mode 100644
index 00000000000..a9e8d9bf0cb
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/jetbrains_bundled_plugin_activity_unique_counter.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ module JetBrainsBundledPluginActivityUniqueCounter
+ JETBRAINS_BUNDLED_API_REQUEST_ACTION = 'i_editor_extensions_user_jetbrains_bundled_api_request'
+ JETBRAINS_BUNDLED_USER_AGENT_REGEX = /\AIntelliJ-GitLab-Plugin/
+
+ class << self
+ def track_api_request_when_trackable(user_agent:, user:)
+ user_agent&.match?(JETBRAINS_BUNDLED_USER_AGENT_REGEX) &&
+ track_unique_action_by_user(JETBRAINS_BUNDLED_API_REQUEST_ACTION, user)
+ end
+
+ private
+
+ def track_unique_action_by_user(action, user)
+ return unless user
+
+ track_unique_action(action, user.id)
+ end
+
+ def track_unique_action(action, value)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_usage_event(action, value)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
index f685f0d65d9..c3e1c34151b 100644
--- a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
+++ b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
@@ -307,3 +307,5 @@
aggregation: weekly
- name: p_ci_templates_terraform_module
aggregation: weekly
+- name: p_ci_templates_pages_zola
+ aggregation: weekly
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 db0c0653f63..bd8c79f4801 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
@@ -79,6 +79,8 @@
aggregation: weekly
- name: i_code_review_user_jetbrains_api_request
aggregation: weekly
+- name: i_editor_extensions_user_jetbrains_bundled_api_request
+ aggregation: weekly
- name: i_code_review_user_gitlab_cli_api_request
aggregation: weekly
- name: i_code_review_user_create_mr_from_issue
diff --git a/lib/gitlab/usage_data_counters/known_events/product_analytics.yml b/lib/gitlab/usage_data_counters/known_events/product_analytics.yml
index 5a791c4b3c2..c43bf9040dd 100644
--- a/lib/gitlab/usage_data_counters/known_events/product_analytics.yml
+++ b/lib/gitlab/usage_data_counters/known_events/product_analytics.yml
@@ -2,3 +2,7 @@
aggregation: weekly
- name: project_initialized_product_analytics
aggregation: weekly
+- name: user_created_analytics_dashboard
+ aggregation: weekly
+- name: user_visited_dashboard
+ aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/known_events/quickactions.yml b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
index 136d284f462..69f92ac5c0a 100644
--- a/lib/gitlab/usage_data_counters/known_events/quickactions.yml
+++ b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
@@ -133,3 +133,7 @@
aggregation: weekly
- name: i_quickactions_blocks
aggregation: weekly
+- name: i_quickactions_unlink
+ aggregation: weekly
+- name: i_quickactions_promote_to
+ aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/known_events/workspaces.yml b/lib/gitlab/usage_data_counters/known_events/workspaces.yml
new file mode 100644
index 00000000000..8a96524b167
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/known_events/workspaces.yml
@@ -0,0 +1,5 @@
+- name: users_updating_workspaces
+ aggregation: weekly
+
+- name: users_creating_workspaces
+ aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb
index d6e05f30a0d..ece2ffea83b 100644
--- a/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb
+++ b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb
@@ -4,7 +4,7 @@ module Gitlab
module UsageDataCounters
class KubernetesAgentCounter < BaseCounter
PREFIX = 'kubernetes_agent'
- KNOWN_EVENTS = %w[gitops_sync k8s_api_proxy_request].freeze
+ KNOWN_EVENTS = %w[gitops_sync k8s_api_proxy_request flux_git_push_notifications_total].freeze
class << self
def increment_event_counts(events)
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 fceeacb60ca..1ed2e891a1f 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
@@ -64,20 +64,15 @@ module Gitlab
end
def track_create_mr_action(user:, merge_request:)
- track_unique_action_by_user(MR_USER_CREATE_ACTION, user)
track_unique_action_by_merge_request(MR_CREATE_ACTION, merge_request)
project = merge_request.target_project
- Gitlab::Tracking.event(
- name,
- :create,
- project: project,
- namespace: project.namespace,
- user: user,
- property: MR_USER_CREATE_ACTION,
- label: 'redis_hll_counters.code_review.i_code_review_user_create_mr_monthly',
- context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
- event: MR_USER_CREATE_ACTION).to_context]
+
+ Gitlab::InternalEvents.track_event(
+ MR_USER_CREATE_ACTION,
+ user_id: user.id,
+ project_id: project.id,
+ namespace_id: project.namespace_id
)
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index b92e7dbb725..dc0112c14d6 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -3,34 +3,8 @@
module Gitlab
module Utils
extend self
- PathTraversalAttackError ||= Class.new(StandardError)
DoubleEncodingError ||= Class.new(StandardError)
- private_class_method def logger
- @logger ||= Gitlab::AppLogger
- end
-
- # Ensure that the relative path will not traverse outside the base directory
- # We url decode the path to avoid passing invalid paths forward in url encoded format.
- # Also see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24223#note_284122580
- # It also checks for ALT_SEPARATOR aka '\' (forward slash)
- def check_path_traversal!(path)
- return unless path
-
- path = path.to_s if path.is_a?(Gitlab::HashedPath)
- raise PathTraversalAttackError, 'Invalid path' unless path.is_a?(String)
-
- path = decode_path(path)
- path_regex = %r{(\A(\.{1,2})\z|\A\.\.[/\\]|[/\\]\.\.\z|[/\\]\.\.[/\\]|\n)}
-
- if path.match?(path_regex)
- logger.warn(message: "Potential path traversal attempt detected", path: "#{path}")
- raise PathTraversalAttackError, 'Invalid path'
- end
-
- path
- end
-
def allowlisted?(absolute_path, allowlist)
path = absolute_path.downcase
@@ -39,20 +13,6 @@ module Gitlab
end
end
- def check_allowed_absolute_path!(path, allowlist)
- return unless Pathname.new(path).absolute?
- return if allowlisted?(path, allowlist)
-
- raise StandardError, "path #{path} is not allowed"
- end
-
- def check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
- traversal_path = check_path_traversal!(path)
- raise StandardError, "path is not a string!" unless traversal_path.is_a?(String)
-
- check_allowed_absolute_path!(traversal_path, path_allowlist)
- end
-
def decode_path(encoded_path)
decoded = CGI.unescape(encoded_path)
if decoded != CGI.unescape(decoded)
@@ -103,12 +63,6 @@ module Gitlab
.gsub(/(\A-+|-+\z)/, '')
end
- # Wraps ActiveSupport's Array#to_sentence to convert the given array to a
- # comma-separated sentence joined with localized 'or' Strings instead of 'and'.
- def to_exclusive_sentence(array)
- array.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or '))
- end
-
# Converts newlines into HTML line break elements
def nlbr(str)
ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe
diff --git a/lib/gitlab/utils/markdown.rb b/lib/gitlab/utils/markdown.rb
index 5087020affe..c95398a15df 100644
--- a/lib/gitlab/utils/markdown.rb
+++ b/lib/gitlab/utils/markdown.rb
@@ -4,7 +4,7 @@ module Gitlab
module Utils
module Markdown
PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze
- PRODUCT_SUFFIX = /\s*\**\((core|starter|premium|ultimate|free|bronze|silver|gold)(\s+(only|self|sass))?\)\**/.freeze
+ PRODUCT_SUFFIX = /\s*\**\((core|starter|premium|ultimate|free|bronze|silver|gold)(\s+(only|self|saas))?\)\**/.freeze
def string_to_anchor(string)
string
diff --git a/lib/gitlab/utils/sanitize_node_link.rb b/lib/gitlab/utils/sanitize_node_link.rb
index 9c34302f75e..183741ff5f5 100644
--- a/lib/gitlab/utils/sanitize_node_link.rb
+++ b/lib/gitlab/utils/sanitize_node_link.rb
@@ -51,7 +51,9 @@ module Gitlab
begin
node[attr] = node[attr].strip
+
uri = Addressable::URI.parse(node[attr])
+ uri = uri.normalize
next unless uri.scheme
next if safe_protocol?(uri.scheme)
diff --git a/lib/gitlab/verify/ci_secure_files.rb b/lib/gitlab/verify/ci_secure_files.rb
new file mode 100644
index 00000000000..9bb7f7260c4
--- /dev/null
+++ b/lib/gitlab/verify/ci_secure_files.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Verify
+ class CiSecureFiles < BatchVerifier
+ def name
+ 'CI Secure Files'
+ end
+
+ def describe(object)
+ "SecureFile: #{object.id}"
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def all_relation
+ ::Ci::SecureFile.all
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def local?(secure_file)
+ secure_file.local?
+ end
+
+ def expected_checksum(secure_file)
+ secure_file.checksum
+ end
+
+ def actual_checksum(secure_file)
+ Digest::SHA256.hexdigest(secure_file.file.read)
+ end
+
+ def remote_object_exists?(secure_file)
+ secure_file.file.file.exists?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/x509/tag.rb b/lib/gitlab/x509/tag.rb
index cf24e6f62bd..b9a3b11ac34 100644
--- a/lib/gitlab/x509/tag.rb
+++ b/lib/gitlab/x509/tag.rb
@@ -11,7 +11,7 @@ module Gitlab
strong_memoize(:signature) do
super
- signature = X509::Signature.new(signature_text, signed_text, @tag.tagger.email, Time.at(@tag.tagger.date.seconds))
+ signature = X509::Signature.new(signature_text, signed_text, @tag.user_email, @tag.date)
signature unless signature.verified_signature.nil?
end
end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index 5eef4fd0e4e..b3346d14ada 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -123,6 +123,10 @@ module GoogleApi
enable_service(gcp_project_id, 'servicenetworking.googleapis.com')
end
+ def enable_vision_api(gcp_project_id)
+ enable_service(gcp_project_id, 'vision.googleapis.com')
+ end
+
def revoke_authorizations
uri = URI(REVOKE_URL)
Gitlab::HTTP.post(uri, body: { 'token' => access_token })
diff --git a/lib/google_cloud/authentication.rb b/lib/google_cloud/authentication.rb
new file mode 100644
index 00000000000..68dd0bdcccb
--- /dev/null
+++ b/lib/google_cloud/authentication.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module GoogleCloud
+ class Authentication
+ def initialize(scope:)
+ @scope = scope
+ end
+
+ def generate_access_token(client_email, private_key)
+ credentials = Google::Auth::ServiceAccountCredentials.make_creds(
+ json_key_io: StringIO.new({ client_email: client_email, private_key: private_key }.to_json),
+ scope: @scope
+ )
+ credentials.fetch_access_token!["access_token"]
+ rescue StandardError => e
+ ::Gitlab::ErrorTracking.track_exception(e, client_email: client_email)
+ nil
+ end
+ end
+end
diff --git a/lib/google_cloud/logging_service/logger.rb b/lib/google_cloud/logging_service/logger.rb
new file mode 100644
index 00000000000..2c6dd6ea732
--- /dev/null
+++ b/lib/google_cloud/logging_service/logger.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module GoogleCloud
+ module LoggingService
+ class Logger
+ WRITE_URL = "https://logging.googleapis.com/v2/entries:write"
+ SCOPE = "https://www.googleapis.com/auth/logging.write"
+
+ def initialize
+ @auth = GoogleCloud::Authentication.new(scope: SCOPE)
+ end
+
+ def log(client_email, private_key, payload)
+ access_token = @auth.generate_access_token(client_email, private_key)
+
+ return unless access_token
+
+ headers = build_headers(access_token)
+
+ post(WRITE_URL, body: payload, headers: headers)
+ end
+
+ private
+
+ def build_headers(access_token)
+ { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
+ end
+
+ def post(url, body:, headers:)
+ Gitlab::HTTP.post(
+ url,
+ body: body,
+ headers: headers
+ )
+ rescue URI::InvalidURIError => e
+ Gitlab::ErrorTracking.log_exception(e)
+ rescue *Gitlab::HTTP::HTTP_ERRORS
+ end
+ end
+ end
+end
diff --git a/lib/grafana/validator.rb b/lib/grafana/validator.rb
index 760263f7ec9..616bb6aaaf0 100644
--- a/lib/grafana/validator.rb
+++ b/lib/grafana/validator.rb
@@ -42,7 +42,6 @@ module Grafana
private
- # See defaults in Banzai::Filter::InlineGrafanaMetricsFilter.
def validate_query_params!
return if [:from, :to].all? { |param| query_params.include?(param) }
diff --git a/lib/kramdown/parser/atlassian_document_format.rb b/lib/kramdown/parser/atlassian_document_format.rb
index d27697a59a6..5a481042b15 100644
--- a/lib/kramdown/parser/atlassian_document_format.rb
+++ b/lib/kramdown/parser/atlassian_document_format.rb
@@ -219,8 +219,8 @@ module Kramdown
# opportunity to replace it later. Mention name can have
# spaces, so double quote it
mention_text = ast_node.dig('attrs', 'text')&.gsub('@', '')
- mention_text = %Q("#{mention_text}") if mention_text&.include?(' ')
- mention_text = %Q(@adf-mention:#{mention_text})
+ mention_text = %("#{mention_text}") if mention_text&.include?(' ')
+ mention_text = %(@adf-mention:#{mention_text})
add_text(mention_text, element, :text)
end
diff --git a/lib/object_storage/fog_helpers.rb b/lib/object_storage/fog_helpers.rb
new file mode 100644
index 00000000000..1db75ea24b9
--- /dev/null
+++ b/lib/object_storage/fog_helpers.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module ObjectStorage
+ module FogHelpers
+ include ::Gitlab::Utils::StrongMemoize
+
+ def available?
+ object_store.enabled
+ end
+
+ private
+
+ def delete_object(key)
+ return unless available?
+
+ connection.delete_object(bucket_name, object_key(key))
+
+ # So far, only GoogleCloudStorage raises an exception when the file is not found.
+ # Other providers support idempotent requests and does not raise an error
+ # when the file is missing.
+ rescue ::Google::Apis::ClientError => e
+ Gitlab::ErrorTracking.log_exception(e)
+ end
+
+ def storage_location_identifier
+ raise NotImplementedError, "#{self} does not implement #{__method__}"
+ end
+
+ def object_store
+ ObjectStorage::Config::LOCATIONS.fetch(storage_location_identifier).object_store
+ end
+
+ def bucket_name
+ object_store.remote_directory
+ end
+
+ def object_key(key)
+ # We allow administrators to create "sub buckets" by setting a prefix.
+ # This makes it possible to deploy GitLab with only one object storage
+ # bucket. This mirrors the implementation in app/uploaders/object_storage.rb.
+ File.join([object_store.bucket_prefix, key].compact)
+ end
+
+ def connection
+ return unless available?
+
+ ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
+ end
+ strong_memoize_attr :connection
+ end
+end
diff --git a/lib/object_storage/pending_direct_upload.rb b/lib/object_storage/pending_direct_upload.rb
index 3e84bc4ebc9..3a930e0e0af 100644
--- a/lib/object_storage/pending_direct_upload.rb
+++ b/lib/object_storage/pending_direct_upload.rb
@@ -2,31 +2,97 @@
module ObjectStorage
class PendingDirectUpload
+ include ObjectStorage::FogHelpers
+
KEY = 'pending_direct_uploads'
+ MAX_UPLOAD_DURATION = 3.hours.freeze
- def self.prepare(location_identifier, path)
- ::Gitlab::Redis::SharedState.with do |redis|
+ def self.prepare(location_identifier, object_storage_path)
+ with_redis do |redis|
# We need to store the location_identifier together with the timestamp to properly delete
# this object if ever this upload gets stale. The location identifier will be used
# by the clean up worker to properly generate the storage options through ObjectStorage::Config.for_location
- redis.hset(KEY, key(location_identifier, path), Time.current.utc.to_i)
+ key = redis_key(location_identifier, object_storage_path)
+ redis.hset(KEY, key, Time.current.utc.to_i)
+ log_event(:prepared, key)
+ end
+ end
+
+ def self.exists?(location_identifier, object_storage_path)
+ with_redis do |redis|
+ redis.hexists(KEY, redis_key(location_identifier, object_storage_path))
+ end
+ end
+
+ def self.complete(location_identifier, object_storage_path)
+ with_redis do |redis|
+ key = redis_key(location_identifier, object_storage_path)
+ redis.hdel(KEY, key)
+ log_event(:completed, key)
end
end
- def self.exists?(location_identifier, path)
- ::Gitlab::Redis::SharedState.with do |redis|
- redis.hexists(KEY, key(location_identifier, path))
+ def self.redis_key(location_identifier, object_storage_path)
+ [location_identifier, object_storage_path].join(':')
+ end
+
+ def self.count
+ with_redis do |redis|
+ redis.hlen(KEY)
end
end
- def self.complete(location_identifier, path)
- ::Gitlab::Redis::SharedState.with do |redis|
- redis.hdel(KEY, key(location_identifier, path))
+ def self.each
+ with_redis do |redis|
+ redis.hscan_each(KEY) do |entry|
+ redis_key, timestamp = entry
+ storage_location_identifier, object_storage_path = redis_key.split(':')
+
+ object = new(
+ redis_key: redis_key,
+ storage_location_identifier: storage_location_identifier,
+ object_storage_path: object_storage_path,
+ timestamp: timestamp
+ )
+
+ yield(object)
+ end
end
end
- def self.key(location_identifier, path)
- [location_identifier, path].join(':')
+ def self.with_redis(&block)
+ Gitlab::Redis::SharedState.with(&block) # rubocop:disable CodeReuse/ActiveRecord
end
+
+ def self.log_event(event, redis_key)
+ Gitlab::AppLogger.info(
+ message: "Pending direct upload #{event}",
+ redis_key: redis_key
+ )
+ end
+
+ def initialize(redis_key:, storage_location_identifier:, object_storage_path:, timestamp:)
+ @redis_key = redis_key
+ @storage_location_identifier = storage_location_identifier.to_sym
+ @object_storage_path = object_storage_path
+ @timestamp = timestamp.to_i
+ end
+
+ def stale?
+ timestamp < MAX_UPLOAD_DURATION.ago.utc.to_i
+ end
+
+ def delete
+ delete_object(object_storage_path)
+
+ self.class.with_redis do |redis|
+ redis.hdel(self.class::KEY, redis_key)
+ self.class.log_event(:deleted, redis_key)
+ end
+ end
+
+ private
+
+ attr_reader :redis_key, :storage_location_identifier, :object_storage_path, :timestamp
end
end
diff --git a/lib/product_analytics/settings.rb b/lib/product_analytics/settings.rb
index 5d52965f5be..1c5f25c36b9 100644
--- a/lib/product_analytics/settings.rb
+++ b/lib/product_analytics/settings.rb
@@ -2,9 +2,16 @@
module ProductAnalytics
class Settings
- CONFIG_KEYS = (%w[jitsu_host jitsu_project_xid jitsu_administrator_email jitsu_administrator_password] +
- %w[product_analytics_data_collector_host product_analytics_clickhouse_connection_string] +
- %w[cube_api_base_url cube_api_key]).freeze
+ BASE_CONFIG_KEYS = %w[product_analytics_data_collector_host cube_api_base_url cube_api_key].freeze
+
+ JITSU_CONFIG_KEYS = (%w[jitsu_host jitsu_project_xid jitsu_administrator_email jitsu_administrator_password] +
+ %w[product_analytics_clickhouse_connection_string] + BASE_CONFIG_KEYS).freeze
+
+ SNOWPLOW_CONFIG_KEYS = (%w[product_analytics_configurator_connection_string] +
+ BASE_CONFIG_KEYS).freeze
+
+ ALL_CONFIG_KEYS = (ProductAnalytics::Settings::BASE_CONFIG_KEYS + ProductAnalytics::Settings::JITSU_CONFIG_KEYS +
+ ProductAnalytics::Settings::SNOWPLOW_CONFIG_KEYS).freeze
def initialize(project:)
@project = project
@@ -14,25 +21,39 @@ module ProductAnalytics
::Gitlab::CurrentSettings.product_analytics_enabled? && configured?
end
- # rubocop:disable GitlabSecurity/PublicSend
def configured?
- CONFIG_KEYS.all? do |key|
- @project.project_setting.public_send(key).present? ||
- ::Gitlab::CurrentSettings.public_send(key).present?
+ return configured_snowplow? if Feature.enabled?(:product_analytics_snowplow_support, @project)
+
+ JITSU_CONFIG_KEYS.all? do |key|
+ get_setting_value(key).present?
+ end
+ end
+
+ def configured_snowplow?
+ SNOWPLOW_CONFIG_KEYS.all? do |key|
+ get_setting_value(key).present?
end
end
- CONFIG_KEYS.each do |key|
+ ALL_CONFIG_KEYS.each do |key|
define_method key.to_sym do
- @project.project_setting.public_send(key).presence || ::Gitlab::CurrentSettings.public_send(key)
+ get_setting_value(key)
end
end
- # rubocop:enable GitlabSecurity/PublicSend
class << self
def for_project(project)
ProductAnalytics::Settings.new(project: project)
end
end
+
+ private
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ def get_setting_value(key)
+ @project.project_setting.public_send(key).presence ||
+ ::Gitlab::CurrentSettings.public_send(key)
+ end
+ # rubocop:enable GitlabSecurity/PublicSend
end
end
diff --git a/lib/quality/seeders/issues.rb b/lib/quality/seeders/issues.rb
index cac034767f6..fb3d78bc8d2 100644
--- a/lib/quality/seeders/issues.rb
+++ b/lib/quality/seeders/issues.rb
@@ -31,7 +31,7 @@ module Quality
}
params[:closed_at] = params[:created_at] + rand(35).days if params[:state] == 'closed'
- create_result = ::Issues::CreateService.new(container: project, current_user: team.sample, params: params, spam_params: nil).execute_without_rate_limiting
+ create_result = ::Issues::CreateService.new(container: project, current_user: team.sample, params: params, perform_spam_check: false).execute_without_rate_limiting
if create_result.success?
created_issues_count += 1
diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb
index 68c8e9675a7..bd1cca6473a 100644
--- a/lib/sidebars/groups/menus/packages_registries_menu.rb
+++ b/lib/sidebars/groups/menus/packages_registries_menu.rb
@@ -36,7 +36,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Package Registry'),
link: group_packages_path(context.group),
- super_sidebar_parent: ::Sidebars::Groups::SuperSidebarMenus::OperationsMenu,
+ super_sidebar_parent: ::Sidebars::Groups::SuperSidebarMenus::DeployMenu,
active_routes: { controller: 'groups/packages' },
item_id: :packages_registry
)
diff --git a/lib/sidebars/groups/super_sidebar_menus/deploy_menu.rb b/lib/sidebars/groups/super_sidebar_menus/deploy_menu.rb
new file mode 100644
index 00000000000..fe9cc5280c7
--- /dev/null
+++ b/lib/sidebars/groups/super_sidebar_menus/deploy_menu.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Groups
+ module SuperSidebarMenus
+ class DeployMenu < ::Sidebars::Menu
+ override :title
+ def title
+ s_('Navigation|Deploy')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'deployments'
+ end
+
+ override :configure_menu_items
+ def configure_menu_items
+ [
+ :packages_registry
+ ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb b/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb
index fe17ada69e4..e716801486e 100644
--- a/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb
+++ b/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb
@@ -11,14 +11,13 @@ module Sidebars
override :sprite_icon
def sprite_icon
- 'deployments'
+ 'cloud-pod'
end
override :configure_menu_items
def configure_menu_items
[
:dependency_proxy,
- :packages_registry,
:container_registry,
:group_kubernetes_clusters
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
diff --git a/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb b/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb
index c79e7e379d0..042e2381744 100644
--- a/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb
+++ b/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb
@@ -19,6 +19,7 @@ module Sidebars
[
:security_dashboard,
:vulnerability_report,
+ :dependency_list,
:audit_events,
:compliance,
:scan_policies
diff --git a/lib/sidebars/groups/super_sidebar_panel.rb b/lib/sidebars/groups/super_sidebar_panel.rb
index 03af904d99d..1f79dff398e 100644
--- a/lib/sidebars/groups/super_sidebar_panel.rb
+++ b/lib/sidebars/groups/super_sidebar_panel.rb
@@ -18,6 +18,7 @@ module Sidebars
add_menu(Sidebars::Groups::SuperSidebarMenus::CodeMenu.new(context))
add_menu(Sidebars::Groups::SuperSidebarMenus::BuildMenu.new(context))
add_menu(Sidebars::Groups::SuperSidebarMenus::SecureMenu.new(context))
+ add_menu(Sidebars::Groups::SuperSidebarMenus::DeployMenu.new(context))
add_menu(Sidebars::Groups::SuperSidebarMenus::OperationsMenu.new(context))
add_menu(Sidebars::Groups::SuperSidebarMenus::MonitorMenu.new(context))
add_menu(Sidebars::Groups::SuperSidebarMenus::AnalyzeMenu.new(context))
diff --git a/lib/sidebars/projects/menus/deployments_menu.rb b/lib/sidebars/projects/menus/deployments_menu.rb
index 19612fcee85..0a411f075b7 100644
--- a/lib/sidebars/projects/menus/deployments_menu.rb
+++ b/lib/sidebars/projects/menus/deployments_menu.rb
@@ -49,7 +49,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: s_('FeatureFlags|Feature flags'),
link: project_feature_flags_path(context.project),
- super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::BuildMenu,
+ super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::DeployMenu,
active_routes: { controller: :feature_flags },
container_html_options: { class: 'shortcuts-feature-flags' },
item_id: :feature_flags
@@ -64,7 +64,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Environments'),
link: project_environments_path(context.project),
- super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::BuildMenu,
+ super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::OperationsMenu,
active_routes: { controller: :environments },
container_html_options: { class: 'shortcuts-environments' },
item_id: :environments
@@ -80,7 +80,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Releases'),
link: project_releases_path(context.project),
- super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::BuildMenu,
+ super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::DeployMenu,
item_id: :releases,
active_routes: { controller: :releases },
container_html_options: { class: 'shortcuts-deployments-releases' }
diff --git a/lib/sidebars/projects/menus/monitor_menu.rb b/lib/sidebars/projects/menus/monitor_menu.rb
index f1fc9f70ef8..a74448d0bdc 100644
--- a/lib/sidebars/projects/menus/monitor_menu.rb
+++ b/lib/sidebars/projects/menus/monitor_menu.rb
@@ -8,7 +8,6 @@ module Sidebars
def configure_menu_items
return false unless feature_enabled?
- add_item(metrics_dashboard_menu_item)
add_item(error_tracking_menu_item)
add_item(alert_management_menu_item)
add_item(incidents_menu_item)
@@ -49,23 +48,6 @@ module Sidebars
context.project.feature_available?(:monitor, context.current_user)
end
- def metrics_dashboard_menu_item
- return ::Sidebars::NilMenuItem.new(item_id: :metrics) if Feature.enabled?(:remove_monitor_metrics)
-
- unless can?(context.current_user, :metrics_dashboard, context.project)
- return ::Sidebars::NilMenuItem.new(item_id: :metrics)
- end
-
- ::Sidebars::MenuItem.new(
- title: _('Metrics'),
- link: project_metrics_dashboard_path(context.project),
- super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::MonitorMenu,
- active_routes: { path: 'metrics_dashboard#show' },
- container_html_options: { class: 'shortcuts-metrics' },
- item_id: :metrics
- )
- end
-
def error_tracking_menu_item
unless can?(context.current_user, :read_sentry_issue, context.project)
return ::Sidebars::NilMenuItem.new(item_id: :error_tracking)
diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb
index 31a1aa56ab5..f41b7ce1a73 100644
--- a/lib/sidebars/projects/menus/packages_registries_menu.rb
+++ b/lib/sidebars/projects/menus/packages_registries_menu.rb
@@ -39,7 +39,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Package Registry'),
link: project_packages_path(context.project),
- super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::OperationsMenu,
+ super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::DeployMenu,
active_routes: { controller: :packages },
item_id: :packages_registry,
container_html_options: { class: 'shortcuts-container-registry' }
@@ -54,7 +54,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Container Registry'),
link: project_container_registry_index_path(context.project),
- super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::OperationsMenu,
+ super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::DeployMenu,
active_routes: { controller: 'projects/registry/repositories' },
item_id: :container_registry
)
@@ -91,14 +91,14 @@ module Sidebars
end
def model_experiments_menu_item
- if Feature.disabled?(:ml_experiment_tracking, context.project)
+ unless can?(context.current_user, :read_model_experiments, context.project)
return ::Sidebars::NilMenuItem.new(item_id: :model_experiments)
end
::Sidebars::MenuItem.new(
title: _('Model experiments'),
link: project_ml_experiments_path(context.project),
- super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu,
+ super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::DeployMenu,
active_routes: { controller: %w[projects/ml/experiments projects/ml/candidates] },
item_id: :model_experiments
)
diff --git a/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb b/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb
index 58b231a269c..2c5dc8a08e7 100644
--- a/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb
@@ -25,8 +25,7 @@ module Sidebars
:code_review,
:merge_request_analytics,
:issues,
- :insights,
- :model_experiments
+ :insights
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
end
end
diff --git a/lib/sidebars/projects/super_sidebar_menus/build_menu.rb b/lib/sidebars/projects/super_sidebar_menus/build_menu.rb
index 30603e1deeb..119ddf28873 100644
--- a/lib/sidebars/projects/super_sidebar_menus/build_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/build_menu.rb
@@ -20,10 +20,7 @@ module Sidebars
:pipelines,
:jobs,
:pipelines_editor,
- :releases,
- :environments,
:pipeline_schedules,
- :feature_flags,
:test_cases,
:artifacts
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
diff --git a/lib/sidebars/projects/super_sidebar_menus/deploy_menu.rb b/lib/sidebars/projects/super_sidebar_menus/deploy_menu.rb
new file mode 100644
index 00000000000..49aa6a23a0e
--- /dev/null
+++ b/lib/sidebars/projects/super_sidebar_menus/deploy_menu.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module SuperSidebarMenus
+ class DeployMenu < ::Sidebars::Menu
+ override :title
+ def title
+ s_('Navigation|Deploy')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'deployments'
+ end
+
+ override :configure_menu_items
+ def configure_menu_items
+ [
+ :releases,
+ :feature_flags,
+ :packages_registry,
+ :container_registry,
+ :model_experiments
+ ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
index fb56f6f3792..6e64ac01ffa 100644
--- a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
@@ -17,7 +17,6 @@ module Sidebars
override :configure_menu_items
def configure_menu_items
[
- :metrics,
:error_tracking,
:alert_management,
:incidents,
diff --git a/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb b/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb
index 64cf4aee9c5..85d60c0bad3 100644
--- a/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb
@@ -11,14 +11,13 @@ module Sidebars
override :sprite_icon
def sprite_icon
- 'deployments'
+ 'cloud-pod'
end
override :configure_menu_items
def configure_menu_items
[
- :packages_registry,
- :container_registry,
+ :environments,
:kubernetes,
:terraform_states,
:infrastructure_registry,
diff --git a/lib/sidebars/projects/super_sidebar_panel.rb b/lib/sidebars/projects/super_sidebar_panel.rb
index 640666fd968..e9de4c2d92c 100644
--- a/lib/sidebars/projects/super_sidebar_panel.rb
+++ b/lib/sidebars/projects/super_sidebar_panel.rb
@@ -18,6 +18,7 @@ module Sidebars
add_menu(Sidebars::Projects::SuperSidebarMenus::CodeMenu.new(context))
add_menu(Sidebars::Projects::SuperSidebarMenus::BuildMenu.new(context))
add_menu(Sidebars::Projects::SuperSidebarMenus::SecureMenu.new(context))
+ add_menu(Sidebars::Projects::SuperSidebarMenus::DeployMenu.new(context))
add_menu(Sidebars::Projects::SuperSidebarMenus::OperationsMenu.new(context))
add_menu(Sidebars::Projects::SuperSidebarMenus::MonitorMenu.new(context))
add_menu(Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu.new(context))
diff --git a/lib/sidebars/user_profile/panel.rb b/lib/sidebars/user_profile/panel.rb
index 9a595fdf64c..1852ef928f4 100644
--- a/lib/sidebars/user_profile/panel.rb
+++ b/lib/sidebars/user_profile/panel.rb
@@ -4,6 +4,9 @@ module Sidebars
module UserProfile
class Panel < ::Sidebars::Panel
include UsersHelper
+ include Gitlab::Allowable
+
+ delegate :current_user, to: :@context
override :configure_menus
def configure_menus
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 13365b9ec07..8c7e429ef24 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -7,24 +7,26 @@ namespace :cache do
desc "GitLab | Cache | Clear redis cache"
task redis: :environment do
- Gitlab::Redis::Cache.with do |redis|
- cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
- projects/*/pipeline_status]
+ [Gitlab::Redis::Cache, Gitlab::Redis::RepositoryCache].each do |redis_instance|
+ redis_instance.with do |redis|
+ cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
+ projects/*/pipeline_status]
- cache_key_pattern.each do |match|
- cursor = REDIS_SCAN_START_STOP
- loop do
- cursor, keys = redis.scan(
- cursor,
- match: match,
- count: REDIS_CLEAR_BATCH_SIZE
- )
+ cache_key_pattern.each do |match|
+ cursor = REDIS_SCAN_START_STOP
+ loop do
+ cursor, keys = redis.scan(
+ cursor,
+ match: match,
+ count: REDIS_CLEAR_BATCH_SIZE
+ )
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(*keys) if keys.any?
- end
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis) if keys.any?
+ end
- break if cursor == REDIS_SCAN_START_STOP
+ break if cursor == REDIS_SCAN_START_STOP
+ end
end
end
end
diff --git a/lib/tasks/frontend.rake b/lib/tasks/frontend.rake
index e768c42736d..a6754f13089 100644
--- a/lib/tasks/frontend.rake
+++ b/lib/tasks/frontend.rake
@@ -18,30 +18,12 @@ unless Rails.env.production?
t.rspec_opts = '--format documentation'
end
- desc 'GitLab | Frontend | Generate fixtures for JavaScript integration tests'
- RSpec::Core::RakeTask.new(:mock_server_rspec_fixtures) do |t, args|
- require 'yaml'
-
- base_path = Pathname.new('spec/frontend_integration/fixture_generators.yml')
- ee_path = Pathname.new('ee') + base_path
-
- fixtures = YAML.safe_load(base_path.read)
- fixtures.concat(Array(YAML.safe_load(ee_path.read))) if Gitlab.ee? && ee_path.exist?
-
- t.pattern = fixtures.join(',')
- ENV['NO_KNAPSACK'] = 'true'
- t.rspec_opts = '--format documentation'
- end
-
desc 'GitLab | Frontend | Run JavaScript tests'
task tests: ['yarn:check'] do
sh "yarn test" do |ok, res|
abort('rake frontend:tests failed') unless ok
end
end
-
- desc 'GitLab | Frontend | Shortcut for generating all fixtures used by MirajeJS mock server'
- task mock_server_fixtures: ['frontend:mock_server_rspec_fixtures', 'gitlab:graphql:schema:dump']
end
desc 'GitLab | Frontend | Shortcut for frontend:fixtures and frontend:tests'
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 2522488f579..b8a6e701876 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -15,6 +15,7 @@ module Tasks
yarn.lock
babel.config.js
config/webpack.config.js
+ .nvmrc
].freeze
# Ruby gems might emit assets which have an impact on compilation
# or have a direct impact on asset compilation (e.g. scss) and therefore
diff --git a/lib/tasks/gitlab/background_migrations.rake b/lib/tasks/gitlab/background_migrations.rake
index eca51c345d1..a4e14af22bd 100644
--- a/lib/tasks/gitlab/background_migrations.rake
+++ b/lib/tasks/gitlab/background_migrations.rake
@@ -6,7 +6,7 @@ namespace :gitlab do
namespace :background_migrations do
desc 'Synchronously finish executing a batched background migration'
task :finalize, [:job_class_name, :table_name, :column_name, :job_arguments] => :environment do |_, args|
- if Gitlab::Database.db_config_names.size > 1
+ if Gitlab::Database.db_config_names(with_schema: :gitlab_shared).size > 1
puts "Please specify the database".color(:red)
exit 1
end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 22e1d903c8d..4143200ece4 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -206,6 +206,16 @@ namespace :gitlab do
Tasks::Gitlab::Backup.restore_task('packages')
end
end
+
+ namespace :ci_secure_files do
+ task create: :gitlab_environment do
+ Tasks::Gitlab::Backup.create_task('ci_secure_files')
+ end
+
+ task restore: :gitlab_environment do
+ Tasks::Gitlab::Backup.restore_task('ci_secure_files')
+ end
+ end
end
# namespace end: backup
end
diff --git a/lib/tasks/gitlab/ci_secure_files/check.rake b/lib/tasks/gitlab/ci_secure_files/check.rake
new file mode 100644
index 00000000000..2c538a9365c
--- /dev/null
+++ b/lib/tasks/gitlab/ci_secure_files/check.rake
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :ci_secure_files do
+ desc 'GitLab | CI Secure Files | Check integrity of uploaded Secure Files'
+ task check: :environment do
+ Gitlab::Verify::RakeTask.run!(Gitlab::Verify::CiSecureFiles)
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/ci_secure_files/migrate.rake b/lib/tasks/gitlab/ci_secure_files/migrate.rake
new file mode 100644
index 00000000000..8de1b7da6be
--- /dev/null
+++ b/lib/tasks/gitlab/ci_secure_files/migrate.rake
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+desc "GitLab | CI Secure Files | Migrate Secure Files to remote storage"
+namespace :gitlab do
+ namespace :ci_secure_files do
+ task migrate: :environment do
+ require 'logger'
+
+ logger = Logger.new($stdout)
+ logger.info('Starting transfer of Secure Files to object storage')
+
+ begin
+ Gitlab::Ci::SecureFiles::MigrationHelper.migrate_to_remote_storage do |file|
+ message = "Transferred Secure File ID #{file.id} (#{file.name}) to object storage"
+
+ logger.info(message)
+ end
+ rescue StandardError => e
+ logger.error("Failed to migrate: #{e.message}")
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 34ccce3ba2f..026cb39a92f 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -473,7 +473,7 @@ namespace :gitlab do
Gitlab::Database::SchemaValidation::TrackInconsistency.new(
inconsistency,
Project.find_by_full_path(gitlab_url),
- User.support_bot
+ User.automation_bot
).execute
puts inconsistency.inspect
@@ -482,18 +482,12 @@ namespace :gitlab do
end
namespace :dictionary do
- DB_DOCS_PATH = Rails.root.join('db', 'docs')
-
desc 'Generate database docs yaml'
task generate: :environment do
next if Gitlab.jh?
- FileUtils.mkdir_p(DB_DOCS_PATH)
-
- if Gitlab.ee?
- Gitlab::Database::EE_DATABASES_NAME_TO_DIR.each do |_, ee_db_dir|
- FileUtils.mkdir_p(Rails.root.join(ee_db_dir, 'docs'))
- end
+ Gitlab::Database.all_database_connections.values.map(&:db_docs_dir).each do |db_dir|
+ FileUtils.mkdir_p(db_dir)
end
Rails.application.eager_load!
@@ -502,7 +496,6 @@ namespace :gitlab do
milestone = version.release.segments.first(2).join('.')
classes = {}
- ignored_tables = %w[p_ci_builds]
Gitlab::Database.database_base_models.each do |_, model_class|
tables = model_class.connection.tables
@@ -521,11 +514,10 @@ namespace :gitlab do
.reject { |c| c.name =~ /^(?:EE::)?Gitlab::(?:BackgroundMigration|DatabaseImporters)::/ }
.reject { |c| c.name =~ /^HABTM_/ }
.reject { |c| c < Gitlab::Database::Migration[1.0]::MigrationRecord }
- .each { |c| classes[c.table_name] << c.name if classes.has_key?(c.table_name) }
+ .each { |c| classes[c.table_name] << c.name if classes.has_key?(c.table_name) && c.name.present? }
sources.each do |source_name|
next if source_name.start_with?('_test_') # Ignore test tables
- next if ignored_tables.include?(source_name)
database = model_class.connection_db_config.name
file = dictionary_file_path(source_name, views, database)
@@ -574,12 +566,7 @@ namespace :gitlab do
def dictionary_file_path(source_name, views, database)
sub_directory = views.include?(source_name) ? 'views' : ''
- path = if Gitlab.ee? && Gitlab::Database::EE_DATABASES_NAME_TO_DIR.key?(database.to_s)
- Rails.root.join(Gitlab::Database::EE_DATABASES_NAME_TO_DIR[database.to_s], 'docs')
- else
- DB_DOCS_PATH
- end
-
+ path = Gitlab::Database.all_database_connections.fetch(database).db_docs_dir
File.join(path, sub_directory, "#{source_name}.yml")
end
diff --git a/lib/tasks/gitlab/packages/events.rake b/lib/tasks/gitlab/packages/events.rake
index 1234ba039a3..b5dfd163dba 100644
--- a/lib/tasks/gitlab/packages/events.rake
+++ b/lib/tasks/gitlab/packages/events.rake
@@ -45,10 +45,7 @@ namespace :gitlab do
events = event_pairs.each_with_object([]) do |(event_type, event_scope), events|
Packages::Event::ORIGINATOR_TYPES.excluding(:guest).each do |originator_type|
events_definition = Packages::Event.unique_counters_for(event_scope, event_type, originator_type).map do |event_name|
- {
- "name" => event_name,
- "aggregation" => "weekly"
- }
+ { "name" => event_name }
end
events.concat(events_definition)
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index b4b34581f43..afe2c564247 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -48,18 +48,18 @@ namespace :tw do
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
CodeOwnerRule.new('Geo', '@axil'),
CodeOwnerRule.new('Gitaly', '@eread'),
- CodeOwnerRule.new('GitLab Dedicated', '@drcatherinepope'),
+ # CodeOwnerRule.new('GitLab Dedicated', ''),
CodeOwnerRule.new('Global Search', '@ashrafkhamis'),
CodeOwnerRule.new('Import and Integrate', '@eread @ashrafkhamis'),
CodeOwnerRule.new('Infrastructure', '@sselhorn'),
# CodeOwnerRule.new('Knowledge', ''),
# CodeOwnerRule.new('MLOps', '')
- CodeOwnerRule.new('Observability', '@drcatherinepope'),
+ # CodeOwnerRule.new('Observability', ''),
CodeOwnerRule.new('Optimize', '@lciutacu'),
CodeOwnerRule.new('Organization', '@lciutacu'),
- CodeOwnerRule.new('Package Registry', '@marcel.amirault'),
+ CodeOwnerRule.new('Package Registry', '@phillipwells'),
CodeOwnerRule.new('Pipeline Authoring', '@marcel.amirault'),
- CodeOwnerRule.new('Pipeline Execution', '@drcatherinepope'),
+ CodeOwnerRule.new('Pipeline Execution', '@marcel.amirault'),
CodeOwnerRule.new('Pipeline Security', '@marcel.amirault'),
CodeOwnerRule.new('Product Analytics', '@lciutacu'),
CodeOwnerRule.new('Product Planning', '@msedlakjakubowski'),
@@ -71,7 +71,7 @@ namespace :tw do
CodeOwnerRule.new('Runner', '@fneill'),
CodeOwnerRule.new('Runner SaaS', '@fneill'),
CodeOwnerRule.new('Security Policies', '@rdickenson'),
- CodeOwnerRule.new('Source Code', '@aqualls'),
+ CodeOwnerRule.new('Source Code', '@aqualls @msedlakjakubowski'),
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
CodeOwnerRule.new('Style Guide', '@sselhorn'),
CodeOwnerRule.new('Tenant Scale', '@lciutacu'),
diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake
index fcbec4b0dba..f5bf1a266e5 100644
--- a/lib/tasks/gitlab/usage_data.rake
+++ b/lib/tasks/gitlab/usage_data.rake
@@ -85,12 +85,13 @@ namespace :gitlab do
end
end
+ # rubocop:disable Gitlab/NoCodeCoverageComment
+ # :nocov: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/299453
def ci_template_event(event_name)
- {
- 'name' => event_name,
- 'aggregation' => 'weekly'
- }
+ { 'name' => event_name }
end
+ # :nocov:
+ # rubocop:enable Gitlab/NoCodeCoverageComment
def implicit_auto_devops_event(expanded_template_name)
event_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_template_event_name(expanded_template_name, :auto_devops_source)
diff --git a/lib/tasks/tanuki_emoji.rake b/lib/tasks/tanuki_emoji.rake
index b02d7a532c4..de2ae656952 100644
--- a/lib/tasks/tanuki_emoji.rake
+++ b/lib/tasks/tanuki_emoji.rake
@@ -157,9 +157,9 @@ namespace :tanuki_emoji do
# SpriteFactory's SCSS is a bit too verbose for our purposes here, so
# let's simplify it
- system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
- system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
- system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path}))
+ system(%(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
+ system(%(sed -i '' "s/ no-repeat//" #{style_path}))
+ system(%(sed -i '' "s/ 0px/ 0/g" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index 81e24f4f7b6..c974aebf503 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -31,6 +31,11 @@ class TmpUser < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
self.table_name = 'users'
- add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
- add_authentication_token_field :feed_token
+ add_authentication_token_field :incoming_email_token,
+ token_generator: -> { User.generate_incoming_mail_token }
+ add_authentication_token_field :feed_token, format_with_prefix: :prefix_for_feed_token
+
+ def prefix_for_feed_token
+ User::FEED_TOKEN_PREFIX
+ end
end