summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /lib
parent4b1de649d0168371549608993deac953eb692019 (diff)
downloadgitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'lib')
-rw-r--r--lib/api/admin/instance_clusters.rb1
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/boards_responses.rb22
-rw-r--r--lib/api/ci/runner.rb4
-rw-r--r--lib/api/composer_packages.rb5
-rw-r--r--lib/api/conan_instance_packages.rb2
-rw-r--r--lib/api/conan_package_endpoints.rb351
-rw-r--r--lib/api/conan_project_packages.rb2
-rw-r--r--lib/api/concerns/packages/conan_endpoints.rb355
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb12
-rw-r--r--lib/api/concerns/packages/nuget_endpoints.rb135
-rw-r--r--lib/api/discussions.rb1
-rw-r--r--lib/api/entities/cluster.rb2
-rw-r--r--lib/api/entities/feature.rb10
-rw-r--r--lib/api/entities/feature_flag.rb4
-rw-r--r--lib/api/entities/issue.rb1
-rw-r--r--lib/api/entities/merge_request_basic.rb1
-rw-r--r--lib/api/entities/note.rb1
-rw-r--r--lib/api/entities/project.rb2
-rw-r--r--lib/api/entities/project_import_status.rb5
-rw-r--r--lib/api/entities/project_snippet.rb2
-rw-r--r--lib/api/entities/project_statistics.rb1
-rw-r--r--lib/api/entities/related_issue.rb2
-rw-r--r--lib/api/feature_flags.rb24
-rw-r--r--lib/api/feature_flags_user_lists.rb2
-rw-r--r--lib/api/features.rb28
-rwxr-xr-xlib/api/go_proxy.rb2
-rw-r--r--lib/api/group_boards.rb2
-rw-r--r--lib/api/group_clusters.rb2
-rw-r--r--lib/api/group_labels.rb2
-rw-r--r--lib/api/helpers.rb30
-rw-r--r--lib/api/helpers/internal_helpers.rb34
-rw-r--r--lib/api/helpers/members_helpers.rb2
-rw-r--r--lib/api/helpers/packages/basic_auth_helpers.rb4
-rw-r--r--lib/api/helpers/packages/conan/api_helpers.rb13
-rw-r--r--lib/api/helpers/projects_helpers.rb4
-rw-r--r--lib/api/helpers/services_helpers.rb61
-rw-r--r--lib/api/helpers/sse_helpers.rb16
-rw-r--r--lib/api/internal/base.rb4
-rw-r--r--lib/api/internal/kubernetes.rb6
-rw-r--r--lib/api/internal/pages.rb43
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/labels.rb4
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/merge_request_approvals.rb2
-rw-r--r--lib/api/merge_requests.rb3
-rw-r--r--lib/api/nuget_packages.rb247
-rw-r--r--lib/api/nuget_project_packages.rb139
-rw-r--r--lib/api/project_clusters.rb2
-rw-r--r--lib/api/project_repository_storage_moves.rb16
-rw-r--r--lib/api/pypi_packages.rb2
-rw-r--r--lib/api/release/links.rb2
-rw-r--r--lib/api/settings.rb6
-rw-r--r--lib/api/statistics.rb2
-rw-r--r--lib/api/usage_data.rb12
-rw-r--r--lib/api/users.rb18
-rw-r--r--lib/api/validations/validators/absence.rb2
-rw-r--r--lib/api/validations/validators/array_none_any.rb6
-rw-r--r--lib/api/validations/validators/check_assignees_count.rb7
-rw-r--r--lib/api/validations/validators/email_or_email_list.rb3
-rw-r--r--lib/api/validations/validators/file_path.rb6
-rw-r--r--lib/api/validations/validators/git_ref.rb6
-rw-r--r--lib/api/validations/validators/git_sha.rb6
-rw-r--r--lib/api/validations/validators/integer_none_any.rb12
-rw-r--r--lib/api/validations/validators/integer_or_custom_value.rb33
-rw-r--r--lib/api/validations/validators/limit.rb3
-rw-r--r--lib/api/validations/validators/untrusted_regexp.rb2
-rw-r--r--lib/atlassian/jira_connect/client.rb75
-rw-r--r--lib/atlassian/jira_connect/serializers/base_entity.rb6
-rw-r--r--lib/atlassian/jira_connect/serializers/build_entity.rb94
-rw-r--r--lib/backup/files.rb8
-rw-r--r--lib/banzai/filter/ascii_doc_sanitization_filter.rb37
-rw-r--r--lib/banzai/filter/base_sanitization_filter.rb2
-rw-r--r--lib/banzai/filter/kroki_filter.rb42
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb5
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/bulk_imports/common/extractors/graphql_extractor.rb27
-rw-r--r--lib/bulk_imports/common/transformers/graphql_cleaner_transformer.rb54
-rw-r--r--lib/bulk_imports/common/transformers/hash_key_digger.rb23
-rw-r--r--lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb39
-rw-r--r--lib/bulk_imports/groups/graphql/get_group_query.rb4
-rw-r--r--lib/bulk_imports/groups/pipelines/group_pipeline.rb5
-rw-r--r--lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb1
-rw-r--r--lib/bulk_imports/importers/group_importer.rb1
-rw-r--r--lib/bulk_imports/pipeline.rb81
-rw-r--r--lib/bulk_imports/pipeline/attributes.rb41
-rw-r--r--lib/bulk_imports/pipeline/runner.rb117
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/constraints/repository_redirect_url_constrainer.rb28
-rw-r--r--lib/extracts_ref.rb10
-rw-r--r--lib/feature.rb32
-rw-r--r--lib/feature/definition.rb35
-rw-r--r--lib/feature/logger.rb9
-rw-r--r--lib/feature/shared.rb16
-rw-r--r--lib/gitlab.rb6
-rw-r--r--lib/gitlab/alert_management/payload.rb3
-rw-r--r--lib/gitlab/analytics/cycle_analytics/default_stages.rb4
-rw-r--r--lib/gitlab/application_context.rb2
-rw-r--r--lib/gitlab/application_rate_limiter.rb3
-rw-r--r--lib/gitlab/asciidoc.rb21
-rw-r--r--lib/gitlab/asciidoc/html5_converter.rb6
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/auth/auth_finders.rb7
-rw-r--r--lib/gitlab/auth/crowd/authentication.rb35
-rw-r--r--lib/gitlab/auth/ldap/config.rb34
-rw-r--r--lib/gitlab/auth/ldap/user.rb14
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb2
-rw-r--r--lib/gitlab/auth/o_auth/user.rb19
-rw-r--r--lib/gitlab/auth/otp/fortinet.rb20
-rw-r--r--lib/gitlab/auth/otp/session_enforcer.rb36
-rw-r--r--lib/gitlab/auth/otp/strategies/base.rb4
-rw-r--r--lib/gitlab/auth/otp/strategies/forti_authenticator.rb7
-rw-r--r--lib/gitlab/auth/otp/strategies/forti_token_cloud.rb72
-rw-r--r--lib/gitlab/auth/request_authenticator.rb9
-rw-r--r--lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities.rb17
-rw-r--r--lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb5
-rw-r--r--lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb2
-rw-r--r--lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth.rb110
-rw-r--r--lib/gitlab/batch_pop_queueing.rb2
-rw-r--r--lib/gitlab/batch_worker_context.rb2
-rw-r--r--lib/gitlab/checks/diff_check.rb21
-rw-r--r--lib/gitlab/checks/push_check.rb2
-rw-r--r--lib/gitlab/checks/snippet_check.rb14
-rw-r--r--lib/gitlab/checks/timed_logger.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/converter.rb3
-rw-r--r--lib/gitlab/ci/build/rules.rb22
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb5
-rw-r--r--lib/gitlab/ci/config/entry/allow_failure.rb31
-rw-r--r--lib/gitlab/ci/config/entry/bridge.rb7
-rw-r--r--lib/gitlab/ci/config/entry/job.rb29
-rw-r--r--lib/gitlab/ci/config/entry/need.rb38
-rw-r--r--lib/gitlab/ci/config/entry/needs.rb11
-rw-r--r--lib/gitlab/ci/config/entry/processable.rb7
-rw-r--r--lib/gitlab/ci/config/entry/root.rb1
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb6
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb7
-rw-r--r--lib/gitlab/ci/features.rb16
-rw-r--r--lib/gitlab/ci/limit.rb34
-rw-r--r--lib/gitlab/ci/mask_secret.rb6
-rw-r--r--lib/gitlab/ci/parsers.rb3
-rw-r--r--lib/gitlab/ci/parsers/codequality/code_climate.rb29
-rw-r--r--lib/gitlab/ci/parsers/coverage/cobertura.rb118
-rw-r--r--lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/limit/deployments.rb39
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/seed.rb26
-rw-r--r--lib/gitlab/ci/pipeline/quota/deployments.rb50
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb22
-rw-r--r--lib/gitlab/ci/pipeline/seed/environment.rb4
-rw-r--r--lib/gitlab/ci/pipeline/seed/pipeline.rb51
-rw-r--r--lib/gitlab/ci/reports/accessibility_reports_comparer.rb33
-rw-r--r--lib/gitlab/ci/reports/codequality_reports.rb43
-rw-r--r--lib/gitlab/ci/reports/codequality_reports_comparer.rb48
-rw-r--r--lib/gitlab/ci/reports/reports_comparer.rb53
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml33
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml24
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml1
-rw-r--r--lib/gitlab/ci/templates/npm.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/npm.latest.gitlab-ci.yml41
-rw-r--r--lib/gitlab/ci/trace/checksum.rb23
-rw-r--r--lib/gitlab/ci/trace/metrics.rb3
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb1
-rw-r--r--lib/gitlab/config/entry/validators.rb26
-rw-r--r--lib/gitlab/cycle_analytics/builds_event_helper.rb4
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/permissions.rb4
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/production_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/updater.rb4
-rw-r--r--lib/gitlab/cycle_analytics/usage_data.rb91
-rw-r--r--lib/gitlab/danger/base_linter.rb95
-rw-r--r--lib/gitlab/danger/changelog.rb1
-rw-r--r--lib/gitlab/danger/commit_linter.rb118
-rw-r--r--lib/gitlab/danger/helper.rb6
-rw-r--r--lib/gitlab/danger/merge_request_linter.rb30
-rw-r--r--lib/gitlab/danger/roulette.rb2
-rw-r--r--lib/gitlab/database/batch_count.rb15
-rw-r--r--lib/gitlab/database/migrations/background_migration_helpers.rb11
-rw-r--r--lib/gitlab/database/postgres_hll/batch_distinct_counter.rb159
-rw-r--r--lib/gitlab/database/postgres_index.rb17
-rw-r--r--lib/gitlab/database/postgres_index_bloat_estimate.rb18
-rw-r--r--lib/gitlab/database/postgresql_adapter/empty_query_ping.rb22
-rw-r--r--lib/gitlab/database/reindexing.rb10
-rw-r--r--lib/gitlab/database/reindexing/concurrent_reindex.rb15
-rw-r--r--lib/gitlab/database/reindexing/index_selection.rb36
-rw-r--r--lib/gitlab/database/reindexing/reindex_action.rb9
-rw-r--r--lib/gitlab/database_importers/self_monitoring/project/create_service.rb2
-rw-r--r--lib/gitlab/deploy_key_access.rb37
-rw-r--r--lib/gitlab/diff/file_collection/base.rb18
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_base.rb8
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_batch.rb21
-rw-r--r--lib/gitlab/diff/file_collection_sorter.rb43
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb2
-rw-r--r--lib/gitlab/email/handler/service_desk_handler.rb4
-rw-r--r--lib/gitlab/encrypted_configuration.rb121
-rw-r--r--lib/gitlab/encrypted_ldap_command.rb104
-rw-r--r--lib/gitlab/experimentation.rb99
-rw-r--r--lib/gitlab/experimentation/controller_concern.rb80
-rw-r--r--lib/gitlab/experimentation/experiment.rb31
-rw-r--r--lib/gitlab/git.rb1
-rw-r--r--lib/gitlab/git/diff_collection.rb6
-rw-r--r--lib/gitlab/git/repository.rb12
-rw-r--r--lib/gitlab/git/wraps_gitaly_errors.rb2
-rw-r--r--lib/gitlab/git_access.rb51
-rw-r--r--lib/gitlab/git_access_project.rb16
-rw-r--r--lib/gitlab/git_access_snippet.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb17
-rw-r--r--lib/gitlab/github_import/client.rb12
-rw-r--r--lib/gitlab/github_import/importer/lfs_objects_importer.rb7
-rw-r--r--lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb44
-rw-r--r--lib/gitlab/github_import/importer/pull_request_review_importer.rb101
-rw-r--r--lib/gitlab/github_import/importer/pull_requests_importer.rb2
-rw-r--r--lib/gitlab/github_import/importer/pull_requests_merged_by_importer.rb38
-rw-r--r--lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb41
-rw-r--r--lib/gitlab/github_import/parallel_scheduling.rb41
-rw-r--r--lib/gitlab/github_import/representation/pull_request.rb17
-rw-r--r--lib/gitlab/github_import/representation/pull_request_review.rb49
-rw-r--r--lib/gitlab/gl_repository.rb1
-rw-r--r--lib/gitlab/gon_helper.rb8
-rw-r--r--lib/gitlab/google_code_import/client.rb54
-rw-r--r--lib/gitlab/google_code_import/importer.rb373
-rw-r--r--lib/gitlab/google_code_import/project_creator.rb32
-rw-r--r--lib/gitlab/google_code_import/repository.rb45
-rw-r--r--lib/gitlab/gpg.rb6
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb4
-rw-r--r--lib/gitlab/graphql/connection_collection_methods.rb13
-rw-r--r--lib/gitlab/graphql/connection_redaction.rb33
-rw-r--r--lib/gitlab/graphql/deferred.rb12
-rw-r--r--lib/gitlab/graphql/docs/helper.rb6
-rw-r--r--lib/gitlab/graphql/docs/templates/default.md.haml4
-rw-r--r--lib/gitlab/graphql/expose_permissions.rb2
-rw-r--r--lib/gitlab/graphql/externally_paginated_array.rb6
-rw-r--r--lib/gitlab/graphql/laziness.rb46
-rw-r--r--lib/gitlab/graphql/lazy.rb2
-rw-r--r--lib/gitlab/graphql/pagination/array_connection.rb15
-rw-r--r--lib/gitlab/graphql/pagination/connections.rb4
-rw-r--r--lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb9
-rw-r--r--lib/gitlab/graphql/pagination/keyset/connection.rb2
-rw-r--r--lib/gitlab/graphql/pagination/keyset/order_info.rb2
-rw-r--r--lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb2
-rw-r--r--lib/gitlab/hook_data/base_builder.rb7
-rw-r--r--lib/gitlab/hook_data/group_member_builder.rb65
-rw-r--r--lib/gitlab/i18n/html_todo.yml314
-rw-r--r--lib/gitlab/i18n/po_linter.rb14
-rw-r--r--lib/gitlab/i18n/translation_entry.rb19
-rw-r--r--lib/gitlab/import_export/group/tree_restorer.rb16
-rw-r--r--lib/gitlab/import_export/import_failure_service.rb31
-rw-r--r--lib/gitlab/import_export/importer.rb5
-rw-r--r--lib/gitlab/import_export/project/import_export.yml4
-rw-r--r--lib/gitlab/import_export/project/sample/relation_tree_restorer.rb4
-rw-r--r--lib/gitlab/import_export/relation_tree_restorer.rb28
-rw-r--r--lib/gitlab/import_export/wiki_restorer.rb28
-rw-r--r--lib/gitlab/import_sources.rb2
-rw-r--r--lib/gitlab/instrumentation_helper.rb10
-rw-r--r--lib/gitlab/kroki.rb38
-rw-r--r--lib/gitlab/kubernetes/deployment.rb117
-rw-r--r--lib/gitlab/kubernetes/helm/v2/client_command.rb11
-rw-r--r--lib/gitlab/kubernetes/helm/v2/reset_command.rb26
-rw-r--r--lib/gitlab/kubernetes/ingress.rb46
-rw-r--r--lib/gitlab/kubernetes/rollout_instances.rb75
-rw-r--r--lib/gitlab/kubernetes/rollout_status.rb72
-rw-r--r--lib/gitlab/legacy_github_import/project_creator.rb2
-rw-r--r--lib/gitlab/markdown_cache/active_record/extension.rb1
-rw-r--r--lib/gitlab/metrics/background_transaction.rb16
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb35
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb10
-rw-r--r--lib/gitlab/metrics/transaction.rb8
-rw-r--r--lib/gitlab/metrics/web_transaction.rb7
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb11
-rw-r--r--lib/gitlab/pagination/gitaly_keyset_pager.rb21
-rw-r--r--lib/gitlab/pagination/offset_header_builder.rb65
-rw-r--r--lib/gitlab/pagination/offset_pagination.rb54
-rw-r--r--lib/gitlab/path_regex.rb16
-rw-r--r--lib/gitlab/performance_bar/logger.rb11
-rw-r--r--lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb28
-rw-r--r--lib/gitlab/performance_bar/stats.rb60
-rw-r--r--lib/gitlab/project_template.rb38
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb35
-rw-r--r--lib/gitlab/rack_attack.rb118
-rw-r--r--lib/gitlab/rack_attack/request.rb77
-rw-r--r--lib/gitlab/rack_attack/user_allowlist.rb23
-rw-r--r--lib/gitlab/repo_path.rb51
-rw-r--r--lib/gitlab/request_forgery_protection.rb8
-rw-r--r--lib/gitlab/sample_data_template.rb3
-rw-r--r--lib/gitlab/sanitizers/exif.rb2
-rw-r--r--lib/gitlab/search/query.rb1
-rw-r--r--lib/gitlab/setup_helper.rb63
-rw-r--r--lib/gitlab/sidekiq_cluster.rb2
-rw-r--r--lib/gitlab/sidekiq_death_handler.rb19
-rw-r--r--lib/gitlab/sidekiq_middleware/client_metrics.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb18
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics_helper.rb (renamed from lib/gitlab/sidekiq_middleware/metrics.rb)2
-rw-r--r--lib/gitlab/sidekiq_middleware/server_metrics.rb4
-rw-r--r--lib/gitlab/throttle.rb50
-rw-r--r--lib/gitlab/tracking.rb13
-rw-r--r--lib/gitlab/tracking/destinations/product_analytics.rb41
-rw-r--r--lib/gitlab/tracking/destinations/snowplow.rb4
-rw-r--r--lib/gitlab/uploads/migration_helper.rb2
-rw-r--r--lib/gitlab/usage_data.rb37
-rw-r--r--lib/gitlab/usage_data_counters.rb40
-rw-r--r--lib/gitlab/usage_data_counters/aggregated_metrics/common.yml26
-rw-r--r--lib/gitlab/usage_data_counters/base_counter.rb6
-rw-r--r--lib/gitlab/usage_data_counters/counter_events/guest_package_events.yml34
-rw-r--r--lib/gitlab/usage_data_counters/editor_unique_counter.rb9
-rw-r--r--lib/gitlab/usage_data_counters/guest_package_event_counter.rb11
-rw-r--r--lib/gitlab/usage_data_counters/guest_package_events.yml34
-rw-r--r--lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb5
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml23
-rw-r--r--lib/gitlab/usage_data_counters/known_events/package_events.yml316
-rw-r--r--lib/gitlab/usage_data_counters/search_counter.rb1
-rw-r--r--lib/gitlab/usage_data_queries.rb7
-rw-r--r--lib/gitlab/user_access.rb4
-rw-r--r--lib/gitlab/utils/usage_data.rb12
-rw-r--r--lib/gitlab/uuid.rb32
-rw-r--r--lib/gitlab/whats_new.rb40
-rw-r--r--lib/gitlab_danger.rb1
-rw-r--r--lib/microsoft_teams/notifier.rb11
-rw-r--r--lib/object_storage/config.rb5
-rw-r--r--lib/object_storage/direct_upload.rb9
-rw-r--r--lib/product_analytics/tracker.rb31
-rw-r--r--lib/quality/test_level.rb4
-rw-r--r--lib/tasks/gettext.rake105
-rw-r--r--lib/tasks/gitlab/assets.rake5
-rw-r--r--lib/tasks/gitlab/db.rake36
-rw-r--r--lib/tasks/gitlab/ldap.rake18
-rw-r--r--lib/tasks/gitlab/packages/events.rake41
-rw-r--r--lib/tasks/gitlab/usage_data.rake12
-rw-r--r--lib/tasks/gitlab/user_management.rake13
-rw-r--r--lib/tasks/gitlab/workhorse.rake27
346 files changed, 6233 insertions, 2948 deletions
diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb
index 679e231b283..b724d3a38dc 100644
--- a/lib/api/admin/instance_clusters.rb
+++ b/lib/api/admin/instance_clusters.rb
@@ -76,6 +76,7 @@ module API
optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace'
optional :domain, type: String, desc: 'Cluster base domain'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
+ optional :managed, type: Boolean, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ea149f25584..06c2b46a2f2 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -211,7 +211,7 @@ module API
mount ::API::ProjectPackages
mount ::API::GroupPackages
mount ::API::PackageFiles
- mount ::API::NugetPackages
+ mount ::API::NugetProjectPackages
mount ::API::PypiPackages
mount ::API::ComposerPackages
mount ::API::ConanProjectPackages
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index f4b23c507f4..e2d30dd7c2b 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -117,8 +117,6 @@ module API
use :list_creation_params
end
post '/lists' do
- authorize_list_type_resource!
-
authorize!(:admin_list, user_project)
create_list
diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb
index 2ae82f78e01..89355c84401 100644
--- a/lib/api/boards_responses.rb
+++ b/lib/api/boards_responses.rb
@@ -45,21 +45,17 @@ module API
def create_list
create_list_service =
- ::Boards::Lists::CreateService.new(board_parent, current_user, create_list_params)
+ ::Boards::Lists::CreateService.new(board_parent, current_user, declared_params.compact.with_indifferent_access)
- list = create_list_service.execute(board)
+ response = create_list_service.execute(board)
- if list.valid?
- present list, with: Entities::List
+ if response.success?
+ present response.payload[:list], with: Entities::List
else
- render_validation_error!(list)
+ render_api_error!({ error: response.errors.first }, 400)
end
end
- def create_list_params
- params.slice(:label_id)
- end
-
def move_list(list)
move_list_service =
::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i })
@@ -80,14 +76,6 @@ module API
end
end
- # rubocop: disable CodeReuse/ActiveRecord
- def authorize_list_type_resource!
- unless available_labels_for(board_parent).exists?(params[:label_id])
- render_api_error!({ error: 'Label not found!' }, 400)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
params :list_creation_params do
requires :label_id, type: Integer, desc: 'The ID of an existing label'
end
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 85232b4ae1b..86e1a939df1 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -176,6 +176,10 @@ module API
optional :state, type: String, desc: %q(Job's status: success, failed)
optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum)
optional :failure_reason, type: String, desc: %q(Job's failure_reason)
+ optional :output, type: Hash, desc: %q(Build log state) do
+ optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum)
+ optional :bytesize, type: Integer, desc: %q(Job's trace size in bytes)
+ end
end
put '/:id' do
job = authenticate_job!
diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb
index 0ac5cc45ccf..1181650fe96 100644
--- a/lib/api/composer_packages.rb
+++ b/lib/api/composer_packages.rb
@@ -38,6 +38,8 @@ module API
packages = ::Packages::Composer::PackagesFinder.new(current_user, user_group).execute
if params[:package_name].present?
+ params[:package_name], params[:sha] = params[:package_name].split('$')
+
packages = packages.with_name(params[:package_name])
end
@@ -93,6 +95,7 @@ module API
get ':id/-/packages/composer/*package_name', requirements: COMPOSER_ENDPOINT_REQUIREMENTS, file_path: true do
not_found! if packages.empty?
+ not_found! if params[:sha].blank?
presenter.package_versions
end
@@ -132,7 +135,7 @@ module API
track_package_event('push_package', :composer)
::Packages::Composer::CreatePackageService
- .new(authorized_user_project, current_user, declared_params)
+ .new(authorized_user_project, current_user, declared_params.merge(build: current_authenticated_job))
.execute
created!
diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb
index 08265201328..8c13b580092 100644
--- a/lib/api/conan_instance_packages.rb
+++ b/lib/api/conan_instance_packages.rb
@@ -4,7 +4,7 @@
module API
class ConanInstancePackages < ::API::Base
namespace 'packages/conan/v1' do
- include ConanPackageEndpoints
+ include ::API::Concerns::Packages::ConanEndpoints
end
end
end
diff --git a/lib/api/conan_package_endpoints.rb b/lib/api/conan_package_endpoints.rb
deleted file mode 100644
index 188a42f26f8..00000000000
--- a/lib/api/conan_package_endpoints.rb
+++ /dev/null
@@ -1,351 +0,0 @@
-# frozen_string_literal: true
-
-# Conan 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 Conan package manager client when users run commands
-# like `conan install` or `conan upload`. The usage of the GitLab Conan repository is documented here:
-# https://docs.gitlab.com/ee/user/packages/conan_repository/#installing-a-package
-#
-# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
-module API
- module ConanPackageEndpoints
- extend ActiveSupport::Concern
-
- PACKAGE_REQUIREMENTS = {
- package_name: API::NO_SLASH_URL_PART_REGEX,
- package_version: API::NO_SLASH_URL_PART_REGEX,
- package_username: API::NO_SLASH_URL_PART_REGEX,
- package_channel: API::NO_SLASH_URL_PART_REGEX
- }.freeze
-
- FILE_NAME_REQUIREMENTS = {
- file_name: API::NO_SLASH_URL_PART_REGEX
- }.freeze
-
- PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex
- CONAN_REVISION_REGEX = Gitlab::Regex.conan_revision_regex
-
- CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze
-
- included do
- feature_category :package_registry
-
- helpers ::API::Helpers::PackagesManagerClientsHelpers
- helpers ::API::Helpers::Packages::Conan::ApiHelpers
- helpers ::API::Helpers::RelatedResourcesHelpers
-
- before do
- require_packages_enabled!
-
- # Personal access token will be extracted from Bearer or Basic authorization
- # in the overridden find_personal_access_token or find_user_from_job_token helpers
- authenticate!
- end
-
- desc 'Ping the Conan API' do
- detail 'This feature was introduced in GitLab 12.2'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'ping' do
- header 'X-Conan-Server-Capabilities', [].join(',')
- end
-
- desc 'Search for packages' do
- detail 'This feature was introduced in GitLab 12.4'
- end
-
- params do
- requires :q, type: String, desc: 'Search query'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'conans/search' do
- service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute
- service.payload
- end
-
- namespace 'users' do
- format :txt
-
- desc 'Authenticate user against conan CLI' do
- detail 'This feature was introduced in GitLab 12.2'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'authenticate' do
- unauthorized! unless token
-
- token.to_jwt
- end
-
- desc 'Check for valid user credentials per conan CLI' do
- detail 'This feature was introduced in GitLab 12.4'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'check_credentials' do
- authenticate!
- :ok
- end
- end
-
- params do
- requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
- requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
- requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
- requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
- end
- namespace 'conans/:package_name/:package_version/:package_username/:package_channel', requirements: PACKAGE_REQUIREMENTS do
- # Get the snapshot
- #
- # the snapshot is a hash of { filename: md5 hash }
- # md5 hash is the has of that file. This hash is used to diff the files existing on the client
- # to determine which client files need to be uploaded if no recipe exists the snapshot is empty
- desc 'Package Snapshot' do
- detail 'This feature was introduced in GitLab 12.5'
- end
-
- params do
- requires :conan_package_reference, type: String, desc: 'Conan package ID'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'packages/:conan_package_reference' do
- authorize!(:read_package, project)
-
- presenter = ::Packages::Conan::PackagePresenter.new(
- package,
- current_user,
- project,
- conan_package_reference: params[:conan_package_reference]
- )
-
- present presenter, with: ::API::Entities::ConanPackage::ConanPackageSnapshot
- end
-
- desc 'Recipe Snapshot' do
- detail 'This feature was introduced in GitLab 12.5'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get do
- authorize!(:read_package, project)
-
- presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project)
-
- present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot
- end
-
- # Get the manifest
- # returns the download urls for the existing recipe in the registry
- #
- # the manifest is a hash of { filename: url }
- # where the url is the download url for the file
- desc 'Package Digest' do
- detail 'This feature was introduced in GitLab 12.5'
- end
- params do
- requires :conan_package_reference, type: String, desc: 'Conan package ID'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'packages/:conan_package_reference/digest' do
- present_package_download_urls
- end
-
- desc 'Recipe Digest' do
- detail 'This feature was introduced in GitLab 12.5'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'digest' do
- present_recipe_download_urls
- end
-
- # Get the download urls
- #
- # returns the download urls for the existing recipe or package in the registry
- #
- # the manifest is a hash of { filename: url }
- # where the url is the download url for the file
- desc 'Package Download Urls' do
- detail 'This feature was introduced in GitLab 12.5'
- end
-
- params do
- requires :conan_package_reference, type: String, desc: 'Conan package ID'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'packages/:conan_package_reference/download_urls' do
- present_package_download_urls
- end
-
- desc 'Recipe Download Urls' do
- detail 'This feature was introduced in GitLab 12.5'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get 'download_urls' do
- present_recipe_download_urls
- end
-
- # Get the upload urls
- #
- # request body contains { filename: filesize } where the filename is the
- # name of the file the conan client is requesting to upload
- #
- # returns { filename: url }
- # where the url is the upload url for the file that the conan client will use
- desc 'Package Upload Urls' do
- detail 'This feature was introduced in GitLab 12.4'
- end
-
- params do
- requires :conan_package_reference, type: String, desc: 'Conan package ID'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- post 'packages/:conan_package_reference/upload_urls' do
- authorize!(:read_package, project)
-
- status 200
- present package_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
- end
-
- desc 'Recipe Upload Urls' do
- detail 'This feature was introduced in GitLab 12.4'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- post 'upload_urls' do
- authorize!(:read_package, project)
-
- status 200
- present recipe_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
- end
-
- desc 'Delete Package' do
- detail 'This feature was introduced in GitLab 12.5'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- delete do
- authorize!(:destroy_package, project)
-
- track_package_event('delete_package', :conan, category: 'API::ConanPackages')
-
- package.destroy
- end
- end
-
- params do
- requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
- requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
- requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
- requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
- requires :recipe_revision, type: String, regexp: CONAN_REVISION_REGEX, desc: 'Conan Recipe Revision'
- end
- namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision', requirements: PACKAGE_REQUIREMENTS do
- before do
- authenticate_non_get!
- end
-
- params do
- requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES
- end
- namespace 'export/:file_name', requirements: FILE_NAME_REQUIREMENTS do
- desc 'Download recipe files' do
- detail 'This feature was introduced in GitLab 12.6'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get do
- download_package_file(:recipe_file)
- end
-
- desc 'Upload recipe package files' do
- detail 'This feature was introduced in GitLab 12.6'
- end
-
- params do
- requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- put do
- upload_package_file(:recipe_file)
- end
-
- desc 'Workhorse authorize the conan recipe file' do
- detail 'This feature was introduced in GitLab 12.6'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- put 'authorize' do
- authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size)
- end
- end
-
- params do
- requires :conan_package_reference, type: String, desc: 'Conan Package ID'
- requires :package_revision, type: String, desc: 'Conan Package Revision'
- requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES
- end
- namespace 'package/:conan_package_reference/:package_revision/:file_name', requirements: FILE_NAME_REQUIREMENTS do
- desc 'Download package files' do
- detail 'This feature was introduced in GitLab 12.5'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- get do
- download_package_file(:package_file)
- end
-
- desc 'Workhorse authorize the conan package file' do
- detail 'This feature was introduced in GitLab 12.6'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- put 'authorize' do
- authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size)
- end
-
- desc 'Upload package files' do
- detail 'This feature was introduced in GitLab 12.6'
- end
-
- params do
- requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
- end
-
- route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
-
- put do
- upload_package_file(:package_file)
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb
index db8cd187811..636b5dca5ed 100644
--- a/lib/api/conan_project_packages.rb
+++ b/lib/api/conan_project_packages.rb
@@ -9,7 +9,7 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/packages/conan/v1' do
- include ConanPackageEndpoints
+ include ::API::Concerns::Packages::ConanEndpoints
end
end
end
diff --git a/lib/api/concerns/packages/conan_endpoints.rb b/lib/api/concerns/packages/conan_endpoints.rb
new file mode 100644
index 00000000000..6c8b3a1ba4a
--- /dev/null
+++ b/lib/api/concerns/packages/conan_endpoints.rb
@@ -0,0 +1,355 @@
+# frozen_string_literal: true
+
+# Conan 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 Conan package manager client when users run commands
+# like `conan install` or `conan upload`. The usage of the GitLab Conan repository is documented here:
+# https://docs.gitlab.com/ee/user/packages/conan_repository/#installing-a-package
+#
+# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
+module API
+ module Concerns
+ module Packages
+ module ConanEndpoints
+ extend ActiveSupport::Concern
+
+ PACKAGE_REQUIREMENTS = {
+ package_name: API::NO_SLASH_URL_PART_REGEX,
+ package_version: API::NO_SLASH_URL_PART_REGEX,
+ package_username: API::NO_SLASH_URL_PART_REGEX,
+ package_channel: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ FILE_NAME_REQUIREMENTS = {
+ file_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex
+ CONAN_REVISION_REGEX = Gitlab::Regex.conan_revision_regex
+
+ CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze
+
+ included do
+ feature_category :package_registry
+
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+ helpers ::API::Helpers::Packages::Conan::ApiHelpers
+ helpers ::API::Helpers::RelatedResourcesHelpers
+
+ before do
+ require_packages_enabled!
+
+ # Personal access token will be extracted from Bearer or Basic authorization
+ # in the overridden find_personal_access_token or find_user_from_job_token helpers
+ authenticate!
+ end
+
+ desc 'Ping the Conan API' do
+ detail 'This feature was introduced in GitLab 12.2'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'ping' do
+ header 'X-Conan-Server-Capabilities', [].join(',')
+ end
+
+ desc 'Search for packages' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+
+ params do
+ requires :q, type: String, desc: 'Search query'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'conans/search' do
+ service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute
+ service.payload
+ end
+
+ namespace 'users' do
+ format :txt
+
+ desc 'Authenticate user against conan CLI' do
+ detail 'This feature was introduced in GitLab 12.2'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'authenticate' do
+ unauthorized! unless token
+
+ token.to_jwt
+ end
+
+ desc 'Check for valid user credentials per conan CLI' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'check_credentials' do
+ authenticate!
+ :ok
+ end
+ end
+
+ params do
+ requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
+ requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
+ requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
+ requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
+ end
+ namespace 'conans/:package_name/:package_version/:package_username/:package_channel', requirements: PACKAGE_REQUIREMENTS do
+ # Get the snapshot
+ #
+ # the snapshot is a hash of { filename: md5 hash }
+ # md5 hash is the has of that file. This hash is used to diff the files existing on the client
+ # to determine which client files need to be uploaded if no recipe exists the snapshot is empty
+ desc 'Package Snapshot' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'packages/:conan_package_reference' do
+ authorize!(:read_package, project)
+
+ presenter = ::Packages::Conan::PackagePresenter.new(
+ package,
+ current_user,
+ project,
+ conan_package_reference: params[:conan_package_reference]
+ )
+
+ present presenter, with: ::API::Entities::ConanPackage::ConanPackageSnapshot
+ end
+
+ desc 'Recipe Snapshot' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get do
+ authorize!(:read_package, project)
+
+ presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project)
+
+ present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot
+ end
+
+ # Get the manifest
+ # returns the download urls for the existing recipe in the registry
+ #
+ # the manifest is a hash of { filename: url }
+ # where the url is the download url for the file
+ desc 'Package Digest' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'packages/:conan_package_reference/digest' do
+ present_package_download_urls
+ end
+
+ desc 'Recipe Digest' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'digest' do
+ present_recipe_download_urls
+ end
+
+ # Get the download urls
+ #
+ # returns the download urls for the existing recipe or package in the registry
+ #
+ # the manifest is a hash of { filename: url }
+ # where the url is the download url for the file
+ desc 'Package Download Urls' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'packages/:conan_package_reference/download_urls' do
+ present_package_download_urls
+ end
+
+ desc 'Recipe Download Urls' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get 'download_urls' do
+ present_recipe_download_urls
+ end
+
+ # Get the upload urls
+ #
+ # request body contains { filename: filesize } where the filename is the
+ # name of the file the conan client is requesting to upload
+ #
+ # returns { filename: url }
+ # where the url is the upload url for the file that the conan client will use
+ desc 'Package Upload Urls' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ post 'packages/:conan_package_reference/upload_urls' do
+ authorize!(:read_package, project)
+
+ status 200
+ present package_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
+ end
+
+ desc 'Recipe Upload Urls' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ post 'upload_urls' do
+ authorize!(:read_package, project)
+
+ status 200
+ present recipe_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
+ end
+
+ desc 'Delete Package' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ delete do
+ authorize!(:destroy_package, project)
+
+ track_package_event('delete_package', :conan, category: 'API::ConanPackages')
+
+ package.destroy
+ end
+ end
+
+ params do
+ requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
+ requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
+ requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
+ requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
+ requires :recipe_revision, type: String, regexp: CONAN_REVISION_REGEX, desc: 'Conan Recipe Revision'
+ end
+ namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision', requirements: PACKAGE_REQUIREMENTS do
+ before do
+ authenticate_non_get!
+ end
+
+ params do
+ requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES
+ end
+ namespace 'export/:file_name', requirements: FILE_NAME_REQUIREMENTS do
+ desc 'Download recipe files' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get do
+ download_package_file(:recipe_file)
+ end
+
+ desc 'Upload recipe package files' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+
+ params do
+ requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ put do
+ upload_package_file(:recipe_file)
+ end
+
+ desc 'Workhorse authorize the conan recipe file' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ put 'authorize' do
+ authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size)
+ end
+ end
+
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan Package ID'
+ requires :package_revision, type: String, desc: 'Conan Package Revision'
+ requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES
+ end
+ namespace 'package/:conan_package_reference/:package_revision/:file_name', requirements: FILE_NAME_REQUIREMENTS do
+ desc 'Download package files' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ get do
+ download_package_file(:package_file)
+ end
+
+ desc 'Workhorse authorize the conan package file' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ put 'authorize' do
+ authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size)
+ end
+
+ desc 'Upload package files' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+
+ params do
+ requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
+ end
+
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+
+ put do
+ upload_package_file(:package_file)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index a91db93b182..833288c6013 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -37,7 +37,7 @@ module API
get 'dist-tags', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
- bad_request!('Package Name') if package_name.blank?
+ bad_request_missing_attribute!('Package Name') if package_name.blank?
authorize_read_package!(project)
@@ -62,9 +62,9 @@ module API
version = env['api.request.body']
tag = params[:tag]
- bad_request!('Package Name') if package_name.blank?
- bad_request!('Version') if version.blank?
- bad_request!('Tag') if tag.blank?
+ bad_request_missing_attribute!('Package Name') if package_name.blank?
+ bad_request_missing_attribute!('Version') if version.blank?
+ bad_request_missing_attribute!('Tag') if tag.blank?
authorize_create_package!(project)
@@ -85,8 +85,8 @@ module API
package_name = params[:package_name]
tag = params[:tag]
- bad_request!('Package Name') if package_name.blank?
- bad_request!('Tag') if tag.blank?
+ bad_request_missing_attribute!('Package Name') if package_name.blank?
+ bad_request_missing_attribute!('Tag') if tag.blank?
authorize_destroy_package!(project)
diff --git a/lib/api/concerns/packages/nuget_endpoints.rb b/lib/api/concerns/packages/nuget_endpoints.rb
new file mode 100644
index 00000000000..5177c4d23c0
--- /dev/null
+++ b/lib/api/concerns/packages/nuget_endpoints.rb
@@ -0,0 +1,135 @@
+# 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{\A0|[1-9]\d*\z}.freeze
+
+ included do
+ helpers do
+ def find_packages
+ packages = package_finder.execute
+
+ not_found!('Packages') unless packages.exists?
+
+ packages
+ end
+
+ def find_package
+ package = package_finder(package_version: params[:package_version]).execute
+ .first
+
+ not_found!('Package') unless package
+
+ package
+ end
+
+ def package_finder(finder_params = {})
+ ::Packages::Nuget::PackageFinder.new(
+ authorized_user_project,
+ **finder_params.merge(package_name: params[:package_name])
+ )
+ 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'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ get 'index', format: :json do
+ authorize_read_package!(authorized_user_project)
+ track_package_event('cli_metadata', :nuget, category: 'API::NugetPackages')
+
+ present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project),
+ 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
+ end
+ namespace '/metadata/*package_name' do
+ before do
+ authorize_read_package!(authorized_user_project)
+ end
+
+ desc 'The NuGet Metadata Service - Package name level' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ get 'index', format: :json do
+ present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages),
+ 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'
+ end
+ params do
+ requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ get '*package_version', format: :json do
+ present ::Packages::Nuget::PackageMetadataPresenter.new(find_package),
+ with: ::API::Entities::Nuget::PackageMetadata
+ end
+ end
+
+ # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
+ params do
+ requires :q, type: String, desc: 'The search term'
+ optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX
+ optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX
+ optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true
+ end
+ namespace '/query' do
+ before do
+ authorize_read_package!(authorized_user_project)
+ end
+
+ desc 'The NuGet Search Service' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ get format: :json do
+ search_options = {
+ include_prerelease_versions: params[:prerelease],
+ per_page: params[:take],
+ padding: params[:skip]
+ }
+ search = ::Packages::Nuget::SearchService
+ .new(authorized_user_project, params[:q], search_options)
+ .execute
+
+ track_package_event('search_package', :nuget, category: 'API::NugetPackages')
+
+ present ::Packages::Nuget::SearchResultsPresenter.new(search),
+ with: ::API::Entities::Nuget::SearchResults
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 4c4ec200060..580d546b360 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -104,6 +104,7 @@ module API
position: params[:position],
id_key => noteable.id
}
+ opts[:commit_id] = params[:commit_id] if noteable.is_a?(MergeRequest) && type == 'DiffNote'
note = create_note(noteable, opts)
diff --git a/lib/api/entities/cluster.rb b/lib/api/entities/cluster.rb
index 67459092a33..b7e76e763f7 100644
--- a/lib/api/entities/cluster.rb
+++ b/lib/api/entities/cluster.rb
@@ -3,7 +3,7 @@
module API
module Entities
class Cluster < Grape::Entity
- expose :id, :name, :created_at, :domain
+ expose :id, :name, :created_at, :domain, :enabled, :managed
expose :provider_type, :platform_type, :environment_scope, :cluster_type, :namespace_per_environment
expose :user, using: Entities::UserBasic
expose :platform_kubernetes, using: Entities::Platform::Kubernetes
diff --git a/lib/api/entities/feature.rb b/lib/api/entities/feature.rb
index 618a7be9c7b..d1151849cd7 100644
--- a/lib/api/entities/feature.rb
+++ b/lib/api/entities/feature.rb
@@ -17,6 +17,16 @@ module API
{ key: gate.key, value: value }
end.compact
end
+
+ class Definition < Grape::Entity
+ ::Feature::Definition::PARAMS.each do |param|
+ expose param
+ end
+ end
+
+ expose :definition, using: Definition do |feature|
+ ::Feature::Definition.definitions[feature.name.to_sym]
+ end
end
end
end
diff --git a/lib/api/entities/feature_flag.rb b/lib/api/entities/feature_flag.rb
index 82fdb20af00..f383eabd5dc 100644
--- a/lib/api/entities/feature_flag.rb
+++ b/lib/api/entities/feature_flag.rb
@@ -6,11 +6,11 @@ module API
expose :name
expose :description
expose :active
- expose :version, if: :feature_flags_new_version_enabled
+ expose :version
expose :created_at
expose :updated_at
expose :scopes, using: FeatureFlag::LegacyScope
- expose :strategies, using: FeatureFlag::Strategy, if: :feature_flags_new_version_enabled
+ expose :strategies, using: FeatureFlag::Strategy
end
end
end
diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb
index 5f2609cf68b..82102854394 100644
--- a/lib/api/entities/issue.rb
+++ b/lib/api/entities/issue.rb
@@ -43,6 +43,7 @@ module API
end
expose :moved_to_id
+ expose :service_desk_reply_to
end
end
end
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
index 69523e3637b..7f1b5b87725 100644
--- a/lib/api/entities/merge_request_basic.rb
+++ b/lib/api/entities/merge_request_basic.rb
@@ -27,6 +27,7 @@ module API
expose(:downvotes) { |merge_request, options| issuable_metadata.downvotes }
expose :author, :assignees, :assignee, using: Entities::UserBasic
+ expose :reviewers, if: -> (merge_request, _) { merge_request.allows_reviewers? }, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :labels do |merge_request, options|
if options[:with_labels_details]
diff --git a/lib/api/entities/note.rb b/lib/api/entities/note.rb
index f22ab73afd0..9a60c04220d 100644
--- a/lib/api/entities/note.rb
+++ b/lib/api/entities/note.rb
@@ -14,6 +14,7 @@ module API
expose :created_at, :updated_at
expose :system?, as: :system
expose :noteable_id, :noteable_type
+ 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
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index 82a44c75382..317caefe0a1 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -67,6 +67,8 @@ module API
expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) }
expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) }
expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) }
+ expose(:operations_access_level) { |project, options| project.project_feature.string_access_level(:operations) }
+ expose(:analytics_access_level) { |project, options| project.project_feature.string_access_level(:analytics) }
expose :emails_disabled
expose :shared_runners_enabled
diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb
index f92593da3fa..e79c1cdf1a2 100644
--- a/lib/api/entities/project_import_status.rb
+++ b/lib/api/entities/project_import_status.rb
@@ -12,9 +12,8 @@ module API
project.import_state&.relation_hard_failures(limit: 100) || []
end
- # TODO: Use `expose_nil` once we upgrade the grape-entity gem
- expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project|
- project.import_state.last_error
+ expose :import_error do |project, _options|
+ project.import_state&.last_error
end
end
end
diff --git a/lib/api/entities/project_snippet.rb b/lib/api/entities/project_snippet.rb
index 8ed87e51375..253fcfcf38f 100644
--- a/lib/api/entities/project_snippet.rb
+++ b/lib/api/entities/project_snippet.rb
@@ -1,4 +1,4 @@
-# frozen_String_literal: true
+# frozen_string_literal: true
module API
module Entities
diff --git a/lib/api/entities/project_statistics.rb b/lib/api/entities/project_statistics.rb
index 32201e88eaf..70980e670b0 100644
--- a/lib/api/entities/project_statistics.rb
+++ b/lib/api/entities/project_statistics.rb
@@ -10,6 +10,7 @@ module API
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
expose :snippets_size
+ expose :packages_size
end
end
end
diff --git a/lib/api/entities/related_issue.rb b/lib/api/entities/related_issue.rb
index 491c606bd49..60793fed5e0 100644
--- a/lib/api/entities/related_issue.rb
+++ b/lib/api/entities/related_issue.rb
@@ -5,6 +5,8 @@ module API
class RelatedIssue < ::API::Entities::Issue
expose :issue_link_id
expose :issue_link_type, as: :link_type
+ expose :issue_link_created_at, as: :link_created_at
+ expose :issue_link_updated_at, as: :link_updated_at
end
end
end
diff --git a/lib/api/feature_flags.rb b/lib/api/feature_flags.rb
index 67168ba9be6..6fdc4535be3 100644
--- a/lib/api/feature_flags.rb
+++ b/lib/api/feature_flags.rb
@@ -62,8 +62,6 @@ module API
attrs = declared_params(include_missing: false)
- ensure_post_version_2_flags_enabled! if attrs[:version] == 'new_version_flag'
-
rename_key(attrs, :scopes, :scopes_attributes)
rename_key(attrs, :strategies, :strategies_attributes)
update_value(attrs, :strategies_attributes) do |strategies|
@@ -143,7 +141,7 @@ module API
end
desc 'Update a feature flag' do
- detail 'This feature will be introduced in GitLab 13.1 if feature_flags_new_version feature flag is removed'
+ detail 'This feature was introduced in GitLab 13.2'
success ::API::Entities::FeatureFlag
end
params do
@@ -163,7 +161,6 @@ module API
end
end
put do
- not_found! unless feature_flags_new_version_enabled?
authorize_update_feature_flag!
render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag?
@@ -228,32 +225,17 @@ module API
def present_entity(result)
present result,
- with: ::API::Entities::FeatureFlag,
- feature_flags_new_version_enabled: feature_flags_new_version_enabled?
- end
-
- def ensure_post_version_2_flags_enabled!
- unless feature_flags_new_version_enabled?
- render_api_error!('Version 2 flags are not enabled for this project', :unprocessable_entity)
- end
+ with: ::API::Entities::FeatureFlag
end
def feature_flag
- @feature_flag ||= if feature_flags_new_version_enabled?
- user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name])
- else
- user_project.operations_feature_flags.legacy_flag.find_by_name!(params[:feature_flag_name])
- end
+ @feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name])
end
def new_version_flag_present?
user_project.operations_feature_flags.new_version_flag.find_by_name(params[:name]).present?
end
- def feature_flags_new_version_enabled?
- Feature.enabled?(:feature_flags_new_version, user_project, default_enabled: true)
- end
-
def rename_key(hash, old_key, new_key)
hash[new_key] = hash.delete(old_key) if hash.key?(old_key)
hash
diff --git a/lib/api/feature_flags_user_lists.rb b/lib/api/feature_flags_user_lists.rb
index 086bcbcdc89..8577da173b1 100644
--- a/lib/api/feature_flags_user_lists.rb
+++ b/lib/api/feature_flags_user_lists.rb
@@ -54,7 +54,7 @@ module API
end
params do
- requires :iid, type: String, desc: 'The internal id of the user list'
+ requires :iid, type: String, desc: 'The internal ID of the user list'
end
resource 'feature_flags_user_lists/:iid' do
desc 'Get a single feature flag user list belonging to a project' do
diff --git a/lib/api/features.rb b/lib/api/features.rb
index 2c2e3e3d0c9..57bd7c38ad2 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -46,6 +46,15 @@ module API
present features, with: Entities::Feature, current_user: current_user
end
+ desc 'Get a list of all feature definitions' do
+ success Entities::Feature::Definition
+ end
+ get :definitions do
+ definitions = ::Feature::Definition.definitions.values.map(&:to_h)
+
+ present definitions, with: Entities::Feature::Definition, current_user: current_user
+ end
+
desc 'Set the gate value for the given feature' do
success Entities::Feature
end
@@ -56,6 +65,7 @@ module API
optional :user, type: String, desc: 'A GitLab username'
optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'"
optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce'
+ optional :force, type: Boolean, desc: 'Skip feature flag validation checks, ie. YAML definition'
mutually_exclusive :key, :feature_group
mutually_exclusive :key, :user
@@ -63,9 +73,8 @@ module API
mutually_exclusive :key, :project
end
post ':name' do
- validate_feature_flag_name!(params[:name])
+ validate_feature_flag_name!(params[:name]) unless params[:force]
- feature = Feature.get(params[:name]) # rubocop:disable Gitlab/AvoidFeatureGet
targets = gate_targets(params)
value = gate_value(params)
key = gate_key(params)
@@ -73,25 +82,26 @@ module API
case value
when true
if gate_specified?(params)
- targets.each { |target| feature.enable(target) }
+ targets.each { |target| Feature.enable(params[:name], target) }
else
- feature.enable
+ Feature.enable(params[:name])
end
when false
if gate_specified?(params)
- targets.each { |target| feature.disable(target) }
+ targets.each { |target| Feature.disable(params[:name], target) }
else
- feature.disable
+ Feature.disable(params[:name])
end
else
if key == :percentage_of_actors
- feature.enable_percentage_of_actors(value)
+ Feature.enable_percentage_of_actors(params[:name], value)
else
- feature.enable_percentage_of_time(value)
+ Feature.enable_percentage_of_time(params[:name], value)
end
end
- present feature, with: Entities::Feature, current_user: current_user
+ present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet
+ with: Entities::Feature, current_user: current_user
end
desc 'Remove the gate value for the given feature'
diff --git a/lib/api/go_proxy.rb b/lib/api/go_proxy.rb
index 8fb4c561c40..2d978019f2a 100755
--- a/lib/api/go_proxy.rb
+++ b/lib/api/go_proxy.rb
@@ -48,7 +48,7 @@ module API
not_found! unless Feature.enabled?(:go_proxy, user_project)
module_name = case_decode params[:module_name]
- bad_request!('Module Name') if module_name.blank?
+ bad_request_missing_attribute!('Module Name') if module_name.blank?
mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute
diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb
index ac5a1a2ce94..2bfd98a5b69 100644
--- a/lib/api/group_boards.rb
+++ b/lib/api/group_boards.rb
@@ -83,8 +83,6 @@ module API
use :list_creation_params
end
post '/lists' do
- authorize_list_type_resource!
-
authorize!(:admin_list, user_group)
create_list
diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb
index a435b050042..81944a653c8 100644
--- a/lib/api/group_clusters.rb
+++ b/lib/api/group_clusters.rb
@@ -75,10 +75,12 @@ module API
params do
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
+ optional :enabled, type: Boolean, desc: 'Determines if cluster is active or not'
optional :domain, type: String, desc: 'Cluster base domain'
optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
+ optional :managed, type: Boolean, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb
index bf3ac8800b7..7fbf4445116 100644
--- a/lib/api/group_labels.rb
+++ b/lib/api/group_labels.rb
@@ -66,7 +66,7 @@ module API
success Entities::GroupLabel
end
params do
- optional :label_id, type: Integer, desc: 'The id of the label to be updated'
+ optional :label_id, type: Integer, desc: 'The ID of the label to be updated'
optional :name, type: String, desc: 'The name of the label to be updated'
use :group_label_update_params
exactly_one_of :label_id, :name
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 147d8407142..6fe25471289 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -271,6 +271,10 @@ module API
authorize! :read_build, user_project
end
+ def authorize_read_build_trace!(build)
+ authorize! :read_build_trace, build
+ end
+
def authorize_destroy_artifacts!
authorize! :destroy_artifacts, user_project
end
@@ -318,7 +322,7 @@ module API
# keys (required) - A hash consisting of keys that must be present
def required_attributes!(keys)
keys.each do |key|
- bad_request!(key) unless params[key].present?
+ bad_request_missing_attribute!(key) unless params[key].present?
end
end
@@ -364,12 +368,16 @@ module API
render_api_error!(message.join(' '), 403)
end
- def bad_request!(attribute)
- message = ["400 (Bad request)"]
- message << "\"" + attribute.to_s + "\" not given" if attribute
+ def bad_request!(reason = nil)
+ message = ['400 Bad request']
+ message << "- #{reason}" if reason
render_api_error!(message.join(' '), 400)
end
+ def bad_request_missing_attribute!(attribute)
+ bad_request!("\"#{attribute}\" not given")
+ end
+
def not_found!(resource = nil)
message = ["404"]
message << resource if resource
@@ -536,13 +544,23 @@ module API
)
end
+ def increment_counter(event_name)
+ feature_name = "usage_data_#{event_name}"
+ return unless Feature.enabled?(feature_name)
+
+ Gitlab::UsageDataCounters.count(event_name)
+ rescue => error
+ Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}")
+ end
+
# @param event_name [String] the event name
# @param values [Array|String] the values counted
def increment_unique_values(event_name, values)
return unless values.present?
- feature_name = "usage_data_#{event_name}"
- return unless Feature.enabled?(feature_name)
+ feature_flag = "usage_data_#{event_name}"
+
+ return unless Feature.enabled?(feature_flag, default_enabled: true)
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name)
rescue => error
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 69b53ea6c2f..12b0a053e79 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -31,8 +31,7 @@ module API
def access_checker_for(actor, protocol)
access_checker_klass.new(actor.key_or_user, container, protocol,
authentication_abilities: ssh_authentication_abilities,
- namespace_path: namespace_path,
- repository_path: project_path,
+ repository_path: repository_path,
redirected_path: redirected_path)
end
@@ -71,18 +70,22 @@ module API
false
end
- def project_path
- project&.path || project_path_match[:project_path]
- end
-
- def namespace_path
- project&.namespace&.full_path || project_path_match[:namespace_path]
- end
-
private
- def project_path_match
- @project_path_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {}
+ def repository_path
+ if container
+ "#{container.full_path}.git"
+ elsif params[:project]
+ # When the project doesn't exist, we still need to pass on the path
+ # to support auto-creation in `GitAccessProject`.
+ #
+ # For consistency with the Git HTTP controllers, we normalize the path
+ # to remove a leading slash and ensure a trailing `.git`.
+ #
+ # NOTE: For GitLab Shell, `params[:project]` is the full repository path
+ # from the SSH command, with an optional trailing `.git`.
+ "#{params[:project].delete_prefix('/').delete_suffix('.git')}.git"
+ end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -96,7 +99,7 @@ module API
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
- # Project id to pass between components that don't share/don't have
+ # Repository id to pass between components that don't share/don't have
# access to the same filesystem mounts
def gl_repository
repo_type.identifier_for_container(container)
@@ -106,8 +109,9 @@ module API
repository.full_path
end
- # Return the repository depending on whether we want the wiki or the
- # regular repository
+ # Return the repository for the detected type and container
+ #
+ # @returns [Repository]
def repository
@repository ||= repo_type.repository_for(container)
end
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
index 431001c227d..8aed578905e 100644
--- a/lib/api/helpers/members_helpers.rb
+++ b/lib/api/helpers/members_helpers.rb
@@ -45,7 +45,7 @@ module API
end
def find_all_members_for_project(project)
- MembersFinder.new(project, current_user).execute(include_relations: [:inherited, :direct, :invited_groups_members])
+ MembersFinder.new(project, current_user).execute(include_relations: [:inherited, :direct, :invited_groups])
end
def find_all_members_for_group(group)
diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb
index e35a8712131..0784efc11d6 100644
--- a/lib/api/helpers/packages/basic_auth_helpers.rb
+++ b/lib/api/helpers/packages/basic_auth_helpers.rb
@@ -7,8 +7,8 @@ module API
extend ::Gitlab::Utils::Override
module Constants
- AUTHENTICATE_REALM_HEADER = 'Www-Authenticate: Basic realm'
- AUTHENTICATE_REALM_NAME = 'GitLab Packages Registry'
+ AUTHENTICATE_REALM_HEADER = 'WWW-Authenticate'
+ AUTHENTICATE_REALM_NAME = 'Basic realm="GitLab Packages Registry"'
end
include Constants
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
index 934e18bdd0a..39ecfc171a9 100644
--- a/lib/api/helpers/packages/conan/api_helpers.rb
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -164,7 +164,11 @@ module API
end
def find_or_create_package
- package || ::Packages::Conan::CreatePackageService.new(project, current_user, params).execute
+ package || ::Packages::Conan::CreatePackageService.new(
+ project,
+ current_user,
+ params.merge(build: current_authenticated_job)
+ ).execute
end
def track_push_package_event
@@ -184,7 +188,11 @@ module API
def create_package_file_with_type(file_type, current_package)
unless params[:file].size == 0 # rubocop: disable Style/ZeroLengthPredicate
# conan sends two upload requests, the first has no file, so we skip record creation if file.size == 0
- ::Packages::Conan::CreatePackageFileService.new(current_package, params[:file], params.merge(conan_file_type: file_type)).execute
+ ::Packages::Conan::CreatePackageFileService.new(
+ current_package,
+ params[:file],
+ params.merge(conan_file_type: file_type, build: current_authenticated_job)
+ ).execute
end
end
@@ -214,6 +222,7 @@ module API
return unless route_authentication_setting[:job_token_allowed]
job = find_job_from_token || raise(::Gitlab::Auth::UnauthorizedError)
+ @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables
job.user
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 0364ba2ad9e..f5f45cf7351 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -6,7 +6,7 @@ module API
extend ActiveSupport::Concern
extend Grape::API::Helpers
- STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size].freeze
+ STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size packages_size].freeze
params :optional_project_params_ce do
optional :description, type: String, desc: 'The description of the project'
@@ -32,6 +32,8 @@ module API
optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`'
optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`'
optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`'
+ optional :operations_access_level, type: String, values: %w(disabled private enabled), desc: 'Operations access level. One of `disabled`, `private` or `enabled`'
+ optional :analytics_access_level, type: String, values: %w(disabled private enabled), desc: 'Analytics access level. One of `disabled`, `private` or `enabled`'
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index 4adb27a7414..9d2fd9978d9 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -304,6 +304,38 @@ module API
desc: 'Project URL'
}
],
+ 'datadog' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'API key used for authentication with Datadog'
+ },
+ {
+ required: false,
+ name: :datadog_site,
+ type: String,
+ desc: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site'
+ },
+ {
+ required: false,
+ name: :api_url,
+ type: String,
+ desc: '(Advanced) Define the full URL for your Datadog site directly'
+ },
+ {
+ required: false,
+ name: :datadog_service,
+ type: String,
+ desc: 'Name of this GitLab instance that all data will be tagged with'
+ },
+ {
+ required: false,
+ name: :datadog_env,
+ type: String,
+ desc: 'The environment tag that traces will be tagged with'
+ }
+ ],
'discord' => [
{
required: true,
@@ -459,6 +491,32 @@ module API
desc: 'Colorize messages'
}
],
+ 'jenkins' => [
+ {
+ required: true,
+ name: :jenkins_url,
+ type: String,
+ desc: 'Jenkins root URL like https://jenkins.example.com'
+ },
+ {
+ required: true,
+ name: :project_name,
+ type: String,
+ desc: 'The URL-friendly project name. Example: my_project_name'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'A user with access to the Jenkins server, if applicable'
+ },
+ {
+ required: false,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ],
'jira' => [
{
required: true,
@@ -758,6 +816,7 @@ module API
::ConfluenceService,
::CampfireService,
::CustomIssueTrackerService,
+ ::DatadogService,
::DiscordService,
::DroneCiService,
::EmailsOnPushService,
@@ -767,6 +826,7 @@ module API
::HangoutsChatService,
::HipchatService,
::IrkerService,
+ ::JenkinsService,
::JiraService,
::MattermostSlashCommandsService,
::SlackSlashCommandsService,
@@ -787,7 +847,6 @@ module API
def self.development_service_classes
[
::MockCiService,
- ::MockDeploymentService,
::MockMonitoringService
]
end
diff --git a/lib/api/helpers/sse_helpers.rb b/lib/api/helpers/sse_helpers.rb
new file mode 100644
index 00000000000..c354694f508
--- /dev/null
+++ b/lib/api/helpers/sse_helpers.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module SSEHelpers
+ def request_from_sse?(project)
+ return false if request.referer.blank?
+
+ uri = URI.parse(request.referer)
+ uri.path.starts_with?(::Gitlab::Routing.url_helpers.project_root_sse_path(project))
+ rescue URI::InvalidURIError
+ false
+ end
+ end
+ end
+end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 61ef1d5bde0..332f2f1986f 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -300,7 +300,7 @@ module API
post '/two_factor_otp_check', feature_category: :authentication_and_authorization do
status 200
- break { success: false } unless Feature.enabled?(:two_factor_for_cli)
+ break { success: false, message: 'Feature flag is disabled' } unless Feature.enabled?(:two_factor_for_cli)
actor.update_last_used_at!
user = actor.user
@@ -316,6 +316,8 @@ module API
otp_validation_result = ::Users::ValidateOtpService.new(user).execute(params.fetch(:otp_attempt))
if otp_validation_result[:status] == :success
+ ::Gitlab::Auth::Otp::SessionEnforcer.new(actor.key).update_session
+
{ success: true }
else
{ success: false, message: 'Invalid OTP' }
diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb
index d4690709de4..73723a96401 100644
--- a/lib/api/internal/kubernetes.rb
+++ b/lib/api/internal/kubernetes.rb
@@ -85,9 +85,7 @@ module API
get '/project_info' do
project = find_project(params[:id])
- # TODO sort out authorization for real
- # https://gitlab.com/gitlab-org/gitlab/-/issues/220912
- unless Ability.allowed?(nil, :download_code, project)
+ unless Guest.can?(:download_code, project) || agent.has_access_to?(project)
not_found!
end
@@ -123,3 +121,5 @@ module API
end
end
end
+
+API::Internal::Kubernetes.prepend_if_ee('EE::API::Internal::Kubernetes')
diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb
index 690f52d89f3..8eaeeae26c2 100644
--- a/lib/api/internal/pages.rb
+++ b/lib/api/internal/pages.rb
@@ -32,26 +32,29 @@ module API
requires :host, type: String, desc: 'The host to query for'
end
get "/" do
- serverless_domain_finder = ServerlessDomainFinder.new(params[:host])
- if serverless_domain_finder.serverless?
- # Handle Serverless domains
- serverless_domain = serverless_domain_finder.execute
- no_content! unless serverless_domain
-
- virtual_domain = Serverless::VirtualDomain.new(serverless_domain)
- no_content! unless virtual_domain
-
- present virtual_domain, with: Entities::Internal::Serverless::VirtualDomain
- else
- # Handle Pages domains
- host = Namespace.find_by_pages_host(params[:host]) || PagesDomain.find_by_domain_case_insensitive(params[:host])
- no_content! unless host
-
- virtual_domain = host.pages_virtual_domain
- no_content! unless virtual_domain
-
- present virtual_domain, with: Entities::Internal::Pages::VirtualDomain
- end
+ ##
+ # Serverless domain proxy has been deprecated and disabled as per
+ # https://gitlab.com/gitlab-org/gitlab-pages/-/issues/467
+ #
+ # serverless_domain_finder = ServerlessDomainFinder.new(params[:host])
+ # if serverless_domain_finder.serverless?
+ # # Handle Serverless domains
+ # serverless_domain = serverless_domain_finder.execute
+ # no_content! unless serverless_domain
+ #
+ # virtual_domain = Serverless::VirtualDomain.new(serverless_domain)
+ # no_content! unless virtual_domain
+ #
+ # present virtual_domain, with: Entities::Internal::Serverless::VirtualDomain
+ # end
+
+ host = Namespace.find_by_pages_host(params[:host]) || PagesDomain.find_by_domain_case_insensitive(params[:host])
+ no_content! unless host
+
+ virtual_domain = host.pages_virtual_domain
+ no_content! unless virtual_domain
+
+ present virtual_domain, with: Entities::Internal::Pages::VirtualDomain
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 6a6ee7a4e1c..73e2163248d 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -435,3 +435,5 @@ module API
end
end
end
+
+API::Issues.prepend_if_ee('EE::API::Issues')
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 51659c2e8a1..44751b3d76c 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -76,6 +76,8 @@ module API
build = find_build!(params[:job_id])
+ authorize_read_build_trace!(build) if build
+
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain'
env['api.format'] = :binary
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index a8fc277989e..c9f29865664 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -57,7 +57,7 @@ module API
success Entities::ProjectLabel
end
params do
- optional :label_id, type: Integer, desc: 'The id of the label to be updated'
+ optional :label_id, type: Integer, desc: 'The ID of the label to be updated'
optional :name, type: String, desc: 'The name of the label to be updated'
use :project_label_update_params
exactly_one_of :label_id, :name
@@ -71,7 +71,7 @@ module API
success Entities::ProjectLabel
end
params do
- optional :label_id, type: Integer, desc: 'The id of the label to be deleted'
+ optional :label_id, type: Integer, desc: 'The ID of the label to be deleted'
optional :name, type: String, desc: 'The name of the label to be deleted'
exactly_one_of :label_id, :name
end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 803de51651a..9bea74e2ce9 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -62,7 +62,7 @@ module API
get ":id/members/:user_id" do
source = find_source(source_type, params[:id])
- members = source.members
+ members = source_members(source)
member = members.find_by!(user_id: params[:user_id])
present_members member
diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb
index 27ef0b9c7cd..00f42703731 100644
--- a/lib/api/merge_request_approvals.rb
+++ b/lib/api/merge_request_approvals.rb
@@ -4,7 +4,7 @@ module API
class MergeRequestApprovals < ::API::Base
before { authenticate_non_get! }
- feature_category :code_review
+ feature_category :source_code_management
helpers do
params :ee_approval_params do
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index d17e451093b..ab0e9b95e4a 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -11,6 +11,7 @@ module API
feature_category :code_review
helpers Helpers::MergeRequestsHelpers
+ helpers Helpers::SSEHelpers
# EE::API::MergeRequests would override the following helpers
helpers do
@@ -216,6 +217,8 @@ module API
handle_merge_request_errors!(merge_request)
+ Gitlab::UsageDataCounters::EditorUniqueCounter.track_sse_edit_action(author: current_user) if request_from_sse?(user_project)
+
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb
deleted file mode 100644
index 65a85f3c930..00000000000
--- a/lib/api/nuget_packages.rb
+++ /dev/null
@@ -1,247 +0,0 @@
-# frozen_string_literal: true
-
-# NuGet Package Manager Client API
-#
-# These API endpoints are not meant to be consumed directly by users. They are
-# called by the NuGet package manager client when users run commands
-# like `nuget install` or `nuget push`.
-module API
- class NugetPackages < ::API::Base
- helpers ::API::Helpers::PackagesManagerClientsHelpers
- helpers ::API::Helpers::Packages::BasicAuthHelpers
-
- feature_category :package_registry
-
- POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze
- NON_NEGATIVE_INTEGER_REGEX = %r{\A0|[1-9]\d*\z}.freeze
-
- PACKAGE_FILENAME = 'package.nupkg'
-
- default_format :json
-
- rescue_from ArgumentError do |e|
- render_api_error!(e.message, 400)
- end
-
- helpers do
- def find_packages
- packages = package_finder.execute
-
- not_found!('Packages') unless packages.exists?
-
- packages
- end
-
- def find_package
- package = package_finder(package_version: params[:package_version]).execute
- .first
-
- not_found!('Package') unless package
-
- package
- end
-
- def package_finder(finder_params = {})
- ::Packages::Nuget::PackageFinder.new(
- authorized_user_project,
- **finder_params.merge(package_name: params[:package_name])
- )
- end
- end
-
- before do
- require_packages_enabled!
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project', regexp: POSITIVE_INTEGER_REGEX
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- before do
- authorized_user_project
- end
-
- namespace ':id/packages/nuget' 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'
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- get 'index', format: :json do
- authorize_read_package!(authorized_user_project)
-
- track_package_event('cli_metadata', :nuget)
-
- present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project),
- with: ::API::Entities::Nuget::ServiceIndex
- end
-
- # 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'
- end
-
- params do
- requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- put do
- authorize_upload!(authorized_user_project)
- bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
-
- file_params = params.merge(
- file: params[:package],
- file_name: PACKAGE_FILENAME
- )
-
- package = ::Packages::Nuget::CreatePackageService.new(authorized_user_project, current_user)
- .execute
-
- package_file = ::Packages::CreatePackageFileService.new(package, file_params)
- .execute
-
- track_package_event('push_package', :nuget)
-
- ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
-
- created!
- rescue ObjectStorage::RemoteStoreError => e
- Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
-
- forbidden!
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- put 'authorize' do
- authorize_workhorse!(
- subject: authorized_user_project,
- has_length: false,
- maximum_size: authorized_user_project.actual_limits.nuget_max_file_size
- )
- end
-
- params do
- requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX
- end
- namespace '/metadata/*package_name' do
- before do
- authorize_read_package!(authorized_user_project)
- end
-
- # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
- desc 'The NuGet Metadata Service - Package name level' do
- detail 'This feature was introduced in GitLab 12.8'
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- get 'index', format: :json do
- present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages),
- 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'
- end
- params do
- requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- get '*package_version', format: :json do
- present ::Packages::Nuget::PackageMetadataPresenter.new(find_package),
- with: ::API::Entities::Nuget::PackageMetadata
- end
- 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
- end
- namespace '/download/*package_name' do
- before do
- authorize_read_package!(authorized_user_project)
- end
-
- desc 'The NuGet Content Service - index request' do
- detail 'This feature was introduced in GitLab 12.8'
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- get 'index', format: :json do
- present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages),
- with: ::API::Entities::Nuget::PackagesVersions
- end
-
- desc 'The NuGet Content Service - content request' do
- detail 'This feature was introduced in GitLab 12.8'
- end
- params do
- requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
- requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- get '*package_version/*package_filename', format: :nupkg do
- filename = "#{params[:package_filename]}.#{params[:format]}"
- package_file = ::Packages::PackageFileFinder.new(find_package, filename, with_file_name_like: true)
- .execute
-
- not_found!('Package') unless package_file
-
- track_package_event('pull_package', :nuget)
-
- # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
- present_carrierwave_file!(package_file.file, supports_direct_download: false)
- end
- end
-
- params do
- requires :q, type: String, desc: 'The search term'
- optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX
- optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX
- optional :prerelease, type: Boolean, desc: 'Include prerelease versions', default: true
- end
- namespace '/query' do
- before do
- authorize_read_package!(authorized_user_project)
- end
-
- # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
- desc 'The NuGet Search Service' do
- detail 'This feature was introduced in GitLab 12.8'
- end
-
- route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
-
- get format: :json do
- search_options = {
- include_prerelease_versions: params[:prerelease],
- per_page: params[:take],
- padding: params[:skip]
- }
- search = Packages::Nuget::SearchService
- .new(authorized_user_project, params[:q], search_options)
- .execute
-
- track_package_event('search_package', :nuget)
-
- present ::Packages::Nuget::SearchResultsPresenter.new(search),
- with: ::API::Entities::Nuget::SearchResults
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
new file mode 100644
index 00000000000..b2516cc91f8
--- /dev/null
+++ b/lib/api/nuget_project_packages.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+# NuGet Package Manager Client API
+#
+# These API endpoints are not meant to be consumed directly by users. They are
+# called by the NuGet package manager client when users run commands
+# like `nuget install` or `nuget push`.
+module API
+ class NugetProjectPackages < ::API::Base
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+ helpers ::API::Helpers::Packages::BasicAuthHelpers
+
+ feature_category :package_registry
+
+ PACKAGE_FILENAME = 'package.nupkg'
+
+ default_format :json
+
+ rescue_from ArgumentError do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ before do
+ require_packages_enabled!
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ authorized_user_project
+ end
+
+ 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'
+ end
+
+ params do
+ requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ put do
+ authorize_upload!(authorized_user_project)
+ bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
+
+ file_params = params.merge(
+ file: params[:package],
+ file_name: PACKAGE_FILENAME
+ )
+
+ package = ::Packages::Nuget::CreatePackageService.new(
+ authorized_user_project,
+ current_user,
+ declared_params.merge(build: current_authenticated_job)
+ ).execute
+
+ package_file = ::Packages::CreatePackageFileService.new(
+ package,
+ file_params.merge(build: current_authenticated_job)
+ ).execute
+
+ track_package_event('push_package', :nuget, category: 'API::NugetPackages')
+
+ ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
+
+ created!
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
+
+ forbidden!
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ put 'authorize' do
+ authorize_workhorse!(
+ subject: authorized_user_project,
+ has_length: false,
+ maximum_size: authorized_user_project.actual_limits.nuget_max_file_size
+ )
+ 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
+ end
+ namespace '/download/*package_name' do
+ before do
+ authorize_read_package!(authorized_user_project)
+ end
+
+ desc 'The NuGet Content Service - index request' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ get 'index', format: :json do
+ present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages),
+ with: ::API::Entities::Nuget::PackagesVersions
+ end
+
+ desc 'The NuGet Content Service - content request' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ params do
+ requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
+ requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
+
+ get '*package_version/*package_filename', format: :nupkg do
+ filename = "#{params[:package_filename]}.#{params[:format]}"
+ package_file = ::Packages::PackageFileFinder.new(find_package, filename, with_file_name_like: true)
+ .execute
+
+ not_found!('Package') unless package_file
+
+ track_package_event('pull_package', :nuget, category: 'API::NugetPackages')
+
+ # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
+ present_carrierwave_file!(package_file.file, supports_direct_download: false)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb
index cfb0c5fd705..6785b28ddef 100644
--- a/lib/api/project_clusters.rb
+++ b/lib/api/project_clusters.rb
@@ -83,6 +83,8 @@ module API
optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
+ optional :enabled, type: Boolean, desc: 'Determines if cluster is active or not'
+ optional :managed, type: Boolean, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb
index fe6de3ea385..196b7d88500 100644
--- a/lib/api/project_repository_storage_moves.rb
+++ b/lib/api/project_repository_storage_moves.rb
@@ -34,6 +34,22 @@ module API
present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
end
+
+ desc 'Schedule bulk project repository storage moves' do
+ detail 'This feature was introduced in GitLab 13.7.'
+ end
+ params do
+ requires :source_storage_name, type: String, desc: 'The source storage shard', values: -> { Gitlab.config.repositories.storages.keys }
+ optional :destination_storage_name, type: String, desc: 'The destination storage shard', values: -> { Gitlab.config.repositories.storages.keys }
+ end
+ post do
+ ::Projects::ScheduleBulkRepositoryShardMovesService.enqueue(
+ declared_params[:source_storage_name],
+ declared_params[:destination_storage_name]
+ )
+
+ accepted!
+ end
end
params do
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index 7104fb8d999..658c6d13847 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -127,7 +127,7 @@ module API
track_package_event('push_package', :pypi)
::Packages::Pypi::CreatePackageService
- .new(authorized_user_project, current_user, declared_params)
+ .new(authorized_user_project, current_user, declared_params.merge(build: current_authenticated_job))
.execute
created!
diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb
index d3a185a51c8..52c73104bb4 100644
--- a/lib/api/release/links.rb
+++ b/lib/api/release/links.rb
@@ -57,7 +57,7 @@ module API
end
params do
- requires :link_id, type: String, desc: 'The id of the link'
+ requires :link_id, type: String, desc: 'The ID of the link'
end
resource 'links/:link_id' do
desc 'Get a link detail of a release' do
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index b95856d99d1..b3f09b431b0 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -52,6 +52,7 @@ module API
optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
+ optional :disable_feed_token, type: Boolean, desc: 'Disable display of RSS/Atom and Calendar `feed_tokens`'
optional :disabled_oauth_sign_in_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Disable certain OAuth sign-in sources'
optional :domain_denylist_enabled, type: Boolean, desc: 'Enable domain denylist for sign ups'
optional :domain_denylist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
@@ -102,6 +103,11 @@ module API
optional :performance_bar_allowed_group_id, type: String, desc: 'Deprecated: Use :performance_bar_allowed_group_path instead. Path of the group that is allowed to toggle the performance bar.' # support legacy names, can be removed in v6
optional :performance_bar_allowed_group_path, type: String, desc: 'Path of the group that is allowed to toggle the performance bar.'
optional :performance_bar_enabled, type: String, desc: 'Deprecated: Pass `performance_bar_allowed_group_path: nil` instead. Allow enabling the performance.' # support legacy names, can be removed in v6
+ optional :personal_access_token_prefix, type: String, desc: 'Prefix to prepend to all personal access tokens'
+ optional :kroki_enabled, type: Boolean, desc: 'Enable Kroki'
+ given kroki_enabled: ->(val) { val } do
+ requires :kroki_url, type: String, desc: 'The Kroki server URL'
+ end
optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
given plantuml_enabled: ->(val) { val } do
requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb
index 1814e1a6782..6818c04fd2e 100644
--- a/lib/api/statistics.rb
+++ b/lib/api/statistics.rb
@@ -4,7 +4,7 @@ module API
class Statistics < ::API::Base
before { authenticated_as_admin! }
- feature_category :instance_statistics
+ feature_category :devops_reports
COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
MergeRequest, Note, Snippet, Key, Milestone].freeze
diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb
index 7b038ec74bb..cad2f52e951 100644
--- a/lib/api/usage_data.rb
+++ b/lib/api/usage_data.rb
@@ -20,6 +20,18 @@ module API
requires :event, type: String, desc: 'The event name that should be tracked'
end
+ post 'increment_counter' do
+ event_name = params[:event]
+
+ increment_counter(event_name)
+
+ status :ok
+ end
+
+ params do
+ requires :event, type: String, desc: 'The event name that should be tracked'
+ end
+
post 'increment_unique_users' do
event_name = params[:event]
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 501ed629c7e..8b9b82877f7 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -534,6 +534,24 @@ module API
user.activate
end
+
+ desc 'Approve a pending user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ post ':id/approve', feature_category: :authentication_and_authorization do
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless can?(current_user, :read_user, user)
+
+ result = ::Users::ApproveService.new(current_user).execute(user)
+
+ if result[:success]
+ result
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
# rubocop: enable CodeReuse/ActiveRecord
desc 'Deactivate an active user. Available only for admins.'
params do
diff --git a/lib/api/validations/validators/absence.rb b/lib/api/validations/validators/absence.rb
index 1f43f3ab126..7858ce7140b 100644
--- a/lib/api/validations/validators/absence.rb
+++ b/lib/api/validations/validators/absence.rb
@@ -7,7 +7,7 @@ module API
def validate_param!(attr_name, params)
return if params.respond_to?(:key?) && !params.key?(attr_name)
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
+ raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:absence))
end
end
end
diff --git a/lib/api/validations/validators/array_none_any.rb b/lib/api/validations/validators/array_none_any.rb
index 7efb8e6ccee..3732c1f575c 100644
--- a/lib/api/validations/validators/array_none_any.rb
+++ b/lib/api/validations/validators/array_none_any.rb
@@ -10,8 +10,10 @@ module API
return if value.is_a?(Array) ||
[IssuableFinder::Params::FILTER_NONE, IssuableFinder::Params::FILTER_ANY].include?(value.to_s.downcase)
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
- message: "should be an array, 'None' or 'Any'"
+ raise Grape::Exceptions::Validation.new(
+ params: [@scope.full_name(attr_name)],
+ message: "should be an array, 'None' or 'Any'"
+ )
end
end
end
diff --git a/lib/api/validations/validators/check_assignees_count.rb b/lib/api/validations/validators/check_assignees_count.rb
index b614058e325..92ada159b46 100644
--- a/lib/api/validations/validators/check_assignees_count.rb
+++ b/lib/api/validations/validators/check_assignees_count.rb
@@ -18,9 +18,10 @@ module API
def validate_param!(attr_name, params)
return if param_allowed?(attr_name, params)
- raise Grape::Exceptions::Validation,
- params: [@scope.full_name(attr_name)],
- message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}"
+ raise Grape::Exceptions::Validation.new(
+ params: [@scope.full_name(attr_name)],
+ message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}"
+ )
end
private
diff --git a/lib/api/validations/validators/email_or_email_list.rb b/lib/api/validations/validators/email_or_email_list.rb
index b7f2a0cd443..da665f39130 100644
--- a/lib/api/validations/validators/email_or_email_list.rb
+++ b/lib/api/validations/validators/email_or_email_list.rb
@@ -11,9 +11,10 @@ module API
return if value.split(',').map { |v| ValidateEmail.valid?(v) }.all?
- raise Grape::Exceptions::Validation,
+ raise Grape::Exceptions::Validation.new(
params: [@scope.full_name(attr_name)],
message: "contains an invalid email address"
+ )
end
end
end
diff --git a/lib/api/validations/validators/file_path.rb b/lib/api/validations/validators/file_path.rb
index 8a815c3b2b8..a6a3c692fd6 100644
--- a/lib/api/validations/validators/file_path.rb
+++ b/lib/api/validations/validators/file_path.rb
@@ -11,8 +11,10 @@ module API
path = Gitlab::Utils.check_path_traversal!(path)
Gitlab::Utils.check_allowed_absolute_path!(path, path_allowlist)
rescue
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
- message: "should be a valid file path"
+ raise Grape::Exceptions::Validation.new(
+ params: [@scope.full_name(attr_name)],
+ message: "should be a valid file path"
+ )
end
end
end
diff --git a/lib/api/validations/validators/git_ref.rb b/lib/api/validations/validators/git_ref.rb
index 1dda9d758a7..dcb1db6ca33 100644
--- a/lib/api/validations/validators/git_ref.rb
+++ b/lib/api/validations/validators/git_ref.rb
@@ -17,8 +17,10 @@ module API
return unless invalid_character?(revision)
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
- message: 'should be a valid reference path'
+ raise Grape::Exceptions::Validation.new(
+ params: [@scope.full_name(attr_name)],
+ message: 'should be a valid reference path'
+ )
end
private
diff --git a/lib/api/validations/validators/git_sha.rb b/lib/api/validations/validators/git_sha.rb
index 657307db1df..665d1878b4c 100644
--- a/lib/api/validations/validators/git_sha.rb
+++ b/lib/api/validations/validators/git_sha.rb
@@ -9,8 +9,10 @@ module API
return if Commit::EXACT_COMMIT_SHA_PATTERN.match?(sha)
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
- message: "should be a valid sha"
+ raise Grape::Exceptions::Validation.new(
+ params: [@scope.full_name(attr_name)],
+ message: "should be a valid sha"
+ )
end
end
end
diff --git a/lib/api/validations/validators/integer_none_any.rb b/lib/api/validations/validators/integer_none_any.rb
index aa8c137a6ab..32ab6e19b98 100644
--- a/lib/api/validations/validators/integer_none_any.rb
+++ b/lib/api/validations/validators/integer_none_any.rb
@@ -3,15 +3,11 @@
module API
module Validations
module Validators
- class IntegerNoneAny < Grape::Validations::Base
- def validate_param!(attr_name, params)
- value = params[attr_name]
+ class IntegerNoneAny < IntegerOrCustomValue
+ private
- return if value.is_a?(Integer) ||
- [IssuableFinder::Params::FILTER_NONE, IssuableFinder::Params::FILTER_ANY].include?(value.to_s.downcase)
-
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
- message: "should be an integer, 'None' or 'Any'"
+ def extract_custom_values(_options)
+ [IssuableFinder::Params::FILTER_NONE, IssuableFinder::Params::FILTER_ANY]
end
end
end
diff --git a/lib/api/validations/validators/integer_or_custom_value.rb b/lib/api/validations/validators/integer_or_custom_value.rb
new file mode 100644
index 00000000000..d2352495948
--- /dev/null
+++ b/lib/api/validations/validators/integer_or_custom_value.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module API
+ module Validations
+ module Validators
+ class IntegerOrCustomValue < Grape::Validations::Base
+ def initialize(attrs, options, required, scope, **opts)
+ @custom_values = extract_custom_values(options)
+ super
+ end
+
+ def validate_param!(attr_name, params)
+ value = params[attr_name]
+
+ 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)
+ raise Grape::Exceptions::Validation.new(
+ params: [@scope.full_name(attr_name)],
+ message: "should be #{valid_options}, however got #{value}"
+ )
+ end
+
+ private
+
+ def extract_custom_values(options)
+ options.is_a?(Hash) ? options[:values] : options
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/validations/validators/limit.rb b/lib/api/validations/validators/limit.rb
index 3bb4cee1d75..e8f894849a5 100644
--- a/lib/api/validations/validators/limit.rb
+++ b/lib/api/validations/validators/limit.rb
@@ -9,9 +9,10 @@ module API
return if value.size <= @option
- raise Grape::Exceptions::Validation,
+ raise Grape::Exceptions::Validation.new(
params: [@scope.full_name(attr_name)],
message: "#{@scope.full_name(attr_name)} must be less than #{@option} characters"
+ )
end
end
end
diff --git a/lib/api/validations/validators/untrusted_regexp.rb b/lib/api/validations/validators/untrusted_regexp.rb
index ec623684e67..3ddea2bd9de 100644
--- a/lib/api/validations/validators/untrusted_regexp.rb
+++ b/lib/api/validations/validators/untrusted_regexp.rb
@@ -11,7 +11,7 @@ module API
Gitlab::UntrustedRegexp.new(value)
rescue RegexpError => e
message = "is an invalid regexp: #{e.message}"
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message
+ raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message)
end
end
end
diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb
index f81ed462174..da24d0e20ee 100644
--- a/lib/atlassian/jira_connect/client.rb
+++ b/lib/atlassian/jira_connect/client.rb
@@ -12,31 +12,68 @@ module Atlassian
@shared_secret = shared_secret
end
+ def send_info(project:, update_sequence_id: nil, **args)
+ common = { project: project, update_sequence_id: update_sequence_id }
+ dev_info = args.slice(:commits, :branches, :merge_requests)
+ build_info = args.slice(:pipelines)
+
+ responses = []
+
+ responses << store_dev_info(**common, **dev_info) if dev_info.present?
+ responses << store_build_info(**common, **build_info) if build_info.present?
+ raise ArgumentError, 'Invalid arguments' if responses.empty?
+
+ responses.compact
+ end
+
+ private
+
+ def store_build_info(project:, pipelines:, update_sequence_id: nil)
+ return unless Feature.enabled?(:jira_sync_builds, project)
+
+ builds = pipelines.map do |pipeline|
+ build = Serializers::BuildEntity.represent(
+ pipeline,
+ update_sequence_id: update_sequence_id
+ )
+ next if build.issue_keys.empty?
+
+ build
+ end.compact
+ return if builds.empty?
+
+ post('/rest/builds/0.1/bulk', { builds: builds })
+ end
+
def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil, update_sequence_id: nil)
- dev_info_json = {
- repositories: [
- Serializers::RepositoryEntity.represent(
- project,
- commits: commits,
- branches: branches,
- merge_requests: merge_requests,
- user_notes_count: user_notes_count(merge_requests),
- update_sequence_id: update_sequence_id
- )
- ]
- }.to_json
-
- uri = URI.join(@base_uri, '/rest/devinfo/0.10/bulk')
-
- headers = {
+ repo = Serializers::RepositoryEntity.represent(
+ project,
+ commits: commits,
+ branches: branches,
+ merge_requests: merge_requests,
+ user_notes_count: user_notes_count(merge_requests),
+ update_sequence_id: update_sequence_id
+ )
+
+ post('/rest/devinfo/0.10/bulk', { repositories: [repo] })
+ end
+
+ def post(path, payload)
+ uri = URI.join(@base_uri, path)
+
+ self.class.post(uri, headers: headers(uri), body: metadata.merge(payload).to_json)
+ end
+
+ def headers(uri)
+ {
'Authorization' => "JWT #{jwt_token('POST', uri)}",
'Content-Type' => 'application/json'
}
-
- self.class.post(uri, headers: headers, body: dev_info_json)
end
- private
+ def metadata
+ { providerMetadata: { product: "GitLab #{Gitlab::VERSION}" } }
+ end
def user_notes_count(merge_requests)
return unless merge_requests
diff --git a/lib/atlassian/jira_connect/serializers/base_entity.rb b/lib/atlassian/jira_connect/serializers/base_entity.rb
index 94deb174a45..640337c0399 100644
--- a/lib/atlassian/jira_connect/serializers/base_entity.rb
+++ b/lib/atlassian/jira_connect/serializers/base_entity.rb
@@ -11,6 +11,12 @@ module Atlassian
expose :update_sequence_id, as: :updateSequenceId
+ def eql(other)
+ other.is_a?(self.class) && to_json == other.to_json
+ end
+
+ alias_method :==, :eql
+
private
def update_sequence_id
diff --git a/lib/atlassian/jira_connect/serializers/build_entity.rb b/lib/atlassian/jira_connect/serializers/build_entity.rb
new file mode 100644
index 00000000000..3eb8b1f1978
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/build_entity.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ # A Jira 'build' represents what we call a 'pipeline'
+ class BuildEntity < Grape::Entity
+ include Gitlab::Routing
+
+ format_with(:iso8601, &:iso8601)
+
+ expose :schema_version, as: :schemaVersion
+ expose :pipeline_id, as: :pipelineId
+ expose :iid, as: :buildNumber
+ expose :update_sequence_id, as: :updateSequenceNumber
+ expose :source_ref, as: :displayName
+ expose :url
+ expose :state
+ expose :updated_at, as: :lastUpdated, format_with: :iso8601
+ expose :issue_keys, as: :issueKeys
+ expose :test_info, as: :testInfo
+ expose :references
+
+ def issue_keys
+ # extract Jira issue keys from either the source branch/ref or the
+ # merge request title.
+ @issue_keys ||= begin
+ src = "#{pipeline.source_ref} #{pipeline.merge_request&.title}"
+ JiraIssueKeyExtractor.new(src).issue_keys
+ end
+ end
+
+ private
+
+ alias_method :pipeline, :object
+ delegate :project, to: :object
+
+ def url
+ project_pipeline_url(project, pipeline)
+ end
+
+ # translate to Jira status
+ def state
+ case pipeline.status
+ when 'scheduled', 'created', 'pending', 'preparing', 'waiting_for_resource' then 'pending'
+ when 'running' then 'in_progress'
+ when 'success' then 'successful'
+ when 'failed' then 'failed'
+ when 'canceled', 'skipped' then 'cancelled'
+ else
+ 'unknown'
+ end
+ end
+
+ def pipeline_id
+ pipeline.ensure_ci_ref!
+
+ pipeline.ci_ref.id.to_s
+ end
+
+ def schema_version
+ '1.0'
+ end
+
+ def test_info
+ builds = pipeline.builds.pluck(:status) # rubocop: disable CodeReuse/ActiveRecord
+ n = builds.size
+ passed = builds.count { |s| s == 'success' }
+ failed = builds.count { |s| s == 'failed' }
+
+ {
+ totalNumber: n,
+ numberPassed: passed,
+ numberFailed: failed,
+ numberSkipped: n - (passed + failed)
+ }
+ end
+
+ def references
+ ref = pipeline.source_ref
+
+ [{
+ commit: { id: pipeline.sha, repositoryUri: project_url(project) },
+ ref: { name: ref, uri: project_commits_url(project, ref) }
+ }]
+ end
+
+ def update_sequence_id
+ options[:update_sequence_id] || Client.generate_update_sequence_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index a0948f8c0f5..0f6ed847dea 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -26,9 +26,15 @@ module Backup
FileUtils.rm_f(backup_tarball)
if ENV['STRATEGY'] == 'copy'
- cmd = [%w[rsync -a], exclude_dirs(:rsync), %W[#{app_files_dir} #{Gitlab.config.backup.path}]].flatten
+ cmd = [%w[rsync -a --delete], exclude_dirs(:rsync), %W[#{app_files_dir} #{Gitlab.config.backup.path}]].flatten
output, status = Gitlab::Popen.popen(cmd)
+ # Retry if rsync source files vanish
+ if status == 24
+ $stdout.puts "Warning: files vanished during rsync, retrying..."
+ output, status = Gitlab::Popen.popen(cmd)
+ end
+
unless status == 0
puts output
raise Backup::Error, 'Backup failed'
diff --git a/lib/banzai/filter/ascii_doc_sanitization_filter.rb b/lib/banzai/filter/ascii_doc_sanitization_filter.rb
index a1a204ec652..11762c3bfb4 100644
--- a/lib/banzai/filter/ascii_doc_sanitization_filter.rb
+++ b/lib/banzai/filter/ascii_doc_sanitization_filter.rb
@@ -6,8 +6,8 @@ module Banzai
#
# Extends Banzai::Filter::BaseSanitizationFilter with specific rules.
class AsciiDocSanitizationFilter < Banzai::Filter::BaseSanitizationFilter
- # Section anchor link pattern
- SECTION_LINK_REF_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze
+ # Anchor link prefixed by "user-content-" pattern
+ PREFIXED_ID_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze
SECTION_HEADINGS = %w(h2 h3 h4 h5 h6).freeze
# Footnote link patterns
@@ -54,43 +54,34 @@ module Banzai
whitelist[:attributes]['table'] = %w(class)
whitelist[:transformers].push(self.class.remove_element_classes)
+ # Allow `id` in anchor and footnote elements
+ whitelist[:attributes]['a'].push('id')
+ whitelist[:attributes]['div'].push('id')
+
# Allow `id` in heading elements for section anchors
SECTION_HEADINGS.each do |header|
whitelist[:attributes][header] = %w(id)
end
- whitelist[:transformers].push(self.class.remove_non_heading_ids)
- # Allow `id` in footnote elements
- FOOTNOTE_LINK_ID_PATTERNS.keys.each do |element|
- whitelist[:attributes][element.to_s].push('id')
- end
- whitelist[:transformers].push(self.class.remove_non_footnote_ids)
+ # Remove ids that are not explicitly allowed
+ whitelist[:transformers].push(self.class.remove_disallowed_ids)
whitelist
end
class << self
- def remove_non_footnote_ids
+ def remove_disallowed_ids
lambda do |env|
node = env[:node]
- return unless (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym])
+ return unless node.name == 'a' || node.name == 'div' || SECTION_HEADINGS.any?(node.name)
return unless node.has_attribute?('id')
- return if node['id'] =~ pattern
-
- node.remove_attribute('id')
- end
- end
-
- def remove_non_heading_ids
- lambda do |env|
- node = env[:node]
-
- return unless SECTION_HEADINGS.any?(node.name)
- return unless node.has_attribute?('id')
+ return if node['id'] =~ PREFIXED_ID_PATTERN
- return if node['id'] =~ SECTION_LINK_REF_PATTERN
+ if (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym])
+ return if node['id'] =~ pattern
+ end
node.remove_attribute('id')
end
diff --git a/lib/banzai/filter/base_sanitization_filter.rb b/lib/banzai/filter/base_sanitization_filter.rb
index fc3791e0cbf..4f9e8cffd11 100644
--- a/lib/banzai/filter/base_sanitization_filter.rb
+++ b/lib/banzai/filter/base_sanitization_filter.rb
@@ -25,7 +25,7 @@ module Banzai
# Allow data-math-style attribute in order to support LaTeX formatting
whitelist[:attributes]['code'] = %w(data-math-style)
- whitelist[:attributes]['pre'] = %w(data-math-style data-mermaid-style)
+ whitelist[:attributes]['pre'] = %w(data-math-style data-mermaid-style data-kroki-style)
# Allow html5 details/summary elements
whitelist[:elements].push('details')
diff --git a/lib/banzai/filter/kroki_filter.rb b/lib/banzai/filter/kroki_filter.rb
new file mode 100644
index 00000000000..dbd4de32a47
--- /dev/null
+++ b/lib/banzai/filter/kroki_filter.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "nokogiri"
+require "asciidoctor/extensions/asciidoctor_kroki/extension"
+
+module Banzai
+ module Filter
+ # HTML that replaces all diagrams supported by Kroki with the corresponding img tags.
+ #
+ class KrokiFilter < HTML::Pipeline::Filter
+ def call
+ return doc unless settings.kroki_enabled
+
+ diagram_selectors = ::Gitlab::Kroki.formats(settings)
+ .map { |diagram_type| %(pre[lang="#{diagram_type}"] > code) }
+ .join(', ')
+
+ return doc unless doc.at(diagram_selectors)
+
+ diagram_format = "svg"
+ doc.css(diagram_selectors).each do |node|
+ diagram_type = node.parent['lang']
+ img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>))
+ node.parent.replace(img_tag)
+ end
+
+ doc
+ end
+
+ private
+
+ def create_image_src(type, format, text)
+ ::AsciidoctorExtensions::KrokiDiagram.new(type, format, text)
+ .get_diagram_uri(settings.kroki_url)
+ end
+
+ def settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index f05902078dc..0b8bd17a71b 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -59,7 +59,7 @@ module Banzai
super(object_sym, tooltip: false)
end
- def data_attributes_for(text, parent, object, data = {})
+ def data_attributes_for(text, parent, object, **data)
super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 6dc0cce6050..1d3bbe43344 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'rouge/plugins/common_mark'
+require "asciidoctor/extensions/asciidoctor_kroki/extension"
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js
module Banzai
@@ -14,7 +15,7 @@ module Banzai
LANG_PARAMS_ATTR = 'data-lang-params'
def call
- doc.search('pre:not([data-math-style]):not([data-mermaid-style]) > code').each do |node|
+ doc.search('pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code').each do |node|
highlight_node(node)
end
@@ -86,7 +87,7 @@ module Banzai
end
def use_rouge?(language)
- %w(math mermaid plantuml suggestion).exclude?(language)
+ (%w(math suggestion) + ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES).exclude?(language)
end
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 7057ac9d707..344afc9b33c 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -19,6 +19,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::MathFilter,
Filter::ColorFilter,
+ Filter::KrokiFilter,
Filter::MermaidFilter,
Filter::VideoLinkFilter,
Filter::AudioLinkFilter,
diff --git a/lib/bulk_imports/common/extractors/graphql_extractor.rb b/lib/bulk_imports/common/extractors/graphql_extractor.rb
index 7d58032cfcc..c0cef61d2b2 100644
--- a/lib/bulk_imports/common/extractors/graphql_extractor.rb
+++ b/lib/bulk_imports/common/extractors/graphql_extractor.rb
@@ -6,15 +6,16 @@ module BulkImports
class GraphqlExtractor
def initialize(query)
@query = query[:query]
- @query_string = @query.to_s
- @variables = @query.variables
end
def extract(context)
- @context = context
+ client = graphql_client(context)
Enumerator.new do |yielder|
- result = graphql_client.execute(parsed_query, query_variables(context.entity))
+ result = client.execute(
+ client.parse(query.to_s),
+ query.variables(context.entity)
+ )
yielder << result.original_hash.deep_dup
end
@@ -22,23 +23,17 @@ module BulkImports
private
- def graphql_client
+ attr_reader :query
+
+ def graphql_client(context)
@graphql_client ||= BulkImports::Clients::Graphql.new(
- url: @context.configuration.url,
- token: @context.configuration.access_token
+ url: context.configuration.url,
+ token: context.configuration.access_token
)
end
def parsed_query
- @parsed_query ||= graphql_client.parse(@query.to_s)
- end
-
- def query_variables(entity)
- return unless @variables
-
- @variables.transform_values do |entity_attribute|
- entity.public_send(entity_attribute) # rubocop:disable GitlabSecurity/PublicSend
- end
+ @parsed_query ||= graphql_client.parse(query.to_s)
end
end
end
diff --git a/lib/bulk_imports/common/transformers/graphql_cleaner_transformer.rb b/lib/bulk_imports/common/transformers/graphql_cleaner_transformer.rb
deleted file mode 100644
index dce0fac6999..00000000000
--- a/lib/bulk_imports/common/transformers/graphql_cleaner_transformer.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-# Cleanup GraphQL original response hash from unnecessary nesting
-# 1. Remove ['data']['group'] or ['data']['project'] hash nesting
-# 2. Remove ['edges'] & ['nodes'] array wrappings
-# 3. Remove ['node'] hash wrapping
-#
-# @example
-# data = {"data"=>{"group"=> {
-# "name"=>"test",
-# "fullName"=>"test",
-# "description"=>"test",
-# "labels"=>{"edges"=>[{"node"=>{"title"=>"label1"}}, {"node"=>{"title"=>"label2"}}, {"node"=>{"title"=>"label3"}}]}}}}
-#
-# BulkImports::Common::Transformers::GraphqlCleanerTransformer.new.transform(nil, data)
-#
-# {"name"=>"test", "fullName"=>"test", "description"=>"test", "labels"=>[{"title"=>"label1"}, {"title"=>"label2"}, {"title"=>"label3"}]}
-module BulkImports
- module Common
- module Transformers
- class GraphqlCleanerTransformer
- EDGES = 'edges'
- NODE = 'node'
-
- def initialize(options = {})
- @options = options
- end
-
- def transform(_, data)
- return data unless data.is_a?(Hash)
-
- data = data.dig('data', 'group') || data.dig('data', 'project') || data
-
- clean_edges_and_nodes(data)
- end
-
- def clean_edges_and_nodes(data)
- case data
- when Array
- data.map(&method(:clean_edges_and_nodes))
- when Hash
- if data.key?(NODE)
- clean_edges_and_nodes(data[NODE])
- else
- data.transform_values { |value| clean_edges_and_nodes(value.try(:fetch, EDGES, value) || value) }
- end
- else
- data
- end
- end
- end
- end
- end
-end
diff --git a/lib/bulk_imports/common/transformers/hash_key_digger.rb b/lib/bulk_imports/common/transformers/hash_key_digger.rb
new file mode 100644
index 00000000000..b4897b5b2bf
--- /dev/null
+++ b/lib/bulk_imports/common/transformers/hash_key_digger.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Common
+ module Transformers
+ class HashKeyDigger
+ def initialize(options = {})
+ @key_path = options[:key_path]
+ end
+
+ def transform(_, data)
+ raise ArgumentError, "Given data must be a Hash" unless data.is_a?(Hash)
+
+ data.dig(*Array.wrap(key_path))
+ end
+
+ private
+
+ attr_reader :key_path
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb b/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb
new file mode 100644
index 00000000000..858c4c8976b
--- /dev/null
+++ b/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Common
+ module Transformers
+ class ProhibitedAttributesTransformer
+ PROHIBITED_REFERENCES = Regexp.union(
+ /\Acached_markdown_version\Z/,
+ /\Aid\Z/,
+ /_id\Z/,
+ /_ids\Z/,
+ /_html\Z/,
+ /attributes/,
+ /\Aremote_\w+_(url|urls|request_header)\Z/ # carrierwave automatically creates these attribute methods for uploads
+ ).freeze
+
+ def initialize(options = {})
+ @options = options
+ end
+
+ def transform(context, data)
+ data.each_with_object({}) do |(key, value), result|
+ prohibited = prohibited_key?(key)
+
+ unless prohibited
+ result[key] = value.is_a?(Hash) ? transform(context, value) : value
+ end
+ end
+ end
+
+ private
+
+ def prohibited_key?(key)
+ key.to_s =~ PROHIBITED_REFERENCES
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/groups/graphql/get_group_query.rb b/lib/bulk_imports/groups/graphql/get_group_query.rb
index c50b99aae4e..2bc0f60baa2 100644
--- a/lib/bulk_imports/groups/graphql/get_group_query.rb
+++ b/lib/bulk_imports/groups/graphql/get_group_query.rb
@@ -29,8 +29,8 @@ module BulkImports
GRAPHQL
end
- def variables
- { full_path: :source_full_path }
+ def variables(entity)
+ { full_path: entity.source_full_path }
end
end
end
diff --git a/lib/bulk_imports/groups/pipelines/group_pipeline.rb b/lib/bulk_imports/groups/pipelines/group_pipeline.rb
index 2b7d0ef7658..5169e292180 100644
--- a/lib/bulk_imports/groups/pipelines/group_pipeline.rb
+++ b/lib/bulk_imports/groups/pipelines/group_pipeline.rb
@@ -6,10 +6,13 @@ module BulkImports
class GroupPipeline
include Pipeline
+ abort_on_failure!
+
extractor Common::Extractors::GraphqlExtractor, query: Graphql::GetGroupQuery
- transformer Common::Transformers::GraphqlCleanerTransformer
+ transformer Common::Transformers::HashKeyDigger, key_path: %w[data group]
transformer Common::Transformers::UnderscorifyKeysTransformer
+ transformer Common::Transformers::ProhibitedAttributesTransformer
transformer Groups::Transformers::GroupAttributesTransformer
loader Groups::Loaders::GroupLoader
diff --git a/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb b/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb
index 6384e9d5972..d7e1a118d0b 100644
--- a/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb
+++ b/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb
@@ -7,6 +7,7 @@ module BulkImports
include Pipeline
extractor BulkImports::Groups::Extractors::SubgroupsExtractor
+ transformer Common::Transformers::ProhibitedAttributesTransformer
transformer BulkImports::Groups::Transformers::SubgroupToEntityTransformer
loader BulkImports::Common::Loaders::EntityLoader
end
diff --git a/lib/bulk_imports/importers/group_importer.rb b/lib/bulk_imports/importers/group_importer.rb
index c7253590c87..82cb1ca03a2 100644
--- a/lib/bulk_imports/importers/group_importer.rb
+++ b/lib/bulk_imports/importers/group_importer.rb
@@ -19,6 +19,7 @@ module BulkImports
)
BulkImports::Groups::Pipelines::GroupPipeline.new.run(context)
+ 'BulkImports::EE::Groups::Pipelines::EpicsPipeline'.constantize.new.run(context) if Gitlab.ee?
BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline.new.run(context)
entity.finish!
diff --git a/lib/bulk_imports/pipeline.rb b/lib/bulk_imports/pipeline.rb
index 70e6030ea2c..a44f8fc7193 100644
--- a/lib/bulk_imports/pipeline.rb
+++ b/lib/bulk_imports/pipeline.rb
@@ -3,10 +3,89 @@
module BulkImports
module Pipeline
extend ActiveSupport::Concern
+ include Gitlab::ClassAttributes
included do
- include Attributes
include Runner
+
+ private
+
+ def extractors
+ @extractors ||= self.class.extractors.map(&method(:instantiate))
+ end
+
+ def transformers
+ @transformers ||= self.class.transformers.map(&method(:instantiate))
+ end
+
+ def loaders
+ @loaders ||= self.class.loaders.map(&method(:instantiate))
+ end
+
+ def after_run
+ @after_run ||= self.class.after_run_callback
+ end
+
+ def pipeline
+ @pipeline ||= self.class.name
+ end
+
+ def instantiate(class_config)
+ class_config[:klass].new(class_config[:options])
+ end
+
+ def abort_on_failure?
+ self.class.abort_on_failure?
+ end
+ end
+
+ class_methods do
+ def extractor(klass, options = nil)
+ add_attribute(:extractors, klass, options)
+ end
+
+ def transformer(klass, options = nil)
+ add_attribute(:transformers, klass, options)
+ end
+
+ def loader(klass, options = nil)
+ add_attribute(:loaders, klass, options)
+ end
+
+ def after_run(&block)
+ class_attributes[:after_run] = block
+ end
+
+ def extractors
+ class_attributes[:extractors]
+ end
+
+ def transformers
+ class_attributes[:transformers]
+ end
+
+ def loaders
+ class_attributes[:loaders]
+ end
+
+ def after_run_callback
+ class_attributes[:after_run]
+ end
+
+ def abort_on_failure!
+ class_attributes[:abort_on_failure] = true
+ end
+
+ def abort_on_failure?
+ class_attributes[:abort_on_failure]
+ end
+
+ private
+
+ def add_attribute(sym, klass, options)
+ class_attributes[sym] ||= []
+ class_attributes[sym] << { klass: klass, options: options }
+ end
end
end
end
diff --git a/lib/bulk_imports/pipeline/attributes.rb b/lib/bulk_imports/pipeline/attributes.rb
deleted file mode 100644
index ebfbaf6f6ba..00000000000
--- a/lib/bulk_imports/pipeline/attributes.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module BulkImports
- module Pipeline
- module Attributes
- extend ActiveSupport::Concern
- include Gitlab::ClassAttributes
-
- class_methods do
- def extractor(klass, options = nil)
- add_attribute(:extractors, klass, options)
- end
-
- def transformer(klass, options = nil)
- add_attribute(:transformers, klass, options)
- end
-
- def loader(klass, options = nil)
- add_attribute(:loaders, klass, options)
- end
-
- def add_attribute(sym, klass, options)
- class_attributes[sym] ||= []
- class_attributes[sym] << { klass: klass, options: options }
- end
-
- def extractors
- class_attributes[:extractors]
- end
-
- def transformers
- class_attributes[:transformers]
- end
-
- def loaders
- class_attributes[:loaders]
- end
- end
- end
- end
-end
diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb
index 04038e50399..88b96f0ab6e 100644
--- a/lib/bulk_imports/pipeline/runner.rb
+++ b/lib/bulk_imports/pipeline/runner.rb
@@ -5,57 +5,102 @@ module BulkImports
module Runner
extend ActiveSupport::Concern
- included do
- private
+ MarkedAsFailedError = Class.new(StandardError)
- def extractors
- @extractors ||= self.class.extractors.map(&method(:instantiate))
- end
+ def run(context)
+ raise MarkedAsFailedError if marked_as_failed?(context)
- def transformers
- @transformers ||= self.class.transformers.map(&method(:instantiate))
- end
+ info(context, message: 'Pipeline started', pipeline_class: pipeline)
- def loaders
- @loaders ||= self.class.loaders.map(&method(:instantiate))
- end
+ extractors.each do |extractor|
+ data = run_pipeline_step(:extractor, extractor.class.name, context) do
+ extractor.extract(context)
+ end
- def pipeline_name
- @pipeline ||= self.class.name
- end
+ if data && data.respond_to?(:each)
+ data.each do |entry|
+ transformers.each do |transformer|
+ entry = run_pipeline_step(:transformer, transformer.class.name, context) do
+ transformer.transform(context, entry)
+ end
+ end
- def instantiate(class_config)
- class_config[:klass].new(class_config[:options])
+ loaders.each do |loader|
+ run_pipeline_step(:loader, loader.class.name, context) do
+ loader.load(context, entry)
+ end
+ end
+ end
+ end
end
+
+ after_run.call(context) if after_run.present?
+ rescue MarkedAsFailedError
+ log_skip(context)
end
- def run(context)
- info(context, message: "Pipeline started", pipeline: pipeline_name)
+ private # rubocop:disable Lint/UselessAccessModifier
- extractors.each do |extractor|
- extractor.extract(context).each do |entry|
- info(context, extractor: extractor.class.name)
+ def run_pipeline_step(type, class_name, context)
+ raise MarkedAsFailedError if marked_as_failed?(context)
- transformers.each do |transformer|
- info(context, transformer: transformer.class.name)
- entry = transformer.transform(context, entry)
- end
+ info(context, type => class_name)
- loaders.each do |loader|
- info(context, loader: loader.class.name)
- loader.load(context, entry)
- end
- end
- end
+ yield
+ rescue MarkedAsFailedError
+ log_skip(context, type => class_name)
+ rescue => e
+ log_import_failure(e, context)
+
+ mark_as_failed(context) if abort_on_failure?
end
- private # rubocop:disable Lint/UselessAccessModifier
+ def mark_as_failed(context)
+ warn(context, message: 'Pipeline failed', pipeline_class: pipeline)
+
+ context.entity.fail_op!
+ end
+
+ def marked_as_failed?(context)
+ return true if context.entity.failed?
+
+ false
+ end
+
+ def log_skip(context, extra = {})
+ log = {
+ message: 'Skipping due to failed pipeline status',
+ pipeline_class: pipeline
+ }.merge(extra)
+
+ info(context, log)
+ end
+
+ def log_import_failure(exception, context)
+ attributes = {
+ bulk_import_entity_id: context.entity.id,
+ pipeline_class: pipeline,
+ exception_class: exception.class.to_s,
+ exception_message: exception.message.truncate(255),
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
+ }
+
+ BulkImports::Failure.create(attributes)
+ end
+
+ def warn(context, extra = {})
+ logger.warn(log_base_params(context).merge(extra))
+ end
def info(context, extra = {})
- logger.info({
- entity: context.entity.id,
- entity_type: context.entity.source_type
- }.merge(extra))
+ logger.info(log_base_params(context).merge(extra))
+ end
+
+ def log_base_params(context)
+ {
+ bulk_import_entity_id: context.entity.id,
+ bulk_import_entity_type: context.entity.source_type
+ }
end
def logger
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 3e9cf2ab320..d41490d2ebd 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -4,7 +4,7 @@ module Constraints
class ProjectUrlConstrainer
def matches?(request, existence_check: true)
namespace_path = request.params[:namespace_id]
- project_path = request.params[:project_id] || request.params[:id] || request.params[:repository_id]
+ project_path = request.params[:project_id] || request.params[:id]
full_path = [namespace_path, project_path].join('/')
return false unless ProjectPathValidator.valid_path?(full_path)
diff --git a/lib/constraints/repository_redirect_url_constrainer.rb b/lib/constraints/repository_redirect_url_constrainer.rb
new file mode 100644
index 00000000000..44df670d8d3
--- /dev/null
+++ b/lib/constraints/repository_redirect_url_constrainer.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Constraints
+ class RepositoryRedirectUrlConstrainer
+ def matches?(request)
+ path = request.params[:repository_path].delete_suffix('.git')
+ query = request.query_string
+
+ git_request?(query) && container_path?(path)
+ end
+
+ # Allow /info/refs, /info/refs?service=git-upload-pack, and
+ # /info/refs?service=git-receive-pack, but nothing else.
+ def git_request?(query)
+ query.blank? ||
+ query == 'service=git-upload-pack' ||
+ query == 'service=git-receive-pack'
+ end
+
+ # Check if the path matches any known repository containers.
+ # These also cover wikis, since a `.wiki` suffix is valid in project/group paths too.
+ def container_path?(path)
+ NamespacePathValidator.valid_path?(path) ||
+ ProjectPathValidator.valid_path?(path) ||
+ path =~ Gitlab::PathRegex.full_snippets_repository_path_regex
+ end
+ end
+end
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index 34511423d4a..d130a9d6f82 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -62,8 +62,7 @@ module ExtractsRef
#
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def assign_ref_vars
- @id = get_id
- @ref, @path = extract_ref(@id)
+ @id, @ref, @path = extract_ref_path
@repo = repository_container.repository
raise InvalidPathError if @ref.match?(/\s/)
@@ -76,6 +75,13 @@ module ExtractsRef
@tree ||= @repo.tree(@commit.id, @path) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ def extract_ref_path
+ id = get_id
+ ref, path = extract_ref(id)
+
+ [id, ref, path]
+ end
+
private
def extract_raw_ref(id)
diff --git a/lib/feature.rb b/lib/feature.rb
index 1f8c530bee5..3d4a919b043 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -68,6 +68,9 @@ class Feature
Feature::Definition.valid_usage!(key, type: type, default_enabled: default_enabled)
end
+ # If `default_enabled: :yaml` we fetch the value from the YAML definition instead.
+ default_enabled = Feature::Definition.default_enabled?(key) if default_enabled == :yaml
+
# During setup the database does not exist yet. So we haven't stored a value
# for the feature yet and return the default.
return default_enabled unless Gitlab::Database.exists?
@@ -87,40 +90,39 @@ class Feature
end
def enable(key, thing = true)
+ log(key: key, action: __method__, thing: thing)
get(key).enable(thing)
end
def disable(key, thing = false)
+ log(key: key, action: __method__, thing: thing)
get(key).disable(thing)
end
- def enable_group(key, group)
- get(key).enable_group(group)
- end
-
- def disable_group(key, group)
- get(key).disable_group(group)
- end
-
def enable_percentage_of_time(key, percentage)
+ log(key: key, action: __method__, percentage: percentage)
get(key).enable_percentage_of_time(percentage)
end
def disable_percentage_of_time(key)
+ log(key: key, action: __method__)
get(key).disable_percentage_of_time
end
def enable_percentage_of_actors(key, percentage)
+ log(key: key, action: __method__, percentage: percentage)
get(key).enable_percentage_of_actors(percentage)
end
def disable_percentage_of_actors(key)
+ log(key: key, action: __method__)
get(key).disable_percentage_of_actors
end
def remove(key)
return unless persisted_name?(key)
+ log(key: key, action: __method__)
get(key).remove
end
@@ -136,8 +138,6 @@ class Feature
end
def register_definitions
- return unless check_feature_flags_definition?
-
Feature::Definition.reload!
end
@@ -147,6 +147,10 @@ class Feature
Feature::Definition.register_hot_reloader!
end
+ def logger
+ @logger ||= Feature::Logger.build
+ end
+
private
def flipper
@@ -194,6 +198,14 @@ class Feature
def l2_cache_backend
Rails.cache
end
+
+ def log(key:, action:, **extra)
+ extra ||= {}
+ extra = extra.transform_keys { |k| "extra.#{k}" }
+ extra = extra.transform_values { |v| v.respond_to?(:flipper_id) ? v.flipper_id : v }
+ extra = extra.transform_values(&:to_s)
+ logger.info(key: key, action: action, **extra)
+ end
end
class Target
diff --git a/lib/feature/definition.rb b/lib/feature/definition.rb
index 0ba1bdc4799..8d9b2fa5234 100644
--- a/lib/feature/definition.rb
+++ b/lib/feature/definition.rb
@@ -13,6 +13,12 @@ class Feature
end
end
+ TYPES.each do |type, _|
+ define_method("#{type}?") do
+ attributes[:type].to_sym == type
+ end
+ end
+
def initialize(path, opts = {})
@path = path
@attributes = {}
@@ -65,9 +71,7 @@ class Feature
"a valid syntax: #{TYPES.dig(type, :example)}"
end
- # We accept an array of defaults as some features are undefined
- # and have `default_enabled: true/false`
- unless Array(default_enabled).include?(default_enabled_in_code)
+ unless default_enabled_in_code == :yaml || default_enabled == default_enabled_in_code
# Raise exception in test and dev
raise Feature::InvalidFeatureFlagError, "The `default_enabled:` of `#{key}` is not equal to config: " \
"#{default_enabled_in_code} vs #{default_enabled}. Ensure to update #{path}"
@@ -90,12 +94,20 @@ class Feature
@definitions ||= load_all!
end
+ def get(key)
+ definitions[key.to_sym]
+ end
+
def reload!
@definitions = load_all!
end
+ def has_definition?(key)
+ definitions.has_key?(key.to_sym)
+ end
+
def valid_usage!(key, type:, default_enabled:)
- if definition = definitions[key.to_sym]
+ if definition = get(key)
definition.valid_usage!(type_in_code: type, default_enabled_in_code: default_enabled)
elsif type_definition = self::TYPES[type]
raise InvalidFeatureFlagError, "Missing feature definition for `#{key}`" unless type_definition[:optional]
@@ -104,6 +116,17 @@ class Feature
end
end
+ def default_enabled?(key)
+ if definition = get(key)
+ definition.default_enabled
+ else
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
+ InvalidFeatureFlagError.new("The feature flag YAML definition for '#{key}' does not exist"))
+
+ false
+ end
+ end
+
def register_hot_reloader!
# Reload feature flags on change of this file or any `.yml`
file_watcher = Rails.configuration.file_watcher.new(reload_files, reload_directories) do
@@ -119,10 +142,6 @@ class Feature
private
def load_all!
- # We currently do not load feature flag definitions
- # in production environments
- return [] unless Gitlab.dev_or_test_env?
-
paths.each_with_object({}) do |glob_path, definitions|
load_all_from_path!(definitions, glob_path)
end
diff --git a/lib/feature/logger.rb b/lib/feature/logger.rb
new file mode 100644
index 00000000000..784a619e182
--- /dev/null
+++ b/lib/feature/logger.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Feature
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'features_json'
+ end
+ end
+end
diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb
index 1fcbc8fa173..17dfe26bd82 100644
--- a/lib/feature/shared.rb
+++ b/lib/feature/shared.rb
@@ -23,7 +23,7 @@ class Feature
example: <<-EOS
Feature.enabled?(:my_feature_flag, project)
Feature.enabled?(:my_feature_flag, project, type: :development)
- push_frontend_feature_flag?(:my_feature_flag, project)
+ push_frontend_feature_flag(:my_feature_flag, project)
EOS
},
ops: {
@@ -33,8 +33,8 @@ class Feature
ee_only: false,
default_enabled: false,
example: <<-EOS
- Feature.enabled?(:my_ops_flag, type: ops)
- push_frontend_feature_flag?(:my_ops_flag, project, type: :ops)
+ Feature.enabled?(:my_ops_flag, type: :ops)
+ push_frontend_feature_flag(:my_ops_flag, project, type: :ops)
EOS
},
licensed: {
@@ -48,6 +48,16 @@ class Feature
project.feature_available?(:my_licensed_feature)
namespace.feature_available?(:my_licensed_feature)
EOS
+ },
+ experiment: {
+ description: 'Short lived, used specifically to run A/B/n experiments.',
+ optional: true,
+ rollout_issue: true,
+ ee_only: true,
+ default_enabled: false,
+ example: <<-EOS
+ experiment(:my_experiment, project: project, actor: current_user) { ...variant code... }
+ EOS
}
}.freeze
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 43785d165fb..0f2fd01e3c7 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -115,4 +115,10 @@ module Gitlab
'web'
end
+
+ def self.maintenance_mode?
+ return false unless ::Feature.enabled?(:maintenance_mode)
+
+ ::Gitlab::CurrentSettings.maintenance_mode
+ end
end
diff --git a/lib/gitlab/alert_management/payload.rb b/lib/gitlab/alert_management/payload.rb
index 177d544d720..ce09ffd87ee 100644
--- a/lib/gitlab/alert_management/payload.rb
+++ b/lib/gitlab/alert_management/payload.rb
@@ -4,7 +4,8 @@ module Gitlab
module AlertManagement
module Payload
MONITORING_TOOLS = {
- prometheus: 'Prometheus'
+ prometheus: 'Prometheus',
+ cilium: 'Cilium'
}.freeze
class << self
diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
index fc91dd6e138..22aa680cbc1 100644
--- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb
+++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
@@ -22,6 +22,10 @@ module Gitlab
]
end
+ def self.find_by_name!(name)
+ all.find { |raw_stage| raw_stage[:name].to_s.eql?(name.to_s) } || raise("Default stage '#{name}' not found")
+ end
+
def self.names
all.map { |stage| stage[:name] }
end
diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb
index b4bbb309c36..84fe3d1c959 100644
--- a/lib/gitlab/application_context.rb
+++ b/lib/gitlab/application_context.rb
@@ -30,7 +30,7 @@ module Gitlab
Labkit::Context.current.to_h.include?(Labkit::Context.log_key(attribute_name))
end
- def initialize(args)
+ def initialize(**args)
unknown_attributes = args.keys - APPLICATION_ATTRIBUTES.map(&:name)
raise ArgumentError, "#{unknown_attributes} are not known keys" if unknown_attributes.any?
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index e92bbe4f529..fbba86d1253 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -34,7 +34,8 @@ module Gitlab
group_testing_hook: { threshold: 5, interval: 1.minute },
profile_add_new_email: { threshold: 5, interval: 1.minute },
profile_resend_email_confirmation: { threshold: 5, interval: 1.minute },
- update_environment_canary_ingress: { threshold: 1, interval: 1.minute }
+ update_environment_canary_ingress: { threshold: 1, interval: 1.minute },
+ auto_rollback_deployment: { threshold: 1, interval: 3.minutes }
}.freeze
end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 5cacd7e5983..a9c2dd001cb 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -2,6 +2,7 @@
require 'asciidoctor'
require 'asciidoctor-plantuml'
+require 'asciidoctor/extensions/asciidoctor_kroki/extension'
require 'asciidoctor/extensions'
require 'gitlab/asciidoc/html5_converter'
require 'gitlab/asciidoc/mermaid_block_processor'
@@ -23,7 +24,14 @@ module Gitlab
'source-highlighter' => 'gitlab-html-pipeline',
'icons' => 'font',
'outfilesuffix' => '.adoc',
- 'max-include-depth' => MAX_INCLUDE_DEPTH
+ 'max-include-depth' => MAX_INCLUDE_DEPTH,
+ # This feature is disabled because it relies on File#read to read the file.
+ # If we want to enable this feature we will need to provide a "GitLab compatible" implementation.
+ # This attribute is typically used to share common config (skinparam...) across all PlantUML diagrams.
+ # The value can be a path or a URL.
+ 'kroki-plantuml-include!' => '',
+ # This feature is disabled because it relies on the local file system to save diagrams retrieved from the Kroki server.
+ 'kroki-fetch-diagram!' => ''
}.freeze
def self.path_attrs(path)
@@ -48,12 +56,21 @@ module Gitlab
extensions = proc do
include_processor ::Gitlab::Asciidoc::IncludeProcessor.new(context)
block ::Gitlab::Asciidoc::MermaidBlockProcessor
+ ::Gitlab::Kroki.formats(Gitlab::CurrentSettings).each do |name|
+ block ::AsciidoctorExtensions::KrokiBlockProcessor, name
+ end
end
extra_attrs = path_attrs(context[:requested_path])
asciidoc_opts = { safe: :secure,
backend: :gitlab_html5,
- attributes: DEFAULT_ADOC_ATTRS.merge(extra_attrs),
+ attributes: DEFAULT_ADOC_ATTRS
+ .merge(extra_attrs)
+ .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
+ }),
extensions: extensions }
context[:pipeline] = :ascii_doc
diff --git a/lib/gitlab/asciidoc/html5_converter.rb b/lib/gitlab/asciidoc/html5_converter.rb
index e5163e1954c..787eab27503 100644
--- a/lib/gitlab/asciidoc/html5_converter.rb
+++ b/lib/gitlab/asciidoc/html5_converter.rb
@@ -19,6 +19,12 @@ module Gitlab
%(<code#{id_attribute(node)} data-math-style="inline">#{node.text}</code>)
end
+ def convert_inline_anchor(node)
+ node.id = "user-content-#{node.id}" if node.id && !node.id.start_with?('user-content-')
+
+ super(node)
+ end
+
private
def id_attribute(node)
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index fadd6eb848d..1aabb05f19e 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -196,11 +196,9 @@ module Gitlab
return unless token
- return if project && token.user.project_bot? && !project.bots.include?(token.user)
-
return unless valid_scoped_token?(token, all_available_scopes)
- if token.user.project_bot? || token.user.can?(:log_in)
+ if token.user.can?(:log_in) || token.user.can?(:bot_log_in, project)
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end
end
@@ -285,7 +283,7 @@ module Gitlab
return unless build.project.builds_enabled?
if build.user
- return unless build.user.can?(:log_in)
+ return unless build.user.can?(:log_in) || build.user.can?(:bot_log_in, build.project)
# If user is assigned to build, use restricted credentials of user
Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index f3975fe219a..caa881eeeab 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -46,6 +46,7 @@ module Gitlab
def find_user_from_feed_token(request_format)
return unless valid_rss_format?(request_format)
+ return if Gitlab::CurrentSettings.disable_feed_token
# NOTE: feed_token was renamed from rss_token but both needs to be supported because
# users might have already added the feed to their RSS reader before the rename
@@ -193,6 +194,10 @@ module Gitlab
def access_token
strong_memoize(:access_token) do
+ # The token can be a PAT or an OAuth (doorkeeper) token
+ # It is also possible that a PAT is encapsulated in a `Bearer` OAuth token
+ # (e.g. NPM client registry auth), this case will be properly handled
+ # by find_personal_access_token
find_oauth_access_token || find_personal_access_token
end
end
@@ -236,7 +241,7 @@ module Gitlab
end
def matches_personal_access_token_length?(token)
- token.length == PersonalAccessToken::TOKEN_LENGTH
+ PersonalAccessToken::TOKEN_LENGTH_RANGE.include?(token.length)
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
diff --git a/lib/gitlab/auth/crowd/authentication.rb b/lib/gitlab/auth/crowd/authentication.rb
new file mode 100644
index 00000000000..7f3e980034e
--- /dev/null
+++ b/lib/gitlab/auth/crowd/authentication.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Crowd
+ class Authentication < Gitlab::Auth::OAuth::Authentication
+ def login(login, password)
+ return unless Gitlab::Auth::OAuth::Provider.enabled?(@provider)
+ return unless login.present? && password.present?
+
+ user_info = user_info_from_authentication(login, password)
+ return unless user_info&.key?(:user)
+
+ Gitlab::Auth::OAuth::User.find_by_uid_and_provider(user_info[:user], provider)
+ end
+
+ private
+
+ def config
+ gitlab_crowd_config = Gitlab::Auth::OAuth::Provider.config_for(@provider)
+ raise "OmniAuth Crowd is not configured." unless gitlab_crowd_config && gitlab_crowd_config[:args]
+
+ OmniAuth::Strategies::Crowd::Configuration.new(
+ gitlab_crowd_config[:args].symbolize_keys)
+ end
+
+ def user_info_from_authentication(login, password)
+ validator = OmniAuth::Strategies::Crowd::CrowdValidator.new(
+ config, login, password, RequestContext.instance.client_ip, nil)
+ validator&.user_info&.symbolize_keys
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index 88cc840c395..f5931a1d5eb 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -53,6 +53,10 @@ module Gitlab
raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}")
end
+ def self.encrypted_secrets
+ Settings.encrypted(Gitlab.config.ldap.secret_file)
+ end
+
def initialize(provider)
if self.class.valid_provider?(provider)
@provider = provider
@@ -89,8 +93,8 @@ module Gitlab
if has_auth?
opts.merge!(
- bind_dn: options['bind_dn'],
- password: options['password']
+ bind_dn: auth_username,
+ password: auth_password
)
end
@@ -155,7 +159,7 @@ module Gitlab
end
def has_auth?
- options['password'] || options['bind_dn']
+ auth_password || auth_username
end
def allow_username_or_email_login
@@ -267,12 +271,32 @@ module Gitlab
{
auth: {
method: :simple,
- username: options['bind_dn'],
- password: options['password']
+ username: auth_username,
+ password: auth_password
}
}
end
+ def secrets
+ @secrets ||= self.class.encrypted_secrets[@provider.delete_prefix('ldap').to_sym]
+ rescue => e
+ Gitlab::AppLogger.error "LDAP encrypted secrets are invalid: #{e.inspect}"
+
+ nil
+ end
+
+ def auth_password
+ return options['password'] if options['password']
+
+ secrets&.fetch(:password, nil)&.chomp
+ end
+
+ def auth_username
+ return options['bind_dn'] if options['bind_dn']
+
+ secrets&.fetch(:bind_dn, nil)&.chomp
+ end
+
def omniauth_user_filter
uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
index 1405fb4ab95..814c17b7e44 100644
--- a/lib/gitlab/auth/ldap/user.rb
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -11,16 +11,6 @@ module Gitlab
module Ldap
class User < Gitlab::Auth::OAuth::User
extend ::Gitlab::Utils::Override
- class << self
- # rubocop: disable CodeReuse/ActiveRecord
- def find_by_uid_and_provider(uid, provider)
- identity = ::Identity.with_extern_uid(provider, uid).take
-
- identity && identity.user
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
def save
super('LDAP')
end
@@ -30,10 +20,6 @@ module Gitlab
find_by_uid_and_provider || find_by_email || build_new_user
end
- def find_by_uid_and_provider
- self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
- end
-
override :should_save?
def should_save?
gl_user.changed? || gl_user.identities.any?(&:changed?)
diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb
index 1eae7af442d..57ff3fcd1f0 100644
--- a/lib/gitlab/auth/o_auth/provider.rb
+++ b/lib/gitlab/auth/o_auth/provider.rb
@@ -18,6 +18,8 @@ module Gitlab
authenticator =
case provider
+ when /crowd/
+ Gitlab::Auth::Crowd::Authentication
when /^ldap/
Gitlab::Auth::Ldap::Authentication
when 'database'
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index 3211d2ffaea..f556a7f40e9 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -9,6 +9,16 @@ module Gitlab
module Auth
module OAuth
class User
+ class << self
+ # rubocop: disable CodeReuse/ActiveRecord
+ def find_by_uid_and_provider(uid, provider)
+ identity = ::Identity.with_extern_uid(provider, uid).take
+
+ identity && identity.user
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
SignupDisabledError = Class.new(StandardError)
SigninDisabledForProviderError = Class.new(StandardError)
@@ -190,15 +200,12 @@ module Gitlab
@auth_hash = AuthHash.new(auth_hash)
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_by_uid_and_provider
- identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
- identity&.user
+ self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
- # rubocop: enable CodeReuse/ActiveRecord
- def build_new_user
- user_params = user_attributes.merge(skip_confirmation: true)
+ def build_new_user(skip_confirmation: true)
+ user_params = user_attributes.merge(skip_confirmation: skip_confirmation)
Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
end
diff --git a/lib/gitlab/auth/otp/fortinet.rb b/lib/gitlab/auth/otp/fortinet.rb
new file mode 100644
index 00000000000..a561e97dfcd
--- /dev/null
+++ b/lib/gitlab/auth/otp/fortinet.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+module Gitlab
+ module Auth
+ module Otp
+ module Fortinet
+ private
+
+ def forti_authenticator_enabled?(user)
+ ::Gitlab.config.forti_authenticator.enabled &&
+ Feature.enabled?(:forti_authenticator, user)
+ end
+
+ def forti_token_cloud_enabled?(user)
+ ::Gitlab.config.forti_token_cloud.enabled &&
+ Feature.enabled?(:forti_token_cloud, user)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/otp/session_enforcer.rb b/lib/gitlab/auth/otp/session_enforcer.rb
new file mode 100644
index 00000000000..8cc280756cc
--- /dev/null
+++ b/lib/gitlab/auth/otp/session_enforcer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Otp
+ class SessionEnforcer
+ OTP_SESSIONS_NAMESPACE = 'session:otp'
+ DEFAULT_EXPIRATION = 15.minutes.to_i
+
+ def initialize(key)
+ @key = key
+ end
+
+ def update_session
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.setex(key_name, DEFAULT_EXPIRATION, true)
+ end
+ end
+
+ def access_restricted?
+ Gitlab::Redis::SharedState.with do |redis|
+ !redis.get(key_name)
+ end
+ end
+
+ private
+
+ attr_reader :key
+
+ def key_name
+ @key_name ||= "#{OTP_SESSIONS_NAMESPACE}:#{key.id}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/otp/strategies/base.rb b/lib/gitlab/auth/otp/strategies/base.rb
index 718630e0e31..7d8513642c4 100644
--- a/lib/gitlab/auth/otp/strategies/base.rb
+++ b/lib/gitlab/auth/otp/strategies/base.rb
@@ -25,6 +25,10 @@ module Gitlab
result
end
+
+ def error_from_response(response)
+ error(response.message, response.code)
+ end
end
end
end
diff --git a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb
index fbcb9fd8cdb..c1433f05db2 100644
--- a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb
+++ b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb
@@ -17,7 +17,10 @@ module Gitlab
# Successful authentication results in HTTP 200: OK
# https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/704555/authentication-auth
- response.ok? ? success : error(message: response.message, http_status: response.code)
+ response.ok? ? success : error_from_response(response)
+ rescue StandardError => ex
+ Gitlab::AppLogger.error(ex)
+ error(ex.message)
end
private
@@ -32,7 +35,7 @@ module Gitlab
def api_credentials
{ username: ::Gitlab.config.forti_authenticator.username,
- password: ::Gitlab.config.forti_authenticator.token }
+ password: ::Gitlab.config.forti_authenticator.access_token }
end
end
end
diff --git a/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb b/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb
new file mode 100644
index 00000000000..d7506eca242
--- /dev/null
+++ b/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Otp
+ module Strategies
+ class FortiTokenCloud < Base
+ include Gitlab::Utils::StrongMemoize
+ BASE_API_URL = 'https://ftc.fortinet.com:9696/api/v1'
+
+ def validate(otp_code)
+ if access_token_create_response.created?
+ otp_verification_response = verify_otp(otp_code)
+
+ otp_verification_response.ok? ? success : error_from_response(otp_verification_response)
+ else
+ error_from_response(access_token_create_response)
+ end
+ end
+
+ private
+
+ # TODO: Cache the access token: https://gitlab.com/gitlab-org/gitlab/-/issues/292437
+ def access_token_create_response
+ # Returns '201 CREATED' on successful creation of a new access token.
+ strong_memoize(:access_token_create_response) do
+ post(
+ url: url('/login'),
+ body: {
+ client_id: ::Gitlab.config.forti_token_cloud.client_id,
+ client_secret: ::Gitlab.config.forti_token_cloud.client_secret
+ }.to_json
+ )
+ end
+ end
+
+ def access_token
+ Gitlab::Json.parse(access_token_create_response)['access_token']
+ end
+
+ def verify_otp(otp_code)
+ # Returns '200 OK' on successful verification.
+ # Uses the access token created via `access_token_create_response` as the auth token.
+ post(
+ url: url('/auth'),
+ headers: { 'Authorization': "Bearer #{access_token}" },
+ body: {
+ username: user.username,
+ token: otp_code
+ }.to_json
+ )
+ end
+
+ def url(path)
+ BASE_API_URL + path
+ end
+
+ def post(url:, body:, headers: {})
+ Gitlab::HTTP.post(
+ url,
+ headers: {
+ 'Content-Type': 'application/json'
+ }.merge(headers),
+ body: body,
+ verify: false # FTC API Docs specifically mentions to turn off SSL Verification while making requests.
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index c6216fa9cad..d28ee54cfbc 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -49,9 +49,16 @@ module Gitlab
private
+ def access_token
+ strong_memoize(:access_token) do
+ super || find_personal_access_token_from_http_basic_auth
+ end
+ end
+
def route_authentication_setting
@route_authentication_setting ||= {
- job_token_allowed: api_request?
+ job_token_allowed: api_request?,
+ basic_auth_personal_access_token: api_request?
}
end
end
diff --git a/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities.rb b/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities.rb
new file mode 100644
index 00000000000..68c91650d93
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class updates vulnerabilities entities with state dismissed
+ class PopulateDismissedStateForVulnerabilities
+ class Vulnerability < ActiveRecord::Base # rubocop:disable Style/Documentation
+ self.table_name = 'vulnerabilities'
+ end
+
+ def perform(*vulnerability_ids)
+ Vulnerability.where(id: vulnerability_ids).update_all(state: 2)
+ PopulateMissingVulnerabilityDismissalInformation.new.perform(*vulnerability_ids)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb b/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb
index bc0a181a06c..04342fdabd4 100644
--- a/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb
+++ b/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb
@@ -26,13 +26,16 @@ module Gitlab
class Finding < ActiveRecord::Base # rubocop:disable Style/Documentation
include ShaAttribute
+ include ::Gitlab::Utils::StrongMemoize
self.table_name = 'vulnerability_occurrences'
sha_attribute :project_fingerprint
def dismissal_feedback
- Feedback.dismissal.where(category: report_type, project_fingerprint: project_fingerprint, project_id: project_id).first
+ strong_memoize(:dismissal_feedback) do
+ Feedback.dismissal.where(category: report_type, project_fingerprint: project_fingerprint, project_id: project_id).first
+ end
end
end
diff --git a/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb
index a0c89cc4664..2e81b1615d8 100644
--- a/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb
+++ b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb
@@ -5,7 +5,7 @@ module Gitlab
# This class creates/updates those project historical vulnerability statistics
# that haven't been created nor initialized. It should only be executed in EE.
class PopulateVulnerabilityHistoricalStatistics
- def perform(project_ids)
+ def perform(project_ids, retention_period = 90)
end
end
end
diff --git a/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth.rb b/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth.rb
new file mode 100644
index 00000000000..d97765cd398
--- /dev/null
+++ b/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class UpdateExistingUsersThatRequireTwoFactorAuth # rubocop:disable Metrics/ClassLength
+ def perform(start_id, stop_id)
+ ActiveRecord::Base.connection.execute <<~SQL
+ UPDATE
+ users
+ SET
+ require_two_factor_authentication_from_group = FALSE
+ WHERE
+ users.id BETWEEN #{start_id}
+ AND #{stop_id}
+ AND users.require_two_factor_authentication_from_group = TRUE
+ AND users.id NOT IN ( SELECT DISTINCT
+ users_groups_query.user_id
+ FROM (
+ SELECT
+ users.id AS user_id,
+ members.source_id AS group_ids
+ FROM
+ users
+ LEFT JOIN members ON members.source_type = 'Namespace'
+ AND members.requested_at IS NULL
+ AND members.user_id = users.id
+ AND members.type = 'GroupMember'
+ WHERE
+ users.require_two_factor_authentication_from_group = TRUE
+ AND users.id BETWEEN #{start_id}
+ AND #{stop_id}) AS users_groups_query
+ INNER JOIN LATERAL ( WITH RECURSIVE "base_and_ancestors" AS (
+ (
+ SELECT
+ "namespaces"."type",
+ "namespaces"."id",
+ "namespaces"."parent_id",
+ "namespaces"."require_two_factor_authentication"
+ FROM
+ "namespaces"
+ WHERE
+ "namespaces"."type" = 'Group'
+ AND "namespaces"."id" = users_groups_query.group_ids)
+ UNION (
+ SELECT
+ "namespaces"."type",
+ "namespaces"."id",
+ "namespaces"."parent_id",
+ "namespaces"."require_two_factor_authentication"
+ FROM
+ "namespaces",
+ "base_and_ancestors"
+ WHERE
+ "namespaces"."type" = 'Group'
+ AND "namespaces"."id" = "base_and_ancestors"."parent_id")),
+ "base_and_descendants" AS (
+ (
+ SELECT
+ "namespaces"."type",
+ "namespaces"."id",
+ "namespaces"."parent_id",
+ "namespaces"."require_two_factor_authentication"
+ FROM
+ "namespaces"
+ WHERE
+ "namespaces"."type" = 'Group'
+ AND "namespaces"."id" = users_groups_query.group_ids)
+ UNION (
+ SELECT
+ "namespaces"."type",
+ "namespaces"."id",
+ "namespaces"."parent_id",
+ "namespaces"."require_two_factor_authentication"
+ FROM
+ "namespaces",
+ "base_and_descendants"
+ WHERE
+ "namespaces"."type" = 'Group'
+ AND "namespaces"."parent_id" = "base_and_descendants"."id"))
+ SELECT
+ "namespaces".*
+ FROM ((
+ SELECT
+ "namespaces"."type",
+ "namespaces"."id",
+ "namespaces"."parent_id",
+ "namespaces"."require_two_factor_authentication"
+ FROM
+ "base_and_ancestors" AS "namespaces"
+ WHERE
+ "namespaces"."type" = 'Group')
+ UNION (
+ SELECT
+ "namespaces"."type",
+ "namespaces"."id",
+ "namespaces"."parent_id",
+ "namespaces"."require_two_factor_authentication"
+ FROM
+ "base_and_descendants" AS "namespaces"
+ WHERE
+ "namespaces"."type" = 'Group')) namespaces
+ WHERE
+ "namespaces"."type" = 'Group'
+ AND "namespaces"."require_two_factor_authentication" = TRUE) AS hierarchy_tree ON TRUE);
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/batch_pop_queueing.rb b/lib/gitlab/batch_pop_queueing.rb
index 61011abddf5..e18f1320ea4 100644
--- a/lib/gitlab/batch_pop_queueing.rb
+++ b/lib/gitlab/batch_pop_queueing.rb
@@ -9,7 +9,7 @@ module Gitlab
# and the following items wait until the next items have been popped from the queue.
# On the other hand, this queueing system, the former part is same, however,
# it pops the enqueued items as batch. This is especially useful when you want to
- # drop redandant items from the queue in order to process important items only,
+ # drop redundant items from the queue in order to process important items only,
# thus it's more efficient than the traditional queueing system.
#
# Caveats:
diff --git a/lib/gitlab/batch_worker_context.rb b/lib/gitlab/batch_worker_context.rb
index 0589206fefc..9bc877fcd8d 100644
--- a/lib/gitlab/batch_worker_context.rb
+++ b/lib/gitlab/batch_worker_context.rb
@@ -23,7 +23,7 @@ module Gitlab
def context_by_arguments
@context_by_arguments ||= objects.each_with_object({}) do |object, result|
arguments = Array.wrap(arguments_proc.call(object))
- context = Gitlab::ApplicationContext.new(context_proc.call(object))
+ context = Gitlab::ApplicationContext.new(**context_proc.call(object))
result[arguments] = context
end
diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb
index 8780b410a07..c0b228dee59 100644
--- a/lib/gitlab/checks/diff_check.rb
+++ b/lib/gitlab/checks/diff_check.rb
@@ -17,17 +17,26 @@ module Gitlab
file_paths = []
- process_commits do |commit|
- validate_once(commit) do
- commit.raw_deltas.each do |diff|
- file_paths.concat([diff.new_path, diff.old_path].compact)
+ if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project, default_enabled: true)
+ paths = project.repository.find_changed_paths(commits.map(&:sha))
+ paths.each do |path|
+ file_paths.concat([path.path])
- validate_diff(diff)
+ validate_diff(path)
+ end
+ else
+ process_commits do |commit|
+ validate_once(commit) do
+ commit.raw_deltas.each do |diff|
+ file_paths.concat([diff.new_path, diff.old_path].compact)
+
+ validate_diff(diff)
+ end
end
end
end
- validate_file_paths(file_paths)
+ validate_file_paths(file_paths.uniq)
end
private
diff --git a/lib/gitlab/checks/push_check.rb b/lib/gitlab/checks/push_check.rb
index 7cc5bc56cbb..47aa25aae4c 100644
--- a/lib/gitlab/checks/push_check.rb
+++ b/lib/gitlab/checks/push_check.rb
@@ -14,7 +14,7 @@ module Gitlab
private
def can_push?
- user_access.can_do_action?(:push_code) ||
+ user_access.can_push_for_ref?(ref) ||
project.branch_allows_collaboration?(user_access.user, branch_name)
end
end
diff --git a/lib/gitlab/checks/snippet_check.rb b/lib/gitlab/checks/snippet_check.rb
index 8c61b782baa..d5efbfcc5bc 100644
--- a/lib/gitlab/checks/snippet_check.rb
+++ b/lib/gitlab/checks/snippet_check.rb
@@ -10,12 +10,13 @@ module Gitlab
ATTRIBUTES = %i[oldrev newrev ref branch_name tag_name logger].freeze
attr_reader(*ATTRIBUTES)
- def initialize(change, default_branch:, logger:)
+ def initialize(change, default_branch:, root_ref:, logger:)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@tag_name = Gitlab::Git.tag_name(@ref)
@default_branch = default_branch
+ @root_ref = root_ref
@logger = logger
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end
@@ -26,12 +27,21 @@ module Gitlab
end
true
+ rescue GitAccess::ForbiddenError => e
+ Gitlab::ErrorTracking.log_exception(e, default_branch: @default_branch, branch_name: @branch_name, creation: creation?, deletion: deletion?)
+
+ raise e
end
private
+ # If the `root_ref` is not present means that this is the first commit to the
+ # repository and when the default branch is going to be created.
+ # We allow the first branch creation no matter the name because
+ # it can be even an imported snippet from an instance with a different
+ # default branch.
def creation?
- @branch_name != @default_branch && super
+ super && @root_ref && (@branch_name != @default_branch)
end
end
end
diff --git a/lib/gitlab/checks/timed_logger.rb b/lib/gitlab/checks/timed_logger.rb
index f365e0a43f6..0db38d32bb3 100644
--- a/lib/gitlab/checks/timed_logger.rb
+++ b/lib/gitlab/checks/timed_logger.rb
@@ -31,7 +31,7 @@ module Gitlab
args = { cancelled: true }
args[:start] = start if timed
- append_message(log_message + time_suffix_message(args))
+ append_message(log_message + time_suffix_message(**args))
raise TimeoutError
end
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
index 6d152c052dc..ddf40296809 100644
--- a/lib/gitlab/ci/ansi2json/converter.rb
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -22,8 +22,7 @@ module Gitlab
start_offset = @state.offset
- @state.new_line!(
- style: Style.new(@state.inherited_style))
+ @state.new_line!(style: Style.new(**@state.inherited_style))
stream.each_line do |line|
consume_line(line)
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index a500a0cc35d..a39afee194c 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -6,18 +6,31 @@ module Gitlab
class Rules
include ::Gitlab::Utils::StrongMemoize
- Result = Struct.new(:when, :start_in, :allow_failure) do
- def build_attributes
+ Result = Struct.new(:when, :start_in, :allow_failure, :variables) do
+ def build_attributes(seed_attributes = {})
{
when: self.when,
options: { start_in: start_in }.compact,
- allow_failure: allow_failure
+ allow_failure: allow_failure,
+ yaml_variables: yaml_variables(seed_attributes[:yaml_variables])
}.compact
end
def pass?
self.when != 'never'
end
+
+ private
+
+ def yaml_variables(seed_variables)
+ return unless variables && seed_variables
+
+ indexed_seed_variables = seed_variables.deep_dup.index_by { |var| var[:key] }
+
+ variables.each_with_object(indexed_seed_variables) do |var, hash|
+ hash[var[0].to_s] = { key: var[0].to_s, value: var[1], public: true }
+ end.values
+ end
end
def initialize(rule_hashes, default_when:)
@@ -32,7 +45,8 @@ module Gitlab
Result.new(
matched_rule.attributes[:when] || @default_when,
matched_rule.attributes[:start_in],
- matched_rule.attributes[:allow_failure]
+ matched_rule.attributes[:allow_failure],
+ matched_rule.attributes[:variables]
)
else
Result.new('never')
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
index cbecce57163..9c2f6eea1dd 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -11,7 +11,7 @@ module Gitlab
def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
- expanded_globs = expand_globs(pipeline, context)
+ expanded_globs = expand_globs(context)
pipeline.modified_paths.any? do |path|
expanded_globs.any? do |glob|
File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
@@ -19,8 +19,7 @@ module Gitlab
end
end
- def expand_globs(pipeline, context)
- return @globs unless ::Feature.enabled?(:ci_variable_expansion_in_rules_changes, pipeline.project, default_enabled: true)
+ def expand_globs(context)
return @globs unless context
@globs.map do |glob|
diff --git a/lib/gitlab/ci/config/entry/allow_failure.rb b/lib/gitlab/ci/config/entry/allow_failure.rb
new file mode 100644
index 00000000000..de768c3a03b
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/allow_failure.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents allow_failure settings.
+ #
+ class AllowFailure < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Validatable
+
+ ALLOWED_KEYS = %i[exit_codes].freeze
+ attributes ALLOWED_KEYS
+
+ validations do
+ validates :config, hash_or_boolean: true
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :exit_codes, array_of_integers_or_integer: true, allow_nil: true
+ end
+
+ def value
+ @config[:exit_codes] = Array.wrap(exit_codes) if exit_codes.present?
+ @config
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb
index 70fcc1d586a..e8e2eef281e 100644
--- a/lib/gitlab/ci/config/entry/bridge.rb
+++ b/lib/gitlab/ci/config/entry/bridge.rb
@@ -22,6 +22,7 @@ module Gitlab
in: ALLOWED_WHEN,
message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
}
+ validates :allow_failure, boolean: true
end
validate on: :composed do
@@ -47,7 +48,7 @@ module Gitlab
inherit: false,
metadata: { allowed_needs: %i[job bridge] }
- attributes :when
+ attributes :when, :allow_failure
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -72,6 +73,10 @@ module Gitlab
def bridge_needs
needs_value[:bridge] if needs_value
end
+
+ def ignored?
+ allow_failure.nil? ? manual_action? : allow_failure
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 1ce7060df22..85e3514499c 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -31,6 +31,7 @@ module Gitlab
validates :dependencies, array_of_strings: true
validates :resource_group, type: String
+ validates :allow_failure, hash_or_boolean: true
end
validates :start_in, duration: { limit: '1 week' }, if: :delayed?
@@ -117,9 +118,14 @@ module Gitlab
description: 'Parallel configuration for this job.',
inherit: false
+ entry :allow_failure, ::Gitlab::Ci::Config::Entry::AllowFailure,
+ description: 'Indicates whether this job is allowed to fail or not.',
+ inherit: false
+
attributes :script, :tags, :when, :dependencies,
:needs, :retry, :parallel, :start_in,
- :interruptible, :timeout, :resource_group, :release
+ :interruptible, :timeout, :resource_group,
+ :release, :allow_failure
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -166,11 +172,32 @@ module Gitlab
release: release_value,
after_script: after_script_value,
ignore: ignored?,
+ allow_failure_criteria: allow_failure_criteria,
needs: needs_defined? ? needs_value : nil,
resource_group: resource_group,
scheduling_type: needs_defined? ? :dag : :stage
).compact
end
+
+ def ignored?
+ allow_failure_defined? ? static_allow_failure : manual_action?
+ end
+
+ private
+
+ def allow_failure_criteria
+ return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled?
+
+ if allow_failure_defined? && allow_failure_value.is_a?(Hash)
+ allow_failure_value
+ end
+ end
+
+ def static_allow_failure
+ return false if allow_failure_value.is_a?(Hash)
+
+ allow_failure_value
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/need.rb b/lib/gitlab/ci/config/entry/need.rb
index abfffb7a5ed..46191eca842 100644
--- a/lib/gitlab/ci/config/entry/need.rb
+++ b/lib/gitlab/ci/config/entry/need.rb
@@ -8,7 +8,19 @@ module Gitlab
strategy :JobString, if: -> (config) { config.is_a?(String) }
strategy :JobHash,
- if: -> (config) { config.is_a?(Hash) && config.key?(:job) && !(config.key?(:project) || config.key?(:ref)) }
+ if: -> (config) { config.is_a?(Hash) && same_pipeline_need?(config) }
+
+ strategy :CrossPipelineDependency,
+ if: -> (config) { config.is_a?(Hash) && cross_pipeline_need?(config) }
+
+ def self.same_pipeline_need?(config)
+ config.key?(:job) &&
+ !(config.key?(:project) || config.key?(:ref) || config.key?(:pipeline))
+ end
+
+ def self.cross_pipeline_need?(config)
+ config.key?(:job) && config.key?(:pipeline) && !config.key?(:project)
+ end
class JobString < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
@@ -50,6 +62,30 @@ module Gitlab
end
end
+ class CrossPipelineDependency < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+
+ ALLOWED_KEYS = %i[pipeline job artifacts].freeze
+ attributes :pipeline, :job, :artifacts
+
+ validations do
+ validates :config, presence: true
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :pipeline, type: String, presence: true
+ validates :job, type: String, presence: true
+ validates :artifacts, boolean: true, allow_nil: true
+ end
+
+ def type
+ :cross_dependency
+ end
+
+ def value
+ super.merge(artifacts: artifacts || artifacts.nil?)
+ end
+ end
+
class UnknownStrategy < ::Gitlab::Config::Entry::Node
def type
end
diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb
index 66cd57b8cf3..dd01cfeedff 100644
--- a/lib/gitlab/ci/config/entry/needs.rb
+++ b/lib/gitlab/ci/config/entry/needs.rb
@@ -10,6 +10,8 @@ module Gitlab
class Needs < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
+ NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT = 5
+
validations do
validate do
unless config.is_a?(Hash) || config.is_a?(Array)
@@ -27,6 +29,15 @@ module Gitlab
errors.add(:config, "uses invalid types: #{extra_keys.join(', ')}")
end
end
+
+ validate on: :composed do
+ cross_dependencies = value[:cross_dependency].to_a
+ cross_pipeline_dependencies = cross_dependencies.select { |dep| dep[:pipeline] }
+
+ if cross_pipeline_dependencies.size > NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT
+ errors.add(:config, "must be less than or equal to #{NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT}")
+ end
+ end
end
def value
diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb
index c0315e5f901..5ef8cfbddb7 100644
--- a/lib/gitlab/ci/config/entry/processable.rb
+++ b/lib/gitlab/ci/config/entry/processable.rb
@@ -32,7 +32,6 @@ module Gitlab
with_options allow_nil: true do
validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true
- validates :allow_failure, boolean: true
end
end
@@ -65,7 +64,7 @@ module Gitlab
inherit: false,
default: {}
- attributes :extends, :rules, :allow_failure
+ attributes :extends, :rules
end
def compose!(deps = nil)
@@ -141,10 +140,6 @@ module Gitlab
def manual_action?
self.when == 'manual'
end
-
- def ignored?
- allow_failure.nil? ? manual_action? : allow_failure
- end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb
index 2d93f1ab06e..54ef84b965a 100644
--- a/lib/gitlab/ci/config/entry/root.rb
+++ b/lib/gitlab/ci/config/entry/root.rb
@@ -50,6 +50,7 @@ module Gitlab
entry :variables, Entry::Variables,
description: 'Environment variables that will be used.',
+ metadata: { use_value_data: true },
reserved: true
entry :stages, Entry::Stages,
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
index 8ffd49b8a93..840f2d6f31a 100644
--- a/lib/gitlab/ci/config/entry/rules/rule.rb
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -6,14 +6,18 @@ module Gitlab
module Entry
class Rules::Rule < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
CLAUSES = %i[if changes exists].freeze
- ALLOWED_KEYS = %i[if changes exists when start_in allow_failure].freeze
+ ALLOWED_KEYS = %i[if changes exists when start_in allow_failure variables].freeze
ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze
attributes :if, :changes, :exists, :when, :start_in, :allow_failure
+ entry :variables, Entry::Variables,
+ description: 'Environment variables to define for rule conditions.'
+
validations do
validates :config, presence: true
validates :config, type: { with: Hash }
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index e258f7128fc..dc164d752be 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -13,7 +13,8 @@ module Gitlab
ALLOWED_VALUE_DATA = %i[value description].freeze
validations do
- validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA }
+ validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA }, if: :use_value_data?
+ validates :config, variables: true, unless: :use_value_data?
end
def value
@@ -28,6 +29,10 @@ module Gitlab
Hash[@config.map { |key, value| [key.to_s, expand_value(value)] }]
end
+ def use_value_data?
+ opt(:use_value_data)
+ end
+
private
def expand_value(value)
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 661189eea50..af1df933b36 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -55,12 +55,8 @@ module Gitlab
::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false)
end
- def self.manual_bridges_enabled?(project)
- ::Feature.enabled?(:ci_manual_bridges, project, default_enabled: true)
- end
-
- def self.auto_rollback_available?(project)
- ::Feature.enabled?(:cd_auto_rollback, project) && project&.feature_available?(:auto_rollback)
+ def self.pipeline_open_merge_requests?(project)
+ ::Feature.enabled?(:ci_pipeline_open_merge_requests, project, default_enabled: false)
end
def self.seed_block_run_before_workflow_rules_enabled?(project)
@@ -70,6 +66,14 @@ module Gitlab
def self.ci_pipeline_editor_page_enabled?(project)
::Feature.enabled?(:ci_pipeline_editor_page, project, default_enabled: false)
end
+
+ def self.allow_failure_with_exit_codes_enabled?
+ ::Feature.enabled?(:ci_allow_failure_with_exit_codes)
+ end
+
+ def self.rules_variables_enabled?(project)
+ ::Feature.enabled?(:ci_rules_variables, project, default_enabled: false)
+ end
end
end
end
diff --git a/lib/gitlab/ci/limit.rb b/lib/gitlab/ci/limit.rb
new file mode 100644
index 00000000000..c22a3c503d5
--- /dev/null
+++ b/lib/gitlab/ci/limit.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ ##
+ # Abstract base class for CI/CD Quotas
+ #
+ class Limit
+ LimitExceededError = Class.new(StandardError)
+
+ def initialize(_context, _resource)
+ end
+
+ def enabled?
+ raise NotImplementedError
+ end
+
+ def exceeded?
+ raise NotImplementedError
+ end
+
+ def message
+ raise NotImplementedError
+ end
+
+ def log_error!(extra_context = {})
+ error = LimitExceededError.new(message)
+ # TODO: change this to Gitlab::ErrorTracking.log_exception(error, extra_context)
+ # https://gitlab.com/gitlab-org/gitlab/issues/32906
+ ::Gitlab::ErrorTracking.track_exception(error, extra_context)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb
index e5a7151b823..062ced9e234 100644
--- a/lib/gitlab/ci/mask_secret.rb
+++ b/lib/gitlab/ci/mask_secret.rb
@@ -9,11 +9,7 @@ module Gitlab
# We assume 'value' must be mutable, given
# that frozen string is enabled.
- ##
- # TODO We need to remove this because it is going to change checksum of
- # a trace.
- #
- value.gsub!(token, 'x' * token.length)
+ value.gsub!(token, 'x' * token.bytesize)
value
end
end
diff --git a/lib/gitlab/ci/parsers.rb b/lib/gitlab/ci/parsers.rb
index 0e44475607b..57f73c265b2 100644
--- a/lib/gitlab/ci/parsers.rb
+++ b/lib/gitlab/ci/parsers.rb
@@ -10,7 +10,8 @@ module Gitlab
junit: ::Gitlab::Ci::Parsers::Test::Junit,
cobertura: ::Gitlab::Ci::Parsers::Coverage::Cobertura,
terraform: ::Gitlab::Ci::Parsers::Terraform::Tfplan,
- accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y
+ accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y,
+ codequality: ::Gitlab::Ci::Parsers::Codequality::CodeClimate
}
end
diff --git a/lib/gitlab/ci/parsers/codequality/code_climate.rb b/lib/gitlab/ci/parsers/codequality/code_climate.rb
new file mode 100644
index 00000000000..628d50b84cb
--- /dev/null
+++ b/lib/gitlab/ci/parsers/codequality/code_climate.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ module Codequality
+ class CodeClimate
+ def parse!(json_data, codequality_report)
+ root = Gitlab::Json.parse(json_data)
+
+ parse_all(root, codequality_report)
+ rescue JSON::ParserError => e
+ codequality_report.set_error_message("JSON parsing failed: #{e}")
+ end
+
+ private
+
+ def parse_all(root, codequality_report)
+ return unless root.present?
+
+ root.each do |degradation|
+ break unless codequality_report.add_degradation(degradation)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/coverage/cobertura.rb b/lib/gitlab/ci/parsers/coverage/cobertura.rb
index 934c797580c..1edcbac2f25 100644
--- a/lib/gitlab/ci/parsers/coverage/cobertura.rb
+++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb
@@ -5,50 +5,113 @@ module Gitlab
module Parsers
module Coverage
class Cobertura
- CoberturaParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
+ InvalidXMLError = Class.new(Gitlab::Ci::Parsers::ParserError)
+ InvalidLineInformationError = Class.new(Gitlab::Ci::Parsers::ParserError)
- def parse!(xml_data, coverage_report)
+ GO_SOURCE_PATTERN = '/usr/local/go/src'
+ MAX_SOURCES = 100
+
+ def parse!(xml_data, coverage_report, project_path: nil, worktree_paths: nil)
root = Hash.from_xml(xml_data)
- parse_all(root, coverage_report)
+ context = {
+ project_path: project_path,
+ paths: worktree_paths&.to_set,
+ sources: []
+ }
+
+ parse_all(root, coverage_report, context)
rescue Nokogiri::XML::SyntaxError
- raise CoberturaParserError, "XML parsing failed"
- rescue
- raise CoberturaParserError, "Cobertura parsing failed"
+ raise InvalidXMLError, "XML parsing failed"
end
private
- def parse_all(root, coverage_report)
+ def parse_all(root, coverage_report, context)
return unless root.present?
root.each do |key, value|
- parse_node(key, value, coverage_report)
+ parse_node(key, value, coverage_report, context)
end
end
- def parse_node(key, value, coverage_report)
- return if key == 'sources'
-
- if key == 'class'
+ def parse_node(key, value, coverage_report, context)
+ if key == 'sources' && value['source'].present?
+ parse_sources(value['source'], context)
+ elsif key == 'package'
Array.wrap(value).each do |item|
- parse_class(item, coverage_report)
+ parse_package(item, coverage_report, context)
+ end
+ elsif key == 'class'
+ # This means the cobertura XML does not have classes within package nodes.
+ # This is possible in some cases like in simple JS project structures
+ # running Jest.
+ Array.wrap(value).each do |item|
+ parse_class(item, coverage_report, context)
end
elsif value.is_a?(Hash)
- parse_all(value, coverage_report)
+ parse_all(value, coverage_report, context)
elsif value.is_a?(Array)
value.each do |item|
- parse_all(item, coverage_report)
+ parse_all(item, coverage_report, context)
end
end
end
- def parse_class(file, coverage_report)
+ def parse_sources(sources, context)
+ return unless context[:project_path] && context[:paths]
+
+ sources = Array.wrap(sources)
+
+ # TODO: Go cobertura has a different format with how their packages
+ # are included in the filename. So we can't rely on the sources.
+ # We'll deal with this later.
+ return if sources.include?(GO_SOURCE_PATTERN)
+
+ sources.each do |source|
+ source = build_source_path(source, context)
+ context[:sources] << source if source.present?
+ end
+ end
+
+ def build_source_path(source, context)
+ # | raw source | extracted |
+ # |-----------------------------|------------|
+ # | /builds/foo/test/SampleLib/ | SampleLib/ |
+ # | /builds/foo/test/something | something |
+ # | /builds/foo/test/ | nil |
+ # | /builds/foo/test | nil |
+ source.split("#{context[:project_path]}/", 2)[1]
+ end
+
+ def parse_package(package, coverage_report, context)
+ classes = package.dig('classes', 'class')
+ return unless classes.present?
+
+ matched_filenames = Array.wrap(classes).map do |item|
+ parse_class(item, coverage_report, context)
+ end
+
+ # Remove these filenames from the paths to avoid conflict
+ # with other packages that may contain the same class filenames
+ remove_matched_filenames(matched_filenames, context)
+ end
+
+ def remove_matched_filenames(filenames, context)
+ return unless context[:paths]
+
+ filenames.each { |f| context[:paths].delete(f) }
+ end
+
+ def parse_class(file, coverage_report, context)
return unless file["filename"].present? && file["lines"].present?
parsed_lines = parse_lines(file["lines"])
+ filename = determine_filename(file["filename"], context)
+
+ coverage_report.add_file(filename, Hash[parsed_lines]) if filename
- coverage_report.add_file(file["filename"], Hash[parsed_lines])
+ filename
end
def parse_lines(lines)
@@ -58,6 +121,27 @@ module Gitlab
# Using `Integer()` here to raise exception on invalid values
[Integer(line["number"]), Integer(line["hits"])]
end
+ rescue
+ raise InvalidLineInformationError, "Line information had invalid values"
+ end
+
+ def determine_filename(filename, context)
+ return filename unless context[:sources].any?
+
+ full_filename = nil
+
+ context[:sources].each_with_index do |source, index|
+ break if index >= MAX_SOURCES
+ break if full_filename = check_source(source, filename, context)
+ end
+
+ full_filename
+ end
+
+ def check_source(source, filename, context)
+ full_path = File.join(source, filename)
+
+ return full_path if context[:paths].include?(full_path)
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb b/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb
index a864c843dd8..2ca51930c19 100644
--- a/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb
+++ b/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb
@@ -35,7 +35,7 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def pipelines
- if ::Feature.enabled?(:ci_auto_cancel_all_pipelines, project, default_enabled: false)
+ if ::Feature.enabled?(:ci_auto_cancel_all_pipelines, project, default_enabled: true)
project.all_pipelines.ci_and_parent_sources
else
project.ci_pipelines
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 06096a33f27..d05be54267c 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -12,7 +12,7 @@ module Gitlab
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update, :bridge, :content, :dry_run,
# These attributes are set by Chains during processing:
- :config_content, :yaml_processor_result, :stage_seeds
+ :config_content, :yaml_processor_result, :pipeline_seed
) do
include Gitlab::Utils::StrongMemoize
diff --git a/lib/gitlab/ci/pipeline/chain/limit/deployments.rb b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb
new file mode 100644
index 00000000000..d684eedcaac
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Limit
+ class Deployments < Chain::Base
+ extend ::Gitlab::Utils::Override
+ include ::Gitlab::Ci::Pipeline::Chain::Helpers
+
+ attr_reader :limit
+ private :limit
+
+ def initialize(*)
+ super
+
+ @limit = ::Gitlab::Ci::Pipeline::Quota::Deployments
+ .new(project.namespace, pipeline, command)
+ end
+
+ override :perform!
+ def perform!
+ return unless limit.exceeded?
+
+ limit.log_error!(project_id: project.id, plan: project.actual_plan_name)
+ error(limit.message, drop_reason: :deployments_limit_exceeded)
+ end
+
+ override :break?
+ def break?
+ limit.exceeded?
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index f9ae37aa273..654e24be8e1 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -10,12 +10,12 @@ module Gitlab
PopulateError = Class.new(StandardError)
def perform!
- raise ArgumentError, 'missing stage seeds' unless @command.stage_seeds
+ raise ArgumentError, 'missing pipeline seed' unless @command.pipeline_seed
##
# Populate pipeline with all stages, and stages with builds.
#
- pipeline.stages = @command.stage_seeds.map(&:to_resource)
+ pipeline.stages = @command.pipeline_seed.stages
if stage_names.empty?
return error('No stages / jobs for this pipeline.')
diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb
index ba86b08d209..083f0bec1df 100644
--- a/lib/gitlab/ci/pipeline/chain/seed.rb
+++ b/lib/gitlab/ci/pipeline/chain/seed.rb
@@ -29,11 +29,11 @@ module Gitlab
##
# Gather all runtime build/stage errors
#
- if stage_seeds_errors
- return error(stage_seeds_errors.join("\n"), config_error: true)
+ if pipeline_seed.errors
+ return error(pipeline_seed.errors.join("\n"), config_error: true)
end
- @command.stage_seeds = stage_seeds
+ @command.pipeline_seed = pipeline_seed
end
def break?
@@ -42,24 +42,12 @@ module Gitlab
private
- def stage_seeds_errors
- stage_seeds.flat_map(&:errors).compact.presence
- end
-
- def stage_seeds
- strong_memoize(:stage_seeds) do
- seeds = stages_attributes.inject([]) do |previous_stages, attributes|
- seed = Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, previous_stages)
- previous_stages + [seed]
- end
-
- seeds.select(&:included?)
+ def pipeline_seed
+ strong_memoize(:pipeline_seed) do
+ stages_attributes = @command.yaml_processor_result.stages_attributes
+ Gitlab::Ci::Pipeline::Seed::Pipeline.new(pipeline, stages_attributes)
end
end
-
- def stages_attributes
- @command.yaml_processor_result.stages_attributes
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/quota/deployments.rb b/lib/gitlab/ci/pipeline/quota/deployments.rb
new file mode 100644
index 00000000000..ed32d0d3d49
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/quota/deployments.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Quota
+ class Deployments < ::Gitlab::Ci::Limit
+ include ::Gitlab::Utils::StrongMemoize
+ include ActionView::Helpers::TextHelper
+
+ def initialize(namespace, pipeline, command)
+ @namespace = namespace
+ @pipeline = pipeline
+ @command = command
+ end
+
+ def enabled?
+ limit > 0
+ end
+
+ def exceeded?
+ return false unless enabled?
+
+ pipeline_deployment_count > limit
+ end
+
+ def message
+ return unless exceeded?
+
+ "Pipeline has too many deployments! Requested #{pipeline_deployment_count}, but the limit is #{limit}."
+ end
+
+ private
+
+ def pipeline_deployment_count
+ strong_memoize(:pipeline_deployment_count) do
+ @command.pipeline_seed.deployments_count
+ end
+ end
+
+ def limit
+ strong_memoize(:limit) do
+ @namespace.actual_limits.ci_pipeline_deployments
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 91dbcc616ea..2271915a72b 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -60,6 +60,7 @@ module Gitlab
@seed_attributes
.deep_merge(pipeline_attributes)
.deep_merge(rules_attributes)
+ .deep_merge(allow_failure_criteria_attributes)
.deep_merge(cache_attributes)
end
@@ -154,9 +155,15 @@ module Gitlab
end
def rules_attributes
- return {} unless @using_rules
+ strong_memoize(:rules_attributes) do
+ next {} unless @using_rules
- rules_result.build_attributes
+ if ::Gitlab::Ci::Features.rules_variables_enabled?(@pipeline.project)
+ rules_result.build_attributes(@seed_attributes)
+ else
+ rules_result.build_attributes
+ end
+ end
end
def rules_result
@@ -176,6 +183,17 @@ module Gitlab
@cache.build_attributes
end
end
+
+ # If a job uses `allow_failure:exit_codes` and `rules:allow_failure`
+ # we need to prevent the exit codes from being persisted because they
+ # would break the behavior defined by `rules:allow_failure`.
+ def allow_failure_criteria_attributes
+ return {} unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled?
+ return {} if rules_attributes[:allow_failure].nil?
+ return {} unless @seed_attributes.dig(:options, :allow_failure_criteria)
+
+ { options: { allow_failure_criteria: nil } }
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/environment.rb b/lib/gitlab/ci/pipeline/seed/environment.rb
index b20dc383419..5dff0788ec9 100644
--- a/lib/gitlab/ci/pipeline/seed/environment.rb
+++ b/lib/gitlab/ci/pipeline/seed/environment.rb
@@ -24,9 +24,7 @@ module Gitlab
end
def auto_stop_in
- if Feature.enabled?(:environment_auto_stop_start_on_create)
- job.environment_auto_stop_in
- end
+ job.environment_auto_stop_in
end
def expanded_environment_name
diff --git a/lib/gitlab/ci/pipeline/seed/pipeline.rb b/lib/gitlab/ci/pipeline/seed/pipeline.rb
new file mode 100644
index 00000000000..da9d853cf68
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/pipeline.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Seed
+ class Pipeline
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline, stages_attributes)
+ @pipeline = pipeline
+ @stages_attributes = stages_attributes
+ end
+
+ def errors
+ stage_seeds.flat_map(&:errors).compact.presence
+ end
+
+ def stages
+ stage_seeds.map(&:to_resource)
+ end
+
+ def size
+ stage_seeds.sum(&:size)
+ end
+
+ def deployments_count
+ stage_seeds.sum do |stage_seed|
+ stage_seed.seeds.count do |build_seed|
+ build_seed.attributes[:environment].present?
+ end
+ end
+ end
+
+ private
+
+ def stage_seeds
+ strong_memoize(:stage_seeds) do
+ seeds = @stages_attributes.inject([]) do |previous_stages, attributes|
+ seed = Gitlab::Ci::Pipeline::Seed::Stage.new(@pipeline, attributes, previous_stages)
+ previous_stages + [seed]
+ end
+
+ seeds.select(&:included?)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/accessibility_reports_comparer.rb b/lib/gitlab/ci/reports/accessibility_reports_comparer.rb
index 210eb17f2d3..ab048672b22 100644
--- a/lib/gitlab/ci/reports/accessibility_reports_comparer.rb
+++ b/lib/gitlab/ci/reports/accessibility_reports_comparer.rb
@@ -3,52 +3,43 @@
module Gitlab
module Ci
module Reports
- class AccessibilityReportsComparer
- include Gitlab::Utils::StrongMemoize
-
- STATUS_SUCCESS = 'success'
- STATUS_FAILED = 'failed'
-
- attr_reader :base_reports, :head_reports
-
- def initialize(base_reports, head_reports)
- @base_reports = base_reports || AccessibilityReports.new
- @head_reports = head_reports
+ class AccessibilityReportsComparer < ReportsComparer
+ def initialize(base_report, head_report)
+ @base_report = base_report || AccessibilityReports.new
+ @head_report = head_report
end
- def status
- head_reports.errors_count > 0 ? STATUS_FAILED : STATUS_SUCCESS
+ def success?
+ head_report.errors_count == 0
end
def existing_errors
strong_memoize(:existing_errors) do
- base_reports.all_errors
+ base_report.all_errors & head_report.all_errors
end
end
def new_errors
strong_memoize(:new_errors) do
- head_reports.all_errors - base_reports.all_errors
+ head_report.all_errors - base_report.all_errors
end
end
def resolved_errors
strong_memoize(:resolved_errors) do
- base_reports.all_errors - head_reports.all_errors
+ base_report.all_errors - head_report.all_errors
end
end
- def errors_count
- head_reports.errors_count
- end
-
def resolved_count
resolved_errors.size
end
def total_count
- existing_errors.size + new_errors.size
+ head_report.errors_count
end
+
+ alias_method :errors_count, :total_count
end
end
end
diff --git a/lib/gitlab/ci/reports/codequality_reports.rb b/lib/gitlab/ci/reports/codequality_reports.rb
new file mode 100644
index 00000000000..060a1e2399b
--- /dev/null
+++ b/lib/gitlab/ci/reports/codequality_reports.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ class CodequalityReports
+ attr_reader :degradations, :error_message
+
+ CODECLIMATE_SCHEMA_PATH = Rails.root.join('app', 'validators', 'json_schemas', 'codeclimate.json').to_s
+
+ def initialize
+ @degradations = {}.with_indifferent_access
+ @error_message = nil
+ end
+
+ def add_degradation(degradation)
+ valid_degradation?(degradation) && @degradations[degradation.dig('fingerprint')] = degradation
+ end
+
+ def set_error_message(error)
+ @error_message = error
+ end
+
+ def degradations_count
+ @degradations.size
+ end
+
+ def all_degradations
+ @degradations.values
+ end
+
+ private
+
+ def valid_degradation?(degradation)
+ JSON::Validator.validate!(CODECLIMATE_SCHEMA_PATH, degradation)
+ rescue JSON::Schema::ValidationError => e
+ set_error_message("Invalid degradation format: #{e.message}")
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/codequality_reports_comparer.rb b/lib/gitlab/ci/reports/codequality_reports_comparer.rb
new file mode 100644
index 00000000000..88e02cd9004
--- /dev/null
+++ b/lib/gitlab/ci/reports/codequality_reports_comparer.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ class CodequalityReportsComparer < ReportsComparer
+ def initialize(base_report, head_report)
+ @base_report = base_report || CodequalityReports.new
+ @head_report = head_report
+ end
+
+ def success?
+ head_report.degradations_count == 0
+ end
+
+ def existing_errors
+ strong_memoize(:existing_errors) do
+ base_report.all_degradations & head_report.all_degradations
+ end
+ end
+
+ def new_errors
+ strong_memoize(:new_errors) do
+ fingerprints = head_report.degradations.keys - base_report.degradations.keys
+ head_report.degradations.fetch_values(*fingerprints)
+ end
+ end
+
+ def resolved_errors
+ strong_memoize(:resolved_errors) do
+ fingerprints = base_report.degradations.keys - head_report.degradations.keys
+ base_report.degradations.fetch_values(*fingerprints)
+ end
+ end
+
+ def resolved_count
+ resolved_errors.size
+ end
+
+ def total_count
+ head_report.degradations_count
+ end
+
+ alias_method :errors_count, :total_count
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/reports_comparer.rb b/lib/gitlab/ci/reports/reports_comparer.rb
new file mode 100644
index 00000000000..d413d3a74f6
--- /dev/null
+++ b/lib/gitlab/ci/reports/reports_comparer.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ class ReportsComparer
+ include Gitlab::Utils::StrongMemoize
+
+ STATUS_SUCCESS = 'success'
+ STATUS_FAILED = 'failed'
+
+ attr_reader :base_report, :head_report
+
+ def initialize(base_report, head_report)
+ @base_report = base_report
+ @head_report = head_report
+ end
+
+ def status
+ success? ? STATUS_SUCCESS : STATUS_FAILED
+ end
+
+ def success?
+ raise NotImplementedError
+ end
+
+ def existing_errors
+ raise NotImplementedError
+ end
+
+ def new_errors
+ raise NotImplementedError
+ end
+
+ def resolved_errors
+ raise NotImplementedError
+ end
+
+ def errors_count
+ raise NotImplementedError
+ end
+
+ def resolved_count
+ resolved_errors.size
+ end
+
+ def total_count
+ existing_errors.size + new_errors.size
+ end
+ end
+ end
+ end
+end
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 fe23641802b..2ae9730ec1a 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -7,7 +7,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
- CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18"
+ CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18-gitlab.1"
needs: []
script:
- export SOURCE_CODE=$PWD
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 385959389de..e5b40e5f49a 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 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0-beta.2"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0"
dependencies: []
review:
diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
index 3b87d53f165..895e6e8ea6d 100644
--- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
@@ -2,6 +2,8 @@ test:
variables:
POSTGRES_VERSION: 9.6.16
POSTGRES_DB: test
+ POSTGRES_USER: user
+ POSTGRES_PASSWORD: testing-password
services:
- "postgres:${POSTGRES_VERSION}"
stage: test
diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
index 3f62d92ad13..23dfeda31cc 100644
--- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -1,6 +1,6 @@
apply:
stage: deploy
- image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.34.1"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.36.0"
environment:
name: production
variables:
diff --git a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
index 65abee1f5eb..3faf07546de 100644
--- a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: ayufan/openshift-cli
+image: openshift/origin-cli
stages:
- build # dummy stage to follow the template guidelines
diff --git a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
index 0ae8fd833c4..135f0df99fe 100644
--- a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
@@ -15,7 +15,8 @@ variables:
FUZZAPI_VERSION: latest
FUZZAPI_CONFIG: .gitlab-api-fuzzing.yml
FUZZAPI_TIMEOUT: 30
- FUZZAPI_REPORT: gl-api-fuzzing-report.xml
+ FUZZAPI_REPORT: gl-api-fuzzing-report.json
+ FUZZAPI_REPORT_ASSET_PATH: assets
#
FUZZAPI_D_NETWORK: testing-net
#
@@ -45,6 +46,7 @@ apifuzzer_fuzz:
variables:
FUZZAPI_PROJECT: $CI_PROJECT_PATH
FUZZAPI_API: http://apifuzzer:80
+ FUZZAPI_NEW_REPORT: 1
TZ: America/Los_Angeles
services:
- name: $FUZZAPI_IMAGE
@@ -61,7 +63,7 @@ apifuzzer_fuzz:
- if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- - if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
+ - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
script:
#
# Validate options
@@ -75,6 +77,9 @@ apifuzzer_fuzz:
# Run user provided pre-script
- sh -c "$FUZZAPI_PRE_SCRIPT"
#
+ # Make sure asset path exists
+ - mkdir -p $FUZZAPI_REPORT_ASSET_PATH
+ #
# Start scanning
- worker-entry
#
@@ -82,8 +87,12 @@ apifuzzer_fuzz:
- sh -c "$FUZZAPI_POST_SCRIPT"
#
artifacts:
+ when: always
+ paths:
+ - $FUZZAPI_REPORT_ASSET_PATH
+ - $FUZZAPI_REPORT
reports:
- junit: $FUZZAPI_REPORT
+ api_fuzzing: $FUZZAPI_REPORT
apifuzzer_fuzz_dnd:
stage: fuzz
@@ -102,7 +111,7 @@ apifuzzer_fuzz_dnd:
- if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- - if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
+ - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
services:
- docker:19.03.12-dind
script:
@@ -115,6 +124,9 @@ apifuzzer_fuzz_dnd:
# Run user provided pre-script
- sh -c "$FUZZAPI_PRE_SCRIPT"
#
+ # Make sure asset path exists
+ - mkdir -p $FUZZAPI_REPORT_ASSET_PATH
+ #
# Start peach testing engine container
- |
docker run -d \
@@ -155,6 +167,8 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_PROFILE \
-e FUZZAPI_CONFIG \
-e FUZZAPI_REPORT \
+ -e FUZZAPI_REPORT_ASSET_PATH \
+ -e FUZZAPI_NEW_REPORT=1 \
-e FUZZAPI_HAR \
-e FUZZAPI_OPENAPI \
-e FUZZAPI_POSTMAN_COLLECTION \
@@ -168,6 +182,8 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_SERVICE_START_TIMEOUT \
-e FUZZAPI_HTTP_USERNAME \
-e FUZZAPI_HTTP_PASSWORD \
+ -e CI_PROJECT_URL \
+ -e CI_JOB_ID \
-e CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH} \
$FUZZAPI_D_WORKER_ENV \
$FUZZAPI_D_WORKER_PORTS \
@@ -193,6 +209,8 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_PROFILE \
-e FUZZAPI_CONFIG \
-e FUZZAPI_REPORT \
+ -e FUZZAPI_REPORT_ASSET_PATH \
+ -e FUZZAPI_NEW_REPORT=1 \
-e FUZZAPI_HAR \
-e FUZZAPI_OPENAPI \
-e FUZZAPI_POSTMAN_COLLECTION \
@@ -206,7 +224,10 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_SERVICE_START_TIMEOUT \
-e FUZZAPI_HTTP_USERNAME \
-e FUZZAPI_HTTP_PASSWORD \
+ -e CI_PROJECT_URL \
+ -e CI_JOB_ID \
-v $CI_PROJECT_DIR:/app \
+ -v `pwd`/$FUZZAPI_REPORT_ASSET_PATH:/app/$FUZZAPI_REPORT_ASSET_PATH:rw \
-p 81:80 \
-p 8001:8000 \
-p 515:514 \
@@ -239,7 +260,9 @@ apifuzzer_fuzz_dnd:
paths:
- ./gl-api_fuzzing*.log
- ./gl-api_fuzzing*.zip
+ - $FUZZAPI_REPORT_ASSET_PATH
+ - $FUZZAPI_REPORT
reports:
- junit: $FUZZAPI_REPORT
+ api_fuzzing: $FUZZAPI_REPORT
# end
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index 3cbde9d30c8..5ea2363a0c5 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -8,7 +8,7 @@ variables:
container_scanning:
stage: test
- image: $SECURE_ANALYZERS_PREFIX/klar:$CS_MAJOR_VERSION
+ image: "$CS_ANALYZER_IMAGE"
variables:
# By default, use the latest clair vulnerabilities database, however, allow it to be overridden here with a specific image
# to enable container scanning to run offline, or to provide a consistent list of vulnerabilities for integration testing purposes
@@ -18,6 +18,7 @@ container_scanning:
# file. See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template
# for details
GIT_STRATEGY: none
+ CS_ANALYZER_IMAGE: $SECURE_ANALYZERS_PREFIX/klar:$CS_MAJOR_VERSION
allow_failure: true
services:
- name: $CLAIR_DB_IMAGE
diff --git a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
index a1b6dc2cc1b..9d47537c0f0 100644
--- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
@@ -12,7 +12,7 @@ variables:
coverage_fuzzing_unlicensed:
- stage: test
+ stage: .pre
allow_failure: true
rules:
- if: $GITLAB_FEATURES !~ /\bcoverage_fuzzing\b/ && $COVFUZZ_DISABLED == null
diff --git a/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml
new file mode 100644
index 00000000000..a0564a16c07
--- /dev/null
+++ b/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml
@@ -0,0 +1,24 @@
+stages:
+ - build
+ - test
+ - deploy
+ - dast
+
+variables:
+ DAST_VERSION: 1
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
+dast:
+ stage: dast
+ image:
+ name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
+ variables:
+ GIT_STRATEGY: none
+ allow_failure: true
+ script:
+ - /analyze
+ artifacts:
+ reports:
+ dast: gl-dast-report.json
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index 3789f0edc1c..b534dad9593 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -28,11 +28,8 @@ dependency_scanning:
.ds-analyzer:
extends: dependency_scanning
allow_failure: true
- rules:
- - if: $DEPENDENCY_SCANNING_DISABLED
- when: never
- - if: $CI_COMMIT_BRANCH &&
- $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ # `rules` must be overridden explicitly by each child job
+ # see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
script:
- /analyzer run
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index a51cb61da6d..f4ee8ebd47e 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -30,10 +30,8 @@ sast:
.sast-analyzer:
extends: sast
allow_failure: true
- rules:
- - if: $SAST_DISABLED
- when: never
- - if: $CI_COMMIT_BRANCH
+ # `rules` must be overridden explicitly by each child job
+ # see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
script:
- /analyzer run
@@ -175,7 +173,7 @@ nodejs-scan-sast:
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/
exists:
- - 'package.json'
+ - '**/package.json'
phpcs-security-audit-sast:
extends: .sast-analyzer
diff --git a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
index 6ebff102ccb..8ca1d2e08ba 100644
--- a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
@@ -14,6 +14,9 @@ variables:
stage: test
image: "$SECURE_ANALYZERS_PREFIX/secrets:$SECRETS_ANALYZER_VERSION"
services: []
+ allow_failure: true
+ # `rules` must be overridden explicitly by each child job
+ # see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
artifacts:
reports:
secret_detection: gl-secret-detection-report.json
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 e455bfac9de..910e711f046 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
@@ -56,5 +56,6 @@ cache:
.destroy: &destroy
stage: cleanup
script:
+ - cd ${TF_ROOT}
- gitlab-terraform destroy
when: manual
diff --git a/lib/gitlab/ci/templates/npm.gitlab-ci.yml b/lib/gitlab/ci/templates/npm.gitlab-ci.yml
index 0a739cf122d..035ba52da84 100644
--- a/lib/gitlab/ci/templates/npm.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/npm.gitlab-ci.yml
@@ -55,5 +55,5 @@ publish_package:
npm publish &&
echo "Successfully published version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} to GitLab's NPM registry: ${CI_PROJECT_URL}/-/packages"
} || {
- echo "No new version of ${NPM_PACKAGE_NAME} published. This is most likely because version ${NPM_PACKAGE_VERSION} already exists in GitLab's NPM registry."; exit 1
+ echo "No new version of ${NPM_PACKAGE_NAME} published. This is most likely because version ${NPM_PACKAGE_VERSION} already exists in GitLab's NPM registry."
}
diff --git a/lib/gitlab/ci/templates/npm.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/npm.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..536cf9bd8d8
--- /dev/null
+++ b/lib/gitlab/ci/templates/npm.latest.gitlab-ci.yml
@@ -0,0 +1,41 @@
+publish:
+ image: node:latest
+ stage: deploy
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_REF_NAME =~ /^v\d+\.\d+\.\d+.*$/
+ changes:
+ - package.json
+ script:
+ # If no .npmrc if included in the repo, generate a temporary one that is configured to publish to GitLab's NPM registry
+ - |
+ if [[ ! -f .npmrc ]]; then
+ echo 'No .npmrc found! Creating one now. Please review the following link for more information: https://docs.gitlab.com/ee/user/packages/npm_registry/index.html#project-level-npm-endpoint-1'
+ {
+ echo "@${CI_PROJECT_ROOT_NAMESPACE}:registry=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
+ echo "${CI_API_V4_URL#http*:}/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=\${CI_JOB_TOKEN}"
+ } >> .npmrc
+ fi
+ - echo "Created the following .npmrc:"; cat .npmrc
+
+ # Extract a few values from package.json
+ - NPM_PACKAGE_NAME=$(node -p "require('./package.json').name")
+ - NPM_PACKAGE_VERSION=$(node -p "require('./package.json').version")
+
+ # Validate that the package name is properly scoped to the project's root namespace.
+ # For more information, see https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention
+ - |
+ if [[ ! $NPM_PACKAGE_NAME =~ ^@$CI_PROJECT_ROOT_NAMESPACE/ ]]; then
+ echo "Invalid package scope! Packages must be scoped in the root namespace of the project, e.g. \"@${CI_PROJECT_ROOT_NAMESPACE}/${CI_PROJECT_NAME}\""
+ echo 'For more information, see https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention'
+ exit 1
+ fi
+
+ # Compare the version in package.json to all published versions.
+ # If the package.json version has not yet been published, run `npm publish`.
+ - |
+ if [[ $(npm view "${NPM_PACKAGE_NAME}" versions) != *"'${NPM_PACKAGE_VERSION}'"* ]]; then
+ npm publish
+ echo "Successfully published version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} to GitLab's NPM registry: ${CI_PROJECT_URL}/-/packages"
+ else
+ echo "Version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} has already been published, so no new version has been published."
+ fi
diff --git a/lib/gitlab/ci/trace/checksum.rb b/lib/gitlab/ci/trace/checksum.rb
index 62532ef1cd2..7cdb6a6c03c 100644
--- a/lib/gitlab/ci/trace/checksum.rb
+++ b/lib/gitlab/ci/trace/checksum.rb
@@ -64,10 +64,33 @@ module Gitlab
end
end
+ def state_bytesize
+ strong_memoize(:state_bytesize) do
+ build.pending_state&.trace_bytesize
+ end
+ end
+
+ def trace_size
+ strong_memoize(:trace_size) do
+ trace_chunks.sum { |chunk| chunk_size(chunk) }
+ end
+ end
+
+ def corrupted?
+ return false unless has_bytesize?
+ return false if valid?
+
+ state_bytesize.to_i != trace_size.to_i
+ end
+
def chunks_count
trace_chunks.to_a.size
end
+ def has_bytesize?
+ state_bytesize.present?
+ end
+
private
def chunk_size(chunk)
diff --git a/lib/gitlab/ci/trace/metrics.rb b/lib/gitlab/ci/trace/metrics.rb
index 097436d84ea..ce9efbda7ea 100644
--- a/lib/gitlab/ci/trace/metrics.rb
+++ b/lib/gitlab/ci/trace/metrics.rb
@@ -18,7 +18,8 @@ module Gitlab
:conflict, # runner has sent unrecognized build state details
:locked, # build trace has been locked by a different mechanism
:stalled, # failed to migrate chunk due to a worker duplication
- :invalid # malformed build trace has been detected using CRC32
+ :invalid, # invalid build trace has been detected using CRC32
+ :corrupted # malformed trace found after comparing CRC32 and size
].freeze
def increment_trace_operation(operation: :unknown)
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
index 52a00e41214..cd7d781a574 100644
--- a/lib/gitlab/ci/yaml_processor/result.rb
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -77,6 +77,7 @@ module Gitlab
options: {
image: job[:image],
services: job[:services],
+ allow_failure_criteria: job[:allow_failure_criteria],
artifacts: job[:artifacts],
dependencies: job[:dependencies],
cross_dependencies: job.dig(:needs, :cross_dependency),
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 2a386657e0b..88786ed82ff 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -134,6 +134,16 @@ module Gitlab
end
end
+ class HashOrBooleanValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || validate_boolean(value)
+ record.errors.add(attribute, 'should be a hash or a boolean value')
+ end
+ end
+ end
+
class KeyValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
@@ -158,6 +168,22 @@ module Gitlab
end
end
+ class ArrayOfIntegersOrIntegerValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_integer(value) || validate_array_of_integers(value)
+ record.errors.add(attribute, 'should be an array of integers or an integer')
+ end
+ end
+
+ private
+
+ def validate_array_of_integers(values)
+ values.is_a?(Array) && values.all? { |value| validate_integer(value) }
+ end
+ end
+
class RegexpValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
diff --git a/lib/gitlab/cycle_analytics/builds_event_helper.rb b/lib/gitlab/cycle_analytics/builds_event_helper.rb
index 0d6f32fdc6f..c39d41578e9 100644
--- a/lib/gitlab/cycle_analytics/builds_event_helper.rb
+++ b/lib/gitlab/cycle_analytics/builds_event_helper.rb
@@ -3,11 +3,11 @@
module Gitlab
module CycleAnalytics
module BuildsEventHelper
- def initialize(*args)
+ def initialize(...)
@projections = [build_table[:id]]
@order = build_table[:created_at]
- super(*args)
+ super(...)
end
def fetch
diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index d75da76415a..790bf32c6c7 100644
--- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -5,7 +5,7 @@ module Gitlab
class CodeEventFetcher < BaseEventFetcher
include CodeHelper
- def initialize(*args)
+ def initialize(...)
@projections = [mr_table[:title],
mr_table[:iid],
mr_table[:id],
@@ -14,7 +14,7 @@ module Gitlab
mr_table[:author_id]]
@order = mr_table[:created_at]
- super(*args)
+ super(...)
end
private
diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 6914cf24c19..fd04ec090b3 100644
--- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -5,14 +5,14 @@ module Gitlab
class IssueEventFetcher < BaseEventFetcher
include IssueHelper
- def initialize(*args)
+ def initialize(...)
@projections = [issue_table[:title],
issue_table[:iid],
issue_table[:id],
issue_table[:created_at],
issue_table[:author_id]]
- super(*args)
+ super(...)
end
private
diff --git a/lib/gitlab/cycle_analytics/permissions.rb b/lib/gitlab/cycle_analytics/permissions.rb
index 55214e6b896..0e094fabb01 100644
--- a/lib/gitlab/cycle_analytics/permissions.rb
+++ b/lib/gitlab/cycle_analytics/permissions.rb
@@ -12,8 +12,8 @@ module Gitlab
production: :read_issue
}.freeze
- def self.get(*args)
- new(*args).get
+ def self.get(...)
+ new(...).get
end
def initialize(user:, project:)
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index bad02e00a13..4d98d589e46 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -5,14 +5,14 @@ module Gitlab
class PlanEventFetcher < BaseEventFetcher
include PlanHelper
- def initialize(*args)
+ def initialize(...)
@projections = [issue_table[:title],
issue_table[:iid],
issue_table[:id],
issue_table[:created_at],
issue_table[:author_id]]
- super(*args)
+ super(...)
end
private
diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
index 8843ab2bcb9..5fa286bd3df 100644
--- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
@@ -5,7 +5,7 @@ module Gitlab
class ProductionEventFetcher < BaseEventFetcher
include ProductionHelper
- def initialize(*args)
+ def initialize(...)
@projections = [issue_table[:title],
issue_table[:iid],
issue_table[:id],
@@ -13,7 +13,7 @@ module Gitlab
issue_table[:author_id],
routes_table[:path]]
- super(*args)
+ super(...)
end
private
diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index f5f8c19683d..0b7d160c7de 100644
--- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -5,7 +5,7 @@ module Gitlab
class ReviewEventFetcher < BaseEventFetcher
include ReviewHelper
- def initialize(*args)
+ def initialize(...)
@projections = [mr_table[:title],
mr_table[:iid],
mr_table[:id],
@@ -13,7 +13,7 @@ module Gitlab
mr_table[:state_id],
mr_table[:author_id]]
- super(*args)
+ super(...)
end
private
diff --git a/lib/gitlab/cycle_analytics/updater.rb b/lib/gitlab/cycle_analytics/updater.rb
index c642809a792..5be351989e0 100644
--- a/lib/gitlab/cycle_analytics/updater.rb
+++ b/lib/gitlab/cycle_analytics/updater.rb
@@ -3,8 +3,8 @@
module Gitlab
module CycleAnalytics
class Updater
- def self.update!(*args)
- new(*args).update!
+ def self.update!(...)
+ new(...).update!
end
def initialize(event_result, from:, to:, klass:)
diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb
deleted file mode 100644
index e58def57e69..00000000000
--- a/lib/gitlab/cycle_analytics/usage_data.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module CycleAnalytics
- class UsageData
- include Gitlab::Utils::StrongMemoize
- PROJECTS_LIMIT = 10
-
- attr_reader :options
-
- def initialize
- @options = { from: 7.days.ago }
- end
-
- def projects
- strong_memoize(:projects) do
- projects = Project.where.not(last_activity_at: nil).order(last_activity_at: :desc).limit(10) +
- Project.where.not(last_repository_updated_at: nil).order(last_repository_updated_at: :desc).limit(10)
-
- projects = projects.uniq.sort_by do |project|
- [project.last_activity_at, project.last_repository_updated_at].min
- end
-
- if projects.size < 10
- projects.concat(Project.where(last_activity_at: nil, last_repository_updated_at: nil).limit(10))
- end
-
- projects.uniq.first(10)
- end
- end
-
- def to_json(*)
- total = 0
-
- values =
- medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh|
- calculations = stage_values(medians)
-
- total += calculations.values.compact.sum
- hsh[stage_name] = calculations
- end
-
- values[:total] = total
-
- { avg_cycle_analytics: values }
- end
-
- private
-
- def medians_per_stage
- projects.each_with_object({}) do |project, hsh|
- ::CycleAnalytics::ProjectLevel.new(project, options: options).all_medians_by_stage.each do |stage_name, median|
- hsh[stage_name] ||= []
- hsh[stage_name] << median
- end
- end
- end
-
- def stage_values(medians)
- medians = medians.map(&:presence).compact
- average = calc_average(medians)
-
- {
- average: average,
- sd: standard_deviation(medians, average),
- missing: projects.length - medians.length
- }
- end
-
- def calc_average(values)
- return if values.empty?
-
- (values.sum / values.length).to_i
- end
-
- def standard_deviation(values, average)
- Math.sqrt(sample_variance(values, average)).to_i
- end
-
- def sample_variance(values, average)
- return 0 if values.length <= 1
-
- sum = values.inject(0) do |acc, val|
- acc + (val - average)**2
- end
-
- sum / (values.length - 1)
- end
- end
- end
-end
diff --git a/lib/gitlab/danger/base_linter.rb b/lib/gitlab/danger/base_linter.rb
new file mode 100644
index 00000000000..df2e9e745aa
--- /dev/null
+++ b/lib/gitlab/danger/base_linter.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Danger
+ class BaseLinter
+ MIN_SUBJECT_WORDS_COUNT = 3
+ MAX_LINE_LENGTH = 72
+ WIP_PREFIX = 'WIP: '
+
+ attr_reader :commit, :problems
+
+ def self.problems_mapping
+ {
+ subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
+ subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
+ subject_starts_with_lowercase: "The %s must start with a capital letter",
+ subject_ends_with_a_period: "The %s must not end with a period"
+ }
+ end
+
+ def self.subject_description
+ 'commit subject'
+ end
+
+ def initialize(commit)
+ @commit = commit
+ @problems = {}
+ end
+
+ def failed?
+ problems.any?
+ end
+
+ def add_problem(problem_key, *args)
+ @problems[problem_key] = sprintf(self.class.problems_mapping[problem_key], *args)
+ end
+
+ def lint_subject
+ if subject_too_short?
+ add_problem(:subject_too_short, self.class.subject_description)
+ end
+
+ if subject_too_long?
+ add_problem(:subject_too_long, self.class.subject_description)
+ end
+
+ if subject_starts_with_lowercase?
+ add_problem(:subject_starts_with_lowercase, self.class.subject_description)
+ end
+
+ if subject_ends_with_a_period?
+ add_problem(:subject_ends_with_a_period, self.class.subject_description)
+ end
+
+ self
+ end
+
+ private
+
+ def subject
+ message_parts[0].delete_prefix(WIP_PREFIX)
+ end
+
+ def subject_too_short?
+ subject.split(' ').length < MIN_SUBJECT_WORDS_COUNT
+ end
+
+ def subject_too_long?
+ line_too_long?(subject)
+ end
+
+ def line_too_long?(line)
+ line.length > MAX_LINE_LENGTH
+ end
+
+ def subject_starts_with_lowercase?
+ return false if ('A'..'Z').cover?(subject[0])
+
+ first_char = subject.sub(/\A(\[.+\]|\w+:)\s/, '')[0]
+ first_char_downcased = first_char.downcase
+ return true unless ('a'..'z').cover?(first_char_downcased)
+
+ first_char.downcase == first_char
+ end
+
+ def subject_ends_with_a_period?
+ subject.end_with?('.')
+ end
+
+ def message_parts
+ @message_parts ||= commit.message.split("\n", 3)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/danger/changelog.rb b/lib/gitlab/danger/changelog.rb
index 607ca1200a0..92af6849b2f 100644
--- a/lib/gitlab/danger/changelog.rb
+++ b/lib/gitlab/danger/changelog.rb
@@ -39,6 +39,7 @@ module Gitlab
def required?
git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} }
end
+ alias_method :db_changes?, :required?
def optional?
categories_need_changelog? && without_no_changelog_label?
diff --git a/lib/gitlab/danger/commit_linter.rb b/lib/gitlab/danger/commit_linter.rb
index 2e469359bdc..7e2e0fb0acb 100644
--- a/lib/gitlab/danger/commit_linter.rb
+++ b/lib/gitlab/danger/commit_linter.rb
@@ -1,40 +1,37 @@
# frozen_string_literal: true
+require_relative 'base_linter'
+
emoji_checker_path = File.expand_path('emoji_checker', __dir__)
defined?(Rails) ? require_dependency(emoji_checker_path) : require_relative(emoji_checker_path)
module Gitlab
module Danger
- class CommitLinter
- MIN_SUBJECT_WORDS_COUNT = 3
- MAX_LINE_LENGTH = 72
+ class CommitLinter < BaseLinter
MAX_CHANGED_FILES_IN_COMMIT = 3
MAX_CHANGED_LINES_IN_COMMIT = 30
SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&|%)\d+(?<!`)}.freeze
- DEFAULT_SUBJECT_DESCRIPTION = 'commit subject'
- WIP_PREFIX = 'WIP: '
- PROBLEMS = {
- subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
- subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
- subject_starts_with_lowercase: "The %s must start with a capital letter",
- subject_ends_with_a_period: "The %s must not end with a period",
- separator_missing: "The commit subject and body must be separated by a blank line",
- details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
+
+ def self.problems_mapping
+ super.merge(
+ {
+ separator_missing: "The commit subject and body must be separated by a blank line",
+ details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
"at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
- details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
- message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
+ details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
+ message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
"to the commit message, and are displayed as plain text outside of GitLab",
- message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
+ message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
"message, and may not be displayed properly everywhere",
- message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
+ message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
"`!123`), as short references are displayed as plain text outside of GitLab"
- }.freeze
-
- attr_reader :commit, :problems
+ }
+ )
+ end
def initialize(commit)
- @commit = commit
- @problems = {}
+ super
+
@linted = false
end
@@ -58,19 +55,11 @@ module Gitlab
!details.nil? && !details.empty?
end
- def failed?
- problems.any?
- end
-
- def add_problem(problem_key, *args)
- @problems[problem_key] = sprintf(PROBLEMS[problem_key], *args)
- end
-
- def lint(subject_description = "commit subject")
+ def lint
return self if @linted
@linted = true
- lint_subject(subject_description)
+ lint_subject
lint_separator
lint_details
lint_message
@@ -78,26 +67,6 @@ module Gitlab
self
end
- def lint_subject(subject_description)
- if subject_too_short?
- add_problem(:subject_too_short, subject_description)
- end
-
- if subject_too_long?
- add_problem(:subject_too_long, subject_description)
- end
-
- if subject_starts_with_lowercase?
- add_problem(:subject_starts_with_lowercase, subject_description)
- end
-
- if subject_ends_with_a_period?
- add_problem(:subject_ends_with_a_period, subject_description)
- end
-
- self
- end
-
private
def lint_separator
@@ -114,15 +83,11 @@ module Gitlab
end
details&.each_line do |line|
- line = line.strip
-
- next unless line_too_long?(line)
-
- url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length }
+ line_without_urls = line.strip.gsub(%r{https?://\S+}, '')
# If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but
# only if the line _without_ the URL does not exceed this limit.
- next unless line_too_long?(line.length - url_size)
+ next unless line_too_long?(line_without_urls)
add_problem(:details_line_too_long)
break
@@ -159,10 +124,6 @@ module Gitlab
files_changed > MAX_CHANGED_FILES_IN_COMMIT && lines_changed > MAX_CHANGED_LINES_IN_COMMIT
end
- def subject
- message_parts[0].delete_prefix(WIP_PREFIX)
- end
-
def separator
message_parts[1]
end
@@ -171,37 +132,6 @@ module Gitlab
message_parts[2]&.gsub(/^Signed-off-by.*$/, '')
end
- def line_too_long?(line)
- case line
- when String
- line.length > MAX_LINE_LENGTH
- when Integer
- line > MAX_LINE_LENGTH
- else
- raise ArgumentError, "The line argument (#{line}) should be a String or an Integer! #{line.class} given."
- end
- end
-
- def subject_too_short?
- subject.split(' ').length < MIN_SUBJECT_WORDS_COUNT
- end
-
- def subject_too_long?
- line_too_long?(subject)
- end
-
- def subject_starts_with_lowercase?
- first_char = subject.sub(/\A(\[.+\]|\w+:)\s/, '')[0]
- first_char_downcased = first_char.downcase
- return true unless ('a'..'z').cover?(first_char_downcased)
-
- first_char.downcase == first_char
- end
-
- def subject_ends_with_a_period?
- subject.end_with?('.')
- end
-
def message_contains_text_emoji?
emoji_checker.includes_text_emoji?(commit.message)
end
@@ -217,10 +147,6 @@ module Gitlab
def emoji_checker
@emoji_checker ||= Gitlab::Danger::EmojiChecker.new
end
-
- def message_parts
- @message_parts ||= commit.message.split("\n", 3)
- end
end
end
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 89f21e8bd23..d22f28ff7f2 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -64,7 +64,7 @@ module Gitlab
# - respond_to?(:gitlab)
# - respond_to?(:gitlab, true)
gitlab
- rescue NoMethodError
+ rescue NameError
nil
end
@@ -268,6 +268,10 @@ module Gitlab
def has_database_scoped_labels?(current_mr_labels)
current_mr_labels.any? { |label| label.start_with?('database::') }
end
+
+ def has_ci_changes?
+ changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
+ end
end
end
end
diff --git a/lib/gitlab/danger/merge_request_linter.rb b/lib/gitlab/danger/merge_request_linter.rb
new file mode 100644
index 00000000000..d401d332aa7
--- /dev/null
+++ b/lib/gitlab/danger/merge_request_linter.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require_relative 'base_linter'
+
+module Gitlab
+ module Danger
+ class MergeRequestLinter < BaseLinter
+ alias_method :lint, :lint_subject
+
+ def self.subject_description
+ 'merge request title'
+ end
+
+ def self.mr_run_options_regex
+ [
+ 'RUN AS-IF-FOSS',
+ 'UPDATE CACHE',
+ 'RUN ALL RSPEC',
+ 'SKIP RSPEC FAIL-FAST'
+ ].join('|')
+ end
+
+ private
+
+ def subject
+ super.gsub(/\[?(#{self.class.mr_run_options_regex})\]?/, '').strip
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
index 23f877b4e0f..328083f7002 100644
--- a/lib/gitlab/danger/roulette.rb
+++ b/lib/gitlab/danger/roulette.rb
@@ -24,7 +24,7 @@ module Gitlab
#
# @return [Array<Spin>]
def spin(project, categories, timezone_experiment: false)
- spins = categories.map do |category|
+ spins = categories.sort.map do |category|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)
spin_for_category(project, category, timezone_experiment: including_timezone)
diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb
index 6f79e965cd5..5a506da0d05 100644
--- a/lib/gitlab/database/batch_count.rb
+++ b/lib/gitlab/database/batch_count.rb
@@ -49,6 +49,8 @@ module Gitlab
MAX_ALLOWED_LOOPS = 10_000
SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep
ALLOWED_MODES = [:itself, :distinct].freeze
+ FALLBACK_FINISH = 0
+ OFFSET_BY_ONE = 1
# Each query should take < 500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705
DEFAULT_DISTINCT_BATCH_SIZE = 10_000
@@ -65,7 +67,7 @@ module Gitlab
(@operation == :count && batch_size <= MIN_REQUIRED_BATCH_SIZE) ||
(@operation == :sum && batch_size < DEFAULT_SUM_BATCH_SIZE) ||
(finish - start) / batch_size >= MAX_ALLOWED_LOOPS ||
- start > finish
+ start >= finish
end
def count(batch_size: nil, mode: :itself, start: nil, finish: nil)
@@ -85,11 +87,13 @@ module Gitlab
results = nil
batch_start = start
- while batch_start <= finish
- batch_relation = build_relation_batch(batch_start, batch_start + batch_size, mode)
+ while batch_start < finish
+ batch_end = [batch_start + batch_size, finish].min
+ batch_relation = build_relation_batch(batch_start, batch_end, mode)
+
begin
results = merge_results(results, batch_relation.send(@operation, *@operation_args)) # rubocop:disable GitlabSecurity/PublicSend
- batch_start += batch_size
+ batch_start = batch_end
rescue ActiveRecord::QueryCanceled => error
# retry with a safe batch size & warmer cache
if batch_size >= 2 * MIN_REQUIRED_BATCH_SIZE
@@ -99,6 +103,7 @@ module Gitlab
return FALLBACK
end
end
+
sleep(SLEEP_TIME_IN_SECONDS)
end
@@ -138,7 +143,7 @@ module Gitlab
end
def actual_finish(finish)
- finish || @relation.unscope(:group, :having).maximum(@column) || 0
+ (finish || @relation.unscope(:group, :having).maximum(@column) || FALLBACK_FINISH) + OFFSET_BY_ONE
end
def check_mode!(mode)
diff --git a/lib/gitlab/database/migrations/background_migration_helpers.rb b/lib/gitlab/database/migrations/background_migration_helpers.rb
index a6cc03aa9eb..36073844765 100644
--- a/lib/gitlab/database/migrations/background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/background_migration_helpers.rb
@@ -55,7 +55,8 @@ module Gitlab
bulk_migrate_async(jobs) unless jobs.empty?
end
- # Queues background migration jobs for an entire table, batched by ID range.
+ # Queues background migration jobs for an entire table in batches.
+ # The default batching column used is the standard primary key `id`.
# Each job is scheduled with a `delay_interval` in between.
# If you use a small interval, then some jobs may run at the same time.
#
@@ -68,6 +69,7 @@ module Gitlab
# is scheduled to be run. These records can be used to trace execution of the background job, but there is no
# builtin support to manage that automatically at this time. You should only set this flag if you are aware of
# how it works, and intend to manually cleanup the database records in your background job.
+ # primary_column_name - The name of the primary key column if the primary key is not `id`
#
# *Returns the final migration delay*
#
@@ -87,8 +89,9 @@ module Gitlab
# # do something
# end
# end
- def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_job_arguments: [], initial_delay: 0, track_jobs: false)
- raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+ def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_job_arguments: [], initial_delay: 0, track_jobs: false, primary_column_name: :id)
+ raise "#{model_class} does not have an ID column of #{primary_column_name} to use for batch ranges" unless model_class.column_names.include?(primary_column_name.to_s)
+ raise "#{primary_column_name} is not an integer column" unless model_class.columns_hash[primary_column_name.to_s].type == :integer
# To not overload the worker too much we enforce a minimum interval both
# when scheduling and performing jobs.
@@ -99,7 +102,7 @@ module Gitlab
final_delay = 0
model_class.each_batch(of: batch_size) do |relation, index|
- start_id, end_id = relation.pluck(Arel.sql('MIN(id), MAX(id)')).first
+ start_id, end_id = relation.pluck(Arel.sql("MIN(#{primary_column_name}), MAX(#{primary_column_name})")).first
# `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to
diff --git a/lib/gitlab/database/postgres_hll/batch_distinct_counter.rb b/lib/gitlab/database/postgres_hll/batch_distinct_counter.rb
new file mode 100644
index 00000000000..33faa2ef1b0
--- /dev/null
+++ b/lib/gitlab/database/postgres_hll/batch_distinct_counter.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module PostgresHll
+ # For large tables, PostgreSQL can take a long time to count rows due to MVCC.
+ # Implements a distinct batch counter based on HyperLogLog algorithm
+ # Needs indexes on the column below to calculate max, min and range queries
+ # For larger tables just set higher batch_size with index optimization
+ #
+ # In order to not use a possible complex time consuming query when calculating min and max values,
+ # the start and finish can be sent specifically, start and finish should contain max and min values for PRIMARY KEY of
+ # relation (most cases `id` column) rather than counted attribute eg:
+ # estimate_distinct_count(start: ::Project.with_active_services.minimum(:id), finish: ::Project.with_active_services.maximum(:id))
+ #
+ # Grouped relations are NOT supported yet.
+ #
+ # @example Usage
+ # ::Gitlab::Database::PostgresHllBatchDistinctCount.new(::Project, :creator_id).estimate_distinct_count
+ # ::Gitlab::Database::PostgresHllBatchDistinctCount.new(::Project.with_active_services.service_desk_enabled.where(time_period))
+ # .estimate_distinct_count(
+ # batch_size: 1_000,
+ # start: ::Project.with_active_services.service_desk_enabled.where(time_period).minimum(:id),
+ # finish: ::Project.with_active_services.service_desk_enabled.where(time_period).maximum(:id)
+ # )
+ #
+ # @note HyperLogLog is an PROBABILISTIC algorithm that ESTIMATES distinct count of given attribute value for supplied relation
+ # Like all probabilistic algorithm is has ERROR RATE margin, that can affect values,
+ # for given implementation no higher value was reported (https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45673#accuracy-estimation) than 5.3%
+ # for the most of a cases this value is lower. However, if the exact value is necessary other tools has to be used.
+ class BatchDistinctCounter
+ ERROR_RATE = 4.9 # max encountered empirical error rate, used in tests
+ FALLBACK = -1
+ MIN_REQUIRED_BATCH_SIZE = 750
+ SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep
+ MAX_DATA_VOLUME = 4_000_000_000
+
+ # Each query should take < 500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705
+ DEFAULT_BATCH_SIZE = 10_000
+
+ BIT_31_MASK = "B'0#{'1' * 31}'"
+ BIT_9_MASK = "B'#{'0' * 23}#{'1' * 9}'"
+ # @example source_query
+ # SELECT CAST(('X' || md5(CAST(%{column} as text))) as bit(32)) attr_hash_32_bits
+ # FROM %{relation}
+ # WHERE %{pkey} >= %{batch_start}
+ # AND %{pkey} < %{batch_end}
+ # AND %{column} IS NOT NULL
+ BUCKETED_DATA_SQL = <<~SQL
+ WITH hashed_attributes AS (%{source_query})
+ SELECT (attr_hash_32_bits & #{BIT_9_MASK})::int AS bucket_num,
+ (31 - floor(log(2, min((attr_hash_32_bits & #{BIT_31_MASK})::int))))::int as bucket_hash
+ FROM hashed_attributes
+ GROUP BY 1
+ SQL
+
+ TOTAL_BUCKETS_NUMBER = 512
+
+ def initialize(relation, column = nil)
+ @relation = relation
+ @column = column || relation.primary_key
+ end
+
+ def unwanted_configuration?(finish, batch_size, start)
+ batch_size <= MIN_REQUIRED_BATCH_SIZE ||
+ (finish - start) >= MAX_DATA_VOLUME ||
+ start > finish
+ end
+
+ def estimate_distinct_count(batch_size: nil, start: nil, finish: nil)
+ raise 'BatchCount can not be run inside a transaction' if ActiveRecord::Base.connection.transaction_open?
+
+ batch_size ||= DEFAULT_BATCH_SIZE
+
+ start = actual_start(start)
+ finish = actual_finish(finish)
+
+ raise "Batch counting expects positive values only for #{@column}" if start < 0 || finish < 0
+ return FALLBACK if unwanted_configuration?(finish, batch_size, start)
+
+ batch_start = start
+ hll_blob = {}
+
+ while batch_start <= finish
+ begin
+ hll_blob.merge!(hll_blob_for_batch(batch_start, batch_start + batch_size)) {|_key, old, new| new > old ? new : old }
+ batch_start += batch_size
+ end
+ sleep(SLEEP_TIME_IN_SECONDS)
+ end
+
+ estimate_cardinality(hll_blob)
+ end
+
+ private
+
+ # arbitrary values that are present in #estimate_cardinality
+ # are sourced from https://www.sisense.com/blog/hyperloglog-in-pure-sql/
+ # article, they are not representing any entity and serves as tune value
+ # for the whole equation
+ def estimate_cardinality(hll_blob)
+ num_zero_buckets = TOTAL_BUCKETS_NUMBER - hll_blob.size
+
+ num_uniques = (
+ ((TOTAL_BUCKETS_NUMBER**2) * (0.7213 / (1 + 1.079 / TOTAL_BUCKETS_NUMBER))) /
+ (num_zero_buckets + hll_blob.values.sum { |bucket_hash| 2**(-1 * bucket_hash)} )
+ ).to_i
+
+ if num_zero_buckets > 0 && num_uniques < 2.5 * TOTAL_BUCKETS_NUMBER
+ ((0.7213 / (1 + 1.079 / TOTAL_BUCKETS_NUMBER)) * (TOTAL_BUCKETS_NUMBER *
+ Math.log2(TOTAL_BUCKETS_NUMBER.to_f / num_zero_buckets)))
+ else
+ num_uniques
+ end
+ end
+
+ def hll_blob_for_batch(start, finish)
+ @relation
+ .connection
+ .execute(BUCKETED_DATA_SQL % { source_query: source_query(start, finish) })
+ .map(&:values)
+ .to_h
+ end
+
+ # Generate the source query SQL snippet for the provided id range
+ #
+ # @example SQL query template
+ # SELECT CAST(('X' || md5(CAST(%{column} as text))) as bit(32)) attr_hash_32_bits
+ # FROM %{relation}
+ # WHERE %{pkey} >= %{batch_start} AND %{pkey} < %{batch_end}
+ # AND %{column} IS NOT NULL
+ #
+ # @param start initial id range
+ # @param finish final id range
+ # @return [String] SQL query fragment
+ def source_query(start, finish)
+ col_as_arel = @column.is_a?(Arel::Attributes::Attribute) ? @column : Arel.sql(@column.to_s)
+ col_as_text = Arel::Nodes::NamedFunction.new('CAST', [col_as_arel.as('text')])
+ md5_of_col = Arel::Nodes::NamedFunction.new('md5', [col_as_text])
+ md5_as_hex = Arel::Nodes::Concat.new(Arel.sql("'X'"), md5_of_col)
+ bits = Arel::Nodes::NamedFunction.new('CAST', [md5_as_hex.as('bit(32)')])
+
+ @relation
+ .where(@relation.primary_key => (start...finish))
+ .where(col_as_arel.not_eq(nil))
+ .select(bits.as('attr_hash_32_bits')).to_sql
+ end
+
+ def actual_start(start)
+ start || @relation.unscope(:group, :having).minimum(@relation.primary_key) || 0
+ end
+
+ def actual_finish(finish)
+ finish || @relation.unscope(:group, :having).maximum(@relation.primary_key) || 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/postgres_index.rb b/lib/gitlab/database/postgres_index.rb
index 2a9f23f0098..6e734834841 100644
--- a/lib/gitlab/database/postgres_index.rb
+++ b/lib/gitlab/database/postgres_index.rb
@@ -3,9 +3,14 @@
module Gitlab
module Database
class PostgresIndex < ActiveRecord::Base
+ include Gitlab::Utils::StrongMemoize
+
self.table_name = 'postgres_indexes'
self.primary_key = 'identifier'
+ has_one :bloat_estimate, class_name: 'Gitlab::Database::PostgresIndexBloatEstimate', foreign_key: :identifier
+ has_many :reindexing_actions, class_name: 'Gitlab::Database::Reindexing::ReindexAction', foreign_key: :index_identifier
+
scope :by_identifier, ->(identifier) do
raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/
@@ -17,11 +22,17 @@ module Gitlab
# is defined on a table that is not partitioned.
scope :regular, -> { where(unique: false, partitioned: false, exclusion: false)}
- scope :random_few, ->(how_many) do
- limit(how_many).order(Arel.sql('RANDOM()'))
+ scope :not_match, ->(regex) { where("name !~ ?", regex)}
+
+ scope :not_recently_reindexed, -> do
+ recent_actions = Reindexing::ReindexAction.recent.where('index_identifier = identifier')
+
+ where('NOT EXISTS (?)', recent_actions)
end
- scope :not_match, ->(regex) { where("name !~ ?", regex)}
+ def bloat_size
+ strong_memoize(:bloat_size) { bloat_estimate&.bloat_size || 0 }
+ end
def to_s
name
diff --git a/lib/gitlab/database/postgres_index_bloat_estimate.rb b/lib/gitlab/database/postgres_index_bloat_estimate.rb
new file mode 100644
index 00000000000..379227bf87c
--- /dev/null
+++ b/lib/gitlab/database/postgres_index_bloat_estimate.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # Use this model with care: Retrieving bloat statistics
+ # for all indexes can be expensive in a large database.
+ #
+ # Best used on a per-index basis.
+ class PostgresIndexBloatEstimate < ActiveRecord::Base
+ self.table_name = 'postgres_index_bloat_estimates'
+ self.primary_key = 'identifier'
+
+ belongs_to :index, foreign_key: :identifier, class_name: 'Gitlab::Database::PostgresIndex'
+
+ alias_attribute :bloat_size, :bloat_size_bytes
+ end
+ end
+end
diff --git a/lib/gitlab/database/postgresql_adapter/empty_query_ping.rb b/lib/gitlab/database/postgresql_adapter/empty_query_ping.rb
new file mode 100644
index 00000000000..906312478ac
--- /dev/null
+++ b/lib/gitlab/database/postgresql_adapter/empty_query_ping.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# rubocop:disable Gitlab/ModuleWithInstanceVariables
+module Gitlab
+ module Database
+ module PostgresqlAdapter
+ module EmptyQueryPing
+ # ActiveRecord uses `SELECT 1` to check if the connection is alive
+ # We patch this here to use an empty query instead, which is a bit faster
+ def active?
+ @lock.synchronize do
+ @connection.query ';'
+ end
+ true
+ rescue PG::Error
+ false
+ end
+ end
+ end
+ end
+end
+# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/lib/gitlab/database/reindexing.rb b/lib/gitlab/database/reindexing.rb
index c77e000254f..832f7438cf9 100644
--- a/lib/gitlab/database/reindexing.rb
+++ b/lib/gitlab/database/reindexing.rb
@@ -3,8 +3,14 @@
module Gitlab
module Database
module Reindexing
- def self.perform(index_selector)
- Coordinator.new(index_selector).perform
+ # Number of indexes to reindex per invocation
+ DEFAULT_INDEXES_PER_INVOCATION = 2
+
+ # candidate_indexes: Array of Gitlab::Database::PostgresIndex
+ def self.perform(candidate_indexes, how_many: DEFAULT_INDEXES_PER_INVOCATION)
+ indexes = IndexSelection.new(candidate_indexes).take(how_many)
+
+ Coordinator.new(indexes).perform
end
def self.candidate_indexes
diff --git a/lib/gitlab/database/reindexing/concurrent_reindex.rb b/lib/gitlab/database/reindexing/concurrent_reindex.rb
index fd3dca88567..a6fe7d61a4f 100644
--- a/lib/gitlab/database/reindexing/concurrent_reindex.rb
+++ b/lib/gitlab/database/reindexing/concurrent_reindex.rb
@@ -59,6 +59,13 @@ module Gitlab
raise ReindexError, "failed to reindex #{index}: #{message}"
end
+ # Some expression indexes (aka functional indexes)
+ # require additional statistics. The existing statistics
+ # are tightly bound to the original index. We have to
+ # rebuild statistics for the new index before dropping
+ # the original one.
+ rebuild_statistics if index.expression?
+
yield replacement_index
ensure
begin
@@ -96,6 +103,14 @@ module Gitlab
end
end
+ def rebuild_statistics
+ logger.info("rebuilding table statistics for #{index.schema}.#{index.tablename}")
+
+ connection.execute(<<~SQL)
+ ANALYZE #{quote_table_name(index.schema)}.#{quote_table_name(index.tablename)}
+ SQL
+ end
+
def replacement_index_name
@replacement_index_name ||= "#{TEMPORARY_INDEX_PREFIX}#{index.indexrelid}"
end
diff --git a/lib/gitlab/database/reindexing/index_selection.rb b/lib/gitlab/database/reindexing/index_selection.rb
new file mode 100644
index 00000000000..406e70791df
--- /dev/null
+++ b/lib/gitlab/database/reindexing/index_selection.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Reindexing
+ class IndexSelection
+ include Enumerable
+
+ delegate :each, to: :indexes
+
+ def initialize(candidates)
+ @candidates = candidates
+ end
+
+ private
+
+ attr_reader :candidates
+
+ def indexes
+ # This is an explicit N+1 query:
+ # Bloat estimates are generally available through a view
+ # for all indexes. However, estimating bloat for all
+ # indexes at once is an expensive operation. Therefore,
+ # we force a N+1 pattern here and estimate bloat on a per-index
+ # basis.
+
+ @indexes ||= filter_candidates.sort_by(&:bloat_size).reverse
+ end
+
+ def filter_candidates
+ candidates.not_recently_reindexed
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/reindexing/reindex_action.rb b/lib/gitlab/database/reindexing/reindex_action.rb
index 0928ef90e5d..8c59cffe5fb 100644
--- a/lib/gitlab/database/reindexing/reindex_action.rb
+++ b/lib/gitlab/database/reindexing/reindex_action.rb
@@ -6,13 +6,20 @@ module Gitlab
class ReindexAction < ActiveRecord::Base
self.table_name = 'postgres_reindex_actions'
+ belongs_to :index, foreign_key: :index_identifier, class_name: 'Gitlab::Database::PostgresIndex'
enum state: { started: 0, finished: 1, failed: 2 }
+ # Amount of time to consider a previous reindexing *recent*
+ RECENT_THRESHOLD = 7.days
+
+ scope :recent, -> { where(state: :finished).where('action_end > ?', Time.zone.now - RECENT_THRESHOLD) }
+
def self.keep_track_of(index, &block)
action = create!(
index_identifier: index.identifier,
action_start: Time.zone.now,
- ondisk_size_bytes_start: index.ondisk_size_bytes
+ ondisk_size_bytes_start: index.ondisk_size_bytes,
+ bloat_estimate_bytes_start: index.bloat_size
)
yield
diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
index 88f035c2d1b..b1093b2fca4 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -147,7 +147,7 @@ module Gitlab
initialize_with_readme: true,
visibility_level: VISIBILITY_LEVEL,
name: PROJECT_NAME,
- description: "This project is automatically generated and will be used to help monitor this GitLab instance. [More information](#{docs_path})",
+ description: "This project is automatically generated and helps monitor this GitLab instance. [Learn more](#{docs_path}).",
namespace_id: group.id
}
end
diff --git a/lib/gitlab/deploy_key_access.rb b/lib/gitlab/deploy_key_access.rb
new file mode 100644
index 00000000000..ca16582d2b4
--- /dev/null
+++ b/lib/gitlab/deploy_key_access.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class DeployKeyAccess < UserAccess
+ def initialize(deploy_key, container: nil)
+ @deploy_key = deploy_key
+ @user = deploy_key.user
+ @container = container
+ end
+
+ def can_push_for_ref?(ref)
+ can_push_to_branch?(ref)
+ end
+
+ private
+
+ attr_reader :deploy_key
+
+ def protected_tag_accessible_to?(ref, action:)
+ assert_project!
+
+ # a deploy key can always push a protected tag
+ # (which is not always the case when pushing to a protected branch)
+ true
+ end
+
+ def can_collaborate?(_ref)
+ assert_project!
+
+ project_has_active_user_keys?
+ end
+
+ def project_has_active_user_keys?
+ user.can?(:read_project, project) && DeployKey.with_write_access_for_project(project).id_in(deploy_key.id).exists?
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index cf0611e44da..8f4f8febec0 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -30,12 +30,16 @@ module Gitlab
@diffs ||= diffable.raw_diffs(diff_options)
end
- def diff_files
- raw_diff_files
+ def diff_files(sorted: false)
+ raw_diff_files(sorted: sorted)
end
- def raw_diff_files
- @raw_diff_files ||= diffs.decorate! { |diff| decorate_diff!(diff) }
+ def raw_diff_files(sorted: false)
+ strong_memoize(:"raw_diff_files_#{sorted}") do
+ collection = diffs.decorate! { |diff| decorate_diff!(diff) }
+ collection = sort_diffs(collection) if sorted
+ collection
+ end
end
def diff_file_paths
@@ -111,6 +115,12 @@ module Gitlab
fallback_diff_refs: fallback_diff_refs,
stats: stats)
end
+
+ def sort_diffs(diffs)
+ return diffs unless Feature.enabled?(:sort_diffs, project, default_enabled: false)
+
+ Gitlab::Diff::FileCollectionSorter.new(diffs).sort
+ end
end
end
end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
index 16257bb5ff5..d2ca86fdfe7 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
@@ -16,7 +16,7 @@ module Gitlab
fallback_diff_refs: merge_request_diff.fallback_diff_refs)
end
- def diff_files
+ def diff_files(sorted: false)
strong_memoize(:diff_files) do
diff_files = super
@@ -26,6 +26,12 @@ module Gitlab
end
end
+ def raw_diff_files(sorted: false)
+ # We force `sorted` to `false` as we don't need to sort the diffs when
+ # dealing with `MergeRequestDiff` since we sort its files on create.
+ super(sorted: false)
+ end
+
override :write_cache
def write_cache
highlight_cache.write_if_empty
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb b/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb
index 9af66318b89..64523f3b730 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb
@@ -11,7 +11,7 @@ module Gitlab
#
class MergeRequestDiffBatch < MergeRequestDiffBase
DEFAULT_BATCH_PAGE = 1
- DEFAULT_BATCH_SIZE = 20
+ DEFAULT_BATCH_SIZE = 30
attr_reader :pagination_data
@@ -21,9 +21,9 @@ module Gitlab
@paginated_collection = load_paginated_collection(batch_page, batch_size, diff_options)
@pagination_data = {
- current_page: @paginated_collection.current_page,
- next_page: @paginated_collection.next_page,
- total_pages: @paginated_collection.total_pages
+ current_page: batch_gradual_load? ? nil : @paginated_collection.current_page,
+ next_page: batch_gradual_load? ? nil : @paginated_collection.next_page,
+ total_pages: batch_gradual_load? ? relation.size : @paginated_collection.total_pages
}
end
@@ -62,17 +62,28 @@ module Gitlab
@merge_request_diff.merge_request_diff_files
end
+ # rubocop: disable CodeReuse/ActiveRecord
def load_paginated_collection(batch_page, batch_size, diff_options)
batch_page ||= DEFAULT_BATCH_PAGE
batch_size ||= DEFAULT_BATCH_SIZE
paths = diff_options&.fetch(:paths, nil)
- paginated_collection = relation.page(batch_page).per(batch_size)
+ paginated_collection = if batch_gradual_load?
+ relation.offset(batch_page).limit([batch_size.to_i, DEFAULT_BATCH_SIZE].min)
+ else
+ relation.page(batch_page).per(batch_size)
+ end
+
paginated_collection = paginated_collection.by_paths(paths) if paths
paginated_collection
end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def batch_gradual_load?
+ Feature.enabled?(:diffs_gradual_load, @merge_request_diff.project, default_enabled: true)
+ end
end
end
end
diff --git a/lib/gitlab/diff/file_collection_sorter.rb b/lib/gitlab/diff/file_collection_sorter.rb
new file mode 100644
index 00000000000..94626875580
--- /dev/null
+++ b/lib/gitlab/diff/file_collection_sorter.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ class FileCollectionSorter
+ attr_reader :diffs
+
+ def initialize(diffs)
+ @diffs = diffs
+ end
+
+ def sort
+ diffs.sort do |a, b|
+ compare_path_parts(path_parts(a), path_parts(b))
+ end
+ end
+
+ private
+
+ def path_parts(diff)
+ (diff.new_path.presence || diff.old_path).split(::File::SEPARATOR)
+ end
+
+ # Used for sorting the file paths by:
+ # 1. Directory name
+ # 2. Depth
+ # 3. File name
+ def compare_path_parts(a_parts, b_parts)
+ a_part = a_parts.shift
+ b_part = b_parts.shift
+
+ return 1 if a_parts.size < b_parts.size && a_parts.empty?
+ return -1 if a_parts.size > b_parts.size && b_parts.empty?
+
+ comparison = a_part <=> b_part
+
+ return comparison unless comparison == 0
+
+ compare_path_parts(a_parts, b_parts)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
index 1beea4f9054..9e476dd4e2b 100644
--- a/lib/gitlab/email/handler/reply_processing.rb
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -45,7 +45,7 @@ module Gitlab
end
def add_attachments(reply)
- attachments = Email::AttachmentUploader.new(mail).execute(upload_params)
+ attachments = Email::AttachmentUploader.new(mail).execute(**upload_params)
reply + attachments.map do |link|
"\n\n#{link[:markdown]}"
diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb
index bcd8b98a06f..0bbe3980f67 100644
--- a/lib/gitlab/email/handler/service_desk_handler.rb
+++ b/lib/gitlab/email/handler/service_desk_handler.rb
@@ -68,7 +68,7 @@ module Gitlab
end
def valid_project_key?(project, slug)
- project.present? && slug == project.full_path_slug && Feature.enabled?(:service_desk_custom_address, project)
+ project.present? && slug == project.full_path_slug && Feature.enabled?(:service_desk_custom_address, project, default_enabled: true)
end
def create_issue!
@@ -78,7 +78,7 @@ module Gitlab
title: issue_title,
description: message_including_template,
confidential: true,
- service_desk_reply_to: from_address
+ external_author: from_address
).execute
raise InvalidIssueError unless @issue.persisted?
diff --git a/lib/gitlab/encrypted_configuration.rb b/lib/gitlab/encrypted_configuration.rb
new file mode 100644
index 00000000000..fe49af3ab33
--- /dev/null
+++ b/lib/gitlab/encrypted_configuration.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class EncryptedConfiguration
+ delegate :[], :fetch, to: :config
+ delegate_missing_to :options
+ attr_reader :content_path, :key, :previous_keys
+
+ CIPHER = "aes-256-gcm"
+ SALT = "GitLabEncryptedConfigSalt"
+
+ class MissingKeyError < RuntimeError
+ def initialize(msg = "Missing encryption key to encrypt/decrypt file with.")
+ super
+ end
+ end
+
+ class InvalidConfigError < RuntimeError
+ def initialize(msg = "Content was not a valid yml config file")
+ super
+ end
+ end
+
+ def self.generate_key(base_key)
+ # Because the salt is static, we want uniqueness to be coming from the base_key
+ # Error if the base_key is empty or suspiciously short
+ raise 'Base key too small' if base_key.blank? || base_key.length < 16
+
+ ActiveSupport::KeyGenerator.new(base_key).generate_key(SALT, ActiveSupport::MessageEncryptor.key_len(CIPHER))
+ end
+
+ def initialize(content_path: nil, base_key: nil, previous_keys: [])
+ @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path } if content_path
+ @key = self.class.generate_key(base_key) if base_key
+ @previous_keys = previous_keys
+ end
+
+ def active?
+ content_path&.exist?
+ end
+
+ def read
+ if active?
+ decrypt(content_path.binread)
+ else
+ ""
+ end
+ end
+
+ def write(contents)
+ # ensure contents are valid to deserialize before write
+ deserialize(contents)
+
+ temp_file = Tempfile.new(File.basename(content_path), File.dirname(content_path))
+ File.open(temp_file.path, 'wb') do |file|
+ file.write(encrypt(contents))
+ end
+ FileUtils.mv(temp_file.path, content_path)
+ ensure
+ temp_file&.unlink
+ end
+
+ def config
+ return @config if @config
+
+ contents = deserialize(read)
+
+ raise InvalidConfigError.new unless contents.is_a?(Hash)
+
+ @config = contents.deep_symbolize_keys
+ end
+
+ def change(&block)
+ writing(read, &block)
+ end
+
+ private
+
+ def writing(contents)
+ updated_contents = yield contents
+
+ write(updated_contents) if updated_contents != contents
+ end
+
+ def encrypt(contents)
+ handle_missing_key!
+ encryptor.encrypt_and_sign(contents)
+ end
+
+ def decrypt(contents)
+ handle_missing_key!
+ encryptor.decrypt_and_verify(contents)
+ end
+
+ def encryptor
+ return @encryptor if @encryptor
+
+ @encryptor = ActiveSupport::MessageEncryptor.new(key, cipher: CIPHER)
+
+ # Allow fallback to previous keys
+ @previous_keys.each do |key|
+ @encryptor.rotate(self.class.generate_key(key))
+ end
+
+ @encryptor
+ end
+
+ def options
+ # Allows top level keys to be referenced using dot syntax
+ @options ||= ActiveSupport::InheritableOptions.new(config)
+ end
+
+ def deserialize(contents)
+ YAML.safe_load(contents, permitted_classes: [Symbol]).presence || {}
+ end
+
+ def handle_missing_key!
+ raise MissingKeyError.new if @key.nil?
+ end
+ end
+end
diff --git a/lib/gitlab/encrypted_ldap_command.rb b/lib/gitlab/encrypted_ldap_command.rb
new file mode 100644
index 00000000000..cdb3e268b51
--- /dev/null
+++ b/lib/gitlab/encrypted_ldap_command.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+# rubocop:disable Rails/Output
+module Gitlab
+ class EncryptedLdapCommand
+ class << self
+ def write(contents)
+ encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets
+ return unless validate_config(encrypted)
+
+ validate_contents(contents)
+ encrypted.write(contents)
+
+ puts "File encrypted and saved."
+ rescue Interrupt
+ puts "Aborted changing file: nothing saved."
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ puts "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?"
+ end
+
+ def edit
+ encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets
+ return unless validate_config(encrypted)
+
+ if ENV["EDITOR"].blank?
+ puts 'No $EDITOR specified to open file. Please provide one when running the command:'
+ puts 'gitlab-rake gitlab:ldap:secret:edit EDITOR=vim'
+ return
+ end
+
+ temp_file = Tempfile.new(File.basename(encrypted.content_path), File.dirname(encrypted.content_path))
+ contents_changed = false
+
+ encrypted.change do |contents|
+ contents = encrypted_file_template unless File.exist?(encrypted.content_path)
+ File.write(temp_file.path, contents)
+ system(ENV['EDITOR'], temp_file.path)
+ changes = File.read(temp_file.path)
+ contents_changed = contents != changes
+ validate_contents(changes)
+ changes
+ end
+
+ puts "Contents were unchanged." unless contents_changed
+ puts "File encrypted and saved."
+ rescue Interrupt
+ puts "Aborted changing file: nothing saved."
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ puts "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?"
+ ensure
+ temp_file&.unlink
+ end
+
+ def show
+ encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets
+ return unless validate_config(encrypted)
+
+ puts encrypted.read.presence || "File '#{encrypted.content_path}' does not exist. Use `gitlab-rake gitlab:ldap:secret:edit` to change that."
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ puts "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?"
+ end
+
+ private
+
+ def validate_config(encrypted)
+ dir_path = File.dirname(encrypted.content_path)
+
+ unless File.exist?(dir_path)
+ puts "Directory #{dir_path} does not exist. Create the directory and try again."
+ return false
+ end
+
+ if encrypted.key.nil?
+ puts "Missing encryption key encrypted_settings_key_base."
+ return false
+ end
+
+ true
+ end
+
+ def validate_contents(contents)
+ begin
+ config = YAML.safe_load(contents, permitted_classes: [Symbol])
+ error_contents = "Did not include any key-value pairs" unless config.is_a?(Hash)
+ rescue Psych::Exception => e
+ error_contents = e.message
+ end
+
+ puts "WARNING: Content was not a valid LDAP secret yml file. #{error_contents}" if error_contents
+
+ contents
+ end
+
+ def encrypted_file_template
+ <<~YAML
+ # main:
+ # password: '123'
+ # user_dn: 'gitlab-adm'
+ YAML
+ end
+ end
+ end
+end
+# rubocop:enable Rails/Output
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 6e39776bbd4..94523813662 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -4,9 +4,11 @@
#
# Utility module for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant.
# Experiment options:
-# - environment (optional, defaults to enabled for development and GitLab.com)
# - tracking_category (optional, used to set the category when tracking an experiment event)
-# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility)
+# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility -- you likely do not need this, see note in the next paragraph.)
+#
+# Using the backwards-compatible subject index (use_backwards_compatible_subject_index option):
+# This option was added when [the calculation of experimentation_subject_index was changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45733/diffs#41af4a6fa5a10c7068559ce21c5188483751d934_157_173). It is not intended to be used by new experiments, it exists merely for the segmentation integrity of in-flight experiments at the time the change was deployed. That is, we want users who were assigned to the "experimental" group or the "control" group before the change to still be in those same groups after the change. See [the original issue](https://gitlab.com/gitlab-org/gitlab/-/issues/270858) and [this related comment](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48110#note_458223745) for more information.
#
# The experiment is controlled by a Feature Flag (https://docs.gitlab.com/ee/development/feature_flags/controls.html),
# which is named "#{experiment_key}_experiment_percentage" and *must* be set with a percentage and not be used for other purposes.
@@ -55,10 +57,6 @@ module Gitlab
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA',
use_backwards_compatible_subject_index: true
},
- new_create_project_ui: {
- tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi',
- use_backwards_compatible_subject_index: true
- },
contact_sales_btn_in_app: {
tracking_category: 'Growth::Conversion::Experiment::ContactSalesInApp',
use_backwards_compatible_subject_index: true
@@ -67,14 +65,6 @@ module Gitlab
tracking_category: 'Growth::Expansion::Experiment::CustomizeHomepage',
use_backwards_compatible_subject_index: true
},
- invite_email: {
- tracking_category: 'Growth::Acquisition::Experiment::InviteEmail',
- use_backwards_compatible_subject_index: true
- },
- invitation_reminders: {
- tracking_category: 'Growth::Acquisition::Experiment::InvitationReminders',
- use_backwards_compatible_subject_index: true
- },
group_only_trials: {
tracking_category: 'Growth::Conversion::Experiment::GroupOnlyTrials',
use_backwards_compatible_subject_index: true
@@ -82,59 +72,68 @@ module Gitlab
default_to_issues_board: {
tracking_category: 'Growth::Conversion::Experiment::DefaultToIssuesBoard',
use_backwards_compatible_subject_index: true
+ },
+ jobs_empty_state: {
+ tracking_category: 'Growth::Activation::Experiment::JobsEmptyState'
+ },
+ remove_known_trial_form_fields: {
+ tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFields'
+ },
+ trimmed_skip_trial_copy: {
+ tracking_category: 'Growth::Conversion::Experiment::TrimmedSkipTrialCopy'
+ },
+ trial_registration_with_social_signin: {
+ tracking_category: 'Growth::Conversion::Experiment::TrialRegistrationWithSocialSigning'
+ },
+ invite_members_empty_project_version_a: {
+ tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA'
}
}.freeze
class << self
- def experiment(key)
- Experiment.new(EXPERIMENTS[key].merge(key: key))
- end
-
- def enabled?(experiment_key)
- return false unless EXPERIMENTS.key?(experiment_key)
+ def get_experiment(experiment_key)
+ return unless EXPERIMENTS.key?(experiment_key)
- experiment = experiment(experiment_key)
- experiment.enabled_for_environment? && experiment.enabled?
+ ::Gitlab::Experimentation::Experiment.new(experiment_key, **EXPERIMENTS[experiment_key])
end
- def enabled_for_attribute?(experiment_key, attribute)
- index = Digest::SHA1.hexdigest(attribute).hex % 100
- enabled_for_value?(experiment_key, index)
- end
+ def active?(experiment_key)
+ experiment = get_experiment(experiment_key)
+ return false unless experiment
- def enabled_for_value?(experiment_key, value)
- enabled?(experiment_key) && experiment(experiment_key).enabled_for_index?(value)
+ experiment.active?
end
- end
- Experiment = Struct.new(
- :key,
- :environment,
- :tracking_category,
- :use_backwards_compatible_subject_index,
- keyword_init: true
- ) do
- def enabled?
- experiment_percentage > 0
- end
+ def in_experiment_group?(experiment_key, subject:)
+ return false if subject.blank?
+ return false unless active?(experiment_key)
- def enabled_for_environment?
- return ::Gitlab.dev_env_or_com? if environment.nil?
+ experiment = get_experiment(experiment_key)
+ return false unless experiment
- environment
+ experiment.enabled_for_index?(index_for_subject(experiment, subject))
end
- def enabled_for_index?(index)
- return false if index.blank?
+ private
- index <= experiment_percentage
- end
+ def index_for_subject(experiment, subject)
+ index = if experiment.use_backwards_compatible_subject_index
+ Digest::SHA1.hexdigest(subject_id(subject)).hex
+ else
+ Zlib.crc32("#{experiment.key}#{subject_id(subject)}")
+ end
- private
+ index % 100
+ end
- # When a feature does not exist, the `percentage_of_time_value` method will return 0
- def experiment_percentage
- @experiment_percentage ||= Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet
+ def subject_id(subject)
+ if subject.respond_to?(:to_global_id)
+ subject.to_global_id.to_s
+ elsif subject.respond_to?(:to_s)
+ subject.to_s
+ else
+ raise ArgumentError.new('Subject must respond to `to_global_id` or `to_s`')
+ end
end
end
end
diff --git a/lib/gitlab/experimentation/controller_concern.rb b/lib/gitlab/experimentation/controller_concern.rb
index c6d15d7d82d..c85d3f4eee6 100644
--- a/lib/gitlab/experimentation/controller_concern.rb
+++ b/lib/gitlab/experimentation/controller_concern.rb
@@ -3,7 +3,7 @@
require 'zlib'
# Controller concern that checks if an `experimentation_subject_id cookie` is present and sets it if absent.
-# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method
+# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name, subject: nil)` method
# to controllers and views. It returns true when the experiment is enabled and the user is selected as part
# of the experimental group.
#
@@ -28,47 +28,56 @@ module Gitlab
}
end
- def push_frontend_experiment(experiment_key)
+ def push_frontend_experiment(experiment_key, subject: nil)
var_name = experiment_key.to_s.camelize(:lower)
- enabled = experiment_enabled?(experiment_key)
+
+ enabled = experiment_enabled?(experiment_key, subject: subject)
gon.push({ experiments: { var_name => enabled } }, true)
end
- def experiment_enabled?(experiment_key)
+ def experiment_enabled?(experiment_key, subject: nil)
+ return true if forced_enabled?(experiment_key)
return false if dnt_enabled?
- return true if Experimentation.enabled_for_value?(experiment_key, experimentation_subject_index(experiment_key))
- return true if forced_enabled?(experiment_key)
+ subject ||= fallback_experimentation_subject_index(experiment_key)
- false
+ Experimentation.in_experiment_group?(experiment_key, subject: subject)
end
- def track_experiment_event(experiment_key, action, value = nil)
+ def track_experiment_event(experiment_key, action, value = nil, subject: nil)
return if dnt_enabled?
- track_experiment_event_for(experiment_key, action, value) do |tracking_data|
+ track_experiment_event_for(experiment_key, action, value, subject: subject) do |tracking_data|
::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), **tracking_data)
end
end
- def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
+ def frontend_experimentation_tracking_data(experiment_key, action, value = nil, subject: nil)
return if dnt_enabled?
- track_experiment_event_for(experiment_key, action, value) do |tracking_data|
+ track_experiment_event_for(experiment_key, action, value, subject: subject) do |tracking_data|
gon.push(tracking_data: tracking_data)
end
end
- def record_experiment_user(experiment_key)
+ def record_experiment_user(experiment_key, context = {})
+ return if dnt_enabled?
+ return unless Experimentation.active?(experiment_key) && current_user
+
+ ::Experiment.add_user(experiment_key, tracking_group(experiment_key, nil, subject: current_user), current_user, context)
+ end
+
+ def record_experiment_conversion_event(experiment_key)
return if dnt_enabled?
- return unless Experimentation.enabled?(experiment_key) && current_user
+ return unless current_user
+ return unless Experimentation.active?(experiment_key)
- ::Experiment.add_user(experiment_key, tracking_group(experiment_key), current_user)
+ ::Experiment.record_conversion_event(experiment_key, current_user)
end
- def experiment_tracking_category_and_group(experiment_key)
- "#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group')}"
+ def experiment_tracking_category_and_group(experiment_key, subject: nil)
+ "#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group', subject: subject)}"
end
private
@@ -81,40 +90,41 @@ module Gitlab
cookies.signed[:experimentation_subject_id]
end
- def experimentation_subject_index(experiment_key)
+ def fallback_experimentation_subject_index(experiment_key)
return if experimentation_subject_id.blank?
- if Experimentation.experiment(experiment_key).use_backwards_compatible_subject_index
- experimentation_subject_id.delete('-').hex % 100
+ if Experimentation.get_experiment(experiment_key).use_backwards_compatible_subject_index
+ experimentation_subject_id.delete('-')
else
- Zlib.crc32("#{experiment_key}#{experimentation_subject_id}") % 100
+ experimentation_subject_id
end
end
- def track_experiment_event_for(experiment_key, action, value)
- return unless Experimentation.enabled?(experiment_key)
+ def track_experiment_event_for(experiment_key, action, value, subject: nil)
+ return unless Experimentation.active?(experiment_key)
- yield experimentation_tracking_data(experiment_key, action, value)
+ yield experimentation_tracking_data(experiment_key, action, value, subject: subject)
end
- def experimentation_tracking_data(experiment_key, action, value)
+ def experimentation_tracking_data(experiment_key, action, value, subject: nil)
{
category: tracking_category(experiment_key),
action: action,
- property: tracking_group(experiment_key, "_group"),
- label: experimentation_subject_id,
+ property: tracking_group(experiment_key, "_group", subject: subject),
+ label: tracking_label(subject),
value: value
}.compact
end
def tracking_category(experiment_key)
- Experimentation.experiment(experiment_key).tracking_category
+ Experimentation.get_experiment(experiment_key).tracking_category
end
- def tracking_group(experiment_key, suffix = nil)
- return unless Experimentation.enabled?(experiment_key)
+ def tracking_group(experiment_key, suffix = nil, subject: nil)
+ return unless Experimentation.active?(experiment_key)
- group = experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL
+ subject ||= fallback_experimentation_subject_index(experiment_key)
+ group = experiment_enabled?(experiment_key, subject: subject) ? GROUP_EXPERIMENTAL : GROUP_CONTROL
suffix ? "#{group}#{suffix}" : group
end
@@ -122,6 +132,16 @@ module Gitlab
def forced_enabled?(experiment_key)
params.has_key?(:force_experiment) && params[:force_experiment] == experiment_key.to_s
end
+
+ def tracking_label(subject)
+ return experimentation_subject_id if subject.blank?
+
+ if subject.respond_to?(:to_global_id)
+ Digest::MD5.hexdigest(subject.to_global_id.to_s)
+ else
+ Digest::MD5.hexdigest(subject.to_s)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/experimentation/experiment.rb b/lib/gitlab/experimentation/experiment.rb
new file mode 100644
index 00000000000..e594c3bedeb
--- /dev/null
+++ b/lib/gitlab/experimentation/experiment.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Experimentation
+ class Experiment
+ attr_reader :key, :tracking_category, :use_backwards_compatible_subject_index
+
+ def initialize(key, **params)
+ @key = key
+ @tracking_category = params[:tracking_category]
+ @use_backwards_compatible_subject_index = params[:use_backwards_compatible_subject_index]
+
+ @experiment_percentage = Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet
+ end
+
+ def active?
+ ::Gitlab.dev_env_or_com? && experiment_percentage > 0
+ end
+
+ def enabled_for_index?(index)
+ return false if index.blank?
+
+ index <= experiment_percentage
+ end
+
+ private
+
+ attr_reader :experiment_percentage
+ end
+ end
+end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 96f3487fd6f..a2215366bdc 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -17,6 +17,7 @@ module Gitlab
CommitError = Class.new(BaseError)
OSError = Class.new(BaseError)
UnknownRef = Class.new(BaseError)
+ CommandTimedOut = Class.new(CommandError)
class << self
include Gitlab::EncodingHelper
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 6090d1b9f69..8df4bc3de05 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -66,6 +66,12 @@ module Gitlab
@iterator = nil
end
+ def sort(&block)
+ @array = @array.sort(&block)
+
+ self
+ end
+
def empty?
any? # Make sure the iterator has been exercised
@empty
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index bc712e87e99..f6601379202 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -467,6 +467,18 @@ module Gitlab
empty_diff_stats
end
+ def find_changed_paths(commits)
+ 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)
+ end
+ rescue CommandError, TypeError, NoRepository
+ []
+ end
+
# Returns a RefName for a given SHA
def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present?
diff --git a/lib/gitlab/git/wraps_gitaly_errors.rb b/lib/gitlab/git/wraps_gitaly_errors.rb
index 9963bcfbf1c..2009683d32c 100644
--- a/lib/gitlab/git/wraps_gitaly_errors.rb
+++ b/lib/gitlab/git/wraps_gitaly_errors.rb
@@ -9,6 +9,8 @@ module Gitlab
raise Gitlab::Git::Repository::NoRepository.new(e)
rescue GRPC::InvalidArgument => e
raise ArgumentError.new(e)
+ rescue GRPC::DeadlineExceeded => e
+ raise Gitlab::Git::CommandTimedOut.new(e)
rescue GRPC::BadStatus => e
raise Gitlab::Git::CommandError.new(e)
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 0576d1dd9db..e0b145f69aa 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -43,7 +43,7 @@ module Gitlab
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :protocol, :authentication_abilities,
- :namespace_path, :redirected_path, :auth_result_type,
+ :repository_path, :redirected_path, :auth_result_type,
:cmd, :changes
attr_accessor :container
@@ -57,21 +57,16 @@ module Gitlab
raise ArgumentError, "No error message defined for #{key}"
end
- def initialize(actor, container, protocol, authentication_abilities:, namespace_path: nil, repository_path: nil, redirected_path: nil, auth_result_type: nil)
+ def initialize(actor, container, protocol, authentication_abilities:, repository_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor
@container = container
@protocol = protocol
@authentication_abilities = Array(authentication_abilities)
- @namespace_path = namespace_path
@repository_path = repository_path
@redirected_path = redirected_path
@auth_result_type = auth_result_type
end
- def repository_path
- @repository_path ||= project&.path
- end
-
def check(cmd, changes)
@changes = changes
@cmd = cmd
@@ -82,6 +77,7 @@ module Gitlab
check_authentication_abilities!
check_command_disabled!
check_command_existence!
+ check_otp_session!
custom_action = check_custom_action
return custom_action if custom_action
@@ -259,6 +255,31 @@ module Gitlab
end
end
+ def check_otp_session!
+ return unless ssh?
+ return if !key? || deploy_key?
+ return unless Feature.enabled?(:two_factor_for_cli)
+ return unless user.two_factor_enabled?
+
+ if ::Gitlab::Auth::Otp::SessionEnforcer.new(actor).access_restricted?
+ message = "OTP verification is required to access the repository.\n\n"\
+ " Use: #{build_ssh_otp_verify_command}"
+
+ raise ForbiddenError, message
+ end
+ end
+
+ def build_ssh_otp_verify_command
+ user = "#{Gitlab.config.gitlab_shell.ssh_user}@" unless Gitlab.config.gitlab_shell.ssh_user.empty?
+ user_host = "#{user}#{Gitlab.config.gitlab_shell.ssh_host}"
+
+ if Gitlab.config.gitlab_shell.ssh_port != 22
+ "ssh #{user_host} -p #{Gitlab.config.gitlab_shell.ssh_port} 2fa_verify"
+ else
+ "ssh #{user_host} 2fa_verify"
+ end
+ end
+
def check_db_accessibility!
return unless receive_pack?
@@ -324,11 +345,11 @@ module Gitlab
end
def check_change_access!
- # Deploy keys with write access can push anything
- return if deploy_key?
+ return if deploy_key? && !deploy_keys_on_protected_branches_enabled?
if changes == ANY
- can_push = user_can_push? ||
+ can_push = (deploy_key? && deploy_keys_on_protected_branches_enabled?) ||
+ user_can_push? ||
project&.any_branch_allows_collaboration?(user_access.user)
unless can_push
@@ -404,6 +425,10 @@ module Gitlab
protocol == 'http'
end
+ def ssh?
+ protocol == 'ssh'
+ end
+
def upload_pack?
cmd == 'git-upload-pack'
end
@@ -454,6 +479,8 @@ module Gitlab
CiAccess.new
elsif user && request_from_ci_build?
BuildAccess.new(user, container: container)
+ elsif deploy_key? && deploy_keys_on_protected_branches_enabled?
+ DeployKeyAccess.new(deploy_key, container: container)
else
UserAccess.new(user, container: container)
end
@@ -531,6 +558,10 @@ module Gitlab
def size_checker
container.repository_size_checker
end
+
+ def deploy_keys_on_protected_branches_enabled?
+ Feature.enabled?(:deploy_keys_on_protected_branches, project)
+ end
end
end
diff --git a/lib/gitlab/git_access_project.rb b/lib/gitlab/git_access_project.rb
index cdefcc84f7d..7e9bab4a8e6 100644
--- a/lib/gitlab/git_access_project.rb
+++ b/lib/gitlab/git_access_project.rb
@@ -35,7 +35,19 @@ module Gitlab
end
def namespace
- @namespace ||= Namespace.find_by_full_path(namespace_path)
+ strong_memoize(:namespace) { Namespace.find_by_full_path(namespace_path) }
+ end
+
+ def namespace_path
+ strong_memoize(:namespace_path) { repository_path_match[:namespace_path] }
+ end
+
+ def project_path
+ strong_memoize(:project_path) { repository_path_match[:project_path] }
+ end
+
+ def repository_path_match
+ strong_memoize(:repository_path_match) { repository_path.match(Gitlab::PathRegex.full_project_git_path_regex) || {} }
end
def ensure_project_on_push!
@@ -44,7 +56,7 @@ module Gitlab
return unless user&.can?(:create_projects, namespace)
project_params = {
- path: repository_path,
+ path: project_path,
namespace_id: namespace.id,
visibility_level: Gitlab::VisibilityLevel::PRIVATE
}
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index 710e2ce90ec..854bf6e9c9e 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -114,7 +114,7 @@ module Gitlab
override :check_single_change_access
def check_single_change_access(change, _skip_lfs_integrity_check: false)
- Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate!
+ Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, root_ref: snippet.repository.root_ref, logger: logger).validate!
Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit, logger: logger).validate!
rescue Checks::TimedLogger::TimeoutError
raise TimeoutError, logger.full_message
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 464d2519b27..599bce176c9 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -216,6 +216,23 @@ module Gitlab
response.flat_map(&:stats)
end
+ def find_changed_paths(commits)
+ request = Gitaly::FindChangedPathsRequest.new(
+ repository: @gitaly_repo,
+ commits: commits
+ )
+
+ response = GitalyClient.call(@repository.storage, :diff_service, :find_changed_paths, request, timeout: GitalyClient.medium_timeout)
+ response.flat_map do |msg|
+ msg.paths.map do |path|
+ OpenStruct.new(
+ status: path.status,
+ path: EncodingHelper.encode!(path.path)
+ )
+ end
+ end
+ end
+
def find_all_commits(opts = {})
request = Gitaly::FindAllCommitsRequest.new(
repository: @gitaly_repo,
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index dfe60fb5a03..328f1f742c5 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -69,6 +69,10 @@ module Gitlab
with_rate_limit { octokit.user(username) }
end
+ def pull_request_reviews(repo_name, iid)
+ with_rate_limit { octokit.pull_request_reviews(repo_name, iid) }
+ end
+
# Returns the details of a GitHub repository.
#
# name - The path (in the form `owner/repository`) of the repository.
@@ -76,6 +80,10 @@ module Gitlab
with_rate_limit { octokit.repo(name) }
end
+ def pull_request(repo_name, iid)
+ with_rate_limit { octokit.pull_request(repo_name, iid) }
+ end
+
def labels(*args)
each_object(:labels, *args)
end
@@ -155,8 +163,8 @@ module Gitlab
end
end
- def search_repos_by_name(name)
- each_page(:search_repositories, search_query(str: name, type: :name))
+ def search_repos_by_name(name, options = {})
+ octokit.search_repositories(search_query(str: name, type: :name), options)
end
def search_query(str:, type:, include_collaborations: true, include_orgs: true)
diff --git a/lib/gitlab/github_import/importer/lfs_objects_importer.rb b/lib/gitlab/github_import/importer/lfs_objects_importer.rb
index 5980b3c2179..c74a7706117 100644
--- a/lib/gitlab/github_import/importer/lfs_objects_importer.rb
+++ b/lib/gitlab/github_import/importer/lfs_objects_importer.rb
@@ -23,16 +23,13 @@ module Gitlab
end
def each_object_to_import
- lfs_objects = Projects::LfsPointers::LfsImportService.new(project).execute
+ lfs_objects = Projects::LfsPointers::LfsObjectDownloadListService.new(project).execute
lfs_objects.each do |object|
yield object
end
rescue StandardError => e
- Gitlab::Import::Logger.error(
- message: 'The Lfs import process failed',
- error: e.message
- )
+ error(project.id, e)
end
end
end
diff --git a/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb b/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb
new file mode 100644
index 00000000000..11181edf0e9
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestMergedByImporter
+ def initialize(pull_request, project, client)
+ @project = project
+ @pull_request = pull_request
+ @client = client
+ end
+
+ def execute
+ merge_request = project.merge_requests.find_by_iid(pull_request.iid)
+ user_finder = GithubImport::UserFinder.new(project, client)
+ gitlab_user_id = user_finder.user_id_for(pull_request.merged_by)
+
+ if gitlab_user_id
+ timestamp = Time.new.utc
+ MergeRequest::Metrics.upsert({
+ target_project_id: project.id,
+ merge_request_id: merge_request.id,
+ merged_by_id: gitlab_user_id,
+ created_at: timestamp,
+ updated_at: timestamp
+ }, unique_by: :merge_request_id)
+ else
+ merge_request.notes.create!(
+ importing: true,
+ note: "*Merged by: #{pull_request.merged_by.login}*",
+ author_id: project.creator_id,
+ project: project,
+ created_at: pull_request.created_at
+ )
+ end
+ end
+
+ private
+
+ attr_reader :project, :pull_request, :client
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/pull_request_review_importer.rb b/lib/gitlab/github_import/importer/pull_request_review_importer.rb
new file mode 100644
index 00000000000..14ee69ba089
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_request_review_importer.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestReviewImporter
+ def initialize(review, project, client)
+ @review = review
+ @project = project
+ @client = client
+ @merge_request = project.merge_requests.find_by_id(review.merge_request_id)
+ end
+
+ def execute
+ user_finder = GithubImport::UserFinder.new(project, client)
+ gitlab_user_id = user_finder.user_id_for(review.author)
+
+ if gitlab_user_id
+ add_review_note!(gitlab_user_id)
+ add_approval!(gitlab_user_id)
+ else
+ add_complementary_review_note!(project.creator_id)
+ end
+ end
+
+ private
+
+ attr_reader :review, :merge_request, :project, :client
+
+ def add_review_note!(author_id)
+ return if review.note.empty?
+
+ add_note!(author_id, review_note_content)
+ end
+
+ def add_complementary_review_note!(author_id)
+ return if review.note.empty? && !review.approval?
+
+ note = "*Created by %{login}*\n\n%{note}" % {
+ note: review_note_content,
+ login: review.author.login
+ }
+
+ add_note!(author_id, note)
+ end
+
+ def review_note_content
+ header = "**Review:** #{review.review_type.humanize}"
+
+ if review.note.present?
+ "#{header}\n\n#{review.note}"
+ else
+ header
+ end
+ end
+
+ def add_note!(author_id, note)
+ note = Note.new(note_attributes(author_id, note))
+
+ note.save!
+ end
+
+ def note_attributes(author_id, note, extra = {})
+ {
+ importing: true,
+ noteable_id: merge_request.id,
+ noteable_type: 'MergeRequest',
+ project_id: project.id,
+ author_id: author_id,
+ note: note,
+ system: false,
+ created_at: review.submitted_at,
+ updated_at: review.submitted_at
+ }.merge(extra)
+ end
+
+ def add_approval!(user_id)
+ return unless review.review_type == 'APPROVED'
+
+ add_approval_system_note!(user_id)
+
+ merge_request.approvals.create!(
+ user_id: user_id,
+ created_at: review.submitted_at
+ )
+ end
+
+ def add_approval_system_note!(user_id)
+ attributes = note_attributes(
+ user_id,
+ 'approved this merge request',
+ system: true,
+ system_note_metadata: SystemNoteMetadata.new(action: 'approved')
+ )
+
+ Note.create!(attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb
index dcae8ca01fa..7f1569f592f 100644
--- a/lib/gitlab/github_import/importer/pull_requests_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def representation_class
- Representation::PullRequest
+ Gitlab::GithubImport::Representation::PullRequest
end
def sidekiq_worker_class
diff --git a/lib/gitlab/github_import/importer/pull_requests_merged_by_importer.rb b/lib/gitlab/github_import/importer/pull_requests_merged_by_importer.rb
new file mode 100644
index 00000000000..466288fde4c
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_requests_merged_by_importer.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestsMergedByImporter
+ include ParallelScheduling
+
+ def importer_class
+ PullRequestMergedByImporter
+ end
+
+ def representation_class
+ Gitlab::GithubImport::Representation::PullRequest
+ end
+
+ def sidekiq_worker_class
+ ImportPullRequestMergedByWorker
+ end
+
+ def collection_method
+ :pull_requests_merged_by
+ end
+
+ def id_for_already_imported_cache(pr)
+ pr.number
+ end
+
+ def each_object_to_import
+ project.merge_requests.with_state(:merged).find_each do |merge_request|
+ pull_request = client.pull_request(project.import_source, merge_request.iid)
+ yield(pull_request)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb b/lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb
new file mode 100644
index 00000000000..6d1b588f0e0
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestsReviewsImporter
+ include ParallelScheduling
+
+ def importer_class
+ PullRequestReviewImporter
+ end
+
+ def representation_class
+ Gitlab::GithubImport::Representation::PullRequestReview
+ end
+
+ def sidekiq_worker_class
+ ImportPullRequestReviewWorker
+ end
+
+ def collection_method
+ :pull_request_reviews
+ end
+
+ def id_for_already_imported_cache(review)
+ review.github_id
+ end
+
+ def each_object_to_import
+ project.merge_requests.find_each do |merge_request|
+ reviews = client.pull_request_reviews(project.import_source, merge_request.iid)
+ reviews.each do |review|
+ review.merge_request_id = merge_request.id
+ yield(review)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
index cabc615ea11..51859010ec3 100644
--- a/lib/gitlab/github_import/parallel_scheduling.rb
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -26,6 +26,8 @@ module Gitlab
end
def execute
+ info(project.id, message: "starting importer")
+
retval =
if parallel?
parallel_import
@@ -43,8 +45,13 @@ module Gitlab
# completed those jobs will just cycle through any remaining pages while
# not scheduling anything.
Gitlab::Cache::Import::Caching.expire(already_imported_cache_key, 15.minutes.to_i)
+ info(project.id, message: "importer finished")
retval
+ rescue => e
+ error(project.id, e)
+
+ raise e
end
# Imports all the objects in sequence in the current thread.
@@ -157,6 +164,40 @@ module Gitlab
def collection_options
{}
end
+
+ private
+
+ def info(project_id, extra = {})
+ logger.info(log_attributes(project_id, extra))
+ end
+
+ def error(project_id, exception)
+ logger.error(
+ log_attributes(
+ project_id,
+ message: 'importer failed',
+ 'error.message': exception.message
+ )
+ )
+
+ Gitlab::ErrorTracking.track_exception(
+ exception,
+ log_attributes(project_id)
+ )
+ end
+
+ def log_attributes(project_id, extra = {})
+ extra.merge(
+ import_source: :github,
+ project_id: project_id,
+ importer: importer_class.name,
+ parallel: parallel?
+ )
+ end
+
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
end
end
end
diff --git a/lib/gitlab/github_import/representation/pull_request.rb b/lib/gitlab/github_import/representation/pull_request.rb
index 0ccc4bfaed3..be192762e05 100644
--- a/lib/gitlab/github_import/representation/pull_request.rb
+++ b/lib/gitlab/github_import/representation/pull_request.rb
@@ -13,18 +13,16 @@ module Gitlab
:source_branch_sha, :target_branch, :target_branch_sha,
:milestone_number, :author, :assignee, :created_at,
:updated_at, :merged_at, :source_repository_id,
- :target_repository_id, :source_repository_owner
+ :target_repository_id, :source_repository_owner, :merged_by
# Builds a PR from a GitHub API response.
#
# issue - An instance of `Sawyer::Resource` containing the PR details.
def self.from_api_response(pr)
- assignee =
- if pr.assignee
- Representation::User.from_api_response(pr.assignee)
- end
-
+ assignee = Representation::User.from_api_response(pr.assignee) if pr.assignee
user = Representation::User.from_api_response(pr.user) if pr.user
+ merged_by = Representation::User.from_api_response(pr.merged_by) if pr.merged_by
+
hash = {
iid: pr.number,
title: pr.title,
@@ -42,7 +40,8 @@ module Gitlab
assignee: assignee,
created_at: pr.created_at,
updated_at: pr.updated_at,
- merged_at: pr.merged_at
+ merged_at: pr.merged_at,
+ merged_by: merged_by
}
new(hash)
@@ -57,8 +56,8 @@ module Gitlab
# Assignees are optional so we only convert it from a Hash if one was
# set.
- hash[:assignee] &&= Representation::User
- .from_json_hash(hash[:assignee])
+ hash[:assignee] &&= Representation::User.from_json_hash(hash[:assignee])
+ hash[:merged_by] &&= Representation::User.from_json_hash(hash[:merged_by])
new(hash)
end
diff --git a/lib/gitlab/github_import/representation/pull_request_review.rb b/lib/gitlab/github_import/representation/pull_request_review.rb
new file mode 100644
index 00000000000..3205259a1ed
--- /dev/null
+++ b/lib/gitlab/github_import/representation/pull_request_review.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class PullRequestReview
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :author, :note, :review_type, :submitted_at, :github_id, :merge_request_id
+
+ def self.from_api_response(review)
+ user = Representation::User.from_api_response(review.user) if review.user
+
+ new(
+ merge_request_id: review.merge_request_id,
+ author: user,
+ note: review.body,
+ review_type: review.state,
+ submitted_at: review.submitted_at,
+ github_id: review.id
+ )
+ end
+
+ # Builds a new note using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+ hash[:submitted_at] = Time.parse(hash[:submitted_at]).in_time_zone
+
+ new(hash)
+ end
+
+ # attributes - A Hash containing the raw note details. The keys of this
+ # Hash must be Symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def approval?
+ review_type == 'APPROVED'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index 352a93817be..d123989ef8e 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -20,6 +20,7 @@ module Gitlab
end,
container_class: ProjectWiki,
project_resolver: -> (wiki) { wiki.try(:project) },
+ guest_read_ability: :download_wiki_code,
suffix: :wiki
).freeze
SNIPPET = RepoType.new(
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2d41ad76618..362da8ea53e 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -61,15 +61,15 @@ module Gitlab
def push_frontend_feature_flag(name, *args, **kwargs)
enabled = Feature.enabled?(name, *args, **kwargs)
- push_to_gon_features(name, enabled)
+ push_to_gon_attributes(:features, name, enabled)
end
- def push_to_gon_features(name, enabled)
+ def push_to_gon_attributes(key, name, enabled)
var_name = name.to_s.camelize(:lower)
# Here the `true` argument signals gon that the value should be merged
# into any existing ones, instead of overwriting them. This allows you to
# use this method to push multiple feature flags.
- gon.push({ features: { var_name => enabled } }, true)
+ gon.push({ key => { var_name => enabled } }, true)
end
def default_avatar_url
@@ -83,3 +83,5 @@ module Gitlab
end
end
end
+
+Gitlab::GonHelper.prepend_if_ee('EE::Gitlab::GonHelper')
diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb
deleted file mode 100644
index 52d714880b5..00000000000
--- a/lib/gitlab/google_code_import/client.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module GoogleCodeImport
- class Client
- attr_reader :raw_data
-
- def self.mask_email(author)
- parts = author.split("@", 2)
- parts[0] = "#{parts[0][0...-3]}..."
- parts.join("@")
- end
-
- def initialize(raw_data)
- @raw_data = raw_data
- end
-
- def valid?
- raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.key?("projects")
- end
-
- def repos
- @repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.select(&:git?)
- end
-
- def incompatible_repos
- @incompatible_repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.reject(&:git?)
- end
-
- def repo(id)
- repos.find { |repo| repo.id == id }
- end
-
- def user_map
- user_map = Hash.new { |hash, user| hash[user] = self.class.mask_email(user) }
-
- repos.each do |repo|
- next unless repo.valid? && repo.issues
-
- repo.issues.each do |raw_issue|
- # Touching is enough to add the entry and masked email.
- user_map[raw_issue["author"]["name"]]
-
- raw_issue["comments"]["items"].each do |raw_comment|
- user_map[raw_comment["author"]["name"]]
- end
- end
- end
-
- Hash[user_map.sort]
- end
- end
- end
-end
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
deleted file mode 100644
index 4da2004b74f..00000000000
--- a/lib/gitlab/google_code_import/importer.rb
+++ /dev/null
@@ -1,373 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module GoogleCodeImport
- class Importer
- attr_reader :project, :repo, :closed_statuses
-
- NICE_LABEL_COLOR_HASH =
- {
- 'Status: New' => '#428bca',
- 'Status: Accepted' => '#5cb85c',
- 'Status: Started' => '#8e44ad',
- 'Priority: Critical' => '#ffcfcf',
- 'Priority: High' => '#deffcf',
- 'Priority: Medium' => '#fff5cc',
- 'Priority: Low' => '#cfe9ff',
- 'Type: Defect' => '#d9534f',
- 'Type: Enhancement' => '#44ad8e',
- 'Type: Task' => '#4b6dd0',
- 'Type: Review' => '#8e44ad',
- 'Type: Other' => '#7f8c8d'
- }.freeze
-
- def initialize(project)
- @project = project
-
- import_data = project.import_data.try(:data)
- repo_data = import_data["repo"] if import_data
- @repo = GoogleCodeImport::Repository.new(repo_data)
-
- @closed_statuses = []
- @known_labels = Set.new
- end
-
- def execute
- return true unless repo.valid?
-
- import_status_labels
-
- import_labels
-
- import_issues
-
- true
- end
-
- private
-
- def user_map
- @user_map ||= begin
- user_map = Hash.new do |hash, user|
- # Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked.
- Client.mask_email(user).sub("...", "\\.\\.\\.")
- end
-
- import_data = project.import_data.try(:data)
- stored_user_map = import_data["user_map"] if import_data
- user_map.update(stored_user_map) if stored_user_map
-
- user_map
- end
- end
-
- def import_status_labels
- repo.raw_data["issuesConfig"]["statuses"].each do |status|
- closed = !status["meansOpen"]
- @closed_statuses << status["status"] if closed
-
- name = nice_status_name(status["status"])
- create_label(name)
- @known_labels << name
- end
- end
-
- def import_labels
- repo.raw_data["issuesConfig"]["labels"].each do |label|
- name = nice_label_name(label["label"])
- create_label(name)
- @known_labels << name
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def import_issues
- return unless repo.issues
-
- while raw_issue = repo.issues.shift
- author = user_map[raw_issue["author"]["name"]]
- date = DateTime.parse(raw_issue["published"]).to_formatted_s(:long)
-
- comments = raw_issue["comments"]["items"]
- issue_comment = comments.shift
-
- content = format_content(issue_comment["content"])
- attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"])
-
- body = format_issue_body(author, date, content, attachments)
- labels = import_issue_labels(raw_issue)
-
- assignee_id = nil
- if raw_issue.key?("owner")
- username = user_map[raw_issue["owner"]["name"]]
-
- if username.start_with?("@")
- username = username[1..-1]
-
- if user = UserFinder.new(username).find_by_username
- assignee_id = user.id
- end
- end
- end
-
- issue = Issue.create!(
- iid: raw_issue['id'],
- project_id: project.id,
- title: raw_issue['title'],
- description: body,
- author_id: project.creator_id,
- assignee_ids: [assignee_id],
- state_id: raw_issue['state'] == 'closed' ? Issue.available_states[:closed] : Issue.available_states[:opened]
- )
-
- issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
- issue.update_attribute(:label_ids, issue_labels.pluck(:id))
-
- import_issue_comments(issue, comments)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def import_issue_labels(raw_issue)
- labels = []
-
- raw_issue["labels"].each do |label|
- name = nice_label_name(label)
- labels << name
-
- unless @known_labels.include?(name)
- create_label(name)
- @known_labels << name
- end
- end
-
- labels << nice_status_name(raw_issue["status"])
- labels
- end
-
- def import_issue_comments(issue, comments)
- Note.transaction do
- while raw_comment = comments.shift
- next if raw_comment.key?("deletedBy")
-
- content = format_content(raw_comment["content"])
- updates = format_updates(raw_comment["updates"])
- attachments = format_attachments(issue.iid, raw_comment["id"], raw_comment["attachments"])
-
- next if content.blank? && updates.blank? && attachments.blank?
-
- author = user_map[raw_comment["author"]["name"]]
- date = DateTime.parse(raw_comment["published"]).to_formatted_s(:long)
-
- body = format_issue_comment_body(
- raw_comment["id"],
- author,
- date,
- content,
- updates,
- attachments
- )
-
- # Needs to match order of `comment_columns` below.
- Note.create!(
- project_id: project.id,
- noteable_type: "Issue",
- noteable_id: issue.id,
- author_id: project.creator_id,
- note: body
- )
- end
- end
- end
-
- def nice_label_color(name)
- NICE_LABEL_COLOR_HASH[name] ||
- case name
- when /\AComponent:/
- '#fff39e'
- when /\AOpSys:/
- '#e2e2e2'
- when /\AMilestone:/
- '#fee3ff'
- when *closed_statuses.map { |s| nice_status_name(s) }
- '#cfcfcf'
- else
- '#e2e2e2'
- end
- end
-
- def nice_label_name(name)
- name.sub("-", ": ")
- end
-
- def nice_status_name(name)
- "Status: #{name}"
- end
-
- def linkify_issues(str)
- str = str.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
- str = str.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
- str
- end
-
- def escape_for_markdown(str)
- # No headings and lists
- str = str.gsub(/^#/, "\\#")
- str = str.gsub(/^-/, "\\-")
-
- # No inline code
- str = str.gsub("`", "\\`")
-
- # Carriage returns make me sad
- str = str.delete("\r")
-
- # Markdown ignores single newlines, but we need them as <br />.
- str = str.gsub("\n", " \n")
-
- str
- end
-
- def create_label(name)
- params = { name: name, color: nice_label_color(name) }
- ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
- end
-
- def format_content(raw_content)
- linkify_issues(escape_for_markdown(raw_content))
- end
-
- def format_updates(raw_updates)
- updates = []
-
- if raw_updates.key?("status")
- updates << "*Status: #{raw_updates["status"]}*"
- end
-
- if raw_updates.key?("owner")
- updates << "*Owner: #{user_map[raw_updates["owner"]]}*"
- end
-
- if raw_updates.key?("cc")
- cc = raw_updates["cc"].map do |l|
- deleted = l.start_with?("-")
- l = l[1..-1] if deleted
- l = user_map[l]
- l = "~~#{l}~~" if deleted
- l
- end
-
- updates << "*Cc: #{cc.join(", ")}*"
- end
-
- if raw_updates.key?("labels")
- labels = raw_updates["labels"].map do |l|
- deleted = l.start_with?("-")
- l = l[1..-1] if deleted
- l = nice_label_name(l)
- l = "~~#{l}~~" if deleted
- l
- end
-
- updates << "*Labels: #{labels.join(", ")}*"
- end
-
- if raw_updates.key?("mergedInto")
- updates << "*Merged into: ##{raw_updates["mergedInto"]}*"
- end
-
- if raw_updates.key?("blockedOn")
- blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on|
- format_blocking_updates(raw_blocked_on)
- end
-
- updates << "*Blocked on: #{blocked_ons.join(", ")}*"
- end
-
- if raw_updates.key?("blocking")
- blockings = raw_updates["blocking"].map do |raw_blocked_on|
- format_blocking_updates(raw_blocked_on)
- end
-
- updates << "*Blocking: #{blockings.join(", ")}*"
- end
-
- updates
- end
-
- def format_blocking_updates(raw_blocked_on)
- name, id = raw_blocked_on.split(":", 2)
-
- deleted = name.start_with?("-")
- name = name[1..-1] if deleted
-
- text =
- if name == project.import_source
- "##{id}"
- else
- "#{project.namespace.full_path}/#{name}##{id}"
- end
-
- text = "~~#{text}~~" if deleted
- text
- end
-
- def format_attachments(issue_id, comment_id, raw_attachments)
- return [] unless raw_attachments
-
- raw_attachments.map do |attachment|
- next if attachment["isDeleted"]
-
- filename = attachment["fileName"]
- link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}"
-
- text = "[#{filename}](#{link})"
- text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i
- text
- end.compact
- end
-
- def format_issue_comment_body(id, author, date, content, updates, attachments)
- body = []
- body << "*Comment #{id} by #{author} on #{date}*"
- body << "---"
-
- if content.blank?
- content = "*(No comment has been entered for this change)*"
- end
-
- body << content
-
- if updates.any?
- body << "---"
- body += updates
- end
-
- if attachments.any?
- body << "---"
- body += attachments
- end
-
- body.join("\n\n")
- end
-
- def format_issue_body(author, date, content, attachments)
- body = []
- body << "*By #{author} on #{date} (imported from Google Code)*"
- body << "---"
-
- if content.blank?
- content = "*(No description has been entered for this issue)*"
- end
-
- body << content
-
- if attachments.any?
- body << "---"
- body += attachments
- end
-
- body.join("\n\n")
- end
- end
- end
-end
diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb
deleted file mode 100644
index eaef85acb98..00000000000
--- a/lib/gitlab/google_code_import/project_creator.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module GoogleCodeImport
- class ProjectCreator
- attr_reader :repo, :namespace, :current_user, :user_map
-
- def initialize(repo, namespace, current_user, user_map = nil)
- @repo = repo
- @namespace = namespace
- @current_user = current_user
- @user_map = user_map
- end
-
- def execute
- ::Projects::CreateService.new(
- current_user,
- name: repo.name,
- path: repo.name,
- description: repo.summary,
- namespace: namespace,
- creator: current_user,
- visibility_level: Gitlab::VisibilityLevel::PUBLIC,
- import_type: "google_code",
- import_source: repo.name,
- import_url: repo.import_url,
- import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } }
- ).execute
- end
- end
- end
-end
diff --git a/lib/gitlab/google_code_import/repository.rb b/lib/gitlab/google_code_import/repository.rb
deleted file mode 100644
index 19627c8cd35..00000000000
--- a/lib/gitlab/google_code_import/repository.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module GoogleCodeImport
- class Repository
- attr_accessor :raw_data
-
- def initialize(raw_data)
- @raw_data = raw_data
- end
-
- def valid?
- raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#project"
- end
-
- def id
- raw_data["externalId"]
- end
-
- def name
- raw_data["name"]
- end
-
- def summary
- raw_data["summary"]
- end
-
- def description
- raw_data["description"]
- end
-
- def git?
- raw_data["versionControlSystem"] == "git"
- end
-
- def import_url
- raw_data["repositoryUrls"].first
- end
-
- def issues
- raw_data["issues"] && raw_data["issues"]["items"]
- end
- end
- end
-end
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 8166bef4510..b1494cf8cf2 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -142,13 +142,11 @@ module Gitlab
end
def tmp_keychains_created
- @tmp_keychains_created ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total,
- 'The number of temporary GPG keychains created')
+ Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total, 'The number of temporary GPG keychains created')
end
def tmp_keychains_removed
- @tmp_keychains_removed ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total,
- 'The number of temporary GPG keychains removed')
+ Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total, 'The number of temporary GPG keychains removed')
end
end
end
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index c70127553fd..6ee446011d4 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -62,8 +62,8 @@ module Gitlab
end
end
- def raise_resource_not_available_error!
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, RESOURCE_ACCESS_ERROR
+ def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR)
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, msg
end
end
end
diff --git a/lib/gitlab/graphql/connection_collection_methods.rb b/lib/gitlab/graphql/connection_collection_methods.rb
new file mode 100644
index 00000000000..0e2c4a98bb6
--- /dev/null
+++ b/lib/gitlab/graphql/connection_collection_methods.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module ConnectionCollectionMethods
+ extend ActiveSupport::Concern
+
+ included do
+ delegate :to_a, :size, :include?, :empty?, to: :nodes
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connection_redaction.rb b/lib/gitlab/graphql/connection_redaction.rb
new file mode 100644
index 00000000000..5e037bb9f63
--- /dev/null
+++ b/lib/gitlab/graphql/connection_redaction.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module ConnectionRedaction
+ class RedactionState
+ attr_reader :redactor
+ attr_reader :redacted_nodes
+
+ def redactor=(redactor)
+ @redactor = redactor
+ @redacted_nodes = nil
+ end
+
+ def redacted(&block)
+ @redacted_nodes ||= redactor.present? ? redactor.redact(yield) : yield
+ end
+ end
+
+ delegate :redactor=, to: :redaction_state
+
+ def nodes
+ redaction_state.redacted { super.to_a }
+ end
+
+ private
+
+ def redaction_state
+ @redaction_state ||= RedactionState.new
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/deferred.rb b/lib/gitlab/graphql/deferred.rb
new file mode 100644
index 00000000000..d0b36aabd5f
--- /dev/null
+++ b/lib/gitlab/graphql/deferred.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# A marker interface that allows use to lazily resolve a wider range of value
+module Gitlab
+ module Graphql
+ module Deferred
+ def execute
+ raise NotImplementedError, 'Deferred classes must provide an execute method'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb
index 503b1064b11..ad9e08e189c 100644
--- a/lib/gitlab/graphql/docs/helper.rb
+++ b/lib/gitlab/graphql/docs/helper.rb
@@ -13,6 +13,12 @@ module Gitlab
def auto_generated_comment
<<-MD.strip_heredoc
+ ---
+ stage: Plan
+ group: Project Management
+ info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+ ---
+
<!---
This documentation is auto generated by a script.
diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml
index 97df4233905..8f5a1788fa5 100644
--- a/lib/gitlab/graphql/docs/templates/default.md.haml
+++ b/lib/gitlab/graphql/docs/templates/default.md.haml
@@ -12,7 +12,7 @@
Each table below documents a GraphQL type. Types match loosely to models, but not all
fields and methods on a model are available via GraphQL.
- CAUTION: **Caution:**
+ WARNING:
Fields that are deprecated are marked with **{warning-solid}**.
Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-process) can be found
in [Removed Items](../removed_items.md).
@@ -21,7 +21,7 @@
:plain
## Object types
- Object types represent the resources that GitLab's GraphQL API can return.
+ Object types represent the resources that the GitLab GraphQL API can return.
They contain _fields_. Each field has its own type, which will either be one of the
basic GraphQL [scalar types](https://graphql.org/learn/schema/#scalar-types)
(e.g.: `String` or `Boolean`) or other object types.
diff --git a/lib/gitlab/graphql/expose_permissions.rb b/lib/gitlab/graphql/expose_permissions.rb
index 365b7cca24f..ab9ed354673 100644
--- a/lib/gitlab/graphql/expose_permissions.rb
+++ b/lib/gitlab/graphql/expose_permissions.rb
@@ -9,7 +9,7 @@ module Gitlab
field :user_permissions, permission_type,
description: description,
null: false,
- resolve: -> (obj, _, _) { obj }
+ method: :itself
end
end
end
diff --git a/lib/gitlab/graphql/externally_paginated_array.rb b/lib/gitlab/graphql/externally_paginated_array.rb
index 4797fe15cd3..873d7f4efdf 100644
--- a/lib/gitlab/graphql/externally_paginated_array.rb
+++ b/lib/gitlab/graphql/externally_paginated_array.rb
@@ -3,12 +3,12 @@
module Gitlab
module Graphql
class ExternallyPaginatedArray < Array
- attr_reader :previous_cursor, :next_cursor
+ attr_reader :start_cursor, :end_cursor
def initialize(previous_cursor, next_cursor, *args)
super(args)
- @previous_cursor = previous_cursor
- @next_cursor = next_cursor
+ @start_cursor = previous_cursor
+ @end_cursor = next_cursor
end
end
end
diff --git a/lib/gitlab/graphql/laziness.rb b/lib/gitlab/graphql/laziness.rb
new file mode 100644
index 00000000000..749d832919d
--- /dev/null
+++ b/lib/gitlab/graphql/laziness.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ # This module allows your class to easily defer and force values.
+ # Its methods are just sugar for calls to the Gitlab::Graphql::Lazy class.
+ #
+ # example:
+ #
+ # class MyAwesomeClass
+ # include ::Gitlab::Graphql::Laziness
+ #
+ # # takes a list of id and list of factors, and computes
+ # # sum of [SomeObject[i]#value * factor[i]]
+ # def resolve(ids:, factors:)
+ # ids.zip(factors)
+ # .map { |id, factor| promise_an_int(id, factor) }
+ # .map(&method(:force))
+ # .sum
+ # end
+ #
+ # # returns a promise for an Integer
+ # def (id, factor)
+ # thunk = SomeObject.lazy_find(id)
+ # defer { force(thunk).value * factor }
+ # end
+ # end
+ #
+ # In the example above, we use defer to delay forcing the batch-loaded
+ # item until we need it, and then we use `force` to consume the lazy values
+ #
+ # If `SomeObject.lazy_find(id)` batches correctly, calling
+ # `resolve` will only perform one batched load for all objects, rather than
+ # loading them individually before combining the results.
+ #
+ module Laziness
+ def defer(&block)
+ ::Gitlab::Graphql::Lazy.new(&block)
+ end
+
+ def force(lazy)
+ ::Gitlab::Graphql::Lazy.force(lazy)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/lazy.rb b/lib/gitlab/graphql/lazy.rb
index 3cc11047387..54013cf4790 100644
--- a/lib/gitlab/graphql/lazy.rb
+++ b/lib/gitlab/graphql/lazy.rb
@@ -24,6 +24,8 @@ module Gitlab
value.force
when ::BatchLoader::GraphQL
value.sync
+ when ::Gitlab::Graphql::Deferred
+ value.execute
when ::GraphQL::Execution::Lazy
value.value # part of the private api, but we can force this as well
when ::Concurrent::Promise
diff --git a/lib/gitlab/graphql/pagination/array_connection.rb b/lib/gitlab/graphql/pagination/array_connection.rb
new file mode 100644
index 00000000000..efc912eaeca
--- /dev/null
+++ b/lib/gitlab/graphql/pagination/array_connection.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# We use the Keyset / Stable cursor connection by default for ActiveRecord::Relation.
+# However, there are times when that may not be powerful enough (yet), and we
+# want to use standard offset pagination.
+module Gitlab
+ module Graphql
+ module Pagination
+ class ArrayConnection < ::GraphQL::Pagination::ArrayConnection
+ prepend ::Gitlab::Graphql::ConnectionRedaction
+ include ::Gitlab::Graphql::ConnectionCollectionMethods
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/pagination/connections.rb b/lib/gitlab/graphql/pagination/connections.rb
index 8f37fa3f474..54a84be4274 100644
--- a/lib/gitlab/graphql/pagination/connections.rb
+++ b/lib/gitlab/graphql/pagination/connections.rb
@@ -12,6 +12,10 @@ module Gitlab
schema.connections.add(
Gitlab::Graphql::ExternallyPaginatedArray,
Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection)
+
+ schema.connections.add(
+ Array,
+ Gitlab::Graphql::Pagination::ArrayConnection)
end
end
end
diff --git a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb
index 12e047420bf..ce309df65d9 100644
--- a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb
+++ b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb
@@ -5,13 +5,10 @@ module Gitlab
module Graphql
module Pagination
class ExternallyPaginatedArrayConnection < GraphQL::Pagination::ArrayConnection
- def start_cursor
- items.previous_cursor
- end
+ include ::Gitlab::Graphql::ConnectionCollectionMethods
+ prepend ::Gitlab::Graphql::ConnectionRedaction
- def end_cursor
- items.next_cursor
- end
+ delegate :start_cursor, :end_cursor, to: :items
def next_page?
end_cursor.present?
diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb
index 252f6371765..2ad8d2f7ab7 100644
--- a/lib/gitlab/graphql/pagination/keyset/connection.rb
+++ b/lib/gitlab/graphql/pagination/keyset/connection.rb
@@ -31,6 +31,8 @@ module Gitlab
module Keyset
class Connection < GraphQL::Pagination::ActiveRecordRelationConnection
include Gitlab::Utils::StrongMemoize
+ include ::Gitlab::Graphql::ConnectionCollectionMethods
+ prepend ::Gitlab::Graphql::ConnectionRedaction
# rubocop: disable Naming/PredicateName
# https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields
diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb
index f3ce3a10703..d37264c1343 100644
--- a/lib/gitlab/graphql/pagination/keyset/order_info.rb
+++ b/lib/gitlab/graphql/pagination/keyset/order_info.rb
@@ -127,3 +127,5 @@ module Gitlab
end
end
end
+
+Gitlab::Graphql::Pagination::Keyset::OrderInfo.prepend_if_ee('EE::Gitlab::Graphql::Pagination::Keyset::OrderInfo')
diff --git a/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb b/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb
index 33f84701562..4a57b7aceca 100644
--- a/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb
+++ b/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb
@@ -7,6 +7,8 @@ module Gitlab
module Graphql
module Pagination
class OffsetActiveRecordRelationConnection < GraphQL::Pagination::ActiveRecordRelationConnection
+ prepend ::Gitlab::Graphql::ConnectionRedaction
+ include ::Gitlab::Graphql::ConnectionCollectionMethods
end
end
end
diff --git a/lib/gitlab/hook_data/base_builder.rb b/lib/gitlab/hook_data/base_builder.rb
index d54175bce81..434d30d9717 100644
--- a/lib/gitlab/hook_data/base_builder.rb
+++ b/lib/gitlab/hook_data/base_builder.rb
@@ -21,6 +21,13 @@ module Gitlab
private
+ def timestamps_data
+ {
+ created_at: object.created_at&.xmlschema,
+ updated_at: object.updated_at&.xmlschema
+ }
+ end
+
def absolute_image_urls(markdown_text)
return markdown_text unless markdown_text.present?
diff --git a/lib/gitlab/hook_data/group_member_builder.rb b/lib/gitlab/hook_data/group_member_builder.rb
new file mode 100644
index 00000000000..32cfd032ffe
--- /dev/null
+++ b/lib/gitlab/hook_data/group_member_builder.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HookData
+ class GroupMemberBuilder < BaseBuilder
+ alias_method :group_member, :object
+
+ # Sample data
+
+ # {
+ # :event_name=>"user_add_to_group",
+ # :group_name=>"GitLab group",
+ # :group_path=>"gitlab",
+ # :group_id=>1,
+ # :user_username=>"robert",
+ # :user_name=>"Robert Mills",
+ # :user_email=>"robert@example.com",
+ # :user_id=>14,
+ # :group_access=>"Guest",
+ # :created_at=>"2020-11-04T10:12:10Z",
+ # :updated_at=>"2020-11-04T10:12:10Z",
+ # :expires_at=>"2020-12-04T10:12:10Z"
+ # }
+
+ def build(event)
+ [
+ timestamps_data,
+ group_member_data,
+ event_data(event)
+ ].reduce(:merge)
+ end
+
+ private
+
+ def group_member_data
+ {
+ group_name: group_member.group.name,
+ group_path: group_member.group.path,
+ group_id: group_member.group.id,
+ user_username: group_member.user.username,
+ user_name: group_member.user.name,
+ user_email: group_member.user.email,
+ user_id: group_member.user.id,
+ group_access: group_member.human_access,
+ expires_at: group_member.expires_at&.xmlschema
+ }
+ end
+
+ def event_data(event)
+ event_name = case event
+ when :create
+ 'user_add_to_group'
+ when :destroy
+ 'user_remove_from_group'
+ when :update
+ 'user_update_for_group'
+ end
+
+ { event_name: event_name }
+ end
+ end
+ end
+end
+
+Gitlab::HookData::GroupMemberBuilder.prepend_if_ee('EE::Gitlab::HookData::GroupMemberBuilder')
diff --git a/lib/gitlab/i18n/html_todo.yml b/lib/gitlab/i18n/html_todo.yml
deleted file mode 100644
index 91e01f8a0b8..00000000000
--- a/lib/gitlab/i18n/html_todo.yml
+++ /dev/null
@@ -1,314 +0,0 @@
-#
-# PLEASE DO NOT ADD NEW STRINGS TO THIS FILE.
-#
-# See https://docs.gitlab.com/ee/development/i18n/externalization.html#html
-# for information on how to handle HTML in translations.
-
-#
-# This file contains strings that need to be fixed to use the
-# updated HTML guidelines. Any strings in this file will no
-# longer be able to be translated until they have been updated.
-#
-# This file (and the functionality around it) will be removed
-# once https://gitlab.com/gitlab-org/gitlab/-/issues/217933 is complete.
-#
-# See https://gitlab.com/gitlab-org/gitlab/-/issues/19485 for more details
-# why this change has been made.
-#
-
-#
-# Strings below are fixed in the source code but the translations are still present in CrowdIn so the
-# locale files will fail the linter. They can be deleted after next CrowdIn sync, likely in:
-# https://gitlab.com/gitlab-org/gitlab/-/issues/226008
-#
-
-"This commit was signed with an <strong>unverified</strong> signature.":
- plural_id:
- translations:
- - "このコミットは<strong>検証されていない</strong> 署名でサインされています。"
- - "Этот коммит был подписан <strong>непроверенной</strong> подписью."
- - "此提交使用 <strong>未经验证的</strong> 签名进行签名。"
- - "Цей коміт підписано <strong>неперевіреним</strong> підписом."
- - "Esta commit fue firmado con una firma <strong>no verificada</strong>."
-"This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user.":
- plural_id:
- translations:
- - "このコミットは <strong>検証済み</strong> の署名でサインされており、このコミッターのメールは同じユーザーのものであることが検証されています。"
- - "Это коммит был подписан <strong>верифицированной</strong> подписью и коммитер подтвердил, что адрес почты принадлежит ему."
- - "此提交使用 <strong>已验证</strong> 的签名进行签名,并且已验证提交者的电子邮件属于同一用户。"
- - "Цей коміт підписано <strong>перевіреним</strong> підписом і адреса електронної пошти комітера гарантовано належить тому самому користувачу."
- - "Este commit fue firmado con una firma verificada, y <strong>se ha verificado</strong> que la dirección de correo electrónico del committer y la firma pertenecen al mismo usuario."
-"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":
- plural_id:
- translations:
- - "分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部署, 請選擇合適的 GitLab CI Yaml 模板併提交更改。%{link_to_autodeploy_doc}"
- - "O branch <strong>%{branch_name}</strong> foi criado. Para configurar o deploy automático, selecione um modelo de Yaml do GitLab CI e commit suas mudanças. %{link_to_autodeploy_doc}"
- - "<strong>%{branch_name}</strong> ブランチが作成されました。自動デプロイを設定するには、GitLab CI Yaml テンプレートを選択して、変更をコミットしてください。 %{link_to_autodeploy_doc}"
- - "La branch <strong>%{branch_name}</strong> è stata creata. Per impostare un rilascio automatico scegli un template CI di Gitlab e committa le tue modifiche %{link_to_autodeploy_doc}"
- - "O ramo <strong>%{branch_name}</strong> foi criado. Para configurar a implantação automática, seleciona um modelo de Yaml do GitLab CI e envia as tuas alterações. %{link_to_autodeploy_doc}"
- - "Ветка <strong>%{branch_name}</strong> создана. Для настройки автоматического развертывания выберите YAML-шаблон для GitLab CI и зафиксируйте свои изменения. %{link_to_autodeploy_doc}"
- - "已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择合适的 GitLab CI Yaml 模板并提交更改。%{link_to_autodeploy_doc}"
- - "Гілка <strong>%{branch_name}</strong> створена. Для настройки автоматичного розгортання виберіть GitLab CI Yaml-шаблон і закомітьте зміни. %{link_to_autodeploy_doc}"
- - "Клонът <strong>%{branch_name}</strong> беше създаден. За да настроите автоматичното внедряване, изберете Yaml шаблон за GitLab CI и подайте промените си. %{link_to_autodeploy_doc}"
- - "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatische Bereitstellung einzurichten, wähle eine GitLab CI Yaml Vorlage und committe deine Änderungen. %{link_to_autodeploy_doc}"
- - "<strong>%{branch_name}</strong> 브랜치가 생성되었습니다. 자동 배포를 설정하려면 GitLab CI Yaml 템플릿을 선택하고 변경 사항을 적용하십시오. %{link_to_autodeploy_doc}"
- - "La branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aŭtomatan disponigadon, bonvolu elekti Yaml-ŝablonon por GitLab CI kaj enmeti viajn ŝanĝojn. %{link_to_autodeploy_doc}"
- - "La branche <strong>%{branch_name}</strong> a été créée. Pour mettre en place le déploiement automatisé, sélectionnez un modèle de fichier YAML pour l’intégration continue (CI) de GitLab, et validez les modifications. %{link_to_autodeploy_doc}"
- - "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"
-"GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project's %{strong_start}Settings > General > Visibility%{strong_end} page.":
- plural_id:
- translations:
- - "GitLab Pagesはこのプロジェクトでは無効になっています。 プロジェクトの%{strong_start} 設定> 全般> 可視性%{strong_end}ページで有効にできます。"
- - "GitLab Pages отключены для этого проекта. Вы можете включить в поле %{strong_start}Настройки > Общие > Видимость%{strong_end} вашего проекта."
- - "此项目禁用GitLab Pages。您可以在您的项目的%{strong_start}设置 > 常规 > 可见性%{strong_end} 页面启用。"
- - "GitLab Pages вимкнено для цього проєкту. Ви можете їх увімкнути перейшовши на сторінку проєкту %{strong_start}Налаштування > Загальні > Видимість%{strong_end}."
- - "Las páginas de GitLab están deshabilitadas para este proyecto. Puede habilitarlas en los ajustes %{strong_start} de su proyecto > General > Visibilidad%{strong_end}."
-"You can invite a new member to <strong>%{project_name}</strong> or invite another group.":
- plural_id:
- translations:
- - "新しいメンバーを<strong>%{project_name} </strong>に招待するか、別のグループを招待することができます。"
- - "Podes convidar um novo para <strong>%{project_name}</strong> ou convidar outro grupo."
- - "邀请新成员或另一个群组加入<strong>%{project_name}</strong>。"
- - "Puede invitar a un nuevo miembro a <strong>%{project_name}</strong> o invitar a otro grupo."
- - "<strong>%{project_name}</strong> projesine yeni bir üye davet edebilir veya başka bir grubu davet edebilirsiniz."
- - "Вы можете пригласить нового участника в <strong>%{project_name}</strong> или пригласить другую группу."
- - "Ви можете запросити нового учасника до <strong>%{project_name}</strong> або запросити іншу групу."
-"You can invite a new member to <strong>%{project_name}</strong>.":
- plural_id:
- translations:
- - "新しいメンバーを<strong>%{project_name} </strong>に招待できます。"
- - "Podes convidar um novo membro para <strong>%{project_name}</strong>."
- - "邀请新成员加入<strong>%{project_name}</strong>。"
- - "Puedes invitar a un nuevo miembro a <strong>%{project_name}</strong>."
- - "<strong>%{project_name}</strong> projesine yeni bir üye davet edebilirsiniz."
- - "Вы можете пригласить нового участника в <strong>%{project_name}</strong>."
- - "Ви можете запросити нового учасника до <strong>%{project_name}</strong>."
-"You can invite another group to <strong>%{project_name}</strong>.":
- plural_id:
- translations:
- - "他のグループを<strong>%{project_name} </strong>に招待できます。"
- - "Podes convidar outro grupo para <strong>%{project_name}</strong>."
- - "您可以邀请另一个群组加入<strong>%{project_name}</strong>。"
- - "Ви можете запросити нову групу до <strong>%{project_name}</strong>."
- - "Puedes invitar a otro grupo a <strong>%{project_name}</strong>."
-"Example: <code>192.168.0.0/24</code>. %{read_more_link}.":
- plural_id:
- translations:
-"Note that PostgreSQL %{pg_version_upcoming} will become the minimum required version in GitLab %{gl_version_upcoming} (%{gl_version_upcoming_date}). Please consider upgrading your environment to a supported PostgreSQL version soon, see <a href=\\\"%{pg_version_upcoming_url}\\\">the related epic</a> for details.":
- plural_id:
- translations:
-"Authorize <strong>%{user}</strong> to use your account?":
- plural_id:
- translations:
-"DeployFreeze|Specify times when deployments are not allowed for an environment. The <code>gitlab-ci.yml</code> file must be updated to make deployment jobs aware of the %{freeze_period_link_start}freeze period%{freeze_period_link_end}.":
- plural_id:
- translations:
-"<project name>":
- translations:
- - "<название проекта>"
- - "<project name>"
- - "<proje adı>"
- - "<naziv projekta>"
- - "<ім’я проєкту>"
- - "<프로젝트 이름>"
-"<strong>Deletes</strong> source branch":
- plural_id:
- translations:
- - "<strong>刪除</strong>來源分支"
- - "<strong>Apagar</strong> branch de origem"
- - "ソースブランチを<strong>削除</strong>"
- - "<strong>刪除</strong>來源分支"
- - "<strong>Apagar</strong> o ramo de origem"
- - "<strong>Удаляет</strong> исходную ветку"
- - "<strong>删除</strong>源分支"
- - "<strong>Видаляє</strong> гілку-джерело"
- - "<strong>Löscht</strong> den Quellbranch"
- - "소스 브랜치 <strong>삭제</strong>"
- - "<strong>Supprime</strong> la branche source"
- - "<strong>elimina</strong> la rama origen"
- - "Kaynak dalı <strong>siler</strong>"
-"Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.":
- plural_id:
- translations:
- - "Você está prestes a excluir este selo. Selos excluídos <strong>não podem</strong> ser restaurados."
- - "このバッジを削除しようとしています。削除されたバッジは<strong>復元できません</strong>。"
- - "Estás prestes a apagar este emblema. Emblemas apagados <strong>não podem</strong> ser restaurados."
- - "Вы собираетесь удалить этот значок. Удаленные значки <strong>не могут</strong> быть восстановлены."
- - "您即将删除此徽章。徽章被删除后 <strong>不能</strong> 恢复。"
- - "Ви збираєтеся видалити цей значок. Вилучені значки <strong>не можуть</strong> бути відновлені."
- - "Du bist gerade dabei dieses Badge zu entfernen. Entfernte Badges können <strong>nicht</strong> rückgängig gemacht werden."
- - "이 배지를 삭제하려고합니다. 삭제 된 배지는 <strong>복원 할 수 없습니다</strong>."
- - "Vous êtes sur le point de supprimer ce badge. Les badges supprimés <strong>ne peuvent pas</strong> être restaurés."
- - "Va a eliminar esta insignia. Las insignias eliminadas <strong>no se pueden</strong> restaurar."
- - "Bu rozeti sileceksiniz. Silinen rozetler geri <strong>yüklenemez</strong>."
-"ClusterIntegration| This will permanently delete the following resources: <ul> <li>All installed applications and related resources</li> <li>The <code>gitlab-managed-apps</code> namespace</li> <li>Any project namespaces</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>":
- plural_id:
- translations:
- - "これにより、次のリソースは完全に削除されます <ul> <li>インストールされているすべてのアプリケーションと関連したリソース</li> <li> <code>gitlab-managed-apps</code> 名前空間</li> <li>任意のプロジェクト名前空間</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>"
- - "此操作将永久删除下列资源: <ul> <li>所有已安装的应用程序和相关资源</li> <li> <code>GitLab管理的应用</code> 命名空间</li> <li>任何项目命名空间</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>"
- - "Esto eliminará permanentemente los siguientes recursos: <ul> <li>Todas las aplicaciones instaladas y sus recursos relacionados</li> <li>El espacio de nombres <code>gitlab-managed-apps</code></li> <li>Cualquier espacio de nombres de proyecto</li> <li><code> clusterroles </code></li> <li><code>clusterrolebindings</code></li> </ul>"
-"Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}":
- plural_id:
- translations:
- - "Configure um arquivo <code>.gitlab-webide.yml</code> no diretório <code>.gitlab</code> para começar a usar o Terminal Web. %{helpStart}Saiba mais.%{helpEnd}"
- - "Webターミナルの使用を開始するには、 <code>.gitlab</code> ディレクトリの <code>.gitlab-webide.yml</code> ファイルを設定します。 詳細は%{helpStart}こちら%{helpEnd}です。"
- - "Сконфигурируйте файл <code>.gitlab-webide.yml</code> в каталоге <code>.gitlab</code> чтобы начать использовать веб-терминал. %{helpStart}Узнайте больше.%{helpEnd}"
- - "在 <code>.gitlab</code> 目录中配置 <code>.gitlab-webide.yml</code> 文件以开始使用Web终端。 %{helpStart}了解更多。%{helpEnd}"
- - "Налаштуйте файл <code>.gitlab-webide.yml</code> у директорії <code>.gitlab</code>, щоб почати використовувати Веб-термінал. %{helpStart}Докладніше.%{helpEnd}"
- - "웹 터미널 사용을 시작하도록 <code>.gitlab</code> 디렉토리에서 <code>.gitlab-webide.yml</code> 파일을 구성하십시오. %{helpStart}자세히 알아보십시오.%{helpEnd}"
- - "Configure un archivo <code>.gitlab-webide.yml</code> en el directorio <code>.gitlab</code> para comenzar a utilizar el Terminal Web. %{helpStart}Aprende más.%{helpEnd}"
-"Depends on <strong>%d closed</strong> merge request.":
- plural_id: "Depends on <strong>%d closed</strong> merge requests."
- translations:
- - "В зависимости от <strong>%d закрытого</strong> запроса на слияние."
- - "В зависимости от <strong>%d закрытых</strong> запросов на слияние."
- - "В зависимости от <strong>%d закрытых</strong> запросов на слияние."
- - "В зависимости от <strong>%d закрытых</strong> запросов на слияние."
- - "依赖于<strong>%d个已关闭的</strong>合并请求"
- - "Залежить від %d <strong>закритого</strong> запиту на злиття."
- - "Залежить від %d <strong>закритих</strong> запитів на злиття."
- - "Залежить від %d <strong>закритих</strong> запитів на злиття."
- - "Залежить від %d <strong>закритих</strong> запитів на злиття."
- - "<strong>%d kapanan</strong> birleştirme isteğine bağlıdır."
- - "<strong>%d kapanan</strong> birleştirme isteğine bağlıdır."
-"Go to <strong>Issues</strong> > <strong>Boards</strong> to access your personalized learning issue board.":
- plural_id:
- translations:
- - "转至<strong>议题</strong> > <strong>看板</strong>访问您的个性化学习议题看板。"
-"Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>":
- plural_id:
- translations:
- - "<span>要讓標籤</span> %{labelTitle} <span>提升到群組標籤嗎?</span>"
- - "<span>Promover a etiqueta</span> %{labelTitle} <span>para etiqueta do Grupo?</span>"
- - "%{labelTitle} <span>ラベルをグループラベルに昇格しますか?</span>"
- - "<span>Повысить метку</span> %{labelTitle} <span>до групповой метки?</span>"
- - "<span>将标记</span> %{labelTitle} <span>升级为群组标记?</span>"
- - "<span>Перенести мітку</span> %{labelTitle} <span>на рівень групи?</span>"
- - "<span>Label</span> %{labelTitle} <span>zu Gruppenlabel hochstufen?</span>"
- - "<span>라벨</span> %{labelTitle} <span>(을)를 그룹 라벨로 승격하시겠습니까?</span>"
- - "<span>Promouvoir l’étiquette</span> %{labelTitle} <span>en étiquette de groupe ?</span>"
- - "<span>¿Promocionar la etiqueta</span> %{labelTitle} <span>a etiqueta de grupo?</span>"
-"Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.":
- plural_id:
- translations:
- - "Travar este %{issuableDisplayName}? Apenas <strong>membros do projeto</strong> poderão comentar."
- - "%{issuableDisplayName} をロックしますか?<strong>プロジェクトメンバー</strong> のみコメントできます。"
- - "锁定此%{issuableDisplayName}吗?锁定后将只有<strong>项目成员</strong>可以发表评论。"
- - "Заблокувати цю %{issuableDisplayName}? Лише <strong>учасники проекту</strong> зможуть коментувати."
- - "%{issuableDisplayName} sperren? Es werden nur noch <strong>Projektmitglieder</strong> kommentieren können."
- - "Verrouiller ce·t·te %{issuableDisplayName} ? Seuls les <strong>membres du projet</strong> seront en mesure de commenter."
- - "¿Bloquear este %{issuableDisplayName}? Sólo los <strong>miembros del proyecto</strong> podrán comentar."
-"PrometheusService|<p class=\\\"text-tertiary\\\">No <a href=\\\"%{docsUrl}\\\">common metrics</a> were found</p>":
- plural_id:
- translations:
- - "<p class=\\\"text-tertiary\\\">Nenhuma <a href=\\\"%{docsUrl}\\\">métrica comum</a> foi encontrada</p>"
- - "<p class=\\\"text-tertiary\\\"><a href=\\\"%{docsUrl}\\\">共通メトリクス</a>は見つかりませんでした</p>"
- - "<p class=\\\"text-tertiary\\\">Ни одной <a href=\\\"%{docsUrl}\\\">общей метрики</a> не найдено</p>"
- - "<p class=\\\"text-tertiary\\\">无<a href=\\\"%{docsUrl}\\\">常用指标</a> </p>"
- - "<p class=\\\"text-tertiary\\\">Ніяких <a href=\\\"%{docsUrl}\\\">загальних метрик</a> не знайдено</p>"
- - "<p class=\\\"text-tertiary\\\">Es wurden keine <a href=\\\"%{docsUrl}\\\">allgemeinen Metriken</a> gefunden</p>"
- - "<p class=\\\"text-tertiary\\\"><a href=\\\"%{docsUrl}\\\">공통 메트릭스</a>가 발견되지 않았습니다.</p>"
- - "<p class=\\\"text-tertiary\\\">Aucune <a href=\\\"%{docsUrl}\\\">métrique commune</a> trouvée</p>"
- - "<p class=\\\"text-tertiary\\\">No se han encontrado<a href=\\\"%{docsUrl}\\\">métricas comunes</a> </p>"
-"This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">enable billing <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> and try again.":
- plural_id:
- translations:
- - "Este projeto não possui faturamento ativado. Para criar um cluster, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">ative o faturamento <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> e tente novamente."
- - "このプロジェクトでは課金が有効になっていません。クラスターを作成するには、<a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\"> 課金を有効<i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> にして再度お試しください。"
- - "此项目未启用账单。要创建群集,请 <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">启用账单 <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> 并重试。"
- - "Для цього проекту вимкнено білінг. Щоб створити кластер, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">увімкніть білінг <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> і спробуйте знову."
- - "Für dieses Projekt ist keine Abrechnung aktiviert. Um ein Cluster zu erstellen, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">aktiviere die Abrechnung<i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> und versuche es erneut."
- - "Ce projet n’a pas de facturation activée. Afin de créer une grappe de serveurs, veuillez <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">activer la facturation<i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> et réessayer."
- - "Este proyecto no tiene la facturación habilitada. Para crear un clúster, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">habilite la facturación <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> e inténtelo de nuevo."
-"Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.":
- plural_id:
- translations:
- - "Desbloquear este %{issuableDisplayName}? <strong>Todos</strong> poderão comentar."
- - "%{issuableDisplayName} のロックを解除しますか? <strong>全員</strong>がコメントできるようになります。"
- - "解锁此%{issuableDisplayName}吗?解锁后<strong>所有人</strong>都将可以发表评论。"
- - "Розблокувати %{issuableDisplayName}? <strong>Будь-хто</strong> зможе залишати коментарі."
- - "Dieses %{issuableDisplayName} entsperren? <strong>Jeder</strong> wird in der Lage sein zu kommentieren."
- - "%{issuableDisplayName}(을)를 잠금해제 하시겠습니까? <strong>모두가</strong> 코멘트 할 수 있게 됩니다."
- - "Déverrouiller %{issuableDisplayName} ? <strong>Tout le monde</strong> sera en mesure de commenter."
- - "Desbloquear este %{issuableDisplayName}? <strong>Todos</strong> podrán comentar."
-"confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.":
- plural_id:
- translations:
- - "Você está prestes a desligar a confidencialidade. Isso significa que <strong>todos</strong> serão capazes de ver e deixar comentários nesse issue."
- - "あなたは公開設定に変更しようとしています。これは<strong>すべての人</strong> が閲覧可能になり、課題に対してコメントを残すことができるようになることを意味します。"
- - "即将关闭私密性。这将使得 <strong>所有用户</strong>都可以查看并且评论当前议题。"
- - "Ви вимикаєте конфіденційність. Це означає, що <strong>будь-хто</strong> зможе бачити і залишати коментарі для цієї задачі."
- - "Du willst die Vertraulichkeit deaktivieren. Das bedeutet, dass <strong>alle</strong> das Ticket betrachten und kommentieren können."
- - "Vous êtes sur le point de désactiver la confidentialité. Cela signifie que <strong>tout le monde</strong> sera en mesure de voir et de laisser un commentaire sur ce ticket."
- - "Va a desactivar la confidencialidad. Esto significa que <strong>todos</strong> podrán ver y dejar un comentario sobre este tema."
-"confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.":
- plural_id:
- translations:
- - "Você está prestes a ligar a confidencialidade. Isso significa que apenas membros da equipe com <strong>ao menos acesso de Relator</strong> serão capazes de ver e deixar comentários nesse issue."
- - "あなたは公開設定に変更しようとしています。これはチームに限定していた<strong>最小限の報告権限</strong>をなくし、課題に対してコメントを残すことができるようになることを意味します。"
- - "即将设置私密性。这将使得 <strong>至少有Reporter以上权限</strong>的团队成员才能查看并且评论当前议题。"
- - "Ви вмикаєте конфіденційність. Це означає що лише учасники команди <strong>рівня репортер або вище</strong> матимуть змогу бачити та залишати коментарі для цієї задачі."
- - "Du willst die Vertraulichkeit aktivieren. Das bedeutet, dass nur Teammitglieder mit <strong>mindestens Reporter-Zugriff</strong> das Ticket betrachten und kommentieren können."
- - "Vous êtes sur le point de d’activer la confidentialité. Cela signifie que seuls les membres de l’équipe avec <strong>au moins un accès en tant que rapporteur</strong> seront en mesure de voir et de laisser des commentaires sur le ticket."
- - "Va a activar la confidencialidad. Esto significa que solo los miembros del equipo con como mínimo,<strong>acceso como Reporter</strong> podrán ver y dejar comentarios sobre la incidencia."
- - "あなたは非公開設定をオンにしようとしています。これは、最低でも<strong>報告権限</strong>を持ったチームメンバーのみが課題を表示したりコメントを残したりすることができるようになるということです。"
-" or <!merge request id>":
- translations:
- - " ወይም <!merge request id>"
- - " ou <!merge request id>"
- - " または <!merge request id>"
- - "或 <!合併請求 id>"
- - " или <!merge request id>"
- - "或<!merge request id>"
- - " або <!merge request id>"
- - " oder <!merge request id>"
- - " o <!merge request id>"
- - " 또는 <!merge request id>"
- - " o <!merge request id>"
- - " veya <!merge request id>"
- - " neu <!merge request id>"
- - " neu <#issue id>"
-" or <#issue id>":
- translations:
- - "或 <#issue id>"
- - " ወይም ‹#issue id›"
- - " ou <identificación #issue>"
- - " ou <#issue id>"
- - " または <#課題 ID>"
- - " o <#issue id>"
- - "或 <#議題 id>"
- - " ou <#issue id>"
- - " или <#issue id>"
- - "或 <#issue id>"
- - " або <#issue id>"
- - " oder <#issue id>"
- - " o <#issue id>"
- - " 또는 <#issue id>"
- - " ou <#issue id>"
- - " o <#issue id>"
- - " veya <#issue id>"
- - " neu <#issue id>"
-" or <&epic id>":
- translations:
- - " ወይም <&epic id>"
- - " または <&エピックID>"
- - " 或 <#史詩 id>"
- - " или <&epic id>"
- - " 或<#epic id>"
- - " або <&epic id>"
- - " oder <&epic id>"
- - " o <&epic id>"
- - " veya <&epic id>"
- - " neu <#epic id>"
- - " 또는 <&epic id>"
-"< 1 hour":
- translations:
- - "1 時間未満"
- - "< 1 小時"
- - "< 1 часа"
- - "< 1小时"
- - "< 1 години"
- - "< 1 hora"
- - "< 1 saat"
- - "< 1 Stunde"
- - "< 1시간"
diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb
index 33054a5b9bf..3bb34ab2811 100644
--- a/lib/gitlab/i18n/po_linter.rb
+++ b/lib/gitlab/i18n/po_linter.rb
@@ -5,14 +5,13 @@ module Gitlab
class PoLinter
include Gitlab::Utils::StrongMemoize
- attr_reader :po_path, :translation_entries, :metadata_entry, :locale, :html_todolist
+ attr_reader :po_path, :translation_entries, :metadata_entry, :locale
VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze
- def initialize(po_path:, html_todolist:, locale: I18n.locale.to_s)
+ def initialize(po_path:, locale: I18n.locale.to_s)
@po_path = po_path
@locale = locale
- @html_todolist = html_todolist
end
def errors
@@ -43,8 +42,7 @@ module Gitlab
@translation_entries = entries.map do |entry_data|
Gitlab::I18n::TranslationEntry.new(
entry_data: entry_data,
- nplurals: metadata_entry.expected_forms,
- html_allowed: html_todolist.fetch(entry_data[:msgid], false)
+ nplurals: metadata_entry.expected_forms
)
end
@@ -97,15 +95,15 @@ module Gitlab
common_message = 'contains < or >. Use variables to include HTML in the string, or the &lt; and &gt; codes ' \
'for the symbols. For more info see: https://docs.gitlab.com/ee/development/i18n/externalization.html#html'
- if entry.msgid_contains_potential_html? && !entry.msgid_html_allowed?
+ if entry.msgid_contains_potential_html?
errors << common_message
end
- if entry.plural_id_contains_potential_html? && !entry.plural_id_html_allowed?
+ if entry.plural_id_contains_potential_html?
errors << 'plural id ' + common_message
end
- if entry.translations_contain_potential_html? && !entry.translations_html_allowed?
+ if entry.translations_contain_potential_html?
errors << 'translation ' + common_message
end
end
diff --git a/lib/gitlab/i18n/translation_entry.rb b/lib/gitlab/i18n/translation_entry.rb
index 25a45332d27..f3cca97950d 100644
--- a/lib/gitlab/i18n/translation_entry.rb
+++ b/lib/gitlab/i18n/translation_entry.rb
@@ -6,12 +6,11 @@ module Gitlab
PERCENT_REGEX = /(?:^|[^%])%(?!{\w*}|[a-z%])/.freeze
ANGLE_BRACKET_REGEX = /[<>]/.freeze
- attr_reader :nplurals, :entry_data, :html_allowed
+ attr_reader :nplurals, :entry_data
- def initialize(entry_data:, nplurals:, html_allowed:)
+ def initialize(entry_data:, nplurals:)
@entry_data = entry_data
@nplurals = nplurals
- @html_allowed = html_allowed
end
def msgid
@@ -97,20 +96,6 @@ module Gitlab
all_translations.any? { |translation| contains_angle_brackets?(translation) }
end
- def msgid_html_allowed?
- html_allowed.present?
- end
-
- def plural_id_html_allowed?
- html_allowed.present? && html_allowed['plural_id'] == plural_id
- end
-
- def translations_html_allowed?
- msgid_html_allowed? && html_allowed['translations'].present? && all_translations.all? do |translation|
- html_allowed['translations'].include?(translation)
- end
- end
-
private
def contains_angle_brackets?(string)
diff --git a/lib/gitlab/import_export/group/tree_restorer.rb b/lib/gitlab/import_export/group/tree_restorer.rb
index d0c0999f291..dfe27118d66 100644
--- a/lib/gitlab/import_export/group/tree_restorer.rb
+++ b/lib/gitlab/import_export/group/tree_restorer.rb
@@ -74,6 +74,12 @@ module Gitlab
group = create_group(group_attributes)
restore_group(group, group_attributes)
+ rescue => e
+ import_failure_service.log_import_failure(
+ source: 'process_child',
+ relation_key: 'group',
+ exception: e
+ )
end
def create_group(group_attributes)
@@ -83,13 +89,17 @@ module Gitlab
parent_group = @groups_mapping.fetch(parent_id) { raise(ArgumentError, 'Parent group not found') }
- ::Groups::CreateService.new(
+ group = ::Groups::CreateService.new(
user,
name: name,
path: path,
parent_id: parent_group.id,
visibility_level: sub_group_visibility_level(group_attributes.attributes, parent_group)
).execute
+
+ group.validate!
+
+ group
end
def restore_group(group, group_attributes)
@@ -134,6 +144,10 @@ module Gitlab
)
end
end
+
+ def import_failure_service
+ Gitlab::ImportExport::ImportFailureService.new(@top_level_group)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/import_failure_service.rb b/lib/gitlab/import_export/import_failure_service.rb
index d4eca551b49..bf7200726a1 100644
--- a/lib/gitlab/import_export/import_failure_service.rb
+++ b/lib/gitlab/import_export/import_failure_service.rb
@@ -28,23 +28,26 @@ module Gitlab
end
def log_import_failure(source:, relation_key: nil, relation_index: nil, exception:, retry_count: 0)
- extra = {
- source: source,
- relation_key: relation_key,
+ attributes = {
relation_index: relation_index,
- retry_count: retry_count
+ source: source,
+ retry_count: retry_count,
+ importable_column_name => importable.id
}
- extra[importable_column_name] = importable.id
-
- Gitlab::ErrorTracking.track_exception(exception, extra)
-
- attributes = {
- exception_class: exception.class.to_s,
- exception_message: exception.message.truncate(255),
- correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
- }.merge(extra)
- ImportFailure.create(attributes)
+ Gitlab::ErrorTracking.track_exception(
+ exception,
+ attributes.merge(relation_name: relation_key)
+ )
+
+ ImportFailure.create(
+ attributes.merge(
+ exception_class: exception.class.to_s,
+ exception_message: exception.message.truncate(255),
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id,
+ relation_key: relation_key
+ )
+ )
end
private
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index 8e78f6e274a..789249c7d91 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -79,10 +79,9 @@ module Gitlab
end
def wiki_restorer
- Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path,
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
shared: shared,
- project: ProjectWiki.new(project),
- wiki_enabled: project.wiki_enabled?)
+ project: ProjectWiki.new(project))
end
def design_repo_restorer
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index ae7ddbc5eba..778b42f4358 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -169,6 +169,7 @@ excluded_attributes:
- :compliance_framework_setting
- :show_default_award_emojis
- :services
+ - :exported_protected_branches
namespaces:
- :runners_token
- :runners_token_encrypted
@@ -219,6 +220,7 @@ excluded_attributes:
- :duplicated_to_id
- :promoted_to_epic_id
- :blocking_issues_count
+ - :service_desk_reply_to
merge_request:
- :milestone_id
- :sprint_id
@@ -340,10 +342,12 @@ excluded_attributes:
- :protected_environment_id
boards:
- :milestone_id
+ - :iteration_id
lists:
- :board_id
- :label_id
- :milestone_id
+ - :iteration_id
epic:
- :start_date_sourcing_milestone_id
- :due_date_sourcing_milestone_id
diff --git a/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb b/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb
index 44ccb67a531..4db92b12968 100644
--- a/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb
@@ -5,8 +5,8 @@ module Gitlab
module Project
module Sample
class RelationTreeRestorer < ImportExport::RelationTreeRestorer
- def initialize(*args)
- super
+ def initialize(...)
+ super(...)
@date_calculator = Gitlab::ImportExport::Project::Sample::DateCalculator.new(dates)
end
diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb
index ea16d978127..8bc87ecb071 100644
--- a/lib/gitlab/import_export/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/relation_tree_restorer.rb
@@ -71,7 +71,7 @@ module Gitlab
end
def process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
- relation_object = build_relation(relation_key, relation_definition, data_hash)
+ relation_object = build_relation(relation_key, relation_definition, relation_index, data_hash)
return unless relation_object
return if importable_class == ::Project && group_model?(relation_object)
@@ -139,23 +139,35 @@ module Gitlab
end
end
- def build_relations(relation_key, relation_definition, data_hashes)
+ def build_relations(relation_key, relation_definition, relation_index, data_hashes)
data_hashes
- .map { |data_hash| build_relation(relation_key, relation_definition, data_hash) }
+ .map { |data_hash| build_relation(relation_key, relation_definition, relation_index, data_hash) }
.tap { |entries| entries.compact! }
end
- def build_relation(relation_key, relation_definition, data_hash)
+ def build_relation(relation_key, relation_definition, relation_index, data_hash)
# TODO: This is hack to not create relation for the author
# Rather make `RelationFactory#set_note_author` to take care of that
return data_hash if relation_key == 'author' || already_restored?(data_hash)
# create relation objects recursively for all sub-objects
relation_definition.each do |sub_relation_key, sub_relation_definition|
- transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
+ transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
end
- @relation_factory.create(relation_factory_params(relation_key, data_hash))
+ relation = @relation_factory.create(**relation_factory_params(relation_key, data_hash))
+
+ if relation && !relation.valid?
+ @shared.logger.warn(
+ message: "[Project/Group Import] Invalid object relation built",
+ relation_key: relation_key,
+ relation_index: relation_index,
+ relation_class: relation.class.name,
+ error_messages: relation.errors.full_messages.join(". ")
+ )
+ end
+
+ relation
end
# Since we update the data hash in place as we restore relation items,
@@ -165,7 +177,7 @@ module Gitlab
!relation_item.is_a?(Hash)
end
- def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
+ def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
sub_data_hash = data_hash[sub_relation_key]
return unless sub_data_hash
@@ -176,11 +188,13 @@ module Gitlab
build_relations(
sub_relation_key,
sub_relation_definition,
+ relation_index,
sub_data_hash).presence
else
build_relation(
sub_relation_key,
sub_relation_definition,
+ relation_index,
sub_data_hash)
end
diff --git a/lib/gitlab/import_export/wiki_restorer.rb b/lib/gitlab/import_export/wiki_restorer.rb
deleted file mode 100644
index 359ba8ba769..00000000000
--- a/lib/gitlab/import_export/wiki_restorer.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ImportExport
- class WikiRestorer < RepoRestorer
- def initialize(project:, shared:, path_to_bundle:, wiki_enabled:)
- super(project: project, shared: shared, path_to_bundle: path_to_bundle)
-
- @project = project
- @wiki_enabled = wiki_enabled
- end
-
- def restore
- project.wiki if create_empty_wiki?
-
- super
- end
-
- private
-
- attr_accessor :project, :wiki_enabled
-
- def create_empty_wiki?
- !File.exist?(path_to_bundle) && wiki_enabled
- end
- end
- end
-end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 58c7744fae0..88753e80391 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -15,7 +15,7 @@ module Gitlab
ImportSource.new('bitbucket', 'Bitbucket Cloud', Gitlab::BitbucketImport::Importer),
ImportSource.new('bitbucket_server', 'Bitbucket Server', Gitlab::BitbucketServerImport::Importer),
ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
- ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
+ ImportSource.new('google_code', 'Google Code', nil),
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
ImportSource.new('git', 'Repo by URL', nil),
ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index d7228099eaf..6b0f01757b7 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -13,7 +13,8 @@ module Gitlab
:rugged_duration_s,
:elasticsearch_calls,
:elasticsearch_duration_s,
- *::Gitlab::Instrumentation::Redis.known_payload_keys]
+ *::Gitlab::Instrumentation::Redis.known_payload_keys,
+ *::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS]
end
def add_instrumentation_data(payload)
@@ -22,6 +23,7 @@ module Gitlab
instrument_redis(payload)
instrument_elasticsearch(payload)
instrument_throttle(payload)
+ instrument_active_record(payload)
end
def instrument_gitaly(payload)
@@ -62,6 +64,12 @@ module Gitlab
payload[:throttle_safelist] = safelist if safelist.present?
end
+ def instrument_active_record(payload)
+ db_counters = ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload
+
+ payload.merge!(db_counters)
+ end
+
# Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
# `enqueued_at` field or `created_at` field is available.
#
diff --git a/lib/gitlab/kroki.rb b/lib/gitlab/kroki.rb
new file mode 100644
index 00000000000..8c5652fb766
--- /dev/null
+++ b/lib/gitlab/kroki.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'asciidoctor/extensions/asciidoctor_kroki/extension'
+
+module Gitlab
+ # Helper methods for Kroki
+ module Kroki
+ BLOCKDIAG_FORMATS = %w[
+ blockdiag
+ seqdiag
+ actdiag
+ nwdiag
+ packetdiag
+ rackdiag
+ ].freeze
+ # Diagrams that require a companion container are disabled for now
+ DIAGRAMS_FORMATS = ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES
+ .reject { |diagram_type| diagram_type == 'mermaid' || diagram_type == 'bpmn' || BLOCKDIAG_FORMATS.include?(diagram_type) }
+ DIAGRAMS_FORMATS_WO_PLANTUML = DIAGRAMS_FORMATS
+ .reject { |diagram_type| diagram_type == 'plantuml' }
+
+ # Get the list of diagram formats that are currently enabled
+ #
+ # Returns an Array of diagram formats.
+ # If Kroki is not enabled, returns an empty Array.
+ def self.formats(current_settings)
+ return [] unless current_settings.kroki_enabled
+
+ # If PlantUML is enabled, PlantUML diagrams will be processed by the PlantUML server.
+ # In other words, the PlantUML server has precedence over Kroki since both can process PlantUML diagrams.
+ if current_settings.plantuml_enabled
+ DIAGRAMS_FORMATS_WO_PLANTUML
+ else
+ DIAGRAMS_FORMATS
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/deployment.rb b/lib/gitlab/kubernetes/deployment.rb
new file mode 100644
index 00000000000..55ed9a7517e
--- /dev/null
+++ b/lib/gitlab/kubernetes/deployment.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class Deployment
+ include Gitlab::Utils::StrongMemoize
+
+ STABLE_TRACK_VALUE = 'stable'.freeze
+
+ def initialize(attributes = {}, pods: [])
+ @attributes = attributes
+ @pods = pods
+ end
+
+ def name
+ metadata['name'] || 'unknown'
+ end
+
+ def labels
+ metadata.fetch('labels', {})
+ end
+
+ def annotations
+ metadata.fetch('annotations', {})
+ end
+
+ def track
+ labels.fetch('track', STABLE_TRACK_VALUE)
+ end
+
+ def stable?
+ track == 'stable'
+ end
+
+ def order
+ stable? ? 1 : 0
+ end
+
+ def outdated?
+ observed_generation < generation
+ end
+
+ def wanted_instances
+ spec.fetch('replicas', 0)
+ end
+
+ def created_instances
+ filtered_pods_by_track.map do |pod|
+ pod_metadata = pod.fetch('metadata', {})
+ pod_name = pod_metadata['name'] || pod_metadata['generateName']
+ pod_status = pod.dig('status', 'phase')
+
+ deployment_instance(pod_name: pod_name, pod_status: pod_status)
+ end
+ end
+
+ # These are replicas that did not get created yet,
+ # So they still do not have any associated pod,
+ # these are marked as pending instances.
+ def not_created_instances
+ pending_instances_count = wanted_instances - filtered_pods_by_track.count
+
+ return [] if pending_instances_count <= 0
+
+ Array.new(pending_instances_count, deployment_instance(pod_name: 'Not provided', pod_status: 'Pending'))
+ end
+
+ def filtered_pods_by_track
+ strong_memoize(:filtered_pods_by_track) do
+ @pods.select { |pod| has_same_track?(pod) }
+ end
+ end
+
+ def instances
+ created_instances + not_created_instances
+ end
+
+ private
+
+ def deployment_instance(pod_name:, pod_status:)
+ {
+ status: pod_status&.downcase,
+ pod_name: pod_name,
+ tooltip: "#{pod_name} (#{pod_status})",
+ track: track,
+ stable: stable?
+ }
+ end
+
+ def has_same_track?(pod)
+ pod_track = pod.dig('metadata', 'labels', 'track') || STABLE_TRACK_VALUE
+
+ pod_track == track
+ end
+
+ def metadata
+ @attributes.fetch('metadata', {})
+ end
+
+ def spec
+ @attributes.fetch('spec', {})
+ end
+
+ def status
+ @attributes.fetch('status', {})
+ end
+
+ def generation
+ metadata.fetch('generation', 0)
+ end
+
+ def observed_generation
+ status.fetch('observedGeneration', 0)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/v2/client_command.rb b/lib/gitlab/kubernetes/helm/v2/client_command.rb
index 88693a28d6c..8b15af9aeea 100644
--- a/lib/gitlab/kubernetes/helm/v2/client_command.rb
+++ b/lib/gitlab/kubernetes/helm/v2/client_command.rb
@@ -22,17 +22,6 @@ module Gitlab
def repository_update_command
'helm repo update'
end
-
- def optional_tls_flags
- return [] unless files.key?(:'ca.pem')
-
- [
- '--tls',
- '--tls-ca-cert', "#{files_dir}/ca.pem",
- '--tls-cert', "#{files_dir}/cert.pem",
- '--tls-key', "#{files_dir}/key.pem"
- ]
- end
end
end
end
diff --git a/lib/gitlab/kubernetes/helm/v2/reset_command.rb b/lib/gitlab/kubernetes/helm/v2/reset_command.rb
index 172a0884c49..00626501a9a 100644
--- a/lib/gitlab/kubernetes/helm/v2/reset_command.rb
+++ b/lib/gitlab/kubernetes/helm/v2/reset_command.rb
@@ -9,9 +9,8 @@ module Gitlab
def generate_script
super + [
- reset_helm_command,
- delete_tiller_replicaset,
- delete_tiller_clusterrolebinding
+ init_command,
+ reset_helm_command
].join("\n")
end
@@ -21,27 +20,8 @@ module Gitlab
private
- # This method can be delete once we upgrade Helm to > 12.13.0
- # https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/27096#note_159695900
- #
- # Tracking this method to be removed here:
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/52791#note_199374155
- def delete_tiller_replicaset
- delete_args = %w[replicaset -n gitlab-managed-apps -l name=tiller]
-
- Gitlab::Kubernetes::KubectlCmd.delete(*delete_args)
- end
-
- def delete_tiller_clusterrolebinding
- delete_args = %w[clusterrolebinding tiller-admin]
-
- Gitlab::Kubernetes::KubectlCmd.delete(*delete_args)
- end
-
def reset_helm_command
- command = %w[helm reset] + optional_tls_flags
-
- command.shelljoin
+ 'helm reset --force'
end
end
end
diff --git a/lib/gitlab/kubernetes/ingress.rb b/lib/gitlab/kubernetes/ingress.rb
new file mode 100644
index 00000000000..c5643dd670a
--- /dev/null
+++ b/lib/gitlab/kubernetes/ingress.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class Ingress
+ include Gitlab::Utils::StrongMemoize
+
+ # Canary Ingress Annotations https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary
+ ANNOTATION_KEY_CANARY = 'nginx.ingress.kubernetes.io/canary'
+ ANNOTATION_KEY_CANARY_WEIGHT = 'nginx.ingress.kubernetes.io/canary-weight'
+
+ def initialize(attributes = {})
+ @attributes = attributes
+ end
+
+ def canary?
+ strong_memoize(:is_canary) do
+ annotations.any? do |key, value|
+ key == ANNOTATION_KEY_CANARY && value == 'true'
+ end
+ end
+ end
+
+ def canary_weight
+ return unless canary?
+ return unless annotations.key?(ANNOTATION_KEY_CANARY_WEIGHT)
+
+ annotations[ANNOTATION_KEY_CANARY_WEIGHT].to_i
+ end
+
+ def name
+ metadata['name']
+ end
+
+ private
+
+ def metadata
+ @attributes.fetch('metadata', {})
+ end
+
+ def annotations
+ metadata.fetch('annotations', {})
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/rollout_instances.rb b/lib/gitlab/kubernetes/rollout_instances.rb
new file mode 100644
index 00000000000..c5dba71f505
--- /dev/null
+++ b/lib/gitlab/kubernetes/rollout_instances.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class RolloutInstances
+ include ::Gitlab::Utils::StrongMemoize
+
+ def initialize(deployments, pods)
+ @deployments = deployments
+ @pods = pods
+ end
+
+ def pod_instances
+ pods = matching_pods + extra_pending_pods
+
+ pods.sort_by(&:order).map do |pod|
+ to_hash(pod)
+ end
+ end
+
+ private
+
+ attr_reader :deployments, :pods
+
+ def matching_pods
+ strong_memoize(:matching_pods) do
+ deployment_tracks = deployments.map(&:track)
+ pods.select { |p| deployment_tracks.include?(p.track) }
+ end
+ end
+
+ def extra_pending_pods
+ wanted_instances = sum_hashes(deployments.map { |d| { d.track => d.wanted_instances } })
+ present_instances = sum_hashes(matching_pods.map { |p| { p.track => 1 } })
+ pending_instances = subtract_hashes(wanted_instances, present_instances)
+
+ pending_instances.flat_map do |track, num|
+ Array.new(num, pending_pod_for(track))
+ end
+ end
+
+ def sum_hashes(hashes)
+ hashes.reduce({}) do |memo, hash|
+ memo.merge(hash) { |_key, memo_val, hash_val| memo_val + hash_val }
+ end
+ end
+
+ def subtract_hashes(hash_a, hash_b)
+ hash_a.merge(hash_b) { |_key, val_a, val_b| [0, val_a - val_b].max }
+ end
+
+ def pending_pod_for(track)
+ ::Gitlab::Kubernetes::Pod.new({
+ 'status' => { 'phase' => 'Pending' },
+ 'metadata' => {
+ 'name' => 'Not provided',
+ 'labels' => {
+ 'track' => track
+ }
+ }
+ })
+ end
+
+ def to_hash(pod)
+ {
+ status: pod.status&.downcase,
+ pod_name: pod.name,
+ tooltip: "#{pod.name} (#{pod.status})",
+ track: pod.track,
+ stable: pod.stable?
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/rollout_status.rb b/lib/gitlab/kubernetes/rollout_status.rb
new file mode 100644
index 00000000000..e275303e650
--- /dev/null
+++ b/lib/gitlab/kubernetes/rollout_status.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ # Calculates the rollout status for a set of kubernetes deployments.
+ #
+ # A GitLab environment may be composed of several Kubernetes deployments and
+ # other resources. The rollout status sums the Kubernetes deployments
+ # together.
+ class RolloutStatus
+ attr_reader :deployments, :instances, :completion, :status, :canary_ingress
+
+ def complete?
+ completion == 100
+ end
+
+ def loading?
+ @status == :loading
+ end
+
+ def not_found?
+ @status == :not_found
+ end
+
+ def found?
+ @status == :found
+ end
+
+ def canary_ingress_exists?
+ canary_ingress.present?
+ end
+
+ def self.from_deployments(*deployments_attrs, pods_attrs: [], ingresses: [])
+ return new([], status: :not_found) if deployments_attrs.empty?
+
+ deployments = deployments_attrs.map do |attrs|
+ ::Gitlab::Kubernetes::Deployment.new(attrs, pods: pods_attrs)
+ end
+ deployments.sort_by!(&:order)
+
+ pods = pods_attrs.map do |attrs|
+ ::Gitlab::Kubernetes::Pod.new(attrs)
+ end
+
+ ingresses = ingresses.map { |ingress| ::Gitlab::Kubernetes::Ingress.new(ingress) }
+
+ new(deployments, pods: pods, ingresses: ingresses)
+ end
+
+ def self.loading
+ new([], status: :loading)
+ end
+
+ def initialize(deployments, pods: [], ingresses: [], status: :found)
+ @status = status
+ @deployments = deployments
+ @instances = RolloutInstances.new(deployments, pods).pod_instances
+ @canary_ingress = ingresses.find(&:canary?)
+
+ @completion =
+ if @instances.empty?
+ 100
+ else
+ # We downcase the pod status in Gitlab::Kubernetes::Deployment#deployment_instance
+ finished = @instances.count { |instance| instance[:status] == ::Gitlab::Kubernetes::Pod::RUNNING.downcase }
+
+ (finished / @instances.count.to_f * 100).to_i
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb
index b484b69c932..c54325bcdf5 100644
--- a/lib/gitlab/legacy_github_import/project_creator.rb
+++ b/lib/gitlab/legacy_github_import/project_creator.rb
@@ -5,7 +5,7 @@ module Gitlab
class ProjectCreator
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
- def initialize(repo, name, namespace, current_user, session_data, type: 'github')
+ def initialize(repo, name, namespace, current_user, type: 'github', **session_data)
@repo = repo
@name = name
@namespace = namespace
diff --git a/lib/gitlab/markdown_cache/active_record/extension.rb b/lib/gitlab/markdown_cache/active_record/extension.rb
index 233d3bf1ac7..1de890c84f9 100644
--- a/lib/gitlab/markdown_cache/active_record/extension.rb
+++ b/lib/gitlab/markdown_cache/active_record/extension.rb
@@ -10,6 +10,7 @@ module Gitlab
# Using before_update here conflicts with elasticsearch-model somehow
before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
+ after_save :store_mentions!, if: :mentionable_attributes_changed?
end
# Always exclude _html fields from attributes (including serialization).
diff --git a/lib/gitlab/metrics/background_transaction.rb b/lib/gitlab/metrics/background_transaction.rb
deleted file mode 100644
index 7b05ae29b02..00000000000
--- a/lib/gitlab/metrics/background_transaction.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Metrics
- class BackgroundTransaction < Transaction
- def initialize(worker_class)
- super()
- @worker_class = worker_class
- end
-
- def labels
- { controller: @worker_class.name, action: 'perform', feature_category: @worker_class.try(:get_feature_category).to_s }
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
deleted file mode 100644
index 8c4e5a8d70c..00000000000
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Metrics
- # Sidekiq middleware for tracking jobs.
- #
- # This middleware is intended to be used as a server-side middleware.
- class SidekiqMiddleware
- def call(worker, payload, queue)
- trans = BackgroundTransaction.new(worker.class)
-
- begin
- # Old gitlad-shell messages don't provide enqueued_at/created_at attributes
- enqueued_at = payload['enqueued_at'] || payload['created_at'] || 0
- trans.set(:gitlab_transaction_sidekiq_queue_duration_total, Time.current.to_f - enqueued_at) do
- multiprocess_mode :livesum
- end
- trans.run { yield }
- rescue Exception => error # rubocop: disable Lint/RescueException
- trans.add_event(:sidekiq_exception)
-
- raise error
- ensure
- add_info_to_payload(payload, trans)
- end
- end
-
- private
-
- def add_info_to_payload(payload, trans)
- payload.merge!(::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload)
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index f9ba0a69b0e..d725d8d7b29 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -16,16 +16,14 @@ module Gitlab
# using a connection.
Thread.current[:uses_db_connection] = true
- return unless current_transaction
-
payload = event.payload
return if payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql])
- current_transaction.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do
+ increment_db_counters(payload)
+
+ current_transaction&.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do
buckets [0.05, 0.1, 0.25]
end
-
- increment_db_counters(payload)
end
def self.db_counter_payload
@@ -53,7 +51,7 @@ module Gitlab
end
def increment(counter)
- current_transaction.increment("gitlab_transaction_#{counter}_total".to_sym, 1)
+ current_transaction&.increment("gitlab_transaction_#{counter}_total".to_sym, 1)
if Gitlab::SafeRequestStore.active?
Gitlab::SafeRequestStore[counter] = Gitlab::SafeRequestStore[counter].to_i + 1
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 95bc90f9dad..3ebafb5c5e4 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -48,23 +48,15 @@ module Gitlab
@finished_at ? (@finished_at - @started_at) : 0.0
end
- def thread_cpu_duration
- System.thread_cpu_duration(@thread_cputime_start)
- end
-
def run
Thread.current[THREAD_KEY] = self
@started_at = System.monotonic_time
- @thread_cputime_start = System.thread_cpu_time
yield
ensure
@finished_at = System.monotonic_time
- observe(:gitlab_transaction_cputime_seconds, thread_cpu_duration) do
- buckets SMALL_BUCKETS
- end
observe(:gitlab_transaction_duration_seconds, duration) do
buckets SMALL_BUCKETS
end
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
index 2064f9290d3..1811389a744 100644
--- a/lib/gitlab/metrics/web_transaction.rb
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -66,9 +66,10 @@ module Gitlab
if route
path = endpoint_paths_cache[route.request_method][route.path]
- # Feature categories will be added for grape endpoints in
- # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/462
- { controller: 'Grape', action: "#{route.request_method} #{path}", feature_category: '' }
+ grape_class = endpoint.options[:for]
+ feature_category = grape_class.try(:feature_category_for_app, endpoint).to_s
+
+ { controller: 'Grape', action: "#{route.request_method} #{path}", feature_category: feature_category }
end
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index 101172cdfcc..b11ee0afc10 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -9,7 +9,7 @@ module Gitlab
APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
- ALLOWLISTED_GIT_ROUTES = {
+ ALLOWLISTED_GIT_READ_ONLY_ROUTES = {
'repositories/git_http' => %w{git_upload_pack}
}.freeze
@@ -34,7 +34,7 @@ module Gitlab
end
def call
- if disallowed_request? && Gitlab::Database.read_only?
+ if disallowed_request? && read_only?
Gitlab::AppLogger.debug('GitLab ReadOnly: preventing possible non read-only operation')
if json_request?
@@ -57,6 +57,11 @@ module Gitlab
!allowlisted_routes
end
+ # Overridden in EE module
+ def read_only?
+ Gitlab::Database.read_only?
+ end
+
def json_request?
APPLICATION_JSON_TYPES.include?(request.media_type)
end
@@ -97,7 +102,7 @@ module Gitlab
return false unless request.post? &&
request.path.end_with?('.git/git-upload-pack')
- ALLOWLISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
+ ALLOWLISTED_GIT_READ_ONLY_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
def internal_route?
diff --git a/lib/gitlab/pagination/gitaly_keyset_pager.rb b/lib/gitlab/pagination/gitaly_keyset_pager.rb
index 651e3d5a807..1350168967e 100644
--- a/lib/gitlab/pagination/gitaly_keyset_pager.rb
+++ b/lib/gitlab/pagination/gitaly_keyset_pager.rb
@@ -15,6 +15,7 @@ module Gitlab
# and supports pagination via gitaly.
def paginate(finder)
return paginate_via_gitaly(finder) if keyset_pagination_enabled?
+ return paginate_first_page_via_gitaly(finder) if paginate_first_page?
branches = ::Kaminari.paginate_array(finder.execute)
Gitlab::Pagination::OffsetPagination
@@ -25,7 +26,11 @@ module Gitlab
private
def keyset_pagination_enabled?
- Feature.enabled?(:branch_list_keyset_pagination, project) && params[:pagination] == 'keyset'
+ Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && params[:pagination] == 'keyset'
+ end
+
+ def paginate_first_page?
+ Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && (params[:page].blank? || params[:page].to_i == 1)
end
def paginate_via_gitaly(finder)
@@ -34,6 +39,20 @@ module Gitlab
end
end
+ # When first page is requested, we paginate the data via Gitaly
+ # Headers are added to immitate offset pagination, while it is the default option
+ def paginate_first_page_via_gitaly(finder)
+ finder.execute(gitaly_pagination: true).tap do |records|
+ total = project.repository.branch_count
+ per_page = params[:per_page].presence || Kaminari.config.default_per_page
+
+ Gitlab::Pagination::OffsetHeaderBuilder.new(
+ request_context: request_context, per_page: per_page, page: 1, next_page: 2,
+ total: total, total_pages: total / per_page + 1
+ ).execute
+ end
+ end
+
def apply_headers(records)
if records.count == params[:per_page]
Gitlab::Pagination::Keyset::HeaderBuilder
diff --git a/lib/gitlab/pagination/offset_header_builder.rb b/lib/gitlab/pagination/offset_header_builder.rb
new file mode 100644
index 00000000000..32089e40932
--- /dev/null
+++ b/lib/gitlab/pagination/offset_header_builder.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ class OffsetHeaderBuilder
+ attr_reader :request_context, :per_page, :page, :next_page, :prev_page, :total, :total_pages
+
+ delegate :params, :header, :request, to: :request_context
+
+ def initialize(request_context:, per_page:, page:, next_page:, prev_page: nil, total:, total_pages:)
+ @request_context = request_context
+ @per_page = per_page
+ @page = page
+ @next_page = next_page
+ @prev_page = prev_page
+ @total = total
+ @total_pages = total_pages
+ end
+
+ def execute(exclude_total_headers: false, data_without_counts: false)
+ header 'X-Per-Page', per_page.to_s
+ header 'X-Page', page.to_s
+ header 'X-Next-Page', next_page.to_s
+ header 'X-Prev-Page', prev_page.to_s
+ header 'Link', pagination_links(data_without_counts)
+
+ return if exclude_total_headers || data_without_counts
+
+ header 'X-Total', total.to_s
+ header 'X-Total-Pages', total_pages.to_s
+ end
+
+ private
+
+ def pagination_links(data_without_counts)
+ [].tap do |links|
+ links << %(<#{page_href(page: prev_page)}>; rel="prev") if prev_page
+ links << %(<#{page_href(page: next_page)}>; rel="next") if next_page
+ links << %(<#{page_href(page: 1)}>; rel="first")
+
+ links << %(<#{page_href(page: total_pages)}>; rel="last") unless data_without_counts
+ end.join(', ')
+ end
+
+ def base_request_uri
+ @base_request_uri ||= URI.parse(request.url).tap do |uri|
+ uri.host = Gitlab.config.gitlab.host
+ uri.port = Gitlab.config.gitlab.port
+ end
+ end
+
+ def build_page_url(query_params:)
+ base_request_uri.tap do |uri|
+ uri.query = query_params
+ end.to_s
+ end
+
+ def page_href(next_page_params = {})
+ query_params = params.merge(**next_page_params, per_page: params[:per_page]).to_query
+
+ build_page_url(query_params: query_params)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/offset_pagination.rb b/lib/gitlab/pagination/offset_pagination.rb
index 46c74b8fe3c..2805b12d95d 100644
--- a/lib/gitlab/pagination/offset_pagination.rb
+++ b/lib/gitlab/pagination/offset_pagination.rb
@@ -48,58 +48,26 @@ module Gitlab
end
def add_pagination_headers(paginated_data, exclude_total_headers)
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
-
- return if exclude_total_headers || data_without_counts?(paginated_data)
-
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', total_pages(paginated_data).to_s
- end
-
- def pagination_links(paginated_data)
- [].tap do |links|
- links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page
- links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page
- links << %(<#{page_href(page: 1)}>; rel="first")
-
- links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data)
- end.join(', ')
- end
-
- def total_pages(paginated_data)
- # Ensure there is in total at least 1 page
- [paginated_data.total_pages, 1].max
+ Gitlab::Pagination::OffsetHeaderBuilder.new(
+ request_context: self, per_page: paginated_data.limit_value, page: paginated_data.current_page,
+ next_page: paginated_data.next_page, prev_page: paginated_data.prev_page,
+ total: total_count(paginated_data), total_pages: total_pages(paginated_data)
+ ).execute(exclude_total_headers: exclude_total_headers, data_without_counts: data_without_counts?(paginated_data))
end
def data_without_counts?(paginated_data)
paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
end
- def base_request_uri
- @base_request_uri ||= URI.parse(request.url).tap do |uri|
- uri.host = Gitlab.config.gitlab.host
- uri.port = Gitlab.config.gitlab.port
- end
+ def total_count(paginated_data)
+ paginated_data.total_count unless data_without_counts?(paginated_data)
end
- def build_page_url(query_params:)
- base_request_uri.tap do |uri|
- uri.query = query_params
- end.to_s
- end
-
- def page_href(next_page_params = {})
- query_params = params.merge(**next_page_params, per_page: per_page).to_query
-
- build_page_url(query_params: query_params)
- end
+ def total_pages(paginated_data)
+ return if data_without_counts?(paginated_data)
- def per_page
- @per_page ||= params[:per_page]
+ # Ensure there is in total at least 1 page
+ [paginated_data.total_pages, 1].max
end
end
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index ad0a5c80604..2ff23980ebd 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -180,12 +180,16 @@ module Gitlab
end
end
- def project_git_route_regex
- @project_git_route_regex ||= /#{project_route_regex}\.git/.freeze
+ def repository_route_regex
+ @repository_route_regex ||= /#{full_namespace_route_regex}|#{personal_snippet_repository_path_regex}/.freeze
end
- def project_wiki_git_route_regex
- @project_wiki_git_route_regex ||= /#{PATH_REGEX_STR}\.wiki/.freeze
+ def repository_git_route_regex
+ @repository_git_route_regex ||= /#{repository_route_regex}\.git/.freeze
+ end
+
+ def repository_wiki_git_route_regex
+ @repository_wiki_git_route_regex ||= /#{full_namespace_route_regex}\.wiki\.git/.freeze
end
def full_namespace_path_regex
@@ -250,10 +254,6 @@ module Gitlab
%r{\A(#{personal_snippet_repository_path_regex}|#{project_snippet_repository_path_regex})\z}
end
- def personal_and_project_snippets_path_regex
- %r{#{personal_snippet_path_regex}|#{project_snippet_path_regex}}
- end
-
def container_image_regex
@container_image_regex ||= %r{([\w\.-]+\/){0,1}[\w\.-]+}.freeze
end
diff --git a/lib/gitlab/performance_bar/logger.rb b/lib/gitlab/performance_bar/logger.rb
new file mode 100644
index 00000000000..a8e2f7d2d4e
--- /dev/null
+++ b/lib/gitlab/performance_bar/logger.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PerformanceBar
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'performance_bar_json'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
index 805283b0f93..bf8d4b202b6 100644
--- a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
+++ b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
@@ -5,7 +5,33 @@ module Gitlab
module PerformanceBar
module RedisAdapterWhenPeekEnabled
def save(request_id)
- super if ::Gitlab::PerformanceBar.enabled_for_request?
+ return unless ::Gitlab::PerformanceBar.enabled_for_request?
+ return if request_id.blank?
+
+ super
+
+ enqueue_stats_job(request_id)
+ end
+
+ # schedules a job which parses peek profile data and adds them
+ # to a structured log
+ def enqueue_stats_job(request_id)
+ return unless gather_stats?
+
+ @client.sadd(GitlabPerformanceBarStatsWorker::STATS_KEY, request_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+ return unless uuid = Gitlab::ExclusiveLease.new(
+ GitlabPerformanceBarStatsWorker::LEASE_KEY,
+ timeout: GitlabPerformanceBarStatsWorker::LEASE_TIMEOUT
+ ).try_obtain
+
+ GitlabPerformanceBarStatsWorker.perform_in(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid)
+ end
+
+ def gather_stats?
+ return unless Feature.enabled?(:performance_bar_stats)
+
+ Gitlab.com? || !Rails.env.production?
end
end
end
diff --git a/lib/gitlab/performance_bar/stats.rb b/lib/gitlab/performance_bar/stats.rb
new file mode 100644
index 00000000000..d1504d88315
--- /dev/null
+++ b/lib/gitlab/performance_bar/stats.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PerformanceBar
+ # This class fetches Peek stats stored in redis and logs them in a
+ # structured log (so these can be then analyzed in Kibana)
+ class Stats
+ def initialize(redis)
+ @redis = redis
+ end
+
+ def process(id)
+ data = request(id)
+ return unless data
+
+ log_sql_queries(id, data)
+ rescue => err
+ logger.error(message: "failed to process request id #{id}: #{err.message}")
+ end
+
+ private
+
+ def request(id)
+ # Peek gem stores request data under peek:requests:request_id key
+ json_data = @redis.get("peek:requests:#{id}")
+ Gitlab::Json.parse(json_data)
+ end
+
+ def log_sql_queries(id, data)
+ return [] unless queries = data.dig('data', 'active-record', 'details')
+
+ queries.each do |query|
+ next unless location = parse_backtrace(query['backtrace'])
+
+ log_info = location.merge(
+ type: :sql,
+ request_id: id,
+ duration_ms: query['duration'].to_f
+ )
+
+ logger.info(log_info)
+ end
+ end
+
+ def parse_backtrace(backtrace)
+ return unless match = /(?<filename>.*):(?<filenum>\d+):in `(?<method>.*)'/.match(backtrace.first)
+
+ {
+ filename: match[:filename],
+ filenum: match[:filenum].to_i,
+ method: match[:method]
+ }
+ end
+
+ def logger
+ @logger ||= Gitlab::PerformanceBar::Logger.build
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index a830f949b21..6ba36fadfa3 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -40,30 +40,30 @@ module Gitlab
# TODO: Review child inheritance of this table (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430928221)
def localized_templates_table
[
- ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'),
- ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'),
- ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'),
- ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'),
+ ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'),
+ ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'),
+ ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'),
+ ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'),
ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'),
- ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'),
- ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'),
- ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'),
- ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'),
- ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'),
- ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'),
- ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'),
- ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'),
+ ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'),
+ ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'),
+ ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby'), 'https://gitlab.com/pages/gatsby'),
+ ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'),
+ ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'),
+ ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML'), 'https://gitlab.com/pages/plain-html'),
+ ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'),
+ ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'),
ProjectTemplate.new('sse_middleman', 'Static Site Editor/Middleman', _('Middleman project with Static Site Editor support'), 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman', 'illustrations/logos/middleman.svg'),
ProjectTemplate.new('gitpod_spring_petclinic', 'Gitpod/Spring Petclinic', _('A Gitpod configured Webapplication in Spring and Java'), 'https://gitlab.com/gitlab-org/project-templates/gitpod-spring-petclinic', 'illustrations/logos/gitpod.svg'),
- ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
+ ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'),
ProjectTemplate.new('jsonnet', 'Jsonnet for Dynamic Child Pipelines', _('An example showing how to use Jsonnet with GitLab dynamic child pipelines'), 'https://gitlab.com/gitlab-org/project-templates/jsonnet'),
- ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management')
+ ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management')
].freeze
end
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
index b0aae363749..1822b0c8bd5 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -102,6 +102,41 @@ module Gitlab
@execution_message[:duplicate] = message
end
+ desc _('Clone this issue')
+ explanation do |project = quick_action_target.project.full_path|
+ _("Clones this issue, without comments, to %{project}.") % { project: project }
+ end
+ params 'path/to/project [--with_notes]'
+ types Issue
+ condition do
+ quick_action_target.persisted? &&
+ current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+ end
+ command :clone do |params = ''|
+ params = params.split(' ')
+ with_notes = params.delete('--with_notes').present?
+
+ # If we have more than 1 param, then the user supplied too many spaces, or mistyped `--with_notes`
+ if params.size > 1
+ @execution_message[:clone] = _('Failed to clone this issue: wrong parameters.')
+ next
+ end
+
+ target_project_path = params[0]
+ target_project = target_project_path.present? ? Project.find_by_full_path(target_project_path) : quick_action_target.project
+
+ if target_project.present?
+ @updates[:target_clone_project] = target_project
+ @updates[:clone_with_notes] = with_notes
+
+ message = _("Cloned this issue to %{path_to_project}.") % { path_to_project: target_project_path || quick_action_target.project.full_path }
+ else
+ message = _("Failed to clone this issue because target project doesn't exist.")
+ end
+
+ @execution_message[:clone] = message
+ end
+
desc _('Move this issue to another project.')
explanation do |path_to_project|
_("Moves this issue to %{path_to_project}.") % { path_to_project: path_to_project }
diff --git a/lib/gitlab/rack_attack.rb b/lib/gitlab/rack_attack.rb
new file mode 100644
index 00000000000..7c336153e32
--- /dev/null
+++ b/lib/gitlab/rack_attack.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+# When adding new user-configurable throttles, remember to update the documentation
+# in doc/user/admin_area/settings/user_and_ip_rate_limits.md
+#
+# Integration specs for throttling can be found in:
+# spec/requests/rack_attack_global_spec.rb
+module Gitlab
+ module RackAttack
+ def self.configure(rack_attack)
+ # This adds some methods used by our throttles to the `Rack::Request`
+ rack_attack::Request.include(Gitlab::RackAttack::Request)
+ # Send the Retry-After header so clients (e.g. python-gitlab) can make good choices about delays
+ Rack::Attack.throttled_response_retry_after_header = true
+ # Configure the throttles
+ configure_throttles(rack_attack)
+
+ configure_user_allowlist
+ end
+
+ def self.configure_user_allowlist
+ @user_allowlist = nil
+ user_allowlist
+ end
+
+ def self.configure_throttles(rack_attack)
+ throttle_or_track(rack_attack, 'throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
+ if !req.should_be_skipped? &&
+ Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
+ req.unauthenticated?
+ req.ip
+ end
+ end
+
+ throttle_or_track(rack_attack, 'throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
+ if req.api_request? &&
+ Gitlab::Throttle.settings.throttle_authenticated_api_enabled
+ req.throttled_user_id([:api])
+ end
+ end
+
+ # Product analytics feature is in experimental stage.
+ # At this point we want to limit amount of events registered
+ # per application (aid stands for application id).
+ throttle_or_track(rack_attack, 'throttle_product_analytics_collector', limit: 100, period: 60) do |req|
+ if req.product_analytics_collector_request?
+ req.params['aid']
+ end
+ end
+
+ throttle_or_track(rack_attack, 'throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
+ if req.web_request? &&
+ Gitlab::Throttle.settings.throttle_authenticated_web_enabled
+ req.throttled_user_id([:api, :rss, :ics])
+ end
+ end
+
+ throttle_or_track(rack_attack, 'throttle_unauthenticated_protected_paths', Gitlab::Throttle.protected_paths_options) do |req|
+ if req.post? &&
+ !req.should_be_skipped? &&
+ req.protected_path? &&
+ Gitlab::Throttle.protected_paths_enabled? &&
+ req.unauthenticated?
+ req.ip
+ end
+ end
+
+ throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req|
+ if req.post? &&
+ req.api_request? &&
+ req.protected_path? &&
+ Gitlab::Throttle.protected_paths_enabled?
+ req.throttled_user_id([:api])
+ end
+ end
+
+ throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req|
+ if req.post? &&
+ req.web_request? &&
+ req.protected_path? &&
+ Gitlab::Throttle.protected_paths_enabled?
+ req.throttled_user_id([:api, :rss, :ics])
+ end
+ end
+
+ rack_attack.safelist('throttle_bypass_header') do |req|
+ Gitlab::Throttle.bypass_header.present? &&
+ req.get_header(Gitlab::Throttle.bypass_header) == '1'
+ end
+ end
+
+ def self.throttle_or_track(rack_attack, throttle_name, *args, &block)
+ if track?(throttle_name)
+ rack_attack.track(throttle_name, *args, &block)
+ else
+ rack_attack.throttle(throttle_name, *args, &block)
+ end
+ end
+
+ def self.track?(name)
+ dry_run_config = ENV['GITLAB_THROTTLE_DRY_RUN'].to_s.strip
+
+ return false if dry_run_config.empty?
+ return true if dry_run_config == '*'
+
+ dry_run_config.split(',').map(&:strip).include?(name)
+ end
+
+ def self.user_allowlist
+ @user_allowlist ||= begin
+ list = UserAllowlist.new(ENV['GITLAB_THROTTLE_USER_ALLOWLIST'])
+ Gitlab::AuthLogger.info(gitlab_throttle_user_allowlist: list.to_a)
+ list
+ end
+ end
+ end
+end
+::Gitlab::RackAttack.prepend_if_ee('::EE::Gitlab::RackAttack')
diff --git a/lib/gitlab/rack_attack/request.rb b/lib/gitlab/rack_attack/request.rb
new file mode 100644
index 00000000000..67e3a5de223
--- /dev/null
+++ b/lib/gitlab/rack_attack/request.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RackAttack
+ module Request
+ def unauthenticated?
+ !(authenticated_user_id([:api, :rss, :ics]) || authenticated_runner_id)
+ end
+
+ def throttled_user_id(request_formats)
+ user_id = authenticated_user_id(request_formats)
+
+ if Gitlab::RackAttack.user_allowlist.include?(user_id)
+ Gitlab::Instrumentation::Throttle.safelist = 'throttle_user_allowlist'
+ return
+ end
+
+ user_id
+ end
+
+ def authenticated_runner_id
+ request_authenticator.runner&.id
+ end
+
+ def api_request?
+ path.start_with?('/api')
+ end
+
+ def api_internal_request?
+ path =~ %r{^/api/v\d+/internal/}
+ end
+
+ def health_check_request?
+ path =~ %r{^/-/(health|liveness|readiness|metrics)}
+ end
+
+ def product_analytics_collector_request?
+ path.start_with?('/-/collector/i')
+ end
+
+ def should_be_skipped?
+ api_internal_request? || health_check_request?
+ end
+
+ def web_request?
+ !api_request? && !health_check_request?
+ end
+
+ def protected_path?
+ !protected_path_regex.nil?
+ end
+
+ def protected_path_regex
+ path =~ protected_paths_regex
+ end
+
+ private
+
+ def authenticated_user_id(request_formats)
+ request_authenticator.user(request_formats)&.id
+ end
+
+ def request_authenticator
+ @request_authenticator ||= Gitlab::Auth::RequestAuthenticator.new(self)
+ end
+
+ def protected_paths
+ Gitlab::CurrentSettings.current_application_settings.protected_paths
+ end
+
+ def protected_paths_regex
+ Regexp.union(protected_paths.map { |path| /\A#{Regexp.escape(path)}/ })
+ end
+ end
+ end
+end
+::Gitlab::RackAttack::Request.prepend_if_ee('::EE::Gitlab::RackAttack::Request')
diff --git a/lib/gitlab/rack_attack/user_allowlist.rb b/lib/gitlab/rack_attack/user_allowlist.rb
new file mode 100644
index 00000000000..f3043f44091
--- /dev/null
+++ b/lib/gitlab/rack_attack/user_allowlist.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'set'
+
+module Gitlab
+ module RackAttack
+ class UserAllowlist
+ extend Forwardable
+
+ def_delegators :@set, :empty?, :include?, :to_a
+
+ def initialize(list)
+ @set = Set.new
+
+ list.to_s.split(',').each do |id|
+ @set << Integer(id) unless id.blank?
+ rescue ArgumentError
+ Gitlab::AuthLogger.error(message: 'ignoring invalid user allowlist entry', entry: id)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 9ee6f67e455..46c84107e0f 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -4,8 +4,15 @@ module Gitlab
module RepoPath
NotFoundError = Class.new(StandardError)
+ # Returns an array containing:
+ # - The repository container
+ # - The related project (if available)
+ # - The repository type
+ # - The original container path (if redirected)
+ #
+ # @returns [HasRepository, Project, String, String]
def self.parse(path)
- repo_path = path.sub(/\.git\z/, '').sub(%r{\A/}, '')
+ repo_path = path.delete_prefix('/').delete_suffix('.git')
redirected_path = nil
# Detect the repo type based on the path, the first one tried is the project
@@ -30,7 +37,15 @@ module Gitlab
[nil, nil, Gitlab::GlRepository.default_type, nil]
end
+ # Returns an array containing:
+ # - The repository container
+ # - The related project (if available)
+ # - The original container path (if redirected)
+ #
+ # @returns [HasRepository, Project, String]
def self.find_container(type, full_path)
+ return [nil, nil, nil] if full_path.blank?
+
if type.snippet?
snippet, redirected_path = find_snippet(full_path)
@@ -47,26 +62,24 @@ module Gitlab
end
def self.find_project(project_path)
- return [nil, nil] if project_path.blank?
-
project = Project.find_by_full_path(project_path, follow_redirects: true)
- redirected_path = redirected?(project, project_path) ? project_path : nil
+ redirected_path = project_path if redirected?(project, project_path)
[project, redirected_path]
end
- def self.redirected?(project, project_path)
- project && project.full_path.casecmp(project_path) != 0
+ def self.redirected?(container, container_path)
+ container && container.full_path.casecmp(container_path) != 0
end
# Snippet_path can be either:
# - snippets/1
# - h5bp/html5-boilerplate/snippets/53
def self.find_snippet(snippet_path)
- return [nil, nil] if snippet_path.blank?
-
snippet_id, project_path = extract_snippet_info(snippet_path)
- project, redirected_path = find_project(project_path)
+ return [nil, nil] unless snippet_id
+
+ project, redirected_path = find_project(project_path) if project_path
[Snippet.find_by_id_and_project(id: snippet_id, project: project), redirected_path]
end
@@ -74,19 +87,23 @@ module Gitlab
# Wiki path can be either:
# - namespace/project
# - group/subgroup/project
- def self.find_wiki(wiki_path)
- return [nil, nil] if wiki_path.blank?
-
- project, redirected_path = find_project(wiki_path)
-
- [project&.wiki, redirected_path]
+ #
+ # And also in EE:
+ # - group
+ # - group/subgroup
+ def self.find_wiki(container_path)
+ container = Routable.find_by_full_path(container_path, follow_redirects: true)
+ redirected_path = container_path if redirected?(container, container_path)
+
+ # In CE, Group#wiki is not available so this will return nil for a group path.
+ [container&.try(:wiki), redirected_path]
end
def self.extract_snippet_info(snippet_path)
path_segments = snippet_path.split('/')
snippet_id = path_segments.pop
- path_segments.pop # Remove snippets from path
- project_path = File.join(path_segments)
+ path_segments.pop # Remove 'snippets' from path
+ project_path = File.join(path_segments).presence
[snippet_id, project_path]
end
diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb
index b1e478093d3..79562a8223b 100644
--- a/lib/gitlab/request_forgery_protection.rb
+++ b/lib/gitlab/request_forgery_protection.rb
@@ -9,14 +9,6 @@ module Gitlab
class Controller < ActionController::Base
protect_from_forgery with: :exception, prepend: true
- rescue_from ActionController::InvalidAuthenticityToken do |e|
- logger.warn "This CSRF token verification failure is handled internally by `GitLab::RequestForgeryProtection`"
- logger.warn "Unlike the logs may suggest, this does not result in an actual 422 response to the user"
- logger.warn "For API requests, the only effect is that `current_user` will be `nil` for the duration of the request"
-
- raise e
- end
-
def index
head :ok
end
diff --git a/lib/gitlab/sample_data_template.rb b/lib/gitlab/sample_data_template.rb
index ae74dc710b7..06ea53e4018 100644
--- a/lib/gitlab/sample_data_template.rb
+++ b/lib/gitlab/sample_data_template.rb
@@ -5,8 +5,7 @@ module Gitlab
class << self
def localized_templates_table
[
- SampleDataTemplate.new('basic', 'Basic', _('Basic Sample Data template with Issues, Merge Requests and Milestones.'), 'https://gitlab.com/gitlab-org/sample-data-templates/basic'),
- SampleDataTemplate.new('serenity_valley', 'Serenity Valley', _('Serenity Valley Sample Data template.'), 'https://gitlab.com/gitlab-org/sample-data-templates/serenity-valley')
+ SampleDataTemplate.new('sample', 'Sample GitLab Project', _('Get started with a project that follows best practices for setting up GitLab for your own organization, including sample Issues, Merge Requests, and Milestones'), 'https://gitlab.com/gitlab-org/sample-data-templates/sample-gitlab-project')
].freeze
end
diff --git a/lib/gitlab/sanitizers/exif.rb b/lib/gitlab/sanitizers/exif.rb
index 78c517c49d8..ed3e32f3e79 100644
--- a/lib/gitlab/sanitizers/exif.rb
+++ b/lib/gitlab/sanitizers/exif.rb
@@ -67,7 +67,7 @@ module Gitlab
batch_size: 1000
}
- relation.find_each(find_params) do |upload|
+ relation.find_each(**find_params) do |upload|
clean(upload.retrieve_uploader, dry_run: dry_run)
sleep sleep_time if sleep_time
rescue => err
diff --git a/lib/gitlab/search/query.rb b/lib/gitlab/search/query.rb
index 27ea0b7367f..5b1f9400bc7 100644
--- a/lib/gitlab/search/query.rb
+++ b/lib/gitlab/search/query.rb
@@ -51,6 +51,7 @@ module Gitlab
end
query = (@raw_query.split - fragments).join(' ')
+ query = '*' if query.empty?
[query, filters]
end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 259d3e300b6..48f204e0b86 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -4,10 +4,10 @@ require 'toml-rb'
module Gitlab
module SetupHelper
- def create_configuration(dir, storage_paths, force: false)
+ def create_configuration(dir, storage_paths, force: false, options: {})
generate_configuration(
- configuration_toml(dir, storage_paths),
- get_config_path(dir),
+ configuration_toml(dir, storage_paths, options),
+ get_config_path(dir, options),
force: force
)
end
@@ -31,7 +31,7 @@ module Gitlab
module Workhorse
extend Gitlab::SetupHelper
class << self
- def configuration_toml(dir, _)
+ def configuration_toml(dir, _, _)
config = { redis: { URL: redis_url } }
TomlRB.dump(config)
@@ -41,8 +41,26 @@ module Gitlab
Gitlab::Redis::SharedState.url
end
- def get_config_path(dir)
- File.join(dir, 'config.toml')
+ def get_config_path(dir, _)
+ File.join(dir, 'config_path')
+ end
+
+ def compile_into(dir)
+ command = %W[#{make} -C #{Rails.root.join('workhorse')} install PREFIX=#{File.absolute_path(dir)}]
+
+ make_out, make_status = Gitlab::Popen.popen(command)
+ unless make_status == 0
+ warn make_out
+ raise 'workhorse make failed'
+ end
+
+ # 'make install' puts the binaries in #{dir}/bin but the init script expects them in dir
+ FileUtils.mv(Dir["#{dir}/bin/*"], dir)
+ end
+
+ def make
+ _, which_status = Gitlab::Popen.popen(%w[which gmake])
+ which_status == 0 ? 'gmake' : 'make'
end
end
end
@@ -58,7 +76,7 @@ module Gitlab
# because it uses a Unix socket.
# For development and testing purposes, an extra storage is added to gitaly,
# which is not known to Rails, but must be explicitly stubbed.
- def configuration_toml(gitaly_dir, storage_paths, gitaly_ruby: true)
+ def configuration_toml(gitaly_dir, storage_paths, options, gitaly_ruby: true)
storages = []
address = nil
@@ -79,14 +97,20 @@ module Gitlab
config = { socket_path: address.sub(/\Aunix:/, '') }
if Rails.env.test?
+ socket_filename = options[:gitaly_socket] || "gitaly.socket"
+
+ config = {
+ # Override the set gitaly_address since Praefect is in the loop
+ socket_path: File.join(gitaly_dir, socket_filename),
+ auth: { token: 'secret' },
+ # Compared to production, tests run in constrained environments. This
+ # number is meant to grow with the number of concurrent rails requests /
+ # sidekiq jobs, and concurrency will be low anyway in test.
+ git: { catfile_cache_size: 5 }
+ }
+
storage_path = Rails.root.join('tmp', 'tests', 'second_storage').to_s
storages << { name: 'test_second_storage', path: storage_path }
-
- config[:auth] = { token: 'secret' }
- # Compared to production, tests run in constrained environments. This
- # number is meant to grow with the number of concurrent rails requests /
- # sidekiq jobs, and concurrency will be low anyway in test.
- config[:git] = { catfile_cache_size: 5 }
end
config[:storage] = storages
@@ -106,8 +130,9 @@ module Gitlab
private
- def get_config_path(dir)
- File.join(dir, 'config.toml')
+ def get_config_path(dir, options)
+ config_filename = options[:config_filename] || 'config.toml'
+ File.join(dir, config_filename)
end
end
end
@@ -115,9 +140,11 @@ module Gitlab
module Praefect
extend Gitlab::SetupHelper
class << self
- def configuration_toml(gitaly_dir, storage_paths)
+ def configuration_toml(gitaly_dir, _, _)
nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }]
- storages = [{ name: 'default', node: nodes }]
+ second_storage_nodes = [{ storage: 'test_second_storage', address: "unix:#{gitaly_dir}/gitaly2.socket", primary: true, token: 'secret' }]
+
+ storages = [{ name: 'default', node: nodes }, { name: 'test_second_storage', node: second_storage_nodes }]
failover = { enabled: false }
config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages, failover: failover }
config[:token] = 'secret' if Rails.env.test?
@@ -127,7 +154,7 @@ module Gitlab
private
- def get_config_path(dir)
+ def get_config_path(dir, _)
File.join(dir, 'praefect.config.toml')
end
end
diff --git a/lib/gitlab/sidekiq_cluster.rb b/lib/gitlab/sidekiq_cluster.rb
index d05c717d2fa..cc1bd282da8 100644
--- a/lib/gitlab/sidekiq_cluster.rb
+++ b/lib/gitlab/sidekiq_cluster.rb
@@ -111,7 +111,7 @@ module Gitlab
end
def self.count_by_queue(queues)
- queues.each_with_object(Hash.new(0)) { |element, hash| hash[element] += 1 }
+ queues.tally
end
def self.proc_details(counts)
diff --git a/lib/gitlab/sidekiq_death_handler.rb b/lib/gitlab/sidekiq_death_handler.rb
new file mode 100644
index 00000000000..f86d9f17b5f
--- /dev/null
+++ b/lib/gitlab/sidekiq_death_handler.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqDeathHandler
+ class << self
+ include ::Gitlab::SidekiqMiddleware::MetricsHelper
+
+ def handler(job, _exception)
+ labels = create_labels(job['class'].constantize, job['queue'])
+
+ counter.increment(labels)
+ end
+
+ def counter
+ @counter ||= ::Gitlab::Metrics.counter(:sidekiq_jobs_dead_total, 'Sidekiq dead jobs')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/client_metrics.rb b/lib/gitlab/sidekiq_middleware/client_metrics.rb
index 245a1b5e024..7ee8a623d30 100644
--- a/lib/gitlab/sidekiq_middleware/client_metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/client_metrics.rb
@@ -2,7 +2,9 @@
module Gitlab
module SidekiqMiddleware
- class ClientMetrics < SidekiqMiddleware::Metrics
+ class ClientMetrics
+ include ::Gitlab::SidekiqMiddleware::MetricsHelper
+
ENQUEUED = :sidekiq_enqueued_jobs_total
def initialize
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index 5efd1b34d32..79ac853ea0c 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -70,10 +70,6 @@ module Gitlab
jid != existing_jid
end
- def droppable?
- idempotent? && ::Feature.disabled?("disable_#{queue_name}_deduplication", type: :ops)
- end
-
def scheduled_at
job['at']
end
@@ -85,6 +81,13 @@ module Gitlab
worker_klass.get_deduplication_options
end
+ def idempotent?
+ return false unless worker_klass
+ return false unless worker_klass.respond_to?(:idempotent?)
+
+ worker_klass.idempotent?
+ end
+
private
attr_reader :queue_name, :job
@@ -128,13 +131,6 @@ module Gitlab
def idempotency_string
"#{worker_class_name}:#{arguments.join('-')}"
end
-
- def idempotent?
- return false unless worker_klass
- return false unless worker_klass.respond_to?(:idempotent?)
-
- worker_klass.idempotent?
- end
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
index 59b0e7e29da..469033a5e52 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
@@ -13,7 +13,7 @@ module Gitlab
if deduplicatable_job? && check! && duplicate_job.duplicate?
job['duplicate-of'] = duplicate_job.existing_jid
- if duplicate_job.droppable?
+ if duplicate_job.idempotent?
Gitlab::SidekiqLogging::DeduplicationLogger.instance.log(
job, "dropped #{strategy_name}", duplicate_job.options)
return false
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics_helper.rb
index 7ae8995c46d..5c1ce2b98e8 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics_helper.rb
@@ -2,7 +2,7 @@
module Gitlab
module SidekiqMiddleware
- class Metrics
+ module MetricsHelper
TRUE_LABEL = "yes"
FALSE_LABEL = "no"
diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb
index 0635c07ae4b..7f3048f4c6e 100644
--- a/lib/gitlab/sidekiq_middleware/server_metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb
@@ -2,7 +2,9 @@
module Gitlab
module SidekiqMiddleware
- class ServerMetrics < SidekiqMiddleware::Metrics
+ class ServerMetrics
+ include ::Gitlab::SidekiqMiddleware::MetricsHelper
+
# SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq
# timeframes than the DEFAULT_BUCKET definition. Defined in seconds.
SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze
diff --git a/lib/gitlab/throttle.rb b/lib/gitlab/throttle.rb
new file mode 100644
index 00000000000..aebf8d92cb3
--- /dev/null
+++ b/lib/gitlab/throttle.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class Throttle
+ def self.settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ # Returns true if we should use the Admin Area protected paths throttle
+ def self.protected_paths_enabled?
+ self.settings.throttle_protected_paths_enabled?
+ end
+
+ def self.omnibus_protected_paths_present?
+ Rack::Attack.throttles.key?('protected paths')
+ end
+
+ def self.bypass_header
+ env_value = ENV['GITLAB_THROTTLE_BYPASS_HEADER']
+ return unless env_value.present?
+
+ "HTTP_#{env_value.upcase.tr('-', '_')}"
+ end
+
+ def self.unauthenticated_options
+ limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period }
+ period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+
+ def self.authenticated_api_options
+ limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period }
+ period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+
+ def self.authenticated_web_options
+ limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period }
+ period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+
+ def self.protected_paths_options
+ limit_proc = proc { |req| settings.throttle_protected_paths_requests_per_period }
+ period_proc = proc { |req| settings.throttle_protected_paths_period_in_seconds.seconds }
+
+ { limit: limit_proc, period: period_proc }
+ end
+ end
+end
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index 19be468e3d5..618e359211b 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -14,8 +14,8 @@ module Gitlab
Gitlab::Tracking.event(category, action.to_s, **args)
end
- def track_self_describing_event(schema_url, event_data_json, **args)
- Gitlab::Tracking.self_describing_event(schema_url, event_data_json, **args)
+ def track_self_describing_event(schema_url, data:, **args)
+ Gitlab::Tracking.self_describing_event(schema_url, data: data, **args)
end
end
@@ -26,10 +26,11 @@ module Gitlab
def event(category, action, label: nil, property: nil, value: nil, context: nil)
snowplow.event(category, action, label: label, property: property, value: value, context: context)
+ product_analytics.event(category, action, label: label, property: property, value: value, context: context)
end
- def self_describing_event(schema_url, event_data_json, context: nil)
- snowplow.self_describing_event(schema_url, event_data_json, context: context)
+ def self_describing_event(schema_url, data:, context: nil)
+ snowplow.self_describing_event(schema_url, data: data, context: context)
end
def snowplow_options(group)
@@ -49,6 +50,10 @@ module Gitlab
def snowplow
@snowplow ||= Gitlab::Tracking::Destinations::Snowplow.new
end
+
+ def product_analytics
+ @product_analytics ||= Gitlab::Tracking::Destinations::ProductAnalytics.new
+ end
end
end
end
diff --git a/lib/gitlab/tracking/destinations/product_analytics.rb b/lib/gitlab/tracking/destinations/product_analytics.rb
new file mode 100644
index 00000000000..cacedbc5b83
--- /dev/null
+++ b/lib/gitlab/tracking/destinations/product_analytics.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Tracking
+ module Destinations
+ class ProductAnalytics < Base
+ extend ::Gitlab::Utils::Override
+ include ::Gitlab::Utils::StrongMemoize
+
+ override :event
+ def event(category, action, label: nil, property: nil, value: nil, context: nil)
+ return unless event_allowed?(category, action)
+ return unless enabled?
+
+ tracker.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i)
+ end
+
+ private
+
+ def event_allowed?(category, action)
+ category == 'epics' && action == 'promote'
+ end
+
+ def enabled?
+ Feature.enabled?(:product_analytics_tracking, type: :ops) &&
+ Gitlab::CurrentSettings.usage_ping_enabled? &&
+ Gitlab::CurrentSettings.self_monitoring_project_id.present?
+ end
+
+ def tracker
+ @tracker ||= SnowplowTracker::Tracker.new(
+ SnowplowTracker::AsyncEmitter.new(::ProductAnalytics::Tracker::COLLECTOR_URL, protocol: Gitlab.config.gitlab.protocol),
+ SnowplowTracker::Subject.new,
+ Gitlab::Tracking::SNOWPLOW_NAMESPACE,
+ Gitlab::CurrentSettings.self_monitoring_project_id.to_s
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/tracking/destinations/snowplow.rb b/lib/gitlab/tracking/destinations/snowplow.rb
index 9cebcfe5ee1..4fa844de325 100644
--- a/lib/gitlab/tracking/destinations/snowplow.rb
+++ b/lib/gitlab/tracking/destinations/snowplow.rb
@@ -15,10 +15,10 @@ module Gitlab
tracker.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i)
end
- def self_describing_event(schema_url, event_data_json, context: nil)
+ def self_describing_event(schema_url, data:, context: nil)
return unless enabled?
- event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, event_data_json)
+ event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, data)
tracker.track_self_describing_event(event_json, context, (Time.now.to_f * 1000).to_i)
end
diff --git a/lib/gitlab/uploads/migration_helper.rb b/lib/gitlab/uploads/migration_helper.rb
index 9377ccfec1e..b610d2a10c6 100644
--- a/lib/gitlab/uploads/migration_helper.rb
+++ b/lib/gitlab/uploads/migration_helper.rb
@@ -75,3 +75,5 @@ module Gitlab
end
end
end
+
+Gitlab::Uploads::MigrationHelper.prepend_if_ee('EE::Gitlab::Uploads::MigrationHelper')
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 4b0dd54683b..f935c677930 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -47,7 +47,6 @@ module Gitlab
.merge(system_usage_data_weekly)
.merge(features_usage_data)
.merge(components_usage_data)
- .merge(cycle_analytics_usage_data)
.merge(object_store_usage_data)
.merge(topology_usage_data)
.merge(usage_activity_by_stage)
@@ -237,7 +236,9 @@ module Gitlab
def system_usage_data_settings
{
- settings: {}
+ settings: {
+ ldap_encrypted_secrets_enabled: alt_usage_data(fallback: nil) { Gitlab::Auth::Ldap::Config.encrypted_secrets.active? }
+ }
}
end
@@ -250,12 +251,6 @@ module Gitlab
}
end
- def cycle_analytics_usage_data
- Gitlab::CycleAnalytics::UsageData.new.to_json
- rescue ActiveRecord::StatementInvalid
- { avg_cycle_analytics: {} }
- end
-
# rubocop:disable CodeReuse/ActiveRecord
def grafana_embed_usage_data
count(Issue.joins('JOIN grafana_integrations USING (project_id)')
@@ -296,20 +291,7 @@ module Gitlab
# @return [Array<#totals>] An array of objects that respond to `#totals`
def usage_data_counters
- [
- Gitlab::UsageDataCounters::WikiPageCounter,
- Gitlab::UsageDataCounters::WebIdeCounter,
- Gitlab::UsageDataCounters::NoteCounter,
- Gitlab::UsageDataCounters::SnippetCounter,
- Gitlab::UsageDataCounters::SearchCounter,
- Gitlab::UsageDataCounters::CycleAnalyticsCounter,
- Gitlab::UsageDataCounters::ProductivityAnalyticsCounter,
- Gitlab::UsageDataCounters::SourceCodeCounter,
- Gitlab::UsageDataCounters::MergeRequestCounter,
- Gitlab::UsageDataCounters::DesignsCounter,
- Gitlab::UsageDataCounters::KubernetesAgentCounter,
- Gitlab::UsageDataCounters::StaticSiteEditorCounter
- ]
+ Gitlab::UsageDataCounters.counters
end
def components_usage_data
@@ -602,7 +584,7 @@ module Gitlab
gitlab: distinct_count(::BulkImport.where(time_period, source_type: :gitlab), :user_id)
},
projects_imported: {
- total: count(Project.where(time_period).where.not(import_type: nil)),
+ total: distinct_count(::Project.where(time_period).where.not(import_type: nil), :creator_id),
gitlab_project: projects_imported_count('gitlab_project', time_period),
gitlab: projects_imported_count('gitlab', time_period),
github: projects_imported_count('github', time_period),
@@ -707,16 +689,12 @@ module Gitlab
end
def aggregated_metrics_monthly
- return {} unless Feature.enabled?(:product_analytics_aggregated_metrics)
-
{
aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_monthly_data
}
end
def aggregated_metrics_weekly
- return {} unless Feature.enabled?(:product_analytics_aggregated_metrics)
-
{
aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_weekly_data
}
@@ -783,12 +761,13 @@ module Gitlab
action_monthly_active_users_web_ide_edit: redis_usage_data { counter.count_web_ide_edit_actions(**date_range) },
action_monthly_active_users_sfe_edit: redis_usage_data { counter.count_sfe_edit_actions(**date_range) },
action_monthly_active_users_snippet_editor_edit: redis_usage_data { counter.count_snippet_editor_edit_actions(**date_range) },
+ action_monthly_active_users_sse_edit: redis_usage_data { counter.count_sse_edit_actions(**date_range) },
action_monthly_active_users_ide_edit: redis_usage_data { counter.count_edit_using_editor(**date_range) }
}
end
def report_snowplow_events?
- self_monitoring_project && Feature.enabled?(:product_analytics, self_monitoring_project)
+ self_monitoring_project && Feature.enabled?(:product_analytics_tracking, type: :ops)
end
def distinct_count_service_desk_enabled_projects(time_period)
@@ -915,7 +894,7 @@ module Gitlab
end
def projects_imported_count(from, time_period)
- distinct_count(::Project.imported_from(from).where(time_period), :creator_id) # rubocop: disable CodeReuse/ActiveRecord
+ distinct_count(::Project.imported_from(from).where(time_period).where.not(import_type: nil), :creator_id) # rubocop: disable CodeReuse/ActiveRecord
end
# rubocop:disable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/usage_data_counters.rb b/lib/gitlab/usage_data_counters.rb
new file mode 100644
index 00000000000..ca7699e64e1
--- /dev/null
+++ b/lib/gitlab/usage_data_counters.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ COUNTERS = [
+ GuestPackageEventCounter,
+ WikiPageCounter,
+ WebIdeCounter,
+ NoteCounter,
+ SnippetCounter,
+ SearchCounter,
+ CycleAnalyticsCounter,
+ ProductivityAnalyticsCounter,
+ SourceCodeCounter,
+ MergeRequestCounter,
+ DesignsCounter,
+ KubernetesAgentCounter,
+ StaticSiteEditorCounter
+ ].freeze
+
+ UsageDataCounterError = Class.new(StandardError)
+ UnknownEvent = Class.new(UsageDataCounterError)
+
+ class << self
+ def counters
+ self::COUNTERS
+ end
+
+ def count(event_name)
+ counters.each do |counter|
+ event = counter.fetch_supported_event(event_name)
+
+ return counter.count(event) if event
+ end
+
+ raise UnknownEvent, "Cannot find counter for event #{event_name}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml b/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml
index 97ec8423b95..b7c0abae227 100644
--- a/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml
+++ b/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml
@@ -11,7 +11,31 @@
- name: product_analytics_test_metrics_union
operator: OR
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']
- feature_flag: product_analytics_aggregated_metrics
- name: product_analytics_test_metrics_intersection
operator: AND
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']
+- name: incident_management_alerts_total_unique_counts
+ operator: OR
+ events: [
+ 'incident_management_alert_status_changed',
+ 'incident_management_alert_assigned',
+ 'incident_management_alert_todo',
+ 'incident_management_alert_create_incident'
+ ]
+ feature_flag: usage_data_incident_management_alerts_total_unique_counts
+- name: incident_management_incidents_total_unique_counts
+ operator: OR
+ events: [
+ 'incident_management_incident_created',
+ 'incident_management_incident_reopened',
+ 'incident_management_incident_closed',
+ 'incident_management_incident_assigned',
+ 'incident_management_incident_todo',
+ 'incident_management_incident_comment',
+ 'incident_management_incident_zoom_meeting',
+ 'incident_management_incident_published',
+ 'incident_management_incident_relate',
+ 'incident_management_incident_unrelate',
+ 'incident_management_incident_change_confidential'
+ ]
+ feature_flag: usage_data_incident_management_incidents_total_unique_counts
diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb
index 44893645cc2..d28fd17a989 100644
--- a/lib/gitlab/usage_data_counters/base_counter.rb
+++ b/lib/gitlab/usage_data_counters/base_counter.rb
@@ -29,6 +29,12 @@ module Gitlab::UsageDataCounters
known_events.map { |event| [counter_key(event), -1] }.to_h
end
+ def fetch_supported_event(event_name)
+ return if prefix.present? && !event_name.start_with?(prefix)
+
+ known_events.find { |event| counter_key(event) == event_name.to_sym }
+ end
+
private
def require_known_event(event)
diff --git a/lib/gitlab/usage_data_counters/counter_events/guest_package_events.yml b/lib/gitlab/usage_data_counters/counter_events/guest_package_events.yml
new file mode 100644
index 00000000000..a9b9f8ea235
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/counter_events/guest_package_events.yml
@@ -0,0 +1,34 @@
+---
+- i_package_composer_guest_delete
+- i_package_composer_guest_pull
+- i_package_composer_guest_push
+- i_package_conan_guest_delete
+- i_package_conan_guest_pull
+- i_package_conan_guest_push
+- i_package_container_guest_delete
+- i_package_container_guest_pull
+- i_package_container_guest_push
+- i_package_debian_guest_delete
+- i_package_debian_guest_pull
+- i_package_debian_guest_push
+- i_package_generic_guest_delete
+- i_package_generic_guest_pull
+- i_package_generic_guest_push
+- i_package_golang_guest_delete
+- i_package_golang_guest_pull
+- i_package_golang_guest_push
+- i_package_maven_guest_delete
+- i_package_maven_guest_pull
+- i_package_maven_guest_push
+- i_package_npm_guest_delete
+- i_package_npm_guest_pull
+- i_package_npm_guest_push
+- i_package_nuget_guest_delete
+- i_package_nuget_guest_pull
+- i_package_nuget_guest_push
+- i_package_pypi_guest_delete
+- i_package_pypi_guest_pull
+- i_package_pypi_guest_push
+- i_package_tag_guest_delete
+- i_package_tag_guest_pull
+- i_package_tag_guest_push
diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
index b68d50ee419..eeb26c11bfa 100644
--- a/lib/gitlab/usage_data_counters/editor_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
@@ -6,6 +6,7 @@ module Gitlab
EDIT_BY_SNIPPET_EDITOR = 'g_edit_by_snippet_ide'
EDIT_BY_SFE = 'g_edit_by_sfe'
EDIT_BY_WEB_IDE = 'g_edit_by_web_ide'
+ EDIT_BY_SSE = 'g_edit_by_sse'
EDIT_CATEGORY = 'ide_edit'
class << self
@@ -38,6 +39,14 @@ module Gitlab
count_unique(events, date_from, date_to)
end
+ def track_sse_edit_action(author:, time: Time.zone.now)
+ track_unique_action(EDIT_BY_SSE, author, time)
+ end
+
+ def count_sse_edit_actions(date_from:, date_to:)
+ count_unique(EDIT_BY_SSE, date_from, date_to)
+ end
+
private
def track_unique_action(action, author, time)
diff --git a/lib/gitlab/usage_data_counters/guest_package_event_counter.rb b/lib/gitlab/usage_data_counters/guest_package_event_counter.rb
new file mode 100644
index 00000000000..a9bcbfadda2
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/guest_package_event_counter.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ class GuestPackageEventCounter < BaseCounter
+ KNOWN_EVENTS_PATH = File.expand_path('counter_events/guest_package_events.yml', __dir__)
+ KNOWN_EVENTS = YAML.safe_load(File.read(KNOWN_EVENTS_PATH)).freeze
+ PREFIX = 'package_guest'
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/guest_package_events.yml b/lib/gitlab/usage_data_counters/guest_package_events.yml
new file mode 100644
index 00000000000..a9b9f8ea235
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/guest_package_events.yml
@@ -0,0 +1,34 @@
+---
+- i_package_composer_guest_delete
+- i_package_composer_guest_pull
+- i_package_composer_guest_push
+- i_package_conan_guest_delete
+- i_package_conan_guest_pull
+- i_package_conan_guest_push
+- i_package_container_guest_delete
+- i_package_container_guest_pull
+- i_package_container_guest_push
+- i_package_debian_guest_delete
+- i_package_debian_guest_pull
+- i_package_debian_guest_push
+- i_package_generic_guest_delete
+- i_package_generic_guest_pull
+- i_package_generic_guest_push
+- i_package_golang_guest_delete
+- i_package_golang_guest_pull
+- i_package_golang_guest_push
+- i_package_maven_guest_delete
+- i_package_maven_guest_pull
+- i_package_maven_guest_push
+- i_package_npm_guest_delete
+- i_package_npm_guest_pull
+- i_package_npm_guest_push
+- i_package_nuget_guest_delete
+- i_package_nuget_guest_pull
+- i_package_nuget_guest_push
+- i_package_pypi_guest_delete
+- i_package_pypi_guest_pull
+- i_package_pypi_guest_push
+- i_package_tag_guest_delete
+- i_package_tag_guest_pull
+- i_package_tag_guest_push
diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
index da013a06777..0fed8e1c211 100644
--- a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
@@ -18,6 +18,7 @@ module Gitlab
ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced'
ISSUE_MOVED = 'g_project_management_issue_moved'
ISSUE_RELATED = 'g_project_management_issue_related'
+ ISSUE_CLONED = 'g_project_management_issue_cloned'
ISSUE_UNRELATED = 'g_project_management_issue_unrelated'
ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate'
ISSUE_LOCKED = 'g_project_management_issue_locked'
@@ -137,6 +138,10 @@ module Gitlab
track_unique_action(ISSUE_COMMENT_REMOVED, author, time)
end
+ def track_issue_cloned_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_CLONED, author, time)
+ end
+
private
def track_unique_action(action, author, time)
diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml
index 85f16ea807b..25cf388aedf 100644
--- a/lib/gitlab/usage_data_counters/known_events/common.yml
+++ b/lib/gitlab/usage_data_counters/known_events/common.yml
@@ -118,6 +118,12 @@
expiry: 29
aggregation: daily
feature_flag: track_editor_edit_actions
+- name: g_edit_by_sse
+ category: ide_edit
+ redis_slot: edit
+ expiry: 29
+ aggregation: daily
+ feature_flag: track_editor_edit_actions
- name: g_edit_by_snippet_ide
category: ide_edit
redis_slot: edit
@@ -145,6 +151,7 @@
- name: design_action
category: source_code
aggregation: daily
+ feature_flag: usage_data_design_action
- name: project_action
category: source_code
aggregation: daily
@@ -229,6 +236,12 @@
category: incident_management
aggregation: weekly
feature_flag: usage_data_incident_management_incident_change_confidential
+# Incident management alerts
+- name: incident_management_alert_create_incident
+ redis_slot: incident_management
+ category: incident_management_alerts
+ aggregation: weekly
+ feature_flag: usage_data_incident_management_alert_create_incident
# Testing category
- name: i_testing_test_case_parsed
category: testing
@@ -396,9 +409,19 @@
redis_slot: project_management
aggregation: daily
feature_flag: track_issue_activity_actions
+- name: g_project_management_issue_cloned
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+ feature_flag: track_issue_activity_actions
# Secrets Management
- name: i_ci_secrets_management_vault_build_created
category: ci_secrets_management
redis_slot: ci_secrets_management
aggregation: weekly
feature_flag: usage_data_i_ci_secrets_management_vault_build_created
+- name: i_snippets_show
+ category: snippets
+ redis_slot: snippets
+ aggregation: weekly
+ feature_flag: usage_data_i_snippets_show
diff --git a/lib/gitlab/usage_data_counters/known_events/package_events.yml b/lib/gitlab/usage_data_counters/known_events/package_events.yml
index 7ed02aa2a85..4c3138dc000 100644
--- a/lib/gitlab/usage_data_counters/known_events/package_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/package_events.yml
@@ -1,265 +1,331 @@
---
-- name: i_package_maven_user_push
- category: maven_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_maven_deploy_token_push
- category: maven_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_maven_user_delete
- category: maven_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_maven_deploy_token_delete
- category: maven_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_maven_user_pull
- category: maven_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_maven_deploy_token_pull
- category: maven_packages
+- name: i_package_composer_deploy_token_delete
+ category: composer_packages
aggregation: weekly
redis_slot: package
-- name: i_package_npm_user_push
- category: npm_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_composer_deploy_token_pull
+ category: composer_packages
aggregation: weekly
redis_slot: package
-- name: i_package_npm_deploy_token_push
- category: npm_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_composer_deploy_token_push
+ category: composer_packages
aggregation: weekly
redis_slot: package
-- name: i_package_npm_user_delete
- category: npm_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_composer_user_delete
+ category: composer_packages
aggregation: weekly
redis_slot: package
-- name: i_package_npm_deploy_token_delete
- category: npm_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_composer_user_pull
+ category: composer_packages
aggregation: weekly
redis_slot: package
-- name: i_package_npm_user_pull
- category: npm_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_composer_user_push
+ category: composer_packages
aggregation: weekly
redis_slot: package
-- name: i_package_npm_deploy_token_pull
- category: npm_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_conan_deploy_token_delete
+ category: conan_packages
aggregation: weekly
redis_slot: package
-- name: i_package_conan_user_push
+ feature_flag: collect_package_events_redis
+- name: i_package_conan_deploy_token_pull
category: conan_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_conan_deploy_token_push
category: conan_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_conan_user_delete
category: conan_packages
aggregation: weekly
redis_slot: package
-- name: i_package_conan_deploy_token_delete
- category: conan_packages
- aggregation: weekly
- redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_conan_user_pull
category: conan_packages
aggregation: weekly
redis_slot: package
-- name: i_package_conan_deploy_token_pull
+ feature_flag: collect_package_events_redis
+- name: i_package_conan_user_push
category: conan_packages
aggregation: weekly
redis_slot: package
-- name: i_package_nuget_user_push
- category: nuget_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_nuget_deploy_token_push
- category: nuget_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_nuget_user_delete
- category: nuget_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_nuget_deploy_token_delete
- category: nuget_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_nuget_user_pull
- category: nuget_packages
- aggregation: weekly
- redis_slot: package
-- name: i_package_nuget_deploy_token_pull
- category: nuget_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_container_deploy_token_delete
+ category: container_packages
aggregation: weekly
redis_slot: package
-- name: i_package_pypi_user_push
- category: pypi_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_container_deploy_token_pull
+ category: container_packages
aggregation: weekly
redis_slot: package
-- name: i_package_pypi_deploy_token_push
- category: pypi_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_container_deploy_token_push
+ category: container_packages
aggregation: weekly
redis_slot: package
-- name: i_package_pypi_user_delete
- category: pypi_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_container_user_delete
+ category: container_packages
aggregation: weekly
redis_slot: package
-- name: i_package_pypi_deploy_token_delete
- category: pypi_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_container_user_pull
+ category: container_packages
aggregation: weekly
redis_slot: package
-- name: i_package_pypi_user_pull
- category: pypi_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_container_user_push
+ category: container_packages
aggregation: weekly
redis_slot: package
-- name: i_package_pypi_deploy_token_pull
- category: pypi_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_debian_deploy_token_delete
+ category: debian_packages
aggregation: weekly
redis_slot: package
-- name: i_package_composer_user_push
- category: composer_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_debian_deploy_token_pull
+ category: debian_packages
aggregation: weekly
redis_slot: package
-- name: i_package_composer_deploy_token_push
- category: composer_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_debian_deploy_token_push
+ category: debian_packages
aggregation: weekly
redis_slot: package
-- name: i_package_composer_user_delete
- category: composer_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_debian_user_delete
+ category: debian_packages
aggregation: weekly
redis_slot: package
-- name: i_package_composer_deploy_token_delete
- category: composer_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_debian_user_pull
+ category: debian_packages
aggregation: weekly
redis_slot: package
-- name: i_package_composer_user_pull
- category: composer_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_debian_user_push
+ category: debian_packages
aggregation: weekly
redis_slot: package
-- name: i_package_composer_deploy_token_pull
- category: composer_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_generic_deploy_token_delete
+ category: generic_packages
aggregation: weekly
redis_slot: package
-- name: i_package_generic_user_push
+ feature_flag: collect_package_events_redis
+- name: i_package_generic_deploy_token_pull
category: generic_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_generic_deploy_token_push
category: generic_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_generic_user_delete
category: generic_packages
aggregation: weekly
redis_slot: package
-- name: i_package_generic_deploy_token_delete
+ feature_flag: collect_package_events_redis
+- name: i_package_generic_user_pull
category: generic_packages
aggregation: weekly
redis_slot: package
-- name: i_package_generic_user_pull
+ feature_flag: collect_package_events_redis
+- name: i_package_generic_user_push
category: generic_packages
aggregation: weekly
redis_slot: package
-- name: i_package_generic_deploy_token_pull
- category: generic_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_golang_deploy_token_delete
+ category: golang_packages
aggregation: weekly
redis_slot: package
-- name: i_package_golang_user_push
+ feature_flag: collect_package_events_redis
+- name: i_package_golang_deploy_token_pull
category: golang_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_golang_deploy_token_push
category: golang_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_golang_user_delete
category: golang_packages
aggregation: weekly
redis_slot: package
-- name: i_package_golang_deploy_token_delete
+ feature_flag: collect_package_events_redis
+- name: i_package_golang_user_pull
category: golang_packages
aggregation: weekly
redis_slot: package
-- name: i_package_golang_user_pull
+ feature_flag: collect_package_events_redis
+- name: i_package_golang_user_push
category: golang_packages
aggregation: weekly
redis_slot: package
-- name: i_package_golang_deploy_token_pull
- category: golang_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_maven_deploy_token_delete
+ category: maven_packages
aggregation: weekly
redis_slot: package
-- name: i_package_debian_user_push
- category: debian_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_maven_deploy_token_pull
+ category: maven_packages
aggregation: weekly
redis_slot: package
-- name: i_package_debian_deploy_token_push
- category: debian_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_maven_deploy_token_push
+ category: maven_packages
aggregation: weekly
redis_slot: package
-- name: i_package_debian_user_delete
- category: debian_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_maven_user_delete
+ category: maven_packages
aggregation: weekly
redis_slot: package
-- name: i_package_debian_deploy_token_delete
- category: debian_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_maven_user_pull
+ category: maven_packages
aggregation: weekly
redis_slot: package
-- name: i_package_debian_user_pull
- category: debian_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_maven_user_push
+ category: maven_packages
aggregation: weekly
redis_slot: package
-- name: i_package_debian_deploy_token_pull
- category: debian_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_npm_deploy_token_delete
+ category: npm_packages
aggregation: weekly
redis_slot: package
-- name: i_package_container_user_push
- category: container_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_npm_deploy_token_pull
+ category: npm_packages
aggregation: weekly
redis_slot: package
-- name: i_package_container_deploy_token_push
- category: container_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_npm_deploy_token_push
+ category: npm_packages
aggregation: weekly
redis_slot: package
-- name: i_package_container_user_delete
- category: container_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_npm_user_delete
+ category: npm_packages
aggregation: weekly
redis_slot: package
-- name: i_package_container_deploy_token_delete
- category: container_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_npm_user_pull
+ category: npm_packages
aggregation: weekly
redis_slot: package
-- name: i_package_container_user_pull
- category: container_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_npm_user_push
+ category: npm_packages
aggregation: weekly
redis_slot: package
-- name: i_package_container_deploy_token_pull
- category: container_packages
+ feature_flag: collect_package_events_redis
+- name: i_package_nuget_deploy_token_delete
+ category: nuget_packages
aggregation: weekly
redis_slot: package
-- name: i_package_tag_user_push
+ feature_flag: collect_package_events_redis
+- name: i_package_nuget_deploy_token_pull
+ category: nuget_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_nuget_deploy_token_push
+ category: nuget_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_nuget_user_delete
+ category: nuget_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_nuget_user_pull
+ category: nuget_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_nuget_user_push
+ category: nuget_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_pypi_deploy_token_delete
+ category: pypi_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_pypi_deploy_token_pull
+ category: pypi_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_pypi_deploy_token_push
+ category: pypi_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_pypi_user_delete
+ category: pypi_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_pypi_user_pull
+ category: pypi_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_pypi_user_push
+ category: pypi_packages
+ aggregation: weekly
+ redis_slot: package
+ feature_flag: collect_package_events_redis
+- name: i_package_tag_deploy_token_delete
category: tag_packages
aggregation: weekly
redis_slot: package
-- name: i_package_tag_deploy_token_push
+ feature_flag: collect_package_events_redis
+- name: i_package_tag_deploy_token_pull
category: tag_packages
aggregation: weekly
redis_slot: package
-- name: i_package_tag_user_delete
+ feature_flag: collect_package_events_redis
+- name: i_package_tag_deploy_token_push
category: tag_packages
aggregation: weekly
redis_slot: package
-- name: i_package_tag_deploy_token_delete
+ feature_flag: collect_package_events_redis
+- name: i_package_tag_user_delete
category: tag_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
- name: i_package_tag_user_pull
category: tag_packages
aggregation: weekly
redis_slot: package
-- name: i_package_tag_deploy_token_pull
+ feature_flag: collect_package_events_redis
+- name: i_package_tag_user_push
category: tag_packages
aggregation: weekly
redis_slot: package
+ feature_flag: collect_package_events_redis
diff --git a/lib/gitlab/usage_data_counters/search_counter.rb b/lib/gitlab/usage_data_counters/search_counter.rb
index 61f98887adc..46aec52b95a 100644
--- a/lib/gitlab/usage_data_counters/search_counter.rb
+++ b/lib/gitlab/usage_data_counters/search_counter.rb
@@ -4,6 +4,7 @@ module Gitlab
module UsageDataCounters
class SearchCounter < BaseCounter
KNOWN_EVENTS = %w[all_searches navbar_searches].freeze
+ PREFIX = nil
class << self
def redis_key(event)
diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb
index c54e766230e..b275bdbacde 100644
--- a/lib/gitlab/usage_data_queries.rb
+++ b/lib/gitlab/usage_data_queries.rb
@@ -25,6 +25,13 @@ module Gitlab
relation.select(relation.all.table[column].sum).to_sql
end
+ # For estimated distinct count use exact query instead of hll
+ # buckets query, because it can't be used to obtain estimations without
+ # supplementary ruby code present in Gitlab::Database::PostgresHll::BatchDistinctCounter
+ def estimate_batch_distinct_count(relation, column = nil, *rest)
+ raw_sql(relation, column, :distinct)
+ end
+
private
def raw_sql(relation, column, distinct = nil)
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index eec89e1ab72..0af7ad6ec17 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -81,6 +81,10 @@ module Gitlab
end
end
+ def can_push_for_ref?(_)
+ can_do_action?(:push_code)
+ end
+
private
def can_push?
diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb
index 5267733d220..0d28a1cd035 100644
--- a/lib/gitlab/utils/usage_data.rb
+++ b/lib/gitlab/utils/usage_data.rb
@@ -38,6 +38,7 @@ module Gitlab
extend self
FALLBACK = -1
+ DISTRIBUTED_HLL_FALLBACK = -2
def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
if batch
@@ -59,6 +60,17 @@ module Gitlab
FALLBACK
end
+ def estimate_batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil)
+ Gitlab::Database::PostgresHll::BatchDistinctCounter.new(relation, column).estimate_distinct_count(batch_size: batch_size, start: start, finish: finish)
+ rescue ActiveRecord::StatementInvalid
+ FALLBACK
+ # catch all rescue should be removed as a part of feature flag rollout issue
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/285485
+ rescue StandardError => error
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
+ DISTRIBUTED_HLL_FALLBACK
+ end
+
def sum(relation, column, batch_size: nil, start: nil, finish: nil)
Gitlab::Database::BatchCount.batch_sum(relation, column, batch_size: batch_size, start: start, finish: finish)
rescue ActiveRecord::StatementInvalid
diff --git a/lib/gitlab/uuid.rb b/lib/gitlab/uuid.rb
new file mode 100644
index 00000000000..12a4efabc44
--- /dev/null
+++ b/lib/gitlab/uuid.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class UUID
+ NAMESPACE_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".freeze
+
+ class << self
+ def v5(name, namespace_id: default_namespace_id)
+ Digest::UUID.uuid_v5(namespace_id, name)
+ end
+
+ private
+
+ def default_namespace_id
+ @default_namespace_id ||= begin
+ namespace_uuid = NAMESPACE_IDS.fetch(Rails.env.to_sym)
+ # Digest::UUID is broken when using a UUID as a 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
+ end
+ end
+end
diff --git a/lib/gitlab/whats_new.rb b/lib/gitlab/whats_new.rb
deleted file mode 100644
index 69ccb48c544..00000000000
--- a/lib/gitlab/whats_new.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module WhatsNew
- CACHE_DURATION = 1.hour
- WHATS_NEW_FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
-
- private
-
- def whats_new_release_items(page: 1)
- Rails.cache.fetch(whats_new_items_cache_key(page), expires_in: CACHE_DURATION) do
- index = page - 1
- file_path = whats_new_file_paths[index]
-
- next if file_path.nil?
-
- file = File.read(file_path)
-
- items = YAML.safe_load(file, permitted_classes: [Date])
-
- items if items.is_a?(Array)
- end
- rescue => e
- Gitlab::ErrorTracking.track_exception(e, page: page)
-
- nil
- end
-
- def whats_new_file_paths
- @whats_new_file_paths ||= Rails.cache.fetch('whats_new:file_paths', expires_in: CACHE_DURATION) do
- Dir.glob(WHATS_NEW_FILES_PATH).sort.reverse
- end
- end
-
- def whats_new_items_cache_key(page)
- filename = /\d*\_\d*\_\d*/.match(whats_new_file_paths&.first)
- "whats_new:release_items:file-#{filename}:page-#{page}"
- end
- end
-end
diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb
index bbb64e0d5da..ec9dd20ccc0 100644
--- a/lib/gitlab_danger.rb
+++ b/lib/gitlab_danger.rb
@@ -14,6 +14,7 @@ class GitlabDanger
product_analytics
utility_css
pajamas
+ pipeline
].freeze
CI_ONLY_RULES ||= %w[
diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb
index 0b21c355a54..39005f56dcb 100644
--- a/lib/microsoft_teams/notifier.rb
+++ b/lib/microsoft_teams/notifier.rb
@@ -14,7 +14,7 @@ module MicrosoftTeams
response = Gitlab::HTTP.post(
@webhook.to_str,
headers: @header,
- body: body(options)
+ body: body(**options)
)
result = true if response
@@ -27,14 +27,13 @@ module MicrosoftTeams
private
- def body(options = {})
+ def body(title: nil, summary: nil, attachments: nil, activity:)
result = { 'sections' => [] }
- result['title'] = options[:title]
- result['summary'] = options[:summary]
- result['sections'] << MicrosoftTeams::Activity.new(options[:activity]).prepare
+ result['title'] = title
+ result['summary'] = summary
+ result['sections'] << MicrosoftTeams::Activity.new(**activity).prepare
- attachments = options[:attachments]
unless attachments.blank?
result['sections'] << { text: attachments }
end
diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb
index cc536ce9b46..f933d4e4866 100644
--- a/lib/object_storage/config.rb
+++ b/lib/object_storage/config.rb
@@ -93,6 +93,11 @@ module ObjectStorage
private
+ # This returns a Hash of HTTP encryption headers to send along to S3.
+ #
+ # They can also be passed in as Fog::AWS::Storage::File attributes, since there
+ # are aliases defined for them:
+ # https://github.com/fog/fog-aws/blob/ab288f29a0974d64fd8290db41080e5578be9651/lib/fog/aws/models/storage/file.rb#L24-L25
def aws_server_side_encryption_headers
{
'x-amz-server-side-encryption' => server_side_encryption,
diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb
index b5864382299..3a8fa51e198 100644
--- a/lib/object_storage/direct_upload.rb
+++ b/lib/object_storage/direct_upload.rb
@@ -184,15 +184,20 @@ module ObjectStorage
private
def rounded_multipart_part_size
- # round multipart_part_size up to minimum_mulitpart_size
+ # round multipart_part_size up to minimum_multipart_size
(multipart_part_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE * MINIMUM_MULTIPART_SIZE
end
def multipart_part_size
+ return MINIMUM_MULTIPART_SIZE if maximum_size == 0
+
maximum_size / number_of_multipart_parts
end
def number_of_multipart_parts
+ # If we don't have max length, we can only assume the file is as large as possible.
+ return MAXIMUM_MULTIPART_PARTS if maximum_size == 0
+
[
# round maximum_size up to minimum_mulitpart_size
(maximum_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE,
@@ -201,7 +206,7 @@ module ObjectStorage
end
def requires_multipart_upload?
- config.aws? && !has_length
+ config.aws? && !has_length && !use_workhorse_s3_client?
end
def upload_id
diff --git a/lib/product_analytics/tracker.rb b/lib/product_analytics/tracker.rb
index 2dc5e1f53ce..d4a88b879f0 100644
--- a/lib/product_analytics/tracker.rb
+++ b/lib/product_analytics/tracker.rb
@@ -7,36 +7,5 @@ module ProductAnalytics
# The collector URL minus protocol and /i
COLLECTOR_URL = Gitlab.config.gitlab.url.sub(/\Ahttps?\:\/\//, '') + '/-/collector'
-
- class << self
- include Gitlab::Utils::StrongMemoize
-
- def event(category, action, label: nil, property: nil, value: nil, context: nil)
- return unless enabled?
-
- snowplow.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i)
- end
-
- private
-
- def enabled?
- Gitlab::CurrentSettings.usage_ping_enabled?
- end
-
- def project_id
- Gitlab::CurrentSettings.self_monitoring_project_id
- end
-
- def snowplow
- strong_memoize(:snowplow) do
- SnowplowTracker::Tracker.new(
- SnowplowTracker::AsyncEmitter.new(COLLECTOR_URL, protocol: Gitlab.config.gitlab.protocol),
- SnowplowTracker::Subject.new,
- Gitlab::Tracking::SNOWPLOW_NAMESPACE,
- project_id.to_s
- )
- end
- end
- end
end
end
diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb
index b239b6812ca..45cfa9b373d 100644
--- a/lib/quality/test_level.rb
+++ b/lib/quality/test_level.rb
@@ -21,6 +21,9 @@ module Quality
config
db
dependencies
+ elastic
+ elastic_integration
+ experiments
factories
finders
frontend
@@ -46,7 +49,6 @@ module Quality
validators
views
workers
- elastic_integration
tooling
],
integration: %w[
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index e2c92054d62..e03c78d5a40 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -1,39 +1,38 @@
+# frozen_string_literal: true
+
require "gettext_i18n_rails/tasks"
namespace :gettext do
- # Customize list of translatable files
- # See: https://github.com/grosser/gettext_i18n_rails#customizing-list-of-translatable-files
- def files_to_translate
- folders = %W(ee app lib config #{locale_path}).join(',')
- exts = %w(rb erb haml slim rhtml js jsx vue handlebars hbs mustache).join(',')
-
- Dir.glob(
- "{#{folders}}/**/*.{#{exts}}"
- )
- end
-
- # Disallow HTML from translatable strings
- # See: https://docs.gitlab.com/ee/development/i18n/externalization.html#html
- def html_todolist
- return @html_todolist if defined?(@html_todolist)
-
- @html_todolist = YAML.load_file(Rails.root.join('lib/gitlab/i18n/html_todo.yml'))
- end
-
task :compile do
# See: https://gitlab.com/gitlab-org/gitlab-foss/issues/33014#note_31218998
- FileUtils.touch(File.join(Rails.root, 'locale/gitlab.pot'))
+ FileUtils.touch(pot_file_path)
Rake::Task['gettext:po_to_json'].invoke
end
desc 'Regenerate gitlab.pot file'
task :regenerate do
- pot_file = 'locale/gitlab.pot'
- # Remove all translated files, this speeds up finding
- FileUtils.rm Dir['locale/**/gitlab.*']
+ ensure_locale_folder_presence!
+
+ # Clean up folders that do not contain a gitlab.po file
+ Pathname.new(locale_path).children.each do |child|
+ next unless child.directory?
+
+ folder_path = child.to_path
+
+ if File.exist?("#{folder_path}/gitlab.po")
+ # remove all translated files to speed up finding
+ FileUtils.rm Dir["#{folder_path}/gitlab.*"]
+ else
+ # remove empty translation folders so we don't generate un-needed .po files
+ puts "Deleting #{folder_path} as it does not contain a 'gitlab.po' file."
+
+ FileUtils.rm_r folder_path
+ end
+ end
+
# remove the `pot` file to ensure it's completely regenerated
- FileUtils.rm_f pot_file
+ FileUtils.rm_f(pot_file_path)
Rake::Task['gettext:find'].invoke
@@ -42,10 +41,12 @@ namespace :gettext do
raise 'failed to cleanup generated locale/*/gitlab.po files'
end
+ raise 'gitlab.pot file not generated' unless File.exist?(pot_file_path)
+
# Remove timestamps from the pot file
- pot_content = File.read pot_file
+ pot_content = File.read pot_file_path
pot_content.gsub!(/^"POT?\-(?:Creation|Revision)\-Date\:.*\n/, '')
- File.write pot_file, pot_content
+ File.write pot_file_path, pot_content
puts <<~MSG
All done. Please commit the changes to `locale/gitlab.pot`.
@@ -64,11 +65,10 @@ namespace :gettext do
linters = files.map do |file|
locale = File.basename(File.dirname(file))
- Gitlab::I18n::PoLinter.new(po_path: file, html_todolist: html_todolist, locale: locale)
+ Gitlab::I18n::PoLinter.new(po_path: file, locale: locale)
end
- pot_file = Rails.root.join('locale/gitlab.pot')
- linters.unshift(Gitlab::I18n::PoLinter.new(po_path: pot_file, html_todolist: html_todolist))
+ linters.unshift(Gitlab::I18n::PoLinter.new(po_path: pot_file_path))
failed_linters = linters.select { |linter| linter.errors.any? }
@@ -84,12 +84,11 @@ namespace :gettext do
end
task :updated_check do
- pot_file = 'locale/gitlab.pot'
# Removing all pre-translated files speeds up `gettext:find` as the
# files don't need to be merged.
# Having `LC_MESSAGES/gitlab.mo files present also confuses the output.
FileUtils.rm Dir['locale/**/gitlab.*']
- FileUtils.rm_f pot_file
+ FileUtils.rm_f pot_file_path
# `gettext:find` writes touches to temp files to `stderr` which would cause
# `static-analysis` to report failures. We can ignore these.
@@ -97,18 +96,18 @@ namespace :gettext do
Rake::Task['gettext:find'].invoke
end
- pot_diff = `git diff -- #{pot_file} | grep -E '^(\\+|-)msgid'`.strip
+ pot_diff = `git diff -- #{pot_file_path} | grep -E '^(\\+|-)msgid'`.strip
# reset the locale folder for potential next tasks
`git checkout -- locale`
if pot_diff.present?
raise <<~MSG
- Changes in translated strings found, please update file `#{pot_file}` by running:
+ Changes in translated strings found, please update file `#{pot_file_path}` by running:
bin/rake gettext:regenerate
- Then commit and push the resulting changes to `#{pot_file}`.
+ Then commit and push the resulting changes to `#{pot_file_path}`.
The diff was:
@@ -117,6 +116,27 @@ namespace :gettext do
end
end
+ private
+
+ # Customize list of translatable files
+ # See: https://github.com/grosser/gettext_i18n_rails#customizing-list-of-translatable-files
+ def files_to_translate
+ folders = %W(ee app lib config #{locale_path}).join(',')
+ exts = %w(rb erb haml slim rhtml js jsx vue handlebars hbs mustache).join(',')
+
+ Dir.glob(
+ "{#{folders}}/**/*.{#{exts}}"
+ )
+ end
+
+ # Disallow HTML from translatable strings
+ # See: https://docs.gitlab.com/ee/development/i18n/externalization.html#html
+ def html_todolist
+ return @html_todolist if defined?(@html_todolist)
+
+ @html_todolist = YAML.safe_load(File.read(Rails.root.join('lib/gitlab/i18n/html_todo.yml')))
+ end
+
def report_errors_for_file(file, errors_for_file)
puts "Errors in `#{file}`:"
@@ -140,4 +160,21 @@ namespace :gettext do
$stderr.reopen(old_stderr)
old_stderr.close
end
+
+ def ensure_locale_folder_presence!
+ unless Dir.exist?(locale_path)
+ raise <<~MSG
+ Cannot find '#{locale_path}' folder. Please ensure you're running this task from the gitlab repo.
+
+ MSG
+ end
+ end
+
+ def locale_path
+ @locale_path ||= Rails.root.join('locale')
+ end
+
+ def pot_file_path
+ @pot_file_path ||= File.join(locale_path, 'gitlab.pot')
+ end
end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index ab2d77eeaf0..54e74fd9c8b 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -81,7 +81,10 @@ namespace :gitlab do
if head_assets_md5 != master_assets_md5 || !public_assets_webpack_dir_exists
FileUtils.rm_r(Tasks::Gitlab::Assets::PUBLIC_ASSETS_WEBPACK_DIR) if public_assets_webpack_dir_exists
- system('yarn webpack')
+
+ unless system('yarn webpack')
+ abort 'Error: Unable to compile webpack production bundle.'.color(:red)
+ end
end
end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index a3f20f31f64..901e349ea31 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -192,16 +192,42 @@ namespace :gitlab do
exit
end
- indexes = if args[:index_name]
- [Gitlab::Database::PostgresIndex.by_identifier(args[:index_name])]
- else
- Gitlab::Database::Reindexing.candidate_indexes.random_few(2)
- end
+ indexes = Gitlab::Database::Reindexing.candidate_indexes
+
+ if identifier = args[:index_name]
+ raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/
+
+ indexes = indexes.where(identifier: identifier)
+
+ raise "Index not found or not supported: #{args[:index_name]}" if indexes.empty?
+ end
+
+ ActiveRecord::Base.logger = Logger.new(STDOUT) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false)
Gitlab::Database::Reindexing.perform(indexes)
rescue => e
Gitlab::AppLogger.error(e)
raise
end
+
+ desc 'Check if there have been user additions to the database'
+ task active: :environment do
+ if ActiveRecord::Base.connection.migration_context.needs_migration?
+ puts "Migrations pending. Database not active"
+ exit 1
+ end
+
+ # A list of projects that GitLab creates automatically on install/upgrade
+ # gc = Gitlab::CurrentSettings.current_application_settings
+ seed_projects = [Gitlab::CurrentSettings.current_application_settings.self_monitoring_project]
+
+ if (Project.count - seed_projects.count {|x| !x.nil? }).eql?(0)
+ puts "No user created projects. Database not active"
+ exit 1
+ end
+
+ puts "Found user created projects. Database active"
+ exit 0
+ end
end
end
diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake
index 0459de27c96..fe7920c621f 100644
--- a/lib/tasks/gitlab/ldap.rake
+++ b/lib/tasks/gitlab/ldap.rake
@@ -36,5 +36,23 @@ namespace :gitlab do
puts "Successfully updated #{plural_updated_count} out of #{plural_id_count} total"
end
end
+
+ namespace :secret do
+ desc 'GitLab | LDAP | Secret | Write LDAP secrets'
+ task write: [:environment] do
+ content = STDIN.tty? ? STDIN.gets : STDIN.read
+ Gitlab::EncryptedLdapCommand.write(content)
+ end
+
+ desc 'GitLab | LDAP | Secret | Edit LDAP secrets'
+ task edit: [:environment] do
+ Gitlab::EncryptedLdapCommand.edit
+ end
+
+ desc 'GitLab | LDAP | Secret | Show LDAP secrets'
+ task show: [:environment] do
+ Gitlab::EncryptedLdapCommand.show
+ end
+ end
end
end
diff --git a/lib/tasks/gitlab/packages/events.rake b/lib/tasks/gitlab/packages/events.rake
index 3484b9b6072..ca507fb5320 100644
--- a/lib/tasks/gitlab/packages/events.rake
+++ b/lib/tasks/gitlab/packages/events.rake
@@ -5,11 +5,29 @@ namespace :gitlab do
namespace :packages do
namespace :events do
task generate: :environment do
+ Rake::Task["gitlab:packages:events:generate_guest"].invoke
+ Rake::Task["gitlab:packages:events:generate_unique"].invoke
+ rescue => e
+ logger.error("Error building events list: #{e}")
+ end
+
+ task generate_guest: :environment do
logger = Logger.new(STDOUT)
logger.info('Building list of package events...')
- path = File.join(File.dirname(::Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH), 'package_events.yml')
+ path = Gitlab::UsageDataCounters::GuestPackageEventCounter::KNOWN_EVENTS_PATH
+ File.open(path, "w") { |file| file << guest_events_list.to_yaml }
+
+ logger.info("Events file `#{path}` generated successfully")
+ rescue => e
+ logger.error("Error building events list: #{e}")
+ end
+
+ task generate_unique: :environment do
+ logger = Logger.new(STDOUT)
+ logger.info('Building list of package events...')
+ path = File.join(File.dirname(Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH), 'package_events.yml')
File.open(path, "w") { |file| file << generate_unique_events_list.to_yaml }
logger.info("Events file `#{path}` generated successfully")
@@ -17,23 +35,34 @@ namespace :gitlab do
logger.error("Error building events list: #{e}")
end
+ private
+
def event_pairs
- ::Packages::Event.event_types.keys.product(::Packages::Event.originator_types.keys)
+ Packages::Event.event_types.keys.product(Packages::Event::EVENT_SCOPES.keys)
end
def generate_unique_events_list
- ::Packages::Event::EVENT_SCOPES.keys.each_with_object([]) do |event_scope, events|
- event_pairs.each do |event_type, originator|
- if name = ::Packages::Event.allowed_event_name(event_scope, event_type, originator)
+ events = event_pairs.each_with_object([]) do |(event_type, event_scope), events|
+ Packages::Event.originator_types.keys.excluding('guest').each do |originator|
+ if name = Packages::Event.allowed_event_name(event_scope, event_type, originator)
events << {
"name" => name,
"category" => "#{event_scope}_packages",
"aggregation" => "weekly",
- "redis_slot" => "package"
+ "redis_slot" => "package",
+ "feature_flag" => "collect_package_events_redis"
}
end
end
end
+
+ events.sort_by { |event| event["name"] }
+ end
+
+ def guest_events_list
+ event_pairs.map do |event_type, event_scope|
+ Packages::Event.allowed_event_name(event_scope, event_type, "guest")
+ end.compact.sort
end
end
end
diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake
index 6f3db91c2b0..d6f5661d5eb 100644
--- a/lib/tasks/gitlab/usage_data.rake
+++ b/lib/tasks/gitlab/usage_data.rake
@@ -9,5 +9,17 @@ namespace :gitlab do
task dump_sql_in_json: :environment do
puts Gitlab::Json.pretty_generate(Gitlab::UsageDataQueries.uncached_data)
end
+
+ desc 'GitLab | UsageData | Generate usage ping in JSON'
+ task generate: :environment do
+ puts Gitlab::Json.pretty_generate(Gitlab::UsageData.uncached_data)
+ end
+
+ desc 'GitLab | UsageData | Generate usage ping and send it to Versions Application'
+ task generate_and_send: :environment do
+ result = SubmitUsagePingService.new.execute
+
+ puts Gitlab::Json.pretty_generate(result.attributes)
+ end
end
end
diff --git a/lib/tasks/gitlab/user_management.rake b/lib/tasks/gitlab/user_management.rake
new file mode 100644
index 00000000000..f47e549e795
--- /dev/null
+++ b/lib/tasks/gitlab/user_management.rake
@@ -0,0 +1,13 @@
+namespace :gitlab do
+ namespace :user_management do
+ desc "GitLab | User management | Update all users of a group with personal project limit to 0 and can_create_group to false"
+ task :disable_project_and_group_creation, [:group_id] => :environment do |t, args|
+ group = Group.find(args.group_id)
+
+ result = User.where(id: group.direct_and_indirect_users_with_inactive.select(:id)).update_all(projects_limit: 0, can_create_group: false)
+ ids_count = group.direct_and_indirect_users_with_inactive.count
+ puts "Done".color(:green) if result == ids_count
+ puts "Something went wrong".color(:red) if result != ids_count
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
index 15084a118b7..2d72a01f66f 100644
--- a/lib/tasks/gitlab/workhorse.rake
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -8,18 +8,25 @@ namespace :gitlab do
abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]")
end
+ # It used to be the case that the binaries in the target directory match
+ # the source code. An administrator could run `make` to rebuild the
+ # binaries for instance. Or they could read the source code, or run `git
+ # log` to see what changed. Or they could patch workhorse for some
+ # reason and recompile it. None of those things make sense anymore once
+ # the transition in https://gitlab.com/groups/gitlab-org/-/epics/4826 is
+ # done: there would be an outdated copy of the workhorse source code for
+ # the administrator to poke at.
+ #
+ # To prevent this possible confusion and make clear what is going on, we
+ # have created a special branch `workhorse-move-notice` in the old
+ # gitlab-workhorse repository which contains no Go files anymore, just a
+ # README explaining what is going on. See:
+ # https://gitlab.com/gitlab-org/gitlab-workhorse/tree/workhorse-move-notice
+ #
args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-workhorse.git')
+ checkout_or_clone_version(version: 'workhorse-move-notice', repo: args.repo, target_dir: args.dir, clone_opts: %w[--depth 1])
- version = Gitlab::Workhorse.version
-
- checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir, clone_opts: %w[--depth 1])
-
- _, status = Gitlab::Popen.popen(%w[which gmake])
- command = status == 0 ? 'gmake' : 'make'
-
- Dir.chdir(args.dir) do
- run_command!([command])
- end
+ Gitlab::SetupHelper::Workhorse.compile_into(args.dir)
end
end
end