summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /lib
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
downloadgitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb16
-rw-r--r--lib/api/applications.rb16
-rw-r--r--lib/api/ci/pipelines.rb93
-rw-r--r--lib/api/ci/runner.rb20
-rw-r--r--lib/api/commit_statuses.rb6
-rw-r--r--lib/api/commits.rb5
-rw-r--r--lib/api/composer_packages.rb2
-rw-r--r--lib/api/conan_instance_packages.rb10
-rw-r--r--lib/api/conan_package_endpoints.rb (renamed from lib/api/conan_packages.rb)38
-rw-r--r--lib/api/conan_project_packages.rb16
-rw-r--r--lib/api/entities/issue_link.rb11
-rw-r--r--lib/api/entities/milestone.rb1
-rw-r--r--lib/api/entities/package.rb2
-rw-r--r--lib/api/entities/related_issue.rb10
-rw-r--r--lib/api/generic_packages.rb34
-rw-r--r--lib/api/github/entities.rb217
-rw-r--r--lib/api/helpers.rb20
-rw-r--r--lib/api/helpers/internal_helpers.rb2
-rw-r--r--lib/api/helpers/packages/conan/api_helpers.rb98
-rw-r--r--lib/api/helpers/packages_helpers.rb4
-rw-r--r--lib/api/helpers/packages_manager_clients_helpers.rb22
-rw-r--r--lib/api/helpers/search_helpers.rb4
-rw-r--r--lib/api/helpers/services_helpers.rb27
-rw-r--r--lib/api/helpers/snippets_helpers.rb46
-rw-r--r--lib/api/internal/base.rb12
-rw-r--r--lib/api/internal/kubernetes.rb32
-rw-r--r--lib/api/issue_links.rb82
-rw-r--r--lib/api/issues.rb13
-rw-r--r--lib/api/jobs.rb48
-rw-r--r--lib/api/maven_packages.rb11
-rw-r--r--lib/api/merge_requests.rb10
-rw-r--r--lib/api/npm_packages.rb4
-rw-r--r--lib/api/nuget_packages.rb13
-rw-r--r--lib/api/project_snippets.rb19
-rw-r--r--lib/api/pypi_packages.rb21
-rw-r--r--lib/api/search.rb4
-rw-r--r--lib/api/settings.rb7
-rw-r--r--lib/api/sidekiq_metrics.rb2
-rw-r--r--lib/api/snippets.rb22
-rw-r--r--lib/api/terraform/state.rb7
-rw-r--r--lib/api/todos.rb13
-rw-r--r--lib/api/usage_data.rb30
-rw-r--r--lib/api/users.rb12
-rw-r--r--lib/api/v3/github.rb232
-rw-r--r--lib/api/variables.rb1
-rw-r--r--lib/api/wikis.rb13
-rw-r--r--lib/atlassian/jira_connect.rb21
-rw-r--r--lib/atlassian/jira_connect/client.rb47
-rw-r--r--lib/atlassian/jira_connect/serializers/author_entity.rb24
-rw-r--r--lib/atlassian/jira_connect/serializers/base_entity.rb22
-rw-r--r--lib/atlassian/jira_connect/serializers/branch_entity.rb32
-rw-r--r--lib/atlassian/jira_connect/serializers/commit_entity.rb45
-rw-r--r--lib/atlassian/jira_connect/serializers/file_entity.rb38
-rw-r--r--lib/atlassian/jira_connect/serializers/pull_request_entity.rb42
-rw-r--r--lib/atlassian/jira_connect/serializers/repository_entity.rb29
-rw-r--r--lib/atlassian/jira_issue_key_extractor.rb17
-rw-r--r--lib/backup/artifacts.rb2
-rw-r--r--lib/backup/database.rb72
-rw-r--r--lib/backup/files.rb27
-rw-r--r--lib/backup/manager.rb12
-rw-r--r--lib/backup/pages.rb2
-rw-r--r--lib/backup/repository.rb28
-rw-r--r--lib/backup/uploads.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb4
-rw-r--r--lib/banzai/filter/alert_reference_filter.rb29
-rw-r--r--lib/banzai/filter/autolink_filter.rb2
-rw-r--r--lib/banzai/filter/emoji_filter.rb2
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb2
-rw-r--r--lib/banzai/filter/inline_metrics_filter.rb3
-rw-r--r--lib/banzai/pipeline/broadcast_message_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb9
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb5
-rw-r--r--lib/banzai/reference_parser/alert_parser.rb19
-rw-r--r--lib/bitbucket_server/representation/comment.rb4
-rw-r--r--lib/bitbucket_server/representation/pull_request.rb6
-rw-r--r--lib/carrier_wave_string_file.rb8
-rw-r--r--lib/constraints/jira_encoded_url_constrainer.rb9
-rw-r--r--lib/container_registry/client.rb11
-rw-r--r--lib/expand_variables.rb2
-rw-r--r--lib/extracts_ref.rb1
-rw-r--r--lib/feature.rb14
-rw-r--r--lib/feature/definition.rb27
-rw-r--r--lib/feature/shared.rb27
-rw-r--r--lib/gitlab/access.rb13
-rw-r--r--lib/gitlab/alert_management/alert_params.rb4
-rw-r--r--lib/gitlab/alert_management/payload.rb48
-rw-r--r--lib/gitlab/alert_management/payload/base.rb167
-rw-r--r--lib/gitlab/alert_management/payload/generic.rb25
-rw-r--r--lib/gitlab/alert_management/payload/managed_prometheus.rb58
-rw-r--r--lib/gitlab/alert_management/payload/prometheus.rb99
-rw-r--r--lib/gitlab/alerting/notification_payload_parser.rb24
-rw-r--r--lib/gitlab/analytics/cycle_analytics/default_stages.rb13
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb2
-rw-r--r--lib/gitlab/analytics/instance_statistics/workers_argument_builder.rb32
-rw-r--r--lib/gitlab/analytics/unique_visits.rb20
-rw-r--r--lib/gitlab/anonymous_session.rb17
-rw-r--r--lib/gitlab/application_context.rb2
-rw-r--r--lib/gitlab/auth.rb16
-rw-r--r--lib/gitlab/auth/atlassian/auth_hash.rb31
-rw-r--r--lib/gitlab/auth/atlassian/identity_linker.rb30
-rw-r--r--lib/gitlab/auth/atlassian/user.rb35
-rw-r--r--lib/gitlab/auth/ldap/adapter.rb4
-rw-r--r--lib/gitlab/auth/ldap/config.rb4
-rw-r--r--lib/gitlab/auth/ldap/person.rb6
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb9
-rw-r--r--lib/gitlab/auth/o_auth/user.rb6
-rw-r--r--lib/gitlab/background_migration.rb2
-rw-r--r--lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb2
-rw-r--r--lib/gitlab/background_migration/calculate_wiki_sizes.rb2
-rw-r--r--lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb2
-rw-r--r--lib/gitlab/background_migration/fix_pages_access_level.rb4
-rw-r--r--lib/gitlab/background_migration/migrate_to_hashed_storage.rb61
-rw-r--r--lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb12
-rw-r--r--lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb14
-rw-r--r--lib/gitlab/background_migration/remove_duplicate_cs_findings.rb13
-rw-r--r--lib/gitlab/background_migration/set_merge_request_diff_files_count.rb21
-rw-r--r--lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb8
-rw-r--r--lib/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings.rb13
-rw-r--r--lib/gitlab/backtrace_cleaner.rb2
-rw-r--r--lib/gitlab/badge/coverage/template.rb4
-rw-r--r--lib/gitlab/badge/pipeline/status.rb7
-rw-r--r--lib/gitlab/badge/pipeline/template.rb4
-rw-r--r--lib/gitlab/badge/template.rb3
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb43
-rw-r--r--lib/gitlab/cache/request_cache.rb2
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb2
-rw-r--r--lib/gitlab/checks/snippet_check.rb8
-rw-r--r--lib/gitlab/ci/ansi2html.rb200
-rw-r--r--lib/gitlab/ci/artifact_file_reader.rb25
-rw-r--r--lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb6
-rw-r--r--lib/gitlab/ci/config.rb4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb34
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb10
-rw-r--r--lib/gitlab/ci/config/entry/root.rb2
-rw-r--r--lib/gitlab/ci/config/normalizer.rb1
-rw-r--r--lib/gitlab/ci/config/normalizer/matrix_strategy.rb13
-rw-r--r--lib/gitlab/ci/features.rb51
-rw-r--r--lib/gitlab/ci/lint.rb108
-rw-r--r--lib/gitlab/ci/mask_secret.rb5
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb6
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/remote.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/process.rb15
-rw-r--r--lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/seed.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/external.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb20
-rw-r--r--lib/gitlab/ci/pipeline/expression/parser.rb28
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb8
-rw-r--r--lib/gitlab/ci/pipeline_object_hierarchy.rb45
-rw-r--r--lib/gitlab/ci/reports/test_case.rb2
-rw-r--r--lib/gitlab/ci/reports/test_suite.rb16
-rw-r--r--lib/gitlab/ci/status/bridge/common.rb18
-rw-r--r--lib/gitlab/ci/status/build/failed.rb3
-rw-r--r--lib/gitlab/ci/status/composite.rb11
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml30
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml26
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml249
-rw-r--r--lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml18
-rw-r--r--lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml98
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml113
-rw-r--r--lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/npm.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/trace.rb39
-rw-r--r--lib/gitlab/ci/trace/metrics.rb44
-rw-r--r--lib/gitlab/ci/trace/stream.rb8
-rw-r--r--lib/gitlab/ci/warnings.rb5
-rw-r--r--lib/gitlab/ci/yaml_processor.rb192
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb120
-rw-r--r--lib/gitlab/cleanup/orphan_job_artifact_files.rb2
-rw-r--r--lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb2
-rw-r--r--lib/gitlab/cleanup/orphan_lfs_file_references.rb32
-rw-r--r--lib/gitlab/cleanup/project_upload_file_finder.rb2
-rw-r--r--lib/gitlab/cleanup/project_uploads.rb2
-rw-r--r--lib/gitlab/cleanup/remote_uploads.rb2
-rw-r--r--lib/gitlab/consul/internal.rb78
-rw-r--r--lib/gitlab/cross_project_access.rb2
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb38
-rw-r--r--lib/gitlab/danger/changelog.rb57
-rw-r--r--lib/gitlab/danger/helper.rb19
-rw-r--r--lib/gitlab/danger/roulette.rb2
-rw-r--r--lib/gitlab/danger/teammate.rb4
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/database.rb2
-rw-r--r--lib/gitlab/database/background_migration_job.rb2
-rw-r--r--lib/gitlab/database/concurrent_reindex.rb143
-rw-r--r--lib/gitlab/database/custom_structure.rb3
-rw-r--r--lib/gitlab/database/migration_helpers.rb19
-rw-r--r--lib/gitlab/database/partitioning/partition_monitoring.rb34
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb4
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb85
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb2
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb2
-rw-r--r--lib/gitlab/database/schema_cleaner.rb11
-rw-r--r--lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb4
-rw-r--r--lib/gitlab/database_importers/instance_administrators/create_group.rb6
-rw-r--r--lib/gitlab/database_importers/self_monitoring/project/create_service.rb2
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/diff/highlight_cache.rb28
-rw-r--r--lib/gitlab/discussions_diff/highlight_cache.rb6
-rw-r--r--lib/gitlab/email/hook/disable_email_interceptor.rb2
-rw-r--r--lib/gitlab/email/receiver.rb10
-rw-r--r--lib/gitlab/encoding_helper.rb2
-rw-r--r--lib/gitlab/error_tracking.rb2
-rw-r--r--lib/gitlab/error_tracking/processor/grpc_error_processor.rb67
-rw-r--r--lib/gitlab/experimentation.rb20
-rw-r--r--lib/gitlab/file_type_detection.rb8
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb2
-rw-r--r--lib/gitlab/git.rb1
-rw-r--r--lib/gitlab/git/base_error.rb21
-rw-r--r--lib/gitlab/git/diff_collection.rb2
-rw-r--r--lib/gitlab/git/keep_around.rb2
-rw-r--r--lib/gitlab/git/repository.rb20
-rw-r--r--lib/gitlab/git/wiki.rb1
-rw-r--r--lib/gitlab/git_access_snippet.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/gitpod.rb30
-rw-r--r--lib/gitlab/gl_repository.rb6
-rw-r--r--lib/gitlab/gl_repository/repo_type.rb2
-rw-r--r--lib/gitlab/gon_helper.rb6
-rw-r--r--lib/gitlab/graphql/docs/helper.rb46
-rw-r--r--lib/gitlab/graphql/docs/templates/default.md.haml46
-rw-r--r--lib/gitlab/graphql/loaders/issuable_loader.rb17
-rw-r--r--lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb5
-rw-r--r--lib/gitlab/graphql/pagination/keyset/order_info.rb38
-rw-r--r--lib/gitlab/graphql/representation/submodule_tree_entry.rb4
-rw-r--r--lib/gitlab/group_search_results.rb30
-rw-r--r--lib/gitlab/hashed_storage/migrator.rb20
-rw-r--r--lib/gitlab/health_checks/simple_abstract_check.rb2
-rw-r--r--lib/gitlab/highlight.rb8
-rw-r--r--lib/gitlab/http.rb2
-rw-r--r--lib/gitlab/i18n/html_todo.yml121
-rw-r--r--lib/gitlab/import_export/lfs_saver.rb2
-rw-r--r--lib/gitlab/import_export/project/import_export.yml12
-rw-r--r--lib/gitlab/jira/dvcs.rb48
-rw-r--r--lib/gitlab/jira/middleware.rb23
-rw-r--r--lib/gitlab/json.rb2
-rw-r--r--lib/gitlab/kas.rb28
-rw-r--r--lib/gitlab/kubernetes/cilium_network_policy.rb42
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb2
-rw-r--r--lib/gitlab/kubernetes/network_policy.rb25
-rw-r--r--lib/gitlab/kubernetes/network_policy_common.rb15
-rw-r--r--lib/gitlab/lfs/client.rb101
-rw-r--r--lib/gitlab/logger.rb3
-rw-r--r--lib/gitlab/marginalia/comment.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/importer.rb41
-rw-r--r--lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb72
-rw-r--r--lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/transformers/errors.rb19
-rw-r--r--lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb54
-rw-r--r--lib/gitlab/metrics/dashboard/url.rb58
-rw-r--r--lib/gitlab/metrics/dashboard/validator.rb16
-rw-r--r--lib/gitlab/metrics/dashboard/validator/client.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/panel.json2
-rw-r--r--lib/gitlab/metrics/exporter/sidekiq_exporter.rb6
-rw-r--r--lib/gitlab/metrics/instrumentation.rb17
-rw-r--r--lib/gitlab/metrics/methods.rb2
-rw-r--r--lib/gitlab/metrics/samplers/action_cable_sampler.rb63
-rw-r--r--lib/gitlab/metrics/samplers/base_sampler.rb2
-rw-r--r--lib/gitlab/metrics/samplers/puma_sampler.rb2
-rw-r--r--lib/gitlab/middleware/multipart.rb99
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb2
-rw-r--r--lib/gitlab/middleware/same_site_cookies.rb89
-rw-r--r--lib/gitlab/object_hierarchy.rb31
-rw-r--r--lib/gitlab/pages/settings.rb8
-rw-r--r--lib/gitlab/pages_transfer.rb19
-rw-r--r--lib/gitlab/project_authorizations.rb2
-rw-r--r--lib/gitlab/project_search_results.rb6
-rw-r--r--lib/gitlab/prometheus/internal.rb4
-rw-r--r--lib/gitlab/prometheus_client.rb15
-rw-r--r--lib/gitlab/quick_actions/extractor.rb185
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb2
-rw-r--r--lib/gitlab/quick_actions/relate_actions.rb29
-rw-r--r--lib/gitlab/quick_actions/substitution_definition.rb4
-rw-r--r--lib/gitlab/redis/shared_state.rb2
-rw-r--r--lib/gitlab/reference_counter.rb6
-rw-r--r--lib/gitlab/regex.rb36
-rw-r--r--lib/gitlab/relative_positioning.rb17
-rw-r--r--lib/gitlab/relative_positioning/gap.rb21
-rw-r--r--lib/gitlab/relative_positioning/item_context.rb259
-rw-r--r--lib/gitlab/relative_positioning/mover.rb155
-rw-r--r--lib/gitlab/relative_positioning/range.rb83
-rw-r--r--lib/gitlab/repository_cache_adapter.rb2
-rw-r--r--lib/gitlab/repository_set_cache.rb2
-rw-r--r--lib/gitlab/request_profiler.rb8
-rw-r--r--lib/gitlab/robots_txt.rb17
-rw-r--r--lib/gitlab/robots_txt/parser.rb37
-rw-r--r--lib/gitlab/sanitizers/exif.rb2
-rw-r--r--lib/gitlab/search/recent_issues.rb17
-rw-r--r--lib/gitlab/search/recent_items.rb59
-rw-r--r--lib/gitlab/search/recent_merge_requests.rb17
-rw-r--r--lib/gitlab/search_results.rb11
-rw-r--r--lib/gitlab/setup_helper.rb19
-rw-r--r--lib/gitlab/shell.rb2
-rw-r--r--lib/gitlab/sherlock/query.rb2
-rw-r--r--lib/gitlab/sidekiq_daemon/memory_killer.rb6
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb3
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none.rb23
-rw-r--r--lib/gitlab/snippet_search_results.rb5
-rw-r--r--lib/gitlab/sourcegraph.rb4
-rw-r--r--lib/gitlab/sql/except.rb22
-rw-r--r--lib/gitlab/sql/intersect.rb23
-rw-r--r--lib/gitlab/sql/set_operator.rb53
-rw-r--r--lib/gitlab/sql/union.rb27
-rw-r--r--lib/gitlab/static_site_editor/config.rb63
-rw-r--r--lib/gitlab/static_site_editor/config/file_config.rb15
-rw-r--r--lib/gitlab/static_site_editor/config/generated_config.rb70
-rw-r--r--lib/gitlab/submodule_links.rb18
-rw-r--r--lib/gitlab/task_helpers.rb2
-rw-r--r--lib/gitlab/template/finders/global_template_finder.rb6
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb18
-rw-r--r--lib/gitlab/testing/robots_blocker_middleware.rb45
-rw-r--r--lib/gitlab/tracking.rb12
-rw-r--r--lib/gitlab/tracking/incident_management.rb3
-rw-r--r--lib/gitlab/usage_data.rb188
-rw-r--r--lib/gitlab/usage_data/topology.rb13
-rw-r--r--lib/gitlab/usage_data_counters/editor_unique_counter.rb56
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb51
-rw-r--r--lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb45
-rw-r--r--lib/gitlab/usage_data_counters/known_events.yml154
-rw-r--r--lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb22
-rw-r--r--lib/gitlab/usage_data_counters/redis_counter.rb6
-rw-r--r--lib/gitlab/usage_data_counters/track_unique_events.rb (renamed from lib/gitlab/usage_data_counters/track_unique_actions.rb)28
-rw-r--r--lib/gitlab/usage_data_queries.rb36
-rw-r--r--lib/gitlab/utils/gzip.rb30
-rw-r--r--lib/gitlab/utils/markdown.rb2
-rw-r--r--lib/gitlab/utils/usage_data.rb47
-rw-r--r--lib/gitlab/web_ide/config.rb4
-rw-r--r--lib/gitlab/web_ide/config/entry/global.rb12
-rw-r--r--lib/gitlab/workhorse.rb5
-rw-r--r--lib/gitlab_danger.rb2
-rw-r--r--lib/mattermost/session.rb4
-rw-r--r--lib/microsoft_teams/notifier.rb2
-rw-r--r--lib/object_storage/config.rb26
-rw-r--r--lib/object_storage/direct_upload.rb8
-rw-r--r--lib/product_analytics/tracker.rb31
-rw-r--r--lib/system_check/app/redis_version_check.rb2
-rw-r--r--lib/tasks/gettext.rake4
-rw-r--r--lib/tasks/gitlab/assets.rake6
-rw-r--r--lib/tasks/gitlab/backup.rake27
-rw-r--r--lib/tasks/gitlab/db.rake7
-rw-r--r--lib/tasks/gitlab/lfs/migrate.rake10
-rw-r--r--lib/tasks/gitlab/sidekiq.rake2
-rw-r--r--lib/tasks/gitlab/usage_data.rake13
-rw-r--r--lib/tasks/pngquant.rake50
-rw-r--r--lib/uploaded_file.rb29
356 files changed, 7207 insertions, 1715 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 2be6792af5f..b37751e1b47 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -167,6 +167,7 @@ module API
mount ::API::GroupVariables
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
+ mount ::API::IssueLinks
mount ::API::Issues
mount ::API::JobArtifacts
mount ::API::Jobs
@@ -193,9 +194,11 @@ module API
mount ::API::NugetPackages
mount ::API::PypiPackages
mount ::API::ComposerPackages
- mount ::API::ConanPackages
+ mount ::API::ConanProjectPackages
+ mount ::API::ConanInstancePackages
mount ::API::MavenPackages
mount ::API::NpmPackages
+ mount ::API::GenericPackages
mount ::API::GoProxy
mount ::API::Pages
mount ::API::PagesDomains
@@ -233,6 +236,7 @@ module API
mount ::API::Templates
mount ::API::Todos
mount ::API::Triggers
+ mount ::API::UsageData
mount ::API::UserCounts
mount ::API::Users
mount ::API::Variables
@@ -244,6 +248,16 @@ module API
mount ::API::Internal::Pages
mount ::API::Internal::Kubernetes
+ version 'v3', using: :path do
+ # Although the following endpoints are kept behind V3 namespace,
+ # they're not deprecated neither should be removed when V3 get
+ # removed. They're needed as a layer to integrate with Jira
+ # Development Panel.
+ namespace '/', requirements: ::API::V3::Github::ENDPOINT_REQUIREMENTS do
+ mount ::API::V3::Github
+ end
+ end
+
route :any, '*path' do
error!('404 Not Found', 404)
end
diff --git a/lib/api/applications.rb b/lib/api/applications.rb
index 4e8d68c8d09..4f2c3ee79ef 100644
--- a/lib/api/applications.rb
+++ b/lib/api/applications.rb
@@ -6,6 +6,15 @@ module API
before { authenticated_as_admin! }
resource :applications do
+ helpers do
+ def validate_redirect_uri(value)
+ uri = ::URI.parse(value)
+ !uri.is_a?(URI::HTTP) || uri.host
+ rescue URI::InvalidURIError
+ false
+ end
+ end
+
desc 'Create a new application' do
detail 'This feature was introduced in GitLab 10.5'
success Entities::ApplicationWithSecret
@@ -19,6 +28,13 @@ module API
desc: 'Application will be used where the client secret is confidential'
end
post do
+ # Validate that host in uri is specified
+ # Please remove it when https://github.com/doorkeeper-gem/doorkeeper/pull/1440 is merged
+ # and the doorkeeper gem version is bumped
+ unless validate_redirect_uri(declared_params[:redirect_uri])
+ render_api_error!({ redirect_uri: ["must be an absolute URI."] }, :bad_request)
+ end
+
application = Doorkeeper::Application.new(declared_params)
if application.save
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index a010e0dd761..045f81074a7 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -15,6 +15,24 @@ module API
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Ci::PipelineBasic
end
+
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: ::CommitStatus::AVAILABLE_STATUSES,
+ coerce_with: ->(scope) {
+ case scope
+ when String
+ [scope]
+ when ::Array
+ scope
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
params do
use :pagination
optional :scope, type: String, values: %w[running pending finished branches tags],
@@ -96,6 +114,64 @@ module API
present pipeline, with: Entities::Ci::Pipeline
end
+ desc 'Get pipeline jobs' do
+ success Entities::Ci::Job
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ use :optional_scope
+ use :pagination
+ end
+
+ get ':id/pipelines/:pipeline_id/jobs' do
+ authorize!(:read_pipeline, user_project)
+
+ pipeline = user_project.all_pipelines.find(params[:pipeline_id])
+
+ if Feature.enabled?(:ci_jobs_finder_refactor)
+ builds = ::Ci::JobsFinder
+ .new(current_user: current_user, pipeline: pipeline, params: params)
+ .execute
+ else
+ authorize!(:read_build, pipeline)
+ builds = pipeline.builds
+ builds = filter_builds(builds, params[:scope])
+ end
+
+ builds = builds.with_preloads
+
+ present paginate(builds), with: Entities::Ci::Job
+ end
+
+ desc 'Get pipeline bridge jobs' do
+ success Entities::Ci::Bridge
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ use :optional_scope
+ use :pagination
+ end
+
+ get ':id/pipelines/:pipeline_id/bridges' do
+ authorize!(:read_build, user_project)
+
+ pipeline = user_project.all_pipelines.find(params[:pipeline_id])
+
+ if Feature.enabled?(:ci_jobs_finder_refactor)
+ bridges = ::Ci::JobsFinder
+ .new(current_user: current_user, pipeline: pipeline, params: params, type: ::Ci::Bridge)
+ .execute
+ else
+ authorize!(:read_pipeline, pipeline)
+ bridges = pipeline.bridges
+ bridges = filter_builds(bridges, params[:scope])
+ end
+
+ bridges = bridges.with_preloads
+
+ present paginate(bridges), with: Entities::Ci::Bridge
+ end
+
desc 'Gets the variables for a given pipeline' do
detail 'This feature was introduced in GitLab 11.11'
success Entities::Ci::Variable
@@ -170,6 +246,21 @@ module API
end
helpers do
+ # NOTE: This method should be removed once the ci_jobs_finder_refactor FF is
+ # removed. https://gitlab.com/gitlab-org/gitlab/-/issues/245183
+ # rubocop: disable CodeReuse/ActiveRecord
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: scope)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def pipeline
strong_memoize(:pipeline) do
user_project.all_pipelines.find(params[:pipeline_id])
@@ -178,7 +269,7 @@ module API
def latest_pipeline
strong_memoize(:latest_pipeline) do
- user_project.latest_pipeline_for_ref(params[:ref])
+ user_project.latest_pipeline(params[:ref])
end
end
end
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 31be1bb7e3e..08903dce3dc 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -159,29 +159,29 @@ module API
end
desc 'Updates a job' do
- http_codes [[200, 'Job was updated'], [403, 'Forbidden']]
+ http_codes [[200, 'Job was updated'],
+ [202, 'Update accepted'],
+ [400, 'Unknown parameters'],
+ [403, 'Forbidden']]
end
params do
requires :token, type: String, desc: %q(Runners's authentication token)
requires :id, type: Integer, desc: %q(Job's ID)
optional :trace, type: String, desc: %q(Job's full trace)
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)
end
put '/:id' do
job = authenticate_job!
- job.trace.set(params[:trace]) if params[:trace]
-
Gitlab::Metrics.add_event(:update_build)
- case params[:state].to_s
- when 'running'
- job.touch if job.needs_touch?
- when 'success'
- job.success!
- when 'failed'
- job.drop!(params[:failure_reason] || :unknown_failure)
+ service = ::Ci::UpdateBuildStateService
+ .new(job, declared_params(include_missing: false))
+
+ service.execute.then do |result|
+ status result.status
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 140351c9e5c..9f5a6e87505 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -117,8 +117,10 @@ module API
render_api_error!('invalid state', 400)
end
- MergeRequest.where(source_project: user_project, source_branch: ref)
- .update_all(head_pipeline_id: pipeline.id) if pipeline.latest?
+ if pipeline.latest?
+ MergeRequest.where(source_project: user_project, source_branch: ref)
+ .update_all(head_pipeline_id: pipeline.id)
+ end
present status, with: Entities::CommitStatus
rescue StateMachines::InvalidTransition => e
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 3c7ed2a25a0..20877fb5c5f 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -136,7 +136,10 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commit(result[:result])
- Gitlab::UsageDataCounters::WebIdeCounter.increment_commits_count if find_user_from_warden
+ if find_user_from_warden
+ Gitlab::UsageDataCounters::WebIdeCounter.increment_commits_count
+ Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user)
+ end
present commit_detail, with: Entities::CommitDetail, stats: params[:stats]
else
diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb
index 05887e58425..31d097c4bea 100644
--- a/lib/api/composer_packages.rb
+++ b/lib/api/composer_packages.rb
@@ -123,7 +123,7 @@ module API
bad_request!
end
- track_event('push_package')
+ package_event('push_package')
::Packages::Composer::CreatePackageService
.new(authorized_user_project, current_user, declared_params)
diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb
new file mode 100644
index 00000000000..209748d79fa
--- /dev/null
+++ b/lib/api/conan_instance_packages.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# Conan Instance-Level Package Manager Client API
+module API
+ class ConanInstancePackages < Grape::API::Instance
+ namespace 'packages/conan/v1' do
+ include ConanPackageEndpoints
+ end
+ end
+end
diff --git a/lib/api/conan_packages.rb b/lib/api/conan_package_endpoints.rb
index 7f2afea9931..445447cfcd2 100644
--- a/lib/api/conan_packages.rb
+++ b/lib/api/conan_package_endpoints.rb
@@ -9,8 +9,8 @@
#
# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
module API
- class ConanPackages < Grape::API::Instance
- helpers ::API::Helpers::PackagesManagerClientsHelpers
+ module ConanPackageEndpoints
+ extend ActiveSupport::Concern
PACKAGE_REQUIREMENTS = {
package_name: API::NO_SLASH_URL_PART_REGEX,
@@ -28,15 +28,19 @@ module API
CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze
- before do
- require_packages_enabled!
+ included do
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+ helpers ::API::Helpers::Packages::Conan::ApiHelpers
+ helpers ::API::Helpers::RelatedResourcesHelpers
- # 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
+ 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
- namespace 'packages/conan/v1' do
desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2'
end
@@ -115,7 +119,7 @@ module API
authorize!(:read_package, project)
presenter = ::Packages::Conan::PackagePresenter.new(
- recipe,
+ package,
current_user,
project,
conan_package_reference: params[:conan_package_reference]
@@ -133,7 +137,7 @@ module API
get do
authorize!(:read_package, project)
- presenter = ::Packages::Conan::PackagePresenter.new(recipe, current_user, project)
+ presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project)
present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot
end
@@ -242,7 +246,7 @@ module API
delete do
authorize!(:destroy_package, project)
- track_event('delete_package')
+ package_event('delete_package', category: 'API::ConanPackages')
package.destroy
end
@@ -295,7 +299,7 @@ module API
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
put 'authorize' do
- authorize_workhorse!(subject: project)
+ authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size)
end
end
@@ -322,7 +326,7 @@ module API
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
put 'authorize' do
- authorize_workhorse!(subject: project)
+ authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size)
end
desc 'Upload package files' do
@@ -341,11 +345,5 @@ module API
end
end
end
-
- helpers do
- include Gitlab::Utils::StrongMemoize
- include ::API::Helpers::RelatedResourcesHelpers
- include ::API::Helpers::Packages::Conan::ApiHelpers
- end
end
end
diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb
new file mode 100644
index 00000000000..c51992231a7
--- /dev/null
+++ b/lib/api/conan_project_packages.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Conan Project-Level Package Manager Client API
+module API
+ class ConanProjectPackages < Grape::API::Instance
+ params do
+ requires :id, type: Integer, desc: 'The ID of a project', regexp: %r{\A[1-9]\d*\z}
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ namespace ':id/packages/conan/v1' do
+ include ConanPackageEndpoints
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/issue_link.rb b/lib/api/entities/issue_link.rb
new file mode 100644
index 00000000000..8e24b046325
--- /dev/null
+++ b/lib/api/entities/issue_link.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssueLink < Grape::Entity
+ expose :source, as: :source_issue, using: ::API::Entities::IssueBasic
+ expose :target, as: :target_issue, using: ::API::Entities::IssueBasic
+ expose :link_type
+ end
+ end
+end
diff --git a/lib/api/entities/milestone.rb b/lib/api/entities/milestone.rb
index 5a0c222d691..b191210a234 100644
--- a/lib/api/entities/milestone.rb
+++ b/lib/api/entities/milestone.rb
@@ -10,6 +10,7 @@ module API
expose :state, :created_at, :updated_at
expose :due_date
expose :start_date
+ expose :expired?, as: :expired
expose :web_url do |milestone, _options|
Gitlab::UrlBuilder.build(milestone)
diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb
index 670965b225c..d903f50befa 100644
--- a/lib/api/entities/package.rb
+++ b/lib/api/entities/package.rb
@@ -28,7 +28,7 @@ module API
expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline
- expose :versions, using: ::API::Entities::PackageVersion
+ expose :versions, using: ::API::Entities::PackageVersion, unless: ->(_, opts) { opts[:collection] }
private
diff --git a/lib/api/entities/related_issue.rb b/lib/api/entities/related_issue.rb
new file mode 100644
index 00000000000..491c606bd49
--- /dev/null
+++ b/lib/api/entities/related_issue.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class RelatedIssue < ::API::Entities::Issue
+ expose :issue_link_id
+ expose :issue_link_type, as: :link_type
+ end
+ end
+end
diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb
new file mode 100644
index 00000000000..98b8a40c7c9
--- /dev/null
+++ b/lib/api/generic_packages.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module API
+ class GenericPackages < Grape::API::Instance
+ before do
+ require_packages_enabled!
+ authenticate!
+
+ require_generic_packages_available!
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ route_setting :authentication, job_token_allowed: true
+
+ namespace ':id/packages/generic' do
+ get 'ping' do
+ :pong
+ end
+ end
+ end
+
+ helpers do
+ include ::API::Helpers::PackagesHelpers
+
+ def require_generic_packages_available!
+ not_found! unless Feature.enabled?(:generic_packages, user_project)
+ end
+ end
+ end
+end
diff --git a/lib/api/github/entities.rb b/lib/api/github/entities.rb
new file mode 100644
index 00000000000..c28a0b8eb7e
--- /dev/null
+++ b/lib/api/github/entities.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+# Simplified version of Github API entities.
+# It's mainly used to mimic Github API and integrate with Jira Development Panel.
+#
+module API
+ module Github
+ module Entities
+ class Repository < Grape::Entity
+ expose :id
+ expose :owner do |project, options|
+ root_namespace = options[:root_namespace] || project.root_namespace
+
+ { login: root_namespace.path }
+ end
+ expose :name do |project, options|
+ ::Gitlab::Jira::Dvcs.encode_project_name(project)
+ end
+ end
+
+ class BranchCommit < Grape::Entity
+ expose :id, as: :sha
+ expose :type do |_|
+ 'commit'
+ end
+ end
+
+ class RepoCommit < Grape::Entity
+ expose :id, as: :sha
+ expose :author do |commit|
+ {
+ login: commit.author&.username,
+ email: commit.author_email
+ }
+ end
+ expose :committer do |commit|
+ {
+ login: commit.author&.username,
+ email: commit.committer_email
+ }
+ end
+ expose :commit do |commit|
+ {
+ author: {
+ name: commit.author_name,
+ email: commit.author_email,
+ date: commit.authored_date.iso8601,
+ type: 'User'
+ },
+ committer: {
+ name: commit.committer_name,
+ email: commit.committer_email,
+ date: commit.committed_date.iso8601,
+ type: 'User'
+ },
+ message: commit.safe_message
+ }
+ end
+ expose :parents do |commit|
+ commit.parent_ids.map { |id| { sha: id } }
+ end
+ expose :files do |commit|
+ commit.diffs.diff_files.flat_map do |diff|
+ additions = diff.added_lines
+ deletions = diff.removed_lines
+
+ if diff.new_file?
+ {
+ status: 'added',
+ filename: diff.new_path,
+ additions: additions,
+ changes: additions
+ }
+ elsif diff.deleted_file?
+ {
+ status: 'removed',
+ filename: diff.old_path,
+ deletions: deletions,
+ changes: deletions
+ }
+ elsif diff.renamed_file?
+ [
+ {
+ status: 'removed',
+ filename: diff.old_path,
+ deletions: deletions,
+ changes: deletions
+ },
+ {
+ status: 'added',
+ filename: diff.new_path,
+ additions: additions,
+ changes: additions
+ }
+ ]
+ else
+ {
+ status: 'modified',
+ filename: diff.new_path,
+ additions: additions,
+ deletions: deletions,
+ changes: (additions + deletions)
+ }
+ end
+ end
+ end
+ end
+
+ class Branch < Grape::Entity
+ expose :name
+
+ expose :commit, using: BranchCommit do |repo_branch, options|
+ options[:project].repository.commit(repo_branch.dereferenced_target)
+ end
+ end
+
+ class User < Grape::Entity
+ expose :id
+ expose :username, as: :login
+ expose :user_url, as: :url
+ expose :user_url, as: :html_url
+ expose :avatar_url
+
+ private
+
+ def user_url
+ Gitlab::Routing.url_helpers.user_url(object)
+ end
+ end
+
+ class NoteableComment < Grape::Entity
+ expose :id
+ expose :author, as: :user, using: User
+ expose :note, as: :body
+ expose :created_at
+ end
+
+ class PullRequest < Grape::Entity
+ expose :title
+ expose :assignee, using: User do |merge_request|
+ merge_request.assignee
+ end
+ expose :author, as: :user, using: User
+ expose :created_at
+ expose :description, as: :body
+ # Since Jira service requests `/repos/-/jira/pulls` (without project
+ # scope), we need to make it work with ID instead IID.
+ expose :id, as: :number
+ # GitHub doesn't have a "merged" or "closed" state. It's just "open" or
+ # "closed".
+ expose :state do |merge_request|
+ case merge_request.state
+ when 'opened', 'locked'
+ 'open'
+ when 'merged'
+ 'closed'
+ else
+ merge_request.state
+ end
+ end
+ expose :merged?, as: :merged
+ expose :merged_at do |merge_request|
+ merge_request.metrics&.merged_at
+ end
+ expose :closed_at do |merge_request|
+ merge_request.metrics&.latest_closed_at
+ end
+ expose :updated_at
+ expose :html_url do |merge_request|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+ expose :head do
+ expose :source_branch, as: :label
+ expose :source_branch, as: :ref
+ expose :source_project, as: :repo, using: Repository
+ end
+ expose :base do
+ expose :target_branch, as: :label
+ expose :target_branch, as: :ref
+ expose :target_project, as: :repo, using: Repository
+ end
+ end
+
+ class PullRequestPayload < Grape::Entity
+ expose :action do |merge_request|
+ case merge_request.state
+ when 'merged', 'closed'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ expose :id
+ expose :pull_request, using: PullRequest do |merge_request|
+ merge_request
+ end
+ end
+
+ class PullRequestEvent < Grape::Entity
+ expose :id do |merge_request|
+ updated_at = merge_request.updated_at.to_i
+ "#{merge_request.id}-#{updated_at}"
+ end
+ expose :type do |_merge_request|
+ 'PullRequestEvent'
+ end
+ expose :updated_at, as: :created_at
+ expose :payload, using: PullRequestPayload do |merge_request|
+ # The merge request data is used by PullRequestPayload and PullRequest, so we just provide it
+ # here. Otherwise Grape::Entity would try to access a field "payload" on Merge Request.
+ merge_request
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 599d5bd0baf..1912a06682e 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -532,11 +532,29 @@ module API
::Gitlab::Tracking.event(category, action.to_s, **args)
rescue => error
- Rails.logger.warn( # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn(
"Tracking event failed for action: #{action}, category: #{category}, 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)
+ return unless Gitlab::CurrentSettings.usage_ping_enabled?
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name)
+ rescue => error
+ Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}")
+ end
+
+ def with_api_params(&block)
+ yield({ api: true, request: request })
+ end
+
protected
def project_finder_params_visibility_ce
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index b7ce1eba3f9..69b53ea6c2f 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -67,7 +67,7 @@ module API
result == 'PONG'
rescue => e
- Rails.logger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}")
false
end
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
index 1161d1386bb..dcbf933a4e1 100644
--- a/lib/api/helpers/packages/conan/api_helpers.rb
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -5,11 +5,13 @@ module API
module Packages
module Conan
module ApiHelpers
+ include Gitlab::Utils::StrongMemoize
+
def present_download_urls(entity)
authorize!(:read_package, project)
presenter = ::Packages::Conan::PackagePresenter.new(
- recipe,
+ package,
current_user,
project,
conan_package_reference: params[:conan_package_reference]
@@ -31,7 +33,7 @@ module API
def recipe_upload_urls
{ upload_urls: Hash[
file_names.select(&method(:recipe_file?)).map do |file_name|
- [file_name, recipe_file_upload_url(file_name)]
+ [file_name, build_recipe_file_upload_url(file_name)]
end
] }
end
@@ -39,7 +41,7 @@ module API
def package_upload_urls
{ upload_urls: Hash[
file_names.select(&method(:package_file?)).map do |file_name|
- [file_name, package_file_upload_url(file_name)]
+ [file_name, build_package_file_upload_url(file_name)]
end
] }
end
@@ -52,32 +54,58 @@ module API
file_name.in?(::Packages::Conan::FileMetadatum::PACKAGE_FILES)
end
- def package_file_upload_url(file_name)
- expose_url(
- api_v4_packages_conan_v1_files_package_path(
- package_name: params[:package_name],
- package_version: params[:package_version],
- package_username: params[:package_username],
- package_channel: params[:package_channel],
- recipe_revision: '0',
- conan_package_reference: params[:conan_package_reference],
- package_revision: '0',
- file_name: file_name
- )
+ def build_package_file_upload_url(file_name)
+ options = url_options(file_name).merge(
+ conan_package_reference: params[:conan_package_reference],
+ package_revision: ::Packages::Conan::FileMetadatum::DEFAULT_PACKAGE_REVISION
)
+
+ package_file_url(options)
+ end
+
+ def build_recipe_file_upload_url(file_name)
+ recipe_file_url(url_options(file_name))
end
- def recipe_file_upload_url(file_name)
- expose_url(
- api_v4_packages_conan_v1_files_export_path(
- package_name: params[:package_name],
- package_version: params[:package_version],
- package_username: params[:package_username],
- package_channel: params[:package_channel],
- recipe_revision: '0',
- file_name: file_name
+ def url_options(file_name)
+ {
+ package_name: params[:package_name],
+ package_version: params[:package_version],
+ package_username: params[:package_username],
+ package_channel: params[:package_channel],
+ file_name: file_name,
+ recipe_revision: ::Packages::Conan::FileMetadatum::DEFAULT_RECIPE_REVISION
+ }
+ end
+
+ def package_file_url(options)
+ case package_scope
+ when :project
+ expose_url(
+ api_v4_projects_packages_conan_v1_files_package_path(
+ options.merge(id: project.id)
+ )
)
- )
+ when :instance
+ expose_url(
+ api_v4_packages_conan_v1_files_package_path(options)
+ )
+ end
+ end
+
+ def recipe_file_url(options)
+ case package_scope
+ when :project
+ expose_url(
+ api_v4_projects_packages_conan_v1_files_export_path(
+ options.merge(id: project.id)
+ )
+ )
+ when :instance
+ expose_url(
+ api_v4_packages_conan_v1_files_export_path(options)
+ )
+ end
end
def recipe
@@ -86,16 +114,23 @@ module API
def project
strong_memoize(:project) do
- full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
- Project.find_by_full_path(full_path)
+ case package_scope
+ when :project
+ find_project!(params[:id])
+ when :instance
+ full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
+ find_project!(full_path)
+ end
end
end
def package
strong_memoize(:package) do
project.packages
+ .conan
.with_name(params[:package_name])
.with_version(params[:package_version])
+ .with_conan_username(params[:package_username])
.with_conan_channel(params[:package_channel])
.order_created
.last
@@ -123,7 +158,7 @@ module API
conan_package_reference: params[:conan_package_reference]
).execute!
- track_event('pull_package') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY
+ package_event('pull_package', category: 'API::ConanPackages') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY
present_carrierwave_file!(package_file.file)
end
@@ -134,7 +169,7 @@ module API
def track_push_package_event
if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params[:file].size > 0 # rubocop: disable Style/ZeroLengthPredicate
- track_event('push_package')
+ package_event('push_package', category: 'API::ConanPackages')
end
end
@@ -155,6 +190,7 @@ module API
def upload_package_file(file_type)
authorize_upload!(project)
+ bad_request!('File is too large') if project.actual_limits.exceeded?(:conan_max_file_size, params['file.size'].to_i)
current_package = find_or_create_package
@@ -234,6 +270,10 @@ module API
token
end
+
+ def package_scope
+ params[:id].present? ? :project : :instance
+ end
end
end
end
diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb
index c6037d52de9..403f5ea3851 100644
--- a/lib/api/helpers/packages_helpers.rb
+++ b/lib/api/helpers/packages_helpers.rb
@@ -47,6 +47,10 @@ module API
authorize_create_package!(subject)
require_gitlab_workhorse!
end
+
+ def package_event(event_name, **args)
+ track_event(event_name, **args)
+ end
end
end
end
diff --git a/lib/api/helpers/packages_manager_clients_helpers.rb b/lib/api/helpers/packages_manager_clients_helpers.rb
index 955d21cb44f..e7662b03577 100644
--- a/lib/api/helpers/packages_manager_clients_helpers.rb
+++ b/lib/api/helpers/packages_manager_clients_helpers.rb
@@ -6,18 +6,8 @@ module API
extend Grape::API::Helpers
include ::API::Helpers::PackagesHelpers
- params :workhorse_upload_params do
- optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
- optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
- optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
- optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
- optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
- optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
- optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
- end
-
def find_job_from_http_basic_auth
- return unless headers
+ return unless request.headers
token = decode_token
@@ -27,7 +17,7 @@ module API
end
def find_deploy_token_from_http_basic_auth
- return unless headers
+ return unless request.headers
token = decode_token
@@ -36,16 +26,10 @@ module API
DeployToken.active.find_by_token(token)
end
- def uploaded_package_file(param_name = :file)
- uploaded_file = UploadedFile.from_params(params, param_name, ::Packages::PackageFileUploader.workhorse_local_upload_path)
- bad_request!('Missing package file!') unless uploaded_file
- uploaded_file
- end
-
private
def decode_token
- encoded_credentials = headers['Authorization'].to_s.split('Basic ', 2).second
+ encoded_credentials = request.headers['Authorization'].to_s.split('Basic ', 2).second
Base64.decode64(encoded_credentials || '').split(':', 2).second
end
end
diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb
index 936684ea1f8..cb5f92fa62a 100644
--- a/lib/api/helpers/search_helpers.rb
+++ b/lib/api/helpers/search_helpers.rb
@@ -17,6 +17,10 @@ module API
# This is a separate method so that EE can redefine it.
%w(issues merge_requests milestones notes wiki_blobs commits blobs users)
end
+
+ def self.search_states
+ %w(all opened closed merged)
+ end
end
end
end
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index ff938358439..4bceda51900 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -631,12 +631,26 @@ module API
name: :issues_url,
type: String,
desc: 'The issues URL'
+ }
+ ],
+ 'ewm' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New Issue URL'
},
{
- required: false,
- name: :description,
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
type: String,
- desc: 'The description of the tracker'
+ desc: 'Issues URL'
}
],
'youtrack' => [
@@ -651,12 +665,6 @@ module API
name: :issues_url,
type: String,
desc: 'The issues URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'The description of the tracker'
}
],
'slack' => [
@@ -747,6 +755,7 @@ module API
::DiscordService,
::DroneCiService,
::EmailsOnPushService,
+ ::EwmService,
::ExternalWikiService,
::FlowdockService,
::HangoutsChatService,
diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb
index 79367da8d1f..9224381735f 100644
--- a/lib/api/helpers/snippets_helpers.rb
+++ b/lib/api/helpers/snippets_helpers.rb
@@ -27,6 +27,24 @@ module API
exactly_one_of :files, :content
end
+ params :update_file_params do |options|
+ optional :files, type: Array, desc: 'An array of files to update' do
+ requires :action, type: String,
+ values: SnippetInputAction::ACTIONS.map(&:to_s),
+ desc: "The type of action to perform on the file, must be one of: #{SnippetInputAction::ACTIONS.join(", ")}"
+ optional :content, type: String, desc: 'The content of a snippet'
+ optional :file_path, file_path: true, type: String, desc: 'The file path of a snippet file'
+ optional :previous_path, file_path: true, type: String, desc: 'The previous path of a snippet file'
+ end
+
+ mutually_exclusive :files, :content
+ mutually_exclusive :files, :file_name
+ end
+
+ params :minimum_update_params do
+ at_least_one_of :content, :description, :files, :file_name, :title, :visibility
+ end
+
def content_for(snippet)
if snippet.empty_repo?
env['api.format'] = :txt
@@ -53,10 +71,30 @@ module API
end
end
- def process_file_args(args)
- args[:snippet_actions] = args.delete(:files)&.map do |file|
- file[:action] = :create
- file.symbolize_keys
+ def process_create_params(args)
+ with_api_params do |api_params|
+ args[:snippet_actions] = args.delete(:files)&.map do |file|
+ file[:action] = :create
+ file.symbolize_keys
+ end
+
+ args.merge(api_params)
+ end
+ end
+
+ def process_update_params(args)
+ with_api_params do |api_params|
+ args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys)
+
+ args.merge(api_params)
+ end
+ end
+
+ def validate_params_for_multiple_files(snippet)
+ return unless params[:content] || params[:file_name]
+
+ if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files?
+ render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400)
end
end
end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 17599c72243..ff687a57888 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -241,14 +241,16 @@ module API
break { success: false, message: "Invalid token expiry date: '#{params[:expires_at]}'" }
end
- access_token = nil
+ result = ::PersonalAccessTokens::CreateService.new(
+ user, name: params[:name], scopes: params[:scopes], expires_at: expires_at
+ ).execute
- ::Users::UpdateService.new(current_user, user: user).execute! do |user|
- access_token = user.personal_access_tokens.create!(
- name: params[:name], scopes: params[:scopes], expires_at: expires_at
- )
+ unless result.status == :success
+ break { success: false, message: "Failed to create token: #{result.message}" }
end
+ access_token = result.payload[:personal_access_token]
+
{ success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at }
end
diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb
index 7f64fd7efe3..6d5dfd086e7 100644
--- a/lib/api/internal/kubernetes.rb
+++ b/lib/api/internal/kubernetes.rb
@@ -4,7 +4,16 @@ module API
# Kubernetes Internal API
module Internal
class Kubernetes < Grape::API::Instance
+ before do
+ check_feature_enabled
+ authenticate_gitlab_kas_request!
+ end
+
helpers do
+ def authenticate_gitlab_kas_request!
+ unauthorized! unless Gitlab::Kas.verify_api_request(headers)
+ end
+
def agent_token
@agent_token ||= cluster_agent_token_from_authorization_token
end
@@ -36,7 +45,7 @@ module API
end
def check_feature_enabled
- not_found! unless Feature.enabled?(:kubernetes_agent_internal_api)
+ not_found! unless Feature.enabled?(:kubernetes_agent_internal_api, default_enabled: true, type: :ops)
end
def check_agent_token
@@ -47,7 +56,6 @@ module API
namespace 'internal' do
namespace 'kubernetes' do
before do
- check_feature_enabled
check_agent_token
end
@@ -89,6 +97,26 @@ module API
}
end
end
+
+ namespace 'kubernetes/usage_metrics' do
+ desc 'POST usage metrics' do
+ detail 'Updates usage metrics for agent'
+ end
+ params do
+ requires :gitops_sync_count, type: Integer, desc: 'The count to increment the gitops_sync metric by'
+ end
+ post '/' do
+ gitops_sync_count = params[:gitops_sync_count]
+
+ if gitops_sync_count < 0
+ bad_request!('gitops_sync_count must be greater than or equal to zero')
+ else
+ Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_gitops_sync(gitops_sync_count)
+
+ no_content!
+ end
+ end
+ end
end
end
end
diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb
new file mode 100644
index 00000000000..6cc5b344f47
--- /dev/null
+++ b/lib/api/issue_links.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module API
+ class IssueLinks < Grape::API::Instance
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get related issues' do
+ success Entities::RelatedIssue
+ end
+ get ':id/issues/:issue_iid/links' do
+ source_issue = find_project_issue(params[:issue_iid])
+ related_issues = source_issue.related_issues(current_user)
+
+ present related_issues,
+ with: Entities::RelatedIssue,
+ current_user: current_user,
+ project: user_project
+ end
+
+ desc 'Relate issues' do
+ success Entities::IssueLink
+ end
+ params do
+ requires :target_project_id, type: String, desc: 'The ID of the target project'
+ requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue'
+ optional :link_type, type: String, values: IssueLink.link_types.keys,
+ desc: 'The type of the relation'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ post ':id/issues/:issue_iid/links' do
+ source_issue = find_project_issue(params[:issue_iid])
+ target_issue = find_project_issue(declared_params[:target_issue_iid],
+ declared_params[:target_project_id])
+
+ create_params = { target_issuable: target_issue, link_type: declared_params[:link_type] }
+
+ result = ::IssueLinks::CreateService
+ .new(source_issue, current_user, create_params)
+ .execute
+
+ if result[:status] == :success
+ issue_link = IssueLink.find_by!(source: source_issue, target: target_issue)
+
+ present issue_link, with: Entities::IssueLink
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ desc 'Remove issues relation' do
+ success Entities::IssueLink
+ end
+ params do
+ requires :issue_link_id, type: Integer, desc: 'The ID of an issue link'
+ end
+ delete ':id/issues/:issue_iid/links/:issue_link_id' do
+ issue_link = IssueLink.find(declared_params[:issue_link_id])
+
+ find_project_issue(params[:issue_iid])
+ find_project_issue(issue_link.target.iid.to_s, issue_link.target.project_id.to_s)
+
+ result = ::IssueLinks::DestroyService
+ .new(issue_link, current_user)
+ .execute
+
+ if result[:status] == :success
+ present issue_link, with: Entities::IssueLink
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 1694a967f26..0e5b0fae6e2 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -114,6 +114,19 @@ module API
present issues, options
end
+
+ desc "Get specified issue (admin only)" do
+ success Entities::Issue
+ end
+ params do
+ requires :id, type: String, desc: 'The ID of the Issue'
+ end
+ get ":id" do
+ authenticated_as_admin!
+ issue = Issue.find(params['id'])
+
+ present issue, with: Entities::Issue, current_user: current_user, project: issue.project
+ end
end
params do
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 084c146abe7..ad46d948f3b 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -48,54 +48,6 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
- desc 'Get pipeline jobs' do
- success Entities::Ci::Job
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- use :optional_scope
- use :pagination
- end
- # rubocop: disable CodeReuse/ActiveRecord
- get ':id/pipelines/:pipeline_id/jobs' do
- authorize!(:read_pipeline, user_project)
- pipeline = user_project.all_pipelines.find(params[:pipeline_id])
- authorize!(:read_build, pipeline)
-
- builds = pipeline.builds
- builds = filter_builds(builds, params[:scope])
- builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace])
-
- present paginate(builds), with: Entities::Ci::Job
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- desc 'Get pipeline bridge jobs' do
- success ::API::Entities::Ci::Bridge
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- use :optional_scope
- use :pagination
- end
- # rubocop: disable CodeReuse/ActiveRecord
- get ':id/pipelines/:pipeline_id/bridges' do
- authorize!(:read_build, user_project)
- pipeline = user_project.ci_pipelines.find(params[:pipeline_id])
- authorize!(:read_pipeline, pipeline)
-
- bridges = pipeline.bridges
- bridges = filter_builds(bridges, params[:scope])
- bridges = bridges.preload(
- :metadata,
- downstream_pipeline: [project: [:route, { namespace: :route }]],
- project: [:namespace]
- )
-
- present paginate(bridges), with: ::API::Entities::Ci::Bridge
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
desc 'Get a specific job of a project' do
success Entities::Ci::Job
end
diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb
index 32a45c59cfa..e6d9a9a7c20 100644
--- a/lib/api/maven_packages.rb
+++ b/lib/api/maven_packages.rb
@@ -107,7 +107,7 @@ module API
when 'sha1'
package_file.file_sha1
else
- track_event('pull_package') if jar_file?(format)
+ package_event('pull_package') if jar_file?(format)
present_carrierwave_file_with_head_support!(package_file.file)
end
end
@@ -145,7 +145,7 @@ module API
when 'sha1'
package_file.file_sha1
else
- track_event('pull_package') if jar_file?(format)
+ package_event('pull_package') if jar_file?(format)
present_carrierwave_file_with_head_support!(package_file.file)
end
@@ -181,7 +181,7 @@ module API
when 'sha1'
package_file.file_sha1
else
- track_event('pull_package') if jar_file?(format)
+ package_event('pull_package') if jar_file?(format)
present_carrierwave_file_with_head_support!(package_file.file)
end
@@ -200,7 +200,7 @@ module API
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
- ::Packages::PackageFileUploader.workhorse_authorize(has_length: true)
+ ::Packages::PackageFileUploader.workhorse_authorize(has_length: true, maximum_size: user_project.actual_limits.maven_max_file_size)
end
desc 'Upload the maven package file' do
@@ -214,6 +214,7 @@ module API
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_upload!
+ bad_request!('File is too large') if user_project.actual_limits.exceeded?(:maven_max_file_size, params[:file].size)
file_name, format = extract_format(params[:file_name])
@@ -232,7 +233,7 @@ module API
when 'md5'
nil
else
- track_event('push_package') if jar_file?(format)
+ package_event('push_package') if jar_file?(format)
file_params = {
file: params[:file],
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 6f25df720c4..4bd72b267a9 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -29,11 +29,13 @@ module API
remove_labels
milestone_id
remove_source_branch
- state_event
+ allow_collaboration
+ allow_maintainer_to_push
+ squash
target_branch
title
+ state_event
discussion_locked
- squash
]
end
@@ -154,13 +156,13 @@ module API
helpers do
params :optional_params do
- optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :assignee_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The array of user IDs to assign issue'
- optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
+ optional :description, type: String, desc: 'The description of the merge request'
optional :labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :add_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :remove_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch'
optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration'
diff --git a/lib/api/npm_packages.rb b/lib/api/npm_packages.rb
index 21ca57b7985..fca405b76b7 100644
--- a/lib/api/npm_packages.rb
+++ b/lib/api/npm_packages.rb
@@ -141,7 +141,7 @@ module API
package_file = ::Packages::PackageFileFinder
.new(package, params[:file_name]).execute!
- track_event('pull_package')
+ package_event('pull_package')
present_carrierwave_file!(package_file.file)
end
@@ -157,7 +157,7 @@ module API
put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do
authorize_create_package!(user_project)
- track_event('push_package')
+ package_event('push_package')
created_package = ::Packages::Npm::CreatePackageService
.new(user_project, current_user, params.merge(build: current_authenticated_job)).execute
diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb
index 56c4de2071d..f84a3acbe6d 100644
--- a/lib/api/nuget_packages.rb
+++ b/lib/api/nuget_packages.rb
@@ -92,6 +92,7 @@ module API
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],
@@ -104,7 +105,7 @@ module API
package_file = ::Packages::CreatePackageFileService.new(package, file_params)
.execute
- track_event('push_package')
+ package_event('push_package')
::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
@@ -118,7 +119,11 @@ module API
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)
+ authorize_workhorse!(
+ subject: authorized_user_project,
+ has_length: false,
+ maximum_size: authorized_user_project.actual_limits.nuget_max_file_size
+ )
end
params do
@@ -193,7 +198,7 @@ module API
not_found!('Package') unless package_file
- track_event('pull_package')
+ package_event('pull_package')
# 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)
@@ -228,7 +233,7 @@ module API
.new(authorized_user_project, params[:q], search_options)
.execute
- track_event('search_package')
+ package_event('search_package')
present ::Packages::Nuget::SearchResultsPresenter.new(search),
with: ::API::Entities::Nuget::SearchResults
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index fba4c60504f..f6e87fece89 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -64,12 +64,8 @@ module API
end
post ":id/snippets" do
authorize! :create_snippet, user_project
- snippet_params = declared_params(include_missing: false).tap do |create_args|
- create_args[:request] = request
- create_args[:api] = true
- process_file_args(create_args)
- end
+ snippet_params = process_create_params(declared_params(include_missing: false))
service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute
snippet = service_response.payload[:snippet]
@@ -88,14 +84,16 @@ module API
end
params do
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
- optional :title, type: String, allow_blank: false, desc: 'The title of the snippet'
- optional :file_name, type: String, desc: 'The file name of the snippet'
optional :content, type: String, allow_blank: false, desc: 'The content of the snippet'
optional :description, type: String, desc: 'The description of a snippet'
+ optional :file_name, type: String, desc: 'The file name of the snippet'
+ optional :title, type: String, allow_blank: false, desc: 'The title of the snippet'
optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
desc: 'The visibility of the snippet'
- at_least_one_of :title, :file_name, :content, :visibility_level
+
+ use :update_file_params
+ use :minimum_update_params
end
# rubocop: disable CodeReuse/ActiveRecord
put ":id/snippets/:snippet_id" do
@@ -104,8 +102,9 @@ module API
authorize! :update_snippet, snippet
- snippet_params = declared_params(include_missing: false)
- .merge(request: request, api: true)
+ validate_params_for_multiple_files(snippet)
+
+ snippet_params = process_update_params(declared_params(include_missing: false))
service_response = ::Snippets::UpdateService.new(user_project, current_user, snippet_params).execute(snippet)
snippet = service_response.payload[:snippet]
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index 739928a61ed..c07db68f8a8 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -64,7 +64,7 @@ module API
requires :sha256, type: String, desc: 'The PyPi package sha256 check sum'
end
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get 'files/:sha256/*file_identifier' do
project = unauthorized_user_project!
@@ -72,7 +72,7 @@ module API
package = packages_finder(project).by_file_name_and_sha256(filename, params[:sha256])
package_file = ::Packages::PackageFileFinder.new(package, filename, with_file_name_like: false).execute
- track_event('pull_package')
+ package_event('pull_package')
present_carrierwave_file!(package_file.file, supports_direct_download: true)
end
@@ -87,11 +87,11 @@ module API
# An Api entry point but returns an HTML file instead of JSON.
# PyPi simple API returns the package descriptor as a simple HTML file.
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get 'simple/*package_name', format: :txt do
authorize_read_package!(authorized_user_project)
- track_event('list_package')
+ package_event('list_package')
packages = find_package_versions
presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project)
@@ -117,11 +117,12 @@ module API
optional :sha256_digest, type: String
end
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
post do
authorize_upload!(authorized_user_project)
+ bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size)
- track_event('push_package')
+ package_event('push_package')
::Packages::Pypi::CreatePackageService
.new(authorized_user_project, current_user, declared_params)
@@ -134,9 +135,13 @@ module API
forbidden!
end
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
post 'authorize' do
- authorize_workhorse!(subject: authorized_user_project, has_length: false)
+ authorize_workhorse!(
+ subject: authorized_user_project,
+ has_length: false,
+ maximum_size: authorized_user_project.actual_limits.pypi_max_file_size
+ )
end
end
end
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 53095e0b81a..b9c6a823f4f 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -32,6 +32,7 @@ module API
search_params = {
scope: params[:scope],
search: params[:search],
+ state: params[:state],
snippets: snippets?,
page: params[:page],
per_page: params[:per_page]
@@ -79,6 +80,7 @@ module API
type: String,
desc: 'The scope of the search',
values: Helpers::SearchHelpers.global_search_scopes
+ optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states
use :pagination
end
get do
@@ -100,6 +102,7 @@ module API
type: String,
desc: 'The scope of the search',
values: Helpers::SearchHelpers.group_search_scopes
+ optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states
use :pagination
end
get ':id/(-/)search' do
@@ -122,6 +125,7 @@ module API
desc: 'The scope of the search',
values: Helpers::SearchHelpers.project_search_scopes
optional :ref, type: String, desc: 'The name of a repository branch or tag. If not given, the default branch is used'
+ optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states
use :pagination
end
get ':id/(-/)search' do
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index f2e0aaecfb9..6e5534d0c9a 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -61,6 +61,10 @@ module API
end
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
+ optional :gitpod_enabled, type: Boolean, desc: 'Enable Gitpod'
+ given gitpod_enabled: ->(val) { val } do
+ requires :gitpod_url, type: String, desc: 'The configured Gitpod instance URL'
+ end
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
@@ -140,11 +144,9 @@ module API
end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
- optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
- optional :snowplow_iglu_registry_url, type: String, desc: 'The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events'
given snowplow_enabled: ->(val) { val } do
requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname'
optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain'
@@ -152,6 +154,7 @@ module API
end
optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute."
optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute."
+ optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes"
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
index de1373144e3..77f2b1e871e 100644
--- a/lib/api/sidekiq_metrics.rb
+++ b/lib/api/sidekiq_metrics.rb
@@ -17,7 +17,7 @@ module API
end
def process_metrics
- Sidekiq::ProcessSet.new.map do |process|
+ Sidekiq::ProcessSet.new(false).map do |process|
{
hostname: process['hostname'],
pid: process['pid'],
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 1a3283aed98..c6ef35875fc 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -76,12 +76,7 @@ module API
post do
authorize! :create_snippet
- attrs = declared_params(include_missing: false).tap do |create_args|
- create_args[:request] = request
- create_args[:api] = true
-
- process_file_args(create_args)
- end
+ attrs = process_create_params(declared_params(include_missing: false))
service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute
snippet = service_response.payload[:snippet]
@@ -99,16 +94,19 @@ module API
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
+
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
- optional :title, type: String, allow_blank: false, desc: 'The title of a snippet'
- optional :file_name, type: String, desc: 'The name of a snippet file'
optional :content, type: String, allow_blank: false, desc: 'The content of a snippet'
optional :description, type: String, desc: 'The description of a snippet'
+ optional :file_name, type: String, desc: 'The name of a snippet file'
+ optional :title, type: String, allow_blank: false, desc: 'The title of a snippet'
optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
desc: 'The visibility of the snippet'
- at_least_one_of :title, :file_name, :content, :visibility
+
+ use :update_file_params
+ use :minimum_update_params
end
put ':id' do
snippet = snippets_for_current_user.find_by_id(params.delete(:id))
@@ -116,8 +114,12 @@ module API
authorize! :update_snippet, snippet
- attrs = declared_params(include_missing: false).merge(request: request, api: true)
+ validate_params_for_multiple_files(snippet)
+
+ attrs = process_update_params(declared_params(include_missing: false))
+
service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet)
+
snippet = service_response.payload[:snippet]
if service_response.success?
diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb
index f6e966defce..7063a3d08b5 100644
--- a/lib/api/terraform/state.rb
+++ b/lib/api/terraform/state.rb
@@ -35,10 +35,10 @@ module API
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get do
remote_state_handler.find_with_lock do |state|
- no_content! unless state.file.exists?
+ no_content! unless state.latest_file && state.latest_file.exists?
env['api.format'] = :binary # this bypasses json serialization
- body state.file.read
+ body state.latest_file.read
status :ok
end
end
@@ -52,8 +52,7 @@ module API
no_content! if data.empty?
remote_state_handler.handle_with_lock do |state|
- state.file = CarrierWaveStringFile.new(data)
- state.save!
+ state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial])
status :ok
end
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 4a73e3e0e94..5eae92a251e 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -39,8 +39,17 @@ module API
resource :todos do
helpers do
+ params :todo_filters do
+ optional :action, String, values: Todo::ACTION_NAMES.values.map(&:to_s)
+ optional :author_id, Integer
+ optional :state, String, values: Todo.state_machine.states.map(&:name).map(&:to_s)
+ optional :type, String, values: TodosFinder.todo_types
+ optional :project_id, Integer
+ optional :group_id, Integer
+ end
+
def find_todos
- TodosFinder.new(current_user, params).execute
+ TodosFinder.new(current_user, declared_params(include_missing: false)).execute
end
def issuable_and_awardable?(type)
@@ -72,7 +81,7 @@ module API
success Entities::Todo
end
params do
- use :pagination
+ use :pagination, :todo_filters
end
get do
todos = paginate(find_todos.with_entity_associations)
diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb
new file mode 100644
index 00000000000..a1512197ee1
--- /dev/null
+++ b/lib/api/usage_data.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module API
+ class UsageData < Grape::API::Instance
+ before { authenticate! }
+
+ namespace 'usage_data' do
+ before do
+ not_found! unless Feature.enabled?(:usage_data_api)
+ forbidden!('Invalid CSRF token is provided') unless verified_request?
+ end
+
+ desc 'Track usage data events' do
+ detail 'This feature was introduced in GitLab 13.4.'
+ 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]
+
+ increment_unique_values(event_name, current_user.id)
+
+ status :ok
+ end
+ end
+ end
+end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 335624963aa..73bb43b88fc 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -84,6 +84,7 @@ module API
optional :created_after, type: DateTime, desc: 'Return users created after the specified time'
optional :created_before, type: DateTime, desc: 'Return users created before the specified time'
optional :without_projects, type: Boolean, default: false, desc: 'Filters only users without projects'
+ optional :exclude_internal, as: :non_internal, type: Boolean, default: false, desc: 'Filters only non internal users'
all_or_none_of :extern_uid, :provider
use :sort_params
@@ -115,6 +116,7 @@ module API
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
+ users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
users = users.preload(:user_detail)
@@ -217,9 +219,15 @@ module API
.where.not(id: user.id).exists?
user_params = declared_params(include_missing: false)
+ admin_making_changes_for_another_user = (current_user != user)
- user_params[:password_expires_at] = Time.current if user_params[:password].present?
- result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute
+ if user_params[:password].present?
+ user_params[:password_expires_at] = Time.current if admin_making_changes_for_another_user
+ end
+
+ result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute do |user|
+ user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user
+ end
if result[:status] == :success
present user, with: Entities::UserWithAdmin, current_user: current_user
diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb
new file mode 100644
index 00000000000..593f90460ac
--- /dev/null
+++ b/lib/api/v3/github.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+# These endpoints partially mimic Github API behavior in order to successfully
+# integrate with Jira Development Panel.
+# Endpoints returning an empty list were temporarily added to avoid 404's
+# during Jira's DVCS integration.
+#
+module API
+ module V3
+ class Github < Grape::API::Instance
+ NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze
+ ENDPOINT_REQUIREMENTS = {
+ namespace: NO_SLASH_URL_PART_REGEX,
+ project: NO_SLASH_URL_PART_REGEX,
+ username: NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ # Used to differentiate Jira Cloud requests from Jira Server requests
+ # Jira Cloud user agent format: Jira DVCS Connector Vertigo/version
+ # Jira Server user agent format: Jira DVCS Connector/version
+ JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'.freeze
+
+ include PaginationParams
+
+ before do
+ authorize_jira_user_agent!(request)
+ authenticate!
+ end
+
+ helpers do
+ params :project_full_path do
+ requires :namespace, type: String
+ requires :project, type: String
+ end
+
+ def authorize_jira_user_agent!(request)
+ not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env)
+ end
+
+ def update_project_feature_usage_for(project)
+ # Prevent errors on GitLab Geo not allowing
+ # UPDATE statements to happen in GET requests.
+ return if Gitlab::Database.read_only?
+
+ project.log_jira_dvcs_integration_usage(cloud: jira_cloud?)
+ end
+
+ def jira_cloud?
+ request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT)
+ end
+
+ def find_project_with_access(params)
+ project = find_project!(
+ ::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys)
+ )
+ not_found! unless can?(current_user, :download_code, project)
+ project
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def find_merge_requests
+ merge_requests = authorized_merge_requests.reorder(updated_at: :desc)
+ paginate(merge_requests)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def find_merge_request_with_access(id, access_level = :read_merge_request)
+ merge_request = authorized_merge_requests.find_by(id: id)
+ not_found! unless can?(current_user, access_level, merge_request)
+ merge_request
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def authorized_merge_requests
+ MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?).execute
+ end
+
+ def authorized_merge_requests_for_project(project)
+ MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?, project_id: project.id).execute
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def find_notes(noteable)
+ # They're not presented on Jira Dev Panel ATM. A comments count with a
+ # redirect link is presented.
+ notes = paginate(noteable.notes.user.reorder(nil))
+ notes.select { |n| n.readable_by?(current_user) }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ resource :orgs do
+ get ':namespace/repos' do
+ present []
+ end
+ end
+
+ resource :user do
+ get :repos do
+ present []
+ end
+ end
+
+ resource :users do
+ params do
+ use :pagination
+ end
+
+ get ':namespace/repos' do
+ namespace = Namespace.find_by_full_path(params[:namespace])
+ not_found!('Namespace') unless namespace
+
+ projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects
+ projects = projects.in_namespace(namespace.self_and_descendants)
+
+ projects_cte = Project.wrap_with_cte(projects)
+ .eager_load_namespace_and_owner
+ .with_route
+
+ present paginate(projects_cte),
+ with: ::API::Github::Entities::Repository,
+ root_namespace: namespace.root_ancestor
+ end
+
+ get ':username' do
+ forbidden! unless can?(current_user, :read_users_list)
+ user = UsersFinder.new(current_user, { username: params[:username] }).execute.first
+ not_found! unless user
+ present user, with: ::API::Github::Entities::User
+ end
+ end
+
+ # Jira dev panel integration weirdly requests for "/-/jira/pulls" instead
+ # "/api/v3/repos/<namespace>/<project>/pulls". This forces us into
+ # returning _all_ Merge Requests from authorized projects (user is a member),
+ # instead just the authorized MRs from a project.
+ # Jira handles the filtering, presenting just MRs mentioning the Jira
+ # issue ID on the MR title / description.
+ resource :repos do
+ # Keeping for backwards compatibility with old Jira integration instructions
+ # so that users that do not change it will not suddenly have a broken integration
+ get '/-/jira/pulls' do
+ present find_merge_requests, with: ::API::Github::Entities::PullRequest
+ end
+
+ get '/-/jira/events' do
+ present []
+ end
+
+ params do
+ use :project_full_path
+ end
+ get ':namespace/:project/pulls' do
+ user_project = find_project_with_access(params)
+
+ merge_requests = authorized_merge_requests_for_project(user_project)
+
+ present paginate(merge_requests), with: ::API::Github::Entities::PullRequest
+ end
+
+ params do
+ use :project_full_path
+ end
+ get ':namespace/:project/pulls/:id' do
+ merge_request = find_merge_request_with_access(params[:id])
+
+ present merge_request, with: ::API::Github::Entities::PullRequest
+ end
+
+ # In Github, each Merge Request is automatically also an issue.
+ # Therefore we return its comments here.
+ # It'll present _just_ the comments counting with a link to GitLab on
+ # Jira dev panel, not the actual note content.
+ get ':namespace/:project/issues/:id/comments' do
+ merge_request = find_merge_request_with_access(params[:id])
+
+ present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment
+ end
+
+ # This refer to "review" comments but Jira dev panel doesn't seem to
+ # present it accordingly.
+ get ':namespace/:project/pulls/:id/comments' do
+ present []
+ end
+
+ # Commits are not presented within "Pull Requests" modal on Jira dev
+ # panel.
+ get ':namespace/:project/pulls/:id/commits' do
+ present []
+ end
+
+ # Self-hosted Jira (tested on 7.11.1) requests this endpoint right
+ # after fetching branches.
+ get ':namespace/:project/events' do
+ user_project = find_project_with_access(params)
+
+ merge_requests = authorized_merge_requests_for_project(user_project)
+
+ present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent
+ end
+
+ params do
+ use :project_full_path
+ use :pagination
+ end
+ get ':namespace/:project/branches' do
+ user_project = find_project_with_access(params)
+
+ update_project_feature_usage_for(user_project)
+
+ branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
+
+ present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project
+ end
+
+ params do
+ use :project_full_path
+ end
+ get ':namespace/:project/commits/:sha' do
+ user_project = find_project_with_access(params)
+
+ commit = user_project.commit(params[:sha])
+
+ not_found! 'Commit' unless commit
+
+ present commit, with: ::API::Github::Entities::RepoCommit
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index cea0cb3a19c..0b3ec10f1b4 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -17,7 +17,6 @@ module API
def find_variable(params)
variables = ::Ci::VariablesFinder.new(user_project, params).execute.to_a
- return variables.first unless ::Gitlab::Ci::Features.variables_api_filter_environment_scope?
return variables.first unless variables.many? # rubocop: disable CodeReuse/ActiveRecord
conflict!("There are multiple variables with provided parameters. Please use 'filter[environment_scope]'")
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 95afa36113c..4eba12157bd 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -83,11 +83,12 @@ module API
put ':id/wikis/:slug' do
authorize! :create_wiki, container
- page = WikiPages::UpdateService
+ response = WikiPages::UpdateService
.new(container: container, current_user: current_user, params: params)
.execute(wiki_page)
+ page = response.payload[:page]
- if page.valid?
+ if response.success?
present page, with: Entities::WikiPage
else
render_validation_error!(page)
@@ -101,11 +102,15 @@ module API
delete ':id/wikis/:slug' do
authorize! :admin_wiki, container
- WikiPages::DestroyService
+ response = WikiPages::DestroyService
.new(container: container, current_user: current_user)
.execute(wiki_page)
- no_content!
+ if response.success?
+ no_content!
+ else
+ render_api_error!(reponse.message)
+ end
end
desc 'Upload an attachment to the wiki repository' do
diff --git a/lib/atlassian/jira_connect.rb b/lib/atlassian/jira_connect.rb
new file mode 100644
index 00000000000..7f693eff59b
--- /dev/null
+++ b/lib/atlassian/jira_connect.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ class << self
+ def app_name
+ "GitLab for Jira (#{gitlab_host})"
+ end
+
+ def app_key
+ "gitlab-jira-connect-#{gitlab_host}"
+ end
+
+ private
+
+ def gitlab_host
+ Gitlab.config.gitlab.host
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb
new file mode 100644
index 00000000000..0b578c03782
--- /dev/null
+++ b/lib/atlassian/jira_connect/client.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ class Client < Gitlab::HTTP
+ def initialize(base_uri, shared_secret)
+ @base_uri = base_uri
+ @shared_secret = shared_secret
+ end
+
+ def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil)
+ dev_info_json = {
+ repositories: [
+ Serializers::RepositoryEntity.represent(
+ project,
+ commits: commits,
+ branches: branches,
+ merge_requests: merge_requests
+ )
+ ]
+ }.to_json
+
+ uri = URI.join(@base_uri, '/rest/devinfo/0.10/bulk')
+
+ headers = {
+ 'Authorization' => "JWT #{jwt_token('POST', uri)}",
+ 'Content-Type' => 'application/json'
+ }
+
+ self.class.post(uri, headers: headers, body: dev_info_json)
+ end
+
+ private
+
+ def jwt_token(http_method, uri)
+ claims = Atlassian::Jwt.build_claims(
+ Atlassian::JiraConnect.app_key,
+ uri,
+ http_method,
+ @base_uri
+ )
+
+ Atlassian::Jwt.encode(claims, @shared_secret)
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/serializers/author_entity.rb b/lib/atlassian/jira_connect/serializers/author_entity.rb
new file mode 100644
index 00000000000..9ab8e34c14b
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/author_entity.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class AuthorEntity < Grape::Entity
+ include Gitlab::Routing
+
+ expose :name
+ expose :email
+
+ with_options(unless: -> (user) { user.is_a?(CommitEntity::CommitAuthor) }) do
+ expose :username
+ expose :url do |user|
+ user_url(user)
+ end
+ expose :avatar do |user|
+ user.avatar_url(only_path: false)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/serializers/base_entity.rb b/lib/atlassian/jira_connect/serializers/base_entity.rb
new file mode 100644
index 00000000000..c5490aa3f54
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/base_entity.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class BaseEntity < Grape::Entity
+ include Gitlab::Routing
+ include GitlabRoutingHelper
+
+ format_with(:string) { |value| value.to_s }
+
+ expose :monotonic_time, as: :updateSequenceId
+
+ private
+
+ def monotonic_time
+ Gitlab::Metrics::System.monotonic_time.to_i
+ end
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/serializers/branch_entity.rb b/lib/atlassian/jira_connect/serializers/branch_entity.rb
new file mode 100644
index 00000000000..c663575b7a8
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/branch_entity.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class BranchEntity < BaseEntity
+ expose :id do |branch|
+ Digest::SHA256.hexdigest(branch.name)
+ end
+ expose :issueKeys do |branch|
+ JiraIssueKeyExtractor.new(branch.name).issue_keys
+ end
+ expose :name
+ expose :lastCommit, using: JiraConnect::Serializers::CommitEntity do |branch, options|
+ options[:project].commit(branch.dereferenced_target)
+ end
+
+ expose :url do |branch, options|
+ project_commits_url(options[:project], branch.name)
+ end
+ expose :createPullRequestUrl do |branch, options|
+ project_new_merge_request_url(
+ options[:project],
+ merge_request: {
+ source_branch: branch.name
+ }
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/serializers/commit_entity.rb b/lib/atlassian/jira_connect/serializers/commit_entity.rb
new file mode 100644
index 00000000000..12eb1ed15ea
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/commit_entity.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class CommitEntity < BaseEntity
+ CommitAuthor = Struct.new(:name, :email)
+
+ expose :id
+ expose :issueKeys do |commit|
+ JiraIssueKeyExtractor.new(commit.safe_message).issue_keys
+ end
+ expose :id, as: :hash
+ expose :short_id, as: :displayId
+ expose :safe_message, as: :message
+ expose :flags do |commit|
+ if commit.merge_commit?
+ ['MERGE_COMMIT']
+ else
+ []
+ end
+ end
+ expose :author, using: JiraConnect::Serializers::AuthorEntity
+ expose :fileCount do |commit|
+ commit.stats.total
+ end
+ expose :files do |commit, options|
+ files = commit.diffs(max_files: 10).diff_files
+ JiraConnect::Serializers::FileEntity.represent files, options.merge(commit: commit)
+ end
+ expose :created_at, as: :authorTimestamp
+
+ expose :url do |commit, options|
+ project_commit_url(options[:project], commit.id)
+ end
+
+ private
+
+ def author
+ object.author || CommitAuthor.new(object.author_name, object.author_email)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/serializers/file_entity.rb b/lib/atlassian/jira_connect/serializers/file_entity.rb
new file mode 100644
index 00000000000..50d31965f93
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/file_entity.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class FileEntity < Grape::Entity
+ include Gitlab::Routing
+
+ expose :path do |file|
+ file.deleted_file? ? file.old_path : file.new_path
+ end
+ expose :changeType do |file|
+ if file.new_file?
+ 'ADDED'
+ elsif file.deleted_file?
+ 'DELETED'
+ elsif file.renamed_file?
+ 'MOVED'
+ else
+ 'MODIFIED'
+ end
+ end
+ expose :added_lines, as: :linesAdded
+ expose :removed_lines, as: :linesRemoved
+
+ expose :url do |file, options|
+ file_path = if file.deleted_file?
+ File.join(options[:commit].parent_id, file.old_path)
+ else
+ File.join(options[:commit].id, file.new_path)
+ end
+
+ project_blob_url(options[:project], file_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/serializers/pull_request_entity.rb b/lib/atlassian/jira_connect/serializers/pull_request_entity.rb
new file mode 100644
index 00000000000..0ddfcbf52ea
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/pull_request_entity.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class PullRequestEntity < BaseEntity
+ STATUS_MAPPING = {
+ 'opened' => 'OPEN',
+ 'locked' => 'OPEN',
+ 'merged' => 'MERGED',
+ 'closed' => 'DECLINED'
+ }.freeze
+
+ expose :id, format_with: :string
+ expose :issueKeys do |mr|
+ JiraIssueKeyExtractor.new(mr.title, mr.description).issue_keys
+ end
+ expose :displayId do |mr|
+ mr.to_reference(full: true)
+ end
+ expose :title
+ expose :author, using: JiraConnect::Serializers::AuthorEntity
+ expose :user_notes_count, as: :commentCount
+ expose :source_branch, as: :sourceBranch
+ expose :target_branch, as: :destinationBranch
+ expose :lastUpdate do |mr|
+ mr.last_edited_at || mr.created_at
+ end
+ expose :status do |mr|
+ STATUS_MAPPING[mr.state] || 'UNKNOWN'
+ end
+
+ expose :sourceBranchUrl do |mr|
+ project_commits_url(mr.project, mr.source_branch)
+ end
+ expose :url do |mr|
+ merge_request_url(mr)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_connect/serializers/repository_entity.rb b/lib/atlassian/jira_connect/serializers/repository_entity.rb
new file mode 100644
index 00000000000..819ca2b62e0
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/repository_entity.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class RepositoryEntity < BaseEntity
+ expose :id, format_with: :string
+ expose :name
+ expose :description
+ expose :url do |project|
+ project_url(project)
+ end
+ expose :avatar do |project|
+ project.avatar_url(only_path: false)
+ end
+
+ expose :commits do |project, options|
+ JiraConnect::Serializers::CommitEntity.represent options[:commits], project: project
+ end
+ expose :branches do |project, options|
+ JiraConnect::Serializers::BranchEntity.represent options[:branches], project: project
+ end
+ expose :pullRequests do |project, options|
+ JiraConnect::Serializers::PullRequestEntity.represent options[:merge_requests], project: project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/atlassian/jira_issue_key_extractor.rb b/lib/atlassian/jira_issue_key_extractor.rb
new file mode 100644
index 00000000000..f1b432787ac
--- /dev/null
+++ b/lib/atlassian/jira_issue_key_extractor.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Atlassian
+ class JiraIssueKeyExtractor
+ def self.has_keys?(*text)
+ new(*text).issue_keys.any?
+ end
+
+ def initialize(*text)
+ @text = text.join(' ')
+ end
+
+ def issue_keys
+ @text.scan(Gitlab::Regex.jira_issue_key_regex).uniq
+ end
+ end
+end
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 33658ae225f..c2266f0bad6 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -9,7 +9,7 @@ module Backup
def initialize(progress)
@progress = progress
- super('artifacts', JobArtifactUploader.root)
+ super('artifacts', JobArtifactUploader.root, excludes: ['tmp'])
end
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index d4c1ce260e4..851445f703d 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -8,10 +8,18 @@ module Backup
attr_reader :progress
attr_reader :config, :db_file_name
- def initialize(progress)
+ IGNORED_ERRORS = [
+ # Ignore the DROP errors; recent database dumps will use --if-exists with pg_dump
+ /does not exist$/,
+ # User may not have permissions to drop extensions or schemas
+ /must be owner of/
+ ].freeze
+ IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze
+
+ def initialize(progress, filename: nil)
@progress = progress
@config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
- @db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
+ @db_file_name = filename || File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
def dump
@@ -27,6 +35,7 @@ module Backup
progress.print "Dumping PostgreSQL database #{config['database']} ... "
pg_env
pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
+ pgsql_args << '--if-exists'
if Gitlab.config.backup.pg_schema
pgsql_args << '-n'
@@ -48,6 +57,8 @@ module Backup
end
report_success(success)
+ progress.flush
+
raise Backup::Error, 'Backup failed' unless success
end
@@ -56,26 +67,65 @@ module Backup
decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name)
decompress_wr.close
- restore_pid =
+ status, errors =
case config["adapter"]
when "postgresql" then
progress.print "Restoring PostgreSQL database #{config['database']} ... "
pg_env
- spawn('psql', config['database'], in: decompress_rd)
+ execute_and_track_errors(pg_restore_cmd, decompress_rd)
end
decompress_rd.close
- success = [decompress_pid, restore_pid].all? do |pid|
- Process.waitpid(pid)
- $?.success?
+ Process.waitpid(decompress_pid)
+ success = $?.success? && status.success?
+
+ if errors.present?
+ progress.print "------ BEGIN ERRORS -----\n".color(:yellow)
+ progress.print errors.join.color(:yellow)
+ progress.print "------ END ERRORS -------\n".color(:yellow)
end
report_success(success)
- abort Backup::Error, 'Restore failed' unless success
+ raise Backup::Error, 'Restore failed' unless success
+
+ errors
end
protected
+ def ignore_error?(line)
+ IGNORED_ERRORS_REGEXP.match?(line)
+ end
+
+ def execute_and_track_errors(cmd, decompress_rd)
+ errors = []
+
+ Open3.popen3(ENV, *cmd) do |stdin, stdout, stderr, thread|
+ stdin.binmode
+
+ out_reader = Thread.new do
+ data = stdout.read
+ $stdout.write(data)
+ end
+
+ err_reader = Thread.new do
+ until (raw_line = stderr.gets).nil?
+ warn(raw_line)
+ errors << raw_line unless ignore_error?(raw_line)
+ end
+ end
+
+ begin
+ IO.copy_stream(decompress_rd, stdin)
+ rescue Errno::EPIPE
+ end
+
+ stdin.close
+ [thread, out_reader, err_reader].each(&:join)
+ [thread.value, errors]
+ end
+ end
+
def pg_env
args = {
'username' => 'PGUSER',
@@ -100,5 +150,11 @@ module Backup
progress.puts '[FAILED]'.color(:red)
end
end
+
+ private
+
+ def pg_restore_cmd
+ ['psql', config['database']]
+ end
end
end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index dae9056a47b..619a62fd6f6 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -7,14 +7,17 @@ module Backup
class Files
include Backup::Helper
- attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir
+ DEFAULT_EXCLUDE = 'lost+found'
- def initialize(name, app_files_dir)
+ attr_reader :name, :app_files_dir, :backup_tarball, :excludes, :files_parent_dir
+
+ def initialize(name, app_files_dir, excludes: [])
@name = name
@app_files_dir = File.realpath(app_files_dir)
@files_parent_dir = File.realpath(File.join(@app_files_dir, '..'))
@backup_files_dir = File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) )
@backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz')
+ @excludes = [DEFAULT_EXCLUDE].concat(excludes)
end
# Copy files from public/files to backup/files
@@ -23,7 +26,7 @@ module Backup
FileUtils.rm_f(backup_tarball)
if ENV['STRATEGY'] == 'copy'
- cmd = %W(rsync -a --exclude=lost+found #{app_files_dir} #{Gitlab.config.backup.path})
+ cmd = [%w(rsync -a), exclude_dirs(:rsync), %W(#{app_files_dir} #{Gitlab.config.backup.path})].flatten
output, status = Gitlab::Popen.popen(cmd)
unless status == 0
@@ -31,10 +34,12 @@ module Backup
raise Backup::Error, 'Backup failed'
end
- run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600])
+ tar_cmd = [tar, exclude_dirs(:tar), %W(-C #{@backup_files_dir} -cf - .)].flatten
+ run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600])
FileUtils.rm_rf(@backup_files_dir)
else
- run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600])
+ tar_cmd = [tar, exclude_dirs(:tar), %W(-C #{app_files_dir} -cf - .)].flatten
+ run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600])
end
end
@@ -81,5 +86,17 @@ module Backup
error = err_r.read
raise Backup::Error, "Backup failed. #{error}" unless error =~ regex
end
+
+ def exclude_dirs(fmt)
+ excludes.map do |s|
+ if s == DEFAULT_EXCLUDE
+ '--exclude=' + s
+ elsif fmt == :rsync
+ '--exclude=/' + s
+ elsif fmt == :tar
+ '--exclude=./' + s
+ end
+ end
+ end
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 915567f8106..2b28b30fd74 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -47,7 +47,7 @@ module Backup
return
end
- directory = connect_to_remote_directory(connection_settings)
+ directory = connect_to_remote_directory(Gitlab.config.backup.upload)
if directory.files.create(create_attributes)
progress.puts "done".color(:green)
@@ -88,7 +88,7 @@ module Backup
# - 1495527097_2017_05_23_9.3.0-pre_gitlab_backup.tar
next unless file =~ /^(\d{10})(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+((-|\.)(pre|rc\d))?(-ee)?)?)?_gitlab_backup\.tar$/
- timestamp = $1.to_i
+ timestamp = Regexp.last_match(1).to_i
if Time.at(timestamp) < (Time.now - keep_time)
begin
@@ -195,9 +195,11 @@ module Backup
@backup_file_list.map {|item| item.gsub("#{FILE_NAME_SUFFIX}", "")}
end
- def connect_to_remote_directory(connection_settings)
- # our settings use string keys, but Fog expects symbols
- connection = ::Fog::Storage.new(connection_settings.symbolize_keys)
+ def connect_to_remote_directory(options)
+ config = ObjectStorage::Config.new(options)
+ config.load_provider
+
+ connection = ::Fog::Storage.new(config.credentials)
# We only attempt to create the directory for local backups. For AWS
# and other cloud providers, we cannot guarantee the user will have
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
index a4be728df08..d7aab33d7cb 100644
--- a/lib/backup/pages.rb
+++ b/lib/backup/pages.rb
@@ -9,7 +9,7 @@ module Backup
def initialize(progress)
@progress = progress
- super('pages', Gitlab.config.pages.path)
+ super('pages', Gitlab.config.pages.path, excludes: [::Projects::UpdatePagesService::TMP_EXTRACT_PATH])
end
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 51fac9e8706..eb0b230904e 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -148,20 +148,22 @@ module Backup
private
def dump_consecutive
- Project.find_each(batch_size: 1000) do |project|
+ Project.includes(:route, :group, namespace: :owner).find_each(batch_size: 1000) do |project|
dump_project(project)
end
end
def dump_storage(storage, semaphore, max_storage_concurrency:)
errors = Queue.new
- queue = SizedQueue.new(1)
+ queue = InterlockSizedQueue.new(1)
threads = Array.new(max_storage_concurrency) do
Thread.new do
Rails.application.executor.wrap do
while project = queue.pop
- semaphore.acquire
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ semaphore.acquire
+ end
begin
dump_project(project)
@@ -176,7 +178,7 @@ module Backup
end
end
- Project.for_repository_storage(storage).find_each(batch_size: 100) do |project|
+ Project.for_repository_storage(storage).includes(:route, :group, namespace: :owner).find_each(batch_size: 100) do |project|
break unless errors.empty?
queue.push(project)
@@ -241,5 +243,23 @@ module Backup
pool.schedule
end
end
+
+ class InterlockSizedQueue < SizedQueue
+ extend ::Gitlab::Utils::Override
+
+ override :pop
+ def pop(*)
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ super
+ end
+ end
+
+ override :push
+ def push(*)
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ super
+ end
+ end
+ end
end
end
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 5a20b6ae0a6..b6a62bc3f29 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -9,7 +9,7 @@ module Backup
def initialize(progress)
@progress = progress
- super('uploads', File.join(Gitlab.config.uploads.storage_path, "uploads"))
+ super('uploads', File.join(Gitlab.config.uploads.storage_path, "uploads"), excludes: ['tmp'])
end
end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index b0a2f6f69d5..2448c2c2bb2 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -265,7 +265,7 @@ module Banzai
extras = []
if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
- extras << "comment #{$1}"
+ extras << "comment #{Regexp.last_match(1)}"
end
extension = matches[:extension] if matches.names.include?("extension")
@@ -436,7 +436,7 @@ module Banzai
escaped = escape_html_entities(text)
escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
- placeholder_data[$1.to_i]
+ placeholder_data[Regexp.last_match(1).to_i]
end
end
end
diff --git a/lib/banzai/filter/alert_reference_filter.rb b/lib/banzai/filter/alert_reference_filter.rb
new file mode 100644
index 00000000000..228a4159c99
--- /dev/null
+++ b/lib/banzai/filter/alert_reference_filter.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ class AlertReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :alert
+
+ def self.object_class
+ AlertManagement::Alert
+ end
+
+ def self.object_sym
+ :alert
+ end
+
+ def parent_records(parent, ids)
+ parent.alert_management_alerts.where(iid: ids.to_a)
+ end
+
+ def url_for_object(alert, project)
+ ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
+ project,
+ alert.iid,
+ only_path: context[:only_path]
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 4723bfbf261..0aa1ee8f604 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -86,7 +86,7 @@ module Banzai
# outside the link element. The entity must be marked HTML safe in
# order to be output literally rather than escaped.
match.gsub!(/((?:&[\w#]+;)+)\z/, '')
- dropped = ($1 || '').html_safe
+ dropped = (Regexp.last_match(1) || '').html_safe
# To match the behaviour of Rinku, if the matched link ends with a
# closing part of a matched pair of punctuation, we remove that trailing
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index fa1690f73ad..b32fe5e8301 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -33,7 +33,7 @@ module Banzai
# Returns a String with :emoji: replaced with gl-emoji unicode.
def emoji_name_element_unicode_filter(text)
text.gsub(emoji_pattern) do |match|
- name = $1
+ name = Regexp.last_match(1)
Gitlab::Emoji.gl_emoji_tag(name)
end
end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 7928272a2cf..e16de13725f 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -64,7 +64,7 @@ module Banzai
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
next unless node.content =~ TAGS_PATTERN
- html = process_tag($1)
+ html = process_tag(Regexp.last_match(1))
node.replace(html) if html && html != node.content
end
diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb
index 543d98e62be..2872ad7b632 100644
--- a/lib/banzai/filter/inline_metrics_filter.rb
+++ b/lib/banzai/filter/inline_metrics_filter.rb
@@ -10,7 +10,6 @@ module Banzai
# the cost of doing a full regex match.
def xpath_search
"descendant-or-self::a[contains(@href,'metrics') and \
- contains(@href,'environments') and \
starts-with(@href, '#{gitlab_domain}')]"
end
@@ -29,7 +28,7 @@ module Banzai
params['project'],
params['environment'],
embedded: true,
- **query_params(params['url'])
+ **query_params(params['url']).except(:environment)
)
end
end
diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb
index e31795e673c..27118269bd0 100644
--- a/lib/banzai/pipeline/broadcast_message_pipeline.rb
+++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb
@@ -7,7 +7,6 @@ module Banzai
@filters ||= FilterArray[
Filter::MarkdownFilter,
Filter::BroadcastMessageSanitizationFilter,
-
Filter::EmojiFilter,
Filter::ColorFilter,
Filter::AutolinkFilter,
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 10ac813ea15..7057ac9d707 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -12,14 +12,11 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::PlantumlFilter,
-
# Must always be before the SanitizationFilter to prevent XSS attacks
Filter::SpacedLinkFilter,
-
Filter::SanitizationFilter,
Filter::AssetProxyFilter,
Filter::SyntaxHighlightFilter,
-
Filter::MathFilter,
Filter::ColorFilter,
Filter::MermaidFilter,
@@ -34,13 +31,10 @@ module Banzai
Filter::ExternalLinkFilter,
Filter::SuggestionFilter,
Filter::FootnoteFilter,
-
*reference_filters,
-
Filter::EmojiFilter,
Filter::TaskListFilter,
Filter::InlineDiffFilter,
-
Filter::SetDirectionFilter
]
end
@@ -65,7 +59,8 @@ module Banzai
Filter::CommitRangeReferenceFilter,
Filter::CommitReferenceFilter,
Filter::LabelReferenceFilter,
- Filter::MilestoneReferenceFilter
+ Filter::MilestoneReferenceFilter,
+ Filter::AlertReferenceFilter
]
end
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index 7fe13100ec2..a2fe6d52a90 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -8,11 +8,9 @@ module Banzai
Filter::HtmlEntityFilter,
Filter::SanitizationFilter,
Filter::AssetProxyFilter,
-
Filter::EmojiFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
-
*reference_filters
]
end
@@ -25,7 +23,8 @@ module Banzai
Filter::MergeRequestReferenceFilter,
Filter::SnippetReferenceFilter,
Filter::CommitRangeReferenceFilter,
- Filter::CommitReferenceFilter
+ Filter::CommitReferenceFilter,
+ Filter::AlertReferenceFilter
]
end
diff --git a/lib/banzai/reference_parser/alert_parser.rb b/lib/banzai/reference_parser/alert_parser.rb
new file mode 100644
index 00000000000..7b864d26f67
--- /dev/null
+++ b/lib/banzai/reference_parser/alert_parser.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Banzai
+ module ReferenceParser
+ class AlertParser < BaseParser
+ self.reference_type = :alert
+
+ def references_relation
+ AlertManagement::Alert
+ end
+
+ private
+
+ def can_read_reference?(user, alert, node)
+ can?(user, :read_alert_management_alert, alert)
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/comment.rb b/lib/bitbucket_server/representation/comment.rb
index 99b97a3b181..7e55c446e76 100644
--- a/lib/bitbucket_server/representation/comment.rb
+++ b/lib/bitbucket_server/representation/comment.rb
@@ -38,7 +38,9 @@ module BitbucketServer
end
def author_username
- author['displayName']
+ author['username'] ||
+ author['slug'] ||
+ author['displayName']
end
def author_email
diff --git a/lib/bitbucket_server/representation/pull_request.rb b/lib/bitbucket_server/representation/pull_request.rb
index c3e927d8de7..2f377bdced2 100644
--- a/lib/bitbucket_server/representation/pull_request.rb
+++ b/lib/bitbucket_server/representation/pull_request.rb
@@ -11,6 +11,12 @@ module BitbucketServer
raw.dig('author', 'user', 'emailAddress')
end
+ def author_username
+ raw.dig('author', 'user', 'username') ||
+ raw.dig('author', 'user', 'slug') ||
+ raw.dig('author', 'user', 'displayName')
+ end
+
def description
raw['description']
end
diff --git a/lib/carrier_wave_string_file.rb b/lib/carrier_wave_string_file.rb
index c9a64d9e631..b6bc3d986ca 100644
--- a/lib/carrier_wave_string_file.rb
+++ b/lib/carrier_wave_string_file.rb
@@ -4,4 +4,12 @@ class CarrierWaveStringFile < StringIO
def original_filename
""
end
+
+ def self.new_file(file_content:, filename:, content_type: "application/octet-stream")
+ {
+ "tempfile" => StringIO.new(file_content),
+ "filename" => filename,
+ "content_type" => content_type
+ }
+ end
end
diff --git a/lib/constraints/jira_encoded_url_constrainer.rb b/lib/constraints/jira_encoded_url_constrainer.rb
new file mode 100644
index 00000000000..92e2fff346b
--- /dev/null
+++ b/lib/constraints/jira_encoded_url_constrainer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Constraints
+ class JiraEncodedUrlConstrainer
+ def matches?(request)
+ request.path.starts_with?('/-/jira') || request.params[:project_id].include?(Gitlab::Jira::Dvcs::ENCODED_SLASH)
+ end
+ end
+end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 118eb8e2d7c..e6ca33d749b 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -21,6 +21,17 @@ module ContainerRegistry
# Taken from: FaradayMiddleware::FollowRedirects
REDIRECT_CODES = Set.new [301, 302, 303, 307]
+ def self.supports_tag_delete?
+ registry_config = Gitlab.config.registry
+ return false unless registry_config.enabled && registry_config.api_url.present?
+
+ return true if ::Gitlab.com?
+
+ token = Auth::ContainerRegistryAuthenticationService.access_token([], [])
+ client = new(registry_config.api_url, token: token)
+ client.supports_tag_delete?
+ end
+
def initialize(base_uri, options = {})
@base_uri = base_uri
@options = options
diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb
index 45af30f46dc..3a50925d628 100644
--- a/lib/expand_variables.rb
+++ b/lib/expand_variables.rb
@@ -7,7 +7,7 @@ module ExpandVariables
value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
variables_hash ||= transform_variables(variables)
- variables_hash[$1 || $2]
+ variables_hash[Regexp.last_match(1) || Regexp.last_match(2)]
end
end
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index 5ef2d888550..adbe93cfa3a 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -111,7 +111,6 @@ module ExtractsRef
end
def use_first_path_segment?(ref)
- return false unless ::Feature.enabled?(:extracts_path_optimization)
return false unless repository_container
return false if repository_container.repository.has_ambiguous_refs?
diff --git a/lib/feature.rb b/lib/feature.rb
index 7cf40b63fdf..71241e98723 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -18,6 +18,10 @@ class Feature
superclass.table_name = 'feature_gates'
end
+ class ActiveSupportCacheStoreAdapter < Flipper::Adapters::ActiveSupportCacheStore
+ # overrides methods in EE
+ end
+
InvalidFeatureFlagError = Class.new(Exception) # rubocop:disable Lint/InheritException
class << self
@@ -137,6 +141,12 @@ class Feature
Feature::Definition.load_all!
end
+ def register_hot_reloader
+ return unless check_feature_flags_definition?
+
+ Feature::Definition.register_hot_reloader!
+ end
+
private
def flipper
@@ -154,7 +164,7 @@ class Feature
# Redis L2 cache
redis_cache_adapter =
- Flipper::Adapters::ActiveSupportCacheStore.new(
+ ActiveSupportCacheStoreAdapter.new(
active_record_adapter,
l2_cache_backend,
expires_in: 1.hour)
@@ -231,4 +241,4 @@ class Feature
end
end
-Feature.prepend_if_ee('EE::Feature')
+Feature::ActiveSupportCacheStoreAdapter.prepend_if_ee('EE::Feature::ActiveSupportCacheStoreAdapter')
diff --git a/lib/feature/definition.rb b/lib/feature/definition.rb
index b0ea55c5805..ee779a86952 100644
--- a/lib/feature/definition.rb
+++ b/lib/feature/definition.rb
@@ -107,6 +107,20 @@ class Feature
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
+ # We use `Feature::Definition` as on Ruby code-reload
+ # a new class definition is created
+ Feature::Definition.load_all!
+ end
+
+ Rails.application.reloaders << file_watcher
+ Rails.application.reloader.to_run { file_watcher.execute_if_updated }
+
+ file_watcher
+ end
+
private
def load_from_file(path)
@@ -130,6 +144,19 @@ class Feature
definitions[definition.key] = definition
end
end
+
+ def reload_files
+ [File.expand_path(__FILE__)]
+ end
+
+ def reload_directories
+ paths.each_with_object({}) do |path, result|
+ path = File.dirname(path)
+ Dir.glob(path).each do |matching_dir|
+ result[matching_dir] = 'yml'
+ end
+ end
+ end
end
end
end
diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb
index 14efbb07100..c06f699ef27 100644
--- a/lib/feature/shared.rb
+++ b/lib/feature/shared.rb
@@ -8,15 +8,38 @@ class Feature
module Shared
# optional: defines if a on-disk definition is required for this feature flag type
# rollout_issue: defines if `bin/feature-flag` asks for rollout issue
+ # default_enabled: defines a default state of a feature flag when created by `bin/feature-flag`
# example: usage being shown when exception is raised
TYPES = {
development: {
description: 'Short lived, used to enable unfinished code to be deployed',
optional: true,
rollout_issue: true,
+ default_enabled: false,
example: <<-EOS
- Feature.enabled?(:my_feature_flag)
- Feature.enabled?(:my_feature_flag, type: :development)
+ Feature.enabled?(:my_feature_flag, project)
+ Feature.enabled?(:my_feature_flag, project, type: :development)
+ push_frontend_feature_flag?(:my_feature_flag, project)
+ EOS
+ },
+ ops: {
+ description: "Long-lived feature flags that control operational aspects of GitLab's behavior",
+ optional: true,
+ rollout_issue: false,
+ default_enabled: false,
+ example: <<-EOS
+ Feature.enabled?(:my_ops_flag, type: ops)
+ push_frontend_feature_flag?(:my_ops_flag, project, type: :ops)
+ EOS
+ },
+ licensed: {
+ description: 'Permanent feature flags used to temporarily disable licensed features.',
+ optional: true,
+ rollout_issue: false,
+ default_enabled: true,
+ example: <<-EOS
+ project.feature_available?(:my_licensed_feature)
+ namespace.feature_available?(:my_licensed_feature)
EOS
}
}.freeze
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index bf4438fb518..830980f0997 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -9,12 +9,13 @@ module Gitlab
module Access
AccessDeniedError = Class.new(StandardError)
- NO_ACCESS = 0
- GUEST = 10
- REPORTER = 20
- DEVELOPER = 30
- MAINTAINER = 40
- OWNER = 50
+ NO_ACCESS = 0
+ MINIMAL_ACCESS = 5
+ GUEST = 10
+ REPORTER = 20
+ DEVELOPER = 30
+ MAINTAINER = 40
+ OWNER = 50
# Branch protection settings
PROTECTION_NONE = 0
diff --git a/lib/gitlab/alert_management/alert_params.rb b/lib/gitlab/alert_management/alert_params.rb
index 84a75e62ecf..3bb839c1114 100644
--- a/lib/gitlab/alert_management/alert_params.rb
+++ b/lib/gitlab/alert_management/alert_params.rb
@@ -20,8 +20,10 @@ module Gitlab
hosts: Array(annotations[:hosts]),
payload: payload,
started_at: parsed_payload['startsAt'],
+ ended_at: parsed_payload['endsAt'],
severity: annotations[:severity],
- fingerprint: annotations[:fingerprint]
+ fingerprint: annotations[:fingerprint],
+ environment: annotations[:environment]
}
end
diff --git a/lib/gitlab/alert_management/payload.rb b/lib/gitlab/alert_management/payload.rb
new file mode 100644
index 00000000000..177d544d720
--- /dev/null
+++ b/lib/gitlab/alert_management/payload.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module AlertManagement
+ module Payload
+ MONITORING_TOOLS = {
+ prometheus: 'Prometheus'
+ }.freeze
+
+ class << self
+ # Instantiates an instance of a subclass of
+ # Gitlab::AlertManagement::Payload::Base. This can
+ # be used to create new alerts or read content from
+ # the payload of an existing AlertManagement::Alert
+ #
+ # @param project [Project]
+ # @param payload [Hash]
+ # @param monitoring_tool [String]
+ def parse(project, payload, monitoring_tool: nil)
+ payload_class = payload_class_for(
+ monitoring_tool: monitoring_tool || payload&.dig('monitoring_tool'),
+ payload: payload
+ )
+
+ payload_class.new(project: project, payload: payload)
+ end
+
+ private
+
+ def payload_class_for(monitoring_tool:, payload:)
+ if monitoring_tool == MONITORING_TOOLS[:prometheus]
+ if gitlab_managed_prometheus?(payload)
+ ::Gitlab::AlertManagement::Payload::ManagedPrometheus
+ else
+ ::Gitlab::AlertManagement::Payload::Prometheus
+ end
+ else
+ ::Gitlab::AlertManagement::Payload::Generic
+ end
+ end
+
+ def gitlab_managed_prometheus?(payload)
+ payload&.dig('labels', 'gitlab_alert_id').present?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb
new file mode 100644
index 00000000000..74e47e5226e
--- /dev/null
+++ b/lib/gitlab/alert_management/payload/base.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+# Representation of a payload of an alert. Defines a constant
+# API so that payloads from various sources can be treated
+# identically. Subclasses should define how to parse payload
+# based on source of alert.
+module Gitlab
+ module AlertManagement
+ module Payload
+ class Base
+ include ActiveModel::Model
+ include Gitlab::Utils::StrongMemoize
+ include Gitlab::Routing
+
+ attr_accessor :project, :payload
+
+ # Any attribute expected to be specifically read from
+ # or derived from an alert payload should be defined.
+ EXPECTED_PAYLOAD_ATTRIBUTES = [
+ :alert_markdown,
+ :alert_title,
+ :annotations,
+ :description,
+ :ends_at,
+ :environment,
+ :environment_name,
+ :full_query,
+ :generator_url,
+ :gitlab_alert,
+ :gitlab_fingerprint,
+ :gitlab_prometheus_alert_id,
+ :gitlab_y_label,
+ :has_required_attributes?,
+ :hosts,
+ :metric_id,
+ :metrics_dashboard_url,
+ :monitoring_tool,
+ :resolved?,
+ :runbook,
+ :service,
+ :severity,
+ :starts_at,
+ :status,
+ :title
+ ].freeze
+
+ # Define expected API for a payload
+ EXPECTED_PAYLOAD_ATTRIBUTES.each do |key|
+ define_method(key) {}
+ end
+
+ # Defines a method which allows access to a given
+ # value within an alert payload
+ #
+ # @param key [Symbol] Name expected to be used to reference value
+ # @param paths [String, Array<String>, Array<Array<String>>,]
+ # List of (nested) keys at value can be found, the
+ # first to yield a result will be used
+ # @param type [Symbol] If value should be converted to another type,
+ # that should be specified here
+ # @param fallback [Proc] Block to be executed to yield a value if
+ # a value cannot be idenitied at any provided paths
+ # Example)
+ # attribute :title
+ # paths: [['title'],
+ # ['details', 'title']]
+ # fallback: Proc.new { 'New Alert' }
+ #
+ # The above sample definition will define a method
+ # called #title which will return the value from the
+ # payload under the key `title` if available, otherwise
+ # looking under `details.title`. If neither returns a
+ # value, the return value will be `'New Alert'`
+ def self.attribute(key, paths:, type: nil, fallback: -> { nil })
+ define_method(key) do
+ strong_memoize(key) do
+ paths = Array(paths).first.is_a?(String) ? [Array(paths)] : paths
+ value = value_for_paths(paths)
+ value = parse_value(value, type) if value
+
+ value.presence || fallback.call
+ end
+ end
+ end
+
+ # Attributes of an AlertManagement::Alert as read
+ # directly from a payload. Prefer accessing
+ # AlertManagement::Alert directly for read operations.
+ def alert_params
+ {
+ description: description,
+ ended_at: ends_at,
+ environment: environment,
+ fingerprint: gitlab_fingerprint,
+ hosts: Array(hosts),
+ monitoring_tool: monitoring_tool,
+ payload: payload,
+ project_id: project.id,
+ prometheus_alert: gitlab_alert,
+ service: service,
+ severity: severity,
+ started_at: starts_at,
+ title: title
+ }.transform_values(&:presence).compact
+ end
+
+ def gitlab_fingerprint
+ strong_memoize(:gitlab_fingerprint) do
+ next unless plain_gitlab_fingerprint
+
+ Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint)
+ end
+ end
+
+ def environment
+ strong_memoize(:environment) do
+ next unless environment_name
+
+ EnvironmentsFinder
+ .new(project, nil, { name: environment_name })
+ .find
+ .first
+ end
+ end
+
+ def resolved?
+ status == 'resolved'
+ end
+
+ def has_required_attributes?
+ true
+ end
+
+ private
+
+ def plain_gitlab_fingerprint; end
+
+ def value_for_paths(paths)
+ target_path = paths.find { |path| payload&.dig(*path) }
+
+ payload&.dig(*target_path) if target_path
+ end
+
+ def parse_value(value, type)
+ case type
+ when :time
+ parse_time(value)
+ when :integer
+ parse_integer(value)
+ else
+ value
+ end
+ end
+
+ def parse_time(value)
+ Time.parse(value).utc
+ rescue ArgumentError
+ end
+
+ def parse_integer(value)
+ Integer(value)
+ rescue ArgumentError, TypeError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alert_management/payload/generic.rb b/lib/gitlab/alert_management/payload/generic.rb
new file mode 100644
index 00000000000..7efdfac75dc
--- /dev/null
+++ b/lib/gitlab/alert_management/payload/generic.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Attribute mapping for alerts via generic alerting integration.
+module Gitlab
+ module AlertManagement
+ module Payload
+ class Generic < Base
+ DEFAULT_TITLE = 'New: Incident'
+ DEFAULT_SEVERITY = 'critical'
+
+ attribute :environment_name, paths: 'gitlab_environment_name'
+ attribute :hosts, paths: 'hosts'
+ attribute :monitoring_tool, paths: 'monitoring_tool'
+ attribute :runbook, paths: 'runbook'
+ attribute :service, paths: 'service'
+ attribute :severity, paths: 'severity', fallback: -> { DEFAULT_SEVERITY }
+ attribute :starts_at, paths: 'start_time', type: :time, fallback: -> { Time.current.utc }
+ attribute :title, paths: 'title', fallback: -> { DEFAULT_TITLE }
+
+ attribute :plain_gitlab_fingerprint, paths: 'fingerprint'
+ private :plain_gitlab_fingerprint
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alert_management/payload/managed_prometheus.rb b/lib/gitlab/alert_management/payload/managed_prometheus.rb
new file mode 100644
index 00000000000..2236e60a0c6
--- /dev/null
+++ b/lib/gitlab/alert_management/payload/managed_prometheus.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# Attribute mapping for alerts via prometheus alerting integration,
+# and for which payload includes gitlab-controlled attributes.
+module Gitlab
+ module AlertManagement
+ module Payload
+ class ManagedPrometheus < ::Gitlab::AlertManagement::Payload::Prometheus
+ attribute :gitlab_prometheus_alert_id,
+ paths: %w(labels gitlab_prometheus_alert_id),
+ type: :integer
+ attribute :metric_id,
+ paths: %w(labels gitlab_alert_id),
+ type: :integer
+
+ def gitlab_alert
+ strong_memoize(:gitlab_alert) do
+ next unless metric_id || gitlab_prometheus_alert_id
+
+ alerts = Projects::Prometheus::AlertsFinder
+ .new(project: project, metric: metric_id, id: gitlab_prometheus_alert_id)
+ .execute
+
+ next if alerts.blank? || alerts.size > 1
+
+ alerts.first
+ end
+ end
+
+ def full_query
+ gitlab_alert&.full_query || super
+ end
+
+ def environment
+ gitlab_alert&.environment || super
+ end
+
+ def metrics_dashboard_url
+ return unless gitlab_alert
+
+ metrics_dashboard_project_prometheus_alert_url(
+ project,
+ gitlab_alert.prometheus_metric_id,
+ environment_id: environment.id,
+ embedded: true,
+ **alert_embed_window_params
+ )
+ end
+
+ private
+
+ def plain_gitlab_fingerprint
+ [metric_id, starts_at_raw].join('/')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alert_management/payload/prometheus.rb b/lib/gitlab/alert_management/payload/prometheus.rb
new file mode 100644
index 00000000000..336e9b319e8
--- /dev/null
+++ b/lib/gitlab/alert_management/payload/prometheus.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+# Attribute mapping for alerts via prometheus alerting integration.
+module Gitlab
+ module AlertManagement
+ module Payload
+ class Prometheus < Base
+ attribute :alert_markdown, paths: %w(annotations gitlab_incident_markdown)
+ attribute :annotations, paths: 'annotations'
+ attribute :description, paths: %w(annotations description)
+ attribute :ends_at, paths: 'endsAt', type: :time
+ attribute :environment_name, paths: %w(labels gitlab_environment_name)
+ attribute :generator_url, paths: %w(generatorURL)
+ attribute :gitlab_y_label,
+ paths: [%w(annotations gitlab_y_label),
+ %w(annotations title),
+ %w(annotations summary),
+ %w(labels alertname)]
+ attribute :runbook, paths: %w(annotations runbook)
+ attribute :starts_at,
+ paths: 'startsAt',
+ type: :time,
+ fallback: -> { Time.current.utc }
+ attribute :status, paths: 'status'
+ attribute :title,
+ paths: [%w(annotations title),
+ %w(annotations summary),
+ %w(labels alertname)]
+
+ attribute :starts_at_raw,
+ paths: [%w(startsAt)]
+ private :starts_at_raw
+
+ METRIC_TIME_WINDOW = 30.minutes
+
+ def monitoring_tool
+ Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
+ end
+
+ # Parses `g0.expr` from `generatorURL`.
+ #
+ # Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1
+ def full_query
+ return unless generator_url
+
+ uri = URI(generator_url)
+
+ Rack::Utils.parse_query(uri.query).fetch('g0.expr')
+ rescue URI::InvalidURIError, KeyError
+ end
+
+ def metrics_dashboard_url
+ return unless environment && full_query && title
+
+ metrics_dashboard_project_environment_url(
+ project,
+ environment,
+ embed_json: dashboard_json,
+ embedded: true,
+ **alert_embed_window_params
+ )
+ end
+
+ def has_required_attributes?
+ project && title && starts_at_raw
+ end
+
+ private
+
+ def plain_gitlab_fingerprint
+ [starts_at_raw, title, full_query].join('/')
+ end
+
+ # Formatted for parsing by JS
+ def alert_embed_window_params
+ {
+ start: (starts_at - METRIC_TIME_WINDOW).utc.strftime('%FT%TZ'),
+ end: (starts_at + METRIC_TIME_WINDOW).utc.strftime('%FT%TZ')
+ }
+ end
+
+ def dashboard_json
+ {
+ panel_groups: [{
+ panels: [{
+ type: 'area-chart',
+ title: title,
+ y_label: gitlab_y_label,
+ metrics: [{
+ query_range: full_query
+ }]
+ }]
+ }]
+ }.to_json
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alerting/notification_payload_parser.rb b/lib/gitlab/alerting/notification_payload_parser.rb
index f285dcf507f..348f851f551 100644
--- a/lib/gitlab/alerting/notification_payload_parser.rb
+++ b/lib/gitlab/alerting/notification_payload_parser.rb
@@ -20,7 +20,8 @@ module Gitlab
def call
{
'annotations' => annotations,
- 'startsAt' => starts_at
+ 'startsAt' => starts_at,
+ 'endsAt' => ends_at
}.compact
end
@@ -55,7 +56,8 @@ module Gitlab
'service' => payload[:service],
'hosts' => hosts.presence,
'severity' => severity,
- 'fingerprint' => fingerprint
+ 'fingerprint' => fingerprint,
+ 'environment' => environment
}
end
@@ -73,8 +75,24 @@ module Gitlab
current_time
end
+ def ends_at
+ Time.parse(payload[:end_time].to_s).rfc3339
+ rescue ArgumentError
+ nil
+ end
+
+ def environment
+ environment_name = payload[:gitlab_environment_name]
+
+ return unless environment_name
+
+ EnvironmentsFinder.new(project, nil, { name: environment_name })
+ .find
+ &.first
+ end
+
def secondary_params
- payload.except(:start_time)
+ payload.except(:start_time, :end_time)
end
def flatten_secondary_params
diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
index 79e60e28fc7..fc91dd6e138 100644
--- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb
+++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
@@ -18,8 +18,7 @@ module Gitlab
params_for_code_stage,
params_for_test_stage,
params_for_review_stage,
- params_for_staging_stage,
- params_for_production_stage
+ params_for_staging_stage
]
end
@@ -86,16 +85,6 @@ module Gitlab
end_event_identifier: :merge_request_first_deployed_to_production
}
end
-
- def self.params_for_production_stage
- {
- name: 'production',
- custom: false,
- relative_position: 7,
- start_event_identifier: :issue_created,
- end_event_identifier: :production_stage_end
- }
- end
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
index cf05ebeb706..b778364a917 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
@@ -6,7 +6,7 @@ module Gitlab
module StageEvents
class ProductionStageEnd < StageEvent
def self.name
- _("Issue first depoloyed to production")
+ _("Issue first deployed to production")
end
def self.identifier
diff --git a/lib/gitlab/analytics/instance_statistics/workers_argument_builder.rb b/lib/gitlab/analytics/instance_statistics/workers_argument_builder.rb
new file mode 100644
index 00000000000..636bba22c23
--- /dev/null
+++ b/lib/gitlab/analytics/instance_statistics/workers_argument_builder.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module InstanceStatistics
+ class WorkersArgumentBuilder
+ def initialize(measurement_identifiers: [], recorded_at: Time.zone.now)
+ @measurement_identifiers = measurement_identifiers
+ @recorded_at = recorded_at
+ end
+
+ def execute
+ measurement_identifiers.map do |measurement_identifier|
+ query_scope = ::Analytics::InstanceStatistics::Measurement::IDENTIFIER_QUERY_MAPPING[measurement_identifier]&.call
+
+ next if query_scope.nil?
+
+ # Determining the query range (id range) as early as possible in order to get more accurate counts.
+ start = query_scope.minimum(:id)
+ finish = query_scope.maximum(:id)
+
+ [measurement_identifier, start, finish, recorded_at]
+ end.compact
+ end
+
+ private
+
+ attr_reader :measurement_identifiers, :recorded_at
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/unique_visits.rb b/lib/gitlab/analytics/unique_visits.rb
index ad746ebbd42..292048dcad9 100644
--- a/lib/gitlab/analytics/unique_visits.rb
+++ b/lib/gitlab/analytics/unique_visits.rb
@@ -14,23 +14,23 @@ module Gitlab
# @param [ActiveSupport::TimeWithZone] end_date end of time frame
# @return [Integer] number of unique visitors
def unique_visits_for(targets:, start_date: 7.days.ago, end_date: start_date + 1.week)
- target_ids = if targets == :analytics
- self.class.analytics_ids
- elsif targets == :compliance
- self.class.compliance_ids
- else
- Array(targets)
- end
+ events = if targets == :analytics
+ self.class.analytics_events
+ elsif targets == :compliance
+ self.class.compliance_events
+ else
+ Array(targets)
+ end
- Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: target_ids, start_date: start_date, end_date: end_date)
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: start_date, end_date: end_date)
end
class << self
- def analytics_ids
+ def analytics_events
Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('analytics')
end
- def compliance_ids
+ def compliance_events
Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('compliance')
end
end
diff --git a/lib/gitlab/anonymous_session.rb b/lib/gitlab/anonymous_session.rb
index 148b6d3310d..911825eef3a 100644
--- a/lib/gitlab/anonymous_session.rb
+++ b/lib/gitlab/anonymous_session.rb
@@ -2,35 +2,34 @@
module Gitlab
class AnonymousSession
- def initialize(remote_ip, session_id: nil)
+ def initialize(remote_ip)
@remote_ip = remote_ip
- @session_id = session_id
end
- def store_session_id_per_ip
+ def count_session_ip
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do
- redis.sadd(session_lookup_name, session_id)
+ redis.incr(session_lookup_name)
redis.expire(session_lookup_name, 24.hours)
end
end
end
- def stored_sessions
+ def session_count
Gitlab::Redis::SharedState.with do |redis|
- redis.scard(session_lookup_name)
+ redis.get(session_lookup_name).to_i
end
end
- def cleanup_session_per_ip_entries
+ def cleanup_session_per_ip_count
Gitlab::Redis::SharedState.with do |redis|
- redis.srem(session_lookup_name, session_id)
+ redis.del(session_lookup_name)
end
end
private
- attr_reader :remote_ip, :session_id
+ attr_reader :remote_ip
def session_lookup_name
@session_lookup_name ||= "#{Gitlab::Redis::SharedState::IP_SESSIONS_LOOKUP_NAMESPACE}:#{remote_ip}"
diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb
index a3feda9bb59..30cb74bcf54 100644
--- a/lib/gitlab/application_context.rb
+++ b/lib/gitlab/application_context.rb
@@ -29,7 +29,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/auth.rb b/lib/gitlab/auth.rb
index ece4946383d..609eef5e365 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -50,7 +50,7 @@ module Gitlab
build_access_token_check(login, password) ||
lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) ||
- personal_access_token_check(password) ||
+ personal_access_token_check(password, project) ||
deploy_token_check(login, password, project) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
@@ -117,7 +117,6 @@ module Gitlab
private
- # rubocop:disable Gitlab/RailsLogger
def rate_limit!(rate_limiter, success:, login:)
return if skip_rate_limit?(login: login)
@@ -132,12 +131,11 @@ module Gitlab
# This returns true when the failures are over the threshold and the IP
# is banned.
if rate_limiter.register_fail!
- Rails.logger.info "IP #{rate_limiter.ip} failed to login " \
+ Gitlab::AppLogger.info "IP #{rate_limiter.ip} failed to login " \
"as #{login} but has been temporarily banned from Git auth"
end
end
end
- # rubocop:enable Gitlab/RailsLogger
def skip_rate_limit?(login:)
CI_JOB_USER == login
@@ -191,12 +189,18 @@ module Gitlab
end
end
- def personal_access_token_check(password)
+ def personal_access_token_check(password, project)
return unless password.present?
token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password)
- if token && valid_scoped_token?(token, all_available_scopes) && token.user.can?(:log_in)
+ 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)
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end
end
diff --git a/lib/gitlab/auth/atlassian/auth_hash.rb b/lib/gitlab/auth/atlassian/auth_hash.rb
new file mode 100644
index 00000000000..047e4eabc51
--- /dev/null
+++ b/lib/gitlab/auth/atlassian/auth_hash.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Atlassian
+ class AuthHash < Gitlab::Auth::OAuth::AuthHash
+ def token
+ credentials[:token]
+ end
+
+ def refresh_token
+ credentials[:refresh_token]
+ end
+
+ def expires?
+ credentials[:expires]
+ end
+
+ def expires_at
+ credentials[:expires_at]
+ end
+
+ private
+
+ def credentials
+ auth_hash[:credentials]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/atlassian/identity_linker.rb b/lib/gitlab/auth/atlassian/identity_linker.rb
new file mode 100644
index 00000000000..4dec54d44d6
--- /dev/null
+++ b/lib/gitlab/auth/atlassian/identity_linker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Atlassian
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ extend ::Gitlab::Utils::Override
+ include ::Gitlab::Utils::StrongMemoize
+
+ private
+
+ override :identity
+ def identity
+ strong_memoize(:identity) do
+ current_user.atlassian_identity || build_atlassian_identity
+ end
+ end
+
+ def build_atlassian_identity
+ identity = current_user.build_atlassian_identity
+ ::Gitlab::Auth::Atlassian::User.assign_identity_from_auth_hash!(identity, auth_hash)
+ end
+
+ def auth_hash
+ ::Gitlab::Auth::Atlassian::AuthHash.new(oauth)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/atlassian/user.rb b/lib/gitlab/auth/atlassian/user.rb
new file mode 100644
index 00000000000..6ab7741cc54
--- /dev/null
+++ b/lib/gitlab/auth/atlassian/user.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Atlassian
+ class User < Gitlab::Auth::OAuth::User
+ def self.assign_identity_from_auth_hash!(identity, auth_hash)
+ identity.extern_uid = auth_hash.uid
+ identity.token = auth_hash.token
+ identity.refresh_token = auth_hash.refresh_token
+ identity.expires_at = Time.at(auth_hash.expires_at).utc.to_datetime if auth_hash.expires?
+
+ identity
+ end
+
+ protected
+
+ def find_by_uid_and_provider
+ ::Atlassian::Identity.find_by_extern_uid(auth_hash.uid)&.user
+ end
+
+ def add_or_update_user_identities
+ return unless gl_user
+
+ identity = gl_user.atlassian_identity || gl_user.build_atlassian_identity
+ self.class.assign_identity_from_auth_hash!(identity, auth_hash)
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = ::Gitlab::Auth::Atlassian::AuthHash.new(auth_hash)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb
index 4f448211abf..b7bb61f0677 100644
--- a/lib/gitlab/auth/ldap/adapter.rb
+++ b/lib/gitlab/auth/ldap/adapter.rb
@@ -55,7 +55,7 @@ module Gitlab
response = ldap.get_operation_result
unless response.code == 0
- Rails.logger.warn("LDAP search error: #{response.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn("LDAP search error: #{response.message}")
end
[]
@@ -67,7 +67,7 @@ module Gitlab
retries += 1
error_message = connection_error_message(error)
- Rails.logger.warn(error_message) # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn(error_message)
if retries < MAX_SEARCH_RETRIES
renew_connection_adapter
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index 7677189eb9f..88cc840c395 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -248,7 +248,7 @@ module Gitlab
begin
custom_options[:cert] = OpenSSL::X509::Certificate.new(custom_options[:cert])
rescue OpenSSL::X509::CertificateError => e
- Rails.logger.error "LDAP TLS Options 'cert' is invalid for provider #{provider}: #{e.message}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "LDAP TLS Options 'cert' is invalid for provider #{provider}: #{e.message}"
end
end
@@ -256,7 +256,7 @@ module Gitlab
begin
custom_options[:key] = OpenSSL::PKey.read(custom_options[:key])
rescue OpenSSL::PKey::PKeyError => e
- Rails.logger.error "LDAP TLS Options 'key' is invalid for provider #{provider}: #{e.message}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "LDAP TLS Options 'key' is invalid for provider #{provider}: #{e.message}"
end
end
diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb
index 8c5000147c4..102820d6bd5 100644
--- a/lib/gitlab/auth/ldap/person.rb
+++ b/lib/gitlab/auth/ldap/person.rb
@@ -45,7 +45,7 @@ module Gitlab
def self.normalize_dn(dn)
::Gitlab::Auth::Ldap::DN.new(dn).to_normalized_s
rescue ::Gitlab::Auth::Ldap::DN::FormatError => e
- Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
dn
end
@@ -57,13 +57,13 @@ module Gitlab
def self.normalize_uid(uid)
::Gitlab::Auth::Ldap::DN.normalize_value(uid)
rescue ::Gitlab::Auth::Ldap::DN::FormatError => e
- Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
uid
end
def initialize(entry, provider)
- Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.debug "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}"
@entry = entry
@provider = provider
end
diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb
index 1ca59aa827b..1eae7af442d 100644
--- a/lib/gitlab/auth/o_auth/provider.rb
+++ b/lib/gitlab/auth/o_auth/provider.rb
@@ -5,10 +5,11 @@ module Gitlab
module OAuth
class Provider
LABELS = {
- "github" => "GitHub",
- "gitlab" => "GitLab.com",
- "google_oauth2" => "Google",
- "azure_oauth2" => "Azure AD"
+ "github" => "GitHub",
+ "gitlab" => "GitLab.com",
+ "google_oauth2" => "Google",
+ "azure_oauth2" => "Azure AD",
+ 'atlassian_oauth2' => 'Atlassian'
}.freeze
def self.authentication(user, provider)
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index 086f4a2e91c..3211d2ffaea 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -273,7 +273,11 @@ module Gitlab
end
def auto_link_user?
- Gitlab.config.omniauth.auto_link_user
+ auto_link = Gitlab.config.omniauth.auto_link_user
+ return auto_link if [true, false].include?(auto_link)
+
+ auto_link = Array(auto_link)
+ auto_link.include?(auth_hash.provider)
end
end
end
diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb
index ea0307e8bd6..d1b9062a23c 100644
--- a/lib/gitlab/background_migration.rb
+++ b/lib/gitlab/background_migration.rb
@@ -9,7 +9,7 @@ module Gitlab
# Begins stealing jobs from the background migrations queue, blocking the
# caller until all jobs have been completed.
#
- # When a migration raises a StandardError is is going to be retries up to
+ # When a migration raises a StandardError it is going to retry up to
# three times, for example, to recover from a deadlock.
#
# When Exception is being raised, it enqueues the migration again, and
diff --git a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
index c912628d0fc..5b9ee8a0ee2 100644
--- a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
+++ b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def perform(start_id, stop_id)
- Rails.logger.info("Setting commits_count for merge request diffs: #{start_id} - #{stop_id}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info("Setting commits_count for merge request diffs: #{start_id} - #{stop_id}")
update = '
commits_count = (
diff --git a/lib/gitlab/background_migration/calculate_wiki_sizes.rb b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
index e62f5edd0e7..76598f6e2a6 100644
--- a/lib/gitlab/background_migration/calculate_wiki_sizes.rb
+++ b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
@@ -10,7 +10,7 @@ module Gitlab
.includes(project: [:route, :group, namespace: [:owner]]).find_each do |statistics|
statistics.refresh!(only: [:wiki_size])
rescue => e
- Rails.logger.error "Failed to update wiki statistics. id: #{statistics.id} message: #{e.message}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "Failed to update wiki statistics. id: #{statistics.id} message: #{e.message}"
end
end
end
diff --git a/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb b/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb
index 4016b807f21..c0099d44b5a 100644
--- a/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb
+++ b/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb
@@ -25,7 +25,7 @@ module Gitlab
certificate_valid_not_after: domain.x509&.not_after&.iso8601
)
rescue => e
- Rails.logger.error "Failed to update pages domain certificate valid time. id: #{domain.id}, message: #{e.message}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "Failed to update pages domain certificate valid time. id: #{domain.id}, message: #{e.message}"
end
end
end
diff --git a/lib/gitlab/background_migration/fix_pages_access_level.rb b/lib/gitlab/background_migration/fix_pages_access_level.rb
index 31d2e78b2d2..8e46021bd93 100644
--- a/lib/gitlab/background_migration/fix_pages_access_level.rb
+++ b/lib/gitlab/background_migration/fix_pages_access_level.rb
@@ -103,8 +103,8 @@ module Gitlab
end
# Private projects are not allowed to have enabled access level, only `private` and `public`
- # If access control is enabled, these projects currently behave as if the have `private` pages_access_level
- # if access control is disabled, these projects currently behave as if the have `public` pages_access_level
+ # If access control is enabled, these projects currently behave as if they have `private` pages_access_level
+ # if access control is disabled, these projects currently behave as if they have `public` pages_access_level
# so we preserve this behaviour for projects with pages already deployed
# for project without pages we always set `private` access_level
def fix_private_access_level(start_id, stop_id)
diff --git a/lib/gitlab/background_migration/migrate_to_hashed_storage.rb b/lib/gitlab/background_migration/migrate_to_hashed_storage.rb
new file mode 100644
index 00000000000..4054db4fb87
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_to_hashed_storage.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Background migration to move any legacy project to Hashed Storage
+ class MigrateToHashedStorage
+ def perform
+ batch_size = helper.batch_size
+ legacy_projects_count = Project.with_unmigrated_storage.count
+
+ if storage_migrator.rollback_pending?
+ logger.warn(
+ migrator: 'MigrateToHashedStorage',
+ message: 'Aborting an storage rollback operation currently in progress'
+ )
+
+ storage_migrator.abort_rollback!
+ end
+
+ if legacy_projects_count == 0
+ logger.info(
+ migrator: 'MigrateToHashedStorage',
+ message: 'There are no projects requiring migration to Hashed Storage'
+ )
+
+ return
+ end
+
+ logger.info(
+ migrator: 'MigrateToHashedStorage',
+ message: "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}"
+ )
+
+ helper.project_id_batches_migration do |start, finish|
+ storage_migrator.bulk_schedule_migration(start: start, finish: finish)
+
+ logger.info(
+ migrator: 'MigrateToHashedStorage',
+ message: "Enqueuing migration of projects in batches of #{batch_size} from ID=#{start} to ID=#{finish}",
+ batch_from: start,
+ batch_to: finish
+ )
+ end
+ end
+
+ private
+
+ def helper
+ Gitlab::HashedStorage::RakeHelper
+ end
+
+ def storage_migrator
+ @storage_migrator ||= Gitlab::HashedStorage::Migrator.new
+ end
+
+ def logger
+ @logger ||= ::Gitlab::BackgroundMigration::Logger.build
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb b/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb
new file mode 100644
index 00000000000..eb72ef1de33
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # rubocop:disable Style/Documentation
+ class PopulateResolvedOnDefaultBranchColumn
+ def perform(*); end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn.prepend_if_ee('EE::Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn')
diff --git a/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb
new file mode 100644
index 00000000000..a0c89cc4664
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # 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)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::PopulateVulnerabilityHistoricalStatistics.prepend_if_ee('EE::Gitlab::BackgroundMigration::PopulateVulnerabilityHistoricalStatistics')
diff --git a/lib/gitlab/background_migration/remove_duplicate_cs_findings.rb b/lib/gitlab/background_migration/remove_duplicate_cs_findings.rb
new file mode 100644
index 00000000000..cc9b0329556
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_duplicate_cs_findings.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class RemoveDuplicateCsFindings
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::RemoveDuplicateCsFindings.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveDuplicateCsFindings')
diff --git a/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb b/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb
index 9f765d03d62..527dd2a0a83 100644
--- a/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb
+++ b/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb
@@ -4,13 +4,18 @@ module Gitlab
module BackgroundMigration
# Sets the MergeRequestDiff#files_count value for old rows
class SetMergeRequestDiffFilesCount
- COUNT_SUBQUERY = <<~SQL
- files_count = (
- SELECT count(*)
- FROM merge_request_diff_files
- WHERE merge_request_diff_files.merge_request_diff_id = merge_request_diffs.id
- )
- SQL
+ # Some historic data has a *lot* of files. Apply a sentinel to these cases
+ FILES_COUNT_SENTINEL = 2**15 - 1
+
+ def self.count_subquery
+ <<~SQL
+ files_count = (
+ SELECT LEAST(#{FILES_COUNT_SENTINEL}, count(*))
+ FROM merge_request_diff_files
+ WHERE merge_request_diff_files.merge_request_diff_id = merge_request_diffs.id
+ )
+ SQL
+ end
class MergeRequestDiff < ActiveRecord::Base # rubocop:disable Style/Documentation
include EachBatch
@@ -20,7 +25,7 @@ module Gitlab
def perform(start_id, end_id)
MergeRequestDiff.where(id: start_id..end_id).each_batch do |relation|
- relation.update_all(COUNT_SUBQUERY)
+ relation.update_all(self.class.count_subquery)
end
end
end
diff --git a/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb b/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb
index 9ac92aab637..c485c23f3be 100644
--- a/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb
+++ b/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb
@@ -11,9 +11,11 @@ module Gitlab
class SetNullPackageFilesFileStoreToLocalValue
LOCAL_STORE = 1 # equal to ObjectStorage::Store::LOCAL
- # Temporary AR class for package files
- class PackageFile < ActiveRecord::Base
- self.table_name = 'packages_package_files'
+ module Packages
+ # Temporary AR class for package files
+ class PackageFile < ActiveRecord::Base
+ self.table_name = 'packages_package_files'
+ end
end
def perform(start_id, stop_id)
diff --git a/lib/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings.rb b/lib/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings.rb
new file mode 100644
index 00000000000..651df36fcfd
--- /dev/null
+++ b/lib/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class UpdateLocationFingerprintForContainerScanningFindings
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::UpdateLocationFingerprintForContainerScanningFindings.prepend_if_ee('EE::Gitlab::BackgroundMigration::UpdateLocationFingerprintForContainerScanningFindings')
diff --git a/lib/gitlab/backtrace_cleaner.rb b/lib/gitlab/backtrace_cleaner.rb
index d04f0983d12..caea05c720d 100644
--- a/lib/gitlab/backtrace_cleaner.rb
+++ b/lib/gitlab/backtrace_cleaner.rb
@@ -31,7 +31,7 @@ module Gitlab
return unless backtrace
Array(Rails.backtrace_cleaner.clean(backtrace)).reject do |line|
- line.match(IGNORED_BACKTRACES_REGEXP)
+ IGNORED_BACKTRACES_REGEXP.match?(line)
end
end
end
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
index 6b78825aefd..1b985f83b22 100644
--- a/lib/gitlab/badge/coverage/template.rb
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def key_text
- if @key_text && @key_text.size <= MAX_KEY_SIZE
+ if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE
@key_text
else
@entity.to_s
@@ -37,7 +37,7 @@ module Gitlab
end
def key_width
- if @key_width && @key_width.between?(1, MAX_KEY_SIZE)
+ if @key_width && @key_width.between?(1, MAX_KEY_WIDTH)
@key_width
else
62
diff --git a/lib/gitlab/badge/pipeline/status.rb b/lib/gitlab/badge/pipeline/status.rb
index 17f179f027d..f061ba22688 100644
--- a/lib/gitlab/badge/pipeline/status.rb
+++ b/lib/gitlab/badge/pipeline/status.rb
@@ -12,6 +12,7 @@ module Gitlab
def initialize(project, ref, opts: {})
@project = project
@ref = ref
+ @ignore_skipped = Gitlab::Utils.to_boolean(opts[:ignore_skipped], default: false)
@customization = {
key_width: opts[:key_width].to_i,
key_text: opts[:key_text]
@@ -26,9 +27,11 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def status
- @project.ci_pipelines
+ pipelines = @project.ci_pipelines
.where(sha: @sha)
- .latest_status(@ref) || 'unknown'
+
+ relation = @ignore_skipped ? pipelines.without_statuses([:skipped]) : pipelines
+ relation.latest_status(@ref) || 'unknown'
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/badge/pipeline/template.rb b/lib/gitlab/badge/pipeline/template.rb
index 781897fab4b..af8e318395b 100644
--- a/lib/gitlab/badge/pipeline/template.rb
+++ b/lib/gitlab/badge/pipeline/template.rb
@@ -29,7 +29,7 @@ module Gitlab
end
def key_text
- if @key_text && @key_text.size <= MAX_KEY_SIZE
+ if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE
@key_text
else
@entity.to_s
@@ -41,7 +41,7 @@ module Gitlab
end
def key_width
- if @key_width && @key_width.between?(1, MAX_KEY_SIZE)
+ if @key_width && @key_width.between?(1, MAX_KEY_WIDTH)
@key_width
else
62
diff --git a/lib/gitlab/badge/template.rb b/lib/gitlab/badge/template.rb
index 97103e3f42c..9ac8f1c17f2 100644
--- a/lib/gitlab/badge/template.rb
+++ b/lib/gitlab/badge/template.rb
@@ -6,7 +6,8 @@ module Gitlab
# Abstract template class for badges
#
class Template
- MAX_KEY_SIZE = 128
+ MAX_KEY_TEXT_SIZE = 64
+ MAX_KEY_WIDTH = 512
def initialize(badge)
@entity = badge.entity
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
index 18a1b64729e..aca5a63a424 100644
--- a/lib/gitlab/bitbucket_server_import/importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -61,17 +61,18 @@ module Gitlab
}.to_json)
end
- def gitlab_user_id(email)
- find_user_id(email) || project.creator_id
- end
+ def find_user_id(by:, value:)
+ return unless value
- def find_user_id(email)
- return unless email
+ return users[value] if users.key?(value)
- return users[email] if users.key?(email)
+ user = if by == :email
+ User.find_by_any_email(value, confirmed: true)
+ else
+ User.find_by_username(value)
+ end
- user = User.find_by_any_email(email, confirmed: true)
- users[email] = user&.id
+ users[value] = user&.id
user&.id
end
@@ -197,9 +198,8 @@ module Gitlab
log_info(stage: 'import_bitbucket_pull_requests', message: 'starting', iid: pull_request.iid)
description = ''
- description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author_email)
+ description += author_line(pull_request)
description += pull_request.description if pull_request.description
- author_id = gitlab_user_id(pull_request.author_email)
attributes = {
iid: pull_request.iid,
@@ -212,7 +212,7 @@ module Gitlab
target_branch: Gitlab::Git.ref_name(pull_request.target_branch_name),
target_branch_sha: pull_request.target_branch_sha,
state_id: MergeRequest.available_states[pull_request.state],
- author_id: author_id,
+ author_id: author_id(pull_request),
created_at: pull_request.created_at,
updated_at: pull_request.updated_at
}
@@ -254,7 +254,7 @@ module Gitlab
committer = merge_event.committer_email
- user_id = gitlab_user_id(committer)
+ user_id = find_user_id(by: :email, value: committer) || project.creator_id
timestamp = merge_event.merge_timestamp
merge_request.update({ merge_commit_sha: merge_event.merge_commit })
metric = MergeRequest::Metrics.find_or_initialize_by(merge_request: merge_request)
@@ -353,7 +353,7 @@ module Gitlab
end
def pull_request_comment_attributes(comment)
- author = find_user_id(comment.author_email)
+ author = uid(comment)
note = ''
unless author
@@ -397,6 +397,23 @@ module Gitlab
def metrics
@metrics ||= Gitlab::Import::Metrics.new(:bitbucket_server_importer, @project)
end
+
+ def author_line(rep_object)
+ return '' if uid(rep_object)
+
+ @formatter.author_line(rep_object.author)
+ end
+
+ def author_id(rep_object)
+ uid(rep_object) || project.creator_id
+ end
+
+ def uid(rep_object)
+ find_user_id(by: :email, value: rep_object.author_email) unless Feature.enabled?(:bitbucket_server_user_mapping_by_username)
+
+ find_user_id(by: :username, value: rep_object.author_username) ||
+ find_user_id(by: :email, value: rep_object.author_email)
+ end
end
end
end
diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb
index 6e48ca90054..3ad919fbba8 100644
--- a/lib/gitlab/cache/request_cache.rb
+++ b/lib/gitlab/cache/request_cache.rb
@@ -55,7 +55,7 @@ module Gitlab
.join(':')
end
- private cache_key_method_name
+ private cache_key_method_name # rubocop: disable Style/AccessModifierDeclarations
end
end
end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
index e18cf6ff8f2..78952db7a3e 100644
--- a/lib/gitlab/checks/lfs_integrity.rb
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -17,7 +17,7 @@ module Gitlab
return false unless new_lfs_pointers.present?
- existing_count = @project.all_lfs_objects
+ existing_count = @project.lfs_objects
.for_oids(new_lfs_pointers.map(&:lfs_oid))
.count
diff --git a/lib/gitlab/checks/snippet_check.rb b/lib/gitlab/checks/snippet_check.rb
index bcecd0fc251..8c61b782baa 100644
--- a/lib/gitlab/checks/snippet_check.rb
+++ b/lib/gitlab/checks/snippet_check.rb
@@ -3,7 +3,6 @@
module Gitlab
module Checks
class SnippetCheck < BaseChecker
- DEFAULT_BRANCH = 'master'.freeze
ERROR_MESSAGES = {
create_delete_branch: 'You can not create or delete branches.'
}.freeze
@@ -11,17 +10,18 @@ module Gitlab
ATTRIBUTES = %i[oldrev newrev ref branch_name tag_name logger].freeze
attr_reader(*ATTRIBUTES)
- def initialize(change, logger:)
+ def initialize(change, default_branch:, 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
@logger = logger
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end
def validate!
- if creation? || deletion?
+ if !@default_branch || creation? || deletion?
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch]
end
@@ -31,7 +31,7 @@ module Gitlab
private
def creation?
- @branch_name != DEFAULT_BRANCH && super
+ @branch_name != @default_branch && super
end
end
end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index e145bd2e9df..1fac00337a3 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -31,105 +31,205 @@ module Gitlab
end
class Converter
- def on_0(_) reset end
+ def on_0(_)
+ reset
+ end
- def on_1(_) enable(STYLE_SWITCHES[:bold]) end
+ def on_1(_)
+ enable(STYLE_SWITCHES[:bold])
+ end
- def on_3(_) enable(STYLE_SWITCHES[:italic]) end
+ def on_3(_)
+ enable(STYLE_SWITCHES[:italic])
+ end
- def on_4(_) enable(STYLE_SWITCHES[:underline]) end
+ def on_4(_)
+ enable(STYLE_SWITCHES[:underline])
+ end
- def on_8(_) enable(STYLE_SWITCHES[:conceal]) end
+ def on_8(_)
+ enable(STYLE_SWITCHES[:conceal])
+ end
- def on_9(_) enable(STYLE_SWITCHES[:cross]) end
+ def on_9(_)
+ enable(STYLE_SWITCHES[:cross])
+ end
- def on_21(_) disable(STYLE_SWITCHES[:bold]) end
+ def on_21(_)
+ disable(STYLE_SWITCHES[:bold])
+ end
- def on_22(_) disable(STYLE_SWITCHES[:bold]) end
+ def on_22(_)
+ disable(STYLE_SWITCHES[:bold])
+ end
- def on_23(_) disable(STYLE_SWITCHES[:italic]) end
+ def on_23(_)
+ disable(STYLE_SWITCHES[:italic])
+ end
- def on_24(_) disable(STYLE_SWITCHES[:underline]) end
+ def on_24(_)
+ disable(STYLE_SWITCHES[:underline])
+ end
- def on_28(_) disable(STYLE_SWITCHES[:conceal]) end
+ def on_28(_)
+ disable(STYLE_SWITCHES[:conceal])
+ end
- def on_29(_) disable(STYLE_SWITCHES[:cross]) end
+ def on_29(_)
+ disable(STYLE_SWITCHES[:cross])
+ end
- def on_30(_) set_fg_color(0) end
+ def on_30(_)
+ set_fg_color(0)
+ end
- def on_31(_) set_fg_color(1) end
+ def on_31(_)
+ set_fg_color(1)
+ end
- def on_32(_) set_fg_color(2) end
+ def on_32(_)
+ set_fg_color(2)
+ end
- def on_33(_) set_fg_color(3) end
+ def on_33(_)
+ set_fg_color(3)
+ end
- def on_34(_) set_fg_color(4) end
+ def on_34(_)
+ set_fg_color(4)
+ end
- def on_35(_) set_fg_color(5) end
+ def on_35(_)
+ set_fg_color(5)
+ end
- def on_36(_) set_fg_color(6) end
+ def on_36(_)
+ set_fg_color(6)
+ end
- def on_37(_) set_fg_color(7) end
+ def on_37(_)
+ set_fg_color(7)
+ end
- def on_38(stack) set_fg_color_256(stack) end
+ def on_38(stack)
+ set_fg_color_256(stack)
+ end
- def on_39(_) set_fg_color(9) end
+ def on_39(_)
+ set_fg_color(9)
+ end
- def on_40(_) set_bg_color(0) end
+ def on_40(_)
+ set_bg_color(0)
+ end
- def on_41(_) set_bg_color(1) end
+ def on_41(_)
+ set_bg_color(1)
+ end
- def on_42(_) set_bg_color(2) end
+ def on_42(_)
+ set_bg_color(2)
+ end
- def on_43(_) set_bg_color(3) end
+ def on_43(_)
+ set_bg_color(3)
+ end
- def on_44(_) set_bg_color(4) end
+ def on_44(_)
+ set_bg_color(4)
+ end
- def on_45(_) set_bg_color(5) end
+ def on_45(_)
+ set_bg_color(5)
+ end
- def on_46(_) set_bg_color(6) end
+ def on_46(_)
+ set_bg_color(6)
+ end
- def on_47(_) set_bg_color(7) end
+ def on_47(_)
+ set_bg_color(7)
+ end
- def on_48(stack) set_bg_color_256(stack) end
+ def on_48(stack)
+ set_bg_color_256(stack)
+ end
- def on_49(_) set_bg_color(9) end
+ def on_49(_)
+ set_bg_color(9)
+ end
- def on_90(_) set_fg_color(0, 'l') end
+ def on_90(_)
+ set_fg_color(0, 'l')
+ end
- def on_91(_) set_fg_color(1, 'l') end
+ def on_91(_)
+ set_fg_color(1, 'l')
+ end
- def on_92(_) set_fg_color(2, 'l') end
+ def on_92(_)
+ set_fg_color(2, 'l')
+ end
- def on_93(_) set_fg_color(3, 'l') end
+ def on_93(_)
+ set_fg_color(3, 'l')
+ end
- def on_94(_) set_fg_color(4, 'l') end
+ def on_94(_)
+ set_fg_color(4, 'l')
+ end
- def on_95(_) set_fg_color(5, 'l') end
+ def on_95(_)
+ set_fg_color(5, 'l')
+ end
- def on_96(_) set_fg_color(6, 'l') end
+ def on_96(_)
+ set_fg_color(6, 'l')
+ end
- def on_97(_) set_fg_color(7, 'l') end
+ def on_97(_)
+ set_fg_color(7, 'l')
+ end
- def on_99(_) set_fg_color(9, 'l') end
+ def on_99(_)
+ set_fg_color(9, 'l')
+ end
- def on_100(_) set_bg_color(0, 'l') end
+ def on_100(_)
+ set_bg_color(0, 'l')
+ end
- def on_101(_) set_bg_color(1, 'l') end
+ def on_101(_)
+ set_bg_color(1, 'l')
+ end
- def on_102(_) set_bg_color(2, 'l') end
+ def on_102(_)
+ set_bg_color(2, 'l')
+ end
- def on_103(_) set_bg_color(3, 'l') end
+ def on_103(_)
+ set_bg_color(3, 'l')
+ end
- def on_104(_) set_bg_color(4, 'l') end
+ def on_104(_)
+ set_bg_color(4, 'l')
+ end
- def on_105(_) set_bg_color(5, 'l') end
+ def on_105(_)
+ set_bg_color(5, 'l')
+ end
- def on_106(_) set_bg_color(6, 'l') end
+ def on_106(_)
+ set_bg_color(6, 'l')
+ end
- def on_107(_) set_bg_color(7, 'l') end
+ def on_107(_)
+ set_bg_color(7, 'l')
+ end
- def on_109(_) set_bg_color(9, 'l') end
+ def on_109(_)
+ set_bg_color(9, 'l')
+ end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section
diff --git a/lib/gitlab/ci/artifact_file_reader.rb b/lib/gitlab/ci/artifact_file_reader.rb
index c2d17cc176e..6395a20ca99 100644
--- a/lib/gitlab/ci/artifact_file_reader.rb
+++ b/lib/gitlab/ci/artifact_file_reader.rb
@@ -45,6 +45,31 @@ module Gitlab
end
def read_zip_file!(file_path)
+ if ::Gitlab::Ci::Features.new_artifact_file_reader_enabled?(job.project)
+ read_with_new_artifact_file_reader(file_path)
+ else
+ read_with_legacy_artifact_file_reader(file_path)
+ end
+ end
+
+ def read_with_new_artifact_file_reader(file_path)
+ job.artifacts_file.use_open_file do |file|
+ zip_file = Zip::File.new(file, false, true)
+ entry = zip_file.find_entry(file_path)
+
+ unless entry
+ raise Error, "Path `#{file_path}` does not exist inside the `#{job.name}` artifacts archive!"
+ end
+
+ if entry.name_is_directory?
+ raise Error, "Path `#{file_path}` was expected to be a file but it was a directory!"
+ end
+
+ zip_file.read(entry)
+ end
+ end
+
+ def read_with_legacy_artifact_file_reader(file_path)
job.artifacts_file.use_file do |archive_path|
Zip::File.open(archive_path) do |zip_file|
entry = zip_file.find_entry(file_path)
diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
index b64990d6a7a..72ef0a8d067 100644
--- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
+++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
@@ -33,7 +33,7 @@ module Gitlab
def kubernetes_namespace
strong_memoize(:kubernetes_namespace) do
- Clusters::KubernetesNamespaceFinder.new(
+ ::Clusters::KubernetesNamespaceFinder.new(
deployment_cluster,
project: environment.project,
environment_name: environment.name,
@@ -47,7 +47,7 @@ module Gitlab
return if conflicting_ci_namespace_requested?(namespace)
- Clusters::Kubernetes::CreateOrUpdateNamespaceService.new(
+ ::Clusters::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: deployment_cluster,
kubernetes_namespace: namespace
).execute
@@ -71,7 +71,7 @@ module Gitlab
end
def build_namespace_record
- Clusters::BuildKubernetesNamespaceService.new(
+ ::Clusters::BuildKubernetesNamespaceService.new(
deployment_cluster,
environment: environment
).execute
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index d81a3fef1f5..9d269831679 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -62,6 +62,10 @@ module Gitlab
root.jobs_value
end
+ def normalized_jobs
+ @normalized_jobs ||= Ci::Config::Normalizer.new(jobs).normalize_jobs
+ end
+
private
def expand_config(config)
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index f960cec1f26..ecc2c5cb729 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -122,39 +122,9 @@ module Gitlab
:needs, :retry, :parallel, :start_in,
:interruptible, :timeout, :resource_group, :release
- Matcher = Struct.new(:name, :config) do
- def applies?
- job_is_not_hidden? &&
- config_is_a_hash? &&
- has_job_keys?
- end
-
- private
-
- def job_is_not_hidden?
- !name.to_s.start_with?('.')
- end
-
- def config_is_a_hash?
- config.is_a?(Hash)
- end
-
- def has_job_keys?
- if name == :default
- config.key?(:script)
- else
- (ALLOWED_KEYS & config.keys).any?
- end
- end
- end
-
def self.matching?(name, config)
- if Gitlab::Ci::Features.job_entry_matches_all_keys?
- Matcher.new(name, config).applies?
- else
- !name.to_s.start_with?('.') &&
- config.is_a?(Hash) && config.key?(:script)
- end
+ !name.to_s.start_with?('.') &&
+ config.is_a?(Hash) && config.key?(:script)
end
def self.visible?
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index 1d3036189b0..b5ce42969a5 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -14,8 +14,8 @@ module Gitlab
validates :config, type: Hash
validate do
- unless has_valid_jobs?
- errors.add(:config, 'should contain valid jobs')
+ each_unmatched_job do |name|
+ errors.add(name, 'config should implement a script: or a trigger: keyword')
end
unless has_visible_job?
@@ -23,9 +23,9 @@ module Gitlab
end
end
- def has_valid_jobs?
- config.all? do |name, value|
- Jobs.find_type(name, value)
+ def each_unmatched_job
+ config.each do |name, value|
+ yield(name) unless Jobs.find_type(name, value)
end
end
diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb
index 19d6a470941..2d93f1ab06e 100644
--- a/lib/gitlab/ci/config/entry/root.rb
+++ b/lib/gitlab/ci/config/entry/root.rb
@@ -134,7 +134,7 @@ module Gitlab
@jobs_config = @config
.except(*self.class.reserved_nodes_names)
.select do |name, config|
- Entry::Jobs.find_type(name, config).present?
+ Entry::Jobs.find_type(name, config).present? || ALLOWED_KEYS.exclude?(name)
end
@config = @config.except(*@jobs_config.keys)
diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb
index 451ba14bb89..22fcd84c968 100644
--- a/lib/gitlab/ci/config/normalizer.rb
+++ b/lib/gitlab/ci/config/normalizer.rb
@@ -11,6 +11,7 @@ module Gitlab
end
def normalize_jobs
+ return {} unless @jobs_config
return @jobs_config if parallelized_jobs.empty?
expand_parallelize_jobs do |job_name, config|
diff --git a/lib/gitlab/ci/config/normalizer/matrix_strategy.rb b/lib/gitlab/ci/config/normalizer/matrix_strategy.rb
index db21274a9ed..5a23836d8a0 100644
--- a/lib/gitlab/ci/config/normalizer/matrix_strategy.rb
+++ b/lib/gitlab/ci/config/normalizer/matrix_strategy.rb
@@ -48,14 +48,13 @@ module Gitlab
}
end
- def name_with_details
- vars = variables.map { |key, value| "#{key}=#{value}"}.join('; ')
-
- "#{job_name} (#{vars})"
- end
-
def name
- "#{job_name} #{instance}/#{total}"
+ vars = variables
+ .values
+ .compact
+ .join(', ')
+
+ "#{job_name}: [#{vars}]"
end
private
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 2f6667d3600..e770187b124 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -31,56 +31,49 @@ module Gitlab
::Feature.enabled?(:ci_store_pipeline_messages, project, default_enabled: true)
end
- # Remove in https://gitlab.com/gitlab-org/gitlab/-/issues/227052
- def self.variables_api_filter_environment_scope?
- ::Feature.enabled?(:ci_variables_api_filter_environment_scope, default_enabled: true)
- end
-
def self.raise_job_rules_without_workflow_rules_warning?
::Feature.enabled?(:ci_raise_job_rules_without_workflow_rules_warning, default_enabled: true)
end
- def self.keep_latest_artifacts_for_ref_enabled?(project)
- ::Feature.enabled?(:keep_latest_artifacts_for_ref, project, default_enabled: false)
- end
-
- def self.destroy_only_unlocked_expired_artifacts_enabled?
- ::Feature.enabled?(:destroy_only_unlocked_expired_artifacts, default_enabled: false)
- end
-
def self.bulk_insert_on_create?(project)
::Feature.enabled?(:ci_bulk_insert_on_create, project, default_enabled: true)
end
- def self.ci_if_parenthesis_enabled?
- ::Feature.enabled?(:ci_if_parenthesis_enabled, default_enabled: true)
+ # NOTE: The feature flag `disallow_to_create_merge_request_pipelines_in_target_project`
+ # is a safe switch to disable the feature for a parituclar project when something went wrong,
+ # therefore it's not supposed to be enabled by default.
+ def self.disallow_to_create_merge_request_pipelines_in_target_project?(target_project)
+ ::Feature.enabled?(:ci_disallow_to_create_merge_request_pipelines_in_target_project, target_project)
end
- def self.allow_to_create_merge_request_pipelines_in_target_project?(target_project)
- ::Feature.enabled?(:ci_allow_to_create_merge_request_pipelines_in_target_project, target_project, default_enabled: true)
+ def self.lint_creates_pipeline_with_dry_run?(project)
+ ::Feature.enabled?(:ci_lint_creates_pipeline_with_dry_run, project, default_enabled: true)
end
- def self.ci_plan_needs_size_limit?(project)
- ::Feature.enabled?(:ci_plan_needs_size_limit, project, default_enabled: true)
+ def self.project_transactionless_destroy?(project)
+ Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false)
end
- def self.job_entry_matches_all_keys?
- ::Feature.enabled?(:ci_job_entry_matches_all_keys)
+ def self.coverage_report_view?(project)
+ ::Feature.enabled?(:coverage_report_view, project, default_enabled: true)
end
- def self.lint_creates_pipeline_with_dry_run?(project)
- ::Feature.enabled?(:ci_lint_creates_pipeline_with_dry_run, project, default_enabled: true)
+ def self.child_of_child_pipeline_enabled?(project)
+ ::Feature.enabled?(:ci_child_of_child_pipeline, project, default_enabled: true)
end
- def self.reset_ci_minutes_for_all_namespaces?
- ::Feature.enabled?(:reset_ci_minutes_for_all_namespaces, default_enabled: false)
+ def self.trace_overwrite?
+ ::Feature.enabled?(:ci_trace_overwrite, type: :ops, default_enabled: false)
end
- def self.expand_names_for_cross_pipeline_artifacts?(project)
- ::Feature.enabled?(:ci_expand_names_for_cross_pipeline_artifacts, project)
+ def self.accept_trace?(project)
+ ::Feature.enabled?(:ci_enable_live_trace, project) &&
+ ::Feature.enabled?(:ci_accept_trace, project, type: :ops, default_enabled: false)
+ end
+
+ def self.new_artifact_file_reader_enabled?(project)
+ ::Feature.enabled?(:ci_new_artifact_file_reader, project, default_enabled: false)
end
end
end
end
-
-::Gitlab::Ci::Features.prepend_if_ee('::EE::Gitlab::Ci::Features')
diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb
new file mode 100644
index 00000000000..86a9ebfa451
--- /dev/null
+++ b/lib/gitlab/ci/lint.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Lint
+ class Result
+ attr_reader :jobs, :errors, :warnings
+
+ def initialize(jobs:, errors:, warnings:)
+ @jobs = jobs
+ @errors = errors
+ @warnings = warnings
+ end
+
+ def valid?
+ @errors.empty?
+ end
+ end
+
+ def initialize(project:, current_user:)
+ @project = project
+ @current_user = current_user
+ end
+
+ def validate(content, dry_run: false)
+ if dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
+ simulate_pipeline_creation(content)
+ else
+ static_validation(content)
+ end
+ end
+
+ private
+
+ def simulate_pipeline_creation(content)
+ pipeline = ::Ci::CreatePipelineService
+ .new(@project, @current_user, ref: @project.default_branch)
+ .execute(:push, dry_run: true, content: content)
+
+ Result.new(
+ jobs: dry_run_convert_to_jobs(pipeline.stages),
+ errors: pipeline.error_messages.map(&:content),
+ warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content)
+ )
+ end
+
+ def static_validation(content)
+ result = Gitlab::Ci::YamlProcessor.new(
+ content,
+ project: @project,
+ user: @current_user,
+ sha: @project.repository.commit.sha
+ ).execute
+
+ Result.new(
+ jobs: static_validation_convert_to_jobs(result),
+ errors: result.errors,
+ warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
+ )
+ end
+
+ def dry_run_convert_to_jobs(stages)
+ stages.reduce([]) do |jobs, stage|
+ jobs + stage.statuses.map do |job|
+ {
+ name: job.name,
+ stage: stage.name,
+ before_script: job.options[:before_script].to_a,
+ script: job.options[:script].to_a,
+ after_script: job.options[:after_script].to_a,
+ tag_list: (job.tag_list if job.is_a?(::Ci::Build)).to_a,
+ environment: job.options.dig(:environment, :name),
+ when: job.when,
+ allow_failure: job.allow_failure
+ }
+ end
+ end
+ end
+
+ def static_validation_convert_to_jobs(result)
+ jobs = []
+ return jobs unless result.valid?
+
+ result.stages.each do |stage_name|
+ result.builds.each do |job|
+ next unless job[:stage] == stage_name
+
+ jobs << {
+ name: job[:name],
+ stage: stage_name,
+ before_script: job.dig(:options, :before_script).to_a,
+ script: job.dig(:options, :script).to_a,
+ after_script: job.dig(:options, :after_script).to_a,
+ tag_list: job[:tag_list].to_a,
+ only: job[:only],
+ except: job[:except],
+ environment: job[:environment],
+ when: job[:when],
+ allow_failure: job[:allow_failure]
+ }
+ end
+ end
+
+ jobs
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb
index 58d55b1bd6f..e5a7151b823 100644
--- a/lib/gitlab/ci/mask_secret.rb
+++ b/lib/gitlab/ci/mask_secret.rb
@@ -8,6 +8,11 @@ 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
end
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index 4190c40eb66..9662209f88e 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -20,11 +20,7 @@ module Gitlab
pipeline_schedule: @command.schedule,
merge_request: @command.merge_request,
external_pull_request: @command.external_pull_request,
- variables_attributes: Array(@command.variables_attributes),
- # This should be removed and set on the database column default
- # level when the keep_latest_artifacts_for_ref feature flag is
- # removed.
- locked: ::Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(@command.project) ? :artifacts_locked : :unlocked
+ variables_attributes: Array(@command.variables_attributes)
)
end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index dbaa6951e64..d1882059dd8 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, :config_processor, :stage_seeds
+ :config_content, :yaml_processor_result, :stage_seeds
) do
include Gitlab::Utils::StrongMemoize
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/remote.rb b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb
index dcc336b8929..4990a5a6eb5 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content/remote.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb
@@ -9,7 +9,7 @@ module Gitlab
class Remote < Source
def content
strong_memoize(:content) do
- next unless ci_config_path =~ URI.regexp(%w[http https])
+ next unless ci_config_path =~ URI::DEFAULT_PARSER.make_regexp(%w[http https])
YAML.dump('include' => [{ 'remote' => ci_config_path }])
end
diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb
index 2cfcb295407..8ccb33ffd34 100644
--- a/lib/gitlab/ci/pipeline/chain/config/process.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/process.rb
@@ -11,20 +11,23 @@ module Gitlab
def perform!
raise ArgumentError, 'missing config content' unless @command.config_content
- @command.config_processor = ::Gitlab::Ci::YamlProcessor.new(
+ result = ::Gitlab::Ci::YamlProcessor.new(
@command.config_content, {
project: project,
sha: @pipeline.sha,
user: current_user,
parent_pipeline: parent_pipeline
}
- )
+ ).execute
+
+ add_warnings_to_pipeline(result.warnings)
- add_warnings_to_pipeline(@command.config_processor.warnings)
- rescue Gitlab::Ci::YamlProcessor::ValidationError => ex
- add_warnings_to_pipeline(ex.warnings)
+ if result.valid?
+ @command.yaml_processor_result = result
+ else
+ error(result.errors.first, config_error: true)
+ end
- error(ex.message, config_error: true)
rescue => ex
Gitlab::ErrorTracking.track_exception(ex,
project_id: project.id,
diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
index a793ae9cc24..3c910963a2a 100644
--- a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
+++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
@@ -39,7 +39,7 @@ module Gitlab
end
def workflow_config
- @command.config_processor.workflow_attributes || {}
+ @command.yaml_processor_result.workflow_attributes || {}
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
index 9267c72efa4..71f22c52869 100644
--- a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
+++ b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
@@ -6,13 +6,13 @@ module Gitlab
module Chain
class RemoveUnwantedChatJobs < Chain::Base
def perform!
- raise ArgumentError, 'missing config processor' unless @command.config_processor
+ raise ArgumentError, 'missing YAML processor result' unless @command.yaml_processor_result
return unless pipeline.chat?
# When scheduling a chat pipeline we only want to run the build
# that matches the chat command.
- @command.config_processor.jobs.select! do |name, _|
+ @command.yaml_processor_result.jobs.select! do |name, _|
name.to_s == command.chat_data[:command].to_s
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb
index e48e79d561b..e10a0bc3718 100644
--- a/lib/gitlab/ci/pipeline/chain/seed.rb
+++ b/lib/gitlab/ci/pipeline/chain/seed.rb
@@ -9,7 +9,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
def perform!
- raise ArgumentError, 'missing config processor' unless @command.config_processor
+ raise ArgumentError, 'missing YAML processor result' unless @command.yaml_processor_result
# Allocate next IID. This operation must be outside of transactions of pipeline creations.
pipeline.ensure_project_iid!
@@ -56,7 +56,7 @@ module Gitlab
end
def stages_attributes
- @command.config_processor.stages_attributes
+ @command.yaml_processor_result.stages_attributes
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb
index 24628338dd2..d056501a6d3 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/external.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb
@@ -51,7 +51,7 @@ module Gitlab
def validate_service_request
Gitlab::HTTP.post(
validation_service_url, timeout: VALIDATION_REQUEST_TIMEOUT,
- body: validation_service_payload(@pipeline, @command.config_processor.stages_attributes)
+ body: validation_service_payload(@pipeline, @command.yaml_processor_result.stages_attributes)
)
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index 5b7365cb33b..ac03ef79ccb 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -24,26 +24,8 @@ module Gitlab
Expression::Lexeme::Or
].freeze
- # To be removed with `ci_if_parenthesis_enabled`
- LEGACY_LEXEMES = [
- Expression::Lexeme::Variable,
- Expression::Lexeme::String,
- Expression::Lexeme::Pattern,
- Expression::Lexeme::Null,
- Expression::Lexeme::Equals,
- Expression::Lexeme::Matches,
- Expression::Lexeme::NotEquals,
- Expression::Lexeme::NotMatches,
- Expression::Lexeme::And,
- Expression::Lexeme::Or
- ].freeze
-
def self.lexemes
- if ::Gitlab::Ci::Features.ci_if_parenthesis_enabled?
- LEXEMES
- else
- LEGACY_LEXEMES
- end
+ LEXEMES
end
MAX_TOKENS = 100
diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb
index 27d7aa2f37e..a20b0015e05 100644
--- a/lib/gitlab/ci/pipeline/expression/parser.rb
+++ b/lib/gitlab/ci/pipeline/expression/parser.rb
@@ -15,12 +15,7 @@ module Gitlab
def tree
results = []
- tokens =
- if ::Gitlab::Ci::Features.ci_if_parenthesis_enabled?
- tokens_rpn
- else
- legacy_tokens_rpn
- end
+ tokens = tokens_rpn
tokens.each do |token|
case token.type
@@ -78,27 +73,6 @@ module Gitlab
output.concat(operators.reverse)
end
-
- # To be removed with `ci_if_parenthesis_enabled`
- def legacy_tokens_rpn
- output = []
- operators = []
-
- @tokens.each do |token|
- case token.type
- when :value
- output.push(token)
- when :logical_operator
- if operators.any? && token.lexeme.precedence >= operators.last.lexeme.precedence
- output.push(operators.pop)
- end
-
- operators.push(token)
- end
- end
-
- output.concat(operators.reverse)
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 3be3fa63b92..91dbcc616ea 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -11,8 +11,6 @@ module Gitlab
delegate :dig, to: :@seed_attributes
- DEFAULT_NEEDS_LIMIT = 10
-
def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline
@seed_attributes = attributes
@@ -140,11 +138,7 @@ module Gitlab
end
def max_needs_allowed
- if ::Gitlab::Ci::Features.ci_plan_needs_size_limit?(@pipeline.project)
- @pipeline.project.actual_limits.ci_needs_size_limit
- else
- DEFAULT_NEEDS_LIMIT
- end
+ @pipeline.project.actual_limits.ci_needs_size_limit
end
def pipeline_attributes
diff --git a/lib/gitlab/ci/pipeline_object_hierarchy.rb b/lib/gitlab/ci/pipeline_object_hierarchy.rb
new file mode 100644
index 00000000000..de3262b10e0
--- /dev/null
+++ b/lib/gitlab/ci/pipeline_object_hierarchy.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class PipelineObjectHierarchy < ::Gitlab::ObjectHierarchy
+ private
+
+ def middle_table
+ ::Ci::Sources::Pipeline.arel_table
+ end
+
+ def from_tables(cte)
+ [objects_table, cte.table, middle_table]
+ end
+
+ def parent_id_column(_cte)
+ middle_table[:source_pipeline_id]
+ end
+
+ def ancestor_conditions(cte)
+ middle_table[:source_pipeline_id].eq(objects_table[:id]).and(
+ middle_table[:pipeline_id].eq(cte.table[:id])
+ ).and(
+ same_project_condition
+ )
+ end
+
+ def descendant_conditions(cte)
+ middle_table[:pipeline_id].eq(objects_table[:id]).and(
+ middle_table[:source_pipeline_id].eq(cte.table[:id])
+ ).and(
+ same_project_condition
+ )
+ end
+
+ def same_project_condition
+ if options[:same_project]
+ middle_table[:source_project_id].eq(middle_table[:project_id])
+ else
+ Arel.sql('TRUE')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb
index 75898745366..15a3c862c9e 100644
--- a/lib/gitlab/ci/reports/test_case.rb
+++ b/lib/gitlab/ci/reports/test_case.rb
@@ -8,7 +8,7 @@ module Gitlab
STATUS_FAILED = 'failed'
STATUS_SKIPPED = 'skipped'
STATUS_ERROR = 'error'
- STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze
+ STATUS_TYPES = [STATUS_ERROR, STATUS_FAILED, STATUS_SUCCESS, STATUS_SKIPPED].freeze
attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job
diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb
index 5ee779227ec..e9b78b841e4 100644
--- a/lib/gitlab/ci/reports/test_suite.rb
+++ b/lib/gitlab/ci/reports/test_suite.rb
@@ -78,11 +78,27 @@ module Gitlab
end
end
+ def sorted
+ sort_by_status
+ sort_by_execution_time_desc
+ self
+ end
+
private
def existing_key?(test_case)
@test_cases[test_case.status]&.key?(test_case.key)
end
+
+ def sort_by_status
+ @test_cases = @test_cases.sort_by { |status, _| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.index(status) }.to_h
+ end
+
+ def sort_by_execution_time_desc
+ @test_cases = @test_cases.keys.each_with_object({}) do |key, hash|
+ hash[key] = @test_cases[key].sort_by { |_key, test_case| -test_case.execution_time }.to_h
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb
index 4746195c618..b95565b5e09 100644
--- a/lib/gitlab/ci/status/bridge/common.rb
+++ b/lib/gitlab/ci/status/bridge/common.rb
@@ -10,14 +10,28 @@ module Gitlab
end
def has_details?
- false
+ !!details_path
+ end
+
+ def details_path
+ return unless Feature.enabled?(:ci_bridge_pipeline_details, subject.project, default_enabled: true)
+ return unless can?(user, :read_pipeline, downstream_pipeline)
+
+ project_pipeline_path(downstream_project, downstream_pipeline)
end
def has_action?
false
end
- def details_path
+ private
+
+ def downstream_pipeline
+ subject.downstream_pipeline
+ end
+
+ def downstream_project
+ downstream_pipeline&.project
end
end
end
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 88846f724e7..f6562737838 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -25,7 +25,8 @@ module Gitlab
insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline',
bridge_pipeline_is_child_pipeline: 'creation of child pipeline not allowed from another child pipeline',
downstream_pipeline_creation_failed: 'downstream pipeline can not be created',
- secrets_provider_not_found: 'secrets provider can not be found'
+ secrets_provider_not_found: 'secrets provider can not be found',
+ reached_max_descendant_pipelines_depth: 'reached maximum depth of child pipelines'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/status/composite.rb b/lib/gitlab/ci/status/composite.rb
index 04a9fc29802..9a4f5644f7d 100644
--- a/lib/gitlab/ci/status/composite.rb
+++ b/lib/gitlab/ci/status/composite.rb
@@ -7,7 +7,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
# This class accepts an array of arrays/hashes/or objects
- def initialize(all_statuses, with_allow_failure: true)
+ def initialize(all_statuses, with_allow_failure: true, dag: false)
unless all_statuses.respond_to?(:pluck)
raise ArgumentError, "all_statuses needs to respond to `.pluck`"
end
@@ -15,6 +15,7 @@ module Gitlab
@status_set = Set.new
@status_key = 0
@allow_failure_key = 1 if with_allow_failure
+ @dag = dag
consume_all_statuses(all_statuses)
end
@@ -31,7 +32,13 @@ module Gitlab
return if none?
strong_memoize(:status) do
- if only_of?(:skipped, :ignored)
+ if @dag && any_of?(:skipped)
+ # The DAG job is skipped if one of the needs does not run at all.
+ 'skipped'
+ elsif @dag && !only_of?(:success, :failed, :canceled, :skipped, :success_with_warnings)
+ # DAG is blocked from executing if a dependent is not "complete"
+ 'pending'
+ elsif only_of?(:skipped, :ignored)
'skipped'
elsif only_of?(:success, :skipped, :success_with_warnings, :ignored)
'success'
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 968ff0fce89..6966ce88b30 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -150,16 +150,22 @@ workflow:
- exists:
- .static
+# NOTE: These links point to the latest templates for development in GitLab canonical project,
+# therefore the actual templates that were included for Auto DevOps pipelines
+# could be different from the contents in the links.
+# To view the actual templates, please replace `master` to the specific GitLab version when
+# the Auto DevOps pipeline started running e.g. `v13.0.2-ee`.
include:
- - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
- - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
- - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
- - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
- - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
- - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
- - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
- - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
- - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
- - template: Security/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
- - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
- - template: Security/Secret-Detection.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+ - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+ - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+ - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+ - template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
+ - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+ - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+ - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+ - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+ - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+ - template: Security/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
+ - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+ - template: Security/Secret-Detection.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
index 8553a940bd7..5edb26a0b56 100644
--- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
@@ -7,7 +7,7 @@ performance:
variables:
DOCKER_TLS_CERTDIR: ""
SITESPEED_IMAGE: sitespeedio/sitespeed.io
- SITESPEED_VERSION: 13.3.0
+ SITESPEED_VERSION: 14.1.0
SITESPEED_OPTIONS: ''
services:
- docker:19.03.12-dind
@@ -20,15 +20,15 @@ performance:
fi
- export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
- mkdir gitlab-exporter
- - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.0.1/index.js
+ - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js
- mkdir sitespeed-results
- |
if [ -f .gitlab-urls.txt ]
then
sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt $SITESPEED_OPTIONS
+ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results .gitlab-urls.txt $SITESPEED_OPTIONS
else
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" $SITESPEED_OPTIONS
+ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" $SITESPEED_OPTIONS
fi
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
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 cf851c875ee..568ceceeaa2 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -9,6 +9,8 @@ code_quality:
DOCKER_TLS_CERTDIR: ""
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1"
needs: []
+ before_script:
+ - export SOURCE_CODE=$PWD
script:
- |
if ! docker info &>/dev/null; then
@@ -16,11 +18,27 @@ code_quality:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
+ - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
+ function propagate_env_vars() {
+ CURRENT_ENV=$(printenv)
+
+ for VAR_NAME; do
+ echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
+ done
+ }
- docker pull --quiet "$CODE_QUALITY_IMAGE"
- - docker run
- --env SOURCE_CODE="$PWD"
- --volume "$PWD":/code
- --volume /var/run/docker.sock:/var/run/docker.sock
+ - |
+ docker run \
+ $(propagate_env_vars \
+ SOURCE_CODE \
+ TIMEOUT_SECONDS \
+ CODECLIMATE_DEBUG \
+ CODECLIMATE_DEV \
+ REPORT_STDOUT \
+ ENGINE_MEMORY_LIMIT_BYTES \
+ ) \
+ --volume "$PWD":/code \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
"$CODE_QUALITY_IMAGE" /code
artifacts:
reports:
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 2922e1c6e88..829fd7a722f 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -2,9 +2,6 @@
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3"
dependencies: []
-include:
- - template: Jobs/Deploy/ECS.gitlab-ci.yml
-
review:
extends: .auto-deploy
stage: review
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..829fd7a722f
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -0,0 +1,249 @@
+.auto-deploy:
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3"
+ dependencies: []
+
+review:
+ extends: .auto-deploy
+ stage: review
+ script:
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy
+ - auto-deploy persist_environment_url
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ on_stop: stop_review
+ artifacts:
+ paths: [environment_url.txt, tiller.log]
+ when: always
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
+
+stop_review:
+ extends: .auto-deploy
+ stage: cleanup
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - auto-deploy initialize_tiller
+ - auto-deploy delete
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ allow_failure: true
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
+ when: manual
+
+# Staging deploys are disabled by default since
+# continuous deployment to production is enabled by default
+# If you prefer to automatically deploy to staging and
+# only manually promote to production, enable this job by setting
+# STAGING_ENABLED.
+
+staging:
+ extends: .auto-deploy
+ stage: staging
+ script:
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy
+ environment:
+ name: staging
+ url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$STAGING_ENABLED'
+
+# Canaries are disabled by default, but if you want them,
+# and know what the downsides are, you can enable this by setting
+# CANARY_ENABLED.
+
+canary:
+ extends: .auto-deploy
+ stage: canary
+ allow_failure: true
+ script:
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy canary
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$CANARY_ENABLED'
+ when: manual
+
+.production: &production_template
+ extends: .auto-deploy
+ stage: production
+ script:
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy
+ - auto-deploy delete canary
+ - auto-deploy delete rollout
+ - auto-deploy persist_environment_url
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ artifacts:
+ paths: [environment_url.txt, tiller.log]
+ when: always
+
+production:
+ <<: *production_template
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$STAGING_ENABLED'
+ when: never
+ - if: '$CANARY_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+
+production_manual:
+ <<: *production_template
+ allow_failure: false
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master" && $STAGING_ENABLED'
+ when: manual
+ - if: '$CI_COMMIT_BRANCH == "master" && $CANARY_ENABLED'
+ when: manual
+
+# This job implements incremental rollout on for every push to `master`.
+
+.rollout: &rollout_template
+ extends: .auto-deploy
+ script:
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy rollout $ROLLOUT_PERCENTAGE
+ - auto-deploy scale stable $((100-ROLLOUT_PERCENTAGE))
+ - auto-deploy delete canary
+ - auto-deploy persist_environment_url
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ artifacts:
+ paths: [environment_url.txt, tiller.log]
+ when: always
+
+.manual_rollout_template: &manual_rollout_template
+ <<: *rollout_template
+ stage: production
+ resource_group: production
+ allow_failure: true
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ # $INCREMENTAL_ROLLOUT_ENABLED is for compamtibilty with pre-GitLab 11.4 syntax
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "manual" || $INCREMENTAL_ROLLOUT_ENABLED'
+ when: manual
+
+.timed_rollout_template: &timed_rollout_template
+ <<: *rollout_template
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "manual"'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"'
+ when: delayed
+ start_in: 5 minutes
+
+timed rollout 10%:
+ <<: *timed_rollout_template
+ stage: incremental rollout 10%
+ variables:
+ ROLLOUT_PERCENTAGE: 10
+
+timed rollout 25%:
+ <<: *timed_rollout_template
+ stage: incremental rollout 25%
+ variables:
+ ROLLOUT_PERCENTAGE: 25
+
+timed rollout 50%:
+ <<: *timed_rollout_template
+ stage: incremental rollout 50%
+ variables:
+ ROLLOUT_PERCENTAGE: 50
+
+timed rollout 100%:
+ <<: *timed_rollout_template
+ <<: *production_template
+ stage: incremental rollout 100%
+ variables:
+ ROLLOUT_PERCENTAGE: 100
+
+rollout 10%:
+ <<: *manual_rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 10
+
+rollout 25%:
+ <<: *manual_rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 25
+
+rollout 50%:
+ <<: *manual_rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 50
+
+rollout 100%:
+ <<: *manual_rollout_template
+ <<: *production_template
+ allow_failure: false
diff --git a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml
index 4a9849c85c9..9a7c513c25f 100644
--- a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml
@@ -8,6 +8,7 @@ load_performance:
K6_VERSION: 0.27.0
K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js
K6_OPTIONS: ''
+ K6_DOCKER_OPTIONS: ''
services:
- docker:19.03.11-dind
script:
@@ -17,7 +18,7 @@ load_performance:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
- - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS
+ - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_DOCKER_OPTIONS $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS
artifacts:
reports:
load_performance: load-performance.json
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 3d0bacda853..7050b41e045 100644
--- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -1,27 +1,11 @@
apply:
stage: deploy
- image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.24.2"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.29.0"
environment:
name: production
variables:
TILLER_NAMESPACE: gitlab-managed-apps
GITLAB_MANAGED_APPS_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/config.yaml
- INGRESS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/ingress/values.yaml
- CERT_MANAGER_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cert-manager/values.yaml
- SENTRY_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/sentry/values.yaml
- GITLAB_RUNNER_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/gitlab-runner/values.yaml
- CILIUM_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cilium/values.yaml
- CILIUM_HUBBLE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cilium/hubble-values.yaml
- JUPYTERHUB_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/jupyterhub/values.yaml
- PROMETHEUS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/prometheus/values.yaml
- ELASTIC_STACK_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/elastic-stack/values.yaml
- VAULT_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/vault/values.yaml
- CROSSPLANE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/crossplane/values.yaml
- FLUENTD_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/fluentd/values.yaml
- KNATIVE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/knative/values.yaml
- POSTHOG_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/posthog/values.yaml
- FALCO_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/falco/values.yaml
- APPARMOR_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/apparmor/values.yaml
script:
- gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml
only:
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 e87f0f28d01..c3a92b67a8b 100644
--- a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
@@ -37,9 +37,6 @@ apifuzzer_fuzz:
$FUZZAPI_OPENAPI == null &&
$FUZZAPI_D_WORKER_IMAGE == null
when: never
- - if: $FUZZAPI_D_WORKER_IMAGE == null &&
- $FUZZAPI_TARGET_URL == null
- when: never
- if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
services:
- docker:19.03.12-dind
@@ -74,13 +71,15 @@ apifuzzer_fuzz:
-e FUZZAPI_TIMEOUT \
-e FUZZAPI_VERBOSE \
-e FUZZAPI_SERVICE_START_TIMEOUT \
+ -e FUZZAPI_HTTP_USERNAME \
+ -e FUZZAPI_HTTP_PASSWORD \
-e GITLAB_FEATURES \
-v $CI_PROJECT_DIR:/app \
-p 80:80 \
-p 8000:8000 \
-p 514:514 \
--restart=no \
- registry.gitlab.com/gitlab-org/security-products/analyzers/api-fuzzing-src:${FUZZAPI_VERSION}-engine
+ registry.gitlab.com/gitlab-org/security-products/analyzers/api-fuzzing:${FUZZAPI_VERSION}-engine
#
# Start target container
- |
@@ -119,6 +118,9 @@ apifuzzer_fuzz:
# Wait for testing to complete if api fuzzer is scanning
- if [ "$FUZZAPI_HAR$FUZZAPI_OPENAPI" != "" ]; then echo "Waiting for API Fuzzer to exit"; docker wait apifuzzer; fi
#
+ # Propagate exit code from api fuzzer (if any)
+ - if [[ $(docker inspect apifuzzer --format='{{.State.ExitCode}}') != "0" ]]; then echo "API Fuzzing exited with an error. Logs are available as job artifacts."; docker logs apifuzzer; exit 1; fi
+ #
# Run user provided pre-script
- sh -c "$FUZZAPI_POST_SCRIPT"
#
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 3f47e575afd..4b957a8f771 100644
--- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
@@ -34,5 +34,5 @@ variables:
rules:
- if: $COVFUZZ_DISABLED
when: never
- - if: $GITLAB_FEATURES =~ /\bcoverage_fuzzing\b/
+ - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bcoverage_fuzzing\b/
- if: $CI_RUNNER_EXECUTABLE_ARCH == "linux"
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 d5275c57ef8..3789f0edc1c 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -12,81 +12,24 @@ variables:
DS_DEFAULT_ANALYZERS: "bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python"
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
DS_MAJOR_VERSION: 2
- DS_DISABLE_DIND: "true"
dependency_scanning:
stage: test
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- DOCKER_TLS_CERTDIR: ""
- allow_failure: true
- services:
- - docker:stable-dind
script:
- - |
- if ! docker info &>/dev/null; then
- if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
- export DOCKER_HOST='tcp://localhost:2375'
- fi
- fi
- - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
- function propagate_env_vars() {
- CURRENT_ENV=$(printenv)
-
- for VAR_NAME; do
- echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
- done
- }
- - |
- docker run \
- $(propagate_env_vars \
- DS_ANALYZER_IMAGES \
- SECURE_ANALYZERS_PREFIX \
- DS_ANALYZER_IMAGE_TAG \
- DS_DEFAULT_ANALYZERS \
- DS_EXCLUDED_PATHS \
- DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- DS_PULL_ANALYZER_IMAGE_TIMEOUT \
- DS_RUN_ANALYZER_TIMEOUT \
- DS_PYTHON_VERSION \
- DS_PIP_VERSION \
- DS_PIP_DEPENDENCY_PATH \
- DS_JAVA_VERSION \
- GEMNASIUM_DB_LOCAL_PATH \
- GEMNASIUM_DB_REMOTE_URL \
- GEMNASIUM_DB_REF_NAME \
- PIP_INDEX_URL \
- PIP_EXTRA_INDEX_URL \
- PIP_REQUIREMENTS_FILE \
- MAVEN_CLI_OPTS \
- GRADLE_CLI_OPTS \
- SBT_CLI_OPTS \
- BUNDLER_AUDIT_UPDATE_DISABLED \
- BUNDLER_AUDIT_ADVISORY_DB_URL \
- BUNDLER_AUDIT_ADVISORY_DB_REF_NAME \
- RETIREJS_JS_ADVISORY_DB \
- RETIREJS_NODE_ADVISORY_DB \
- DS_REMEDIATE \
- ) \
- --volume "$PWD:/code" \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_MAJOR_VERSION" /code
+ - echo "$CI_JOB_NAME is used for configuration only, and its script should not be executed"
+ - exit 1
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
dependencies: []
rules:
- - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'true'
- when: never
- - if: $CI_COMMIT_BRANCH &&
- $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ - when: never
.ds-analyzer:
extends: dependency_scanning
- services: []
+ allow_failure: true
rules:
- - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ - if: $DEPENDENCY_SCANNING_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdependency_scanning\b/
@@ -96,9 +39,11 @@ dependency_scanning:
gemnasium-dependency_scanning:
extends: .ds-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/gemnasium:$DS_MAJOR_VERSION"
+ name: "$DS_ANALYZER_IMAGE"
+ variables:
+ DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium:$DS_MAJOR_VERSION"
rules:
- - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ - if: $DEPENDENCY_SCANNING_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
@@ -112,13 +57,16 @@ gemnasium-dependency_scanning:
- '{package-lock.json,*/package-lock.json,*/*/package-lock.json}'
- '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
- '{packages.lock.json,*/packages.lock.json,*/*/packages.lock.json}'
+ - '{conan.lock,*/conan.lock,*/*/conan.lock}'
gemnasium-maven-dependency_scanning:
extends: .ds-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION"
+ name: "$DS_ANALYZER_IMAGE"
+ variables:
+ DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION"
rules:
- - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ - if: $DEPENDENCY_SCANNING_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
@@ -132,9 +80,11 @@ gemnasium-maven-dependency_scanning:
gemnasium-python-dependency_scanning:
extends: .ds-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/gemnasium-python:$DS_MAJOR_VERSION"
+ name: "$DS_ANALYZER_IMAGE"
+ variables:
+ DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-python:$DS_MAJOR_VERSION"
rules:
- - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ - if: $DEPENDENCY_SCANNING_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
@@ -155,9 +105,11 @@ gemnasium-python-dependency_scanning:
bundler-audit-dependency_scanning:
extends: .ds-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/bundler-audit:$DS_MAJOR_VERSION"
+ name: "$DS_ANALYZER_IMAGE"
+ variables:
+ DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/bundler-audit:$DS_MAJOR_VERSION"
rules:
- - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ - if: $DEPENDENCY_SCANNING_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
@@ -168,9 +120,11 @@ bundler-audit-dependency_scanning:
retire-js-dependency_scanning:
extends: .ds-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/retire.js:$DS_MAJOR_VERSION"
+ name: "$DS_ANALYZER_IMAGE"
+ variables:
+ DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/retire.js:$DS_MAJOR_VERSION"
rules:
- - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ - if: $DEPENDENCY_SCANNING_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 6eb17341472..77ea11d01d1 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -9,48 +9,29 @@ variables:
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
- SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, secrets, sobelow, pmd-apex, kubesec"
+ SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, sobelow, pmd-apex, kubesec"
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
SAST_ANALYZER_IMAGE_TAG: 2
- SAST_DISABLE_DIND: "true"
SCAN_KUBERNETES_MANIFESTS: "false"
sast:
stage: test
- allow_failure: true
artifacts:
reports:
sast: gl-sast-report.json
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'true'
- when: never
- - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bsast\b/
- image: docker:stable
+ - when: never
variables:
SEARCH_MAX_DEPTH: 4
- DOCKER_DRIVER: overlay2
- DOCKER_TLS_CERTDIR: ""
- services:
- - docker:stable-dind
script:
- - |
- if ! docker info &>/dev/null; then
- if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
- export DOCKER_HOST='tcp://localhost:2375'
- fi
- fi
- - |
- docker run \
- $(awk 'BEGIN{for(v in ENVIRON) print v}' | grep -v -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | awk '{printf " -e %s", $0}') \
- --volume "$PWD:/code" \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- "registry.gitlab.com/gitlab-org/security-products/sast:$SAST_ANALYZER_IMAGE_TAG" /app/bin/run /code
+ - echo "$CI_JOB_NAME is used for configuration only, and its script should not be executed"
+ - exit 1
.sast-analyzer:
extends: sast
- services: []
+ allow_failure: true
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH
script:
@@ -59,9 +40,11 @@ sast:
bandit-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /bandit/
@@ -71,9 +54,11 @@ bandit-sast:
brakeman-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /brakeman/
@@ -83,9 +68,11 @@ brakeman-sast:
eslint-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /eslint/
@@ -99,9 +86,11 @@ eslint-sast:
flawfinder-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /flawfinder/
@@ -112,9 +101,11 @@ flawfinder-sast:
kubesec-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /kubesec/ &&
@@ -123,9 +114,11 @@ kubesec-sast:
gosec-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /gosec/
@@ -135,9 +128,11 @@ gosec-sast:
nodejs-scan-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/
@@ -147,9 +142,11 @@ nodejs-scan-sast:
phpcs-security-audit-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /phpcs-security-audit/
@@ -159,31 +156,25 @@ phpcs-security-audit-sast:
pmd-apex-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /pmd-apex/
exists:
- '**/*.cls'
-secrets-sast:
- extends: .sast-analyzer
- image:
- name: "$SECURE_ANALYZERS_PREFIX/secrets:$SAST_ANALYZER_IMAGE_TAG"
- rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
- when: never
- - if: $CI_COMMIT_BRANCH &&
- $SAST_DEFAULT_ANALYZERS =~ /secrets/
-
security-code-scan-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /security-code-scan/
@@ -194,9 +185,11 @@ security-code-scan-sast:
sobelow-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /sobelow/
@@ -206,9 +199,11 @@ sobelow-sast:
spotbugs-sast:
extends: .sast-analyzer
image:
- name: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
rules:
- - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ - if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /spotbugs/
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 b897c7b482f..bde6a0fbebb 100644
--- a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
@@ -35,6 +35,7 @@ secret_detection:
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
script:
- git fetch origin $CI_DEFAULT_BRANCH $CI_BUILD_REF_NAME
- - export SECRET_DETECTION_COMMIT_TO=$(git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_BUILD_REF_NAME | tail -n 1)
- - export SECRET_DETECTION_COMMIT_FROM=$CI_COMMIT_SHA
+ - git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_BUILD_REF_NAME > "$CI_COMMIT_SHA"_commit_list.txt
+ - export SECRET_DETECTION_COMMITS_FILE="$CI_COMMIT_SHA"_commit_list.txt
- /analyzer run
+ - rm "$CI_COMMIT_SHA"_commit_list.txt
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
index 9dbd9b679a8..e591e3cc1e2 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
@@ -12,15 +12,15 @@ performance:
variables:
URL: ''
SITESPEED_IMAGE: sitespeedio/sitespeed.io
- SITESPEED_VERSION: 13.3.0
+ SITESPEED_VERSION: 14.1.0
SITESPEED_OPTIONS: ''
services:
- docker:stable-dind
script:
- mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js
- mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
paths:
diff --git a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml
index f964b3b2caf..cd23af562e5 100644
--- a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml
@@ -14,10 +14,11 @@ load_performance:
K6_VERSION: 0.27.0
K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js
K6_OPTIONS: ''
+ K6_DOCKER_OPTIONS: ''
services:
- docker:stable-dind
script:
- - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS
+ - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_DOCKER_OPTIONS $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS
artifacts:
reports:
load_performance: load-performance.json
diff --git a/lib/gitlab/ci/templates/npm.gitlab-ci.yml b/lib/gitlab/ci/templates/npm.gitlab-ci.yml
index 035ba52da84..0a739cf122d 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."
+ 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
}
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index f76aacc2d19..348e5472cb4 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -79,22 +79,13 @@ module Gitlab
job.trace_chunks.any? || current_path.present? || old_trace.present?
end
- def read
- stream = Gitlab::Ci::Trace::Stream.new do
- if trace_artifact
- trace_artifact.open
- elsif job.trace_chunks.any?
- Gitlab::Ci::Trace::ChunkedIO.new(job)
- elsif current_path
- File.open(current_path, "rb")
- elsif old_trace
- StringIO.new(old_trace)
- end
- end
+ def read(should_retry: true, &block)
+ read_stream(&block)
+ rescue Errno::ENOENT
+ raise unless should_retry
- yield stream
- ensure
- stream&.close
+ job.reset
+ read_stream(&block)
end
def write(mode, &blk)
@@ -141,6 +132,24 @@ module Gitlab
private
+ def read_stream
+ stream = Gitlab::Ci::Trace::Stream.new do
+ if trace_artifact
+ trace_artifact.open
+ elsif job.trace_chunks.any?
+ Gitlab::Ci::Trace::ChunkedIO.new(job)
+ elsif current_path
+ File.open(current_path, "rb")
+ elsif old_trace
+ StringIO.new(old_trace)
+ end
+ end
+
+ yield stream
+ ensure
+ stream&.close
+ end
+
def unsafe_write!(mode, &blk)
stream = Gitlab::Ci::Trace::Stream.new do
if trace_artifact
diff --git a/lib/gitlab/ci/trace/metrics.rb b/lib/gitlab/ci/trace/metrics.rb
new file mode 100644
index 00000000000..82a7d5fb83c
--- /dev/null
+++ b/lib/gitlab/ci/trace/metrics.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Trace
+ class Metrics
+ extend Gitlab::Utils::StrongMemoize
+
+ OPERATIONS = [:appended, :streamed, :chunked, :mutated, :overwrite,
+ :accepted, :finalized, :discarded, :conflict].freeze
+
+ def increment_trace_operation(operation: :unknown)
+ unless OPERATIONS.include?(operation)
+ raise ArgumentError, "unknown trace operation: #{operation}"
+ end
+
+ self.class.trace_operations.increment(operation: operation)
+ end
+
+ def increment_trace_bytes(size)
+ self.class.trace_bytes.increment(by: size.to_i)
+ end
+
+ def self.trace_operations
+ strong_memoize(:trace_operations) do
+ name = :gitlab_ci_trace_operations_total
+ comment = 'Total amount of different operations on a build trace'
+
+ Gitlab::Metrics.counter(name, comment)
+ end
+ end
+
+ def self.trace_bytes
+ strong_memoize(:trace_bytes) do
+ name = :gitlab_ci_trace_bytes_total
+ comment = 'Total amount of build trace bytes transferred'
+
+ Gitlab::Metrics.counter(name, comment)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 20f5620dd64..618438c8887 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -8,7 +8,7 @@ module Gitlab
BUFFER_SIZE = 4096
LIMIT_SIZE = 500.kilobytes
- attr_reader :stream
+ attr_reader :stream, :metrics
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
@@ -16,9 +16,10 @@ module Gitlab
alias_method :present?, :valid?
- def initialize
+ def initialize(metrics = Trace::Metrics.new)
@stream = yield
@stream&.binmode
+ @metrics = metrics
end
def valid?
@@ -43,6 +44,9 @@ module Gitlab
def append(data, offset)
data = data.force_encoding(Encoding::BINARY)
+ metrics.increment_trace_operation(operation: :streamed)
+ metrics.increment_trace_bytes(data.bytesize)
+
stream.seek(offset, IO::SEEK_SET)
stream.write(data)
stream.truncate(offset + data.bytesize)
diff --git a/lib/gitlab/ci/warnings.rb b/lib/gitlab/ci/warnings.rb
new file mode 100644
index 00000000000..7138fd21b72
--- /dev/null
+++ b/lib/gitlab/ci/warnings.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module Gitlab::Ci::Warnings
+ MAX_LIMIT = 25
+end
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index b7046064f44..ee55eb8b22a 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -1,183 +1,65 @@
# frozen_string_literal: true
+# This is the CI Linter component that runs the syntax validations
+# while parsing the YAML config into a data structure that is
+# then presented to the caller as result object.
+# After syntax validations (done by Ci::Config), this component also
+# runs logical validation on the built data structure.
module Gitlab
module Ci
class YamlProcessor
- # ValidationError is treated like a result object in the form of an exception.
- # We can return any warnings, raised during the config validation, along with
- # the error object until we support multiple messages to be returned.
- class ValidationError < StandardError
- attr_reader :warnings
-
- def initialize(message, warnings: [])
- @warnings = warnings
- super(message)
- end
- end
-
- include Gitlab::Config::Entry::LegacyValidationHelpers
+ ValidationError = Class.new(StandardError)
- attr_reader :stages, :jobs
+ def self.validation_message(content, opts = {})
+ result = new(content, opts).execute
- class Result
- attr_reader :config, :errors, :warnings
+ result.errors.first
+ end
- def initialize(config: nil, errors: [], warnings: [])
- @config = config
- @errors = errors
- @warnings = warnings
- end
+ def initialize(config_content, opts = {})
+ @config_content = config_content
+ @opts = opts
+ end
- def valid?
- config.present? && errors.empty?
+ def execute
+ if @config_content.blank?
+ return Result.new(errors: ['Please provide content of .gitlab-ci.yml'])
end
- end
- def initialize(config, opts = {})
- @ci_config = Gitlab::Ci::Config.new(config, **opts)
- @config = @ci_config.to_hash
+ @ci_config = Gitlab::Ci::Config.new(@config_content, **@opts)
unless @ci_config.valid?
- error!(@ci_config.errors.first)
+ return Result.new(ci_config: @ci_config, errors: @ci_config.errors, warnings: @ci_config.warnings)
end
- initial_parsing
- rescue Gitlab::Ci::Config::ConfigError => e
- error!(e.message)
- end
-
- def self.new_with_validation_errors(content, opts = {})
- return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) if content.blank?
+ run_logical_validations!
- config = Gitlab::Ci::Config.new(content, **opts)
- return Result.new(errors: config.errors, warnings: config.warnings) unless config.valid?
-
- config = Gitlab::Ci::YamlProcessor.new(content, opts)
- Result.new(config: config, warnings: config.warnings)
-
- rescue ValidationError => e
- Result.new(errors: [e.message], warnings: e.warnings)
+ Result.new(ci_config: @ci_config, warnings: @ci_config&.warnings)
rescue Gitlab::Ci::Config::ConfigError => e
- Result.new(errors: [e.message])
- end
-
- def warnings
- @ci_config&.warnings || []
- end
-
- def builds
- @jobs.map do |name, _|
- build_attributes(name)
- end
- end
-
- def build_attributes(name)
- job = @jobs.fetch(name.to_sym, {})
-
- { stage_idx: @stages.index(job[:stage]),
- stage: job[:stage],
- tag_list: job[:tags],
- name: job[:name].to_s,
- allow_failure: job[:ignore],
- when: job[:when] || 'on_success',
- environment: job[:environment_name],
- coverage_regex: job[:coverage],
- yaml_variables: transform_to_yaml_variables(job[:variables]),
- needs_attributes: job.dig(:needs, :job),
- interruptible: job[:interruptible],
- only: job[:only],
- except: job[:except],
- rules: job[:rules],
- cache: job[:cache],
- resource_group_key: job[:resource_group],
- scheduling_type: job[:scheduling_type],
- secrets: job[:secrets],
- options: {
- image: job[:image],
- services: job[:services],
- artifacts: job[:artifacts],
- dependencies: job[:dependencies],
- cross_dependencies: job.dig(:needs, :cross_dependency),
- job_timeout: job[:timeout],
- before_script: job[:before_script],
- script: job[:script],
- after_script: job[:after_script],
- environment: job[:environment],
- retry: job[:retry],
- parallel: job[:parallel],
- instance: job[:instance],
- start_in: job[:start_in],
- trigger: job[:trigger],
- bridge_needs: job.dig(:needs, :bridge)&.first,
- release: release(job)
- }.compact }.compact
- end
+ Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings)
- def release(job)
- job[:release]
- end
-
- def stage_builds_attributes(stage)
- @jobs.values
- .select { |job| job[:stage] == stage }
- .map { |job| build_attributes(job[:name]) }
- end
-
- def stages_attributes
- @stages.uniq.map do |stage|
- seeds = stage_builds_attributes(stage)
-
- { name: stage, index: @stages.index(stage), builds: seeds }
- end
- end
-
- def workflow_attributes
- {
- rules: @config.dig(:workflow, :rules),
- yaml_variables: transform_to_yaml_variables(@variables)
- }
- end
-
- def self.validation_message(content, opts = {})
- return 'Please provide content of .gitlab-ci.yml' if content.blank?
-
- begin
- Gitlab::Ci::YamlProcessor.new(content, opts)
- nil
- rescue ValidationError => e
- e.message
- end
+ rescue ValidationError => e
+ Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings)
end
private
- def initial_parsing
- ##
- # Global config
- #
- @variables = @ci_config.variables
+ def run_logical_validations!
@stages = @ci_config.stages
-
- ##
- # Jobs
- #
- @jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs
+ @jobs = @ci_config.normalized_jobs
@jobs.each do |name, job|
- # logical validation for job
- validate_job_stage!(name, job)
- validate_job_dependencies!(name, job)
- validate_job_needs!(name, job)
- validate_dynamic_child_pipeline_dependencies!(name, job)
- validate_job_environment!(name, job)
+ validate_job!(name, job)
end
end
- def transform_to_yaml_variables(variables)
- variables.to_h.map do |key, value|
- { key: key.to_s, value: value, public: true }
- end
+ def validate_job!(name, job)
+ validate_job_stage!(name, job)
+ validate_job_dependencies!(name, job)
+ validate_job_needs!(name, job)
+ validate_dynamic_child_pipeline_dependencies!(name, job)
+ validate_job_environment!(name, job)
end
def validate_job_stage!(name, job)
@@ -188,10 +70,6 @@ module Gitlab
end
end
- def error!(message)
- raise ValidationError.new(message, warnings: warnings)
- end
-
def validate_job_dependencies!(name, job)
return unless job[:dependencies]
@@ -267,6 +145,10 @@ module Gitlab
error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined")
end
end
+
+ def error!(message)
+ raise ValidationError.new(message)
+ end
end
end
end
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
new file mode 100644
index 00000000000..68f61e52df7
--- /dev/null
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+# A data object that wraps `Ci::Config` and any messages
+# (errors, warnings) generated by the YamlProcessor.
+module Gitlab
+ module Ci
+ class YamlProcessor
+ class Result
+ attr_reader :errors, :warnings
+
+ def initialize(ci_config: nil, errors: [], warnings: [])
+ @ci_config = ci_config
+ @errors = errors || []
+ @warnings = warnings || []
+ end
+
+ def valid?
+ errors.empty?
+ end
+
+ def stages_attributes
+ stages.uniq.map do |stage|
+ seeds = stage_builds_attributes(stage)
+
+ { name: stage, index: stages.index(stage), builds: seeds }
+ end
+ end
+
+ def builds
+ jobs.map do |name, _|
+ build_attributes(name)
+ end
+ end
+
+ def stage_builds_attributes(stage)
+ jobs.values
+ .select { |job| job[:stage] == stage }
+ .map { |job| build_attributes(job[:name]) }
+ end
+
+ def workflow_attributes
+ {
+ rules: hash_config.dig(:workflow, :rules),
+ yaml_variables: transform_to_yaml_variables(variables)
+ }
+ end
+
+ def jobs
+ @jobs ||= @ci_config.normalized_jobs
+ end
+
+ def stages
+ @stages ||= @ci_config.stages
+ end
+
+ def build_attributes(name)
+ job = jobs.fetch(name.to_sym, {})
+
+ { stage_idx: stages.index(job[:stage]),
+ stage: job[:stage],
+ tag_list: job[:tags],
+ name: job[:name].to_s,
+ allow_failure: job[:ignore],
+ when: job[:when] || 'on_success',
+ environment: job[:environment_name],
+ coverage_regex: job[:coverage],
+ yaml_variables: transform_to_yaml_variables(job[:variables]),
+ needs_attributes: job.dig(:needs, :job),
+ interruptible: job[:interruptible],
+ only: job[:only],
+ except: job[:except],
+ rules: job[:rules],
+ cache: job[:cache],
+ resource_group_key: job[:resource_group],
+ scheduling_type: job[:scheduling_type],
+ secrets: job[:secrets],
+ options: {
+ image: job[:image],
+ services: job[:services],
+ artifacts: job[:artifacts],
+ dependencies: job[:dependencies],
+ cross_dependencies: job.dig(:needs, :cross_dependency),
+ job_timeout: job[:timeout],
+ before_script: job[:before_script],
+ script: job[:script],
+ after_script: job[:after_script],
+ environment: job[:environment],
+ retry: job[:retry],
+ parallel: job[:parallel],
+ instance: job[:instance],
+ start_in: job[:start_in],
+ trigger: job[:trigger],
+ bridge_needs: job.dig(:needs, :bridge)&.first,
+ release: release(job)
+ }.compact }.compact
+ end
+
+ private
+
+ def variables
+ @variables ||= @ci_config.variables
+ end
+
+ def hash_config
+ @hash_config ||= @ci_config.to_hash
+ end
+
+ def release(job)
+ job[:release]
+ end
+
+ def transform_to_yaml_variables(variables)
+ variables.to_h.map do |key, value|
+ { key: key.to_s, value: value, public: true }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cleanup/orphan_job_artifact_files.rb b/lib/gitlab/cleanup/orphan_job_artifact_files.rb
index 017adc7be4a..6d18f9070cc 100644
--- a/lib/gitlab/cleanup/orphan_job_artifact_files.rb
+++ b/lib/gitlab/cleanup/orphan_job_artifact_files.rb
@@ -18,7 +18,7 @@ module Gitlab
@limit = limit
@dry_run = dry_run
@niceness = (niceness || DEFAULT_NICENESS).downcase
- @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
+ @logger = logger || Gitlab::AppLogger
@total_found = @total_cleaned = 0
new_batch!
diff --git a/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb b/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb
index 6ad05c7b2e4..4b1d16eb974 100644
--- a/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb
+++ b/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb
@@ -22,7 +22,7 @@ module Gitlab
attr_reader :batch_size, :dry_run
attr_accessor :artifact_files
- def initialize(batch_size:, dry_run: true, logger: Rails.logger) # rubocop:disable Gitlab/RailsLogger
+ def initialize(batch_size:, dry_run: true, logger: Gitlab::AppLogger)
@batch_size = batch_size
@dry_run = dry_run
@logger = logger
diff --git a/lib/gitlab/cleanup/orphan_lfs_file_references.rb b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
index 3df243e319e..14eac474e27 100644
--- a/lib/gitlab/cleanup/orphan_lfs_file_references.rb
+++ b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
@@ -12,7 +12,7 @@ module Gitlab
def initialize(project, dry_run: true, logger: nil, limit: nil)
@project = project
@dry_run = dry_run
- @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
+ @logger = logger || Gitlab::AppLogger
@limit = limit
end
@@ -25,7 +25,7 @@ module Gitlab
private
def remove_orphan_references
- invalid_references = project.lfs_objects_projects.where(lfs_object: orphan_objects) # rubocop:disable CodeReuse/ActiveRecord
+ invalid_references = project.lfs_objects_projects.lfs_object_in(orphan_objects)
if dry_run
log_info("Found invalid references: #{invalid_references.count}")
@@ -41,26 +41,22 @@ module Gitlab
end
end
- def lfs_oids_from_repository
- project.repository.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid)
- end
-
- def orphan_oids
- lfs_oids_from_database - lfs_oids_from_repository
- end
+ def orphan_objects
+ # Get these first so racing with a git push can't remove any LFS objects
+ oids = project.lfs_objects_oids
- def lfs_oids_from_database
- oids = []
+ repos = [
+ project.repository,
+ project.design_repository,
+ project.wiki.repository
+ ].select(&:exists?)
- project.lfs_objects.each_batch do |relation|
- oids += relation.pluck(:oid) # rubocop:disable CodeReuse/ActiveRecord
+ repos.flat_map do |repo|
+ oids -= repo.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid)
end
- oids
- end
-
- def orphan_objects
- LfsObject.where(oid: orphan_oids) # rubocop:disable CodeReuse/ActiveRecord
+ # The remaining OIDs are not used by any repository, so are orphans
+ LfsObject.for_oids(oids)
end
def log_info(msg)
diff --git a/lib/gitlab/cleanup/project_upload_file_finder.rb b/lib/gitlab/cleanup/project_upload_file_finder.rb
index 3d35d474f5d..0f40f683354 100644
--- a/lib/gitlab/cleanup/project_upload_file_finder.rb
+++ b/lib/gitlab/cleanup/project_upload_file_finder.rb
@@ -49,7 +49,7 @@ module Gitlab
cmd = %W[#{ionice} -c Idle] + cmd if ionice
log_msg = "find command: \"#{cmd.join(' ')}\""
- Rails.logger.info log_msg # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info log_msg
cmd
end
diff --git a/lib/gitlab/cleanup/project_uploads.rb b/lib/gitlab/cleanup/project_uploads.rb
index 056e075cb21..77231665e7e 100644
--- a/lib/gitlab/cleanup/project_uploads.rb
+++ b/lib/gitlab/cleanup/project_uploads.rb
@@ -8,7 +8,7 @@ module Gitlab
attr_reader :logger
def initialize(logger: nil)
- @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
+ @logger = logger || Gitlab::AppLogger
end
def run!(dry_run: true)
diff --git a/lib/gitlab/cleanup/remote_uploads.rb b/lib/gitlab/cleanup/remote_uploads.rb
index 42c93b7aecb..6cadb9424f7 100644
--- a/lib/gitlab/cleanup/remote_uploads.rb
+++ b/lib/gitlab/cleanup/remote_uploads.rb
@@ -7,7 +7,7 @@ module Gitlab
BATCH_SIZE = 100
def initialize(logger: nil)
- @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
+ @logger = logger || Gitlab::AppLogger
end
def run!(dry_run: false)
diff --git a/lib/gitlab/consul/internal.rb b/lib/gitlab/consul/internal.rb
new file mode 100644
index 00000000000..3afc24ddab9
--- /dev/null
+++ b/lib/gitlab/consul/internal.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Consul
+ class Internal
+ Error = Class.new(StandardError)
+ UnexpectedResponseError = Class.new(Gitlab::Consul::Internal::Error)
+ SocketError = Class.new(Gitlab::Consul::Internal::Error)
+ SSLError = Class.new(Gitlab::Consul::Internal::Error)
+ ECONNREFUSED = Class.new(Gitlab::Consul::Internal::Error)
+
+ class << self
+ def api_url
+ Gitlab.config.consul.api_url.to_s.presence if Gitlab.config.consul
+ rescue Settingslogic::MissingSetting
+ Gitlab::AppLogger.error('Consul api_url is not present in config/gitlab.yml')
+
+ nil
+ end
+
+ def discover_service(service_name:)
+ return unless service_name.present? && api_url
+
+ api_path = URI.join(api_url, '/v1/catalog/service/', URI.encode_www_form_component(service_name)).to_s
+ services = json_get(api_path, allow_local_requests: true, open_timeout: 5, read_timeout: 10)
+
+ # Use the first service definition
+ service = services&.first
+
+ return unless service
+
+ service_address = service['ServiceAddress'] || service['Address']
+ service_port = service['ServicePort']
+
+ [service_address, service_port]
+ end
+
+ def discover_prometheus_server_address
+ service_address, service_port = discover_service(service_name: 'prometheus')
+
+ return unless service_address && service_port
+
+ "#{service_address}:#{service_port}"
+ end
+
+ private
+
+ def json_get(path, options)
+ response = get(path, options)
+ code = response.try(:code)
+ body = response.try(:body)
+
+ raise Consul::Internal::UnexpectedResponseError unless code == 200 && body
+
+ parse_response_body(body)
+ end
+
+ def parse_response_body(body)
+ Gitlab::Json.parse(body)
+ rescue
+ raise Consul::Internal::UnexpectedResponseError
+ end
+
+ def get(path, options)
+ Gitlab::HTTP.get(path, options)
+ rescue ::SocketError
+ raise Consul::Internal::SocketError
+ rescue OpenSSL::SSL::SSLError
+ raise Consul::Internal::SSLError
+ rescue Errno::ECONNREFUSED
+ raise Consul::Internal::ECONNREFUSED
+ rescue
+ raise Consul::Internal::UnexpectedResponseError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cross_project_access.rb b/lib/gitlab/cross_project_access.rb
index 4ddc7e02d1b..93baf1e596c 100644
--- a/lib/gitlab/cross_project_access.rb
+++ b/lib/gitlab/cross_project_access.rb
@@ -18,7 +18,7 @@ module Gitlab
end
def add_check(
- klass,
+ klass,
actions: {},
positive_condition: nil,
negative_condition: nil,
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
deleted file mode 100644
index d5f2e868606..00000000000
--- a/lib/gitlab/cycle_analytics/production_stage.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module CycleAnalytics
- class ProductionStage < BaseStage
- include ProductionHelper
-
- def start_time_attrs
- @start_time_attrs ||= issue_table[:created_at]
- end
-
- def end_time_attrs
- @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
- end
-
- def name
- :production
- end
-
- def title
- s_('CycleAnalyticsStage|Total')
- end
-
- def legend
- _("Related Issues")
- end
-
- def description
- _("From issue creation until deploy to production")
- end
-
- def query
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
- end
- end
- end
-end
diff --git a/lib/gitlab/danger/changelog.rb b/lib/gitlab/danger/changelog.rb
index 4427c331b8e..607ca1200a0 100644
--- a/lib/gitlab/danger/changelog.rb
+++ b/lib/gitlab/danger/changelog.rb
@@ -11,8 +11,36 @@ module Gitlab
'meta'
].freeze
NO_CHANGELOG_CATEGORIES = %i[docs none].freeze
+ CREATE_CHANGELOG_COMMAND = 'bin/changelog -m %<mr_iid>s "%<mr_title>s"'
+ CREATE_EE_CHANGELOG_COMMAND = 'bin/changelog --ee -m %<mr_iid>s "%<mr_title>s"'
+ CHANGELOG_MODIFIED_URL_TEXT = "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n"
+ CHANGELOG_MISSING_URL_TEXT = "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**:\n\n"
- def needed?
+ OPTIONAL_CHANGELOG_MESSAGE = <<~MSG
+ If you want to create a changelog entry for GitLab FOSS, run the following:
+
+ #{CREATE_CHANGELOG_COMMAND}
+
+ If you want to create a changelog entry for GitLab EE, run the following instead:
+
+ #{CREATE_EE_CHANGELOG_COMMAND}
+
+ If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.
+ MSG
+
+ REQUIRED_CHANGELOG_MESSAGE = <<~MSG
+ To create a changelog entry, run the following:
+
+ #{CREATE_CHANGELOG_COMMAND}
+
+ This merge request requires a changelog entry because it [introduces a database migration](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry).
+ MSG
+
+ def required?
+ git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} }
+ end
+
+ def optional?
categories_need_changelog? && without_no_changelog_label?
end
@@ -20,16 +48,35 @@ module Gitlab
@found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
end
- def sanitized_mr_title
- gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`')
- end
-
def ee_changelog?
found.start_with?('ee/')
end
+ def modified_text
+ CHANGELOG_MODIFIED_URL_TEXT +
+ format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title)
+ end
+
+ def required_text
+ CHANGELOG_MISSING_URL_TEXT +
+ format(REQUIRED_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title)
+ end
+
+ def optional_text
+ CHANGELOG_MISSING_URL_TEXT +
+ format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title)
+ end
+
private
+ def mr_iid
+ gitlab.mr_json["iid"]
+ end
+
+ def sanitized_mr_title
+ helper.sanitize_mr_title(gitlab.mr_json["title"])
+ end
+
def categories_need_changelog?
(helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any?
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 077c71f1233..3626ec5bf5b 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -44,7 +44,10 @@ module Gitlab
# "+ # Test change",
# "- # Old change" ]
def changed_lines(changed_file)
- git.diff_for_file(changed_file).patch.split("\n").select { |line| %r{^[+-]}.match?(line) }
+ diff = git.diff_for_file(changed_file)
+ return [] unless diff
+
+ diff.patch.split("\n").select { |line| %r{^[+-]}.match?(line) }
end
def all_ee_changes
@@ -171,6 +174,7 @@ module Gitlab
%r{\A(ee/)?scripts/} => :engineering_productivity,
%r{\Atooling/} => :engineering_productivity,
%r{(CODEOWNERS)} => :engineering_productivity,
+ %r{(tests.yml)} => :engineering_productivity,
%r{\A(ee/)?spec/features/} => :test,
%r{\A(ee/)?spec/support/shared_examples/features/} => :test,
@@ -191,6 +195,7 @@ module Gitlab
# Files that don't fit into any category are marked with :none
%r{\A(ee/)?changelogs/} => :none,
%r{\Alocale/gitlab\.pot\z} => :none,
+ %r{\Adata/whats_new/} => :none,
# Fallbacks in case the above patterns miss anything
%r{\.rb\z} => :backend,
@@ -205,16 +210,6 @@ module Gitlab
usernames.map { |u| Gitlab::Danger::Teammate.new('username' => u) }
end
- def missing_database_labels(current_mr_labels)
- labels = if has_database_scoped_labels?(current_mr_labels)
- ['database']
- else
- ['database', 'database::review pending']
- end
-
- labels - current_mr_labels
- end
-
def sanitize_mr_title(title)
title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`')
end
@@ -258,8 +253,6 @@ module Gitlab
all_changed_files.grep(regex)
end
- private
-
def has_database_scoped_labels?(current_mr_labels)
current_mr_labels.any? { |label| label.start_with?('database::') }
end
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
index 2e6181d1cab..a6866868e6c 100644
--- a/lib/gitlab/danger/roulette.rb
+++ b/lib/gitlab/danger/roulette.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require_relative 'teammate'
-require_relative 'request_helper'
+require_relative 'request_helper' unless defined?(Gitlab::Danger::RequestHelper)
module Gitlab
module Danger
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index 9b389907090..ebd96be40d7 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -45,9 +45,7 @@ module Gitlab
has_capability?(project, category, :maintainer, labels)
end
- def markdown_name(timezone_experiment: false, author: nil)
- return @markdown_name unless timezone_experiment
-
+ def markdown_name(author: nil)
"#{@markdown_name} (#{utc_offset_text(author)})"
end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index af363705bed..f941c57a6dd 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -86,7 +86,7 @@ module Gitlab
#
# rubocop:disable Metrics/ParameterLists
def build(
- project:, user:, ref:, oldrev: nil, newrev: nil,
+ project:, user:, ref:, oldrev: nil, newrev: nil,
commits: [], commits_count: nil, message: nil, push_options: {},
with_changed_files: true)
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 990c940d200..accc6330253 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -335,7 +335,7 @@ module Gitlab
end
rescue Prometheus::Client::LabelSetValidator::LabelSetError => err
# Ensure that errors in recording these metrics don't affect the operation of the application
- Rails.logger.error("Unable to observe database transaction duration: #{err}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("Unable to observe database transaction duration: #{err}")
end
# MonkeyPatch for ActiveRecord::Base for adding observability
diff --git a/lib/gitlab/database/background_migration_job.rb b/lib/gitlab/database/background_migration_job.rb
index 445735b232a..1b9d7cbc9a1 100644
--- a/lib/gitlab/database/background_migration_job.rb
+++ b/lib/gitlab/database/background_migration_job.rb
@@ -3,6 +3,8 @@
module Gitlab
module Database
class BackgroundMigrationJob < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
+ include EachBatch
+
self.table_name = :background_migration_jobs
scope :for_migration_class, -> (class_name) { where(class_name: normalize_class_name(class_name)) }
diff --git a/lib/gitlab/database/concurrent_reindex.rb b/lib/gitlab/database/concurrent_reindex.rb
new file mode 100644
index 00000000000..485ab35e55d
--- /dev/null
+++ b/lib/gitlab/database/concurrent_reindex.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class ConcurrentReindex
+ include Gitlab::Utils::StrongMemoize
+ include MigrationHelpers
+
+ ReindexError = Class.new(StandardError)
+
+ PG_IDENTIFIER_LENGTH = 63
+ TEMPORARY_INDEX_PREFIX = 'tmp_reindex_'
+ REPLACED_INDEX_PREFIX = 'old_reindex_'
+
+ attr_reader :index_name, :logger
+
+ def initialize(index_name, logger:)
+ @index_name = index_name
+ @logger = logger
+ end
+
+ def execute
+ raise ReindexError, "index #{index_name} does not exist" unless index_exists?
+
+ raise ReindexError, 'UNIQUE indexes are currently not supported' if index_unique?
+
+ logger.debug("dropping dangling index from previous run: #{replacement_index_name}")
+ remove_replacement_index
+
+ begin
+ create_replacement_index
+
+ unless replacement_index_valid?
+ message = 'replacement index was created as INVALID'
+ logger.error("#{message}, cleaning up")
+ raise ReindexError, "failed to reindex #{index_name}: #{message}"
+ end
+
+ swap_replacement_index
+ rescue Gitlab::Database::WithLockRetries::AttemptsExhaustedError => e
+ logger.error('failed to obtain the required database locks to swap the indexes, cleaning up')
+ raise ReindexError, e.message
+ rescue ActiveRecord::ActiveRecordError, PG::Error => e
+ logger.error("database error while attempting reindex of #{index_name}: #{e.message}")
+ raise ReindexError, e.message
+ ensure
+ logger.info("dropping unneeded replacement index: #{replacement_index_name}")
+ remove_replacement_index
+ end
+ end
+
+ private
+
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+
+ def replacement_index_name
+ @replacement_index_name ||= constrained_index_name(TEMPORARY_INDEX_PREFIX)
+ end
+
+ def index
+ strong_memoize(:index) do
+ find_index(index_name)
+ end
+ end
+
+ def index_exists?
+ !index.nil?
+ end
+
+ def index_unique?
+ index.indisunique
+ end
+
+ def constrained_index_name(prefix)
+ "#{prefix}#{index_name}".slice(0, PG_IDENTIFIER_LENGTH)
+ end
+
+ def create_replacement_index
+ create_replacement_index_statement = index.indexdef
+ .sub(/CREATE INDEX/, 'CREATE INDEX CONCURRENTLY')
+ .sub(/#{index_name}/, replacement_index_name)
+
+ logger.info("creating replacement index #{replacement_index_name}")
+ logger.debug("replacement index definition: #{create_replacement_index_statement}")
+
+ disable_statement_timeout do
+ connection.execute(create_replacement_index_statement)
+ end
+ end
+
+ def replacement_index_valid?
+ find_index(replacement_index_name).indisvalid
+ end
+
+ def find_index(index_name)
+ record = connection.select_one(<<~SQL)
+ SELECT
+ pg_index.indisunique,
+ pg_index.indisvalid,
+ pg_indexes.indexdef
+ FROM pg_index
+ INNER JOIN pg_class ON pg_class.oid = pg_index.indexrelid
+ INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
+ INNER JOIN pg_indexes ON pg_class.relname = pg_indexes.indexname
+ WHERE pg_namespace.nspname = 'public'
+ AND pg_class.relname = #{connection.quote(index_name)}
+ SQL
+
+ OpenStruct.new(record) if record
+ end
+
+ def swap_replacement_index
+ replaced_index_name = constrained_index_name(REPLACED_INDEX_PREFIX)
+
+ logger.info("swapping replacement index #{replacement_index_name} with #{index_name}")
+
+ with_lock_retries do
+ rename_index(index_name, replaced_index_name)
+ rename_index(replacement_index_name, index_name)
+ rename_index(replaced_index_name, replacement_index_name)
+ end
+ end
+
+ def rename_index(old_index_name, new_index_name)
+ connection.execute("ALTER INDEX #{old_index_name} RENAME TO #{new_index_name}")
+ end
+
+ def remove_replacement_index
+ disable_statement_timeout do
+ connection.execute("DROP INDEX CONCURRENTLY IF EXISTS #{replacement_index_name}")
+ end
+ end
+
+ def with_lock_retries(&block)
+ arguments = { klass: self.class, logger: logger }
+
+ Gitlab::Database::WithLockRetries.new(arguments).run(raise_on_exhaustion: true, &block)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/custom_structure.rb b/lib/gitlab/database/custom_structure.rb
index c5a76c5a787..e4404e73a63 100644
--- a/lib/gitlab/database/custom_structure.rb
+++ b/lib/gitlab/database/custom_structure.rb
@@ -8,8 +8,7 @@ module Gitlab
def dump
File.open(self.class.custom_dump_filepath, 'wb') do |io|
io << "-- this file tracks custom GitLab data, such as foreign keys referencing partitioned tables\n"
- io << "-- more details can be found in the issue: https://gitlab.com/gitlab-org/gitlab/-/issues/201872\n"
- io << "SET search_path=public;\n\n"
+ io << "-- more details can be found in the issue: https://gitlab.com/gitlab-org/gitlab/-/issues/201872\n\n"
dump_partitioned_foreign_keys(io) if partitioned_foreign_keys_exist?
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index b62b6e20dd5..723f0f6a308 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -87,7 +87,7 @@ module Gitlab
options = options.merge({ algorithm: :concurrently })
if index_exists?(table_name, column_name, options)
- Rails.logger.warn "Index not created because it already exists (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn "Index not created because it already exists (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}"
return
end
@@ -113,7 +113,7 @@ module Gitlab
options = options.merge({ algorithm: :concurrently })
unless index_exists?(table_name, column_name, options)
- Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}"
return
end
@@ -143,7 +143,7 @@ module Gitlab
options = options.merge({ algorithm: :concurrently })
unless index_exists_by_name?(table_name, index_name)
- Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}"
return
end
@@ -163,7 +163,6 @@ module Gitlab
# defaults to "CASCADE".
# name - The name of the foreign key.
#
- # rubocop:disable Gitlab/RailsLogger
def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, name: nil, validate: true)
# Transactions would result in ALTER TABLE locks being held for the
# duration of the transaction, defeating the purpose of this method.
@@ -183,7 +182,7 @@ module Gitlab
"source: #{source}, target: #{target}, column: #{options[:column]}, "\
"name: #{options[:name]}, on_delete: #{options[:on_delete]}"
- Rails.logger.warn warning_message
+ Gitlab::AppLogger.warn warning_message
else
# Using NOT VALID allows us to create a key without immediately
# validating it. This means we keep the ALTER TABLE lock only for a
@@ -217,7 +216,6 @@ module Gitlab
end
end
end
- # rubocop:enable Gitlab/RailsLogger
def validate_foreign_key(source, column, name: nil)
fk_name = name || concurrent_foreign_key_name(source, column)
@@ -540,10 +538,10 @@ module Gitlab
# table - The table containing the column.
# column - The name of the column to change.
# new_type - The new column type.
- def change_column_type_concurrently(table, column, new_type, type_cast_function: nil)
+ def change_column_type_concurrently(table, column, new_type, type_cast_function: nil, batch_column_name: :id)
temp_column = "#{column}_for_type_change"
- rename_column_concurrently(table, column, temp_column, type: new_type, type_cast_function: type_cast_function)
+ rename_column_concurrently(table, column, temp_column, type: new_type, type_cast_function: type_cast_function, batch_column_name: batch_column_name)
end
# Performs cleanup of a concurrent type change.
@@ -1085,7 +1083,6 @@ into similar problems in the future (e.g. when new tables are created).
# Should be unique per table (not per column)
# validate - Whether to validate the constraint in this call
#
- # rubocop:disable Gitlab/RailsLogger
def add_check_constraint(table, check, constraint_name, validate: true)
validate_check_constraint_name!(constraint_name)
@@ -1102,7 +1099,7 @@ into similar problems in the future (e.g. when new tables are created).
table: #{table}, check: #{check}, constraint name: #{constraint_name}
MESSAGE
- Rails.logger.warn warning_message
+ Gitlab::AppLogger.warn warning_message
else
# Only add the constraint without validating it
# Even though it is fast, ADD CONSTRAINT requires an EXCLUSIVE lock
@@ -1187,7 +1184,7 @@ into similar problems in the future (e.g. when new tables are created).
column #{table}.#{column} is already defined as `NOT NULL`
MESSAGE
- Rails.logger.warn warning_message
+ Gitlab::AppLogger.warn warning_message
end
end
diff --git a/lib/gitlab/database/partitioning/partition_monitoring.rb b/lib/gitlab/database/partitioning/partition_monitoring.rb
new file mode 100644
index 00000000000..9ec9ae684a5
--- /dev/null
+++ b/lib/gitlab/database/partitioning/partition_monitoring.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Partitioning
+ class PartitionMonitoring
+ attr_reader :models
+
+ def initialize(models = PartitionCreator.models)
+ @models = models
+ end
+
+ def report_metrics
+ models.each do |model|
+ strategy = model.partitioning_strategy
+
+ gauge_present.set({ table: model.table_name }, strategy.current_partitions.size)
+ gauge_missing.set({ table: model.table_name }, strategy.missing_partitions.size)
+ end
+ end
+
+ private
+
+ def gauge_present
+ @gauge_present ||= Gitlab::Metrics.gauge(:db_partitions_present, 'Number of database partitions present')
+ end
+
+ def gauge_missing
+ @gauge_missing ||= Gitlab::Metrics.gauge(:db_partitions_missing, 'Number of database partitions currently expected, but not present')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
index 1fb9476b7d9..2def3a4d3a9 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
@@ -31,7 +31,7 @@ module Gitlab
current_keys << specified_key
else
- Rails.logger.warn "foreign key not added because it already exists: #{specified_key}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn "foreign key not added because it already exists: #{specified_key}"
current_keys
end
end
@@ -56,7 +56,7 @@ module Gitlab
existing_key.delete
current_keys.delete(existing_key)
else
- Rails.logger.warn "foreign key not removed because it doesn't exist: #{specified_key}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn "foreign key not removed because it doesn't exist: #{specified_key}"
end
current_keys
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index 84b6fb9f76e..f7b0306b769 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -6,6 +6,7 @@ module Gitlab
module TableManagementHelpers
include ::Gitlab::Database::SchemaHelpers
include ::Gitlab::Database::DynamicModelHelpers
+ include ::Gitlab::Database::MigrationHelpers
include ::Gitlab::Database::Migrations::BackgroundMigrationHelpers
ALLOWED_TABLES = %w[audit_events].freeze
@@ -15,6 +16,12 @@ module Gitlab
BATCH_INTERVAL = 2.minutes.freeze
BATCH_SIZE = 50_000
+ JobArguments = Struct.new(:start_id, :stop_id, :source_table_name, :partitioned_table_name, :source_column) do
+ def self.from_array(arguments)
+ self.new(*arguments)
+ end
+ end
+
# Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a timestamp column.
# One partition is created per month between the given `min_date` and `max_date`. Also installs a trigger on
# the original table to copy writes into the partitioned table. To copy over historic data from before creation
@@ -134,6 +141,42 @@ module Gitlab
end
end
+ # Executes cleanup tasks from a previous BackgroundMigration to backfill a partitioned table by finishing
+ # pending jobs and performing a final data synchronization.
+ # This performs two steps:
+ # 1. Wait to finish any pending BackgroundMigration jobs that have not succeeded
+ # 2. Inline copy any missed rows from the original table to the partitioned table
+ #
+ # **NOTE** Migrations using this method cannot be scheduled in the same release as the migration that
+ # schedules the background migration using the `enqueue_background_migration` helper, or else the
+ # background migration jobs will be force-executed.
+ #
+ # Example:
+ #
+ # finalize_backfilling_partitioned_table :audit_events
+ #
+ def finalize_backfilling_partitioned_table(table_name)
+ assert_table_is_allowed(table_name)
+ assert_not_in_transaction_block(scope: ERROR_SCOPE)
+
+ partitioned_table_name = make_partitioned_table_name(table_name)
+ unless table_exists?(partitioned_table_name)
+ raise "could not find partitioned table for #{table_name}, " \
+ "this could indicate the previous partitioning migration has been rolled back."
+ end
+
+ Gitlab::BackgroundMigration.steal(MIGRATION_CLASS_NAME) do |raw_arguments|
+ JobArguments.from_array(raw_arguments).source_table_name == table_name.to_s
+ end
+
+ primary_key = connection.primary_key(table_name)
+ copy_missed_records(table_name, partitioned_table_name, primary_key)
+
+ disable_statement_timeout do
+ execute("VACUUM FREEZE ANALYZE #{partitioned_table_name}")
+ end
+ end
+
private
def assert_table_is_allowed(table_name)
@@ -161,10 +204,8 @@ module Gitlab
def create_range_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_key)
if table_exists?(partitioned_table_name)
- # rubocop:disable Gitlab/RailsLogger
- Rails.logger.warn "Partitioned table not created because it already exists" \
+ Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \
" (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} "
- # rubocop:enable Gitlab/RailsLogger
return
end
@@ -217,10 +258,8 @@ module Gitlab
def create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound)
if table_exists?(table_for_range_partition(partition_name))
- # rubocop:disable Gitlab/RailsLogger
- Rails.logger.warn "Partition not created because it already exists" \
+ Gitlab::AppLogger.warn "Partition not created because it already exists" \
" (this may be due to an aborted migration or similar): partition_name: #{partition_name}"
- # rubocop:enable Gitlab/RailsLogger
return
end
@@ -241,10 +280,8 @@ module Gitlab
def create_sync_function(name, partitioned_table_name, unique_key)
if function_exists?(name)
- # rubocop:disable Gitlab/RailsLogger
- Rails.logger.warn "Partitioning sync function not created because it already exists" \
+ Gitlab::AppLogger.warn "Partitioning sync function not created because it already exists" \
" (this may be due to an aborted migration or similar): function name: #{name}"
- # rubocop:enable Gitlab/RailsLogger
return
end
@@ -276,17 +313,15 @@ module Gitlab
def create_sync_trigger(table_name, trigger_name, function_name)
if trigger_exists?(table_name, trigger_name)
- # rubocop:disable Gitlab/RailsLogger
- Rails.logger.warn "Partitioning sync trigger not created because it already exists" \
+ Gitlab::AppLogger.warn "Partitioning sync trigger not created because it already exists" \
" (this may be due to an aborted migration or similar): trigger name: #{trigger_name}"
- # rubocop:enable Gitlab/RailsLogger
return
end
create_trigger(table_name, trigger_name, function_name, fires: 'AFTER INSERT OR UPDATE OR DELETE')
end
- def enqueue_background_migration(source_table_name, partitioned_table_name, source_key)
+ def enqueue_background_migration(source_table_name, partitioned_table_name, source_column)
source_model = define_batchable_model(source_table_name)
queue_background_migration_jobs_by_range_at_intervals(
@@ -294,13 +329,35 @@ module Gitlab
MIGRATION_CLASS_NAME,
BATCH_INTERVAL,
batch_size: BATCH_SIZE,
- other_job_arguments: [source_table_name.to_s, partitioned_table_name, source_key],
+ other_job_arguments: [source_table_name.to_s, partitioned_table_name, source_column],
track_jobs: true)
end
def cleanup_migration_jobs(table_name)
::Gitlab::Database::BackgroundMigrationJob.for_partitioning_migration(MIGRATION_CLASS_NAME, table_name).delete_all
end
+
+ def copy_missed_records(source_table_name, partitioned_table_name, source_column)
+ backfill_table = BackfillPartitionedTable.new
+ relation = ::Gitlab::Database::BackgroundMigrationJob.pending
+ .for_partitioning_migration(MIGRATION_CLASS_NAME, source_table_name)
+
+ relation.each_batch do |batch|
+ batch.each do |pending_migration_job|
+ job_arguments = JobArguments.from_array(pending_migration_job.arguments)
+ start_id = job_arguments.start_id
+ stop_id = job_arguments.stop_id
+
+ say("Backfilling data into partitioned table for ids from #{start_id} to #{stop_id}")
+ job_updated_count = backfill_table.perform(start_id, stop_id, source_table_name,
+ partitioned_table_name, source_column)
+
+ unless job_updated_count > 0
+ raise "failed to update tracking record for ids from #{start_id} to #{stop_id}"
+ end
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
index 4fbbfdc4914..562e651cabc 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
@@ -71,7 +71,7 @@ module Gitlab
unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
- Rails.logger.error message # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error message
end
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
index 8b92b296408..5dbf30bad4e 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
@@ -56,7 +56,7 @@ module Gitlab
unless gitlab_shell.mv_repository(project.repository_storage,
old_path,
new_path)
- Rails.logger.error "Error moving #{old_path} to #{new_path}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "Error moving #{old_path} to #{new_path}"
end
end
diff --git a/lib/gitlab/database/schema_cleaner.rb b/lib/gitlab/database/schema_cleaner.rb
index 7c415287878..8f93da2b66c 100644
--- a/lib/gitlab/database/schema_cleaner.rb
+++ b/lib/gitlab/database/schema_cleaner.rb
@@ -18,11 +18,18 @@ module Gitlab
structure.gsub!(/^SELECT pg_catalog\.set_config\('search_path'.+/, '')
structure.gsub!(/^--.*/, "\n")
- structure = "SET search_path=public;\n" + structure
+ # We typically don't assume we're working with the public schema.
+ # pg_dump uses fully qualified object names though, since we have multiple schemas
+ # in the database.
+ #
+ # The intention here is to not introduce an assumption about the standard schema,
+ # unless we have a good reason to do so.
+ structure.gsub!(/public\.(\w+)/, '\1')
+ structure.gsub!(/CREATE EXTENSION IF NOT EXISTS (\w+) WITH SCHEMA public;/, 'CREATE EXTENSION IF NOT EXISTS \1;')
structure.gsub!(/\n{3,}/, "\n\n")
- io << structure
+ io << structure.strip
io << <<~MSG
-- schema_migrations.version information is no longer stored in this file,
-- but instead tracked in the db/schema_migrations directory
diff --git a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
index fb0fcc5a93b..8a5f53be20f 100644
--- a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
+++ b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
@@ -18,6 +18,7 @@ module Gitlab
business: 0,
response: 1,
system: 2,
+ custom: 3,
cluster_health: -100
}
@@ -34,7 +35,8 @@ module Gitlab
aws_elb: _('Response metrics (AWS ELB)'),
nginx: _('Response metrics (NGINX)'),
kubernetes: _('System metrics (Kubernetes)'),
- cluster_health: _('Cluster Health')
+ cluster_health: _('Cluster Health'),
+ custom: _('Custom metrics')
}
end
end
diff --git a/lib/gitlab/database_importers/instance_administrators/create_group.rb b/lib/gitlab/database_importers/instance_administrators/create_group.rb
index 5bf0e5a320d..d9425810405 100644
--- a/lib/gitlab/database_importers/instance_administrators/create_group.rb
+++ b/lib/gitlab/database_importers/instance_administrators/create_group.rb
@@ -6,6 +6,8 @@ module Gitlab
class CreateGroup < ::BaseService
include Stepable
+ NAME = 'GitLab Instance'
+ PATH_PREFIX = 'gitlab-instance'
VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL
steps :validate_application_settings,
@@ -117,12 +119,12 @@ module Gitlab
def create_group_params
{
- name: 'GitLab Instance Administrators',
+ name: NAME,
visibility_level: VISIBILITY_LEVEL,
# The 8 random characters at the end are so that the path does not
# clash with any existing group that the user might have created.
- path: "gitlab-instance-administrators-#{SecureRandom.hex(4)}"
+ path: "#{PATH_PREFIX}-#{SecureRandom.hex(4)}"
}
end
end
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 07a4c3bf5e6..88f035c2d1b 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -9,7 +9,7 @@ module Gitlab
include SelfMonitoring::Helpers
VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL
- PROJECT_NAME = 'GitLab self monitoring'
+ PROJECT_NAME = 'Monitoring'
steps :validate_application_settings,
:create_group,
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 0d027809ba8..a5259079345 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -60,7 +60,7 @@ module Gitlab
# Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content.
if rich_line
- line_prefix = diff_line.text =~ /\A(.)/ ? $1 : ' '
+ line_prefix = diff_line.text =~ /\A(.)/ ? Regexp.last_match(1) : ' '
"#{line_prefix}#{rich_line}".html_safe
end
end
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index 0c3b6b72313..0eb22e6b3cb 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -3,6 +3,7 @@
module Gitlab
module Diff
class HighlightCache
+ include Gitlab::Utils::Gzip
include Gitlab::Utils::StrongMemoize
EXPIRATION = 1.week
@@ -83,7 +84,7 @@ module Gitlab
redis.hset(
key,
diff_file_id,
- compose_data(highlighted_diff_lines_hash.to_json)
+ gzip_compress(highlighted_diff_lines_hash.to_json)
)
end
@@ -145,35 +146,12 @@ module Gitlab
end
results.map! do |result|
- Gitlab::Json.parse(extract_data(result), symbolize_names: true) unless result.nil?
+ Gitlab::Json.parse(gzip_decompress(result), symbolize_names: true) unless result.nil?
end
file_paths.zip(results).to_h
end
- def compose_data(json_data)
- # #compress returns ASCII-8BIT, so we need to force the encoding to
- # UTF-8 before caching it in redis, else we risk encoding mismatch
- # errors.
- #
- ActiveSupport::Gzip.compress(json_data).force_encoding("UTF-8")
- rescue Zlib::GzipFile::Error
- json_data
- end
-
- def extract_data(data)
- # Since we could be dealing with an already populated cache full of data
- # that isn't gzipped, we want to also check to see if the data is
- # gzipped before we attempt to #decompress it, thus we check the first
- # 2 bytes for "\x1F\x8B" to confirm it is a gzipped string. While a
- # non-gzipped string will raise a Zlib::GzipFile::Error, which we're
- # rescuing, we don't want to count on rescue for control flow.
- #
- data[0..1] == "\x1F\x8B" ? ActiveSupport::Gzip.decompress(data) : data
- rescue Zlib::GzipFile::Error
- data
- end
-
def cacheable?(diff_file)
diffable.present? && diff_file.text? && diff_file.diffable?
end
diff --git a/lib/gitlab/discussions_diff/highlight_cache.rb b/lib/gitlab/discussions_diff/highlight_cache.rb
index 4bec6467c1a..3337aeb9262 100644
--- a/lib/gitlab/discussions_diff/highlight_cache.rb
+++ b/lib/gitlab/discussions_diff/highlight_cache.rb
@@ -3,6 +3,8 @@
module Gitlab
module DiscussionsDiff
class HighlightCache
+ extend Gitlab::Utils::Gzip
+
class << self
VERSION = 1
EXPIRATION = 1.week
@@ -17,7 +19,7 @@ module Gitlab
mapping.each do |raw_key, value|
key = cache_key_for(raw_key)
- multi.set(key, value.to_json, ex: EXPIRATION)
+ multi.set(key, gzip_compress(value.to_json), ex: EXPIRATION)
end
end
end
@@ -44,7 +46,7 @@ module Gitlab
content.map! do |lines|
next unless lines
- Gitlab::Json.parse(lines).map! do |line|
+ Gitlab::Json.parse(gzip_decompress(lines)).map! do |line|
Gitlab::Diff::Line.safe_init_from_hash(line)
end
end
diff --git a/lib/gitlab/email/hook/disable_email_interceptor.rb b/lib/gitlab/email/hook/disable_email_interceptor.rb
index 58dc1527c7a..6e2e0201684 100644
--- a/lib/gitlab/email/hook/disable_email_interceptor.rb
+++ b/lib/gitlab/email/hook/disable_email_interceptor.rb
@@ -7,7 +7,7 @@ module Gitlab
def self.delivering_email(message)
message.perform_deliveries = false
- Rails.logger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}"
end
end
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index bf6c28b9f90..f5e47b43a9a 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -54,7 +54,8 @@ module Gitlab
def key_from_additional_headers(mail)
find_key_from_references(mail) ||
find_key_from_delivered_to_header(mail) ||
- find_key_from_envelope_to_header(mail)
+ find_key_from_envelope_to_header(mail) ||
+ find_key_from_x_envelope_to_header(mail)
end
def ensure_references_array(references)
@@ -91,6 +92,13 @@ module Gitlab
end
end
+ def find_key_from_x_envelope_to_header(mail)
+ Array(mail[:x_envelope_to]).find do |header|
+ key = Gitlab::IncomingEmail.key_from_address(header.value)
+ break key if key
+ end
+ end
+
def ignore_auto_reply!(mail)
if auto_submitted?(mail) || auto_replied?(mail)
raise AutoGeneratedEmailError
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 67f8d691a77..7b79de00c66 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -59,7 +59,7 @@ module Gitlab
begin
CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
rescue ArgumentError => e
- Rails.logger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}")
''
end
diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb
index 8d5611411c9..803acef9a40 100644
--- a/lib/gitlab/error_tracking.rb
+++ b/lib/gitlab/error_tracking.rb
@@ -26,6 +26,8 @@ module Gitlab
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
config.processors << ::Gitlab::ErrorTracking::Processor::SidekiqProcessor
+ config.processors << ::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor
+
# Sanitize authentication headers
config.sanitize_http_headers = %w[Authorization Private-Token]
config.tags = extra_tags_from_env.merge(program: Gitlab.process_name)
diff --git a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb
new file mode 100644
index 00000000000..871e9c4b7c8
--- /dev/null
+++ b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ module Processor
+ class GrpcErrorProcessor < ::Raven::Processor
+ DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)')
+
+ def process(value)
+ process_first_exception_value(value)
+ process_custom_fingerprint(value)
+
+ value
+ end
+
+ # Sentry can report multiple exceptions in an event. Sanitize
+ # only the first one since that's what is used for grouping.
+ def process_first_exception_value(value)
+ exceptions = value.dig(:exception, :values)
+
+ return unless exceptions.is_a?(Array)
+
+ entry = exceptions.first
+
+ return unless entry.is_a?(Hash)
+
+ exception_type = entry[:type]
+ raw_message = entry[:value]
+
+ return unless exception_type&.start_with?('GRPC::')
+ return unless raw_message.present?
+
+ message, debug_str = split_debug_error_string(raw_message)
+
+ entry[:value] = message if message
+ extra = value[:extra] || {}
+ extra[:grpc_debug_error_string] = debug_str if debug_str
+ end
+
+ def process_custom_fingerprint(value)
+ fingerprint = value[:fingerprint]
+
+ return value unless custom_grpc_fingerprint?(fingerprint)
+
+ message, _ = split_debug_error_string(fingerprint[1])
+ fingerprint[1] = message if message
+ end
+
+ private
+
+ def custom_grpc_fingerprint?(fingerprint)
+ fingerprint.is_a?(Array) && fingerprint.length == 2 && fingerprint[0].start_with?('GRPC::')
+ end
+
+ def split_debug_error_string(message)
+ return unless message
+
+ match = DEBUG_ERROR_STRING_REGEX.match(message)
+
+ return unless match
+
+ [match[1], match[2]]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 9908369426a..dca60c93fb2 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -42,9 +42,6 @@ module Gitlab
ci_notification_dot: {
tracking_category: 'Growth::Expansion::Experiment::CiNotificationDot'
},
- buy_ci_minutes_version_a: {
- tracking_category: 'Growth::Expansion::Experiment::BuyCiMinutesVersionA'
- },
upgrade_link_in_user_menu_a: {
tracking_category: 'Growth::Expansion::Experiment::UpgradeLinkInUserMenuA'
},
@@ -62,6 +59,9 @@ module Gitlab
},
customize_homepage: {
tracking_category: 'Growth::Expansion::Experiment::CustomizeHomepage'
+ },
+ invite_email: {
+ tracking_category: 'Growth::Acquisition::Experiment::InviteEmail'
}
}.freeze
@@ -78,7 +78,7 @@ module Gitlab
included do
before_action :set_experimentation_subject_id_cookie, unless: :dnt_enabled?
- helper_method :experiment_enabled?
+ helper_method :experiment_enabled?, :experiment_tracking_category_and_group
end
def set_experimentation_subject_id_cookie
@@ -118,6 +118,10 @@ module Gitlab
::Experiment.add_user(experiment_key, tracking_group(experiment_key), current_user)
end
+ def experiment_tracking_category_and_group(experiment_key)
+ "#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group')}"
+ end
+
private
def dnt_enabled?
@@ -144,7 +148,7 @@ module Gitlab
{
category: tracking_category(experiment_key),
action: action,
- property: "#{tracking_group(experiment_key)}_group",
+ property: tracking_group(experiment_key, "_group"),
label: experimentation_subject_id,
value: value
}.compact
@@ -154,10 +158,12 @@ module Gitlab
Experimentation.experiment(experiment_key).tracking_category
end
- def tracking_group(experiment_key)
+ def tracking_group(experiment_key, suffix = nil)
return unless Experimentation.enabled?(experiment_key)
- experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL
+ group = experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL
+
+ suffix ? "#{group}#{suffix}" : group
end
def forced_enabled?(experiment_key)
diff --git a/lib/gitlab/file_type_detection.rb b/lib/gitlab/file_type_detection.rb
index 475d50e37bf..38ccd2c38a9 100644
--- a/lib/gitlab/file_type_detection.rb
+++ b/lib/gitlab/file_type_detection.rb
@@ -20,6 +20,8 @@
module Gitlab
module FileTypeDetection
SAFE_IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
+ SAFE_IMAGE_FOR_SCALING_EXT = %w[png jpg jpeg].freeze
+
PDF_EXT = 'pdf'
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
@@ -46,6 +48,12 @@ module Gitlab
extension_match?(SAFE_IMAGE_EXT)
end
+ # For the time being, we restrict image scaling requests to the most popular and safest formats only,
+ # which are JPGs and PNGs. See https://gitlab.com/gitlab-org/gitlab/-/issues/237848 for more info.
+ def image_safe_for_scaling?
+ extension_match?(SAFE_IMAGE_FOR_SCALING_EXT)
+ end
+
def video?
extension_match?(SAFE_VIDEO_EXT)
end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 2c53f9b026d..bd5d2e53180 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -39,7 +39,7 @@ module Gitlab
def user_map
@user_map ||= begin
- user_map = Hash.new
+ user_map = {}
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
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index b6bffb11344..96f3487fd6f 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -13,7 +13,6 @@ module Gitlab
TAG_REF_PREFIX = "refs/tags/"
BRANCH_REF_PREFIX = "refs/heads/"
- BaseError = Class.new(StandardError)
CommandError = Class.new(BaseError)
CommitError = Class.new(BaseError)
OSError = Class.new(BaseError)
diff --git a/lib/gitlab/git/base_error.rb b/lib/gitlab/git/base_error.rb
new file mode 100644
index 00000000000..a7eaa82b347
--- /dev/null
+++ b/lib/gitlab/git/base_error.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ class BaseError < StandardError
+ DEBUG_ERROR_STRING_REGEX = /(.*?) debug_error_string:.*$/m.freeze
+
+ def initialize(msg = nil)
+ if msg
+ raw_message = msg.to_s
+ match = DEBUG_ERROR_STRING_REGEX.match(raw_message)
+ raw_message = match[1] if match
+
+ super(raw_message)
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 46896961867..e6121d688ba 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -37,7 +37,7 @@ module Gitlab
@byte_count = 0
@overflow = false
@empty = true
- @array = Array.new
+ @array = []
end
def each(&block)
diff --git a/lib/gitlab/git/keep_around.rb b/lib/gitlab/git/keep_around.rb
index d58f10bdbb7..b6fc335c979 100644
--- a/lib/gitlab/git/keep_around.rb
+++ b/lib/gitlab/git/keep_around.rb
@@ -27,7 +27,7 @@ module Gitlab
# This will still fail if the file is corrupted (e.g. 0 bytes)
raw_repository.write_ref(keep_around_ref_name(sha), sha)
rescue Gitlab::Git::CommandError => ex
- Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 596b4e9f692..8ace4157ad7 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -19,15 +19,15 @@ module Gitlab
GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'
- NoRepository = Class.new(StandardError)
- InvalidRepository = Class.new(StandardError)
- InvalidBlobName = Class.new(StandardError)
- InvalidRef = Class.new(StandardError)
- GitError = Class.new(StandardError)
- DeleteBranchError = Class.new(StandardError)
- TagExistsError = Class.new(StandardError)
- ChecksumError = Class.new(StandardError)
- class CreateTreeError < StandardError
+ NoRepository = Class.new(::Gitlab::Git::BaseError)
+ InvalidRepository = Class.new(::Gitlab::Git::BaseError)
+ InvalidBlobName = Class.new(::Gitlab::Git::BaseError)
+ InvalidRef = Class.new(::Gitlab::Git::BaseError)
+ GitError = Class.new(::Gitlab::Git::BaseError)
+ DeleteBranchError = Class.new(::Gitlab::Git::BaseError)
+ TagExistsError = Class.new(::Gitlab::Git::BaseError)
+ ChecksumError = Class.new(::Gitlab::Git::BaseError)
+ class CreateTreeError < ::Gitlab::Git::BaseError
attr_reader :error_code
def initialize(error_code)
@@ -955,7 +955,7 @@ module Gitlab
gitaly_repository_client.cleanup if exists?
end
rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup
- Rails.logger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}")
Gitlab::Metrics.counter(
:failed_repository_cleanup_total,
'Number of failed repository cleanup events'
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 76771f0417b..da2d015ca4a 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -6,7 +6,6 @@ module Gitlab
include Gitlab::Git::WrapsGitalyErrors
DuplicatePageError = Class.new(StandardError)
- OperationError = Class.new(StandardError)
DEFAULT_PAGINATION = Kaminari.config.default_per_page
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index f2b4e930707..ae83e45f2b3 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -119,7 +119,7 @@ module Gitlab
override :check_single_change_access
def check_single_change_access(change, _skip_lfs_integrity_check: false)
- Checks::SnippetCheck.new(change, logger: logger).validate!
+ Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate!
Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate!
rescue Checks::TimedLogger::TimeoutError
raise TimeoutError, logger.full_message
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 131c00db612..e1324530412 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -450,7 +450,7 @@ module Gitlab
stack_string = Gitlab::BacktraceCleaner.clean_backtrace(caller).drop(1).join("\n")
- Gitlab::SafeRequestStore[:stack_counter] ||= Hash.new
+ Gitlab::SafeRequestStore[:stack_counter] ||= {}
count = Gitlab::SafeRequestStore[:stack_counter][stack_string] || 0
Gitlab::SafeRequestStore[:stack_counter][stack_string] = count + 1
diff --git a/lib/gitlab/gitpod.rb b/lib/gitlab/gitpod.rb
new file mode 100644
index 00000000000..11b54db72ea
--- /dev/null
+++ b/lib/gitlab/gitpod.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class Gitpod
+ class << self
+ def feature_conditional?
+ feature.conditional?
+ end
+
+ def feature_available?
+ # The gitpod_bundle feature could be conditionally applied, so check if `!off?`
+ !feature.off?
+ end
+
+ def feature_enabled?(actor = nil)
+ feature.enabled?(actor)
+ end
+
+ def feature_and_settings_enabled?(actor = nil)
+ feature_enabled?(actor) && Gitlab::CurrentSettings.gitpod_enabled
+ end
+
+ private
+
+ def feature
+ Feature.get(:gitpod) # rubocop:disable Gitlab/AvoidFeatureGet
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index 7346de13626..54dca93a891 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -7,19 +7,19 @@ module Gitlab
PROJECT = RepoType.new(
name: :project,
access_checker_class: Gitlab::GitAccessProject,
- repository_resolver: -> (project) { project&.repository }
+ repository_resolver: -> (project) { ::Repository.new(project.full_path, project, shard: project.repository_storage, disk_path: project.disk_path) }
).freeze
WIKI = RepoType.new(
name: :wiki,
access_checker_class: Gitlab::GitAccessWiki,
- repository_resolver: -> (container) { container&.wiki&.repository },
+ repository_resolver: -> (container) { ::Repository.new(container.wiki.full_path, container, shard: container.wiki.repository_storage, disk_path: container.wiki.disk_path, repo_type: WIKI) },
project_resolver: -> (container) { container.is_a?(Project) ? container : nil },
suffix: :wiki
).freeze
SNIPPET = RepoType.new(
name: :snippet,
access_checker_class: Gitlab::GitAccessSnippet,
- repository_resolver: -> (snippet) { snippet&.repository },
+ repository_resolver: -> (snippet) { ::Repository.new(snippet.full_path, snippet, shard: snippet.repository_storage, disk_path: snippet.disk_path, repo_type: SNIPPET) },
container_class: Snippet,
project_resolver: -> (snippet) { snippet&.project },
guest_read_ability: :read_snippet
diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb
index 2c0038b61e2..346f6be0d98 100644
--- a/lib/gitlab/gl_repository/repo_type.rb
+++ b/lib/gitlab/gl_repository/repo_type.rb
@@ -57,6 +57,8 @@ module Gitlab
end
def repository_for(container)
+ return unless container
+
repository_resolver.call(container)
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index dfba68ce899..66517ecd743 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -4,6 +4,7 @@
module Gitlab
module GonHelper
+ include StartupCssHelper
include WebpackHelper
def add_gon_variables
@@ -45,9 +46,12 @@ module Gitlab
push_frontend_feature_flag(:snippets_vue, default_enabled: true)
push_frontend_feature_flag(:monaco_blobs, default_enabled: true)
push_frontend_feature_flag(:monaco_ci, default_enabled: false)
- push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
+ push_frontend_feature_flag(:snippets_edit_vue, default_enabled: true)
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
+
+ # Startup CSS feature is a special one as it can be enabled by means of cookies and params
+ gon.push({ features: { 'startupCss' => use_startup_css? } }, true)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb
index 0dd28b32511..dcd0e12cbfc 100644
--- a/lib/gitlab/graphql/docs/helper.rb
+++ b/lib/gitlab/graphql/docs/helper.rb
@@ -21,30 +21,47 @@ module Gitlab
MD
end
- def sorted_fields(fields)
- fields.sort_by { |field| field[:name] }
+ def render_name_and_description(object)
+ content = "### #{object[:name]}\n"
+
+ if object[:description].present?
+ content += "\n#{object[:description]}.\n"
+ end
+
+ content
+ end
+
+ def sorted_by_name(objects)
+ objects.sort_by { |o| o[:name] }
end
def render_field(field)
'| %s | %s | %s |' % [
- render_field_name(field),
+ render_name(field),
render_field_type(field[:type][:info]),
- render_field_description(field)
+ render_description(field)
]
end
- def render_field_name(field)
- rendered_name = "`#{field[:name]}`"
- rendered_name += ' **{warning-solid}**' if field[:is_deprecated]
+ def render_enum_value(value)
+ '| %s | %s |' % [
+ render_name(value),
+ render_description(value)
+ ]
+ end
+
+ def render_name(object)
+ rendered_name = "`#{object[:name]}`"
+ rendered_name += ' **{warning-solid}**' if object[:is_deprecated]
rendered_name
end
- # Returns the field description. If the field has been deprecated,
+ # Returns the object description. If the object has been deprecated,
# the deprecation reason will be returned in place of the description.
- def render_field_description(field)
- return field[:description] unless field[:is_deprecated]
+ def render_description(object)
+ return object[:description] unless object[:is_deprecated]
- "**Deprecated:** #{field[:deprecation_reason]}"
+ "**Deprecated:** #{object[:deprecation_reason]}"
end
# Some fields types are arrays of other types and are displayed
@@ -70,6 +87,13 @@ module Gitlab
!object_type[:name]["__"]
end
end
+
+ # We ignore the built-in enum types.
+ def enums
+ graphql_enum_types.select do |enum_type|
+ !enum_type[:name].in?(%w(__DirectiveLocation __TypeKind))
+ end
+ end
end
end
end
diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml
index 8c033526557..ec052943589 100644
--- a/lib/gitlab/graphql/docs/templates/default.md.haml
+++ b/lib/gitlab/graphql/docs/templates/default.md.haml
@@ -15,15 +15,45 @@
CAUTION: **Caution:**
Fields that are deprecated are marked with **{warning-solid}**.
\
+
+:plain
+ ## Object types
+
+ Object types represent the resources that GitLab's 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.
+
+ For more information, see
+ [Object Types and Fields](https://graphql.org/learn/schema/#object-types-and-fields)
+ on `graphql.org`.
+\
+
- objects.each do |type|
- unless type[:fields].empty?
- = "## #{type[:name]}"
- - if type[:description]&.present?
- \
- = type[:description]
- \
- ~ "| Name | Type | Description |"
- ~ "| --- | ---- | ---------- |"
- - sorted_fields(type[:fields]).each do |field|
+ = render_name_and_description(type)
+ ~ "| Field | Type | Description |"
+ ~ "| ----- | ---- | ----------- |"
+ - sorted_by_name(type[:fields]).each do |field|
= render_field(field)
\
+
+:plain
+ ## Enumeration types
+
+ Also called _Enums_, enumeration types are a special kind of scalar that
+ is restricted to a particular set of allowed values.
+
+ For more information, see
+ [Enumeration Types](https://graphql.org/learn/schema/#enumeration-types)
+ on `graphql.org`.
+\
+
+- enums.each do |enum|
+ - unless enum[:values].empty?
+ = render_name_and_description(enum)
+ ~ "| Value | Description |"
+ ~ "| ----- | ----------- |"
+ - sorted_by_name(enum[:values]).each do |value|
+ = render_enum_value(value)
+ \
diff --git a/lib/gitlab/graphql/loaders/issuable_loader.rb b/lib/gitlab/graphql/loaders/issuable_loader.rb
index 1cc0fbe215f..8ac4be2b661 100644
--- a/lib/gitlab/graphql/loaders/issuable_loader.rb
+++ b/lib/gitlab/graphql/loaders/issuable_loader.rb
@@ -15,6 +15,7 @@ module Gitlab
def batching_find_all(&with_query)
if issuable_finder.params.keys == ['iids']
+ issuable_finder.parent = parent
batch_load_issuables(issuable_finder.params[:iids], with_query)
else
post_process(find_all, with_query)
@@ -22,24 +23,12 @@ module Gitlab
end
def find_all
- issuable_finder.params[parent_param] = parent if parent
-
+ issuable_finder.parent_param = parent if parent
issuable_finder.execute
end
private
- def parent_param
- case parent
- when Project
- :project_id
- when Group
- :group_id
- else
- raise "Unexpected parent: #{parent.class}"
- end
- end
-
def post_process(query, with_query)
if with_query
with_query.call(query)
@@ -56,7 +45,7 @@ module Gitlab
return if parent.nil?
BatchLoader::GraphQL
- .for([parent_param, iid.to_s])
+ .for([issuable_finder.parent_param, iid.to_s])
.batch(key: batch_key) do |params, loader, args|
batch_key = args[:key]
user = batch_key.current_user
diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb
index afea7c602be..bd785880b57 100644
--- a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb
+++ b/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb
@@ -29,7 +29,10 @@ module Gitlab
def table_condition(order_info, value, operator)
if order_info.named_function
target = order_info.named_function
- value = value&.downcase if target&.name&.downcase == 'lower'
+
+ if target.try(:name)&.casecmp('lower') == 0
+ value = value&.downcase
+ end
else
target = arel_table[order_info.attribute_name]
end
diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb
index 12bcc4993b5..f54695ddb9a 100644
--- a/lib/gitlab/graphql/pagination/keyset/order_info.rb
+++ b/lib/gitlab/graphql/pagination/keyset/order_info.rb
@@ -71,25 +71,43 @@ module Gitlab
def extract_nulls_last_order(order_value)
tokens = order_value.downcase.split
- [tokens.first, (tokens[1] == 'asc' ? :asc : :desc), nil]
+ column_reference = tokens.first
+ sort_direction = tokens[1] == 'asc' ? :asc : :desc
+
+ # Handles the case when the order value is coming from another table.
+ # Example: table_name.column_name
+ # Query the value using the fully qualified column name: pass table_name.column_name as the named_function
+ if fully_qualified_column_reference?(column_reference)
+ [column_reference, sort_direction, Arel.sql(column_reference)]
+ else
+ [column_reference, sort_direction, nil]
+ end
+ end
+
+ # Example: table_name.column_name
+ def fully_qualified_column_reference?(attribute)
+ attribute.to_s.count('.') == 1
end
def extract_attribute_values(order_value)
- named = nil
- name = if ordering_by_lower?(order_value)
- named = order_value.expr
- named.expressions[0].name.to_s
- else
- order_value.expr.name
- end
-
- [name, order_value.direction, named]
+ if ordering_by_lower?(order_value)
+ [order_value.expr.expressions[0].name.to_s, order_value.direction, order_value.expr]
+ elsif ordering_by_similarity?(order_value)
+ ['similarity', order_value.direction, order_value.expr]
+ else
+ [order_value.expr.name, order_value.direction, nil]
+ end
end
# determine if ordering using LOWER, eg. "ORDER BY LOWER(boards.name)"
def ordering_by_lower?(order_value)
order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'lower'
end
+
+ # determine if ordering using SIMILARITY scoring based on Gitlab::Database::SimilarityScore
+ def ordering_by_similarity?(order_value)
+ order_value.to_sql.match?(/SIMILARITY\(.+\*/)
+ end
end
end
end
diff --git a/lib/gitlab/graphql/representation/submodule_tree_entry.rb b/lib/gitlab/graphql/representation/submodule_tree_entry.rb
index 8d17cb9eecc..aa5e74cc837 100644
--- a/lib/gitlab/graphql/representation/submodule_tree_entry.rb
+++ b/lib/gitlab/graphql/representation/submodule_tree_entry.rb
@@ -24,11 +24,11 @@ module Gitlab
end
def web_url
- @submodule_links.first
+ @submodule_links&.web
end
def tree_url
- @submodule_links.last
+ @submodule_links&.tree
end
end
end
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index eb4361cdc53..0cc3de297ba 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -4,28 +4,32 @@ module Gitlab
class GroupSearchResults < SearchResults
attr_reader :group
- def initialize(current_user, limit_projects, group, query, default_project_filter: false)
- super(current_user, limit_projects, query, default_project_filter: default_project_filter)
-
+ def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, filters: {})
@group = group
+
+ super(current_user, query, limit_projects, default_project_filter: default_project_filter, filters: filters)
end
# rubocop:disable CodeReuse/ActiveRecord
def users
- # 1: get all groups the current user has access to
- groups = GroupsFinder.new(current_user).execute.joins(:users)
+ # get all groups the current user has access to
+ # ignore order inherited from GroupsFinder to improve performance
+ current_user_groups = GroupsFinder.new(current_user).execute.unscope(:order)
+
+ # the hierarchy of the current group
+ group_groups = @group.self_and_hierarchy.unscope(:order)
+
+ # the groups where the above hierarchies intersect
+ intersect_groups = group_groups.where(id: current_user_groups)
- # 2: Get the group's whole hierarchy
- group_users = @group.direct_and_indirect_users
+ # members of @group hierarchy where the user has access to the groups
+ members = GroupMember.where(group: intersect_groups).non_invite
- # 3: get all users the current user has access to (->
- # `SearchResults#users`), which also applies the query.
+ # get all users the current user has access to (-> `SearchResults#users`), which also applies the query
users = super
- # 4: filter for users that belong to the previously selected groups
- users
- .where(id: group_users.select('id'))
- .where(id: groups.select('members.user_id'))
+ # filter users that belong to the previously selected groups
+ users.where(id: members.select(:user_id))
end
# rubocop:enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb
index b72d08549fe..b57560544c8 100644
--- a/lib/gitlab/hashed_storage/migrator.rb
+++ b/lib/gitlab/hashed_storage/migrator.rb
@@ -62,28 +62,24 @@ module Gitlab
# Flag a project to be migrated to Hashed Storage
#
# @param [Project] project that will be migrated
- # rubocop:disable Gitlab/RailsLogger
def migrate(project)
- Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
+ Gitlab::AppLogger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
project.migrate_to_hashed_storage!
rescue => err
- Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
+ Gitlab::AppLogger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
end
- # rubocop:enable Gitlab/RailsLogger
# Flag a project to be rolled-back to Legacy Storage
#
# @param [Project] project that will be rolled-back
- # rubocop:disable Gitlab/RailsLogger
def rollback(project)
- Rails.logger.info "Starting storage rollback of #{project.full_path} (ID=#{project.id})..."
+ Gitlab::AppLogger.info "Starting storage rollback of #{project.full_path} (ID=#{project.id})..."
project.rollback_to_legacy_storage!
rescue => err
- Rails.logger.error("#{err.message} rolling-back storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
+ Gitlab::AppLogger.error("#{err.message} rolling-back storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
end
- # rubocop:enable Gitlab/RailsLogger
# Returns whether we have any pending storage migration
#
@@ -97,6 +93,14 @@ module Gitlab
any_non_empty_queue?(::HashedStorage::RollbackerWorker, ::HashedStorage::ProjectRollbackWorker)
end
+ # Remove all remaining scheduled rollback operations
+ #
+ def abort_rollback!
+ [::HashedStorage::RollbackerWorker, ::HashedStorage::ProjectRollbackWorker].each do |worker|
+ Sidekiq::Queue.new(worker.queue).clear
+ end
+ end
+
private
def any_non_empty_queue?(*workers)
diff --git a/lib/gitlab/health_checks/simple_abstract_check.rb b/lib/gitlab/health_checks/simple_abstract_check.rb
index 4e0b9296819..ae99768b7b4 100644
--- a/lib/gitlab/health_checks/simple_abstract_check.rb
+++ b/lib/gitlab/health_checks/simple_abstract_check.rb
@@ -24,7 +24,7 @@ module Gitlab
result, elapsed = with_timing(&method(:check))
return if result.nil?
- Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless successful?(result) # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("#{human_name} check returned unexpected result #{result}") unless successful?(result)
[
metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0),
metric("#{metric_prefix}_success", successful?(result) ? 1 : 0),
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 07c43fa4832..40dee0142b9 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -3,8 +3,8 @@
module Gitlab
class Highlight
TIMEOUT_BACKGROUND = 30.seconds
- TIMEOUT_FOREGROUND = 3.seconds
- MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
+ TIMEOUT_FOREGROUND = 1.5.seconds
+ MAXIMUM_TEXT_HIGHLIGHT_SIZE = 512.kilobytes
def self.highlight(blob_name, blob_content, language: nil, plain: false)
new(blob_name, blob_content, language: language)
@@ -31,8 +31,8 @@ module Gitlab
def lexer
@lexer ||= custom_language || begin
Rouge::Lexer.guess(filename: @blob_name, source: @blob_content).new
- rescue Rouge::Guesser::Ambiguous => e
- e.alternatives.min_by(&:tag)
+ rescue Rouge::Guesser::Ambiguous => e
+ e.alternatives.min_by(&:tag)
end
end
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index 559e1828a70..be87dcc0ff9 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -35,7 +35,7 @@ module Gitlab
def self.perform_request(http_method, path, options, &block)
log_info = options.delete(:extra_log_info)
options_with_timeouts =
- if !options.has_key?(:timeout) && Feature.enabled?(:http_default_timeouts)
+ if !options.has_key?(:timeout)
options.with_defaults(DEFAULT_TIMEOUT_OPTIONS)
else
options
diff --git a/lib/gitlab/i18n/html_todo.yml b/lib/gitlab/i18n/html_todo.yml
index bfd96ba8579..91e01f8a0b8 100644
--- a/lib/gitlab/i18n/html_todo.yml
+++ b/lib/gitlab/i18n/html_todo.yml
@@ -16,67 +16,6 @@
# why this change has been made.
#
-" 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시간"
-
#
# 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:
@@ -313,3 +252,63 @@
- "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/import_export/lfs_saver.rb b/lib/gitlab/import_export/lfs_saver.rb
index 18c590e1ca9..515fd98630c 100644
--- a/lib/gitlab/import_export/lfs_saver.rb
+++ b/lib/gitlab/import_export/lfs_saver.rb
@@ -16,7 +16,7 @@ module Gitlab
end
def save
- project.all_lfs_objects.find_in_batches(batch_size: BATCH_SIZE) do |batch|
+ project.lfs_objects.find_in_batches(batch_size: BATCH_SIZE) do |batch|
batch.each do |lfs_object|
save_lfs_object(lfs_object)
end
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index a240c367a42..9ec5df8cde9 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -190,8 +190,20 @@ excluded_attributes:
- :stored_externally
- :external_diff_store
- :merge_request_id
+ - :verification_retry_at
+ - :verified_at
+ - :verification_retry_count
+ - :verification_checksum
+ - :verification_failure
merge_request_diff_commits:
- :merge_request_diff_id
+ merge_request_diff_detail:
+ - :merge_request_diff_id
+ - :verification_retry_at
+ - :verified_at
+ - :verification_retry_count
+ - :verification_checksum
+ - :verification_failure
merge_request_diff_files:
- :diff
- :external_diff_offset
diff --git a/lib/gitlab/jira/dvcs.rb b/lib/gitlab/jira/dvcs.rb
new file mode 100644
index 00000000000..4415f98fc7f
--- /dev/null
+++ b/lib/gitlab/jira/dvcs.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Jira
+ module Dvcs
+ ENCODED_SLASH = '@'.freeze
+ SLASH = '/'.freeze
+ ENCODED_ROUTE_REGEX = /[a-zA-Z0-9_\-\.#{ENCODED_SLASH}]+/.freeze
+
+ def self.encode_slash(path)
+ path.gsub(SLASH, ENCODED_SLASH)
+ end
+
+ def self.decode_slash(path)
+ path.gsub(ENCODED_SLASH, SLASH)
+ end
+
+ # To present two types of projects stored by Jira,
+ # Type 1 are projects imported prior to nested group support,
+ # those project names are not full_path, so they are presented differently
+ # to maintain backwards compatibility.
+ # Type 2 are projects imported after nested group support,
+ # those project names are encoded full path
+ #
+ # @param [Project] project
+ def self.encode_project_name(project)
+ if project.namespace.has_parent?
+ encode_slash(project.full_path)
+ else
+ project.path
+ end
+ end
+
+ # To interpret two types of project names stored by Jira (see `encode_project_name`)
+ #
+ # @param [String] project
+ # Either an encoded full path, or just project name
+ # @param [String] namespace
+ def self.restore_full_path(namespace:, project:)
+ if project.include?(ENCODED_SLASH)
+ project.gsub(ENCODED_SLASH, SLASH)
+ else
+ "#{namespace}/#{project}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/jira/middleware.rb b/lib/gitlab/jira/middleware.rb
new file mode 100644
index 00000000000..8a74729da49
--- /dev/null
+++ b/lib/gitlab/jira/middleware.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Jira
+ class Middleware
+ def self.jira_dvcs_connector?(env)
+ env['HTTP_USER_AGENT']&.downcase&.start_with?('jira dvcs connector')
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ if self.class.jira_dvcs_connector?(env)
+ env['HTTP_AUTHORIZATION'] = env['HTTP_AUTHORIZATION']&.sub('token', 'Bearer')
+ end
+
+ @app.call(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb
index d6681347f42..29cfec443e8 100644
--- a/lib/gitlab/json.rb
+++ b/lib/gitlab/json.rb
@@ -22,7 +22,7 @@ module Gitlab
string = string.to_s unless string.is_a?(String)
legacy_mode = legacy_mode_enabled?(opts.delete(:legacy_mode))
- data = adapter_load(string, opts)
+ data = adapter_load(string, **opts)
handle_legacy_mode!(data) if legacy_mode
diff --git a/lib/gitlab/kas.rb b/lib/gitlab/kas.rb
new file mode 100644
index 00000000000..08dde98e965
--- /dev/null
+++ b/lib/gitlab/kas.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kas
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Kas-Api-Request'
+ JWT_ISSUER = 'gitlab-kas'
+
+ include JwtAuthenticatable
+
+ class << self
+ def verify_api_request(request_headers)
+ decode_jwt_for_issuer(JWT_ISSUER, request_headers[INTERNAL_API_REQUEST_HEADER])
+ rescue JWT::DecodeError
+ nil
+ end
+
+ def secret_path
+ Gitlab.config.gitlab_kas.secret_file
+ end
+
+ def ensure_secret!
+ return if File.exist?(secret_path)
+
+ write_secret
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/cilium_network_policy.rb b/lib/gitlab/kubernetes/cilium_network_policy.rb
index 55afd2b586e..9043932bbe5 100644
--- a/lib/gitlab/kubernetes/cilium_network_policy.rb
+++ b/lib/gitlab/kubernetes/cilium_network_policy.rb
@@ -9,8 +9,12 @@ module Gitlab
API_VERSION = "cilium.io/v2"
KIND = 'CiliumNetworkPolicy'
- def initialize(name:, namespace:, selector:, ingress:, resource_version:, labels: nil, creation_timestamp: nil, egress: nil)
+ # We are modeling existing kubernetes resource and don't have
+ # control over amount of parameters.
+ # rubocop:disable Metrics/ParameterLists
+ def initialize(name:, namespace:, selector:, ingress:, resource_version: nil, description: nil, labels: nil, creation_timestamp: nil, egress: nil)
@name = name
+ @description = description
@namespace = namespace
@labels = labels
@creation_timestamp = creation_timestamp
@@ -19,15 +23,7 @@ module Gitlab
@ingress = ingress
@egress = egress
end
-
- def generate
- ::Kubeclient::Resource.new.tap do |resource|
- resource.kind = KIND
- resource.apiVersion = API_VERSION
- resource.metadata = metadata
- resource.spec = spec
- end
- end
+ # rubocop:enable Metrics/ParameterLists
def self.from_yaml(manifest)
return unless manifest
@@ -39,6 +35,7 @@ module Gitlab
spec = policy[:spec]
self.new(
name: metadata[:name],
+ description: policy[:description],
namespace: metadata[:namespace],
resource_version: metadata[:resourceVersion],
labels: metadata[:labels],
@@ -58,6 +55,7 @@ module Gitlab
spec = resource[:spec].to_h
self.new(
name: metadata[:name],
+ description: resource[:description],
namespace: metadata[:namespace],
resource_version: metadata[:resourceVersion],
labels: metadata[:labels]&.to_h,
@@ -68,21 +66,39 @@ module Gitlab
)
end
+ override :resource
+ def resource
+ resource = {
+ apiVersion: API_VERSION,
+ kind: KIND,
+ metadata: metadata,
+ spec: spec
+ }
+ resource[:description] = description if description
+ resource
+ end
+
private
- attr_reader :name, :namespace, :labels, :creation_timestamp, :resource_version, :ingress, :egress
+ attr_reader :name, :description, :namespace, :labels, :creation_timestamp, :resource_version, :ingress, :egress
def selector
@selector ||= {}
end
- override :spec
+ def metadata
+ meta = { name: name, namespace: namespace }
+ meta[:labels] = labels if labels
+ meta[:resourceVersion] = resource_version if resource_version
+ meta
+ end
+
def spec
{
endpointSelector: selector,
ingress: ingress,
egress: egress
- }
+ }.compact
end
end
end
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index 9e3cf58bb1e..fa68afd39f5 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -92,6 +92,7 @@ module Gitlab
# group client
delegate :create_network_policy,
:get_network_policies,
+ :get_network_policy,
:update_network_policy,
:delete_network_policy,
to: :networking_client
@@ -100,6 +101,7 @@ module Gitlab
# group client
delegate :create_cilium_network_policy,
:get_cilium_network_policies,
+ :get_cilium_network_policy,
:update_cilium_network_policy,
:delete_cilium_network_policy,
to: :cilium_networking_client
diff --git a/lib/gitlab/kubernetes/network_policy.rb b/lib/gitlab/kubernetes/network_policy.rb
index 28810dc4453..c8e9b987443 100644
--- a/lib/gitlab/kubernetes/network_policy.rb
+++ b/lib/gitlab/kubernetes/network_policy.rb
@@ -6,6 +6,8 @@ module Gitlab
include NetworkPolicyCommon
extend ::Gitlab::Utils::Override
+ KIND = 'NetworkPolicy'
+
def initialize(name:, namespace:, selector:, ingress:, labels: nil, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil)
@name = name
@namespace = namespace
@@ -17,13 +19,6 @@ module Gitlab
@egress = egress
end
- def generate
- ::Kubeclient::Resource.new.tap do |resource|
- resource.metadata = metadata
- resource.spec = spec
- end
- end
-
def self.from_yaml(manifest)
return unless manifest
@@ -63,6 +58,15 @@ module Gitlab
)
end
+ override :resource
+ def resource
+ {
+ kind: KIND,
+ metadata: metadata,
+ spec: spec
+ }
+ end
+
private
attr_reader :name, :namespace, :labels, :creation_timestamp, :policy_types, :ingress, :egress
@@ -71,7 +75,12 @@ module Gitlab
@selector ||= {}
end
- override :spec
+ def metadata
+ meta = { name: name, namespace: namespace }
+ meta[:labels] = labels if labels
+ meta
+ end
+
def spec
{
podSelector: selector,
diff --git a/lib/gitlab/kubernetes/network_policy_common.rb b/lib/gitlab/kubernetes/network_policy_common.rb
index 3b6e46d21ef..99517454508 100644
--- a/lib/gitlab/kubernetes/network_policy_common.rb
+++ b/lib/gitlab/kubernetes/network_policy_common.rb
@@ -5,6 +5,10 @@ module Gitlab
module NetworkPolicyCommon
DISABLED_BY_LABEL = :'network-policy.gitlab.com/disabled_by'
+ def generate
+ ::Kubeclient::Resource.new(resource)
+ end
+
def as_json(opts = nil)
{
name: name,
@@ -46,19 +50,12 @@ module Gitlab
private
- def metadata
- meta = { name: name, namespace: namespace }
- meta[:labels] = labels if labels
- meta[:resourceVersion] = resource_version if defined?(resource_version)
- meta
- end
-
- def spec
+ def resource
raise NotImplementedError
end
def manifest
- YAML.dump({ metadata: metadata, spec: spec }.deep_stringify_keys)
+ YAML.dump(resource.deep_stringify_keys)
end
end
end
diff --git a/lib/gitlab/lfs/client.rb b/lib/gitlab/lfs/client.rb
new file mode 100644
index 00000000000..e4d600694c2
--- /dev/null
+++ b/lib/gitlab/lfs/client.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+module Gitlab
+ module Lfs
+ # Gitlab::Lfs::Client implements a simple LFS client, designed to talk to
+ # LFS servers as described in these documents:
+ # * https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
+ # * https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
+ class Client
+ attr_reader :base_url
+
+ def initialize(base_url, credentials:)
+ @base_url = base_url
+ @credentials = credentials
+ end
+
+ def batch(operation, objects)
+ body = {
+ operation: operation,
+ transfers: ['basic'],
+ # We don't know `ref`, so can't send it
+ objects: objects.map { |object| { oid: object.oid, size: object.size } }
+ }
+
+ rsp = Gitlab::HTTP.post(
+ batch_url,
+ basic_auth: basic_auth,
+ body: body.to_json,
+ headers: { 'Content-Type' => 'application/vnd.git-lfs+json' }
+ )
+
+ raise BatchSubmitError unless rsp.success?
+
+ # HTTParty provides rsp.parsed_response, but it only kicks in for the
+ # application/json content type in the response, which we can't rely on
+ body = Gitlab::Json.parse(rsp.body)
+ transfer = body.fetch('transfer', 'basic')
+
+ raise UnsupportedTransferError.new(transfer.inspect) unless transfer == 'basic'
+
+ body
+ end
+
+ def upload(object, upload_action, authenticated:)
+ file = object.file.open
+
+ params = {
+ body_stream: file,
+ headers: {
+ 'Content-Length' => object.size.to_s,
+ 'Content-Type' => 'application/octet-stream'
+ }.merge(upload_action['header'] || {})
+ }
+
+ params[:basic_auth] = basic_auth unless authenticated
+
+ rsp = Gitlab::HTTP.put(upload_action['href'], params)
+
+ raise ObjectUploadError unless rsp.success?
+ ensure
+ file&.close
+ end
+
+ private
+
+ attr_reader :credentials
+
+ def batch_url
+ base_url + '/info/lfs/objects/batch'
+ end
+
+ def basic_auth
+ return unless credentials[:auth_method] == "password"
+
+ { username: credentials[:user], password: credentials[:password] }
+ end
+
+ class BatchSubmitError < StandardError
+ def message
+ "Failed to submit batch"
+ end
+ end
+
+ class UnsupportedTransferError < StandardError
+ def initialize(transfer = nil)
+ super
+ @transfer = transfer
+ end
+
+ def message
+ "Unsupported transfer: #{@transfer}"
+ end
+ end
+
+ class ObjectUploadError < StandardError
+ def message
+ "Failed to upload object"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb
index 2ec8c268d09..89a4e36a232 100644
--- a/lib/gitlab/logger.rb
+++ b/lib/gitlab/logger.rb
@@ -32,7 +32,8 @@ module Gitlab
end
def self.build
- Gitlab::SafeRequestStore[self.cache_key] ||= new(self.full_log_path)
+ Gitlab::SafeRequestStore[self.cache_key] ||=
+ new(self.full_log_path, level: ::Logger::DEBUG)
end
def self.full_log_path
diff --git a/lib/gitlab/marginalia/comment.rb b/lib/gitlab/marginalia/comment.rb
index d5dae5ef4b3..7b4e4b06f00 100644
--- a/lib/gitlab/marginalia/comment.rb
+++ b/lib/gitlab/marginalia/comment.rb
@@ -28,7 +28,7 @@ module Gitlab
# We are using 'Marginalia::SidekiqInstrumentation' which does not support 'ActiveJob::Base'.
# Gitlab also uses 'ActionMailer::MailDeliveryJob' which inherits from ActiveJob::Base.
# So below condition is used to return metadata for such jobs.
- if job.is_a?(ActionMailer::MailDeliveryJob) || job.is_a?(ActionMailer::DeliveryJob)
+ if job.is_a?(ActionMailer::MailDeliveryJob)
{
"class" => job.arguments.first,
"jid" => job.job_id
diff --git a/lib/gitlab/metrics/dashboard/importer.rb b/lib/gitlab/metrics/dashboard/importer.rb
new file mode 100644
index 00000000000..ca835650648
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/importer.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ class Importer
+ def initialize(dashboard_path, project)
+ @dashboard_path = dashboard_path.to_s
+ @project = project
+ end
+
+ def execute
+ return false unless Dashboard::Validator.validate(dashboard_hash, project: project, dashboard_path: dashboard_path)
+
+ Dashboard::Importers::PrometheusMetrics.new(dashboard_hash, project: project, dashboard_path: dashboard_path).execute
+ rescue Gitlab::Config::Loader::FormatError
+ false
+ end
+
+ def execute!
+ Dashboard::Validator.validate!(dashboard_hash, project: project, dashboard_path: dashboard_path)
+
+ Dashboard::Importers::PrometheusMetrics.new(dashboard_hash, project: project, dashboard_path: dashboard_path).execute!
+ end
+
+ private
+
+ attr_accessor :dashboard_path, :project
+
+ def dashboard_hash
+ @dashboard_hash ||= begin
+ raw_dashboard = Dashboard::RepoDashboardFinder.read_dashboard(project, dashboard_path)
+ return unless raw_dashboard.present?
+
+ ::Gitlab::Config::Loader::Yaml.new(raw_dashboard).load_raw!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb b/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb
new file mode 100644
index 00000000000..d1490d5d9b6
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Importers
+ class PrometheusMetrics
+ ALLOWED_ATTRIBUTES = %i(title query y_label unit legend group dashboard_path).freeze
+
+ # Takes a JSON schema validated dashboard hash and
+ # imports metrics to database
+ def initialize(dashboard_hash, project:, dashboard_path:)
+ @dashboard_hash = dashboard_hash
+ @project = project
+ @dashboard_path = dashboard_path
+ end
+
+ def execute
+ import
+ rescue ActiveRecord::RecordInvalid, ::Gitlab::Metrics::Dashboard::Transformers::TransformerError
+ false
+ end
+
+ def execute!
+ import
+ end
+
+ private
+
+ attr_reader :dashboard_hash, :project, :dashboard_path
+
+ def import
+ delete_stale_metrics
+ create_or_update_metrics
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def create_or_update_metrics
+ # TODO: use upsert and worker for callbacks?
+ prometheus_metrics_attributes.each do |attributes|
+ prometheus_metric = PrometheusMetric.find_or_initialize_by(attributes.slice(:identifier, :project))
+ prometheus_metric.update!(attributes.slice(*ALLOWED_ATTRIBUTES))
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def delete_stale_metrics
+ identifiers = prometheus_metrics_attributes.map { |metric_attributes| metric_attributes[:identifier] }
+
+ stale_metrics = PrometheusMetric.for_project(project)
+ .for_dashboard_path(dashboard_path)
+ .for_group(Enums::PrometheusMetric.groups[:custom])
+ .not_identifier(identifiers)
+
+ # TODO: use destroy_all and worker for callbacks?
+ stale_metrics.each(&:destroy)
+ end
+
+ def prometheus_metrics_attributes
+ @prometheus_metrics_attributes ||= begin
+ Dashboard::Transformers::Yml::V1::PrometheusMetrics.new(
+ dashboard_hash,
+ project: project,
+ dashboard_path: dashboard_path
+ ).execute
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb b/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb
index cfec027155e..06cfa5cc58e 100644
--- a/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb
@@ -27,7 +27,7 @@ module Gitlab
private
def custom_group_titles
- @custom_group_titles ||= PrometheusMetricEnums.custom_group_details.values.map { |group_details| group_details[:group_title] }
+ @custom_group_titles ||= Enums::PrometheusMetric.custom_group_details.values.map { |group_details| group_details[:group_title] }
end
def edit_path(metric)
diff --git a/lib/gitlab/metrics/dashboard/transformers/errors.rb b/lib/gitlab/metrics/dashboard/transformers/errors.rb
new file mode 100644
index 00000000000..4d94ab098ae
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/transformers/errors.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Transformers
+ TransformerError = Class.new(StandardError)
+
+ module Errors
+ class MissingAttribute < TransformerError
+ def initialize(attribute_name)
+ super("Missing attribute: '#{attribute_name}'")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb b/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb
new file mode 100644
index 00000000000..4e46eec17d6
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Transformers
+ module Yml
+ module V1
+ # Takes a JSON schema validated dashboard hash and
+ # maps it to PrometheusMetric model attributes
+ class PrometheusMetrics
+ def initialize(dashboard_hash, project: nil, dashboard_path: nil)
+ @dashboard_hash = dashboard_hash.with_indifferent_access
+ @project = project
+ @dashboard_path = dashboard_path
+
+ @dashboard_hash.default_proc = -> (h, k) { raise Transformers::Errors::MissingAttribute, k.to_s }
+ end
+
+ def execute
+ prometheus_metrics = []
+
+ dashboard_hash[:panel_groups].each do |panel_group|
+ panel_group[:panels].each do |panel|
+ panel[:metrics].each do |metric|
+ prometheus_metrics << {
+ project: project,
+ title: panel[:title],
+ y_label: panel[:y_label],
+ query: metric[:query_range] || metric[:query],
+ unit: metric[:unit],
+ legend: metric[:label],
+ identifier: metric[:id],
+ group: Enums::PrometheusMetric.groups[:custom],
+ common: false,
+ dashboard_path: dashboard_path
+ }.compact
+ end
+ end
+ end
+
+ prometheus_metrics
+ end
+
+ private
+
+ attr_reader :dashboard_hash, :project, :dashboard_path
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
index 160ecfb85c9..6dcc73c0f6a 100644
--- a/lib/gitlab/metrics/dashboard/url.rb
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -10,20 +10,23 @@ module Gitlab
QUERY_PATTERN = '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
ANCHOR_PATTERN = '(?<anchor>\#[a-z0-9_-]+)?'
- OPTIONAL_DASH_PATTERN = '(?:/-)?'
+ DASH_PATTERN = '(?:/-)'
- # Matches urls for a metrics dashboard. This could be
- # either the /metrics endpoint or the /metrics_dashboard
- # endpoint.
+ # Matches urls for a metrics dashboard.
+ # This regex needs to match the old metrics URL, the new metrics URL,
+ # and the dashboard URL (inline_metrics_redactor_filter.rb
+ # uses this regex to match against the dashboard URL.)
#
- # EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics
+ # EX - Old URL: https://<host>/<namespace>/<project>/environments/<env_id>/metrics
+ # OR
+ # New URL: https://<host>/<namespace>/<project>/-/metrics?environment=<env_id>
+ # OR
+ # dashboard URL: https://<host>/<namespace>/<project>/environments/<env_id>/metrics_dashboard
def metrics_regex
strong_memoize(:metrics_regex) do
regex_for_project_metrics(
%r{
- /environments
- /(?<environment>\d+)
- /(metrics_dashboard|metrics)
+ ( #{environment_metrics_regex} ) | ( #{non_environment_metrics_regex} )
}x
)
end
@@ -36,6 +39,7 @@ module Gitlab
strong_memoize(:grafana_regex) do
regex_for_project_metrics(
%r{
+ #{DASH_PATTERN}?
/grafana
/metrics_dashboard
}x
@@ -44,16 +48,22 @@ module Gitlab
end
# Matches dashboard urls for a metric chart embed
- # for cluster metrics
+ # for cluster metrics.
+ # This regex needs to match the dashboard URL as well, not just the trigger URL.
+ # The inline_metrics_redactor_filter.rb uses this regex to match against
+ # the dashboard URL.
#
# EX - https://<host>/<namespace>/<project>/-/clusters/<cluster_id>/?group=Cluster%20Health&title=Memory%20Usage&y_label=Memory%20(GiB)
+ # dashboard URL - https://<host>/<namespace>/<project>/-/clusters/<cluster_id>/metrics_dashboard?group=Cluster%20Health&title=Memory%20Usage&y_label=Memory%20(GiB)
def clusters_regex
strong_memoize(:clusters_regex) do
regex_for_project_metrics(
%r{
+ #{DASH_PATTERN}?
/clusters
/(?<cluster_id>\d+)
/?
+ ( (/metrics) | ( /metrics_dashboard\.json ) )?
}x
)
end
@@ -67,10 +77,11 @@ module Gitlab
strong_memoize(:alert_regex) do
regex_for_project_metrics(
%r{
+ #{DASH_PATTERN}?
/prometheus
/alerts
/(?<alert>\d+)
- /metrics_dashboard
+ /metrics_dashboard(\.json)?
}x
)
end
@@ -95,16 +106,37 @@ module Gitlab
private
+ def environment_metrics_regex
+ %r{
+ #{DASH_PATTERN}?
+ /environments
+ /(?<environment>\d+)
+ /(metrics_dashboard|metrics)
+ }x
+ end
+
+ def non_environment_metrics_regex
+ %r{
+ #{DASH_PATTERN}
+ /metrics
+ (?= # Lookahead to ensure there is an environment query param
+ \?
+ .*
+ environment=(?<environment>\d+)
+ .*
+ )
+ }x
+ end
+
def regex_for_project_metrics(path_suffix_pattern)
%r{
- (?<url>
+ ^(?<url>
#{gitlab_host_pattern}
#{project_path_pattern}
- #{OPTIONAL_DASH_PATTERN}
#{path_suffix_pattern}
#{QUERY_PATTERN}
#{ANCHOR_PATTERN}
- )
+ )$
}x
end
diff --git a/lib/gitlab/metrics/dashboard/validator.rb b/lib/gitlab/metrics/dashboard/validator.rb
index 8edd9c397f9..1e8dc059968 100644
--- a/lib/gitlab/metrics/dashboard/validator.rb
+++ b/lib/gitlab/metrics/dashboard/validator.rb
@@ -4,24 +4,22 @@ module Gitlab
module Metrics
module Dashboard
module Validator
- DASHBOARD_SCHEMA_PATH = 'lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json'.freeze
+ DASHBOARD_SCHEMA_PATH = Rails.root.join(*%w[lib gitlab metrics dashboard validator schemas dashboard.json]).freeze
class << self
def validate(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
- errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project)
- errors.empty?
+ errors(content, schema_path, dashboard_path: dashboard_path, project: project).empty?
end
def validate!(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
- errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project)
+ errors = errors(content, schema_path, dashboard_path: dashboard_path, project: project)
errors.empty? || raise(errors.first)
end
- private
-
- def _validate(content, schema_path, dashboard_path: nil, project: nil)
- client = Validator::Client.new(content, schema_path, dashboard_path: dashboard_path, project: project)
- client.execute
+ def errors(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
+ Validator::Client
+ .new(content, schema_path, dashboard_path: dashboard_path, project: project)
+ .execute
end
end
end
diff --git a/lib/gitlab/metrics/dashboard/validator/client.rb b/lib/gitlab/metrics/dashboard/validator/client.rb
index c63415abcfc..588c677ca28 100644
--- a/lib/gitlab/metrics/dashboard/validator/client.rb
+++ b/lib/gitlab/metrics/dashboard/validator/client.rb
@@ -46,7 +46,7 @@ module Gitlab
def validate_against_schema
schemer.validate(content).map do |error|
- Errors::SchemaValidationError.new(error)
+ ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new(error)
end
end
end
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/panel.json b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json
index 011eef53e40..2ae9608036e 100644
--- a/lib/gitlab/metrics/dashboard/validator/schemas/panel.json
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json
@@ -4,7 +4,7 @@
"properties": {
"type": {
"type": "string",
- "enum": ["area-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap"],
+ "enum": ["area-chart", "line-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap", "gauge"],
"default": "area-chart"
},
"title": { "type": "string" },
diff --git a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
index 054b4949dd6..36262b09b2d 100644
--- a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
+++ b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
@@ -12,7 +12,11 @@ module Gitlab
end
def log_filename
- File.join(Rails.root, 'log', 'sidekiq_exporter.log')
+ if settings['log_enabled']
+ File.join(Rails.root, 'log', 'sidekiq_exporter.log')
+ else
+ File::NULL
+ end
end
private
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index ff3fffe7b95..66361529546 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -120,9 +120,6 @@ module Gitlab
def self.instrument(type, mod, name)
return unless ::Gitlab::Metrics.enabled?
- name = name.to_sym
- target = type == :instance ? mod : mod.singleton_class
-
if type == :instance
target = mod
method_name = "##{name}"
@@ -154,6 +151,8 @@ module Gitlab
'*args'
end
+ method_visibility = method_visibility_for(target, name)
+
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
if trans = Gitlab::Metrics::Instrumentation.transaction
@@ -163,11 +162,23 @@ module Gitlab
super
end
end
+ #{method_visibility} :#{name}
EOF
target.prepend(proxy_module)
end
+ def self.method_visibility_for(mod, name)
+ if mod.private_method_defined?(name)
+ :private
+ elsif mod.protected_method_defined?(name)
+ :protected
+ else
+ :public
+ end
+ end
+ private_class_method :method_visibility_for
+
# Small layer of indirection to make it easier to stub out the current
# transaction.
def self.transaction
diff --git a/lib/gitlab/metrics/methods.rb b/lib/gitlab/metrics/methods.rb
index 2b5d1c710f6..3100450bc00 100644
--- a/lib/gitlab/metrics/methods.rb
+++ b/lib/gitlab/metrics/methods.rb
@@ -52,7 +52,7 @@ module Gitlab
end
def disabled_by_feature(options)
- options.with_feature && !::Feature.enabled?(options.with_feature)
+ options.with_feature && !::Feature.enabled?(options.with_feature, type: :ops)
end
def build_metric!(type, name, options)
diff --git a/lib/gitlab/metrics/samplers/action_cable_sampler.rb b/lib/gitlab/metrics/samplers/action_cable_sampler.rb
new file mode 100644
index 00000000000..9f4979fa673
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/action_cable_sampler.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class ActionCableSampler < BaseSampler
+ SAMPLING_INTERVAL_SECONDS = 5
+
+ def initialize(interval = SAMPLING_INTERVAL_SECONDS, action_cable: ::ActionCable.server)
+ super(interval)
+ @action_cable = action_cable
+ end
+
+ def metrics
+ @metrics ||= {
+ active_connections: ::Gitlab::Metrics.gauge(
+ :action_cable_active_connections, 'Number of ActionCable WS clients currently connected'
+ ),
+ pool_min_size: ::Gitlab::Metrics.gauge(
+ :action_cable_pool_min_size, 'Minimum number of worker threads in ActionCable thread pool'
+ ),
+ pool_max_size: ::Gitlab::Metrics.gauge(
+ :action_cable_pool_max_size, 'Maximum number of worker threads in ActionCable thread pool'
+ ),
+ pool_current_size: ::Gitlab::Metrics.gauge(
+ :action_cable_pool_current_size, 'Current number of worker threads in ActionCable thread pool'
+ ),
+ pool_largest_size: ::Gitlab::Metrics.gauge(
+ :action_cable_pool_largest_size, 'Largest number of worker threads observed so far in ActionCable thread pool'
+ ),
+ pool_completed_tasks: ::Gitlab::Metrics.gauge(
+ :action_cable_pool_tasks_total, 'Total number of tasks executed in ActionCable thread pool'
+ ),
+ pool_pending_tasks: ::Gitlab::Metrics.gauge(
+ :action_cable_pool_pending_tasks, 'Number of tasks waiting to be executed in ActionCable thread pool'
+ )
+ }
+ end
+
+ def sample
+ pool = @action_cable.worker_pool.executor
+ labels = {
+ server_mode: server_mode
+ }
+
+ metrics[:active_connections].set(labels, @action_cable.connections.size)
+ metrics[:pool_min_size].set(labels, pool.min_length)
+ metrics[:pool_max_size].set(labels, pool.max_length)
+ metrics[:pool_current_size].set(labels, pool.length)
+ metrics[:pool_largest_size].set(labels, pool.largest_length)
+ metrics[:pool_completed_tasks].set(labels, pool.completed_task_count)
+ metrics[:pool_pending_tasks].set(labels, pool.queue_length)
+ end
+
+ private
+
+ def server_mode
+ Gitlab::ActionCable::Config.in_app? ? 'in-app' : 'standalone'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb
index ff3e7be567f..39a49187e45 100644
--- a/lib/gitlab/metrics/samplers/base_sampler.rb
+++ b/lib/gitlab/metrics/samplers/base_sampler.rb
@@ -21,7 +21,7 @@ module Gitlab
def safe_sample
sample
rescue => e
- Rails.logger.warn("#{self.class}: #{e}, stopping") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn("#{self.class}: #{e}, stopping")
stop
end
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
index b5343d5e66a..d295beb59f1 100644
--- a/lib/gitlab/metrics/samplers/puma_sampler.rb
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -42,7 +42,7 @@ module Gitlab
def puma_stats
Puma.stats
rescue NoMethodError
- Rails.logger.info "PumaSampler: stats are not available yet, waiting for Puma to boot" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info "PumaSampler: stats are not available yet, waiting for Puma to boot"
nil
end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index c0b671abd44..8e6ac7610f2 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -29,6 +29,8 @@ module Gitlab
module Middleware
class Multipart
RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+ JWT_PARAM_SUFFIX = '.gitlab-workhorse-upload'
+ JWT_PARAM_FIXED_KEY = 'upload'
class Handler
def initialize(env, message)
@@ -57,7 +59,8 @@ module Gitlab
yield
ensure
- @open_files.each(&:close)
+ @open_files.compact
+ .each(&:close)
end
# This function calls itself recursively
@@ -122,15 +125,89 @@ module Gitlab
def allowed_paths
[
+ Dir.tmpdir,
::FileUploader.root,
- Gitlab.config.uploads.storage_path,
- JobArtifactUploader.workhorse_upload_path,
- LfsObjectUploader.workhorse_upload_path,
+ ::Gitlab.config.uploads.storage_path,
+ ::JobArtifactUploader.workhorse_upload_path,
+ ::LfsObjectUploader.workhorse_upload_path,
File.join(Rails.root, 'public/uploads/tmp')
] + package_allowed_paths
end
end
+ # TODO this class is meant to replace Handler when the feature flag
+ # upload_middleware_jwt_params_handler is removed
+ class HandlerForJWTParams < Handler
+ def with_open_files
+ @rewritten_fields.keys.each do |field|
+ parsed_field = Rack::Utils.parse_nested_query(field)
+ raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
+
+ key, value = parsed_field.first
+ if value.nil? # we have a top level param, eg. field = 'foo' and not 'foo[bar]'
+ raise "invalid field: #{field.inspect}" if field != key
+
+ value = open_file(extract_upload_params_from(@request.params, with_prefix: key))
+ @open_files << value
+ else
+ value = decorate_params_value(value, @request.params[key])
+ end
+
+ update_param(key, value)
+ end
+
+ yield
+ ensure
+ @open_files.compact
+ .each(&:close)
+ end
+
+ # This function calls itself recursively
+ def decorate_params_value(hash_path, value_hash)
+ unless hash_path.is_a?(Hash) && hash_path.count == 1
+ raise "invalid path: #{hash_path.inspect}"
+ end
+
+ path_key, path_value = hash_path.first
+
+ unless value_hash.is_a?(Hash) && value_hash[path_key]
+ raise "invalid value hash: #{value_hash.inspect}"
+ end
+
+ case path_value
+ when nil
+ value_hash[path_key] = open_file(extract_upload_params_from(value_hash[path_key]))
+ @open_files << value_hash[path_key]
+ value_hash
+ when Hash
+ decorate_params_value(path_value, value_hash[path_key])
+ value_hash
+ else
+ raise "unexpected path value: #{path_value.inspect}"
+ end
+ end
+
+ def open_file(params)
+ ::UploadedFile.from_params_without_field(params, allowed_paths)
+ end
+
+ private
+
+ def extract_upload_params_from(params, with_prefix: '')
+ param_key = "#{with_prefix}#{JWT_PARAM_SUFFIX}"
+ jwt_token = params[param_key]
+ raise "Empty JWT param: #{param_key}" if jwt_token.blank?
+
+ payload = Gitlab::Workhorse.decode_jwt(jwt_token).first
+ raise "Invalid JWT payload: not a Hash" unless payload.is_a?(Hash)
+
+ upload_params = payload.fetch(JWT_PARAM_FIXED_KEY, {})
+ raise "Empty params for: #{param_key}" if upload_params.empty?
+
+ upload_params
+ end
+ end
+
def initialize(app)
@app = app
end
@@ -139,14 +216,24 @@ module Gitlab
encoded_message = env.delete(RACK_ENV_KEY)
return @app.call(env) if encoded_message.blank?
- message = Gitlab::Workhorse.decode_jwt(encoded_message)[0]
+ message = ::Gitlab::Workhorse.decode_jwt(encoded_message)[0]
- Handler.new(env, message).with_open_files do
+ handler_class.new(env, message).with_open_files do
@app.call(env)
end
rescue UploadedFile::InvalidPathError => e
[400, { 'Content-Type' => 'text/plain' }, e.message]
end
+
+ private
+
+ def handler_class
+ if Feature.enabled?(:upload_middleware_jwt_params_handler)
+ ::Gitlab::Middleware::Multipart::HandlerForJWTParams
+ else
+ ::Gitlab::Middleware::Multipart::Handler
+ end
+ end
end
end
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index 6573506e926..cfea4aaddf3 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -36,7 +36,7 @@ module Gitlab
def call
if disallowed_request? && Gitlab::Database.read_only?
- Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation') # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.debug('GitLab ReadOnly: preventing possible non read-only operation')
if json_request?
return [403, { 'Content-Type' => APPLICATION_JSON }, [{ 'message' => ERROR_MESSAGE }.to_json]]
diff --git a/lib/gitlab/middleware/same_site_cookies.rb b/lib/gitlab/middleware/same_site_cookies.rb
index 45968035e79..37ccc5abb10 100644
--- a/lib/gitlab/middleware/same_site_cookies.rb
+++ b/lib/gitlab/middleware/same_site_cookies.rb
@@ -30,6 +30,7 @@ module Gitlab
set_cookie = headers['Set-Cookie']&.strip
return result if set_cookie.blank? || !ssl?
+ return result if same_site_none_incompatible?(env['HTTP_USER_AGENT'])
cookies = set_cookie.split(COOKIE_SEPARATOR)
@@ -39,11 +40,11 @@ module Gitlab
# Chrome will drop SameSite=None cookies without the Secure
# flag. If we remove this middleware, we may need to ensure
# that all cookies set this flag.
- if ssl? && !(cookie =~ /;\s*secure/i)
+ unless SECURE_REGEX.match?(cookie)
cookie << '; Secure'
end
- unless cookie =~ /;\s*samesite=/i
+ unless SAME_SITE_REGEX.match?(cookie)
cookie << '; SameSite=None'
end
end
@@ -55,9 +56,93 @@ module Gitlab
private
+ # Taken from https://www.chromium.org/updates/same-site/incompatible-clients
+ # We use RE2 instead of the browser gem for performance.
+ IOS_REGEX = RE2('\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/')
+ MACOS_REGEX = RE2('\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\/')
+ SAFARI_REGEX = RE2('Version\/.* Safari\/')
+ CHROMIUM_REGEX = RE2('Chrom(e|ium)')
+ CHROMIUM_VERSION_REGEX = RE2('Chrom[^ \/]+\/(\d+)')
+ UC_BROWSER_REGEX = RE2('UCBrowser\/')
+ UC_BROWSER_VERSION_REGEX = RE2('UCBrowser\/(\d+)\.(\d+)\.(\d+)')
+
+ SECURE_REGEX = RE2(';\s*secure', case_sensitive: false)
+ SAME_SITE_REGEX = RE2(';\s*samesite=', case_sensitive: false)
+
def ssl?
Gitlab.config.gitlab.https
end
+
+ def same_site_none_incompatible?(user_agent)
+ return false if user_agent.blank?
+
+ has_webkit_same_site_bug?(user_agent) || drops_unrecognized_same_site_cookies?(user_agent)
+ end
+
+ def has_webkit_same_site_bug?(user_agent)
+ ios_version?(12, user_agent) ||
+ (macos_version?(10, 14, user_agent) && safari?(user_agent))
+ end
+
+ def drops_unrecognized_same_site_cookies?(user_agent)
+ if uc_browser?(user_agent)
+ return !uc_browser_version_at_least?(12, 13, 2, user_agent)
+ end
+
+ chromium_based?(user_agent) && chromium_version_between?(51, 66, user_agent)
+ end
+
+ def ios_version?(major, user_agent)
+ m = IOS_REGEX.match(user_agent)
+
+ return false if m.nil?
+
+ m[1].to_i == major
+ end
+
+ def macos_version?(major, minor, user_agent)
+ m = MACOS_REGEX.match(user_agent)
+
+ return false if m.nil?
+
+ m[1].to_i == major && m[2].to_i == minor
+ end
+
+ def safari?(user_agent)
+ SAFARI_REGEX.match?(user_agent)
+ end
+
+ def chromium_based?(user_agent)
+ CHROMIUM_REGEX.match?(user_agent)
+ end
+
+ def chromium_version_between?(from_major, to_major, user_agent)
+ m = CHROMIUM_VERSION_REGEX.match(user_agent)
+
+ return false if m.nil?
+
+ version = m[1].to_i
+ version >= from_major && version <= to_major
+ end
+
+ def uc_browser?(user_agent)
+ UC_BROWSER_REGEX.match?(user_agent)
+ end
+
+ def uc_browser_version_at_least?(major, minor, build, user_agent)
+ m = UC_BROWSER_VERSION_REGEX.match(user_agent)
+
+ return false if m.nil?
+
+ major_version = m[1].to_i
+ minor_version = m[2].to_i
+ build_version = m[3].to_i
+
+ return major_version > major if major_version != major
+ return minor_version > minor if minor_version != minor
+
+ build_version >= build
+ end
end
end
end
diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb
index 41d80fe9aa6..e18b6d003e0 100644
--- a/lib/gitlab/object_hierarchy.rb
+++ b/lib/gitlab/object_hierarchy.rb
@@ -7,18 +7,19 @@ module Gitlab
class ObjectHierarchy
DEPTH_COLUMN = :depth
- attr_reader :ancestors_base, :descendants_base, :model
+ attr_reader :ancestors_base, :descendants_base, :model, :options
# ancestors_base - An instance of ActiveRecord::Relation for which to
# get parent objects.
# descendants_base - An instance of ActiveRecord::Relation for which to
# get child objects. If omitted, ancestors_base is used.
- def initialize(ancestors_base, descendants_base = ancestors_base)
+ def initialize(ancestors_base, descendants_base = ancestors_base, options: {})
raise ArgumentError.new("Model of ancestors_base does not match model of descendants_base") if ancestors_base.model != descendants_base.model
@ancestors_base = ancestors_base
@descendants_base = descendants_base
@model = ancestors_base.model
+ @options = options
end
# Returns the set of descendants of a given relation, but excluding the given
@@ -133,8 +134,8 @@ module Gitlab
# Recursively get all the ancestors of the base set.
parent_query = model
- .from([objects_table, cte.table])
- .where(objects_table[:id].eq(cte.table[:parent_id]))
+ .from(from_tables(cte))
+ .where(ancestor_conditions(cte))
.except(:order)
if hierarchy_order
@@ -148,7 +149,7 @@ module Gitlab
).where(cte.table[:tree_cycle].eq(false))
end
- parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id
+ parent_query = parent_query.where(parent_id_column(cte).not_eq(stop_id)) if stop_id
cte << parent_query
cte
@@ -166,8 +167,8 @@ module Gitlab
# Recursively get all the descendants of the base set.
descendants_query = model
- .from([objects_table, cte.table])
- .where(objects_table[:parent_id].eq(cte.table[:id]))
+ .from(from_tables(cte))
+ .where(descendant_conditions(cte))
.except(:order)
if with_depth
@@ -190,6 +191,22 @@ module Gitlab
model.arel_table
end
+ def parent_id_column(cte)
+ cte.table[:parent_id]
+ end
+
+ def from_tables(cte)
+ [objects_table, cte.table]
+ end
+
+ def ancestor_conditions(cte)
+ objects_table[:id].eq(cte.table[:parent_id])
+ end
+
+ def descendant_conditions(cte)
+ objects_table[:parent_id].eq(cte.table[:id])
+ end
+
def read_only(relation)
# relations using a CTE are not safe to use with update_all as it will
# throw away the CTE, hence we mark them as read-only.
diff --git a/lib/gitlab/pages/settings.rb b/lib/gitlab/pages/settings.rb
index e3dbeee7b13..8650a80a85e 100644
--- a/lib/gitlab/pages/settings.rb
+++ b/lib/gitlab/pages/settings.rb
@@ -6,12 +6,8 @@ module Gitlab
DiskAccessDenied = Class.new(StandardError)
def path
- if ::Gitlab::Runtime.web_server? && ENV['GITLAB_PAGES_DENY_DISK_ACCESS'] == '1'
- begin
- raise DiskAccessDenied
- rescue DiskAccessDenied => ex
- ::Gitlab::ErrorTracking.track_exception(ex)
- end
+ if ::Gitlab::Runtime.web_server? && !::Gitlab::Runtime.test_suite?
+ raise DiskAccessDenied
end
super
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
index a70dc826f97..8ae0ec5a78a 100644
--- a/lib/gitlab/pages_transfer.rb
+++ b/lib/gitlab/pages_transfer.rb
@@ -1,7 +1,26 @@
# frozen_string_literal: true
+# To make a call happen in a new Sidekiq job, add `.async` before the call. For
+# instance:
+#
+# PagesTransfer.new.async.move_namespace(...)
+#
module Gitlab
class PagesTransfer < ProjectTransfer
+ class Async
+ METHODS = %w[move_namespace move_project rename_project rename_namespace].freeze
+
+ METHODS.each do |meth|
+ define_method meth do |*args|
+ PagesTransferWorker.perform_async(meth, args)
+ end
+ end
+ end
+
+ def async
+ @async ||= Async.new
+ end
+
def root_dir
Gitlab.config.pages.path
end
diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb
index ff90a009b2e..23e380b3cf1 100644
--- a/lib/gitlab/project_authorizations.rb
+++ b/lib/gitlab/project_authorizations.rb
@@ -99,6 +99,7 @@ module Gitlab
.and(members[:source_type].eq('Namespace'))
.and(members[:requested_at].eq(nil))
.and(members[:user_id].eq(user.id))
+ .and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS))
Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond))
end
@@ -119,6 +120,7 @@ module Gitlab
.and(members[:source_type].eq('Namespace'))
.and(members[:requested_at].eq(nil))
.and(members[:user_id].eq(user.id))
+ .and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS))
Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond))
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index f8141278e48..ded8d4ade3f 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -4,11 +4,11 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
- def initialize(current_user, project, query, repository_ref = nil)
- @current_user = current_user
+ def initialize(current_user, query, project:, repository_ref: nil, filters: {})
@project = project
@repository_ref = repository_ref.presence
- @query = query
+
+ super(current_user, query, [project], filters: filters)
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
diff --git a/lib/gitlab/prometheus/internal.rb b/lib/gitlab/prometheus/internal.rb
index d59352119ba..c2f4035821e 100644
--- a/lib/gitlab/prometheus/internal.rb
+++ b/lib/gitlab/prometheus/internal.rb
@@ -25,6 +25,10 @@ module Gitlab
end
end
+ def self.server_address
+ uri&.strip&.sub(/^http[s]?:\/\//, '')
+ end
+
def self.listen_address
Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus
rescue Settingslogic::MissingSetting
diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb
index 56e1154a672..965349ad711 100644
--- a/lib/gitlab/prometheus_client.rb
+++ b/lib/gitlab/prometheus_client.rb
@@ -42,6 +42,15 @@ module Gitlab
response_body == HEALTHY_RESPONSE
end
+ def ready?
+ response = get(ready_url, {})
+
+ # From Prometheus docs: This endpoint returns 200 when Prometheus is ready to serve traffic (i.e. respond to queries).
+ response.code == 200
+ rescue => e
+ raise PrometheusClient::UnexpectedResponseError, "#{e.message}"
+ end
+
def proxy(type, args)
path = api_path(type)
get(path, args)
@@ -103,7 +112,11 @@ module Gitlab
end
def health_url
- [api_url, '-/healthy'].join('/')
+ "#{api_url}/-/healthy"
+ end
+
+ def ready_url
+ "#{api_url}/-/ready"
end
private
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index cd07122ffd9..dd7a27ead01 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -9,6 +9,62 @@ module Gitlab
# extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
# ```
class Extractor
+ CODE_REGEX = %r{
+ (?<code>
+ # Code blocks:
+ # ```
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # ```
+
+ ^```
+ .+?
+ \n```$
+ )
+ }mix.freeze
+
+ INLINE_CODE_REGEX = %r{
+ (?<inline_code>
+ # Inline code on separate rows:
+ # `
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # `
+
+ ^.*`\n*
+ .+?
+ \n*`$
+ )
+ }mix.freeze
+
+ HTML_BLOCK_REGEX = %r{
+ (?<html>
+ # HTML block:
+ # <tag>
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # </tag>
+
+ ^<[^>]+?>\n
+ .+?
+ \n<\/[^>]+?>$
+ )
+ }mix.freeze
+
+ QUOTE_BLOCK_REGEX = %r{
+ (?<html>
+ # Quote block:
+ # >>>
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # >>>
+
+ ^>>>
+ .+?
+ \n>>>$
+ )
+ }mix.freeze
+
+ EXCLUSION_REGEX = %r{
+ #{CODE_REGEX} | #{INLINE_CODE_REGEX} | #{HTML_BLOCK_REGEX} | #{QUOTE_BLOCK_REGEX}
+ }mix.freeze
+
attr_reader :command_definitions
def initialize(command_definitions)
@@ -35,9 +91,7 @@ module Gitlab
def extract_commands(content, only: nil)
return [content, []] unless content
- content, commands = perform_regex(content, only: only)
-
- perform_substitutions(content, commands)
+ perform_regex(content, only: only)
end
# Encloses quick action commands into code span markdown
@@ -55,13 +109,19 @@ module Gitlab
private
def perform_regex(content, only: nil, redact: false)
- commands = []
- content = content.dup
+ names = command_names(limit_to_commands: only).map(&:to_s)
+ sub_names = substitution_names.map(&:to_s)
+ commands = []
+ content = content.dup
content.delete!("\r")
- names = command_names(limit_to_commands: only).map(&:to_s)
- content.gsub!(commands_regex(names: names)) do
- command, output = process_commands($~, redact)
+ content.gsub!(commands_regex(names: names, sub_names: sub_names)) do
+ command, output = if $~[:substitution]
+ process_substitutions($~)
+ else
+ process_commands($~, redact)
+ end
+
commands << command
output
end
@@ -86,6 +146,21 @@ module Gitlab
[command, output]
end
+ def process_substitutions(matched_text)
+ output = matched_text[0]
+ command = []
+
+ if matched_text[:substitution]
+ cmd = matched_text[:substitution].downcase
+ command = [cmd, matched_text[:arg]].reject(&:blank?)
+
+ substitution = substitution_definitions.find { |definition| definition.all_names.include?(cmd.to_sym) }
+ output = substitution.perform_substitution(self, output) if substitution
+ end
+
+ [command, output]
+ end
+
# Builds a regular expression to match known commands.
# First match group captures the command name and
# second match group captures its arguments.
@@ -93,51 +168,9 @@ module Gitlab
# It looks something like:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
- def commands_regex(names:)
+ def commands_regex(names:, sub_names:)
@commands_regex[names] ||= %r{
- (?<code>
- # Code blocks:
- # ```
- # Anything, including `/cmd arg` which are ignored by this filter
- # ```
-
- ^```
- .+?
- \n```$
- )
- |
- (?<inline_code>
- # Inline code on separate rows:
- # `
- # Anything, including `/cmd arg` which are ignored by this filter
- # `
-
- ^.*`\n*
- .+?
- \n*`$
- )
- |
- (?<html>
- # HTML block:
- # <tag>
- # Anything, including `/cmd arg` which are ignored by this filter
- # </tag>
-
- ^<[^>]+?>\n
- .+?
- \n<\/[^>]+?>$
- )
- |
- (?<html>
- # Quote block:
- # >>>
- # Anything, including `/cmd arg` which are ignored by this filter
- # >>>
-
- ^>>>
- .+?
- \n>>>$
- )
+ #{EXCLUSION_REGEX}
|
(?:
# Command not in a blockquote, blockcode, or HTML tag:
@@ -151,32 +184,19 @@ module Gitlab
)?
(?:\s*\n|$)
)
- }mix
- end
-
- def perform_substitutions(content, commands)
- return unless content
-
- substitution_definitions = self.command_definitions.select do |definition|
- definition.is_a?(Gitlab::QuickActions::SubstitutionDefinition)
- end
-
- substitution_definitions.each do |substitution|
- regex = commands_regex(names: substitution.all_names)
- content = content.gsub(regex) do |text|
- if $~[:cmd]
- command = [substitution.name.to_s]
- command << $~[:arg] if $~[:arg].present?
- commands << command
-
- substitution.perform_substitution(self, text)
- else
- text
- end
- end
- end
+ |
+ (?:
+ # Substitution not in a blockquote, blockcode, or HTML tag:
- [content, commands]
+ ^\/
+ (?<substitution>#{Regexp.new(Regexp.union(sub_names).source, Regexp::IGNORECASE)})
+ (?:
+ [ ]
+ (?<arg>[^\n]*)
+ )?
+ (?:\s*\n|$)
+ )
+ }mix
end
def command_names(limit_to_commands:)
@@ -190,6 +210,17 @@ module Gitlab
command.all_names
end.compact
end
+
+ def substitution_names
+ substitution_definitions.flat_map { |command| command.all_names }
+ .compact
+ end
+
+ def substitution_definitions
+ @substition_definitions ||= command_definitions.select do |command|
+ command.is_a?(Gitlab::QuickActions::SubstitutionDefinition)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index aff3ed53734..6607c73a5c3 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -91,6 +91,7 @@ module Gitlab
params '%"milestone"'
types Issue, MergeRequest
condition do
+ quick_action_target.supports_milestone? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) &&
find_milestones(project, state: 'active').any?
end
@@ -113,6 +114,7 @@ module Gitlab
condition do
quick_action_target.persisted? &&
quick_action_target.milestone_id? &&
+ quick_action_target.supports_milestone? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
end
command :remove_milestone do
diff --git a/lib/gitlab/quick_actions/relate_actions.rb b/lib/gitlab/quick_actions/relate_actions.rb
new file mode 100644
index 00000000000..95f71214667
--- /dev/null
+++ b/lib/gitlab/quick_actions/relate_actions.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module QuickActions
+ module RelateActions
+ extend ActiveSupport::Concern
+ include ::Gitlab::QuickActions::Dsl
+
+ included do
+ desc _('Mark this issue as related to another issue')
+ explanation do |related_reference|
+ _('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
+ end
+ execution_message do |related_reference|
+ _('Marked this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
+ end
+ params '#issue'
+ types Issue
+ condition do
+ quick_action_target.persisted? &&
+ current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+ end
+ command :relate do |related_param|
+ IssueLinks::CreateService.new(quick_action_target, current_user, { issuable_references: [related_param] }).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/quick_actions/substitution_definition.rb b/lib/gitlab/quick_actions/substitution_definition.rb
index cd4d202e8d0..24b4e3c62b3 100644
--- a/lib/gitlab/quick_actions/substitution_definition.rb
+++ b/lib/gitlab/quick_actions/substitution_definition.rb
@@ -9,10 +9,6 @@ module Gitlab
true
end
- def match(content)
- content.match %r{^/#{all_names.join('|')}(?![\S]) ?(.*)$}
- end
-
def perform_substitution(context, content)
return unless content
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index 8ab53700932..2848c9f0b59 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -9,7 +9,7 @@ module Gitlab
SESSION_NAMESPACE = 'session:gitlab'
USER_SESSIONS_NAMESPACE = 'session:user:gitlab'
USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'
- IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab'
+ IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2'
DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'
REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'
diff --git a/lib/gitlab/reference_counter.rb b/lib/gitlab/reference_counter.rb
index 5fdfa5e75ed..c2fa2e1330a 100644
--- a/lib/gitlab/reference_counter.rb
+++ b/lib/gitlab/reference_counter.rb
@@ -51,10 +51,8 @@ module Gitlab
redis_cmd do |redis|
current_value = redis.decr(key)
if current_value < 0
- # rubocop:disable Gitlab/RailsLogger
- Rails.logger.warn("Reference counter for #{gl_repository} decreased" \
+ Gitlab::AppLogger.warn("Reference counter for #{gl_repository} decreased" \
" when its value was less than 1. Resetting the counter.")
- # rubocop:enable Gitlab/RailsLogger
redis.del(key)
end
end
@@ -87,7 +85,7 @@ module Gitlab
true
rescue => e
- Rails.logger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}")
false
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 2d625737e05..8e23ac6aca5 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -46,6 +46,21 @@ module Gitlab
maven_app_name_regex
end
+ def pypi_version_regex
+ # See the official regex: https://github.com/pypa/packaging/blob/16.7/packaging/version.py#L159
+
+ @pypi_version_regex ||= %r{
+ \A(?:
+ v?
+ (?:([0-9]+)!)? (?# epoch)
+ ([0-9]+(?:\.[0-9]+)*) (?# release segment)
+ ([-_\.]?((a|b|c|rc|alpha|beta|pre|preview))[-_\.]?([0-9]+)?)? (?# pre-release)
+ ((?:-([0-9]+))|(?:[-_\.]?(post|rev|r)[-_\.]?([0-9]+)?))? (?# post release)
+ ([-_\.]?(dev)[-_\.]?([0-9]+)?)? (?# dev release)
+ (?:\+([a-z0-9]+(?:[-_\.][a-z0-9]+)*))? (?# local version)
+ )\z}xi.freeze
+ end
+
def unbounded_semver_regex
# See the official regex: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
@@ -84,6 +99,10 @@ module Gitlab
\b (?# word boundary)
/ix.freeze
end
+
+ def generic_package_version_regex
+ /\A\d+\.\d+\.\d+\z/
+ end
end
extend self
@@ -102,7 +121,11 @@ module Gitlab
end
def group_name_regex
- @group_name_regex ||= /\A[\p{Alnum}\u{00A9}-\u{1f9ff}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9ff}_()\. ]*\z/.freeze
+ @group_name_regex ||= /\A#{group_name_regex_chars}\z/.freeze
+ end
+
+ def group_name_regex_chars
+ @group_name_regex_chars ||= /[\p{Alnum}\u{00A9}-\u{1f9ff}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9ff}_()\. ]*/.freeze
end
def group_name_regex_message
@@ -269,7 +292,14 @@ module Gitlab
def base64_regex
@base64_regex ||= /(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?/.freeze
end
+
+ def feature_flag_regex
+ /\A[a-z]([-_a-z0-9]*[a-z0-9])?\z/
+ end
+
+ def feature_flag_regex_message
+ "can contain only lowercase letters, digits, '_' and '-'. " \
+ "Must start with a letter, and cannot end with '-' or '_'"
+ end
end
end
-
-Gitlab::Regex.prepend_if_ee('EE::Gitlab::Regex')
diff --git a/lib/gitlab/relative_positioning.rb b/lib/gitlab/relative_positioning.rb
new file mode 100644
index 00000000000..b5a923f0824
--- /dev/null
+++ b/lib/gitlab/relative_positioning.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RelativePositioning
+ STEPS = 10
+ IDEAL_DISTANCE = 2**(STEPS - 1) + 1
+
+ MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
+ START_POSITION = 0
+ MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
+
+ MAX_GAP = IDEAL_DISTANCE * 2
+ MIN_GAP = 2
+
+ NoSpaceLeft = Class.new(StandardError)
+ end
+end
diff --git a/lib/gitlab/relative_positioning/gap.rb b/lib/gitlab/relative_positioning/gap.rb
new file mode 100644
index 00000000000..ab894141a60
--- /dev/null
+++ b/lib/gitlab/relative_positioning/gap.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+#
+module Gitlab
+ module RelativePositioning
+ class Gap
+ attr_reader :start_pos, :end_pos
+
+ def initialize(start_pos, end_pos)
+ @start_pos, @end_pos = start_pos, end_pos
+ end
+
+ def ==(other)
+ other.is_a?(self.class) && other.start_pos == start_pos && other.end_pos == end_pos
+ end
+
+ def delta
+ ((start_pos - end_pos) / 2.0).abs.ceil.clamp(0, RelativePositioning::IDEAL_DISTANCE)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/relative_positioning/item_context.rb b/lib/gitlab/relative_positioning/item_context.rb
new file mode 100644
index 00000000000..cd03a347355
--- /dev/null
+++ b/lib/gitlab/relative_positioning/item_context.rb
@@ -0,0 +1,259 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RelativePositioning
+ # This class is API private - it should not be explicitly instantiated
+ # outside of tests
+ # rubocop: disable CodeReuse/ActiveRecord
+ class ItemContext
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :object, :model_class, :range
+ attr_accessor :ignoring
+
+ def initialize(object, range, ignoring: nil)
+ @object = object
+ @range = range
+ @model_class = object.class
+ @ignoring = ignoring
+ end
+
+ def ==(other)
+ other.is_a?(self.class) && other.object == object && other.range == range && other.ignoring == ignoring
+ end
+
+ def positioned?
+ relative_position.present?
+ end
+
+ def min_relative_position
+ strong_memoize(:min_relative_position) { calculate_relative_position('MIN') }
+ end
+
+ def max_relative_position
+ strong_memoize(:max_relative_position) { calculate_relative_position('MAX') }
+ end
+
+ def prev_relative_position
+ calculate_relative_position('MAX') { |r| nextify(r, false) } if object.relative_position
+ end
+
+ def next_relative_position
+ calculate_relative_position('MIN') { |r| nextify(r) } if object.relative_position
+ end
+
+ def nextify(relation, gt = true)
+ if gt
+ relation.where("relative_position > ?", relative_position)
+ else
+ relation.where("relative_position < ?", relative_position)
+ end
+ end
+
+ def relative_siblings(relation = scoped_items)
+ object.exclude_self(relation)
+ end
+
+ # Handles the possibility that the position is already occupied by a sibling
+ def place_at_position(position, lhs)
+ current_occupant = relative_siblings.find_by(relative_position: position)
+
+ if current_occupant.present?
+ Mover.new(position, range).move(object, lhs.object, current_occupant)
+ else
+ object.relative_position = position
+ end
+ end
+
+ def lhs_neighbour
+ scoped_items
+ .where('relative_position < ?', relative_position)
+ .reorder(relative_position: :desc)
+ .first
+ .then { |x| neighbour(x) }
+ end
+
+ def rhs_neighbour
+ scoped_items
+ .where('relative_position > ?', relative_position)
+ .reorder(relative_position: :asc)
+ .first
+ .then { |x| neighbour(x) }
+ end
+
+ def neighbour(item)
+ return unless item.present?
+
+ self.class.new(item, range, ignoring: ignoring)
+ end
+
+ def scoped_items
+ r = model_class.relative_positioning_query_base(object)
+ r = object.exclude_self(r, excluded: ignoring) if ignoring.present?
+ r
+ end
+
+ def calculate_relative_position(calculation)
+ # When calculating across projects, this is much more efficient than
+ # MAX(relative_position) without the GROUP BY, due to index usage:
+ # https://gitlab.com/gitlab-org/gitlab-foss/issues/54276#note_119340977
+ relation = scoped_items
+ .order(Gitlab::Database.nulls_last_order('position', 'DESC'))
+ .group(grouping_column)
+ .limit(1)
+
+ relation = yield relation if block_given?
+
+ relation
+ .pluck(grouping_column, Arel.sql("#{calculation}(relative_position) AS position"))
+ .first&.last
+ end
+
+ def grouping_column
+ model_class.relative_positioning_parent_column
+ end
+
+ def max_sibling
+ sib = relative_siblings
+ .order(Gitlab::Database.nulls_last_order('relative_position', 'DESC'))
+ .first
+
+ neighbour(sib)
+ end
+
+ def min_sibling
+ sib = relative_siblings
+ .order(Gitlab::Database.nulls_last_order('relative_position', 'ASC'))
+ .first
+
+ neighbour(sib)
+ end
+
+ def shift_left
+ move_sequence_before(true)
+ object.reset
+ end
+
+ def shift_right
+ move_sequence_after(true)
+ object.reset
+ end
+
+ def create_space_left
+ find_next_gap_before.tap { |gap| move_sequence_before(false, next_gap: gap) }
+ end
+
+ def create_space_right
+ find_next_gap_after.tap { |gap| move_sequence_after(false, next_gap: gap) }
+ end
+
+ def find_next_gap_before
+ items_with_next_pos = scoped_items
+ .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
+ .where('relative_position <= ?', relative_position)
+ .order(relative_position: :desc)
+
+ find_next_gap(items_with_next_pos, range.first)
+ end
+
+ def find_next_gap_after
+ items_with_next_pos = scoped_items
+ .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
+ .where('relative_position >= ?', relative_position)
+ .order(:relative_position)
+
+ find_next_gap(items_with_next_pos, range.last)
+ end
+
+ def find_next_gap(items_with_next_pos, default_end)
+ gap = model_class
+ .from(items_with_next_pos, :items)
+ .where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
+ .limit(1)
+ .pluck(:pos, :next_pos)
+ .first
+
+ return if gap.nil? || gap.first == default_end
+
+ Gap.new(gap.first, gap.second || default_end)
+ end
+
+ def relative_position
+ object.relative_position
+ end
+
+ private
+
+ # Moves the sequence before the current item to the middle of the next gap
+ # For example, we have
+ #
+ # 5 . . . . . 11 12 13 14 [15] 16 . 17
+ # -----------
+ #
+ # This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
+ #
+ # 5 . . 8 9 10 11 . . . [15] 16 . 17
+ # ---------
+ #
+ # Creating a gap to the left of the current item. We can understand this as
+ # dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
+ #
+ # If `include_self` is true, the current item will also be moved, creating a
+ # gap to the right of the current item:
+ #
+ # 5 . . 8 9 10 11 [14] . . . 16 . 17
+ # --------------
+ #
+ # As an optimization, the gap can be precalculated and passed to this method.
+ #
+ # @api private
+ # @raises NoSpaceLeft if the sequence cannot be moved
+ def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
+ raise NoSpaceLeft unless next_gap.present?
+
+ delta = next_gap.delta
+
+ move_sequence(next_gap.start_pos, relative_position, -delta, include_self)
+ end
+
+ # Moves the sequence after the current item to the middle of the next gap
+ # For example, we have:
+ #
+ # 8 . 10 [11] 12 13 14 15 . . . . . 21
+ # -----------
+ #
+ # This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
+ #
+ # 8 . 10 [11] . . . 15 16 17 18 . . 21
+ # -----------
+ #
+ # Creating a gap to the right of the current item. We can understand this as
+ # dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
+ #
+ # If `include_self` is true, the current item will also be moved, creating a
+ # gap to the left of the current item:
+ #
+ # 8 . 10 . . . [14] 15 16 17 18 . . 21
+ # ----------------
+ #
+ # As an optimization, the gap can be precalculated and passed to this method.
+ #
+ # @api private
+ # @raises NoSpaceLeft if the sequence cannot be moved
+ def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
+ raise NoSpaceLeft unless next_gap.present?
+
+ delta = next_gap.delta
+
+ move_sequence(relative_position, next_gap.start_pos, delta, include_self)
+ end
+
+ def move_sequence(start_pos, end_pos, delta, include_self = false)
+ relation = include_self ? scoped_items : relative_siblings
+
+ object.update_relative_siblings(relation, (start_pos..end_pos), delta)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/lib/gitlab/relative_positioning/mover.rb b/lib/gitlab/relative_positioning/mover.rb
new file mode 100644
index 00000000000..9d891bfbe3b
--- /dev/null
+++ b/lib/gitlab/relative_positioning/mover.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RelativePositioning
+ class Mover
+ attr_reader :range, :start_position
+
+ def initialize(start, range)
+ @range = range
+ @start_position = start
+ end
+
+ def move_to_end(object)
+ focus = context(object, ignoring: object)
+ max_pos = focus.max_relative_position
+
+ move_to_range_end(focus, max_pos)
+ end
+
+ def move_to_start(object)
+ focus = context(object, ignoring: object)
+ min_pos = focus.min_relative_position
+
+ move_to_range_start(focus, min_pos)
+ end
+
+ def move(object, first, last)
+ raise ArgumentError, 'object is required' unless object
+
+ lhs = context(first, ignoring: object)
+ rhs = context(last, ignoring: object)
+ focus = context(object)
+ range = RelativePositioning.range(lhs, rhs)
+
+ if range.cover?(focus)
+ # Moving a object already within a range is a no-op
+ elsif range.open_on_left?
+ move_to_range_start(focus, range.rhs.relative_position)
+ elsif range.open_on_right?
+ move_to_range_end(focus, range.lhs.relative_position)
+ else
+ pos_left, pos_right = create_space_between(range)
+ desired_position = position_between(pos_left, pos_right)
+ focus.place_at_position(desired_position, range.lhs)
+ end
+ end
+
+ def context(object, ignoring: nil)
+ return unless object
+
+ ItemContext.new(object, range, ignoring: ignoring)
+ end
+
+ private
+
+ def gap_too_small?(pos_a, pos_b)
+ return false unless pos_a && pos_b
+
+ (pos_a - pos_b).abs < MIN_GAP
+ end
+
+ def move_to_range_end(context, max_pos)
+ range_end = range.last + 1
+
+ new_pos = if max_pos.nil?
+ start_position
+ elsif gap_too_small?(max_pos, range_end)
+ max = context.max_sibling
+ max.ignoring = context.object
+ max.shift_left
+ position_between(max.relative_position, range_end)
+ else
+ position_between(max_pos, range_end)
+ end
+
+ context.object.relative_position = new_pos
+ end
+
+ def move_to_range_start(context, min_pos)
+ range_end = range.first - 1
+
+ new_pos = if min_pos.nil?
+ start_position
+ elsif gap_too_small?(min_pos, range_end)
+ sib = context.min_sibling
+ sib.ignoring = context.object
+ sib.shift_right
+ position_between(sib.relative_position, range_end)
+ else
+ position_between(min_pos, range_end)
+ end
+
+ context.object.relative_position = new_pos
+ end
+
+ def create_space_between(range)
+ pos_left = range.lhs&.relative_position
+ pos_right = range.rhs&.relative_position
+
+ return [pos_left, pos_right] unless gap_too_small?(pos_left, pos_right)
+
+ gap = range.rhs.create_space_left
+ [pos_left - gap.delta, pos_right]
+ rescue NoSpaceLeft
+ gap = range.lhs.create_space_right
+ [pos_left, pos_right + gap.delta]
+ end
+
+ # This method takes two integer values (positions) and
+ # calculates the position between them. The range is huge as
+ # the maximum integer value is 2147483647.
+ #
+ # We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
+ #
+ # Then we handle one of three cases:
+ # - If the gap is too small, we raise NoSpaceLeft
+ # - If the gap is larger than MAX_GAP, we place the new position at most
+ # IDEAL_DISTANCE from the edge of the gap.
+ # - otherwise we place the new position at the midpoint.
+ #
+ # The new position will always satisfy: pos_before <= midpoint <= pos_after
+ #
+ # As a precondition, the gap between pos_before and pos_after MUST be >= 2.
+ # If the gap is too small, NoSpaceLeft is raised.
+ #
+ # @raises NoSpaceLeft
+ def position_between(pos_before, pos_after)
+ pos_before ||= range.first
+ pos_after ||= range.last
+
+ pos_before, pos_after = [pos_before, pos_after].sort
+
+ gap_width = pos_after - pos_before
+
+ if gap_too_small?(pos_before, pos_after)
+ raise NoSpaceLeft
+ elsif gap_width > MAX_GAP
+ if pos_before <= range.first
+ pos_after - IDEAL_DISTANCE
+ elsif pos_after >= range.last
+ pos_before + IDEAL_DISTANCE
+ else
+ midpoint(pos_before, pos_after)
+ end
+ else
+ midpoint(pos_before, pos_after)
+ end
+ end
+
+ def midpoint(lower_bound, upper_bound)
+ ((lower_bound + upper_bound) / 2.0).ceil.clamp(lower_bound, upper_bound - 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/relative_positioning/range.rb b/lib/gitlab/relative_positioning/range.rb
new file mode 100644
index 00000000000..174d5ef4b35
--- /dev/null
+++ b/lib/gitlab/relative_positioning/range.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RelativePositioning
+ IllegalRange = Class.new(ArgumentError)
+
+ class Range
+ attr_reader :lhs, :rhs
+
+ def open_on_left?
+ lhs.nil?
+ end
+
+ def open_on_right?
+ rhs.nil?
+ end
+
+ def cover?(item_context)
+ return false unless item_context
+ return false unless item_context.positioned?
+ return true if item_context.object == lhs&.object
+ return true if item_context.object == rhs&.object
+
+ pos = item_context.relative_position
+
+ return lhs.relative_position < pos if open_on_right?
+ return pos < rhs.relative_position if open_on_left?
+
+ lhs.relative_position < pos && pos < rhs.relative_position
+ end
+
+ def ==(other)
+ other.is_a?(RelativePositioning::Range) && lhs == other.lhs && rhs == other.rhs
+ end
+ end
+
+ def self.range(lhs, rhs)
+ if lhs && rhs
+ ClosedRange.new(lhs, rhs)
+ elsif lhs
+ StartingFrom.new(lhs)
+ elsif rhs
+ EndingAt.new(rhs)
+ else
+ raise IllegalRange, 'One of rhs or lhs must be provided' unless lhs && rhs
+ end
+ end
+
+ class ClosedRange < RelativePositioning::Range
+ def initialize(lhs, rhs)
+ @lhs, @rhs = lhs, rhs
+ raise IllegalRange, 'Either lhs or rhs is missing' unless lhs && rhs
+ raise IllegalRange, 'lhs and rhs cannot be the same object' if lhs == rhs
+ end
+ end
+
+ class StartingFrom < RelativePositioning::Range
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(lhs)
+ @lhs = lhs
+ raise IllegalRange, 'lhs is required' unless lhs
+ end
+
+ def rhs
+ strong_memoize(:rhs) { lhs.rhs_neighbour }
+ end
+ end
+
+ class EndingAt < RelativePositioning::Range
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(rhs)
+ @rhs = rhs
+ raise IllegalRange, 'rhs is required' unless rhs
+ end
+
+ def lhs
+ strong_memoize(:lhs) { rhs.lhs_neighbour }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb
index f6a5c6ed754..eb7c9bccf96 100644
--- a/lib/gitlab/repository_cache_adapter.rb
+++ b/lib/gitlab/repository_cache_adapter.rb
@@ -218,7 +218,7 @@ module Gitlab
def expire_method_caches(methods)
methods.each do |name|
unless cached_methods.include?(name.to_sym)
- Rails.logger.error "Requested to expire non-existent method '#{name}' for Repository" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "Requested to expire non-existent method '#{name}' for Repository"
next
end
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
index 1e2d86b7ad2..69c1688767c 100644
--- a/lib/gitlab/repository_set_cache.rb
+++ b/lib/gitlab/repository_set_cache.rb
@@ -22,7 +22,7 @@ module Gitlab
with do |redis|
redis.multi do
- redis.del(full_key)
+ redis.unlink(full_key)
# Splitting into groups of 1000 prevents us from creating a too-long
# Redis command
diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb
index dd1482da40d..541d505e735 100644
--- a/lib/gitlab/request_profiler.rb
+++ b/lib/gitlab/request_profiler.rb
@@ -11,7 +11,7 @@ module Gitlab
Profile.new(File.basename(path))
end.select(&:valid?)
end
- module_function :all
+ module_function :all # rubocop: disable Style/AccessModifierDeclarations
def find(name)
file_path = File.join(PROFILES_DIR, name)
@@ -19,18 +19,18 @@ module Gitlab
Profile.new(name)
end
- module_function :find
+ module_function :find # rubocop: disable Style/AccessModifierDeclarations
def profile_token
Rails.cache.fetch('profile-token') do
Devise.friendly_token
end
end
- module_function :profile_token
+ module_function :profile_token # rubocop: disable Style/AccessModifierDeclarations
def remove_all_profiles
FileUtils.rm_rf(PROFILES_DIR)
end
- module_function :remove_all_profiles
+ module_function :remove_all_profiles # rubocop: disable Style/AccessModifierDeclarations
end
end
diff --git a/lib/gitlab/robots_txt.rb b/lib/gitlab/robots_txt.rb
new file mode 100644
index 00000000000..2f395548770
--- /dev/null
+++ b/lib/gitlab/robots_txt.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RobotsTxt
+ def self.disallowed?(path)
+ parsed_robots_txt.disallowed?(path)
+ end
+
+ def self.parsed_robots_txt
+ @parsed_robots_txt ||= Parser.new(robots_txt)
+ end
+
+ def self.robots_txt
+ File.read(Rails.root.join('public', 'robots.txt'))
+ end
+ end
+end
diff --git a/lib/gitlab/robots_txt/parser.rb b/lib/gitlab/robots_txt/parser.rb
new file mode 100644
index 00000000000..b9a3837e468
--- /dev/null
+++ b/lib/gitlab/robots_txt/parser.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RobotsTxt
+ class Parser
+ attr_reader :disallow_rules
+
+ def initialize(content)
+ @raw_content = content
+
+ @disallow_rules = parse_raw_content!
+ end
+
+ def disallowed?(path)
+ disallow_rules.any? { |rule| path =~ rule }
+ end
+
+ private
+
+ # This parser is very basic as it only knows about `Disallow:` lines,
+ # and simply ignores all other lines.
+ #
+ # Order of predecence, 'Allow:`, etc are ignored for now.
+ def parse_raw_content!
+ @raw_content.each_line.map do |line|
+ if line.start_with?('Disallow:')
+ value = line.sub('Disallow:', '').strip
+ value = Regexp.escape(value).gsub('\*', '.*')
+ Regexp.new("^#{value}")
+ else
+ nil
+ end
+ end.compact
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sanitizers/exif.rb b/lib/gitlab/sanitizers/exif.rb
index 7e22bf4d7df..78c517c49d8 100644
--- a/lib/gitlab/sanitizers/exif.rb
+++ b/lib/gitlab/sanitizers/exif.rb
@@ -48,7 +48,7 @@ module Gitlab
attr_reader :logger
- def initialize(logger: Rails.logger) # rubocop:disable Gitlab/RailsLogger
+ def initialize(logger: Gitlab::AppLogger)
@logger = logger
end
diff --git a/lib/gitlab/search/recent_issues.rb b/lib/gitlab/search/recent_issues.rb
new file mode 100644
index 00000000000..413218da64d
--- /dev/null
+++ b/lib/gitlab/search/recent_issues.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Search
+ class RecentIssues < RecentItems
+ private
+
+ def type
+ Issue
+ end
+
+ def finder
+ IssuesFinder
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search/recent_items.rb b/lib/gitlab/search/recent_items.rb
new file mode 100644
index 00000000000..40d96ded275
--- /dev/null
+++ b/lib/gitlab/search/recent_items.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Search
+ # This is an abstract class used for storing/searching recently viewed
+ # items. The #type and #finder methods are the only ones needed to be
+ # implemented by classes inheriting from this.
+ class RecentItems
+ ITEMS_LIMIT = 100
+ EXPIRES_AFTER = 7.days
+
+ def initialize(user:, items_limit: ITEMS_LIMIT, expires_after: EXPIRES_AFTER)
+ @user = user
+ @items_limit = items_limit
+ @expires_after = expires_after
+ end
+
+ def log_view(item)
+ with_redis do |redis|
+ redis.zadd(key, Time.now.to_f, item.id)
+ redis.expire(key, @expires_after)
+
+ # There is a race condition here where we could end up removing an
+ # item from 2 places concurrently but this is fine since worst case
+ # scenario we remove an extra item from the end of the list.
+ if redis.zcard(key) > @items_limit
+ redis.zremrangebyrank(key, 0, 0) # Remove least recent
+ end
+ end
+ end
+
+ def search(term)
+ ids = with_redis do |redis|
+ redis.zrevrange(key, 0, @items_limit - 1)
+ end.map(&:to_i)
+
+ finder.new(@user, search: term, in: 'title').execute.reorder(nil).id_in_ordered(ids) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ private
+
+ def with_redis(&blk)
+ Gitlab::Redis::SharedState.with(&blk) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def key
+ "recent_items:#{type.name.downcase}:#{@user.id}"
+ end
+
+ def type
+ raise NotImplementedError
+ end
+
+ def finder
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search/recent_merge_requests.rb b/lib/gitlab/search/recent_merge_requests.rb
new file mode 100644
index 00000000000..7b14e3b33e5
--- /dev/null
+++ b/lib/gitlab/search/recent_merge_requests.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Search
+ class RecentMergeRequests < RecentItems
+ private
+
+ def type
+ MergeRequest
+ end
+
+ def finder
+ MergeRequestsFinder
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 3d5f64ce05b..06d8dca2f70 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -7,7 +7,7 @@ module Gitlab
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20
- attr_reader :current_user, :query
+ attr_reader :current_user, :query, :filters
# Limit search results by passed projects
# It allows us to search only for projects user has access to
@@ -19,11 +19,12 @@ module Gitlab
# query
attr_reader :default_project_filter
- def initialize(current_user, limit_projects, query, default_project_filter: false)
+ def initialize(current_user, query, limit_projects = nil, default_project_filter: false, filters: {})
@current_user = current_user
- @limit_projects = limit_projects || Project.all
@query = query
+ @limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter
+ @filters = filters
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true, preload_method: nil)
@@ -186,10 +187,12 @@ module Gitlab
params[:sort] = 'updated_desc'
if query =~ /#(\d+)\z/
- params[:iids] = $1
+ params[:iids] = Regexp.last_match(1)
else
params[:search] = query
end
+
+ params[:state] = filters[:state] if filters.key?(:state)
end
end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 64a30fbe16c..4df6a50c8dd 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -28,6 +28,25 @@ module Gitlab
end
# rubocop:enable Rails/Output
+ module Workhorse
+ extend Gitlab::SetupHelper
+ class << self
+ def configuration_toml(dir, _)
+ config = { redis: { URL: redis_url } }
+
+ TomlRB.dump(config)
+ end
+
+ def redis_url
+ Gitlab::Redis::SharedState.url
+ end
+
+ def get_config_path(dir)
+ File.join(dir, 'config.toml')
+ end
+ end
+ end
+
module Gitaly
extend Gitlab::SetupHelper
class << self
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 24f49f6b943..3419989c110 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -116,7 +116,7 @@ module Gitlab
true
rescue => e
- Rails.logger.warn("Repository does not exist: #{e} at: #{disk_path}.git") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn("Repository does not exist: #{e} at: #{disk_path}.git")
Gitlab::ErrorTracking.track_exception(e, path: disk_path, storage: storage)
false
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
index cbd89b7629f..6f1d2ad23c1 100644
--- a/lib/gitlab/sherlock/query.rb
+++ b/lib/gitlab/sherlock/query.rb
@@ -105,7 +105,7 @@ module Gitlab
query.each_line
.map { |line| line.strip }
.join("\n")
- .gsub(PREFIX_NEWLINE) { "\n#{$1} " }
+ .gsub(PREFIX_NEWLINE) { "\n#{Regexp.last_match(1)} " }
end
end
end
diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb
index b8a4eedd620..e1a87a77f04 100644
--- a/lib/gitlab/sidekiq_daemon/memory_killer.rb
+++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb
@@ -230,8 +230,10 @@ module Gitlab
end
def rss_increase_by_jobs
- Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord
- rss_increase_by_job(job)
+ Gitlab::SidekiqDaemon::Monitor.instance.jobs_mutex.synchronize do
+ Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord
+ rss_increase_by_job(job)
+ end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
index bb0c18735bb..6417ec20960 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
@@ -5,7 +5,7 @@ module Gitlab
module DuplicateJobs
class Client
def call(worker_class, job, queue, _redis_pool, &block)
- DuplicateJob.new(job, queue).schedule(&block)
+ ::Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(job, queue).schedule(&block)
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index 0dc53c61e84..5efd1b34d32 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -71,7 +71,7 @@ module Gitlab
end
def droppable?
- idempotent? && ::Feature.disabled?("disable_#{queue_name}_deduplication")
+ idempotent? && ::Feature.disabled?("disable_#{queue_name}_deduplication", type: :ops)
end
def scheduled_at
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb
index a08310a58ff..6fdef4c354e 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb
@@ -7,7 +7,8 @@ module Gitlab
UnknownStrategyError = Class.new(StandardError)
STRATEGIES = {
- until_executing: UntilExecuting
+ until_executing: UntilExecuting,
+ none: None
}.freeze
def self.for(name)
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none.rb
new file mode 100644
index 00000000000..cd101cd16b6
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module DuplicateJobs
+ module Strategies
+ # This strategy will never deduplicate a job
+ class None
+ def initialize(_duplicate_job)
+ end
+
+ def schedule(_job)
+ yield
+ end
+
+ def perform(_job)
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index 1d253ca90f3..41ec19f0da8 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -4,11 +4,8 @@ module Gitlab
class SnippetSearchResults < SearchResults
include SnippetsHelper
- attr_reader :current_user
-
def initialize(current_user, query)
- @current_user = current_user
- @query = query
+ super(current_user, query)
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
diff --git a/lib/gitlab/sourcegraph.rb b/lib/gitlab/sourcegraph.rb
index ec404ebd309..231d5aea129 100644
--- a/lib/gitlab/sourcegraph.rb
+++ b/lib/gitlab/sourcegraph.rb
@@ -12,8 +12,8 @@ module Gitlab
!feature.off?
end
- def feature_enabled?(thing = nil)
- feature.enabled?(thing)
+ def feature_enabled?(actor = nil)
+ feature.enabled?(actor)
end
private
diff --git a/lib/gitlab/sql/except.rb b/lib/gitlab/sql/except.rb
new file mode 100644
index 00000000000..82cbfa8d4ab
--- /dev/null
+++ b/lib/gitlab/sql/except.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SQL
+ # Class for building SQL EXCEPT statements.
+ #
+ # ORDER BYs are dropped from the relations as the final sort order is not
+ # guaranteed any way.
+ #
+ # Example usage:
+ #
+ # except = Gitlab::SQL::Except.new([user.projects, user.personal_projects])
+ # sql = except.to_sql
+ #
+ # Project.where("id IN (#{sql})")
+ class Except < SetOperator
+ def self.operator_keyword
+ 'EXCEPT'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/intersect.rb b/lib/gitlab/sql/intersect.rb
new file mode 100644
index 00000000000..c661db3d4c5
--- /dev/null
+++ b/lib/gitlab/sql/intersect.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SQL
+ # Class for building SQL INTERSECT statements.
+ #
+ # ORDER BYs are dropped from the relations as the final sort order is not
+ # guaranteed any way.
+ #
+ # Example usage:
+ #
+ # hierarchies = [group1.self_and_hierarchy, group2.self_and_hierarchy]
+ # intersect = Gitlab::SQL::Intersect.new(hierarchies)
+ # sql = intersect.to_sql
+ #
+ # Project.where("id IN (#{sql})")
+ class Intersect < SetOperator
+ def self.operator_keyword
+ 'INTERSECT'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/set_operator.rb b/lib/gitlab/sql/set_operator.rb
new file mode 100644
index 00000000000..d58a1415493
--- /dev/null
+++ b/lib/gitlab/sql/set_operator.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SQL
+ # Class for building SQL set operator statements (UNION, INTERSECT, and
+ # EXCEPT).
+ #
+ # ORDER BYs are dropped from the relations as the final sort order is not
+ # guaranteed any way.
+ #
+ # Example usage:
+ #
+ # union = Gitlab::SQL::Union.new([user.personal_projects, user.projects])
+ # sql = union.to_sql
+ #
+ # Project.where("id IN (#{sql})")
+ class SetOperator
+ def initialize(relations, remove_duplicates: true)
+ @relations = relations
+ @remove_duplicates = remove_duplicates
+ end
+
+ def self.operator_keyword
+ raise NotImplementedError
+ end
+
+ def to_sql
+ # Some relations may include placeholders for prepared statements, these
+ # aren't incremented properly when joining relations together this way.
+ # By using "unprepared_statements" we remove the usage of placeholders
+ # (thus fixing this problem), at a slight performance cost.
+ fragments = ActiveRecord::Base.connection.unprepared_statement do
+ relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
+ end
+
+ if fragments.any?
+ "(" + fragments.join(")\n#{operator_keyword_fragment}\n(") + ")"
+ else
+ 'NULL'
+ end
+ end
+
+ # UNION [ALL] | INTERSECT [ALL] | EXCEPT [ALL]
+ def operator_keyword_fragment
+ remove_duplicates ? self.class.operator_keyword : "#{self.class.operator_keyword} ALL"
+ end
+
+ private
+
+ attr_reader :relations, :remove_duplicates
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index b15f2ca385a..7fb3487a5e5 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -13,30 +13,9 @@ module Gitlab
# sql = union.to_sql
#
# Project.where("id IN (#{sql})")
- class Union
- def initialize(relations, remove_duplicates: true)
- @relations = relations
- @remove_duplicates = remove_duplicates
- end
-
- def to_sql
- # Some relations may include placeholders for prepared statements, these
- # aren't incremented properly when joining relations together this way.
- # By using "unprepared_statements" we remove the usage of placeholders
- # (thus fixing this problem), at a slight performance cost.
- fragments = ActiveRecord::Base.connection.unprepared_statement do
- @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
- end
-
- if fragments.any?
- "(" + fragments.join(")\n#{union_keyword}\n(") + ")"
- else
- 'NULL'
- end
- end
-
- def union_keyword
- @remove_duplicates ? 'UNION' : 'UNION ALL'
+ class Union < SetOperator
+ def self.operator_keyword
+ 'UNION'
end
end
end
diff --git a/lib/gitlab/static_site_editor/config.rb b/lib/gitlab/static_site_editor/config.rb
deleted file mode 100644
index d335a434335..00000000000
--- a/lib/gitlab/static_site_editor/config.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- class Config
- SUPPORTED_EXTENSIONS = %w[.md].freeze
-
- def initialize(repository, ref, file_path, return_url)
- @repository = repository
- @ref = ref
- @file_path = file_path
- @return_url = return_url
- @commit_id = repository.commit(ref)&.id if ref
- end
-
- def payload
- {
- branch: ref,
- path: file_path,
- commit_id: commit_id,
- project_id: project.id,
- project: project.path,
- namespace: project.namespace.full_path,
- return_url: sanitize_url(return_url),
- is_supported_content: supported_content?.to_s,
- base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path)
- }
- end
-
- private
-
- attr_reader :repository, :ref, :file_path, :return_url, :commit_id
-
- delegate :project, to: :repository
-
- def supported_content?
- master_branch? && extension_supported? && file_exists?
- end
-
- def master_branch?
- ref == 'master'
- end
-
- def extension_supported?
- return true if file_path.end_with?('.md.erb') && Feature.enabled?(:sse_erb_support, project)
-
- SUPPORTED_EXTENSIONS.any? { |ext| file_path.end_with?(ext) }
- end
-
- def file_exists?
- commit_id.present? && !repository.blob_at(commit_id, file_path).nil?
- end
-
- def full_path
- "#{ref}/#{file_path}"
- end
-
- def sanitize_url(url)
- url if Gitlab::UrlSanitizer.valid_web?(url)
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/file_config.rb b/lib/gitlab/static_site_editor/config/file_config.rb
new file mode 100644
index 00000000000..f647c85e1c8
--- /dev/null
+++ b/lib/gitlab/static_site_editor/config/file_config.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module StaticSiteEditor
+ module Config
+ class FileConfig
+ def data
+ {
+ static_site_generator: 'middleman'
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/static_site_editor/config/generated_config.rb b/lib/gitlab/static_site_editor/config/generated_config.rb
new file mode 100644
index 00000000000..f3dce74a32f
--- /dev/null
+++ b/lib/gitlab/static_site_editor/config/generated_config.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module StaticSiteEditor
+ module Config
+ class GeneratedConfig
+ SUPPORTED_EXTENSIONS = %w[.md].freeze
+
+ def initialize(repository, ref, path, return_url)
+ @repository = repository
+ @ref = ref
+ @path = path
+ @return_url = return_url
+ end
+
+ def data
+ merge_requests_illustration_path = ActionController::Base.helpers.image_path('illustrations/merge_requests.svg')
+ {
+ branch: ref,
+ path: path,
+ commit_id: commit_id,
+ project_id: project.id,
+ project: project.path,
+ namespace: project.namespace.full_path,
+ return_url: sanitize_url(return_url),
+ is_supported_content: supported_content?.to_s,
+ base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path),
+ merge_requests_illustration_path: merge_requests_illustration_path
+ }
+ end
+
+ private
+
+ attr_reader :repository, :ref, :path, :return_url
+
+ delegate :project, to: :repository
+
+ def commit_id
+ repository.commit(ref)&.id if ref
+ end
+
+ def supported_content?
+ master_branch? && extension_supported? && file_exists?
+ end
+
+ def master_branch?
+ ref == 'master'
+ end
+
+ def extension_supported?
+ return true if path.end_with?('.md.erb') && Feature.enabled?(:sse_erb_support, project)
+
+ SUPPORTED_EXTENSIONS.any? { |ext| path.end_with?(ext) }
+ end
+
+ def file_exists?
+ commit_id.present? && !repository.blob_at(commit_id, path).nil?
+ end
+
+ def full_path
+ "#{ref}/#{path}"
+ end
+
+ def sanitize_url(url)
+ url if Gitlab::UrlSanitizer.valid_web?(url)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/submodule_links.rb b/lib/gitlab/submodule_links.rb
index b0ee0877f30..38b10c5892d 100644
--- a/lib/gitlab/submodule_links.rb
+++ b/lib/gitlab/submodule_links.rb
@@ -4,14 +4,18 @@ module Gitlab
class SubmoduleLinks
include Gitlab::Utils::StrongMemoize
+ Urls = Struct.new(:web, :tree, :compare)
+
def initialize(repository)
@repository = repository
@cache_store = {}
end
- def for(submodule, sha)
+ def for(submodule, sha, diff_file = nil)
submodule_url = submodule_url_for(sha, submodule.path)
- SubmoduleHelper.submodule_links_for_url(submodule.id, submodule_url, repository)
+ old_submodule_id = old_submodule_id(submodule_url, diff_file)
+ urls = SubmoduleHelper.submodule_links_for_url(submodule.id, submodule_url, repository, old_submodule_id)
+ Urls.new(*urls) if urls.any?
end
private
@@ -29,5 +33,15 @@ module Gitlab
urls = submodule_urls_for(sha)
urls && urls[path]
end
+
+ def old_submodule_id(submodule_url, diff_file)
+ return unless diff_file&.old_blob && diff_file&.old_content_sha
+
+ # if the submodule url has changed from old_sha to sha, a compare link does not make sense
+ #
+ old_submodule_url = submodule_url_for(diff_file.old_content_sha, diff_file.old_blob.path)
+
+ diff_file.old_blob.id if old_submodule_url == submodule_url
+ end
end
end
diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index 73187d8dea8..c702c6f1add 100644
--- a/lib/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -24,6 +24,8 @@ module Gitlab
# Returns "yes" the user chose to continue
# Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue
def ask_to_continue
+ return if Gitlab::Utils.to_boolean(ENV['GITLAB_ASSUME_YES'])
+
answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no})
raise Gitlab::TaskAbortedByUserError unless answer == "yes"
end
diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb
index 3669d652fd3..9b39d386674 100644
--- a/lib/gitlab/template/finders/global_template_finder.rb
+++ b/lib/gitlab/template/finders/global_template_finder.rb
@@ -5,10 +5,10 @@ module Gitlab
module Template
module Finders
class GlobalTemplateFinder < BaseTemplateFinder
- def initialize(base_dir, extension, categories = {}, exclusions: [])
+ def initialize(base_dir, extension, categories = {}, excluded_patterns: [])
@categories = categories
@extension = extension
- @exclusions = exclusions
+ @excluded_patterns = excluded_patterns
super(base_dir)
end
@@ -43,7 +43,7 @@ module Gitlab
private
def excluded?(file_name)
- @exclusions.include?(file_name)
+ @excluded_patterns.any? { |pattern| pattern.match?(file_name) }
end
def select_directory(file_name)
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 26a9dc9fd38..bb1e9db55fa 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -3,12 +3,16 @@
module Gitlab
module Template
class GitlabCiYmlTemplate < BaseTemplate
+ BASE_EXCLUDED_PATTERNS = [%r{\.latest$}].freeze
+
def content
explanation = "# This file is a template, and might need editing before it works on your project."
[explanation, super].join("\n")
end
class << self
+ include Gitlab::Utils::StrongMemoize
+
def extension
'.gitlab-ci.yml'
end
@@ -22,10 +26,14 @@ module Gitlab
}
end
- def disabled_templates
- %w[
- Verify/Browser-Performance
- ]
+ def excluded_patterns
+ strong_memoize(:excluded_patterns) do
+ BASE_EXCLUDED_PATTERNS + additional_excluded_patterns
+ end
+ end
+
+ def additional_excluded_patterns
+ [%r{Verify/Browser-Performance}]
end
def base_dir
@@ -34,7 +42,7 @@ module Gitlab
def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(
- self.base_dir, self.extension, self.categories, exclusions: self.disabled_templates
+ self.base_dir, self.extension, self.categories, excluded_patterns: self.excluded_patterns
)
end
end
diff --git a/lib/gitlab/testing/robots_blocker_middleware.rb b/lib/gitlab/testing/robots_blocker_middleware.rb
new file mode 100644
index 00000000000..034492122df
--- /dev/null
+++ b/lib/gitlab/testing/robots_blocker_middleware.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+# rubocop:disable Style/ClassVars
+module Gitlab
+ module Testing
+ class RobotsBlockerMiddleware
+ @@block_requests = Concurrent::AtomicBoolean.new(false)
+
+ # Block requests according to robots.txt.
+ # Any new requests disallowed by robots.txt will return an HTTP 503 status.
+ def self.block_requests!
+ @@block_requests.value = true
+ end
+
+ # Allows the server to accept requests again.
+ def self.allow_requests!
+ @@block_requests.value = false
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = Rack::Request.new(env)
+
+ if block_requests? && Gitlab::RobotsTxt.disallowed?(request.path_info)
+ block_request(env)
+ else
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def block_requests?
+ @@block_requests.true?
+ end
+
+ def block_request(env)
+ [503, {}, []]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index 37688d6e0e7..02d354ec43a 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -47,8 +47,7 @@ module Gitlab
cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain,
app_id: Gitlab::CurrentSettings.snowplow_app_id,
form_tracking: additional_features,
- link_click_tracking: additional_features,
- iglu_registry_url: Gitlab::CurrentSettings.snowplow_iglu_registry_url
+ link_click_tracking: additional_features
}.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
end
@@ -56,12 +55,19 @@ module Gitlab
def snowplow
@snowplow ||= SnowplowTracker::Tracker.new(
- SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'https'),
+ emitter,
SnowplowTracker::Subject.new,
SNOWPLOW_NAMESPACE,
Gitlab::CurrentSettings.snowplow_app_id
)
end
+
+ def emitter
+ SnowplowTracker::AsyncEmitter.new(
+ Gitlab::CurrentSettings.snowplow_collector_hostname,
+ protocol: 'https'
+ )
+ end
end
end
end
diff --git a/lib/gitlab/tracking/incident_management.rb b/lib/gitlab/tracking/incident_management.rb
index 5fa819b3696..df2a0658b36 100644
--- a/lib/gitlab/tracking/incident_management.rb
+++ b/lib/gitlab/tracking/incident_management.rb
@@ -35,6 +35,9 @@ module Gitlab
},
pagerduty_active: {
name: 'pagerduty_webhook'
+ },
+ auto_close_incident: {
+ name: 'auto_close_incident'
}
}.with_indifferent_access.freeze
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 70efe86143e..89605ce5d07 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -38,6 +38,8 @@ module Gitlab
.merge(usage_activity_by_stage(:usage_activity_by_stage_monthly, last_28_days_time_period))
.merge(analytics_unique_visits_data)
.merge(compliance_unique_visits_data)
+ .merge(search_unique_visits_data)
+ .merge(redis_hll_counters)
end
end
@@ -110,6 +112,8 @@ module Gitlab
clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available),
clusters_applications_cilium: count(::Clusters::Applications::Cilium.available),
clusters_management_project: count(::Clusters::Cluster.with_management_project),
+ kubernetes_agents: count(::Clusters::Agent),
+ kubernetes_agents_with_token: distinct_count(::Clusters::AgentToken, :agent_id),
in_review_folder: count(::Environment.in_review_folder),
grafana_integrated_projects: count(GrafanaIntegration.enabled),
groups: count(Group),
@@ -129,6 +133,8 @@ module Gitlab
lfs_objects: count(LfsObject),
milestone_lists: count(List.milestone),
milestones: count(Milestone),
+ projects_with_packages: distinct_count(::Packages::Package, :project_id),
+ packages: count(::Packages::Package),
pages_domains: count(PagesDomain),
pool_repositories: count(PoolRepository),
projects: count(Project),
@@ -160,7 +166,8 @@ module Gitlab
user_preferences_usage,
ingress_modsecurity_usage,
container_expiration_policies_usage,
- service_desk_counts
+ service_desk_counts,
+ snowplow_event_counts
).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
@@ -168,6 +175,19 @@ module Gitlab
end
# rubocop: enable Metrics/AbcSize
+ def snowplow_event_counts(time_period: {})
+ return {} unless report_snowplow_events?
+
+ {
+ promoted_issues: count(
+ self_monitoring_project
+ .product_analytics_events
+ .by_category_and_action('epics', 'promote')
+ .where(time_period)
+ )
+ }
+ end
+
def system_usage_data_monthly
{
counts_monthly: {
@@ -176,9 +196,12 @@ module Gitlab
successful_deployments: deployment_count(Deployment.success.where(last_28_days_time_period)),
failed_deployments: deployment_count(Deployment.failed.where(last_28_days_time_period)),
# rubocop: enable UsageData/LargeTable:
+ packages: count(::Packages::Package.where(last_28_days_time_period)),
personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)),
project_snippets: count(ProjectSnippet.where(last_28_days_time_period))
- }.tap do |data|
+ }.merge(
+ snowplow_event_counts(time_period: last_28_days_time_period(column: :collector_tstamp))
+ ).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
}
@@ -240,7 +263,8 @@ module Gitlab
Gitlab::UsageDataCounters::ProductivityAnalyticsCounter,
Gitlab::UsageDataCounters::SourceCodeCounter,
Gitlab::UsageDataCounters::MergeRequestCounter,
- Gitlab::UsageDataCounters::DesignsCounter
+ Gitlab::UsageDataCounters::DesignsCounter,
+ Gitlab::UsageDataCounters::KubernetesAgentCounter
]
end
@@ -264,6 +288,9 @@ module Gitlab
database: {
adapter: alt_usage_data { Gitlab::Database.adapter_name },
version: alt_usage_data { Gitlab::Database.version }
+ },
+ mail: {
+ smtp_server: alt_usage_data { ActionMailer::Base.smtp_settings[:address] }
}
}
end
@@ -371,7 +398,9 @@ module Gitlab
# so we can just check for subdomains of atlassian.net
results = {
projects_jira_server_active: 0,
- projects_jira_cloud_active: 0
+ projects_jira_cloud_active: 0,
+ projects_jira_dvcs_cloud_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled),
+ projects_jira_dvcs_server_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false))
}
# rubocop: disable UsageData/LargeTable:
@@ -399,7 +428,7 @@ module Gitlab
{
jira_imports_total_imported_count: count(finished_jira_imports),
jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id),
- jira_imports_total_imported_issues_count: alt_usage_data { JiraImportState.finished_imports_count }
+ jira_imports_total_imported_issues_count: sum(JiraImportState.finished, :imported_issues_count)
}
# rubocop: enable UsageData/LargeTable
end
@@ -409,7 +438,7 @@ module Gitlab
def successful_deployments_with_cluster(scope)
scope
.joins(cluster: :deployments)
- .merge(Clusters::Cluster.enabled)
+ .merge(::Clusters::Cluster.enabled)
.merge(Deployment.success)
end
# rubocop: enable UsageData/LargeTable
@@ -419,16 +448,17 @@ module Gitlab
{} # augmented in EE
end
- # rubocop: disable CodeReuse/ActiveRecord
def merge_requests_users(time_period)
- distinct_count(
- Event.where(target_type: Event::TARGET_TYPES[:merge_request].to_s).where(time_period),
- :author_id,
- start: user_minimum_id,
- finish: user_maximum_id
- )
+ counter = Gitlab::UsageDataCounters::TrackUniqueEvents
+
+ redis_usage_data do
+ counter.count_unique_events(
+ event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION,
+ date_from: time_period[:created_at].first,
+ date_to: time_period[:created_at].last
+ )
+ end
end
- # rubocop: enable CodeReuse/ActiveRecord
def installation_type
if Rails.env.production?
@@ -438,8 +468,8 @@ module Gitlab
end
end
- def last_28_days_time_period
- { created_at: 28.days.ago..Time.current }
+ def last_28_days_time_period(column: :created_at)
+ { column => 28.days.ago..Time.current }
end
# Source: https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/ping_metrics_to_stage_mapping_data.csv
@@ -510,7 +540,22 @@ module Gitlab
events: distinct_count(::Event.where(time_period), :author_id),
groups: distinct_count(::GroupMember.where(time_period), :user_id),
users_created: count(::User.where(time_period), start: user_minimum_id, finish: user_maximum_id),
- omniauth_providers: filtered_omniauth_provider_names.reject { |name| name == 'group_saml' }
+ omniauth_providers: filtered_omniauth_provider_names.reject { |name| name == 'group_saml' },
+ projects_imported: {
+ gitlab_project: projects_imported_count('gitlab_project', time_period),
+ gitlab: projects_imported_count('gitlab', time_period),
+ github: projects_imported_count('github', time_period),
+ bitbucket: projects_imported_count('bitbucket', time_period),
+ bitbucket_server: projects_imported_count('bitbucket_server', time_period),
+ gitea: projects_imported_count('gitea', time_period),
+ git: projects_imported_count('git', time_period),
+ manifest: projects_imported_count('manifest', time_period)
+ },
+ issues_imported: {
+ jira: distinct_count(::JiraImportState.where(time_period), :user_id),
+ fogbugz: projects_imported_count('fogbugz', time_period),
+ phabricator: projects_imported_count('phabricator', time_period)
+ }
}
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -527,9 +572,13 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def usage_activity_by_stage_package(time_period)
- {}
+ {
+ projects_with_packages: distinct_count(::Project.with_packages.where(time_period), :creator_id)
+ }
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Omitted because no user, creator or author associated: `boards`, `labels`, `milestones`, `uploads`
# Omitted because too expensive: `epics_deepest_relationship_level`
@@ -542,7 +591,10 @@ module Gitlab
projects: distinct_count(::Project.where(time_period), :creator_id),
todos: distinct_count(::Todo.where(time_period), :author_id),
service_desk_enabled_projects: distinct_count_service_desk_enabled_projects(time_period),
- service_desk_issues: count(::Issue.service_desk.where(time_period))
+ service_desk_issues: count(::Issue.service_desk.where(time_period)),
+ projects_jira_active: distinct_count(::Project.with_active_jira_services.where(time_period), :creator_id),
+ projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_jira_services.with_jira_dvcs_cloud.where(time_period), :creator_id),
+ projects_jira_dvcs_server_active: distinct_count(::Project.with_active_jira_services.with_jira_dvcs_server.where(time_period), :creator_id)
}
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -583,9 +635,13 @@ module Gitlab
{}
end
+ def redis_hll_counters
+ { redis_hll_counters: ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events_data }
+ end
+
def analytics_unique_visits_data
- results = ::Gitlab::Analytics::UniqueVisits.analytics_ids.each_with_object({}) do |target_id, hash|
- hash[target_id] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target_id) }
+ results = ::Gitlab::Analytics::UniqueVisits.analytics_events.each_with_object({}) do |target, hash|
+ hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) }
end
results['analytics_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) }
results['analytics_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics, start_date: 4.weeks.ago.to_date, end_date: Date.current) }
@@ -594,8 +650,8 @@ module Gitlab
end
def compliance_unique_visits_data
- results = ::Gitlab::Analytics::UniqueVisits.compliance_ids.each_with_object({}) do |target_id, hash|
- hash[target_id] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target_id) }
+ results = ::Gitlab::Analytics::UniqueVisits.compliance_events.each_with_object({}) do |target, hash|
+ hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) }
end
results['compliance_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance) }
results['compliance_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance, start_date: 4.weeks.ago.to_date, end_date: Date.current) }
@@ -603,41 +659,53 @@ module Gitlab
{ compliance_unique_visits: results }
end
+ def search_unique_visits_data
+ events = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('search')
+ results = events.each_with_object({}) do |event, hash|
+ hash[event] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: event, start_date: 7.days.ago.to_date, end_date: Date.current) }
+ end
+
+ results['search_unique_visits_for_any_target_weekly'] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: 7.days.ago.to_date, end_date: Date.current) }
+ results['search_unique_visits_for_any_target_monthly'] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: 4.weeks.ago.to_date, end_date: Date.current) }
+
+ { search_unique_visits: results }
+ end
+
def action_monthly_active_users(time_period)
- counter = Gitlab::UsageDataCounters::TrackUniqueActions
+ date_range = { date_from: time_period[:created_at].first, date_to: time_period[:created_at].last }
- project_count = redis_usage_data do
- counter.count_unique(
- event_action: Gitlab::UsageDataCounters::TrackUniqueActions::PUSH_ACTION,
- date_from: time_period[:created_at].first,
- date_to: time_period[:created_at].last
- )
- end
+ event_monthly_active_users(date_range)
+ .merge!(ide_monthly_active_users(date_range))
+ end
- design_count = redis_usage_data do
- counter.count_unique(
- event_action: Gitlab::UsageDataCounters::TrackUniqueActions::DESIGN_ACTION,
- date_from: time_period[:created_at].first,
- date_to: time_period[:created_at].last
- )
- end
+ private
- wiki_count = redis_usage_data do
- counter.count_unique(
- event_action: Gitlab::UsageDataCounters::TrackUniqueActions::WIKI_ACTION,
- date_from: time_period[:created_at].first,
- date_to: time_period[:created_at].last
- )
+ def event_monthly_active_users(date_range)
+ data = {
+ action_monthly_active_users_project_repo: Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION,
+ action_monthly_active_users_design_management: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION,
+ action_monthly_active_users_wiki_repo: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION
+ }
+
+ data.each do |key, event|
+ data[key] = redis_usage_data { Gitlab::UsageDataCounters::TrackUniqueEvents.count_unique_events(event_action: event, **date_range) }
end
+ end
+
+ def ide_monthly_active_users(date_range)
+ counter = Gitlab::UsageDataCounters::EditorUniqueCounter
{
- action_monthly_active_users_project_repo: project_count,
- action_monthly_active_users_design_management: design_count,
- action_monthly_active_users_wiki_repo: wiki_count
+ 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_ide_edit: redis_usage_data { counter.count_edit_using_editor(date_range) }
}
end
- private
+ def report_snowplow_events?
+ self_monitoring_project && Feature.enabled?(:product_analytics, self_monitoring_project)
+ end
def distinct_count_service_desk_enabled_projects(time_period)
project_creator_id_start = user_minimum_id
@@ -716,6 +784,22 @@ module Gitlab
end
end
+ def project_minimum_id
+ strong_memoize(:project_minimum_id) do
+ ::Project.minimum(:id)
+ end
+ end
+
+ def project_maximum_id
+ strong_memoize(:project_maximum_id) do
+ ::Project.maximum(:id)
+ end
+ end
+
+ def self_monitoring_project
+ Gitlab::CurrentSettings.self_monitoring_project
+ end
+
def clear_memoized
clear_memoization(:issue_minimum_id)
clear_memoization(:issue_maximum_id)
@@ -726,14 +810,14 @@ module Gitlab
clear_memoization(:deployment_maximum_id)
clear_memoization(:approval_merge_request_rule_minimum_id)
clear_memoization(:approval_merge_request_rule_maximum_id)
+ clear_memoization(:project_minimum_id)
+ clear_memoization(:project_maximum_id)
end
# rubocop: disable CodeReuse/ActiveRecord
- # rubocop: disable UsageData/DistinctCountByLargeForeignKey
def cluster_applications_user_distinct_count(applications, time_period)
distinct_count(applications.where(time_period).available.joins(:cluster), 'clusters.user_id')
end
- # rubocop: enable UsageData/DistinctCountByLargeForeignKey
def clusters_user_distinct_count(clusters, time_period)
distinct_count(clusters.where(time_period), :user_id)
@@ -755,6 +839,10 @@ module Gitlab
def deployment_count(relation)
count relation, start: deployment_minimum_id, finish: deployment_maximum_id
end
+
+ def projects_imported_count(from, time_period)
+ distinct_count(::Project.imported_from(from).where(time_period), :creator_id) # rubocop: disable CodeReuse/ActiveRecord
+ end
end
end
end
diff --git a/lib/gitlab/usage_data/topology.rb b/lib/gitlab/usage_data/topology.rb
index edc4dc75750..7f7854c3eb1 100644
--- a/lib/gitlab/usage_data/topology.rb
+++ b/lib/gitlab/usage_data/topology.rb
@@ -40,9 +40,10 @@ module Gitlab
private
def topology_fetch_all_data
- with_prometheus_client(fallback: {}) do |client|
+ with_prometheus_client(fallback: {}, verify: false) do |client|
{
application_requests_per_hour: topology_app_requests_per_hour(client),
+ query_apdex_weekly_average: topology_query_apdex_weekly_average(client),
nodes: topology_node_data(client)
}.compact
end
@@ -63,6 +64,16 @@ module Gitlab
(result['value'].last.to_f * 1.hour).to_i
end
+ def topology_query_apdex_weekly_average(client)
+ result = query_safely('gitlab_usage_ping:sql_duration_apdex:ratio_rate5m', 'query_apdex', fallback: nil) do |query|
+ client.query(aggregate_one_week(query)).first
+ end
+
+ return unless result
+
+ result['value'].last.to_f
+ end
+
def topology_node_data(client)
# node-level data
by_instance_mem = topology_node_memory(client)
diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
new file mode 100644
index 00000000000..b68d50ee419
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ module EditorUniqueCounter
+ 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_CATEGORY = 'ide_edit'
+
+ class << self
+ def track_web_ide_edit_action(author:, time: Time.zone.now)
+ track_unique_action(EDIT_BY_WEB_IDE, author, time)
+ end
+
+ def count_web_ide_edit_actions(date_from:, date_to:)
+ count_unique(EDIT_BY_WEB_IDE, date_from, date_to)
+ end
+
+ def track_sfe_edit_action(author:, time: Time.zone.now)
+ track_unique_action(EDIT_BY_SFE, author, time)
+ end
+
+ def count_sfe_edit_actions(date_from:, date_to:)
+ count_unique(EDIT_BY_SFE, date_from, date_to)
+ end
+
+ def track_snippet_editor_edit_action(author:, time: Time.zone.now)
+ track_unique_action(EDIT_BY_SNIPPET_EDITOR, author, time)
+ end
+
+ def count_snippet_editor_edit_actions(date_from:, date_to:)
+ count_unique(EDIT_BY_SNIPPET_EDITOR, date_from, date_to)
+ end
+
+ def count_edit_using_editor(date_from:, date_to:)
+ events = Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(EDIT_CATEGORY)
+ count_unique(events, date_from, date_to)
+ end
+
+ private
+
+ def track_unique_action(action, author, time)
+ return unless Feature.enabled?(:track_editor_edit_actions, default_enabled: true)
+ return unless author
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(author.id, action, time)
+ end
+
+ def count_unique(actions, date_from, date_to)
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: actions, start_date: date_from, end_date: date_to)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index c9c39225068..53bf6daea4c 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -31,7 +31,11 @@ module Gitlab
# * Track event: Gitlab::UsageDataCounters::HLLRedisCounter.track_event(user_id, 'g_compliance_dashboard')
# * Get unique counts per user: Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_dashboard', start_date: 28.days.ago, end_date: Date.current)
class << self
+ include Gitlab::Utils::UsageData
+
def track_event(entity_id, event_name, time = Time.zone.now)
+ return unless Gitlab::CurrentSettings.usage_ping_enabled?
+
event = event_for(event_name)
raise UnknownEvent.new("Unknown event #{event_name}") unless event.present?
@@ -50,15 +54,51 @@ module Gitlab
keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date)
- Gitlab::Redis::HLL.count(keys: keys)
+ redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) }
+ end
+
+ def categories
+ @categories ||= known_events.map { |event| event[:category] }.uniq
end
+ # @param category [String] the category name
+ # @return [Array<String>] list of event names for given category
def events_for_category(category)
- known_events.select { |event| event[:category] == category }.map { |event| event[:name] }
+ known_events.select { |event| event[:category] == category.to_s }.map { |event| event[:name] }
+ end
+
+ def unique_events_data
+ categories.each_with_object({}) do |category, category_results|
+ events_names = events_for_category(category)
+
+ event_results = events_names.each_with_object({}) do |event, hash|
+ hash[event] = unique_events(event_names: event, start_date: 7.days.ago.to_date, end_date: Date.current)
+ end
+
+ if eligible_for_totals?(events_names)
+ event_results["#{category}_total_unique_counts_weekly"] = unique_events(event_names: events_names, start_date: 7.days.ago.to_date, end_date: Date.current)
+ event_results["#{category}_total_unique_counts_monthly"] = unique_events(event_names: events_names, start_date: 4.weeks.ago.to_date, end_date: Date.current)
+ end
+
+ category_results["#{category}"] = event_results
+ end
+ end
+
+ def known_event?(event_name)
+ event_for(event_name).present?
end
private
+ # Allow to add totals for events that are in the same redis slot, category and have the same aggregation level
+ # and if there are more than 1 event
+ def eligible_for_totals?(events_names)
+ return false if events_names.size <= 1
+
+ events = events_for(events_names)
+ events_in_same_slot?(events) && events_in_same_category?(events) && events_same_aggregation?(events)
+ end
+
def keys_for_aggregation(aggregation, events:, start_date:, end_date:)
if aggregation.to_sym == :daily
daily_redis_keys(events: events, start_date: start_date, end_date: end_date)
@@ -76,8 +116,11 @@ module Gitlab
end
def events_in_same_slot?(events)
+ # if we check one event then redis_slot is only one to check
+ return true if events.size == 1
+
slot = events.first[:redis_slot]
- events.all? { |event| event[:redis_slot] == slot }
+ events.all? { |event| event[:redis_slot].present? && event[:redis_slot] == slot }
end
def events_in_same_category?(events)
@@ -91,7 +134,7 @@ module Gitlab
end
def expiry(event)
- return event[:expiry] if event[:expiry].present?
+ return event[:expiry].days if event[:expiry].present?
event[:aggregation].to_sym == :daily ? DEFAULT_DAILY_KEY_EXPIRY_LENGTH : DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH
end
diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
new file mode 100644
index 00000000000..fc1b5a59487
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ module IssueActivityUniqueCounter
+ ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed'
+ ISSUE_DESCRIPTION_CHANGED = 'g_project_management_issue_description_changed'
+ ISSUE_ASSIGNEE_CHANGED = 'g_project_management_issue_assignee_changed'
+ ISSUE_MADE_CONFIDENTIAL = 'g_project_management_issue_made_confidential'
+ ISSUE_MADE_VISIBLE = 'g_project_management_issue_made_visible'
+ ISSUE_CATEGORY = 'issues_edit'
+
+ class << self
+ def track_issue_title_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_TITLE_CHANGED, author, time)
+ end
+
+ def track_issue_description_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_DESCRIPTION_CHANGED, author, time)
+ end
+
+ def track_issue_assignee_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_ASSIGNEE_CHANGED, author, time)
+ end
+
+ def track_issue_made_confidential_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_MADE_CONFIDENTIAL, author, time)
+ end
+
+ def track_issue_made_visible_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_MADE_VISIBLE, author, time)
+ end
+
+ private
+
+ def track_unique_action(action, author, time)
+ return unless Feature.enabled?(:track_issue_activity_actions)
+ return unless author
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(author.id, action, time)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/known_events.yml b/lib/gitlab/usage_data_counters/known_events.yml
index b7e516fa8b1..25e7f858bb1 100644
--- a/lib/gitlab/usage_data_counters/known_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events.yml
@@ -3,86 +3,206 @@
- name: g_compliance_dashboard
redis_slot: compliance
category: compliance
- expiry: 84 # expiration time in days, equivalent to 12 weeks
aggregation: weekly
- name: g_compliance_audit_events
category: compliance
redis_slot: compliance
- expiry: 84
aggregation: weekly
- name: i_compliance_audit_events
category: compliance
redis_slot: compliance
- expiry: 84
aggregation: weekly
- name: i_compliance_credential_inventory
category: compliance
redis_slot: compliance
- expiry: 84
+ aggregation: weekly
+- name: a_compliance_audit_events_api
+ category: compliance
+ redis_slot: compliance
aggregation: weekly
# Analytics category
- name: g_analytics_contribution
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_insights
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_issues
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_productivity
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_valuestream
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_pipelines
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_code_reviews
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_valuestream
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_insights
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_issues
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_repo
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: i_analytics_cohorts
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: i_analytics_dev_ops_score
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
+- name: g_analytics_merge_request
+ category: analytics
+ redis_slot: analytics
+ aggregation: weekly
+- name: p_analytics_merge_request
+ category: analytics
+ redis_slot: analytics
+ aggregation: weekly
+- name: i_analytics_instance_statistics
+ category: analytics
+ redis_slot: analytics
+ aggregation: weekly
+- name: g_edit_by_web_ide
+ category: ide_edit
+ redis_slot: edit
+ expiry: 29
+ aggregation: daily
+- name: g_edit_by_sfe
+ category: ide_edit
+ redis_slot: edit
+ expiry: 29
+ aggregation: daily
+- name: g_edit_by_snippet_ide
+ category: ide_edit
+ redis_slot: edit
+ expiry: 29
+ aggregation: daily
+- name: i_search_total
+ category: search
+ redis_slot: search
+ aggregation: weekly
+- name: i_search_advanced
+ category: search
+ redis_slot: search
+ aggregation: weekly
+- name: i_search_paid
+ category: search
+ redis_slot: search
+ aggregation: weekly
+- name: wiki_action
+ category: source_code
+ aggregation: daily
+- name: design_action
+ category: source_code
+ aggregation: daily
+- name: project_action
+ category: source_code
+ aggregation: daily
+- name: merge_request_action
+ category: source_code
+ aggregation: daily
+- name: i_source_code_code_intelligence
+ redis_slot: source_code
+ category: source_code
+ aggregation: daily
+# Incident management
+- name: incident_management_alert_status_changed
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_alert_assigned
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_alert_todo
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_created
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_reopened
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_closed
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_assigned
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_todo
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_comment
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_zoom_meeting
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_published
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_relate
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_unrelate
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+- name: incident_management_incident_change_confidential
+ redis_slot: incident_management
+ category: incident_management
+ aggregation: weekly
+# Project Management group
+- name: g_project_management_issue_title_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_description_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_assignee_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_made_confidential
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_made_visible
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
diff --git a/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb
new file mode 100644
index 00000000000..eae42bdc4a1
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ class KubernetesAgentCounter < BaseCounter
+ PREFIX = 'kubernetes_agent'
+ KNOWN_EVENTS = %w[gitops_sync].freeze
+
+ class << self
+ def increment_gitops_sync(incr)
+ raise ArgumentError, 'must be greater than or equal to zero' if incr < 0
+
+ # rather then hitting redis for this no-op, we return early
+ # note: redis returns the increment, so we mimic this here
+ return 0 if incr == 0
+
+ increment_by(redis_key(:gitops_sync), incr)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/redis_counter.rb b/lib/gitlab/usage_data_counters/redis_counter.rb
index 75d5a75e3a4..2406f771fd8 100644
--- a/lib/gitlab/usage_data_counters/redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/redis_counter.rb
@@ -9,6 +9,12 @@ module Gitlab
Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) }
end
+ def increment_by(redis_counter_key, incr)
+ return unless Gitlab::CurrentSettings.usage_ping_enabled
+
+ Gitlab::Redis::SharedState.with { |redis| redis.incrby(redis_counter_key, incr) }
+ end
+
def total_count(redis_counter_key)
Gitlab::Redis::SharedState.with { |redis| redis.get(redis_counter_key).to_i }
end
diff --git a/lib/gitlab/usage_data_counters/track_unique_actions.rb b/lib/gitlab/usage_data_counters/track_unique_events.rb
index 0df982572a4..7053744b665 100644
--- a/lib/gitlab/usage_data_counters/track_unique_actions.rb
+++ b/lib/gitlab/usage_data_counters/track_unique_events.rb
@@ -2,12 +2,11 @@
module Gitlab
module UsageDataCounters
- module TrackUniqueActions
- KEY_EXPIRY_LENGTH = 29.days
-
+ module TrackUniqueEvents
WIKI_ACTION = :wiki_action
DESIGN_ACTION = :design_action
PUSH_ACTION = :project_action
+ MERGE_REQUEST_ACTION = :merge_request_action
ACTION_TRANSFORMATIONS = HashWithIndifferentAccess.new({
wiki: {
@@ -22,26 +21,30 @@ module Gitlab
},
project: {
pushed: PUSH_ACTION
+ },
+ merge_request: {
+ closed: MERGE_REQUEST_ACTION,
+ merged: MERGE_REQUEST_ACTION,
+ created: MERGE_REQUEST_ACTION,
+ commented: MERGE_REQUEST_ACTION
}
}).freeze
class << self
def track_event(event_action:, event_target:, author_id:, time: Time.zone.now)
- return unless Gitlab::CurrentSettings.usage_ping_enabled
return unless valid_target?(event_target)
return unless valid_action?(event_action)
transformed_target = transform_target(event_target)
transformed_action = transform_action(event_action, transformed_target)
- target_key = key(transformed_action, time)
- Gitlab::Redis::HLL.add(key: target_key, value: author_id, expiry: KEY_EXPIRY_LENGTH)
- end
+ return unless Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(transformed_action.to_s)
- def count_unique(event_action:, date_from:, date_to:)
- keys = (date_from.to_date..date_to.to_date).map { |date| key(event_action, date) }
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(author_id, transformed_action.to_s, time)
+ end
- Gitlab::Redis::HLL.count(keys: keys)
+ def count_unique_events(event_action:, date_from:, date_to:)
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: event_action.to_s, start_date: date_from, end_date: date_to)
end
private
@@ -61,11 +64,6 @@ module Gitlab
def valid_action?(action)
Event.actions.key?(action)
end
-
- def key(event_action, date)
- year_day = date.strftime('%G-%j')
- "#{year_day}-{#{event_action}}"
- end
end
end
end
diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb
new file mode 100644
index 00000000000..bacd63ab282
--- /dev/null
+++ b/lib/gitlab/usage_data_queries.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ # This class is used by the `gitlab:usage_data:dump_sql` rake tasks to output SQL instead of running it.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091
+ class UsageDataQueries < UsageData
+ class << self
+ def count(relation, column = nil, *rest)
+ raw_sql(relation, column)
+ end
+
+ def distinct_count(relation, column = nil, *rest)
+ raw_sql(relation, column, :distinct)
+ end
+
+ def redis_usage_data(counter = nil, &block)
+ if block_given?
+ { redis_usage_data_block: block.to_s }
+ elsif counter.present?
+ { redis_usage_data_counter: counter }
+ end
+ end
+
+ def sum(relation, column, *rest)
+ relation.select(relation.all.table[column].sum).to_sql # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ private
+
+ def raw_sql(relation, column, distinct = nil)
+ column ||= relation.primary_key
+ relation.select(relation.all.table[column].count(distinct)).to_sql
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/utils/gzip.rb b/lib/gitlab/utils/gzip.rb
new file mode 100644
index 00000000000..898be651554
--- /dev/null
+++ b/lib/gitlab/utils/gzip.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Utils
+ module Gzip
+ def gzip_compress(data)
+ # .compress returns ASCII-8BIT, so we need to force the encoding to
+ # UTF-8 before caching it in redis, else we risk encoding mismatch
+ # errors.
+ #
+ ActiveSupport::Gzip.compress(data).force_encoding("UTF-8")
+ rescue Zlib::GzipFile::Error
+ data
+ end
+
+ def gzip_decompress(data)
+ # Since we could be dealing with an already populated cache full of data
+ # that isn't gzipped, we want to also check to see if the data is
+ # gzipped before we attempt to .decompress it, thus we check the first
+ # 2 bytes for "\x1F\x8B" to confirm it is a gzipped string. While a
+ # non-gzipped string will raise a Zlib::GzipFile::Error, which we're
+ # rescuing, we don't want to count on rescue for control flow.
+ #
+ data[0..1] == "\x1F\x8B" ? ActiveSupport::Gzip.decompress(data) : data
+ rescue Zlib::GzipFile::Error
+ data
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/utils/markdown.rb b/lib/gitlab/utils/markdown.rb
index 82c4a0e3b23..e783ac785cc 100644
--- a/lib/gitlab/utils/markdown.rb
+++ b/lib/gitlab/utils/markdown.rb
@@ -4,11 +4,13 @@ module Gitlab
module Utils
module Markdown
PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze
+ PRODUCT_SUFFIX = /\s*\**\((core|starter|premium|ultimate)(\s+only)?\)\**/.freeze
def string_to_anchor(string)
string
.strip
.downcase
+ .gsub(PRODUCT_SUFFIX, '')
.gsub(PUNCTUATION_REGEXP, '') # remove punctuation
.tr(' ', '-') # replace spaces with dash
.squeeze('-') # replace multiple dashes with one
diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb
index 36046ca14bf..ca6a36c9cea 100644
--- a/lib/gitlab/utils/usage_data.rb
+++ b/lib/gitlab/utils/usage_data.rb
@@ -59,6 +59,12 @@ module Gitlab
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
+ FALLBACK
+ end
+
def alt_usage_data(value = nil, fallback: FALLBACK, &block)
if block_given?
yield
@@ -77,11 +83,11 @@ module Gitlab
end
end
- def with_prometheus_client(fallback: nil)
- return fallback unless Gitlab::Prometheus::Internal.prometheus_enabled?
+ def with_prometheus_client(fallback: nil, verify: true)
+ client = prometheus_client(verify: verify)
+ return fallback unless client
- prometheus_address = Gitlab::Prometheus::Internal.uri
- yield Gitlab::PrometheusClient.new(prometheus_address, allow_local_requests: true)
+ yield client
end
def measure_duration
@@ -96,8 +102,41 @@ module Gitlab
yield.merge(key => Time.current)
end
+ # @param event_name [String] the event name
+ # @param values [Array|String] the values counted
+ def track_usage_event(event_name, values)
+ return unless Feature.enabled?(:"usage_data_#{event_name}", default_enabled: true)
+ return unless Gitlab::CurrentSettings.usage_ping_enabled?
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name.to_s)
+ end
+
private
+ def prometheus_client(verify:)
+ server_address = prometheus_server_address
+
+ return unless server_address
+
+ # There really is not a way to discover whether a Prometheus connection is using TLS or not
+ # Try TLS first because HTTPS will return fast if failed.
+ %w[https http].find do |scheme|
+ api_url = "#{scheme}://#{server_address}"
+ client = Gitlab::PrometheusClient.new(api_url, allow_local_requests: true, verify: verify)
+ break client if client.ready?
+ rescue
+ nil
+ end
+ end
+
+ def prometheus_server_address
+ if Gitlab::Prometheus::Internal.prometheus_enabled?
+ Gitlab::Prometheus::Internal.server_address
+ elsif Gitlab::Consul::Internal.api_url
+ Gitlab::Consul::Internal.discover_prometheus_server_address
+ end
+ end
+
def redis_usage_counter
yield
rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
diff --git a/lib/gitlab/web_ide/config.rb b/lib/gitlab/web_ide/config.rb
index 3b1fa162b53..b2ab5c0b6e3 100644
--- a/lib/gitlab/web_ide/config.rb
+++ b/lib/gitlab/web_ide/config.rb
@@ -34,6 +34,10 @@ module Gitlab
@global.terminal_value
end
+ def schemas_value
+ @global.schemas_value
+ end
+
private
def build_config(config, opts = {})
diff --git a/lib/gitlab/web_ide/config/entry/global.rb b/lib/gitlab/web_ide/config/entry/global.rb
index 50c3f2d294f..2c67c7d02d4 100644
--- a/lib/gitlab/web_ide/config/entry/global.rb
+++ b/lib/gitlab/web_ide/config/entry/global.rb
@@ -12,18 +12,22 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[terminal].freeze
+ def self.allowed_keys
+ %i[terminal].freeze
+ end
validations do
- validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, allowed_keys: Global.allowed_keys
end
+ attributes allowed_keys
+
entry :terminal, Entry::Terminal,
description: 'Configuration of the webide terminal.'
-
- attributes :terminal
end
end
end
end
end
+
+::Gitlab::WebIde::Config::Entry::Global.prepend_if_ee('EE::Gitlab::WebIde::Config::Entry::Global')
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index e3b1cb3d016..8a5acd242d9 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -156,10 +156,11 @@ module Gitlab
]
end
- def send_scaled_image(location, width)
+ def send_scaled_image(location, width, content_type)
params = {
'Location' => location,
- 'Width' => width
+ 'Width' => width,
+ 'ContentType' => content_type
}
[
diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb
index a98ac9200da..a906beda80e 100644
--- a/lib/gitlab_danger.rb
+++ b/lib/gitlab_danger.rb
@@ -13,6 +13,7 @@ class GitlabDanger
commit_messages
telemetry
utility_css
+ pajamas
].freeze
CI_ONLY_RULES ||= %w[
@@ -22,6 +23,7 @@ class GitlabDanger
roulette
ce_ee_vue_templates
sidekiq_queues
+ specialization_labels
].freeze
MESSAGE_PREFIX = '==>'.freeze
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index eea7daa3d8e..b349b46dc18 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -41,7 +41,7 @@ module Mattermost
begin
yield self
rescue Errno::ECONNREFUSED => e
- Rails.logger.error(e.message + "\n" + e.backtrace.join("\n")) # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error(e.message + "\n" + e.backtrace.join("\n"))
raise Mattermost::NoSessionError
ensure
destroy
@@ -52,7 +52,7 @@ module Mattermost
# Next methods are needed for Doorkeeper
def pre_auth
@pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
- Doorkeeper.configuration, server.client_via_uid, params)
+ Doorkeeper.configuration, params)
end
def authorization
diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb
index 096e1e2ee96..0b21c355a54 100644
--- a/lib/microsoft_teams/notifier.rb
+++ b/lib/microsoft_teams/notifier.rb
@@ -19,7 +19,7 @@ module MicrosoftTeams
result = true if response
rescue Gitlab::HTTP::Error, StandardError => error
- Rails.logger.info("#{self.class.name}: Error while connecting to #{@webhook}: #{error.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.info("#{self.class.name}: Error while connecting to #{@webhook}: #{error.message}")
end
result
diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb
index d0777914cb5..cc536ce9b46 100644
--- a/lib/object_storage/config.rb
+++ b/lib/object_storage/config.rb
@@ -2,12 +2,26 @@
module ObjectStorage
class Config
+ AWS_PROVIDER = 'AWS'
+ AZURE_PROVIDER = 'AzureRM'
+ GOOGLE_PROVIDER = 'Google'
+
attr_reader :options
def initialize(options)
@options = options.to_hash.deep_symbolize_keys
end
+ def load_provider
+ if aws?
+ require 'fog/aws'
+ elsif google?
+ require 'fog/google'
+ elsif azure?
+ require 'fog/azurerm'
+ end
+ end
+
def credentials
@credentials ||= options[:connection] || {}
end
@@ -30,7 +44,7 @@ module ObjectStorage
# AWS-specific options
def aws?
- provider == 'AWS'
+ provider == AWS_PROVIDER
end
def use_iam_profile?
@@ -54,12 +68,18 @@ module ObjectStorage
end
# End AWS-specific options
+ # Begin Azure-specific options
+ def azure_storage_domain
+ credentials[:azure_storage_domain]
+ end
+ # End Azure-specific options
+
def google?
- provider == 'Google'
+ provider == GOOGLE_PROVIDER
end
def azure?
- provider == 'AzureRM'
+ provider == AZURE_PROVIDER
end
def fog_attributes
diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb
index 90199114f2c..b5864382299 100644
--- a/lib/object_storage/direct_upload.rb
+++ b/lib/object_storage/direct_upload.rb
@@ -99,12 +99,18 @@ module ObjectStorage
ObjectStorage: {
Provider: 'AzureRM',
GoCloudConfig: {
- URL: "azblob://#{bucket_name}"
+ URL: azure_gocloud_url
}
}
}
end
+ def azure_gocloud_url
+ url = "azblob://#{bucket_name}"
+ url += "?domain=#{config.azure_storage_domain}" if config.azure_storage_domain.present?
+ url
+ end
+
def use_workhorse_s3_client?
return false unless Feature.enabled?(:use_workhorse_s3_client, default_enabled: true)
return false unless config.use_iam_profile? || config.consolidated_settings?
diff --git a/lib/product_analytics/tracker.rb b/lib/product_analytics/tracker.rb
index d4a88b879f0..2dc5e1f53ce 100644
--- a/lib/product_analytics/tracker.rb
+++ b/lib/product_analytics/tracker.rb
@@ -7,5 +7,36 @@ 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/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb
index aff0ee52e0d..697ced3590b 100644
--- a/lib/system_check/app/redis_version_check.rb
+++ b/lib/system_check/app/redis_version_check.rb
@@ -37,7 +37,7 @@ module SystemCheck
@custom_error_message
)
for_more_information(
- 'doc/administration/high_availability/redis.md#provide-your-own-redis-instance-core-only'
+ 'doc/administration/high_availability/redis.md#provide-your-own-redis-instance'
)
fix_and_rerun
end
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 31030d061f2..1e28d15f75e 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -38,7 +38,9 @@ namespace :gettext do
Rake::Task['gettext:find'].invoke
# leave only the required changes.
- `git checkout -- locale/*/gitlab.po`
+ unless system(*%w(git checkout -- locale/*/gitlab.po))
+ raise 'failed to cleanup generated locale/*/gitlab.po files'
+ end
# Remove timestamps from the pot file
pot_content = File.read pot_file
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 69a3526b872..caa583fb3a9 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -55,6 +55,7 @@ namespace :gitlab do
rake:assets:precompile
gitlab:assets:compile_webpack_if_needed
gitlab:assets:fix_urls
+ gitlab:assets:check_page_bundle_mixins_css_for_sideeffects
].each(&::Gitlab::TaskHelpers.method(:invoke_and_time_task))
end
@@ -127,5 +128,10 @@ namespace :gitlab do
abort 'Error: Unable to compile webpack DLL.'.color(:red)
end
end
+
+ desc 'GitLab | Assets | Check that scss mixins do not introduce any sideffects'
+ task :check_page_bundle_mixins_css_for_sideeffects do
+ system('./scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js')
+ end
end
end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index b0f1ca39387..2a3713ed85c 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -47,6 +47,11 @@ namespace :gitlab do
begin
unless ENV['force'] == 'yes'
warning = <<-MSG.strip_heredoc
+ Be sure to stop Puma, Sidekiq, and any other process that
+ connects to the database before proceeding. For Omnibus
+ installs, see the following link for more information:
+ https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-for-omnibus-gitlab-installations
+
Before restoring the database, we will remove all existing
tables to avoid future upgrade problems. Be aware that if you have
custom tables in the GitLab database these tables and all data will be
@@ -131,7 +136,21 @@ namespace :gitlab do
task restore: :gitlab_environment do
puts_time "Restoring database ... ".color(:blue)
- Backup::Database.new(progress).restore
+ errors = Backup::Database.new(progress).restore
+
+ if errors.present?
+ warning = <<~MSG
+ There were errors in restoring the schema. This may cause
+ issues if this results in missing indexes, constraints, or
+ columns. Please record the errors above and contact GitLab
+ Support if you have questions:
+ https://about.gitlab.com/support/
+ MSG
+
+ warn warning.color(:red)
+ ask_to_continue
+ end
+
puts_time "done".color(:green)
end
end
@@ -273,5 +292,7 @@ namespace :gitlab do
$stdout
end
end
- end # namespace end: backup
-end # namespace end: gitlab
+ end
+ # namespace end: backup
+end
+# namespace end: gitlab
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 61318570fd5..425f66918b0 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -166,5 +166,12 @@ namespace :gitlab do
Rake::Task['db:test:load'].enhance do
Rake::Task['gitlab:db:create_dynamic_partitions'].invoke
end
+
+ desc 'reindex a regular (non-unique) index without downtime to eliminate bloat'
+ task :reindex, [:index_name] => :environment do |_, args|
+ raise ArgumentError, 'must give the index name to reindex' unless args[:index_name]
+
+ Gitlab::Database::ConcurrentReindex.new(args[:index_name], logger: Logger.new(STDOUT)).execute
+ end
end
end
diff --git a/lib/tasks/gitlab/lfs/migrate.rake b/lib/tasks/gitlab/lfs/migrate.rake
index 470a12c39cd..3d4c847a0f0 100644
--- a/lib/tasks/gitlab/lfs/migrate.rake
+++ b/lib/tasks/gitlab/lfs/migrate.rake
@@ -9,12 +9,12 @@ namespace :gitlab do
LfsObject.with_files_stored_locally
.find_each(batch_size: 10) do |lfs_object|
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- logger.info("Transferred LFS object #{lfs_object.oid} of size #{lfs_object.size.to_i.bytes} to object storage")
- rescue => e
- logger.error("Failed to transfer LFS object #{lfs_object.oid} with error: #{e.message}")
- end
+ logger.info("Transferred LFS object #{lfs_object.oid} of size #{lfs_object.size.to_i.bytes} to object storage")
+ rescue => e
+ logger.error("Failed to transfer LFS object #{lfs_object.oid} with error: #{e.message}")
+ end
end
task migrate_to_local: :environment do
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index eb3de195626..6f00db42d78 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -5,7 +5,7 @@ return if Rails.env.production?
namespace :gitlab do
namespace :sidekiq do
def write_yaml(path, banner, object)
- File.write(path, banner + YAML.dump(object))
+ File.write(path, banner + YAML.dump(object).gsub(/ *$/m, ''))
end
namespace :all_queues_yml do
diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake
new file mode 100644
index 00000000000..6f3db91c2b0
--- /dev/null
+++ b/lib/tasks/gitlab/usage_data.rake
@@ -0,0 +1,13 @@
+namespace :gitlab do
+ namespace :usage_data do
+ desc 'GitLab | UsageData | Generate raw SQLs for usage ping in YAML'
+ task dump_sql_in_yaml: :environment do
+ puts Gitlab::UsageDataQueries.uncached_data.to_yaml
+ end
+
+ desc 'GitLab | UsageData | Generate raw SQLs for usage ping in JSON'
+ task dump_sql_in_json: :environment do
+ puts Gitlab::Json.pretty_generate(Gitlab::UsageDataQueries.uncached_data)
+ end
+ end
+end
diff --git a/lib/tasks/pngquant.rake b/lib/tasks/pngquant.rake
index ceb4de55373..63bc1c7c16e 100644
--- a/lib/tasks/pngquant.rake
+++ b/lib/tasks/pngquant.rake
@@ -2,10 +2,10 @@ return if Rails.env.production?
require 'png_quantizator'
require 'parallel'
+require_relative '../../tooling/lib/tooling/images'
# The amount of variance (in bytes) allowed in
# file size when testing for compression size
-TOLERANCE = 10000
namespace :pngquant do
# Returns an array of all images eligible for compression
@@ -13,55 +13,13 @@ namespace :pngquant do
Dir.glob('doc/**/*.png', File::FNM_CASEFOLD)
end
- # Runs pngquant on an image and optionally
- # writes the result to disk
- def compress_image(file, overwrite_original)
- compressed_file = "#{file}.compressed"
- FileUtils.copy(file, compressed_file)
-
- pngquant_file = PngQuantizator::Image.new(compressed_file)
-
- # Run the image repeatedly through pngquant until
- # the change in file size is within TOLERANCE
- loop do
- before = File.size(compressed_file)
- pngquant_file.quantize!
- after = File.size(compressed_file)
- break if before - after <= TOLERANCE
- end
-
- savings = File.size(file) - File.size(compressed_file)
- is_uncompressed = savings > TOLERANCE
-
- if is_uncompressed && overwrite_original
- FileUtils.copy(compressed_file, file)
- end
-
- FileUtils.remove(compressed_file)
-
- [is_uncompressed, savings]
- end
-
- # Ensures pngquant is available and prints an error if not
- def check_executable
- unless system('pngquant --version', out: File::NULL)
- warn(
- 'Error: pngquant executable was not detected in the system.'.color(:red),
- 'Download pngquant at https://pngquant.org/ and place the executable in /usr/local/bin'.color(:green)
- )
- abort
- end
- end
-
desc 'GitLab | Pngquant | Compress all documentation PNG images using pngquant'
task :compress do
- check_executable
-
files = doc_images
puts "Compressing #{files.size} PNG files in doc/**"
Parallel.each(files) do |file|
- was_uncompressed, savings = compress_image(file, true)
+ was_uncompressed, savings = Tooling::Image.compress_image(file)
if was_uncompressed
puts "#{file} was reduced by #{savings} bytes"
@@ -71,13 +29,11 @@ namespace :pngquant do
desc 'GitLab | Pngquant | Checks that all documentation PNG images have been compressed with pngquant'
task :lint do
- check_executable
-
files = doc_images
puts "Checking #{files.size} PNG files in doc/**"
uncompressed_files = Parallel.map(files) do |file|
- is_uncompressed, _ = compress_image(file, false)
+ is_uncompressed, _ = Tooling::Image.compress_image(file, true)
if is_uncompressed
puts "Uncompressed file detected: ".color(:red) + file
file
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
index 73029c934f4..cd5943b552e 100644
--- a/lib/uploaded_file.rb
+++ b/lib/uploaded_file.rb
@@ -42,6 +42,32 @@ class UploadedFile
@remote_id = remote_id
end
+ def self.from_params_without_field(params, upload_paths)
+ path = params['path']
+ remote_id = params['remote_id']
+ return if path.blank? && remote_id.blank?
+
+ # don't use file_path if remote_id is set
+ if remote_id.present?
+ file_path = nil
+ elsif path.present?
+ file_path = File.realpath(path)
+
+ unless self.allowed_path?(file_path, Array(upload_paths).compact)
+ raise InvalidPathError, "insecure path used '#{file_path}'"
+ end
+ end
+
+ UploadedFile.new(
+ file_path,
+ filename: params['name'],
+ content_type: params['type'] || 'application/octet-stream',
+ sha256: params['sha256'],
+ remote_id: remote_id,
+ size: params['size']
+ )
+ end
+
def self.from_params(params, field, upload_paths, path_override = nil)
path = path_override || params["#{field}.path"]
remote_id = params["#{field}.remote_id"]
@@ -52,8 +78,7 @@ class UploadedFile
elsif path.present?
file_path = File.realpath(path)
- paths = Array(upload_paths) << Dir.tmpdir
- unless self.allowed_path?(file_path, paths.compact)
+ unless self.allowed_path?(file_path, Array(upload_paths).compact)
raise InvalidPathError, "insecure path used '#{file_path}'"
end
end