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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /lib/gitlab
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'lib/gitlab')
-rw-r--r--lib/gitlab/alert_management/alert_params.rb46
-rw-r--r--lib/gitlab/alert_management/alert_status_counts.rb10
-rw-r--r--lib/gitlab/alert_management/payload/base.rb22
-rw-r--r--lib/gitlab/alert_management/payload/generic.rb4
-rw-r--r--lib/gitlab/alerting/alert.rb215
-rw-r--r--lib/gitlab/alerting/alert_annotation.rb11
-rw-r--r--lib/gitlab/alerting/notification_payload_parser.rb107
-rw-r--r--lib/gitlab/application_context.rb4
-rw-r--r--lib/gitlab/auth.rb2
-rw-r--r--lib/gitlab/auth/auth_finders.rb2
-rw-r--r--lib/gitlab/auth/otp/strategies/base.rb32
-rw-r--r--lib/gitlab/auth/otp/strategies/devise.rb15
-rw-r--r--lib/gitlab/auth/otp/strategies/forti_authenticator.rb41
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb4
-rw-r--r--lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule.rb73
-rw-r--r--lib/gitlab/background_migration/backfill_snippet_repositories.rb2
-rw-r--r--lib/gitlab/background_migration/migrate_u2f_webauthn.rb46
-rw-r--r--lib/gitlab/background_migration/migrate_users_bio_to_user_details.rb2
-rw-r--r--lib/gitlab/background_migration/remove_duplicated_cs_findings_without_vulnerability_id.rb13
-rw-r--r--lib/gitlab/background_migration/replace_blocked_by_links.rb29
-rw-r--r--lib/gitlab/background_migration/sync_blocking_issues_count.rb13
-rw-r--r--lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser.rb25
-rw-r--r--lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser/isolated_mentioned_group_parser.rb25
-rw-r--r--lib/gitlab/background_migration/user_mentions/lib/gitlab/isolated_reference_extractor.rb30
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb5
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/concerns/namespace/recursive_traversal.rb74
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/group.rb91
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/namespace.rb33
-rw-r--r--lib/gitlab/bulk_import/client.rb72
-rw-r--r--lib/gitlab/checks/matching_merge_request.rb2
-rw-r--r--lib/gitlab/checks/post_push_message.rb6
-rw-r--r--lib/gitlab/ci/ansi2json/converter.rb23
-rw-r--r--lib/gitlab/ci/ansi2json/line.rb7
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb3
-rw-r--r--lib/gitlab/ci/artifact_file_reader.rb27
-rw-r--r--lib/gitlab/ci/config.rb4
-rw-r--r--lib/gitlab/ci/config/entry/bridge.rb19
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb24
-rw-r--r--lib/gitlab/ci/config/entry/include.rb6
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb29
-rw-r--r--lib/gitlab/ci/config/entry/needs.rb23
-rw-r--r--lib/gitlab/ci/config/entry/ports.rb26
-rw-r--r--lib/gitlab/ci/config/entry/product/matrix.rb2
-rw-r--r--lib/gitlab/ci/config/entry/product/variables.rb6
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb3
-rw-r--r--lib/gitlab/ci/config/entry/rules.rb21
-rw-r--r--lib/gitlab/ci/config/entry/services.rb26
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb22
-rw-r--r--lib/gitlab/ci/features.rb30
-rw-r--r--lib/gitlab/ci/lint.rb7
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb34
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/process.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/build/cache.rb4
-rw-r--r--lib/gitlab/ci/reports/test_case.rb9
-rw-r--r--lib/gitlab/ci/reports/test_suite.rb16
-rw-r--r--lib/gitlab/ci/runner/backoff.rb75
-rw-r--r--lib/gitlab/ci/status/bridge/action.rb12
-rw-r--r--lib/gitlab/ci/status/bridge/common.rb1
-rw-r--r--lib/gitlab/ci/status/bridge/factory.rb5
-rw-r--r--lib/gitlab/ci/status/bridge/manual.rb12
-rw-r--r--lib/gitlab/ci/status/bridge/play.rb19
-rw-r--r--lib/gitlab/ci/status/canceled.rb4
-rw-r--r--lib/gitlab/ci/status/created.rb4
-rw-r--r--lib/gitlab/ci/status/failed.rb4
-rw-r--r--lib/gitlab/ci/status/manual.rb4
-rw-r--r--lib/gitlab/ci/status/pending.rb4
-rw-r--r--lib/gitlab/ci/status/preparing.rb4
-rw-r--r--lib/gitlab/ci/status/running.rb4
-rw-r--r--lib/gitlab/ci/status/scheduled.rb4
-rw-r--r--lib/gitlab/ci/status/skipped.rb4
-rw-r--r--lib/gitlab/ci/status/success.rb4
-rw-r--r--lib/gitlab/ci/status/waiting_for_resource.rb4
-rw-r--r--lib/gitlab/ci/templates/AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml11
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Bash.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Clojure.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Crystal.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Django.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Elixir.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml14
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml16
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml9
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy/EC2.gitlab-ci.yml39
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml1
-rw-r--r--lib/gitlab/ci/templates/Laravel.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/PHP.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Ruby.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Rust.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml1
-rw-r--r--lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml43
-rw-r--r--lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml22
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml53
-rw-r--r--lib/gitlab/ci/trace.rb13
-rw-r--r--lib/gitlab/ci/trace/checksum.rb83
-rw-r--r--lib/gitlab/ci/trace/metrics.rb33
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb4
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb8
-rw-r--r--lib/gitlab/cleanup/orphan_lfs_file_references.rb5
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb4
-rw-r--r--lib/gitlab/code_navigation_path.rb1
-rw-r--r--lib/gitlab/config/entry/composable_array.rb58
-rw-r--r--lib/gitlab/config/entry/composable_hash.rb47
-rw-r--r--lib/gitlab/config/entry/factory.rb2
-rw-r--r--lib/gitlab/config/entry/legacy_validation_helpers.rb14
-rw-r--r--lib/gitlab/config/entry/simplifiable.rb2
-rw-r--r--lib/gitlab/config/entry/validators.rb8
-rw-r--r--lib/gitlab/conflict/file_collection.rb8
-rw-r--r--lib/gitlab/danger/commit_linter.rb4
-rw-r--r--lib/gitlab/danger/helper.rb11
-rw-r--r--lib/gitlab/danger/roulette.rb15
-rw-r--r--lib/gitlab/danger/teammate.rb7
-rw-r--r--lib/gitlab/data_builder/deployment.rb4
-rw-r--r--lib/gitlab/database.rb40
-rw-r--r--lib/gitlab/database/batch_count.rb48
-rw-r--r--lib/gitlab/database/bulk_update.rb168
-rw-r--r--lib/gitlab/database/concurrent_reindex.rb143
-rw-r--r--lib/gitlab/database/count/reltuples_count_strategy.rb3
-rw-r--r--lib/gitlab/database/migration_helpers.rb196
-rw-r--r--lib/gitlab/database/partitioning/partition_creator.rb4
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb2
-rw-r--r--lib/gitlab/database/postgres_index.rb31
-rw-r--r--lib/gitlab/database/reindexing.rb18
-rw-r--r--lib/gitlab/database/reindexing/concurrent_reindex.rb127
-rw-r--r--lib/gitlab/database/reindexing/coordinator.rb41
-rw-r--r--lib/gitlab/database/reindexing/reindex_action.rb35
-rw-r--r--lib/gitlab/database/schema_helpers.rb17
-rw-r--r--lib/gitlab/database/similarity_score.rb11
-rw-r--r--lib/gitlab/database/with_lock_retries.rb8
-rw-r--r--lib/gitlab/design_management/copy_design_collection_model_attributes.yml43
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_base.rb9
-rw-r--r--lib/gitlab/diff/highlight_cache.rb47
-rw-r--r--lib/gitlab/exclusive_lease_helpers.rb2
-rw-r--r--lib/gitlab/experimentation.rb46
-rw-r--r--lib/gitlab/git/diff.rb12
-rw-r--r--lib/gitlab/git/diff_collection.rb34
-rw-r--r--lib/gitlab/git/diff_stats_collection.rb4
-rw-r--r--lib/gitlab/git/repository.rb19
-rw-r--r--lib/gitlab/git/wiki.rb10
-rw-r--r--lib/gitlab/git_access.rb36
-rw-r--r--lib/gitlab/git_access_result/custom_action.rb2
-rw-r--r--lib/gitlab/git_access_snippet.rb25
-rw-r--r--lib/gitlab/git_access_wiki.rb10
-rw-r--r--lib/gitlab/gitaly_client/diff_stitcher.rb10
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb5
-rw-r--r--lib/gitlab/gitpod.rb8
-rw-r--r--lib/gitlab/gl_repository.rb10
-rw-r--r--lib/gitlab/gl_repository/identifier.rb13
-rw-r--r--lib/gitlab/gl_repository/repo_type.rb6
-rw-r--r--lib/gitlab/gon_helper.rb10
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb4
-rw-r--r--lib/gitlab/graphql/global_id_compatibility.rb20
-rw-r--r--lib/gitlab/graphql/markdown_field.rb8
-rw-r--r--lib/gitlab/graphql/markdown_field/resolver.rb22
-rw-r--r--lib/gitlab/graphql/pagination/keyset/connection.rb3
-rw-r--r--lib/gitlab/graphql/pagination/keyset/last_items.rb57
-rw-r--r--lib/gitlab/graphql/pagination/keyset/order_info.rb16
-rw-r--r--lib/gitlab/graphql/query_analyzers/logger_analyzer.rb11
-rw-r--r--lib/gitlab/group_search_results.rb6
-rw-r--r--lib/gitlab/health_checks/unicorn_check.rb2
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb4
-rw-r--r--lib/gitlab/import_export/base/relation_factory.rb30
-rw-r--r--lib/gitlab/import_export/file_importer.rb4
-rw-r--r--lib/gitlab/import_export/json/ndjson_reader.rb6
-rw-r--r--lib/gitlab/import_export/lfs_saver.rb8
-rw-r--r--lib/gitlab/import_export/members_mapper.rb6
-rw-r--r--lib/gitlab/import_export/project/import_export.yml6
-rw-r--r--lib/gitlab/import_export/project/sample/date_calculator.rb37
-rw-r--r--lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb51
-rw-r--r--lib/gitlab/import_export/project/tree_restorer.rb10
-rw-r--r--lib/gitlab/import_export/project/tree_saver.rb2
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb14
-rw-r--r--lib/gitlab/import_export/saver.rb4
-rw-r--r--lib/gitlab/import_export/uploads_manager.rb2
-rw-r--r--lib/gitlab/import_export/version_checker.rb4
-rw-r--r--lib/gitlab/instrumentation/redis.rb2
-rw-r--r--lib/gitlab/issuables_count_for_state.rb57
-rw-r--r--lib/gitlab/job_waiter.rb17
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb15
-rw-r--r--lib/gitlab/kubernetes/pod.rb36
-rw-r--r--lib/gitlab/lfs/client.rb44
-rw-r--r--lib/gitlab/manifest_import/metadata.rb49
-rw-r--r--lib/gitlab/marginalia.rb10
-rw-r--r--lib/gitlab/marginalia/active_record_instrumentation.rb2
-rw-r--r--lib/gitlab/markdown_cache.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb49
-rw-r--r--lib/gitlab/metrics/dashboard/stages/custom_dashboard_metrics_inserter.rb24
-rw-r--r--lib/gitlab/metrics/dashboard/stages/url_validator.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/transformers/errors.rb6
-rw-r--r--lib/gitlab/metrics/requests_rack_middleware.rb43
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb2
-rw-r--r--lib/gitlab/middleware/go.rb9
-rw-r--r--lib/gitlab/middleware/handle_null_bytes.rb61
-rw-r--r--lib/gitlab/middleware/multipart.rb3
-rw-r--r--lib/gitlab/pagination/offset_pagination.rb10
-rw-r--r--lib/gitlab/project_search_results.rb4
-rw-r--r--lib/gitlab/project_template.rb58
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb12
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb14
-rw-r--r--lib/gitlab/redis/hll.rb20
-rw-r--r--lib/gitlab/regex.rb90
-rw-r--r--lib/gitlab/relative_positioning/item_context.rb4
-rw-r--r--lib/gitlab/repo_path.rb15
-rw-r--r--lib/gitlab/repository_size_checker.rb11
-rw-r--r--lib/gitlab/sample_data_template.rb22
-rw-r--r--lib/gitlab/search/recent_items.rb21
-rw-r--r--lib/gitlab/search_results.rb67
-rw-r--r--lib/gitlab/seeder.rb11
-rw-r--r--lib/gitlab/sidekiq_daemon/memory_killer.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/worker_context/server.rb6
-rw-r--r--lib/gitlab/slash_commands/presenters/help.rb2
-rw-r--r--lib/gitlab/static_site_editor/config/file_config.rb35
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/global.rb39
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb26
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/mount.rb39
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb33
-rw-r--r--lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb26
-rw-r--r--lib/gitlab/static_site_editor/config/generated_config.rb12
-rw-r--r--lib/gitlab/subscription_portal.rb11
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb2
-rw-r--r--lib/gitlab/themes.rb24
-rw-r--r--lib/gitlab/usage_data.rb104
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb3
-rw-r--r--lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb124
-rw-r--r--lib/gitlab/usage_data_counters/known_events.yml97
-rw-r--r--lib/gitlab/usage_data_counters/static_site_editor_counter.rb16
-rw-r--r--lib/gitlab/usage_data_queries.rb2
-rw-r--r--lib/gitlab/utils/usage_data.rb5
-rw-r--r--lib/gitlab/view/presenter/factory.rb2
-rw-r--r--lib/gitlab/visibility_level_checker.rb2
-rw-r--r--lib/gitlab/webpack/dev_server_middleware.rb9
-rw-r--r--lib/gitlab/webpack/manifest.rb108
-rw-r--r--lib/gitlab/whats_new.rb28
-rw-r--r--lib/gitlab/workhorse.rb3
245 files changed, 4067 insertions, 1265 deletions
diff --git a/lib/gitlab/alert_management/alert_params.rb b/lib/gitlab/alert_management/alert_params.rb
deleted file mode 100644
index 3bb839c1114..00000000000
--- a/lib/gitlab/alert_management/alert_params.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module AlertManagement
- class AlertParams
- MONITORING_TOOLS = {
- prometheus: 'Prometheus'
- }.freeze
-
- def self.from_generic_alert(project:, payload:)
- parsed_payload = Gitlab::Alerting::NotificationPayloadParser.call(payload, project).with_indifferent_access
- annotations = parsed_payload[:annotations]
-
- {
- project_id: project.id,
- title: annotations[:title],
- description: annotations[:description],
- monitoring_tool: annotations[:monitoring_tool],
- service: annotations[:service],
- hosts: Array(annotations[:hosts]),
- payload: payload,
- started_at: parsed_payload['startsAt'],
- ended_at: parsed_payload['endsAt'],
- severity: annotations[:severity],
- fingerprint: annotations[:fingerprint],
- environment: annotations[:environment]
- }
- end
-
- def self.from_prometheus_alert(project:, parsed_alert:)
- {
- project_id: project.id,
- title: parsed_alert.title,
- description: parsed_alert.description,
- monitoring_tool: MONITORING_TOOLS[:prometheus],
- payload: parsed_alert.payload,
- started_at: parsed_alert.starts_at,
- ended_at: parsed_alert.ends_at,
- fingerprint: parsed_alert.gitlab_fingerprint,
- environment: parsed_alert.environment,
- prometheus_alert: parsed_alert.gitlab_alert
- }
- end
- end
- end
-end
diff --git a/lib/gitlab/alert_management/alert_status_counts.rb b/lib/gitlab/alert_management/alert_status_counts.rb
index 382026236e0..e55e0016599 100644
--- a/lib/gitlab/alert_management/alert_status_counts.rb
+++ b/lib/gitlab/alert_management/alert_status_counts.rb
@@ -6,8 +6,6 @@ module Gitlab
class AlertStatusCounts
include Gitlab::Utils::StrongMemoize
- STATUSES = ::AlertManagement::Alert::STATUSES
-
attr_reader :project
def self.declarative_policy_class
@@ -21,7 +19,7 @@ module Gitlab
end
# Define method for each status
- STATUSES.each_key do |status|
+ ::AlertManagement::Alert.status_names.each do |status|
define_method(status) { counts[status] }
end
@@ -30,7 +28,7 @@ module Gitlab
end
def all
- counts.values.sum # rubocop:disable CodeReuse/ActiveRecord
+ counts.values.sum
end
private
@@ -44,9 +42,7 @@ module Gitlab
end
def counts_by_status
- ::AlertManagement::AlertsFinder
- .counts_by_status(current_user, project, params)
- .transform_keys { |status_id| STATUSES.key(status_id) }
+ ::AlertManagement::AlertsFinder.counts_by_status(current_user, project, params)
end
end
end
diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb
index 74e47e5226e..0fd593a3780 100644
--- a/lib/gitlab/alert_management/payload/base.rb
+++ b/lib/gitlab/alert_management/payload/base.rb
@@ -88,19 +88,19 @@ module Gitlab
# AlertManagement::Alert directly for read operations.
def alert_params
{
- description: description,
+ description: description&.truncate(::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH),
ended_at: ends_at,
environment: environment,
fingerprint: gitlab_fingerprint,
- hosts: Array(hosts),
- monitoring_tool: monitoring_tool,
+ hosts: truncate_hosts(Array(hosts).flatten),
+ monitoring_tool: monitoring_tool&.truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH),
payload: payload,
project_id: project.id,
prometheus_alert: gitlab_alert,
- service: service,
+ service: service&.truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH),
severity: severity,
started_at: starts_at,
- title: title
+ title: title&.truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH)
}.transform_values(&:presence).compact
end
@@ -135,6 +135,18 @@ module Gitlab
def plain_gitlab_fingerprint; end
+ def truncate_hosts(hosts)
+ return hosts if hosts.join.length <= ::AlertManagement::Alert::HOSTS_MAX_LENGTH
+
+ hosts.inject([]) do |new_hosts, host|
+ remaining_length = ::AlertManagement::Alert::HOSTS_MAX_LENGTH - new_hosts.join.length
+
+ break new_hosts unless remaining_length > 0
+
+ new_hosts << host.to_s.truncate(remaining_length, omission: '')
+ end
+ end
+
def value_for_paths(paths)
target_path = paths.find { |path| payload&.dig(*path) }
diff --git a/lib/gitlab/alert_management/payload/generic.rb b/lib/gitlab/alert_management/payload/generic.rb
index 7efdfac75dc..e8e85155bef 100644
--- a/lib/gitlab/alert_management/payload/generic.rb
+++ b/lib/gitlab/alert_management/payload/generic.rb
@@ -8,6 +8,8 @@ module Gitlab
DEFAULT_TITLE = 'New: Incident'
DEFAULT_SEVERITY = 'critical'
+ attribute :description, paths: 'description'
+ attribute :ends_at, paths: 'end_time', type: :time
attribute :environment_name, paths: 'gitlab_environment_name'
attribute :hosts, paths: 'hosts'
attribute :monitoring_tool, paths: 'monitoring_tool'
@@ -23,3 +25,5 @@ module Gitlab
end
end
end
+
+Gitlab::AlertManagement::Payload::Generic.prepend_if_ee('EE::Gitlab::AlertManagement::Payload::Generic')
diff --git a/lib/gitlab/alerting/alert.rb b/lib/gitlab/alerting/alert.rb
deleted file mode 100644
index 94b81b7d290..00000000000
--- a/lib/gitlab/alerting/alert.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Alerting
- class Alert
- include ActiveModel::Model
- include Gitlab::Utils::StrongMemoize
- include Presentable
-
- attr_accessor :project, :payload, :am_alert
-
- def self.for_alert_management_alert(project:, alert:)
- params = if alert.prometheus?
- alert.payload
- else
- Gitlab::Alerting::NotificationPayloadParser.call(alert.payload.to_h, alert.project)
- end
-
- self.new(project: project, payload: params, am_alert: alert)
- end
-
- def gitlab_alert
- strong_memoize(:gitlab_alert) do
- parse_gitlab_alert_from_payload
- end
- end
-
- def metric_id
- strong_memoize(:metric_id) do
- payload&.dig('labels', 'gitlab_alert_id')
- end
- end
-
- def gitlab_prometheus_alert_id
- strong_memoize(:gitlab_prometheus_alert_id) do
- payload&.dig('labels', 'gitlab_prometheus_alert_id')
- end
- end
-
- def title
- strong_memoize(:title) do
- gitlab_alert&.title || parse_title_from_payload
- end
- end
-
- def description
- strong_memoize(:description) do
- parse_description_from_payload
- end
- end
-
- def environment
- strong_memoize(:environment) do
- gitlab_alert&.environment || parse_environment_from_payload
- end
- end
-
- def annotations
- strong_memoize(:annotations) do
- parse_annotations_from_payload || []
- end
- end
-
- def starts_at
- strong_memoize(:starts_at) do
- parse_datetime_from_payload('startsAt')
- end
- end
-
- def starts_at_raw
- strong_memoize(:starts_at_raw) do
- payload&.dig('startsAt')
- end
- end
-
- def ends_at
- strong_memoize(:ends_at) do
- parse_datetime_from_payload('endsAt')
- end
- end
-
- def full_query
- strong_memoize(:full_query) do
- gitlab_alert&.full_query || parse_expr_from_payload
- end
- end
-
- def y_label
- strong_memoize(:y_label) do
- parse_y_label_from_payload || title
- end
- end
-
- def alert_markdown
- strong_memoize(:alert_markdown) do
- parse_alert_markdown_from_payload
- end
- end
-
- def status
- strong_memoize(:status) do
- payload&.dig('status')
- end
- end
-
- def firing?
- status == 'firing'
- end
-
- def resolved?
- status == 'resolved'
- end
-
- def gitlab_managed?
- metric_id.present?
- end
-
- def gitlab_fingerprint
- Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint)
- end
-
- def valid?
- payload.respond_to?(:dig) && project && title && starts_at
- end
-
- def present
- super(presenter_class: Projects::Prometheus::AlertPresenter)
- end
-
- private
-
- def plain_gitlab_fingerprint
- if gitlab_managed?
- [metric_id, starts_at_raw].join('/')
- else # self managed
- [starts_at_raw, title, full_query].join('/')
- end
- end
-
- def parse_environment_from_payload
- environment_name = payload&.dig('labels', 'gitlab_environment_name')
-
- return unless environment_name
-
- EnvironmentsFinder.new(project, nil, { name: environment_name })
- .find
- &.first
- end
-
- def parse_gitlab_alert_from_payload
- alerts_found = matching_gitlab_alerts
-
- return if alerts_found.blank? || alerts_found.size > 1
-
- alerts_found.first
- end
-
- def matching_gitlab_alerts
- return unless metric_id || gitlab_prometheus_alert_id
-
- Projects::Prometheus::AlertsFinder
- .new(project: project, metric: metric_id, id: gitlab_prometheus_alert_id)
- .execute
- end
-
- def parse_title_from_payload
- payload&.dig('annotations', 'title') ||
- payload&.dig('annotations', 'summary') ||
- payload&.dig('labels', 'alertname')
- end
-
- def parse_description_from_payload
- payload&.dig('annotations', 'description')
- end
-
- def parse_annotations_from_payload
- payload&.dig('annotations')&.map do |label, value|
- Alerting::AlertAnnotation.new(label: label, value: value)
- end
- end
-
- def parse_datetime_from_payload(field)
- value = payload&.dig(field)
- return unless value
-
- # value is a rfc3339 timestamp
- # Timestamps from Prometheus and Alertmanager are UTC RFC3339 timestamps like: '2018-03-12T09:06:00Z' (Z represents 0 offset or UTC)
- # .utc sets the datetime zone to `UTC`
- Time.rfc3339(value).utc
- rescue ArgumentError
- end
-
- # Parses `g0.expr` from `generatorURL`.
- #
- # Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1
- def parse_expr_from_payload
- url = payload&.dig('generatorURL')
- return unless url
-
- uri = URI(url)
-
- Rack::Utils.parse_query(uri.query).fetch('g0.expr')
- rescue URI::InvalidURIError, KeyError
- end
-
- def parse_alert_markdown_from_payload
- payload&.dig('annotations', 'gitlab_incident_markdown')
- end
-
- def parse_y_label_from_payload
- payload&.dig('annotations', 'gitlab_y_label')
- end
- end
- end
-end
diff --git a/lib/gitlab/alerting/alert_annotation.rb b/lib/gitlab/alerting/alert_annotation.rb
deleted file mode 100644
index a4b3a97b08c..00000000000
--- a/lib/gitlab/alerting/alert_annotation.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Alerting
- class AlertAnnotation
- include ActiveModel::Model
-
- attr_accessor :label, :value
- end
- end
-end
diff --git a/lib/gitlab/alerting/notification_payload_parser.rb b/lib/gitlab/alerting/notification_payload_parser.rb
deleted file mode 100644
index 348f851f551..00000000000
--- a/lib/gitlab/alerting/notification_payload_parser.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Alerting
- class NotificationPayloadParser
- BadPayloadError = Class.new(StandardError)
-
- DEFAULT_TITLE = 'New: Incident'
- DEFAULT_SEVERITY = 'critical'
-
- def initialize(payload, project)
- @payload = payload.to_h.with_indifferent_access
- @project = project
- end
-
- def self.call(payload, project)
- new(payload, project).call
- end
-
- def call
- {
- 'annotations' => annotations,
- 'startsAt' => starts_at,
- 'endsAt' => ends_at
- }.compact
- end
-
- private
-
- attr_reader :payload, :project
-
- def title
- payload[:title].presence || DEFAULT_TITLE
- end
-
- def severity
- payload[:severity].presence || DEFAULT_SEVERITY
- end
-
- def fingerprint
- Gitlab::AlertManagement::Fingerprint.generate(payload[:fingerprint])
- end
-
- def annotations
- primary_params
- .reverse_merge(flatten_secondary_params)
- .transform_values(&:presence)
- .compact
- end
-
- def primary_params
- {
- 'title' => title,
- 'description' => payload[:description],
- 'monitoring_tool' => payload[:monitoring_tool],
- 'service' => payload[:service],
- 'hosts' => hosts.presence,
- 'severity' => severity,
- 'fingerprint' => fingerprint,
- 'environment' => environment
- }
- end
-
- def hosts
- Array(payload[:hosts]).reject(&:blank?)
- end
-
- def current_time
- Time.current.change(usec: 0).rfc3339
- end
-
- def starts_at
- Time.parse(payload[:start_time].to_s).rfc3339
- rescue ArgumentError
- 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, :end_time)
- end
-
- def flatten_secondary_params
- Gitlab::Utils::SafeInlineHash.merge_keys!(secondary_params)
- rescue ArgumentError
- raise BadPayloadError, 'The payload is too big'
- end
- end
- end
-end
-
-Gitlab::Alerting::NotificationPayloadParser.prepend_if_ee('EE::Gitlab::Alerting::NotificationPayloadParser')
diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb
index 30cb74bcf54..b4bbb309c36 100644
--- a/lib/gitlab/application_context.rb
+++ b/lib/gitlab/application_context.rb
@@ -12,7 +12,8 @@ module Gitlab
Attribute.new(:namespace, Namespace),
Attribute.new(:user, User),
Attribute.new(:caller_id, String),
- Attribute.new(:related_class, String)
+ Attribute.new(:related_class, String),
+ Attribute.new(:feature_category, String)
].freeze
def self.with_context(args, &block)
@@ -45,6 +46,7 @@ module Gitlab
hash[:root_namespace] = -> { root_namespace_path } if include_namespace?
hash[:caller_id] = caller_id if set_values.include?(:caller_id)
hash[:related_class] = related_class if set_values.include?(:related_class)
+ hash[:feature_category] = feature_category if set_values.include?(:feature_category)
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 609eef5e365..001c083c778 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -371,7 +371,7 @@ module Gitlab
end
def find_build_by_token(token)
- ::Ci::Build.running.find_by_token(token)
+ ::Ci::AuthJobFinder.new(token: token).execute
end
def user_auth_attempt!(user, success:)
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index ccf52bae9a5..3d3f7212053 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -290,7 +290,7 @@ module Gitlab
end
def api_request?
- current_request.path.starts_with?('/api/')
+ current_request.path.starts_with?(Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/api/'))
end
def archive_request?
diff --git a/lib/gitlab/auth/otp/strategies/base.rb b/lib/gitlab/auth/otp/strategies/base.rb
new file mode 100644
index 00000000000..718630e0e31
--- /dev/null
+++ b/lib/gitlab/auth/otp/strategies/base.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Otp
+ module Strategies
+ class Base
+ def initialize(user)
+ @user = user
+ end
+
+ private
+
+ attr_reader :user
+
+ def success
+ { status: :success }
+ end
+
+ def error(message, http_status = nil)
+ result = { message: message,
+ status: :error }
+
+ result[:http_status] = http_status if http_status
+
+ result
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/otp/strategies/devise.rb b/lib/gitlab/auth/otp/strategies/devise.rb
new file mode 100644
index 00000000000..93068d6c9b0
--- /dev/null
+++ b/lib/gitlab/auth/otp/strategies/devise.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Otp
+ module Strategies
+ class Devise < Base
+ def validate(otp_code)
+ user.validate_and_consume_otp!(otp_code) ? success : error('invalid OTP code')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb
new file mode 100644
index 00000000000..fbcb9fd8cdb
--- /dev/null
+++ b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Auth
+ module Otp
+ module Strategies
+ class FortiAuthenticator < Base
+ def validate(otp_code)
+ body = { username: user.username,
+ token_code: otp_code }
+
+ response = Gitlab::HTTP.post(
+ auth_url,
+ headers: { 'Content-Type': 'application/json' },
+ body: body.to_json,
+ basic_auth: api_credentials)
+
+ # Successful authentication results in HTTP 200: OK
+ # https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/704555/authentication-auth
+ response.ok? ? success : error(message: response.message, http_status: response.code)
+ end
+
+ private
+
+ def auth_url
+ host = ::Gitlab.config.forti_authenticator.host
+ port = ::Gitlab.config.forti_authenticator.port
+ path = 'api/v1/auth/'
+
+ "https://#{host}:#{port}/#{path}"
+ end
+
+ def api_credentials
+ { username: ::Gitlab.config.forti_authenticator.username,
+ password: ::Gitlab.config.forti_authenticator.token }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
index cc4b8d887ff..36b54ba2e46 100644
--- a/lib/gitlab/auth/user_access_denied_reason.rb
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -11,6 +11,8 @@ module Gitlab
case rejection_type
when :internal
"This action cannot be performed by internal users"
+ when :blocked_pending_approval
+ "Your account is pending approval from your administrator and hence blocked."
when :terms_not_accepted
"You (#{@user.to_reference}) must accept the Terms of Service in order to perform this action. "\
"Please access GitLab from a web browser to accept these terms."
@@ -31,6 +33,8 @@ module Gitlab
def rejection_type
if @user.internal?
:internal
+ elsif @user.blocked_pending_approval?
+ :blocked_pending_approval
elsif @user.required_terms_not_accepted?
:terms_not_accepted
elsif @user.deactivated?
diff --git a/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule.rb b/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule.rb
new file mode 100644
index 00000000000..2148e96f6b4
--- /dev/null
+++ b/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Compare all current rules to project rules
+ class AddModifiedToApprovalMergeRequestRule
+ # Stubbed class to access the Group table
+ class Group < ActiveRecord::Base
+ self.table_name = 'namespaces'
+ self.inheritance_column = :_type_disabled
+ end
+
+ # Stubbed class to access the ApprovalMergeRequestRule table
+ class ApprovalMergeRequestRule < ActiveRecord::Base
+ self.table_name = 'approval_merge_request_rules'
+
+ has_one :approval_merge_request_rule_source, class_name: 'AddModifiedToApprovalMergeRequestRule::ApprovalMergeRequestRuleSource'
+ has_one :approval_project_rule, through: :approval_merge_request_rule_source
+ has_and_belongs_to_many :groups,
+ class_name: 'AddModifiedToApprovalMergeRequestRule::Group', join_table: "#{self.table_name}_groups"
+ end
+
+ # Stubbed class to access the ApprovalProjectRule table
+ class ApprovalProjectRule < ActiveRecord::Base
+ self.table_name = 'approval_project_rules'
+
+ has_many :approval_merge_request_rule_sources, class_name: 'AddModifiedToApprovalMergeRequestRule::ApprovalMergeRequestRuleSource'
+ has_and_belongs_to_many :groups,
+ class_name: 'AddModifiedToApprovalMergeRequestRule::Group', join_table: "#{self.table_name}_groups"
+ end
+
+ # Stubbed class to access the ApprovalMergeRequestRuleSource table
+ class ApprovalMergeRequestRuleSource < ActiveRecord::Base
+ self.table_name = 'approval_merge_request_rule_sources'
+
+ belongs_to :approval_merge_request_rule, class_name: 'AddModifiedToApprovalMergeRequestRule::ApprovalMergeRequestRule'
+ belongs_to :approval_project_rule, class_name: 'AddModifiedToApprovalMergeRequestRule::ApprovalProjectRule'
+ end
+
+ def perform(start_id, stop_id)
+ approval_merge_requests_rules = ApprovalMergeRequestRule
+ .joins(:groups, :approval_merge_request_rule_source)
+ .where(id: start_id..stop_id)
+ .pluck(
+ 'approval_merge_request_rule_sources.id as ars_id',
+ 'approval_merge_request_rules_groups.id as amrg_id'
+ )
+
+ approval_project_rules = ApprovalProjectRule
+ .joins(:groups, approval_merge_request_rule_sources: :approval_merge_request_rule)
+ .where(approval_merge_request_rules: { id: start_id..stop_id })
+ .pluck(
+ 'approval_merge_request_rule_sources.id as ars_id',
+ 'approval_project_rules_groups.id as apg_id'
+ )
+
+ different_names_or_approval_sources = ApprovalMergeRequestRule.joins(:approval_project_rule, :approval_merge_request_rule_source)
+ .where(id: start_id..stop_id)
+ .where('approval_merge_request_rules.name != approval_project_rules.name OR ' \
+ 'approval_merge_request_rules.approvals_required != approval_project_rules.approvals_required')
+ .pluck('approval_merge_request_rule_sources.id as ars_id')
+
+ intersected_set = approval_merge_requests_rules.to_set ^ approval_project_rules.to_set
+ source_ids = intersected_set.collect { |rule| rule[0] }.uniq
+
+ rule_sources = ApprovalMergeRequestRuleSource.where(id: source_ids + different_names_or_approval_sources)
+ changed_merge_request_rules = ApprovalMergeRequestRule.where(id: rule_sources.select(:approval_merge_request_rule_id))
+
+ changed_merge_request_rules.update_all(modified_from_project_rule: true)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_snippet_repositories.rb b/lib/gitlab/background_migration/backfill_snippet_repositories.rb
index 21538000fec..8befade8c3a 100644
--- a/lib/gitlab/background_migration/backfill_snippet_repositories.rb
+++ b/lib/gitlab/background_migration/backfill_snippet_repositories.rb
@@ -109,7 +109,7 @@ module Gitlab
end
def create_commit(snippet)
- snippet.snippet_repository.multi_files_action(commit_author(snippet), snippet_action(snippet), commit_attrs)
+ snippet.snippet_repository.multi_files_action(commit_author(snippet), snippet_action(snippet), **commit_attrs)
end
# If the user is not allowed to access git or update the snippet
diff --git a/lib/gitlab/background_migration/migrate_u2f_webauthn.rb b/lib/gitlab/background_migration/migrate_u2f_webauthn.rb
new file mode 100644
index 00000000000..b8c14aa2573
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_u2f_webauthn.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+require "webauthn/u2f_migrator"
+
+module Gitlab
+ module BackgroundMigration
+ class MigrateU2fWebauthn
+ class U2fRegistration < ActiveRecord::Base
+ self.table_name = 'u2f_registrations'
+ end
+
+ class WebauthnRegistration < ActiveRecord::Base
+ self.table_name = 'webauthn_registrations'
+ end
+
+ def perform(start_id, end_id)
+ old_registrations = U2fRegistration.where(id: start_id..end_id)
+ old_registrations.each_slice(100) do |slice|
+ now = Time.now
+ values = slice.map do |u2f_registration|
+ converted_credential = WebAuthn::U2fMigrator.new(
+ app_id: Gitlab.config.gitlab.url,
+ certificate: u2f_registration.certificate,
+ key_handle: u2f_registration.key_handle,
+ public_key: u2f_registration.public_key,
+ counter: u2f_registration.counter
+ ).credential
+
+ {
+ credential_xid: Base64.strict_encode64(converted_credential.id),
+ public_key: Base64.strict_encode64(converted_credential.public_key),
+ counter: u2f_registration.counter || 0,
+ name: u2f_registration.name || '',
+ user_id: u2f_registration.user_id,
+ u2f_registration_id: u2f_registration.id,
+ created_at: now,
+ updated_at: now
+ }
+ end
+
+ WebauthnRegistration.insert_all(values, unique_by: :credential_xid, returning: false)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/migrate_users_bio_to_user_details.rb b/lib/gitlab/background_migration/migrate_users_bio_to_user_details.rb
index ca64d13b118..bbe2164ae4e 100644
--- a/lib/gitlab/background_migration/migrate_users_bio_to_user_details.rb
+++ b/lib/gitlab/background_migration/migrate_users_bio_to_user_details.rb
@@ -13,8 +13,6 @@ module Gitlab
end
def perform(start_id, stop_id)
- return if Feature.disabled?(:migrate_bio_to_user_details, default_enabled: true)
-
relation = User
.select("id AS user_id", "substring(COALESCE(bio, '') from 1 for 255) AS bio")
.where("(COALESCE(bio, '') IS DISTINCT FROM '')")
diff --git a/lib/gitlab/background_migration/remove_duplicated_cs_findings_without_vulnerability_id.rb b/lib/gitlab/background_migration/remove_duplicated_cs_findings_without_vulnerability_id.rb
new file mode 100644
index 00000000000..cd305adc7cd
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_duplicated_cs_findings_without_vulnerability_id.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class RemoveDuplicatedCsFindingsWithoutVulnerabilityId
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::RemoveDuplicatedCsFindingsWithoutVulnerabilityId.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveDuplicatedCsFindingsWithoutVulnerabilityId')
diff --git a/lib/gitlab/background_migration/replace_blocked_by_links.rb b/lib/gitlab/background_migration/replace_blocked_by_links.rb
new file mode 100644
index 00000000000..26626aaef79
--- /dev/null
+++ b/lib/gitlab/background_migration/replace_blocked_by_links.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class ReplaceBlockedByLinks
+ class IssueLink < ActiveRecord::Base
+ self.table_name = 'issue_links'
+ end
+
+ def perform(start_id, stop_id)
+ blocked_by_links = IssueLink.where(id: start_id..stop_id).where(link_type: 2)
+
+ ActiveRecord::Base.transaction do
+ # if there is duplicit bi-directional relation (issue2 is blocked by issue1
+ # and issue1 already links issue2), then we can just delete 'blocked by'.
+ # This should be rare as we have a pre-create check which checks if issues are
+ # already linked
+ blocked_by_links
+ .joins('INNER JOIN issue_links as opposite_links ON issue_links.source_id = opposite_links.target_id AND issue_links.target_id = opposite_links.source_id')
+ .where('opposite_links.link_type': 1)
+ .delete_all
+
+ blocked_by_links.update_all('source_id=target_id,target_id=source_id,link_type=1')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/sync_blocking_issues_count.rb b/lib/gitlab/background_migration/sync_blocking_issues_count.rb
new file mode 100644
index 00000000000..6262320128c
--- /dev/null
+++ b/lib/gitlab/background_migration/sync_blocking_issues_count.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class SyncBlockingIssuesCount
+ def perform(start_id, end_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::SyncBlockingIssuesCount.prepend_if_ee('EE::Gitlab::BackgroundMigration::SyncBlockingIssuesCount')
diff --git a/lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser.rb b/lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser.rb
new file mode 100644
index 00000000000..3def5eb3369
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Lib
+ module Banzai
+ # isolated Banzai::ReferenceParser
+ module ReferenceParser
+ # Returns the reference parser class for the given type
+ #
+ # Example:
+ #
+ # Banzai::ReferenceParser['isolated_mentioned_group']
+ #
+ # This would return the `::Gitlab::BackgroundMigration::UserMentions::Lib::Banzai::ReferenceParser::IsolatedMentionedGroupParser` class.
+ def self.[](name)
+ const_get("::Gitlab::BackgroundMigration::UserMentions::Lib::Banzai::ReferenceParser::#{name.to_s.camelize}Parser", false)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser/isolated_mentioned_group_parser.rb b/lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser/isolated_mentioned_group_parser.rb
new file mode 100644
index 00000000000..d3d032ba433
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/lib/banzai/reference_parser/isolated_mentioned_group_parser.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Lib
+ module Banzai
+ module ReferenceParser
+ # isolated Banzai::ReferenceParser::MentionedGroupParser
+ class IsolatedMentionedGroupParser < ::Banzai::ReferenceParser::MentionedGroupParser
+ extend ::Gitlab::Utils::Override
+
+ self.reference_type = :user
+
+ override :references_relation
+ def references_relation
+ ::Gitlab::BackgroundMigration::UserMentions::Models::Group
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/lib/gitlab/isolated_reference_extractor.rb b/lib/gitlab/background_migration/user_mentions/lib/gitlab/isolated_reference_extractor.rb
new file mode 100644
index 00000000000..1d3a3af81a1
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/lib/gitlab/isolated_reference_extractor.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Lib
+ module Gitlab
+ # Extract possible GFM references from an arbitrary String for further processing.
+ class IsolatedReferenceExtractor < ::Gitlab::ReferenceExtractor
+ REFERABLES = %i(isolated_mentioned_group).freeze
+
+ REFERABLES.each do |type|
+ define_method("#{type}s") do
+ @references[type] ||= isolated_references(type)
+ end
+ end
+
+ def isolated_references(type)
+ context = ::Banzai::RenderContext.new(project, current_user)
+ processor = ::Gitlab::BackgroundMigration::UserMentions::Lib::Banzai::ReferenceParser[type].new(context)
+
+ refs = processor.process(html_documents)
+ refs[:visible]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb b/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb
index 69ba3f9132b..be9c0ad2b3a 100644
--- a/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb
+++ b/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb
@@ -36,7 +36,8 @@ module Gitlab
if extractor
extractors[current_user] = extractor
else
- extractor = extractors[current_user] ||= ::Gitlab::ReferenceExtractor.new(project, current_user)
+ extractor = extractors[current_user] ||=
+ Gitlab::BackgroundMigration::UserMentions::Lib::Gitlab::IsolatedReferenceExtractor.new(project, current_user)
extractor.reset_memoized_values
end
@@ -71,7 +72,7 @@ module Gitlab
mentioned_users_ids = array_to_sql(refs.mentioned_users.pluck(:id))
mentioned_projects_ids = array_to_sql(refs.mentioned_projects.pluck(:id))
- mentioned_groups_ids = array_to_sql(refs.mentioned_groups.pluck(:id))
+ mentioned_groups_ids = array_to_sql(refs.isolated_mentioned_groups.pluck(:id))
return if mentioned_users_ids.blank? && mentioned_projects_ids.blank? && mentioned_groups_ids.blank?
diff --git a/lib/gitlab/background_migration/user_mentions/models/concerns/namespace/recursive_traversal.rb b/lib/gitlab/background_migration/user_mentions/models/concerns/namespace/recursive_traversal.rb
new file mode 100644
index 00000000000..5cadfa45b5b
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/concerns/namespace/recursive_traversal.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ module Concerns
+ module Namespace
+ # extracted methods for recursive traversing of namespace hierarchy
+ module RecursiveTraversal
+ extend ActiveSupport::Concern
+
+ def root_ancestor
+ return self if persisted? && parent_id.nil?
+
+ strong_memoize(:root_ancestor) do
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: id))
+ .base_and_ancestors
+ .reorder(nil)
+ .find_by(parent_id: nil)
+ end
+ end
+
+ # Returns all ancestors, self, and descendants of the current namespace.
+ def self_and_hierarchy
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: id))
+ .all_objects
+ end
+
+ # Returns all the ancestors of the current namespaces.
+ def ancestors
+ return self.class.none unless parent_id
+
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: parent_id))
+ .base_and_ancestors
+ end
+
+ # returns all ancestors upto but excluding the given namespace
+ # when no namespace is given, all ancestors upto the top are returned
+ def ancestors_upto(top = nil, hierarchy_order: nil)
+ Gitlab::ObjectHierarchy.new(self.class.where(id: id))
+ .ancestors(upto: top, hierarchy_order: hierarchy_order)
+ end
+
+ def self_and_ancestors(hierarchy_order: nil)
+ return self.class.where(id: id) unless parent_id
+
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: id))
+ .base_and_ancestors(hierarchy_order: hierarchy_order)
+ end
+
+ # Returns all the descendants of the current namespace.
+ def descendants
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(parent_id: id))
+ .base_and_descendants
+ end
+
+ def self_and_descendants
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: id))
+ .base_and_descendants
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/group.rb b/lib/gitlab/background_migration/user_mentions/models/group.rb
new file mode 100644
index 00000000000..bc04172b9a2
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/group.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ # isolated Group model
+ class Group < ::Gitlab::BackgroundMigration::UserMentions::Models::Namespace
+ self.store_full_sti_class = false
+ has_one :saml_provider
+
+ def self.declarative_policy_class
+ "GroupPolicy"
+ end
+
+ def max_member_access_for_user(user)
+ return GroupMember::NO_ACCESS unless user
+
+ return GroupMember::OWNER if user.admin?
+
+ max_member_access = members_with_parents.where(user_id: user)
+ .reorder(access_level: :desc)
+ .first
+ &.access_level
+
+ max_member_access || GroupMember::NO_ACCESS
+ end
+
+ def members_with_parents
+ # Avoids an unnecessary SELECT when the group has no parents
+ source_ids =
+ if has_parent?
+ self_and_ancestors.reorder(nil).select(:id)
+ else
+ id
+ end
+
+ group_hierarchy_members = GroupMember.active_without_invites_and_requests
+ .where(source_id: source_ids)
+
+ GroupMember.from_union([group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares])
+ end
+
+ # rubocop: disable Metrics/AbcSize
+ def members_from_self_and_ancestor_group_shares
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ source_ids =
+ if has_parent?
+ self_and_ancestors.reorder(nil).select(:id)
+ else
+ id
+ end
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
+ cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
+ cte_alias = cte.table.alias(GroupGroupLink.table_name)
+
+ # Instead of members.access_level, we need to maximize that access_level at
+ # the respective group_group_links.group_access.
+ member_columns = GroupMember.attribute_names.map do |column_name|
+ if column_name == 'access_level'
+ smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
+ 'access_level')
+ else
+ group_member_table[column_name]
+ end
+ end
+
+ GroupMember
+ .with(cte.to_arel)
+ .select(*member_columns)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:requested_at].eq(nil))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .where(group_member_table[:source_type].eq('Namespace'))
+ end
+ # rubocop: enable Metrics/AbcSize
+
+ def smallest_value_arel(args, column_alias)
+ Arel::Nodes::As.new(
+ Arel::Nodes::NamedFunction.new('LEAST', args),
+ Arel::Nodes::SqlLiteral.new(column_alias))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/namespace.rb b/lib/gitlab/background_migration/user_mentions/models/namespace.rb
new file mode 100644
index 00000000000..6d7b9a86e69
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/namespace.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ # isolated Namespace model
+ class Namespace < ApplicationRecord
+ include ::Gitlab::VisibilityLevel
+ include ::Gitlab::Utils::StrongMemoize
+ include Gitlab::BackgroundMigration::UserMentions::Models::Concerns::Namespace::RecursiveTraversal
+
+ belongs_to :parent, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::Namespace"
+
+ def visibility_level_field
+ :visibility_level
+ end
+
+ def has_parent?
+ parent_id.present? || parent.present?
+ end
+
+ # Overridden in EE::Namespace
+ def feature_available?(_feature)
+ false
+ end
+ end
+ end
+ end
+ end
+end
+
+Namespace.prepend_if_ee('::EE::Namespace')
diff --git a/lib/gitlab/bulk_import/client.rb b/lib/gitlab/bulk_import/client.rb
new file mode 100644
index 00000000000..c6e77a158cd
--- /dev/null
+++ b/lib/gitlab/bulk_import/client.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BulkImport
+ class Client
+ API_VERSION = 'v4'.freeze
+ DEFAULT_PAGE = 1.freeze
+ DEFAULT_PER_PAGE = 30.freeze
+
+ ConnectionError = Class.new(StandardError)
+
+ def initialize(uri:, token:, page: DEFAULT_PAGE, per_page: DEFAULT_PER_PAGE, api_version: API_VERSION)
+ @uri = URI.parse(uri)
+ @token = token&.strip
+ @page = page
+ @per_page = per_page
+ @api_version = api_version
+ end
+
+ def get(resource, query = {})
+ response = with_error_handling do
+ Gitlab::HTTP.get(
+ resource_url(resource),
+ headers: request_headers,
+ follow_redirects: false,
+ query: query.merge(request_query)
+ )
+ end
+
+ response.parsed_response
+ end
+
+ private
+
+ def request_query
+ {
+ page: @page,
+ per_page: @per_page
+ }
+ end
+
+ def request_headers
+ {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{@token}"
+ }
+ end
+
+ def with_error_handling
+ response = yield
+
+ raise ConnectionError.new("Error #{response.code}") unless response.success?
+
+ response
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ raise ConnectionError, e
+ end
+
+ def base_uri
+ @base_uri ||= "#{@uri.scheme}://#{@uri.host}:#{@uri.port}"
+ end
+
+ def api_url
+ Gitlab::Utils.append_path(base_uri, "/api/#{@api_version}")
+ end
+
+ def resource_url(resource)
+ Gitlab::Utils.append_path(api_url, resource)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/matching_merge_request.rb b/lib/gitlab/checks/matching_merge_request.rb
index 71361b12d07..db7af0088d0 100644
--- a/lib/gitlab/checks/matching_merge_request.rb
+++ b/lib/gitlab/checks/matching_merge_request.rb
@@ -20,3 +20,5 @@ module Gitlab
end
end
end
+
+Gitlab::Checks::MatchingMergeRequest.prepend_if_ee('EE::Gitlab::Checks::MatchingMergeRequest')
diff --git a/lib/gitlab/checks/post_push_message.rb b/lib/gitlab/checks/post_push_message.rb
index b3c981d252b..0d93e7ac8a1 100644
--- a/lib/gitlab/checks/post_push_message.rb
+++ b/lib/gitlab/checks/post_push_message.rb
@@ -44,11 +44,7 @@ module Gitlab
end
def url_to_repo
- protocol == 'ssh' ? message_subject.ssh_url_to_repo : message_subject.http_url_to_repo
- end
-
- def message_subject
- repository.repo_type.wiki? ? project.wiki : container
+ protocol == 'ssh' ? container.ssh_url_to_repo : container.http_url_to_repo
end
end
end
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
index 0373a12ab69..6d152c052dc 100644
--- a/lib/gitlab/ci/ansi2json/converter.rb
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -104,23 +104,24 @@ module Gitlab
action = scanner[1]
timestamp = scanner[2]
section = scanner[3]
+ options = parse_section_options(scanner[4])
section_name = sanitize_section_name(section)
- if action == "start"
- handle_section_start(scanner, section_name, timestamp)
- elsif action == "end"
+ if action == 'start'
+ handle_section_start(scanner, section_name, timestamp, options)
+ elsif action == 'end'
handle_section_end(scanner, section_name, timestamp)
else
raise 'unsupported action'
end
end
- def handle_section_start(scanner, section, timestamp)
+ def handle_section_start(scanner, section, timestamp, options)
# We make a new line for new section
flush_current_line
- @state.open_section(section, timestamp)
+ @state.open_section(section, timestamp, options)
# we need to consume match after handling
# the open of section, as we want the section
@@ -157,6 +158,18 @@ module Gitlab
def sanitize_section_name(section)
section.to_s.downcase.gsub(/[^a-z0-9]/, '-')
end
+
+ def parse_section_options(raw_options)
+ return unless raw_options
+
+ # We need to remove the square brackets and split
+ # by comma to get a list of the options
+ options = raw_options[1...-1].split ','
+
+ # Now split each option by equals to separate
+ # each in the format [key, value]
+ options.to_h { |option| option.split '=' }
+ end
end
end
end
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
index 21aa1f84353..b1dee0e1ecc 100644
--- a/lib/gitlab/ci/ansi2json/line.rb
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -32,7 +32,7 @@ module Gitlab
end
attr_reader :offset, :sections, :segments, :current_segment,
- :section_header, :section_duration
+ :section_header, :section_duration, :section_options
def initialize(offset:, style:, sections: [])
@offset = offset
@@ -68,6 +68,10 @@ module Gitlab
@sections << section
end
+ def set_section_options(options)
+ @section_options = options
+ end
+
def set_as_section_header
@section_header = true
end
@@ -90,6 +94,7 @@ module Gitlab
result[:section] = sections.last if sections.any?
result[:section_header] = true if @section_header
result[:section_duration] = @section_duration if @section_duration
+ result[:section_options] = @section_options if @section_options
end
end
end
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
index 38d36e6950c..b2b6ce649ed 100644
--- a/lib/gitlab/ci/ansi2json/state.rb
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -26,10 +26,11 @@ module Gitlab
Base64.urlsafe_encode64(state.to_json)
end
- def open_section(section, timestamp)
+ def open_section(section, timestamp, options)
@open_sections[section] = timestamp
@current_line.add_section(section)
+ @current_line.set_section_options(options)
@current_line.set_as_section_header
end
diff --git a/lib/gitlab/ci/artifact_file_reader.rb b/lib/gitlab/ci/artifact_file_reader.rb
index 6395a20ca99..b0fad026ec5 100644
--- a/lib/gitlab/ci/artifact_file_reader.rb
+++ b/lib/gitlab/ci/artifact_file_reader.rb
@@ -45,14 +45,6 @@ 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)
@@ -69,25 +61,6 @@ module Gitlab
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)
- 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.get_input_stream(entry) do |is|
- is.read
- end
- end
- end
- end
-
def max_archive_size_in_mb
ActiveSupport::NumberHelper.number_to_human_size(MAX_ARCHIVE_SIZE)
end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 9d269831679..071a8ef830f 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -54,6 +54,10 @@ module Gitlab
root.variables_value
end
+ def variables_with_data
+ root.variables_entry.value_with_data
+ end
+
def stages
root.stages_value
end
diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb
index a8b67a1db4f..1740032e5c7 100644
--- a/lib/gitlab/ci/config/entry/bridge.rb
+++ b/lib/gitlab/ci/config/entry/bridge.rb
@@ -11,15 +11,18 @@ module Gitlab
class Bridge < ::Gitlab::Config::Entry::Node
include ::Gitlab::Ci::Config::Entry::Processable
+ ALLOWED_WHEN = %w[on_success on_failure always manual].freeze
ALLOWED_KEYS = %i[trigger].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS + PROCESSABLE_ALLOWED_KEYS
with_options allow_nil: true do
- validates :when,
- inclusion: { in: %w[on_success on_failure always],
- message: 'should be on_success, on_failure or always' }
+ validates :allow_failure, boolean: true
+ validates :when, inclusion: {
+ in: ALLOWED_WHEN,
+ message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
+ }
end
validate on: :composed do
@@ -57,11 +60,19 @@ module Gitlab
true
end
+ def manual_action?
+ self.when == 'manual'
+ end
+
+ def ignored?
+ allow_failure.nil? ? manual_action? : allow_failure
+ end
+
def value
super.merge(
trigger: (trigger_value if trigger_defined?),
needs: (needs_value if needs_defined?),
- ignore: !!allow_failure,
+ ignore: ignored?,
when: self.when,
scheduling_type: needs_defined? && !bridge_needs ? :dag : :stage
).compact
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index a304d9b724f..6b036182706 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -9,14 +9,28 @@ module Gitlab
#
class Cache < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[key untracked paths policy].freeze
+ ALLOWED_KEYS = %i[key untracked paths when policy].freeze
+ ALLOWED_POLICY = %w[pull-push push pull].freeze
DEFAULT_POLICY = 'pull-push'
+ ALLOWED_WHEN = %w[on_success on_failure always].freeze
+ DEFAULT_WHEN = 'on_success'
validations do
- validates :config, allowed_keys: ALLOWED_KEYS
- validates :policy, inclusion: { in: %w[pull-push push pull], message: 'should be pull-push, push, or pull' }, allow_blank: true
+ validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
+ validates :policy,
+ inclusion: { in: ALLOWED_POLICY, message: 'should be pull-push, push, or pull' },
+ allow_blank: true
+
+ with_options allow_nil: true do
+ validates :when,
+ inclusion: {
+ in: ALLOWED_WHEN,
+ message: 'should be on_success, on_failure or always'
+ }
+ end
end
entry :key, Entry::Key,
@@ -28,13 +42,15 @@ module Gitlab
entry :paths, Entry::Paths,
description: 'Specify which paths should be cached across builds.'
- attributes :policy
+ attributes :policy, :when
def value
result = super
result[:key] = key_value
result[:policy] = policy || DEFAULT_POLICY
+ # Use self.when to avoid conflict with reserved word
+ result[:when] = self.when || DEFAULT_WHEN
result
end
diff --git a/lib/gitlab/ci/config/entry/include.rb b/lib/gitlab/ci/config/entry/include.rb
index 9c2e5f641d0..ad0ed00aa6f 100644
--- a/lib/gitlab/ci/config/entry/include.rb
+++ b/lib/gitlab/ci/config/entry/include.rb
@@ -10,7 +10,7 @@ module Gitlab
class Include < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
- ALLOWED_KEYS = %i[local file remote template artifact job].freeze
+ ALLOWED_KEYS = %i[local file remote template artifact job project ref].freeze
validations do
validates :config, hash_or_string: true
@@ -22,6 +22,10 @@ module Gitlab
if config[:artifact] && config[:job].blank?
errors.add(:config, "must specify the job where to fetch the artifact from")
end
+
+ if config[:project] && config[:file].blank?
+ errors.add(:config, "must specify the file where to fetch the config from")
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index b5ce42969a5..b0fd9cef10b 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a set of jobs.
#
- class Jobs < ::Gitlab::Config::Entry::Node
+ class Jobs < ::Gitlab::Config::Entry::ComposableHash
include ::Gitlab::Config::Entry::Validatable
validations do
@@ -36,6 +36,10 @@ module Gitlab
end
end
+ def composable_class(name, config)
+ self.class.find_type(name, config)
+ end
+
TYPES = [Entry::Hidden, Entry::Job, Entry::Bridge].freeze
private_constant :TYPES
@@ -49,29 +53,6 @@ module Gitlab
type.matching?(name, config)
end
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def compose!(deps = nil)
- super do
- @config.each do |name, config|
- node = self.class.find_type(name, config)
- next unless node
-
- factory = ::Gitlab::Config::Entry::Factory.new(node)
- .value(config || {})
- .metadata(name: name)
- .with(key: name, parent: self,
- description: "#{name} job definition.")
-
- @entries[name] = factory.create!
- end
-
- @entries.each_value do |entry|
- entry.compose!(deps)
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb
index d7ba8624882..66cd57b8cf3 100644
--- a/lib/gitlab/ci/config/entry/needs.rb
+++ b/lib/gitlab/ci/config/entry/needs.rb
@@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a set of needs dependencies.
#
- class Needs < ::Gitlab::Config::Entry::Node
+ class Needs < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@@ -29,27 +29,16 @@ module Gitlab
end
end
- def compose!(deps = nil)
- super(deps) do
- [@config].flatten.each_with_index do |need, index|
- @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need)
- .value(need)
- .with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord
- .create!
- end
-
- @entries.each_value do |entry|
- entry.compose!(deps)
- end
- end
- end
-
def value
- values = @entries.values.select(&:type)
+ values = @entries.select(&:type)
values.group_by(&:type).transform_values do |values|
values.map(&:value)
end
end
+
+ def composable_class
+ Entry::Need
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/ports.rb b/lib/gitlab/ci/config/entry/ports.rb
index 01ffcc7dd87..d26b31deca8 100644
--- a/lib/gitlab/ci/config/entry/ports.rb
+++ b/lib/gitlab/ci/config/entry/ports.rb
@@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a configuration of the ports of a Docker service.
#
- class Ports < ::Gitlab::Config::Entry::Node
+ class Ports < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@@ -16,28 +16,8 @@ module Gitlab
validates :config, port_unique: true
end
- def compose!(deps = nil)
- super do
- @entries = []
- @config.each do |config|
- @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Port)
- .value(config || {})
- .with(key: "port", parent: self, description: "port definition.") # rubocop:disable CodeReuse/ActiveRecord
- .create!
- end
-
- @entries.each do |entry|
- entry.compose!(deps)
- end
- end
- end
-
- def value
- @entries.map(&:value)
- end
-
- def descendants
- @entries
+ def composable_class
+ Entry::Port
end
end
end
diff --git a/lib/gitlab/ci/config/entry/product/matrix.rb b/lib/gitlab/ci/config/entry/product/matrix.rb
index 6af809d46c1..d4ee0978e1b 100644
--- a/lib/gitlab/ci/config/entry/product/matrix.rb
+++ b/lib/gitlab/ci/config/entry/product/matrix.rb
@@ -46,13 +46,11 @@ module Gitlab
end
end
- # rubocop:disable CodeReuse/ActiveRecord
def number_of_generated_jobs
value.sum do |config|
config.values.reduce(1) { |acc, values| acc * values.size }
end
end
- # rubocop:enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/ci/config/entry/product/variables.rb b/lib/gitlab/ci/config/entry/product/variables.rb
index ac4f70fb69e..2481989060e 100644
--- a/lib/gitlab/ci/config/entry/product/variables.rb
+++ b/lib/gitlab/ci/config/entry/product/variables.rb
@@ -14,7 +14,7 @@ module Gitlab
validations do
validates :config, variables: { array_values: true }
validates :config, length: {
- minimum: 2,
+ minimum: :minimum,
too_short: 'requires at least %{count} items'
}
end
@@ -28,6 +28,10 @@ module Gitlab
.map { |key, value| [key.to_s, Array(value).map(&:to_s)] }
.to_h
end
+
+ def minimum
+ ::Gitlab::Ci::Features.one_dimensional_matrix_enabled? ? 1 : 2
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 0ae65f43723..f2fd8ac7fd9 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -15,7 +15,7 @@ module Gitlab
%i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance browser_performance load_performance license_management license_scanning metrics lsif
dotenv cobertura terraform accessibility cluster_applications
- requirements coverage_fuzzing].freeze
+ requirements coverage_fuzzing api_fuzzing].freeze
attributes ALLOWED_KEYS
@@ -25,6 +25,7 @@ module Gitlab
with_options allow_nil: true do
validates :junit, array_of_strings_or_string: true
+ validates :api_fuzzing, array_of_strings_or_string: true
validates :coverage_fuzzing, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true
diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb
index 2fbc3d9e367..bf74f995e80 100644
--- a/lib/gitlab/ci/config/entry/rules.rb
+++ b/lib/gitlab/ci/config/entry/rules.rb
@@ -4,7 +4,7 @@ module Gitlab
module Ci
class Config
module Entry
- class Rules < ::Gitlab::Config::Entry::Node
+ class Rules < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@@ -12,24 +12,13 @@ module Gitlab
validates :config, type: Array
end
- def compose!(deps = nil)
- super(deps) do
- @config.each_with_index do |rule, index|
- @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule)
- .value(rule)
- .with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord
- .create!
- end
-
- @entries.each_value do |entry|
- entry.compose!(deps)
- end
- end
- end
-
def value
@config
end
+
+ def composable_class
+ Entry::Rules::Rule
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb
index 83baa83711f..44e2903a300 100644
--- a/lib/gitlab/ci/config/entry/services.rb
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a configuration of Docker services.
#
- class Services < ::Gitlab::Config::Entry::Node
+ class Services < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@@ -15,28 +15,8 @@ module Gitlab
validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) }
end
- def compose!(deps = nil)
- super do
- @entries = []
- @config.each do |config|
- @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
- .value(config || {})
- .with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord
- .create!
- end
-
- @entries.each do |entry|
- entry.compose!(deps)
- end
- end
- end
-
- def value
- @entries.map(&:value)
- end
-
- def descendants
- @entries
+ def composable_class
+ Entry::Service
end
end
end
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index c9d0c7cb568..e258f7128fc 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -10,16 +10,32 @@ module Gitlab
class Variables < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
+ ALLOWED_VALUE_DATA = %i[value description].freeze
+
validations do
- validates :config, variables: true
+ validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA }
+ end
+
+ def value
+ Hash[@config.map { |key, value| [key.to_s, expand_value(value)[:value]] }]
end
def self.default(**)
{}
end
- def value
- Hash[@config.map { |key, value| [key.to_s, value.to_s] }]
+ def value_with_data
+ Hash[@config.map { |key, value| [key.to_s, expand_value(value)] }]
+ end
+
+ private
+
+ def expand_value(value)
+ if value.is_a?(Hash)
+ { value: value[:value].to_s, description: value[:description] }
+ else
+ { value: value.to_s, description: nil }
+ end
end
end
end
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index e770187b124..1b58e3ec71a 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -10,10 +10,6 @@ module Gitlab
::Feature.enabled?(:ci_artifacts_exclude, default_enabled: true)
end
- def self.job_heartbeats_runner?(project)
- ::Feature.enabled?(:ci_job_heartbeats_runner, project, default_enabled: true)
- end
-
def self.instance_variables_ui_enabled?
::Feature.enabled?(:ci_instance_variables_ui, default_enabled: true)
end
@@ -35,10 +31,6 @@ module Gitlab
::Feature.enabled?(:ci_raise_job_rules_without_workflow_rules_warning, default_enabled: true)
end
- def self.bulk_insert_on_create?(project)
- ::Feature.enabled?(:ci_bulk_insert_on_create, project, default_enabled: true)
- end
-
# 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.
@@ -54,25 +46,25 @@ module Gitlab
Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false)
end
- def self.coverage_report_view?(project)
- ::Feature.enabled?(:coverage_report_view, project, default_enabled: true)
- end
-
- def self.child_of_child_pipeline_enabled?(project)
- ::Feature.enabled?(:ci_child_of_child_pipeline, project, default_enabled: true)
- end
-
def self.trace_overwrite?
::Feature.enabled?(:ci_trace_overwrite, type: :ops, default_enabled: false)
end
def self.accept_trace?(project)
::Feature.enabled?(:ci_enable_live_trace, project) &&
- ::Feature.enabled?(:ci_accept_trace, project, type: :ops, default_enabled: false)
+ ::Feature.enabled?(:ci_accept_trace, project, type: :ops, default_enabled: true)
+ end
+
+ def self.log_invalid_trace_chunks?(project)
+ ::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false)
+ end
+
+ def self.one_dimensional_matrix_enabled?
+ ::Feature.enabled?(:one_dimensional_matrix, default_enabled: true)
end
- def self.new_artifact_file_reader_enabled?(project)
- ::Feature.enabled?(:ci_new_artifact_file_reader, project, default_enabled: false)
+ def self.manual_bridges_enabled?(project)
+ ::Feature.enabled?(:ci_manual_bridges, project, default_enabled: true)
end
end
end
diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb
index 86a9ebfa451..44f2ac23ce3 100644
--- a/lib/gitlab/ci/lint.rb
+++ b/lib/gitlab/ci/lint.rb
@@ -4,10 +4,11 @@ module Gitlab
module Ci
class Lint
class Result
- attr_reader :jobs, :errors, :warnings
+ attr_reader :jobs, :merged_yaml, :errors, :warnings
- def initialize(jobs:, errors:, warnings:)
+ def initialize(jobs:, merged_yaml:, errors:, warnings:)
@jobs = jobs
+ @merged_yaml = merged_yaml
@errors = errors
@warnings = warnings
end
@@ -39,6 +40,7 @@ module Gitlab
Result.new(
jobs: dry_run_convert_to_jobs(pipeline.stages),
+ merged_yaml: pipeline.merged_yaml,
errors: pipeline.error_messages.map(&:content),
warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content)
)
@@ -54,6 +56,7 @@ module Gitlab
Result.new(
jobs: static_validation_convert_to_jobs(result),
+ merged_yaml: result.merged_yaml,
errors: result.errors,
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
)
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 5746f38ae5b..50cd703da4a 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -8,12 +8,17 @@ module Gitlab
JunitParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
ATTACHMENT_TAG_REGEX = /\[\[ATTACHMENT\|(?<path>.+?)\]\]/.freeze
- def parse!(xml_data, test_suite, **args)
+ def parse!(xml_data, test_suite, job:)
root = Hash.from_xml(xml_data)
+ total_parsed = 0
+ max_test_cases = job.max_test_cases_per_report
all_cases(root) do |test_case|
- test_case = create_test_case(test_case, args)
+ test_case = create_test_case(test_case, test_suite, job)
test_suite.add_test_case(test_case)
+ total_parsed += 1
+
+ ensure_test_cases_limited!(total_parsed, max_test_cases)
end
rescue Nokogiri::XML::SyntaxError => e
test_suite.set_suite_error("JUnit XML parsing failed: #{e}")
@@ -23,6 +28,12 @@ module Gitlab
private
+ def ensure_test_cases_limited!(total_parsed, limit)
+ return unless limit > 0 && total_parsed > limit
+
+ raise JunitParserError.new("number of test cases exceeded the limit of #{limit}")
+ end
+
def all_cases(root, parent = nil, &blk)
return unless root.present?
@@ -33,20 +44,24 @@ module Gitlab
all_cases(node['testsuites'], root, &blk) unless parent
# we require at least one level of testsuites or testsuite
- each_case(node['testcase'], &blk) if parent
+ each_case(node['testcase'], node['name'], &blk) if parent
# we allow multiple nested 'testsuite' (eg. PHPUnit)
all_cases(node['testsuite'], root, &blk)
end
end
- def each_case(testcase, &blk)
+ def each_case(testcase, testsuite_name, &blk)
return unless testcase.present?
- [testcase].flatten.compact.map(&blk)
+ [testcase].flatten.compact.each do |tc|
+ tc['suite_name'] = testsuite_name
+
+ yield(tc)
+ end
end
- def create_test_case(data, args)
+ def create_test_case(data, test_suite, job)
if data.key?('failure')
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
system_output = data['failure']
@@ -63,6 +78,7 @@ module Gitlab
end
::Gitlab::Ci::Reports::TestCase.new(
+ suite_name: data['suite_name'] || test_suite.name,
classname: data['classname'],
name: data['name'],
file: data['file'],
@@ -70,10 +86,14 @@ module Gitlab
status: status,
system_output: system_output,
attachment: attachment,
- job: args.fetch(:job)
+ job: job
)
end
+ def suite_name(parent, test_suite)
+ parent.dig('testsuite', 'name') || test_suite.name
+ end
+
def attachment_path(data)
return unless data
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index d1882059dd8..06096a33f27 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -16,7 +16,7 @@ module Gitlab
) do
include Gitlab::Utils::StrongMemoize
- def initialize(**params)
+ def initialize(params = {})
params.each do |key, value|
self[key] = value
end
diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb
index 8ccb33ffd34..c3fbd0c9e24 100644
--- a/lib/gitlab/ci/pipeline/chain/config/process.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/process.rb
@@ -28,6 +28,8 @@ module Gitlab
error(result.errors.first, config_error: true)
end
+ @pipeline.merged_yaml = result.merged_yaml
+
rescue => ex
Gitlab::ErrorTracking.track_exception(ex,
project_id: project.id,
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index 34649fe16f3..81ef3bb074d 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -8,7 +8,7 @@ module Gitlab
include Chain::Helpers
def perform!
- BulkInsertableAssociations.with_bulk_insert(enabled: ::Gitlab::Ci::Features.bulk_insert_on_create?(project)) do
+ BulkInsertableAssociations.with_bulk_insert do
pipeline.save!
end
rescue ActiveRecord::RecordInvalid => e
diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb
index a4127ea0be2..8d6fe13c3b9 100644
--- a/lib/gitlab/ci/pipeline/seed/build/cache.rb
+++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb
@@ -13,6 +13,7 @@ module Gitlab
@paths = local_cache.delete(:paths)
@policy = local_cache.delete(:policy)
@untracked = local_cache.delete(:untracked)
+ @when = local_cache.delete(:when)
raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
end
@@ -24,7 +25,8 @@ module Gitlab
key: key_string,
paths: @paths,
policy: @policy,
- untracked: @untracked
+ untracked: @untracked,
+ when: @when
}.compact.presence
}.compact
}
diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb
index 15a3c862c9e..8c70dbb6931 100644
--- a/lib/gitlab/ci/reports/test_case.rb
+++ b/lib/gitlab/ci/reports/test_case.rb
@@ -10,9 +10,10 @@ module Gitlab
STATUS_ERROR = 'error'
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
+ attr_reader :suite_name, :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job
def initialize(params)
+ @suite_name = params.fetch(:suite_name)
@name = params.fetch(:name)
@classname = params.fetch(:classname)
@file = params.fetch(:file, nil)
@@ -23,7 +24,7 @@ module Gitlab
@attachment = params.fetch(:attachment, nil)
@job = params.fetch(:job, nil)
- @key = sanitize_key_name("#{classname}_#{name}")
+ @key = hash_key("#{suite_name}_#{classname}_#{name}")
end
def has_attachment?
@@ -42,8 +43,8 @@ module Gitlab
private
- def sanitize_key_name(key)
- key.gsub(/[^0-9A-Za-z]/, '-')
+ def hash_key(key)
+ Digest::SHA256.hexdigest(key)
end
end
end
diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb
index e9b78b841e4..00920dfbd54 100644
--- a/lib/gitlab/ci/reports/test_suite.rb
+++ b/lib/gitlab/ci/reports/test_suite.rb
@@ -12,18 +12,24 @@ module Gitlab
def initialize(name = nil)
@name = name
@test_cases = {}
+ @all_test_cases = []
@total_time = 0.0
- @duplicate_cases = []
end
def add_test_case(test_case)
- @duplicate_cases << test_case if existing_key?(test_case)
-
@test_cases[test_case.status] ||= {}
@test_cases[test_case.status][test_case.key] = test_case
@total_time += test_case.execution_time
end
+ def each_test_case
+ @test_cases.each do |status, test_cases|
+ test_cases.values.each do |test_case|
+ yield test_case
+ end
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def total_count
return 0 if suite_error
@@ -86,10 +92,6 @@ module Gitlab
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
diff --git a/lib/gitlab/ci/runner/backoff.rb b/lib/gitlab/ci/runner/backoff.rb
new file mode 100644
index 00000000000..95d7719e9cb
--- /dev/null
+++ b/lib/gitlab/ci/runner/backoff.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Runner
+ ##
+ # Runner Backoff class is an implementation of an exponential backoff
+ # used when a runner communicates with GitLab. We typically use it when a
+ # runner retries sending a build status after we created a build pending
+ # state.
+ #
+ # Backoff is calculated based on the backoff slot which is always a power
+ # of 2:
+ #
+ # 0s - 3s duration -> 1 second backoff
+ # 4s - 7s duration -> 2 seconds backoff
+ # 8s - 15s duration -> 4 seconds backoff
+ # 16s - 31s duration -> 8 seconds backoff
+ # 32s - 63s duration -> 16 seconds backoff
+ # 64s - 127s duration -> 32 seconds backoff
+ # 127s - 256s+ duration -> 64 seconds backoff
+ #
+ # It means that first 15 requests made by a runner will need to respect
+ # following backoffs:
+ #
+ # 0s -> 1 second backoff (backoff started, slot 0, 2^0 backoff)
+ # 1s -> 1 second backoff
+ # 2s -> 1 second backoff
+ # 3s -> 1 seconds backoff
+ # (slot 1 - 2^1 backoff)
+ # 4s -> 2 seconds backoff
+ # 6s -> 2 seconds backoff
+ # (slot 2 - 2^2 backoff)
+ # 8s -> 4 seconds backoff
+ # 12s -> 4 seconds backoff
+ # (slot 3 - 2^3 backoff)
+ # 16s -> 8 seconds backoff
+ # 24s -> 8 seconds backoff
+ # (slot 4 - 2^4 backoff)
+ # 32s -> 16 seconds backoff
+ # 48s -> 16 seconds backoff
+ # (slot 5 - 2^5 backoff)
+ # 64s -> 32 seconds backoff
+ # 96s -> 32 seconds backoff
+ # (slot 6 - 2^6 backoff)
+ # 128s -> 64 seconds backoff
+ #
+ # There is a cap on the backoff - it will never exceed 64 seconds.
+ #
+ class Backoff
+ def initialize(started)
+ @started = started
+
+ if duration < 0
+ raise ArgumentError, 'backoff duration negative'
+ end
+ end
+
+ def duration
+ (Time.current - @started).ceil
+ end
+
+ def slot
+ return 0 if duration < 2
+
+ Math.log(duration, 2).floor - 1
+ end
+
+ def to_seconds
+ 2**[slot, 6].min
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/bridge/action.rb b/lib/gitlab/ci/status/bridge/action.rb
new file mode 100644
index 00000000000..1ba4700d9b0
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/action.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ class Action < Status::Build::Action
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb
index b95565b5e09..d66d4b20bba 100644
--- a/lib/gitlab/ci/status/bridge/common.rb
+++ b/lib/gitlab/ci/status/bridge/common.rb
@@ -14,7 +14,6 @@ module Gitlab
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)
diff --git a/lib/gitlab/ci/status/bridge/factory.rb b/lib/gitlab/ci/status/bridge/factory.rb
index 5d397dba0de..b9bd66cee71 100644
--- a/lib/gitlab/ci/status/bridge/factory.rb
+++ b/lib/gitlab/ci/status/bridge/factory.rb
@@ -6,7 +6,10 @@ module Gitlab
module Bridge
class Factory < Status::Factory
def self.extended_statuses
- [Status::Bridge::Failed]
+ [[Status::Bridge::Failed],
+ [Status::Bridge::Manual],
+ [Status::Bridge::Play],
+ [Status::Bridge::Action]]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/bridge/manual.rb b/lib/gitlab/ci/status/bridge/manual.rb
new file mode 100644
index 00000000000..e07e645a34d
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/manual.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ class Manual < Status::Build::Manual
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/bridge/play.rb b/lib/gitlab/ci/status/bridge/play.rb
new file mode 100644
index 00000000000..ae00ef6c2ad
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/play.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ class Play < Status::Build::Play
+ def has_action?
+ can?(user, :play_job, subject)
+ end
+
+ def self.matches?(bridge, user)
+ bridge.playable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index 07f37732023..f173964b36c 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_canceled'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index fface4bb97b..33e67314d93 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_created'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index 770ed7d4d5a..215d27734a7 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_failed'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index 50c92add400..eb376df5f22 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_manual'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index cea7e6ed938..4280ad84534 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_pending'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/preparing.rb b/lib/gitlab/ci/status/preparing.rb
index 1ebdbc482b7..e59d1d2eed1 100644
--- a/lib/gitlab/ci/status/preparing.rb
+++ b/lib/gitlab/ci/status/preparing.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_preparing'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index ac7dd74cdce..eed1983e60e 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_running'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
index 16ad1da89e3..e9068c326cf 100644
--- a/lib/gitlab/ci/status/scheduled.rb
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_scheduled'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index aaec1e1d201..238aa3ab4f9 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_skipped'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index 020f2c5b89f..2a10e60414e 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -19,6 +19,10 @@ module Gitlab
def favicon
'favicon_status_success'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/waiting_for_resource.rb b/lib/gitlab/ci/status/waiting_for_resource.rb
index 4c9e706bc51..2026148f752 100644
--- a/lib/gitlab/ci/status/waiting_for_resource.rb
+++ b/lib/gitlab/ci/status/waiting_for_resource.rb
@@ -23,6 +23,10 @@ module Gitlab
def group
'waiting-for-resource'
end
+
+ def details_path
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/ci/templates/AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml b/lib/gitlab/ci/templates/AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml
new file mode 100644
index 00000000000..267027a1b8a
--- /dev/null
+++ b/lib/gitlab/ci/templates/AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml
@@ -0,0 +1,11 @@
+stages:
+ - provision
+ - review
+ - production
+
+variables:
+ AUTO_DEVOPS_PLATFORM_TARGET: EC2
+
+include:
+ - template: Jobs/CF-Provision.gitlab-ci.yml
+ - template: Jobs/Deploy/EC2.gitlab-ci.yml
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 6966ce88b30..cba13f374f4 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -20,6 +20,7 @@
# * dast: DAST_DISABLED
# * review: REVIEW_DISABLED
# * stop_review: REVIEW_DISABLED
+# * code_intelligence: CODE_INTELLIGENCE_DISABLED
#
# In order to deploy, you must have a Kubernetes cluster configured either
# via a project integration, or via group/project variables.
@@ -159,6 +160,7 @@ include:
- 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/Code-Intelligence.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Intelligence.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
diff --git a/lib/gitlab/ci/templates/Bash.gitlab-ci.yml b/lib/gitlab/ci/templates/Bash.gitlab-ci.yml
index 368069844ea..67e58d9ee99 100644
--- a/lib/gitlab/ci/templates/Bash.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Bash.gitlab-ci.yml
@@ -1,4 +1,4 @@
-# see https://docs.gitlab.com/ce/ci/yaml/README.html for all available options
+# see https://docs.gitlab.com/ee/ci/yaml/README.html for all available options
# you can delete this line if you're not using Docker
image: busybox:latest
diff --git a/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml b/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml
index c3568c0d2c8..0c5850bdb8e 100644
--- a/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml
@@ -1,7 +1,7 @@
# Based on openjdk:8, already includes lein
image: clojure:lein-2.7.0
# If you need to configure a database, add a `services` section here
-# See https://docs.gitlab.com/ce/ci/services/postgres.html
+# See https://docs.gitlab.com/ee/ci/services/postgres.html
# Make sure you configure the connection as well
before_script:
diff --git a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
index e9301a2638d..538f96c4084 100644
--- a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
@@ -4,7 +4,7 @@ image: "crystallang/crystal:latest"
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
# services:
# - mysql:latest
# - redis:latest
diff --git a/lib/gitlab/ci/templates/Django.gitlab-ci.yml b/lib/gitlab/ci/templates/Django.gitlab-ci.yml
index d35fcb0f807..c657c7e8eb1 100644
--- a/lib/gitlab/ci/templates/Django.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Django.gitlab-ci.yml
@@ -4,7 +4,7 @@ image: python:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
services:
- mysql:latest
- postgres:latest
@@ -13,7 +13,7 @@ variables:
POSTGRES_DB: database_name
# This folder is cached between builds
-# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+# http://docs.gitlab.com/ee/ci/yaml/README.html#cache
cache:
paths:
- ~/.cache/pip/
diff --git a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
index 4d4c6a64cd5..7271526ab1b 100644
--- a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
@@ -2,7 +2,7 @@ image: elixir:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
services:
- mysql:latest
- redis:latest
diff --git a/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml
new file mode 100644
index 00000000000..31ca68c57d7
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml
@@ -0,0 +1,14 @@
+stages:
+ - provision
+
+cloud_formation:
+ image: 'registry.gitlab.com/gitlab-org/cloud-deploy/aws-cloudformation:latest'
+ stage: provision
+ script:
+ - gl-cloudformation create-stack
+ rules:
+ - if: '($AUTO_DEVOPS_PLATFORM_TARGET != "EC2") || ($AUTO_DEVOPS_PLATFORM_TARGET != "ECS")'
+ when: never
+ - if: '$CI_KUBERNETES_ACTIVE'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml
new file mode 100644
index 00000000000..83bc5548614
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml
@@ -0,0 +1,16 @@
+code_intelligence_go:
+ stage: test
+ needs: []
+ allow_failure: true
+ image: sourcegraph/lsif-go:v1
+ rules:
+ - if: $CODE_INTELLIGENCE_DISABLED
+ when: never
+ - if: $CI_COMMIT_BRANCH
+ exists:
+ - '**/*.go'
+ script:
+ - lsif-go
+ artifacts:
+ reports:
+ lsif: dump.lsif
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 568ceceeaa2..ec33020205b 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -9,9 +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:
+ - export SOURCE_CODE=$PWD
- |
if ! docker info &>/dev/null; then
if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index 86f946aafda..77216a6e404 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.dast-auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.5"
dast_environment_deploy:
extends: .dast-auto-deploy
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 829fd7a722f..32a207a85d1 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.5"
dependencies: []
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
index 829fd7a722f..8b921305c11 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0-beta.2"
dependencies: []
review:
@@ -91,7 +91,7 @@ canary:
- auto-deploy ensure_namespace
- auto-deploy initialize_tiller
- auto-deploy create_secret
- - auto-deploy deploy canary
+ - auto-deploy deploy canary 50
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
@@ -114,7 +114,6 @@ canary:
- auto-deploy create_secret
- auto-deploy deploy
- auto-deploy delete canary
- - auto-deploy delete rollout
- auto-deploy persist_environment_url
environment:
name: production
@@ -163,9 +162,7 @@ production_manual:
- 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 deploy canary $ROLLOUT_PERCENTAGE
- auto-deploy persist_environment_url
environment:
name: production
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy/EC2.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy/EC2.gitlab-ci.yml
new file mode 100644
index 00000000000..ed2172ef7f5
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Deploy/EC2.gitlab-ci.yml
@@ -0,0 +1,39 @@
+stages:
+ - review
+ - production
+
+.push-and-deploy:
+ image: 'registry.gitlab.com/gitlab-org/cloud-deploy/aws-ec2:latest'
+ script:
+ - gl-ec2 push-to-s3
+ - gl-ec2 deploy-to-ec2
+
+review_ec2:
+ extends: .push-and-deploy
+ stage: review
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ rules:
+ - if: '$AUTO_DEVOPS_PLATFORM_TARGET != "EC2"'
+ when: never
+ - if: '$CI_KUBERNETES_ACTIVE'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
+
+production_ec2:
+ extends: .push-and-deploy
+ stage: production
+ environment:
+ name: production
+ rules:
+ - if: '$AUTO_DEVOPS_PLATFORM_TARGET != "EC2"'
+ when: never
+ - if: '$CI_KUBERNETES_ACTIVE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
index da474f8ac88..317e8bfab0e 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
@@ -10,6 +10,7 @@
.deploy_to_ecs:
image: 'registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest'
+ dependencies: []
script:
- ecs update-task-definition
diff --git a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
index 9bde04dff19..5d2c8024524 100644
--- a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
@@ -4,7 +4,7 @@ image: php:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
services:
- mysql:latest
@@ -13,7 +13,7 @@ variables:
MYSQL_ROOT_PASSWORD: secret
# This folder is cached between builds
-# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+# http://docs.gitlab.com/ee/ci/yaml/README.html#cache
cache:
paths:
- vendor/
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 7050b41e045..a9638f564f3 100644
--- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -1,6 +1,6 @@
apply:
stage: deploy
- image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.29.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.33.0"
environment:
name: production
variables:
diff --git a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
index b87178141a1..92379ded77c 100644
--- a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
@@ -4,14 +4,14 @@ image: node:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
services:
- mysql:latest
- redis:latest
- postgres:latest
# This folder is cached between builds
-# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+# http://docs.gitlab.com/ee/ci/yaml/README.html#cache
cache:
paths:
- node_modules/
diff --git a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
index 25ea20e454f..84e8223e69b 100644
--- a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
@@ -19,7 +19,7 @@ before_script:
- php composer.phar install
# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
-# See http://docs.gitlab.com/ce/ci/services/README.html for examples.
+# See http://docs.gitlab.com/ee/ci/services/README.html for examples.
services:
- mysql:5.7
diff --git a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
index a683561a455..3a6eac63892 100644
--- a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
@@ -1,7 +1,7 @@
image: node:latest
# This folder is cached between builds
-# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+# http://docs.gitlab.com/ee/ci/yaml/README.html#cache
cache:
paths:
- node_modules/
diff --git a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
index 0c8859dc779..f2f92fe0704 100644
--- a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
@@ -1,5 +1,5 @@
# Template project: https://gitlab.com/pages/jekyll
-# Docs: https://docs.gitlab.com/ce/pages/
+# Docs: https://docs.gitlab.com/ee/pages/
image: ruby:2.6
variables:
diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
index b3cad8b858a..275364afae4 100644
--- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
@@ -4,7 +4,7 @@ image: "ruby:2.5"
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
services:
- mysql:latest
- redis:latest
diff --git a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
index f35470367cc..94117a79d1c 100644
--- a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
@@ -4,7 +4,7 @@ image: "rust:latest"
# Optional: Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
-# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
+# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
# services:
# - mysql:latest
# - redis:latest
diff --git a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
index 4b957a8f771..e268b48d133 100644
--- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml
@@ -35,4 +35,3 @@ variables:
- if: $COVFUZZ_DISABLED
when: never
- if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bcoverage_fuzzing\b/
- - if: $CI_RUNNER_EXECUTABLE_ARCH == "linux"
diff --git a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
index cc34d23decc..63237e41376 100644
--- a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
@@ -1,7 +1,7 @@
# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/license_compliance/
#
# Configure the scanning tool through the environment variables.
-# List of the variables: https://gitlab.com/gitlab-org/security-products/license-management#settings
+# List of the variables: https://gitlab.com/gitlab-org/security-products/analyzers/license-finder#settings
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 77ea11d01d1..4418ff18d73 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -9,7 +9,7 @@ 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, 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, mobsf"
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
SAST_ANALYZER_IMAGE_TAG: 2
SCAN_KUBERNETES_MANIFESTS: "false"
@@ -125,6 +125,42 @@ gosec-sast:
exists:
- '**/*.go'
+mobsf-android-sast:
+ extends: .sast-analyzer
+ services:
+ - name: opensecurity/mobile-security-framework-mobsf:latest
+ alias: mobsf
+ image:
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/mobsf:$SAST_ANALYZER_IMAGE_TAG"
+ rules:
+ - if: $SAST_DISABLED
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $SAST_DEFAULT_ANALYZERS =~ /mobsf/ &&
+ $SAST_EXPERIMENTAL_FEATURES == 'true'
+ exists:
+ - '**/AndroidManifest.xml'
+
+mobsf-ios-sast:
+ extends: .sast-analyzer
+ services:
+ - name: opensecurity/mobile-security-framework-mobsf:latest
+ alias: mobsf
+ image:
+ name: "$SAST_ANALYZER_IMAGE"
+ variables:
+ SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/mobsf:$SAST_ANALYZER_IMAGE_TAG"
+ rules:
+ - if: $SAST_DISABLED
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $SAST_DEFAULT_ANALYZERS =~ /mobsf/ &&
+ $SAST_EXPERIMENTAL_FEATURES == 'true'
+ exists:
+ - '**/*.xcodeproj/*'
+
nodejs-scan-sast:
extends: .sast-analyzer
image:
@@ -203,6 +239,11 @@ spotbugs-sast:
variables:
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
rules:
+ - if: $SAST_DEFAULT_ANALYZERS =~ /mobsf/ &&
+ $SAST_EXPERIMENTAL_FEATURES == 'true'
+ exists:
+ - '**/AndroidManifest.xml'
+ when: never
- if: $SAST_DISABLED
when: never
- if: $CI_COMMIT_BRANCH &&
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 bde6a0fbebb..6ebff102ccb 100644
--- a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
@@ -34,8 +34,8 @@ secret_detection:
when: never
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
script:
- - git fetch origin $CI_DEFAULT_BRANCH $CI_BUILD_REF_NAME
- - 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
+ - git fetch origin $CI_DEFAULT_BRANCH $CI_COMMIT_REF_NAME
+ - git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_COMMIT_REF_NAME > "$CI_COMMIT_SHA"_commit_list.txt
- export SECRET_DETECTION_COMMITS_FILE="$CI_COMMIT_SHA"_commit_list.txt
- /analyzer run
- rm "$CI_COMMIT_SHA"_commit_list.txt
diff --git a/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..b08ccf18b58
--- /dev/null
+++ b/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
@@ -0,0 +1,22 @@
+include:
+ - template: Terraform/Base.latest.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
+
+stages:
+ - init
+ - validate
+ - build
+ - deploy
+
+init:
+ extends: .init
+
+validate:
+ extends: .validate
+
+build:
+ extends: .build
+
+deploy:
+ extends: .deploy
+ dependencies:
+ - build
diff --git a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..000a1a7f580
--- /dev/null
+++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
@@ -0,0 +1,53 @@
+# Terraform/Base.latest
+#
+# The purpose of this template is to provide flexibility to the user so
+# they are able to only include the jobs that they find interesting.
+#
+# Therefore, this template is not supposed to run any jobs. The idea is to only
+# create hidden jobs. See: https://docs.gitlab.com/ee/ci/yaml/#hide-jobs
+#
+# There is a more opinionated template which we suggest the users to abide,
+# which is the lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
+
+image:
+ name: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
+
+before_script:
+ - cd ${TF_ROOT}
+
+variables:
+ TF_ROOT: ${CI_PROJECT_DIR}
+
+cache:
+ key: "${TF_ROOT}"
+ paths:
+ - ${TF_ROOT}/.terraform/
+
+.init: &init
+ stage: init
+ script:
+ - gitlab-terraform init
+
+.validate: &validate
+ stage: validate
+ script:
+ - gitlab-terraform validate
+
+.build: &build
+ stage: build
+ script:
+ - gitlab-terraform plan
+ - gitlab-terraform plan-json
+ artifacts:
+ paths:
+ - ${TF_ROOT}/plan.cache
+ reports:
+ terraform: ${TF_ROOT}/plan.json
+
+.deploy: &deploy
+ stage: deploy
+ script:
+ - gitlab-terraform apply
+ when: manual
+ only:
+ - master
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 348e5472cb4..0222ca021b7 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -16,6 +16,7 @@ module Gitlab
ArchiveError = Class.new(StandardError)
AlreadyArchivedError = Class.new(StandardError)
+ LockedError = Class.new(StandardError)
attr_reader :job
@@ -79,11 +80,9 @@ module Gitlab
job.trace_chunks.any? || current_path.present? || old_trace.present?
end
- def read(should_retry: true, &block)
+ def read(&block)
read_stream(&block)
- rescue Errno::ENOENT
- raise unless should_retry
-
+ rescue Errno::ENOENT, ChunkedIO::FailedToGetChunkError
job.reset
read_stream(&block)
end
@@ -130,6 +129,12 @@ module Gitlab
end
end
+ def lock(&block)
+ in_write_lock(&block)
+ rescue FailedToObtainLockError
+ raise LockedError, "build trace `#{job.id}` is locked"
+ end
+
private
def read_stream
diff --git a/lib/gitlab/ci/trace/checksum.rb b/lib/gitlab/ci/trace/checksum.rb
new file mode 100644
index 00000000000..62532ef1cd2
--- /dev/null
+++ b/lib/gitlab/ci/trace/checksum.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Trace
+ ##
+ # Trace::Checksum class is responsible for calculating a CRC32 checksum
+ # of an entire build trace using partial build trace chunks stored in a
+ # database.
+ #
+ # CRC32 checksum can be easily calculated by combining partial checksums
+ # in a right order.
+ #
+ # Then we compare CRC32 checksum provided by a GitLab Runner and expect
+ # it to be the same as the CRC32 checksum derived from partial chunks.
+ #
+ class Checksum
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :build
+
+ def initialize(build)
+ @build = build
+ end
+
+ def valid?
+ return false unless state_crc32.present?
+
+ state_crc32 == chunks_crc32
+ end
+
+ def state_crc32
+ strong_memoize(:state_crc32) { build.pending_state&.crc32 }
+ end
+
+ def chunks_crc32
+ strong_memoize(:chunks_crc32) do
+ trace_chunks.reduce(0) do |crc32, chunk|
+ Zlib.crc32_combine(crc32, chunk.crc32, chunk_size(chunk))
+ end
+ end
+ end
+
+ def last_chunk
+ strong_memoize(:last_chunk) { trace_chunks.max }
+ end
+
+ ##
+ # Trace chunks will be persisted in a database if an object store is
+ # not configured - in that case we do not want to load entire raw data
+ # of all the chunks into memory.
+ #
+ # We ignore `raw_data` attribute instead, and rely on internal build
+ # trace chunk database adapter to handle
+ # `ActiveModel::MissingAttributeError` exception.
+ #
+ # Alternative solution would be separating chunk data from chunk
+ # metadata on the database level too.
+ #
+ def trace_chunks
+ strong_memoize(:trace_chunks) do
+ build.trace_chunks.persisted
+ .select(::Ci::BuildTraceChunk.metadata_attributes)
+ end
+ end
+
+ def chunks_count
+ trace_chunks.to_a.size
+ end
+
+ private
+
+ def chunk_size(chunk)
+ if chunk == last_chunk
+ chunk.size
+ else
+ ::Ci::BuildTraceChunk::CHUNK_SIZE
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/metrics.rb b/lib/gitlab/ci/trace/metrics.rb
index 82a7d5fb83c..097436d84ea 100644
--- a/lib/gitlab/ci/trace/metrics.rb
+++ b/lib/gitlab/ci/trace/metrics.rb
@@ -6,8 +6,20 @@ module Gitlab
class Metrics
extend Gitlab::Utils::StrongMemoize
- OPERATIONS = [:appended, :streamed, :chunked, :mutated, :overwrite,
- :accepted, :finalized, :discarded, :conflict].freeze
+ OPERATIONS = [
+ :appended, # new trace data has been written to a chunk
+ :streamed, # new trace data has been sent by a runner
+ :chunked, # new trace chunk has been created
+ :mutated, # trace has been mutated when removing secrets
+ :overwrite, # runner requested overwritting a build trace
+ :accepted, # scheduled chunks for migration and responded with 202
+ :finalized, # all live build trace chunks have been persisted
+ :discarded, # failed to persist live chunks before timeout
+ :conflict, # runner has sent unrecognized build state details
+ :locked, # build trace has been locked by a different mechanism
+ :stalled, # failed to migrate chunk due to a worker duplication
+ :invalid # malformed build trace has been detected using CRC32
+ ].freeze
def increment_trace_operation(operation: :unknown)
unless OPERATIONS.include?(operation)
@@ -18,7 +30,11 @@ module Gitlab
end
def increment_trace_bytes(size)
- self.class.trace_bytes.increment(by: size.to_i)
+ self.class.trace_bytes.increment({}, size.to_i)
+ end
+
+ def observe_migration_duration(seconds)
+ self.class.finalize_histogram.observe({}, seconds.to_f)
end
def self.trace_operations
@@ -38,6 +54,17 @@ module Gitlab
Gitlab::Metrics.counter(name, comment)
end
end
+
+ def self.finalize_histogram
+ strong_memoize(:finalize_histogram) do
+ name = :gitlab_ci_trace_finalize_duration_seconds
+ comment = 'Duration of build trace chunks migration to object storage'
+ buckets = [0.1, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0, 60.0, 300.0]
+ labels = {}
+
+ ::Gitlab::Metrics.histogram(name, comment, labels, buckets)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index a072036daa8..84a9280e507 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -36,9 +36,9 @@ module Gitlab
def self.fabricate(resource)
case resource
when Hash
- self.new(resource.symbolize_keys)
+ self.new(**resource.symbolize_keys)
when ::Ci::HasVariable
- self.new(resource.to_runner_variable)
+ self.new(**resource.to_runner_variable)
when self
resource.dup
else
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
index 68f61e52df7..52a00e41214 100644
--- a/lib/gitlab/ci/yaml_processor/result.rb
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -95,6 +95,14 @@ module Gitlab
}.compact }.compact
end
+ def merged_yaml
+ @ci_config&.to_hash&.to_yaml
+ end
+
+ def variables_with_data
+ @ci_config.variables_with_data
+ end
+
private
def variables
diff --git a/lib/gitlab/cleanup/orphan_lfs_file_references.rb b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
index 14eac474e27..a6638b2cbc8 100644
--- a/lib/gitlab/cleanup/orphan_lfs_file_references.rb
+++ b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
@@ -19,6 +19,11 @@ module Gitlab
def run!
log_info("Looking for orphan LFS files for project #{project.name_with_namespace}")
+ if project.lfs_objects.empty?
+ log_info("Project #{project.name_with_namespace} is linked to 0 LFS objects. Nothing to do")
+ return
+ end
+
remove_orphan_references
end
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
index 92c799875b5..822012e0ed6 100644
--- a/lib/gitlab/cluster/puma_worker_killer_initializer.rb
+++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
@@ -5,8 +5,8 @@ module Gitlab
class PumaWorkerKillerInitializer
def self.start(
puma_options,
- puma_per_worker_max_memory_mb: 850,
- puma_master_max_memory_mb: 550,
+ puma_per_worker_max_memory_mb: 1024,
+ puma_master_max_memory_mb: 800,
additional_puma_dev_max_memory_mb: 200
)
require 'puma_worker_killer'
diff --git a/lib/gitlab/code_navigation_path.rb b/lib/gitlab/code_navigation_path.rb
index 909d0536b5f..7d36f2f12cf 100644
--- a/lib/gitlab/code_navigation_path.rb
+++ b/lib/gitlab/code_navigation_path.rb
@@ -13,7 +13,6 @@ module Gitlab
end
def full_json_path_for(path)
- return unless Feature.enabled?(:code_navigation, project, default_enabled: true)
return unless build
raw_project_job_artifacts_path(project, build, path: "lsif/#{path}.json", file_type: :lsif)
diff --git a/lib/gitlab/config/entry/composable_array.rb b/lib/gitlab/config/entry/composable_array.rb
new file mode 100644
index 00000000000..e7ad259e826
--- /dev/null
+++ b/lib/gitlab/config/entry/composable_array.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Entry that represents a composable array definition
+ #
+ class ComposableArray < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include Gitlab::Utils::StrongMemoize
+
+ # TODO: Refactor `Validatable` code so that validations can apply to a child class
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231
+ validations do
+ validates :config, type: Array
+ end
+
+ def compose!(deps = nil)
+ super do
+ @entries = Array(@entries)
+
+ # TODO: Isolate handling for a hash via: `[@config].flatten` to the `Needs` entry
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/264376
+ [@config].flatten.each_with_index do |value, index|
+ raise ArgumentError, 'Missing Composable class' unless composable_class
+
+ composable_class_name = composable_class.name.demodulize.underscore
+
+ @entries << ::Gitlab::Config::Entry::Factory.new(composable_class)
+ .value(value)
+ .with(key: composable_class_name, parent: self, description: "#{composable_class_name} definition") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+
+ def value
+ @entries.map(&:value)
+ end
+
+ def descendants
+ @entries
+ end
+
+ def composable_class
+ strong_memoize(:composable_class) do
+ opt(:composable_class)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/composable_hash.rb b/lib/gitlab/config/entry/composable_hash.rb
new file mode 100644
index 00000000000..9531b7e56fd
--- /dev/null
+++ b/lib/gitlab/config/entry/composable_hash.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Entry that represents a composable hash definition
+ # Where each hash key can be any value written by the user
+ #
+ class ComposableHash < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ # TODO: Refactor `Validatable` code so that validations can apply to a child class
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231
+ validations do
+ validates :config, type: Hash
+ end
+
+ def compose!(deps = nil)
+ super do
+ @config.each do |name, config|
+ entry_class = composable_class(name, config)
+ raise ArgumentError, 'Missing Composable class' unless entry_class
+
+ entry_class_name = entry_class.name.demodulize.underscore
+
+ factory = ::Gitlab::Config::Entry::Factory.new(entry_class)
+ .value(config || {})
+ .with(key: name, parent: self, description: "#{name} #{entry_class_name} definition") # rubocop:disable CodeReuse/ActiveRecord
+ .metadata(name: name)
+
+ @entries[name] = factory.create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+
+ def composable_class(name, config)
+ opt(:composable_class)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb
index 7c5ffaa7621..f76c98f7cbf 100644
--- a/lib/gitlab/config/entry/factory.rb
+++ b/lib/gitlab/config/entry/factory.rb
@@ -79,7 +79,7 @@ module Gitlab
end
def fabricate(entry_class, value = nil)
- entry_class.new(value, @metadata) do |node|
+ entry_class.new(value, **@metadata) do |node|
node.key = @attributes[:key]
node.parent = @attributes[:parent]
node.default = @attributes[:default]
diff --git a/lib/gitlab/config/entry/legacy_validation_helpers.rb b/lib/gitlab/config/entry/legacy_validation_helpers.rb
index 415f6f77214..be7d26fed4e 100644
--- a/lib/gitlab/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/config/entry/legacy_validation_helpers.rb
@@ -50,6 +50,12 @@ module Gitlab
variables.values.flatten(1).all?(&method(:validate_alphanumeric))
end
+ def validate_string_or_hash_value_variables(variables, allowed_value_data)
+ variables.is_a?(Hash) &&
+ variables.keys.all?(&method(:validate_alphanumeric)) &&
+ variables.values.all? { |value| validate_string_or_hash_value_variable(value, allowed_value_data) }
+ end
+
def validate_alphanumeric(value)
validate_string(value) || validate_integer(value)
end
@@ -62,6 +68,14 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
+ def validate_string_or_hash_value_variable(value, allowed_value_data)
+ if value.is_a?(Hash)
+ (value.keys - allowed_value_data).empty? && value.values.all?(&method(:validate_alphanumeric))
+ else
+ validate_alphanumeric(value)
+ end
+ end
+
def validate_regexp(value)
Gitlab::UntrustedRegexp::RubySyntax.valid?(value)
end
diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb
index 315f1947e2c..ee28891a174 100644
--- a/lib/gitlab/config/entry/simplifiable.rb
+++ b/lib/gitlab/config/entry/simplifiable.rb
@@ -19,7 +19,7 @@ module Gitlab
entry = self.class.entry_class(strategy)
- @subject = entry.new(config, metadata, &blk)
+ @subject = entry.new(config, **metadata, &blk)
super(@subject)
end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index a7ec98ace6e..2a386657e0b 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -274,6 +274,8 @@ module Gitlab
def validate_each(record, attribute, value)
if options[:array_values]
validate_key_array_values(record, attribute, value)
+ elsif options[:allowed_value_data]
+ validate_key_hash_values(record, attribute, value, options[:allowed_value_data])
else
validate_key_values(record, attribute, value)
end
@@ -290,6 +292,12 @@ module Gitlab
record.errors.add(attribute, 'should be a hash of key value pairs, value can be an array')
end
end
+
+ def validate_key_hash_values(record, attribute, value, allowed_value_data)
+ unless validate_string_or_hash_value_variables(value, allowed_value_data)
+ record.errors.add(attribute, 'should be a hash of key value pairs, value can be a hash')
+ end
+ end
end
class ExpressionValidator < ActiveModel::EachValidator
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index 53406af2c4e..047600af267 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -21,11 +21,13 @@ module Gitlab
def resolve(user, commit_message, files)
msg = commit_message || default_commit_message
resolution = Gitlab::Git::Conflict::Resolution.new(user, files, msg)
- args = {
+
+ resolver.resolve_conflicts(
+ @source_repo,
+ resolution,
source_branch: merge_request.source_branch,
target_branch: merge_request.target_branch
- }
- resolver.resolve_conflicts(@source_repo, resolution, args)
+ )
ensure
@merge_request.clear_memoized_shas
end
diff --git a/lib/gitlab/danger/commit_linter.rb b/lib/gitlab/danger/commit_linter.rb
index 954934518d7..7b01db125a9 100644
--- a/lib/gitlab/danger/commit_linter.rb
+++ b/lib/gitlab/danger/commit_linter.rb
@@ -10,7 +10,7 @@ module Gitlab
MAX_LINE_LENGTH = 72
MAX_CHANGED_FILES_IN_COMMIT = 3
MAX_CHANGED_LINES_IN_COMMIT = 30
- SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(#|!|&|%)\d+\b}.freeze
+ SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&|%)\d+(?<!`)}.freeze
DEFAULT_SUBJECT_DESCRIPTION = 'commit subject'
WIP_PREFIX = 'WIP: '
PROBLEMS = {
@@ -118,7 +118,7 @@ module Gitlab
next unless line_too_long?(line)
- url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length } # rubocop:disable CodeReuse/ActiveRecord
+ url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length }
# If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but
# only if the line _without_ the URL does not exceed this limit.
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 3626ec5bf5b..783a5f1715c 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -123,7 +123,8 @@ module Gitlab
none: "",
qa: "~QA",
test: "~test ~Quality for `spec/features/*`",
- engineering_productivity: '~"Engineering Productivity" for CI, Danger'
+ engineering_productivity: '~"Engineering Productivity" for CI, Danger',
+ ci_template: '~"ci::templates"'
}.freeze
# First-match win, so be sure to put more specific regex at the top...
CATEGORIES = {
@@ -176,6 +177,8 @@ module Gitlab
%r{(CODEOWNERS)} => :engineering_productivity,
%r{(tests.yml)} => :engineering_productivity,
+ %r{\Alib/gitlab/ci/templates} => :ci_template,
+
%r{\A(ee/)?spec/features/} => :test,
%r{\A(ee/)?spec/support/shared_examples/features/} => :test,
%r{\A(ee/)?spec/support/shared_contexts/features/} => :test,
@@ -214,6 +217,12 @@ module Gitlab
title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`')
end
+ def draft_mr?
+ return false unless gitlab_helper
+
+ DRAFT_REGEX.match?(gitlab_helper.mr_json['title'])
+ end
+
def security_mr?
return false unless gitlab_helper
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
index a6866868e6c..23f877b4e0f 100644
--- a/lib/gitlab/danger/roulette.rb
+++ b/lib/gitlab/danger/roulette.rb
@@ -52,6 +52,11 @@ module Gitlab
# Fetch an already picked backend maintainer, or pick one otherwise
spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
end
+ when :ci_template
+ if spin.maintainer.nil?
+ # Fetch an already picked backend maintainer, or pick one otherwise
+ spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
+ end
end
end
@@ -146,13 +151,19 @@ module Gitlab
%i[reviewer traintainer maintainer].map do |role|
spin_role_for_category(team, role, project, category)
end
+ hungry_reviewers = reviewers.select { |member| member.hungry }
+ hungry_traintainers = traintainers.select { |member| member.hungry }
# TODO: take CODEOWNERS into account?
# https://gitlab.com/gitlab-org/gitlab/issues/26723
- # Make traintainers have triple the chance to be picked as a reviewer
random = new_random(mr_source_branch)
- reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
+
+ # Make hungry traintainers have 4x the chance to be picked as a reviewer
+ # Make traintainers have 3x the chance to be picked as a reviewer
+ # Make hungry reviewers have 2x the chance to be picked as a reviewer
+ weighted_reviewers = reviewers + hungry_reviewers + traintainers + traintainers + traintainers + hungry_traintainers
+ reviewer = spin_for_person(weighted_reviewers, random: random, timezone_experiment: timezone_experiment)
maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
Spin.new(category, reviewer, maintainer, false, timezone_experiment)
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index ebd96be40d7..4481977db15 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -3,7 +3,7 @@
module Gitlab
module Danger
class Teammate
- attr_reader :options, :username, :name, :role, :projects, :available, :tz_offset_hours
+ attr_reader :options, :username, :name, :role, :projects, :available, :hungry, :tz_offset_hours
# The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb
def initialize(options = {})
@@ -14,6 +14,7 @@ module Gitlab
@role = options['role']
@projects = options['projects']
@available = options['available']
+ @hungry = options['hungry']
@tz_offset_hours = options['tz_offset_hours']
end
@@ -31,10 +32,8 @@ module Gitlab
projects&.has_key?(name)
end
- # Traintainers also count as reviewers
def reviewer?(project, category, labels)
- has_capability?(project, category, :reviewer, labels) ||
- traintainer?(project, category, labels)
+ has_capability?(project, category, :reviewer, labels)
end
def traintainer?(project, category, labels)
diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb
index 70587b3132a..87ebe832862 100644
--- a/lib/gitlab/data_builder/deployment.rb
+++ b/lib/gitlab/data_builder/deployment.rb
@@ -20,8 +20,8 @@ module Gitlab
environment: deployment.environment.name,
project: deployment.project.hook_attrs,
short_sha: deployment.short_sha,
- user: deployment.user.hook_attrs,
- user_url: Gitlab::UrlBuilder.build(deployment.user),
+ user: deployment.deployed_by.hook_attrs,
+ user_url: Gitlab::UrlBuilder.build(deployment.deployed_by),
commit_url: Gitlab::UrlBuilder.build(deployment.commit),
commit_title: deployment.commit.title
}
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index accc6330253..45d271a2fd4 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -92,10 +92,6 @@ module Gitlab
@version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
- def self.postgresql_9_or_less?
- version.to_f < 10
- end
-
def self.postgresql_minimum_supported_version?
version.to_f >= MINIMUM_POSTGRES_VERSION
end
@@ -127,28 +123,6 @@ module Gitlab
# ignore - happens when Rake tasks yet have to create a database, e.g. for testing
end
- # map some of the function names that changed between PostgreSQL 9 and 10
- # https://wiki.postgresql.org/wiki/New_in_postgres_10
- def self.pg_wal_lsn_diff
- Gitlab::Database.postgresql_9_or_less? ? 'pg_xlog_location_diff' : 'pg_wal_lsn_diff'
- end
-
- def self.pg_current_wal_insert_lsn
- Gitlab::Database.postgresql_9_or_less? ? 'pg_current_xlog_insert_location' : 'pg_current_wal_insert_lsn'
- end
-
- def self.pg_last_wal_receive_lsn
- Gitlab::Database.postgresql_9_or_less? ? 'pg_last_xlog_receive_location' : 'pg_last_wal_receive_lsn'
- end
-
- def self.pg_last_wal_replay_lsn
- Gitlab::Database.postgresql_9_or_less? ? 'pg_last_xlog_replay_location' : 'pg_last_wal_replay_lsn'
- end
-
- def self.pg_last_xact_replay_timestamp
- 'pg_last_xact_replay_timestamp'
- end
-
def self.nulls_last_order(field, direction = 'ASC')
Arel.sql("#{field} #{direction} NULLS LAST")
end
@@ -276,6 +250,20 @@ module Gitlab
false
end
+ def self.system_id
+ row = connection.execute('SELECT system_identifier FROM pg_control_system()').first
+
+ row['system_identifier']
+ end
+
+ def self.get_write_location(ar_connection)
+ row = ar_connection
+ .select_all("SELECT pg_current_wal_insert_lsn()::text AS location")
+ .first
+
+ row['location'] if row
+ end
+
private_class_method :database_version
def self.add_post_migrate_path_to_rails(force: false)
diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb
index 1762b81b7d8..11d9881aac2 100644
--- a/lib/gitlab/database/batch_count.rb
+++ b/lib/gitlab/database/batch_count.rb
@@ -8,15 +8,20 @@
# In order to not use a possible complex time consuming query when calculating min and max for batch_distinct_count
# the start and finish can be sent specifically
#
+# Grouped relations can be used as well. However, the preferred batch count should be around 10K because group by count is more expensive.
+#
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705
#
# Examples:
# extend ::Gitlab::Database::BatchCount
# batch_count(User.active)
# batch_count(::Clusters::Cluster.aws_installed.enabled, :cluster_id)
+# batch_count(Namespace.group(:type))
# batch_distinct_count(::Project, :creator_id)
# batch_distinct_count(::Project.with_active_services.service_desk_enabled.where(time_period), start: ::User.minimum(:id), finish: ::User.maximum(:id))
+# batch_distinct_count(Project.group(:visibility_level), :creator_id)
# batch_sum(User, :sign_in_count)
+# batch_sum(Issue.group(:state_id), :weight))
module Gitlab
module Database
module BatchCount
@@ -77,34 +82,45 @@ module Gitlab
raise "Batch counting expects positive values only for #{@column}" if start < 0 || finish < 0
return FALLBACK if unwanted_configuration?(finish, batch_size, start)
- counter = 0
+ results = nil
batch_start = start
while batch_start <= finish
+ batch_relation = build_relation_batch(batch_start, batch_start + batch_size, mode)
begin
- counter += batch_fetch(batch_start, batch_start + batch_size, mode)
+ results = merge_results(results, batch_relation.send(@operation, *@operation_args)) # rubocop:disable GitlabSecurity/PublicSend
batch_start += batch_size
- rescue ActiveRecord::QueryCanceled
+ rescue ActiveRecord::QueryCanceled => error
# retry with a safe batch size & warmer cache
if batch_size >= 2 * MIN_REQUIRED_BATCH_SIZE
batch_size /= 2
else
+ log_canceled_batch_fetch(batch_start, mode, batch_relation.to_sql, error)
return FALLBACK
end
end
sleep(SLEEP_TIME_IN_SECONDS)
end
- counter
+ results
end
- def batch_fetch(start, finish, mode)
- # rubocop:disable GitlabSecurity/PublicSend
- @relation.select(@column).public_send(mode).where(between_condition(start, finish)).send(@operation, *@operation_args)
+ def merge_results(results, object)
+ return object unless results
+
+ if object.is_a?(Hash)
+ results.merge!(object) { |_, a, b| a + b }
+ else
+ results + object
+ end
end
private
+ def build_relation_batch(start, finish, mode)
+ @relation.select(@column).public_send(mode).where(between_condition(start, finish)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def batch_size_for_mode_and_operation(mode, operation)
return DEFAULT_SUM_BATCH_SIZE if operation == :sum
@@ -118,11 +134,11 @@ module Gitlab
end
def actual_start(start)
- start || @relation.minimum(@column) || 0
+ start || @relation.unscope(:group, :having).minimum(@column) || 0
end
def actual_finish(finish)
- finish || @relation.maximum(@column) || 0
+ finish || @relation.unscope(:group, :having).maximum(@column) || 0
end
def check_mode!(mode)
@@ -130,6 +146,20 @@ module Gitlab
raise 'Use distinct count for optimized distinct counting' if @relation.limit(1).distinct_value.present? && mode != :distinct
raise 'Use distinct count only with non id fields' if @column == :id && mode == :distinct
end
+
+ def log_canceled_batch_fetch(batch_start, mode, query, error)
+ Gitlab::AppJsonLogger
+ .error(
+ event: 'batch_count',
+ relation: @relation.table_name,
+ operation: @operation,
+ operation_args: @operation_args,
+ start: batch_start,
+ mode: mode,
+ query: query,
+ message: "Query has been canceled with message: #{error.message}"
+ )
+ end
end
end
end
diff --git a/lib/gitlab/database/bulk_update.rb b/lib/gitlab/database/bulk_update.rb
new file mode 100644
index 00000000000..1403d561890
--- /dev/null
+++ b/lib/gitlab/database/bulk_update.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # Constructs queries of the form:
+ #
+ # with cte(a, b, c) as (
+ # select * from (values (:x, :y, :z), (:q, :r, :s)) as t
+ # )
+ # update table set b = cte.b, c = cte.c where a = cte.a
+ #
+ # Which is useful if you want to update a set of records in a single query
+ # but cannot express the update as a calculation (i.e. you have arbitrary
+ # updates to perform).
+ #
+ # The requirements are that the table must have an ID column used to
+ # identify the rows to be updated.
+ #
+ # Usage:
+ #
+ # mapping = {
+ # issue_a => { title: 'This title', relative_position: 100 },
+ # issue_b => { title: 'That title', relative_position: 173 }
+ # }
+ #
+ # ::Gitlab::Database::BulkUpdate.execute(%i[title relative_position], mapping)
+ #
+ # Note that this is a very low level tool, and operates on the raw column
+ # values. Enums/state fields must be translated into their underlying
+ # representations, for example, and no hooks will be called.
+ #
+ module BulkUpdate
+ LIST_SEPARATOR = ', '
+
+ class Setter
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(model, columns, mapping)
+ @table_name = model.table_name
+ @connection = model.connection
+ @columns = self.class.column_definitions(model, columns)
+ @mapping = self.class.value_mapping(mapping)
+ end
+
+ def update!
+ if without_prepared_statement?
+ # A workaround for https://github.com/rails/rails/issues/24893
+ # When prepared statements are prevented (such as when using the
+ # query counter or in omnibus by default), we cannot call
+ # `exec_update`, since that will discard the bindings.
+ connection.send(:exec_no_cache, sql, log_name, params) # rubocop: disable GitlabSecurity/PublicSend
+ else
+ connection.exec_update(sql, log_name, params)
+ end
+ end
+
+ def self.column_definitions(model, columns)
+ raise ArgumentError, 'invalid columns' if columns.blank? || columns.any? { |c| !c.is_a?(Symbol) }
+ raise ArgumentError, 'cannot set ID' if columns.include?(:id)
+
+ ([:id] | columns).map { |name| column_definition(model, name) }
+ end
+
+ def self.column_definition(model, name)
+ definition = model.column_for_attribute(name)
+ raise ArgumentError, "Unknown column: #{name}" unless definition.type
+
+ definition
+ end
+
+ def self.value_mapping(mapping)
+ raise ArgumentError, 'invalid mapping' if mapping.blank?
+ raise ArgumentError, 'invalid mapping value' if mapping.any? { |_k, v| !v.is_a?(Hash) }
+
+ mapping
+ end
+
+ private
+
+ attr_reader :table_name, :connection, :columns, :mapping
+
+ def log_name
+ strong_memoize(:log_name) do
+ "BulkUpdate #{table_name} #{columns.drop(1).map(&:name)}:#{mapping.size}"
+ end
+ end
+
+ def params
+ mapping.flat_map do |k, v|
+ obj_id = k.try(:id) || k
+ v = v.merge(id: obj_id)
+ columns.map { |c| query_attribute(c, k, v.with_indifferent_access) }
+ end
+ end
+
+ # A workaround for https://github.com/rails/rails/issues/24893
+ # We need to detect if prepared statements have been disabled.
+ def without_prepared_statement?
+ strong_memoize(:without_prepared_statement) do
+ connection.send(:without_prepared_statement?, [1]) # rubocop: disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def query_attribute(column, key, values)
+ value = values[column.name]
+ key[column.name] = value if key.try(:id) # optimistic update
+ ActiveRecord::Relation::QueryAttribute.from_user(nil, value, ActiveModel::Type.lookup(column.type))
+ end
+
+ def values
+ counter = 0
+ typed = false
+
+ mapping.map do |k, v|
+ binds = columns.map do |c|
+ bind = "$#{counter += 1}"
+ # PG is not great at inferring types - help it for the first row.
+ bind += "::#{c.sql_type}" unless typed
+ bind
+ end
+ typed = true
+
+ "(#{list_of(binds)})"
+ end
+ end
+
+ def list_of(list)
+ list.join(LIST_SEPARATOR)
+ end
+
+ def sql
+ <<~SQL
+ WITH cte(#{list_of(cte_columns)}) AS (VALUES #{list_of(values)})
+ UPDATE #{table_name} SET #{list_of(updates)} FROM cte WHERE cte_id = id
+ SQL
+ end
+
+ def column_names
+ strong_memoize(:column_names) { columns.map(&:name) }
+ end
+
+ def cte_columns
+ strong_memoize(:cte_columns) do
+ column_names.map do |c|
+ connection.quote_column_name("cte_#{c}")
+ end
+ end
+ end
+
+ def updates
+ column_names.zip(cte_columns).drop(1).map do |dest, src|
+ "#{connection.quote_column_name(dest)} = cte.#{src}"
+ end
+ end
+ end
+
+ def self.execute(columns, mapping, &to_class)
+ raise ArgumentError if mapping.blank?
+
+ entries_by_class = mapping.group_by { |k, v| block_given? ? to_class.call(k) : k.class }
+
+ entries_by_class.each do |model, entries|
+ Setter.new(model, columns, entries).update!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/concurrent_reindex.rb b/lib/gitlab/database/concurrent_reindex.rb
deleted file mode 100644
index 485ab35e55d..00000000000
--- a/lib/gitlab/database/concurrent_reindex.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-# 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/count/reltuples_count_strategy.rb b/lib/gitlab/database/count/reltuples_count_strategy.rb
index e226ed7613a..89190320cf9 100644
--- a/lib/gitlab/database/count/reltuples_count_strategy.rb
+++ b/lib/gitlab/database/count/reltuples_count_strategy.rb
@@ -74,8 +74,9 @@ module Gitlab
def get_statistics(table_names, check_statistics: true)
time = 6.hours.ago
- query = PgClass.joins("LEFT JOIN pg_stat_user_tables USING (relname)")
+ query = PgClass.joins("LEFT JOIN pg_stat_user_tables ON pg_stat_user_tables.relid = pg_class.oid")
.where(relname: table_names)
+ .where('schemaname = current_schema()')
.select('pg_class.relname AS table_name, reltuples::bigint AS estimate')
if check_statistics
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 723f0f6a308..66b6ce1ec55 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -176,7 +176,7 @@ module Gitlab
name: name.presence || concurrent_foreign_key_name(source, column)
}
- if foreign_key_exists?(source, target, options)
+ if foreign_key_exists?(source, target, **options)
warning_message = "Foreign key not created because it exists already " \
"(this may be due to an aborted migration or similar): " \
"source: #{source}, target: #{target}, column: #{options[:column]}, "\
@@ -330,13 +330,13 @@ module Gitlab
# * +timing_configuration+ - [[ActiveSupport::Duration, ActiveSupport::Duration], ...] lock timeout for the block, sleep time before the next iteration, defaults to `Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION`
# * +logger+ - [Gitlab::JsonLogger]
# * +env+ - [Hash] custom environment hash, see the example with `DISABLE_LOCK_RETRIES`
- def with_lock_retries(**args, &block)
+ def with_lock_retries(*args, **kwargs, &block)
merged_args = {
klass: self.class,
logger: Gitlab::BackgroundMigration::Logger
- }.merge(args)
+ }.merge(kwargs)
- Gitlab::Database::WithLockRetries.new(merged_args).run(&block)
+ Gitlab::Database::WithLockRetries.new(**merged_args).run(&block)
end
def true_value
@@ -544,6 +544,16 @@ module Gitlab
rename_column_concurrently(table, column, temp_column, type: new_type, type_cast_function: type_cast_function, batch_column_name: batch_column_name)
end
+ # Reverses operations performed by change_column_type_concurrently.
+ #
+ # table - The table containing the column.
+ # column - The name of the column to change.
+ def undo_change_column_type_concurrently(table, column)
+ temp_column = "#{column}_for_type_change"
+
+ undo_rename_column_concurrently(table, column, temp_column)
+ end
+
# Performs cleanup of a concurrent type change.
#
# table - The table containing the column.
@@ -560,6 +570,65 @@ module Gitlab
end
end
+ # Reverses operations performed by cleanup_concurrent_column_type_change.
+ #
+ # table - The table containing the column.
+ # column - The name of the column to change.
+ # old_type - The type of the original column used with change_column_type_concurrently.
+ # type_cast_function - Required if the conversion back to the original type is not automatic
+ # batch_column_name - option for tables without a primary key, in this case
+ # another unique integer column can be used. Example: :user_id
+ def undo_cleanup_concurrent_column_type_change(table, column, old_type, type_cast_function: nil, batch_column_name: :id)
+ temp_column = "#{column}_for_type_change"
+
+ # Using a descriptive name that includes orinal column's name risks
+ # taking us above the 63 character limit, so we use a hash
+ identifier = "#{table}_#{column}_for_type_change"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
+ temp_undo_cleanup_column = "tmp_undo_cleanup_column_#{hashed_identifier}"
+
+ unless column_exists?(table, batch_column_name)
+ raise "Column #{batch_column_name} does not exist on #{table}"
+ end
+
+ if transaction_open?
+ raise 'undo_cleanup_concurrent_column_type_change can not be run inside a transaction'
+ end
+
+ check_trigger_permissions!(table)
+
+ begin
+ create_column_from(
+ table,
+ column,
+ temp_undo_cleanup_column,
+ type: old_type,
+ batch_column_name: batch_column_name,
+ type_cast_function: type_cast_function
+ )
+
+ transaction do
+ # This has to be performed in a transaction as otherwise we might
+ # have inconsistent data.
+ rename_column(table, column, temp_column)
+ rename_column(table, temp_undo_cleanup_column, column)
+
+ install_rename_triggers(table, column, temp_column)
+ end
+ rescue
+ # create_column_from can not run inside a transaction, which means
+ # that there is a risk that if any of the operations that follow it
+ # fail, we'll be left with an inconsistent schema
+ # For those reasons, we make sure that we drop temp_undo_cleanup_column
+ # if an error is caught
+ if column_exists?(table, temp_undo_cleanup_column)
+ remove_column(table, temp_undo_cleanup_column)
+ end
+
+ raise
+ end
+ end
+
# Cleans up a concurrent column name.
#
# This method takes care of removing previously installed triggers as well
@@ -882,7 +951,7 @@ module Gitlab
# column.
opclasses[new] = opclasses.delete(old) if opclasses[old]
- options[:opclasses] = opclasses
+ options[:opclass] = opclasses
end
add_concurrent_index(table, new_columns, options)
@@ -994,10 +1063,10 @@ into similar problems in the future (e.g. when new tables are created).
def postgres_exists_by_name?(table, name)
index_sql = <<~SQL
SELECT COUNT(*)
- FROM pg_index
- JOIN pg_class i ON (indexrelid=i.oid)
- JOIN pg_class t ON (indrelid=t.oid)
- WHERE i.relname = '#{name}' AND t.relname = '#{table}'
+ FROM pg_catalog.pg_indexes
+ WHERE schemaname = #{connection.quote(current_schema)}
+ AND tablename = #{connection.quote(table)}
+ AND indexname = #{connection.quote(name)}
SQL
connection.select_value(index_sql).to_i > 0
@@ -1053,11 +1122,15 @@ into similar problems in the future (e.g. when new tables are created).
# the table name in addition to using the constraint_name
check_sql = <<~SQL
SELECT COUNT(*)
- FROM pg_constraint
- JOIN pg_class ON pg_constraint.conrelid = pg_class.oid
- WHERE pg_constraint.contype = 'c'
- AND pg_constraint.conname = '#{constraint_name}'
- AND pg_class.relname = '#{table}'
+ FROM pg_catalog.pg_constraint con
+ INNER JOIN pg_catalog.pg_class rel
+ ON rel.oid = con.conrelid
+ INNER JOIN pg_catalog.pg_namespace nsp
+ ON nsp.oid = con.connamespace
+ WHERE con.contype = 'c'
+ AND con.conname = #{connection.quote(constraint_name)}
+ AND nsp.nspname = #{connection.quote(current_schema)}
+ AND rel.relname = #{connection.quote(table)}
SQL
connection.select_value(check_sql) > 0
@@ -1147,6 +1220,64 @@ into similar problems in the future (e.g. when new tables are created).
end
end
+ # Copies all check constraints for the old column to the new column.
+ #
+ # table - The table containing the columns.
+ # old - The old column.
+ # new - The new column.
+ # schema - The schema the table is defined for
+ # If it is not provided, then the current_schema is used
+ def copy_check_constraints(table, old, new, schema: nil)
+ if transaction_open?
+ raise 'copy_check_constraints can not be run inside a transaction'
+ end
+
+ unless column_exists?(table, old)
+ raise "Column #{old} does not exist on #{table}"
+ end
+
+ unless column_exists?(table, new)
+ raise "Column #{new} does not exist on #{table}"
+ end
+
+ table_with_schema = schema.present? ? "#{schema}.#{table}" : table
+
+ check_constraints_for(table, old, schema: schema).each do |check_c|
+ validate = !(check_c["constraint_def"].end_with? "NOT VALID")
+
+ # Normalize:
+ # - Old constraint definitions:
+ # '(char_length(entity_path) <= 5500)'
+ # - Definitionss from pg_get_constraintdef(oid):
+ # 'CHECK ((char_length(entity_path) <= 5500))'
+ # - Definitions from pg_get_constraintdef(oid, pretty_bool):
+ # 'CHECK (char_length(entity_path) <= 5500)'
+ # - Not valid constraints: 'CHECK (...) NOT VALID'
+ # to a single format that we can use:
+ # '(char_length(entity_path) <= 5500)'
+ check_definition = check_c["constraint_def"]
+ .sub(/^\s*(CHECK)?\s*\({0,2}/, '(')
+ .sub(/\){0,2}\s*(NOT VALID)?\s*$/, ')')
+
+ constraint_name = begin
+ if check_definition == "(#{old} IS NOT NULL)"
+ not_null_constraint_name(table_with_schema, new)
+ elsif check_definition.start_with? "(char_length(#{old}) <="
+ text_limit_name(table_with_schema, new)
+ else
+ check_constraint_name(table_with_schema, new, 'copy_check_constraint')
+ end
+ end
+
+ add_check_constraint(
+ table_with_schema,
+ check_definition.gsub(old.to_s, new.to_s),
+ constraint_name,
+ validate: validate
+ )
+ end
+ end
+
# Migration Helpers for adding limit to text columns
def add_text_limit(table, column, limit, constraint_name: nil, validate: true)
add_check_constraint(
@@ -1274,6 +1405,37 @@ into similar problems in the future (e.g. when new tables are created).
end
end
+ # Returns an ActiveRecord::Result containing the check constraints
+ # defined for the given column.
+ #
+ # If the schema is not provided, then the current_schema is used
+ def check_constraints_for(table, column, schema: nil)
+ check_sql = <<~SQL
+ SELECT
+ ccu.table_schema as schema_name,
+ ccu.table_name as table_name,
+ ccu.column_name as column_name,
+ con.conname as constraint_name,
+ pg_get_constraintdef(con.oid) as constraint_def
+ FROM pg_catalog.pg_constraint con
+ INNER JOIN pg_catalog.pg_class rel
+ ON rel.oid = con.conrelid
+ INNER JOIN pg_catalog.pg_namespace nsp
+ ON nsp.oid = con.connamespace
+ INNER JOIN information_schema.constraint_column_usage ccu
+ ON con.conname = ccu.constraint_name
+ AND nsp.nspname = ccu.constraint_schema
+ AND rel.relname = ccu.table_name
+ WHERE nsp.nspname = #{connection.quote(schema.presence || current_schema)}
+ AND rel.relname = #{connection.quote(table)}
+ AND ccu.column_name = #{connection.quote(column)}
+ AND con.contype = 'c'
+ ORDER BY constraint_name
+ SQL
+
+ connection.exec_query(check_sql)
+ end
+
def statement_timeout_disabled?
# This is a string of the form "100ms" or "0" when disabled
connection.select_value('SHOW statement_timeout') == "0"
@@ -1284,8 +1446,9 @@ into similar problems in the future (e.g. when new tables are created).
check_sql = <<~SQL
SELECT c.is_nullable
FROM information_schema.columns c
- WHERE c.table_name = '#{table}'
- AND c.column_name = '#{column}'
+ WHERE c.table_schema = #{connection.quote(current_schema)}
+ AND c.table_name = #{connection.quote(table)}
+ AND c.column_name = #{connection.quote(column)}
SQL
connection.select_value(check_sql) == 'YES'
@@ -1352,6 +1515,7 @@ into similar problems in the future (e.g. when new tables are created).
copy_indexes(table, old, new)
copy_foreign_keys(table, old, new)
+ copy_check_constraints(table, old, new)
end
def validate_timestamp_column_name!(column_name)
diff --git a/lib/gitlab/database/partitioning/partition_creator.rb b/lib/gitlab/database/partitioning/partition_creator.rb
index 4c1b13fe3b5..547e0b9b957 100644
--- a/lib/gitlab/database/partitioning/partition_creator.rb
+++ b/lib/gitlab/database/partitioning/partition_creator.rb
@@ -72,10 +72,10 @@ module Gitlab
end
def with_lock_retries(&block)
- Gitlab::Database::WithLockRetries.new({
+ Gitlab::Database::WithLockRetries.new(
klass: self.class,
logger: Gitlab::AppLogger
- }).run(&block)
+ ).run(&block)
end
def connection
diff --git a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
index f9ad1e60776..17a42d997e6 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
@@ -11,8 +11,6 @@ module Gitlab
PAUSE_SECONDS = 0.25
def perform(start_id, stop_id, source_table, partitioned_table, source_column)
- return unless Feature.enabled?(:backfill_partitioned_audit_events, default_enabled: true)
-
if transaction_open?
raise "Aborting job to backfill partitioned #{source_table} table! Do not run this job in a transaction block!"
end
diff --git a/lib/gitlab/database/postgres_index.rb b/lib/gitlab/database/postgres_index.rb
new file mode 100644
index 00000000000..2a9f23f0098
--- /dev/null
+++ b/lib/gitlab/database/postgres_index.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class PostgresIndex < ActiveRecord::Base
+ self.table_name = 'postgres_indexes'
+ self.primary_key = 'identifier'
+
+ scope :by_identifier, ->(identifier) do
+ raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/
+
+ find(identifier)
+ end
+
+ # A 'regular' index is a non-unique index,
+ # that does not serve an exclusion constraint and
+ # is defined on a table that is not partitioned.
+ scope :regular, -> { where(unique: false, partitioned: false, exclusion: false)}
+
+ scope :random_few, ->(how_many) do
+ limit(how_many).order(Arel.sql('RANDOM()'))
+ end
+
+ scope :not_match, ->(regex) { where("name !~ ?", regex)}
+
+ def to_s
+ name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/reindexing.rb b/lib/gitlab/database/reindexing.rb
new file mode 100644
index 00000000000..074752fe75b
--- /dev/null
+++ b/lib/gitlab/database/reindexing.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Reindexing
+ def self.perform(index_selector)
+ Coordinator.new(index_selector).perform
+ end
+
+ def self.candidate_indexes
+ Gitlab::Database::PostgresIndex
+ .regular
+ .not_match("^#{ConcurrentReindex::TEMPORARY_INDEX_PREFIX}")
+ .not_match("^#{ConcurrentReindex::REPLACED_INDEX_PREFIX}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/reindexing/concurrent_reindex.rb b/lib/gitlab/database/reindexing/concurrent_reindex.rb
new file mode 100644
index 00000000000..fd3dca88567
--- /dev/null
+++ b/lib/gitlab/database/reindexing/concurrent_reindex.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Reindexing
+ class ConcurrentReindex
+ include Gitlab::Utils::StrongMemoize
+
+ ReindexError = Class.new(StandardError)
+
+ PG_IDENTIFIER_LENGTH = 63
+ TEMPORARY_INDEX_PREFIX = 'tmp_reindex_'
+ REPLACED_INDEX_PREFIX = 'old_reindex_'
+ STATEMENT_TIMEOUT = 6.hours
+
+ attr_reader :index, :logger
+
+ def initialize(index, logger: Gitlab::AppLogger)
+ @index = index
+ @logger = logger
+ end
+
+ def perform
+ raise ReindexError, 'UNIQUE indexes are currently not supported' if index.unique?
+ raise ReindexError, 'partitioned indexes are currently not supported' if index.partitioned?
+ raise ReindexError, 'indexes serving an exclusion constraint are currently not supported' if index.exclusion?
+ raise ReindexError, 'index is a left-over temporary index from a previous reindexing run' if index.name.start_with?(TEMPORARY_INDEX_PREFIX, REPLACED_INDEX_PREFIX)
+
+ logger.info "Starting reindex of #{index}"
+
+ with_rebuilt_index do |replacement_index|
+ swap_index(replacement_index)
+ end
+ end
+
+ private
+
+ def with_rebuilt_index
+ if Gitlab::Database::PostgresIndex.find_by(schema: index.schema, name: replacement_index_name)
+ logger.debug("dropping dangling index from previous run (if it exists): #{replacement_index_name}")
+ remove_index(index.schema, replacement_index_name)
+ end
+
+ create_replacement_index_statement = index.definition
+ .sub(/CREATE INDEX #{index.name}/, "CREATE INDEX CONCURRENTLY #{replacement_index_name}")
+
+ logger.info("creating replacement index #{replacement_index_name}")
+ logger.debug("replacement index definition: #{create_replacement_index_statement}")
+
+ set_statement_timeout do
+ connection.execute(create_replacement_index_statement)
+ end
+
+ replacement_index = Gitlab::Database::PostgresIndex.find_by(schema: index.schema, name: replacement_index_name)
+
+ unless replacement_index.valid_index?
+ message = 'replacement index was created as INVALID'
+ logger.error("#{message}, cleaning up")
+ raise ReindexError, "failed to reindex #{index}: #{message}"
+ end
+
+ yield replacement_index
+ ensure
+ begin
+ remove_index(index.schema, replacement_index_name)
+ rescue => e
+ logger.error(e)
+ end
+ end
+
+ def swap_index(replacement_index)
+ logger.info("swapping replacement index #{replacement_index} with #{index}")
+
+ with_lock_retries do
+ rename_index(index.schema, index.name, replaced_index_name)
+ rename_index(replacement_index.schema, replacement_index.name, index.name)
+ rename_index(index.schema, replaced_index_name, replacement_index.name)
+ end
+ end
+
+ def rename_index(schema, old_index_name, new_index_name)
+ connection.execute(<<~SQL)
+ ALTER INDEX #{quote_table_name(schema)}.#{quote_table_name(old_index_name)}
+ RENAME TO #{quote_table_name(new_index_name)}
+ SQL
+ end
+
+ def remove_index(schema, name)
+ logger.info("Removing index #{schema}.#{name}")
+
+ set_statement_timeout do
+ connection.execute(<<~SQL)
+ DROP INDEX CONCURRENTLY
+ IF EXISTS #{quote_table_name(schema)}.#{quote_table_name(name)}
+ SQL
+ end
+ end
+
+ def replacement_index_name
+ @replacement_index_name ||= "#{TEMPORARY_INDEX_PREFIX}#{index.indexrelid}"
+ end
+
+ def replaced_index_name
+ @replaced_index_name ||= "#{REPLACED_INDEX_PREFIX}#{index.indexrelid}"
+ end
+
+ def with_lock_retries(&block)
+ arguments = { klass: self.class, logger: logger }
+
+ Gitlab::Database::WithLockRetries.new(**arguments).run(raise_on_exhaustion: true, &block)
+ end
+
+ def set_statement_timeout
+ execute("SET statement_timeout TO '%ds'" % STATEMENT_TIMEOUT)
+ yield
+ ensure
+ execute('RESET statement_timeout')
+ end
+
+ delegate :execute, :quote_table_name, to: :connection
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/reindexing/coordinator.rb b/lib/gitlab/database/reindexing/coordinator.rb
new file mode 100644
index 00000000000..0957f43e166
--- /dev/null
+++ b/lib/gitlab/database/reindexing/coordinator.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Reindexing
+ class Coordinator
+ include ExclusiveLeaseGuard
+
+ # Maximum lease time for the global Redis lease
+ # This should be higher than the maximum time for any
+ # long running step in the reindexing process (compare with
+ # statement timeouts).
+ TIMEOUT_PER_ACTION = 1.day
+
+ attr_reader :indexes
+
+ def initialize(indexes)
+ @indexes = indexes
+ end
+
+ def perform
+ indexes.each do |index|
+ # This obtains a global lease such that there's
+ # only one live reindexing process at a time.
+ try_obtain_lease do
+ ReindexAction.keep_track_of(index) do
+ ConcurrentReindex.new(index).perform
+ end
+ end
+ end
+ end
+
+ private
+
+ def lease_timeout
+ TIMEOUT_PER_ACTION
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/reindexing/reindex_action.rb b/lib/gitlab/database/reindexing/reindex_action.rb
new file mode 100644
index 00000000000..0928ef90e5d
--- /dev/null
+++ b/lib/gitlab/database/reindexing/reindex_action.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Reindexing
+ class ReindexAction < ActiveRecord::Base
+ self.table_name = 'postgres_reindex_actions'
+
+ enum state: { started: 0, finished: 1, failed: 2 }
+
+ def self.keep_track_of(index, &block)
+ action = create!(
+ index_identifier: index.identifier,
+ action_start: Time.zone.now,
+ ondisk_size_bytes_start: index.ondisk_size_bytes
+ )
+
+ yield
+
+ action.state = :finished
+ rescue
+ action.state = :failed
+ raise
+ ensure
+ index.reload # rubocop:disable Cop/ActiveRecordAssociationReload
+
+ action.action_end = Time.zone.now
+ action.ondisk_size_bytes_end = index.ondisk_size_bytes
+
+ action.save!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_helpers.rb b/lib/gitlab/database/schema_helpers.rb
index dda4d8eecdb..3d929c62933 100644
--- a/lib/gitlab/database/schema_helpers.rb
+++ b/lib/gitlab/database/schema_helpers.rb
@@ -32,11 +32,14 @@ module Gitlab
def trigger_exists?(table_name, name)
connection.select_value(<<~SQL)
SELECT 1
- FROM pg_trigger
- INNER JOIN pg_class
- ON pg_trigger.tgrelid = pg_class.oid
- WHERE pg_class.relname = '#{table_name}'
- AND pg_trigger.tgname = '#{name}'
+ FROM pg_catalog.pg_trigger trgr
+ INNER JOIN pg_catalog.pg_class rel
+ ON trgr.tgrelid = rel.oid
+ INNER JOIN pg_catalog.pg_namespace nsp
+ ON nsp.oid = rel.relnamespace
+ WHERE nsp.nspname = #{connection.quote(current_schema)}
+ AND rel.relname = #{connection.quote(table_name)}
+ AND trgr.tgname = #{connection.quote(name)}
SQL
end
@@ -68,10 +71,10 @@ module Gitlab
end
def with_lock_retries(&block)
- Gitlab::Database::WithLockRetries.new({
+ Gitlab::Database::WithLockRetries.new(
klass: self.class,
logger: Gitlab::BackgroundMigration::Logger
- }).run(&block)
+ ).run(&block)
end
def assert_not_in_transaction_block(scope:)
diff --git a/lib/gitlab/database/similarity_score.rb b/lib/gitlab/database/similarity_score.rb
index 2633c29438a..ff78fd0218c 100644
--- a/lib/gitlab/database/similarity_score.rb
+++ b/lib/gitlab/database/similarity_score.rb
@@ -6,6 +6,11 @@ module Gitlab
EMPTY_STRING = Arel.sql("''").freeze
EXPRESSION_ON_INVALID_INPUT = Arel::Nodes::NamedFunction.new('CAST', [Arel.sql('0').as('integer')]).freeze
DEFAULT_MULTIPLIER = 1
+ DISPLAY_NAME = self.name.underscore.freeze
+
+ # Adds a "magic" comment in the generated SQL expression in order to be able to tell if we're sorting by similarity.
+ # Example: /* gitlab/database/similarity_score */ SIMILARITY(COALESCE...
+ SIMILARITY_FUNCTION_CALL_WITH_ANNOTATION = "/* #{DISPLAY_NAME} */ SIMILARITY".freeze
# This method returns an Arel expression that can be used in an ActiveRecord query to order the resultset by similarity.
#
@@ -74,6 +79,10 @@ module Gitlab
end
end
+ def self.order_by_similarity?(arel_query)
+ arel_query.to_sql.include?(SIMILARITY_FUNCTION_CALL_WITH_ANNOTATION)
+ end
+
# (SIMILARITY(COALESCE(column, ''), 'search_string') * CAST(multiplier AS numeric))
def self.rule_to_arel(search, rule)
Arel::Nodes::Grouping.new(
@@ -91,7 +100,7 @@ module Gitlab
# SIMILARITY(COALESCE(column, ''), 'search_string')
def self.similarity_function_call(search, column)
- Arel::Nodes::NamedFunction.new('SIMILARITY', [column, Arel.sql(search)])
+ Arel::Nodes::NamedFunction.new(SIMILARITY_FUNCTION_CALL_WITH_ANNOTATION, [column, Arel.sql(search)])
end
# CAST(multiplier AS numeric)
diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb
index a9c86e4e267..3fb52d786ad 100644
--- a/lib/gitlab/database/with_lock_retries.rb
+++ b/lib/gitlab/database/with_lock_retries.rb
@@ -95,7 +95,7 @@ module Gitlab
run_block_with_transaction
rescue ActiveRecord::LockWaitTimeout
if retry_with_lock_timeout?
- disable_idle_in_transaction_timeout
+ disable_idle_in_transaction_timeout if ActiveRecord::Base.connection.transaction_open?
wait_until_next_retry
reset_db_settings
@@ -149,7 +149,7 @@ module Gitlab
log(message: "Couldn't acquire lock to perform the migration", current_iteration: current_iteration)
log(message: "Executing the migration without lock timeout", current_iteration: current_iteration)
- execute("SET LOCAL lock_timeout TO '0'")
+ disable_lock_timeout if ActiveRecord::Base.connection.transaction_open?
run_block
@@ -184,6 +184,10 @@ module Gitlab
execute("SET LOCAL idle_in_transaction_session_timeout TO '0'")
end
+ def disable_lock_timeout
+ execute("SET LOCAL lock_timeout TO '0'")
+ end
+
def reset_db_settings
execute('RESET idle_in_transaction_session_timeout; RESET lock_timeout')
end
diff --git a/lib/gitlab/design_management/copy_design_collection_model_attributes.yml b/lib/gitlab/design_management/copy_design_collection_model_attributes.yml
new file mode 100644
index 00000000000..1d341e6520e
--- /dev/null
+++ b/lib/gitlab/design_management/copy_design_collection_model_attributes.yml
@@ -0,0 +1,43 @@
+# This file exists to lock the attributes of Design Management models
+# that get copied in `DesignManagement::CopyDesignCollection::CopyService`
+# to specific schemas.
+#
+# This allows us to perform sanity checks and alert when there are changes
+# to the schema by running expectations against the lists in this file
+# and the actual schema of the models in `copy_designs_service_spec.rb`.
+#
+# If you are here because you received a failed test in
+# `copy_designs_service_spec.rb`, you need to decide how to handle the
+# changes and whether the new attribute(s) should be included in the copy
+# or ignored.
+
+# COPY.
+# Add attributes that should be copied to the `{model}_attributes` lists:
+design_attributes:
+ - filename
+ - relative_position
+
+version_attributes:
+ - author_id
+ - created_at
+
+action_attributes: # (None)
+
+# IGNORE.
+# Add attributes that should not be copied to the `ignore_{model}_attributes` lists:
+ignore_design_attributes:
+ - id
+ - issue_id
+ - project_id
+
+ignore_version_attributes:
+ - id
+ - issue_id
+ - sha
+
+ignore_action_attributes:
+ - id
+ - design_id
+ - event
+ - image_v432x230
+ - version_id
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
index d54e1aad19a..16257bb5ff5 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
@@ -11,7 +11,7 @@ module Gitlab
super(merge_request_diff,
project: merge_request_diff.project,
- diff_options: diff_options,
+ diff_options: merged_diff_options(diff_options),
diff_refs: merge_request_diff.diff_refs,
fallback_diff_refs: merge_request_diff.fallback_diff_refs)
end
@@ -64,6 +64,13 @@ module Gitlab
diff_stats_cache.read || super
end
end
+
+ def merged_diff_options(diff_options)
+ project = @merge_request_diff.project
+ max_diff_options = ::Commit.max_diff_options(project: project).merge(project: project)
+
+ diff_options.present? ? diff_options.merge(max_diff_options) : max_diff_options
+ end
end
end
end
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index 0eb22e6b3cb..90cb9c8638a 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -20,10 +20,27 @@ module Gitlab
# - Assigns DiffFile#highlighted_diff_lines for cached files
#
def decorate(diff_file)
- if content = read_file(diff_file)
- diff_file.highlighted_diff_lines = content.map do |line|
- Gitlab::Diff::Line.safe_init_from_hash(line)
- end
+ content = read_file(diff_file)
+
+ return [] unless content
+
+ # TODO: We could add some kind of flag to #initialize that would allow
+ # us to force re-caching
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/263508
+ #
+ if content.empty? && recache_due_to_size?(diff_file)
+ # If the file is missing from the cache and there's reason to believe
+ # it is uncached due to a size issue around changing the values for
+ # max patch size, manually populate the hash and then set the value.
+ #
+ new_cache_content = {}
+ new_cache_content[diff_file.file_path] = diff_file.highlighted_diff_lines.map(&:to_hash)
+
+ write_to_redis_hash(new_cache_content)
+
+ set_highlighted_diff_lines(diff_file, read_file(diff_file))
+ else
+ set_highlighted_diff_lines(diff_file, content)
end
end
@@ -58,6 +75,28 @@ module Gitlab
private
+ def set_highlighted_diff_lines(diff_file, content)
+ diff_file.highlighted_diff_lines = content.map do |line|
+ Gitlab::Diff::Line.safe_init_from_hash(line)
+ end
+ end
+
+ def recache_due_to_size?(diff_file)
+ diff_file_class = diff_file.diff.class
+
+ current_patch_safe_limit_bytes = diff_file_class.patch_safe_limit_bytes
+ default_patch_safe_limit_bytes = diff_file_class.patch_safe_limit_bytes(diff_file_class::DEFAULT_MAX_PATCH_BYTES)
+
+ # If the diff is >= than the default limit, but less than the current
+ # limit, it is likely uncached due to having hit the default limit,
+ # making it eligible for recalculating.
+ #
+ diff_file.diff.diff_bytesize.between?(
+ default_patch_safe_limit_bytes,
+ current_patch_safe_limit_bytes
+ )
+ end
+
def cacheable_files
strong_memoize(:cacheable_files) do
diff_files.select { |file| cacheable?(file) && read_file(file).nil? }
diff --git a/lib/gitlab/exclusive_lease_helpers.rb b/lib/gitlab/exclusive_lease_helpers.rb
index 10762d83588..da5b0afad38 100644
--- a/lib/gitlab/exclusive_lease_helpers.rb
+++ b/lib/gitlab/exclusive_lease_helpers.rb
@@ -35,7 +35,7 @@ module Gitlab
lease.obtain(1 + retries)
- yield(lease.retried?)
+ yield(lease.retried?, lease)
ensure
lease&.cancel
end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index dca60c93fb2..1ce3ffe4c86 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -48,12 +48,12 @@ module Gitlab
invite_members_version_a: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionA'
},
+ invite_members_version_b: {
+ tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionB'
+ },
new_create_project_ui: {
tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi'
},
- terms_opt_in: {
- tracking_category: 'Growth::Acquisition::Experiment::TermsOptIn'
- },
contact_sales_btn_in_app: {
tracking_category: 'Growth::Conversion::Experiment::ContactSalesInApp'
},
@@ -62,6 +62,15 @@ module Gitlab
},
invite_email: {
tracking_category: 'Growth::Acquisition::Experiment::InviteEmail'
+ },
+ invitation_reminders: {
+ tracking_category: 'Growth::Acquisition::Experiment::InvitationReminders'
+ },
+ group_only_trials: {
+ tracking_category: 'Growth::Conversion::Experiment::GroupOnlyTrials'
+ },
+ default_to_issues_board: {
+ tracking_category: 'Growth::Conversion::Experiment::DefaultToIssuesBoard'
}
}.freeze
@@ -91,28 +100,40 @@ module Gitlab
}
end
+ def push_frontend_experiment(experiment_key)
+ var_name = experiment_key.to_s.camelize(:lower)
+ enabled = experiment_enabled?(experiment_key)
+
+ gon.push({ experiments: { var_name => enabled } }, true)
+ end
+
def experiment_enabled?(experiment_key)
return false if dnt_enabled?
- return true if Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index)
+ return true if Experimentation.enabled_for_value?(experiment_key, experimentation_subject_index)
return true if forced_enabled?(experiment_key)
false
end
def track_experiment_event(experiment_key, action, value = nil)
+ return if dnt_enabled?
+
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
- ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data)
+ ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), **tracking_data)
end
end
def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
+ return if dnt_enabled?
+
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
gon.push(tracking_data: tracking_data)
end
end
def record_experiment_user(experiment_key)
+ return if dnt_enabled?
return unless Experimentation.enabled?(experiment_key) && current_user
::Experiment.add_user(experiment_key, tracking_group(experiment_key), current_user)
@@ -183,9 +204,14 @@ module Gitlab
experiment.enabled? && experiment.enabled_for_environment?
end
- def enabled_for_user?(experiment_key, experimentation_subject_index)
+ def enabled_for_attribute?(experiment_key, attribute)
+ index = Digest::SHA1.hexdigest(attribute).hex % 100
+ enabled_for_value?(experiment_key, index)
+ end
+
+ def enabled_for_value?(experiment_key, experimentation_subject_index)
enabled?(experiment_key) &&
- experiment(experiment_key).enabled_for_experimentation_subject?(experimentation_subject_index)
+ experiment(experiment_key).enabled_for_index?(experimentation_subject_index)
end
end
@@ -200,10 +226,10 @@ module Gitlab
environment
end
- def enabled_for_experimentation_subject?(experimentation_subject_index)
- return false if experimentation_subject_index.blank?
+ def enabled_for_index?(index)
+ return false if index.blank?
- experimentation_subject_index <= experiment_percentage
+ index <= experiment_percentage
end
private
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 09a49b6c1ca..78c47023c08 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -120,8 +120,8 @@ module Gitlab
# default.
#
# Patches surpassing this limit should still be persisted in the database.
- def patch_safe_limit_bytes
- patch_hard_limit_bytes / 10
+ def patch_safe_limit_bytes(limit = patch_hard_limit_bytes)
+ limit / 10
end
# Returns the limit for a single diff file (patch).
@@ -174,9 +174,13 @@ module Gitlab
@line_count ||= Util.count_lines(@diff)
end
+ def diff_bytesize
+ @diff_bytesize ||= @diff.bytesize
+ end
+
def too_large?
if @too_large.nil?
- @too_large = @diff.bytesize >= self.class.patch_hard_limit_bytes
+ @too_large = diff_bytesize >= self.class.patch_hard_limit_bytes
else
@too_large
end
@@ -194,7 +198,7 @@ module Gitlab
def collapsed?
return @collapsed if defined?(@collapsed)
- @collapsed = !expanded && @diff.bytesize >= self.class.patch_safe_limit_bytes
+ @collapsed = !expanded && diff_bytesize >= self.class.patch_safe_limit_bytes
end
def collapse!
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index e6121d688ba..6090d1b9f69 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -7,19 +7,27 @@ module Gitlab
class DiffCollection
include Enumerable
- DEFAULT_LIMITS = { max_files: 100, max_lines: 5000 }.freeze
-
attr_reader :limits
delegate :max_files, :max_lines, :max_bytes, :safe_max_files, :safe_max_lines, :safe_max_bytes, to: :limits
+ def self.default_limits(project: nil)
+ if Feature.enabled?(:increased_diff_limits, project)
+ { max_files: 200, max_lines: 7500 }
+ else
+ { max_files: 100, max_lines: 5000 }
+ end
+ end
+
def self.limits(options = {})
limits = {}
- limits[:max_files] = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
- limits[:max_lines] = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
+ defaults = default_limits(project: options[:project])
+ limits[:max_files] = options.fetch(:max_files, defaults[:max_files])
+ limits[:max_lines] = options.fetch(:max_lines, defaults[:max_lines])
limits[:max_bytes] = limits[:max_files] * 5.kilobytes # Average 5 KB per file
- limits[:safe_max_files] = [limits[:max_files], DEFAULT_LIMITS[:max_files]].min
- limits[:safe_max_lines] = [limits[:max_lines], DEFAULT_LIMITS[:max_lines]].min
+
+ limits[:safe_max_files] = [limits[:max_files], defaults[:max_files]].min
+ limits[:safe_max_lines] = [limits[:max_lines], defaults[:max_lines]].min
limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file
limits[:max_patch_bytes] = Gitlab::Git::Diff.patch_hard_limit_bytes
@@ -110,11 +118,17 @@ module Gitlab
files >= safe_max_files || @line_count > safe_max_lines || @byte_count >= safe_max_bytes
end
+ def expand_diff?
+ # Force single-entry diff collections to always present as expanded
+ #
+ @iterator.size == 1 || !@enforce_limits || @expanded
+ end
+
def each_gitaly_patch
i = @array.length
@iterator.each do |raw|
- diff = Gitlab::Git::Diff.new(raw, expanded: !@enforce_limits || @expanded)
+ diff = Gitlab::Git::Diff.new(raw, expanded: expand_diff?)
if raw.overflow_marker
@overflow = true
@@ -137,11 +151,9 @@ module Gitlab
break
end
- expanded = !@enforce_limits || @expanded
-
- diff = Gitlab::Git::Diff.new(raw, expanded: expanded)
+ diff = Gitlab::Git::Diff.new(raw, expanded: expand_diff?)
- if !expanded && over_safe_limits?(i) && diff.line_count > 0
+ if !expand_diff? && over_safe_limits?(i) && diff.line_count > 0
diff.collapse!
end
diff --git a/lib/gitlab/git/diff_stats_collection.rb b/lib/gitlab/git/diff_stats_collection.rb
index 7e49d79676e..e30ec836a49 100644
--- a/lib/gitlab/git/diff_stats_collection.rb
+++ b/lib/gitlab/git/diff_stats_collection.rb
@@ -22,8 +22,8 @@ module Gitlab
@collection.map(&:path)
end
- def real_size
- max_files = ::Commit.max_diff_options[:max_files]
+ def real_size(project: nil)
+ max_files = ::Commit.max_diff_options(project: project)[:max_files]
if paths.size > max_files
"#{max_files}+"
else
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 8ace4157ad7..1a3409c1f84 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -297,10 +297,17 @@ module Gitlab
end
file_name = "#{name}.#{extension}"
- File.join(storage_path, self.gl_repository, sha, file_name)
+ File.join(storage_path, self.gl_repository, sha, archive_version_path, file_name)
end
private :archive_file_path
+ def archive_version_path
+ return '' unless Feature.enabled?(:include_lfs_blobs_in_archive)
+
+ '@v2'
+ end
+ private :archive_version_path
+
# Return repo size in megabytes
def size
size = gitaly_repository_client.repository_size
@@ -580,9 +587,9 @@ module Gitlab
tags.find { |tag| tag.name == name }
end
- def merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
+ def merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref, allow_conflicts)
wrapped_gitaly_errors do
- gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
+ gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref, allow_conflicts)
end
end
@@ -610,7 +617,7 @@ module Gitlab
}
wrapped_gitaly_errors do
- gitaly_operation_client.user_revert(args)
+ gitaly_operation_client.user_revert(**args)
end
end
@@ -626,7 +633,7 @@ module Gitlab
}
wrapped_gitaly_errors do
- gitaly_operation_client.user_cherry_pick(args)
+ gitaly_operation_client.user_cherry_pick(**args)
end
end
@@ -640,7 +647,7 @@ module Gitlab
}
wrapped_gitaly_errors do
- gitaly_operation_client.user_update_submodule(args)
+ gitaly_operation_client.user_update_submodule(**args)
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index da2d015ca4a..11919be594d 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -100,10 +100,6 @@ module Gitlab
wrapped_gitaly_errors do
gitaly_find_page(title: title, version: version, dir: dir)
end
- rescue Gitlab::Git::CommandError
- # Return nil for invalid versions.
- # This can be removed with https://gitlab.com/gitlab-org/gitaly/-/merge_requests/2323 in place.
- nil
end
def file(name, version)
@@ -159,6 +155,8 @@ module Gitlab
return unless wiki_page
Gitlab::Git::WikiPage.new(wiki_page, version)
+ rescue GRPC::InvalidArgument
+ nil
end
def gitaly_find_file(name, version)
@@ -173,9 +171,9 @@ module Gitlab
gitaly_pages =
if load_content
- gitaly_wiki_client.load_all_pages(params)
+ gitaly_wiki_client.load_all_pages(**params)
else
- gitaly_wiki_client.list_all_pages(params)
+ gitaly_wiki_client.list_all_pages(**params)
end
gitaly_pages.map do |wiki_page, version|
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index b67b3a37440..0576d1dd9db 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -47,6 +47,16 @@ module Gitlab
:cmd, :changes
attr_accessor :container
+ def self.error_message(key)
+ self.ancestors.each do |cls|
+ return cls.const_get('ERROR_MESSAGES', false).fetch(key)
+ rescue NameError, KeyError
+ next
+ end
+
+ raise ArgumentError, "No error message defined for #{key}"
+ end
+
def initialize(actor, container, protocol, authentication_abilities:, namespace_path: nil, repository_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor
@container = container
@@ -137,6 +147,10 @@ module Gitlab
private
def check_container!
+ # Strict nil check, to avoid any surprises with Object#present?
+ # which can delegate to #empty?
+ raise NotFoundError, not_found_message if container.nil?
+
check_project! if project?
end
@@ -204,9 +218,7 @@ module Gitlab
end
def check_project_accessibility!
- if project.blank? || !can_read_project?
- raise NotFoundError, not_found_message
- end
+ raise NotFoundError, not_found_message unless can_read_project?
end
def not_found_message
@@ -279,10 +291,10 @@ module Gitlab
error_message(:download)
end
- # We assume that all git-access classes are in project context by default.
- # Override this method to be more specific.
def project?
- true
+ # Strict nil check, to avoid any surprises with Object#present?
+ # which can delegate to #empty?
+ !project.nil?
end
def project
@@ -290,7 +302,7 @@ module Gitlab
end
def check_push_access!
- if container.repository_read_only?
+ if project&.repository_read_only?
raise ForbiddenError, error_message(:read_only)
end
@@ -411,13 +423,7 @@ module Gitlab
protected
def error_message(key)
- self.class.ancestors.each do |cls|
- return cls.const_get('ERROR_MESSAGES', false).fetch(key)
- rescue NameError, KeyError
- next
- end
-
- raise ArgumentError, "No error message defined for #{key}"
+ self.class.error_message(key)
end
def success_result
@@ -504,7 +510,7 @@ module Gitlab
changes_size = 0
changes_list.each do |change|
- changes_size += repository.new_blobs(change[:newrev]).sum(&:size) # rubocop: disable CodeReuse/ActiveRecord
+ changes_size += repository.new_blobs(change[:newrev]).sum(&:size)
check_size_against_limit(changes_size)
end
diff --git a/lib/gitlab/git_access_result/custom_action.rb b/lib/gitlab/git_access_result/custom_action.rb
index e03459ea7a1..224c6b9eb52 100644
--- a/lib/gitlab/git_access_result/custom_action.rb
+++ b/lib/gitlab/git_access_result/custom_action.rb
@@ -12,7 +12,7 @@ module Gitlab
# 'data' => {
# 'api_endpoints' => %w{geo/proxy_git_ssh/info_refs_receive_pack geo/proxy_git_ssh/receive_pack},
# 'gl_username' => user.username,
- # 'primary_repo' => geo_primary_http_url_to_repo(project_or_wiki)
+ # 'primary_repo' => geo_primary_http_url_to_repo(container)
# }
# }
#
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index ae83e45f2b3..710e2ce90ec 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -21,6 +21,11 @@ module Gitlab
@authentication_abilities &= [:download_code, :push_code]
end
+ override :project
+ def project
+ container.project if container.is_a?(ProjectSnippet)
+ end
+
override :check
def check(cmd, changes)
check_snippet_accessibility!
@@ -46,29 +51,19 @@ module Gitlab
# snippets never return custom actions, such as geo replication.
end
- override :project?
- def project?
- project_snippet?
- end
-
- override :project
- def project
- snippet&.project
- end
-
override :check_valid_actor!
def check_valid_actor!
# TODO: Investigate if expanding actor/authentication types are needed.
# https://gitlab.com/gitlab-org/gitlab/issues/202190
- if actor && !actor.is_a?(User) && !actor.instance_of?(Key)
+ if actor && !allowed_actor?
raise ForbiddenError, ERROR_MESSAGES[:authentication_mechanism]
end
super
end
- def project_snippet?
- snippet.is_a?(ProjectSnippet)
+ def allowed_actor?
+ actor.is_a?(User) || actor.instance_of?(Key)
end
override :check_push_access!
@@ -120,7 +115,7 @@ module Gitlab
override :check_single_change_access
def check_single_change_access(change, _skip_lfs_integrity_check: false)
Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate!
- Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate!
+ Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit, logger: logger).validate!
rescue Checks::TimedLogger::TimeoutError
raise TimeoutError, logger.full_message
end
@@ -138,3 +133,5 @@ module Gitlab
end
end
end
+
+Gitlab::GitAccessSnippet.prepend_if_ee('EE::Gitlab::GitAccessSnippet')
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index a941282e713..3011b794b8f 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -12,6 +12,11 @@ module Gitlab
write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze
+ override :project
+ def project
+ container.project if container.is_a?(ProjectWiki)
+ end
+
override :download_ability
def download_ability
:download_wiki_code
@@ -40,11 +45,6 @@ module Gitlab
def not_found_message
error_message(:not_found)
end
-
- override :repository
- def repository
- container.wiki.repository
- end
end
end
diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb
index 98d327a7329..e98ae75590d 100644
--- a/lib/gitlab/gitaly_client/diff_stitcher.rb
+++ b/lib/gitlab/gitaly_client/diff_stitcher.rb
@@ -5,8 +5,10 @@ module Gitlab
class DiffStitcher
include Enumerable
- def initialize(rpc_response)
- @rpc_response = rpc_response
+ delegate :size, to: :rpc_response
+
+ def initialize(rpc_response_param)
+ @rpc_response = rpc_response_param
end
def each
@@ -31,6 +33,10 @@ module Gitlab
end
end
end
+
+ private
+
+ attr_reader :rpc_response
end
end
end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 786eb3ca4ae..4850d646de4 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -102,7 +102,7 @@ module Gitlab
end
end
- def user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
+ def user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref, allow_conflicts)
request = Gitaly::UserMergeToRefRequest.new(
repository: @gitaly_repo,
source_sha: source_sha,
@@ -110,7 +110,8 @@ module Gitlab
target_ref: encode_binary(target_ref),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
message: encode_binary(message),
- first_parent_ref: encode_binary(first_parent_ref)
+ first_parent_ref: encode_binary(first_parent_ref),
+ allow_conflicts: allow_conflicts
)
response = GitalyClient.call(@repository.storage, :operation_service,
diff --git a/lib/gitlab/gitpod.rb b/lib/gitlab/gitpod.rb
index 11b54db72ea..e35fb8fed02 100644
--- a/lib/gitlab/gitpod.rb
+++ b/lib/gitlab/gitpod.rb
@@ -3,17 +3,13 @@
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?
+ !feature.off? || feature_enabled?
end
def feature_enabled?(actor = nil)
- feature.enabled?(actor)
+ Feature.enabled?(:gitpod, actor, default_enabled: true)
end
def feature_and_settings_enabled?(actor = nil)
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index 54dca93a891..352a93817be 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -4,6 +4,8 @@ module Gitlab
class GlRepository
include Singleton
+ # TODO: Refactor these constants into proper classes
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/259008
PROJECT = RepoType.new(
name: :project,
access_checker_class: Gitlab::GitAccessProject,
@@ -12,8 +14,12 @@ module Gitlab
WIKI = RepoType.new(
name: :wiki,
access_checker_class: Gitlab::GitAccessWiki,
- 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 },
+ repository_resolver: -> (container) do
+ wiki = container.is_a?(Wiki) ? container : container.wiki # Also allow passing a Project, Group, or Geo::DeletedProject
+ ::Repository.new(wiki.full_path, wiki, shard: wiki.repository_storage, disk_path: wiki.disk_path, repo_type: WIKI)
+ end,
+ container_class: ProjectWiki,
+ project_resolver: -> (wiki) { wiki.try(:project) },
suffix: :wiki
).freeze
SNIPPET = RepoType.new(
diff --git a/lib/gitlab/gl_repository/identifier.rb b/lib/gitlab/gl_repository/identifier.rb
index 57350b1edb0..f521a14ea19 100644
--- a/lib/gitlab/gl_repository/identifier.rb
+++ b/lib/gitlab/gl_repository/identifier.rb
@@ -53,12 +53,13 @@ module Gitlab
private
def container_class
- case @container_type
- when 'project'
- Project
- when 'group'
- Group
- end
+ # NOTE: This is currently only used and supported for group wikis
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/219192
+ return unless @repo_type_name == 'wiki'
+
+ "#{@container_type}_#{@repo_type_name}".classify.constantize
+ rescue NameError
+ nil
end
end
diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb
index 346f6be0d98..4b1f4fcc2a2 100644
--- a/lib/gitlab/gl_repository/repo_type.rb
+++ b/lib/gitlab/gl_repository/repo_type.rb
@@ -29,10 +29,6 @@ module Gitlab
end
def identifier_for_container(container)
- if container.is_a?(Group)
- return "#{container.class.name.underscore}-#{container.id}-#{name}"
- end
-
"#{name}-#{container.id}"
end
@@ -84,3 +80,5 @@ module Gitlab
end
end
end
+
+Gitlab::GlRepository::RepoType.prepend_if_ee('EE::Gitlab::GlRepository::RepoType')
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 66517ecd743..10660649623 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -43,12 +43,10 @@ module Gitlab
# Initialize gon.features with any flags that should be
# made globally available to the frontend
- 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: true)
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
+ push_frontend_feature_flag(:usage_data_api, default_enabled: true)
+ push_frontend_feature_flag(:security_auto_fix, 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)
@@ -59,9 +57,9 @@ module Gitlab
# name - The name of the feature flag, e.g. `my_feature`.
# args - Any additional arguments to pass to `Feature.enabled?`. This allows
# you to check if a flag is enabled for a particular user.
- def push_frontend_feature_flag(name, *args)
+ def push_frontend_feature_flag(name, *args, **kwargs)
var_name = name.to_s.camelize(:lower)
- enabled = Feature.enabled?(name, *args)
+ enabled = Feature.enabled?(name, *args, **kwargs)
# Here the `true` argument signals gon that the value should be merged
# into any existing ones, instead of overwriting them. This allows you to
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index 27673e5c27a..c70127553fd 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -29,8 +29,8 @@ module Gitlab
raise NotImplementedError, "Implement #find_object in #{self.class.name}"
end
- def authorized_find!(*args)
- object = Graphql::Lazy.force(find_object(*args))
+ def authorized_find!(*args, **kwargs)
+ object = Graphql::Lazy.force(find_object(*args, **kwargs))
authorize!(object)
diff --git a/lib/gitlab/graphql/global_id_compatibility.rb b/lib/gitlab/graphql/global_id_compatibility.rb
new file mode 100644
index 00000000000..a96e4c4b976
--- /dev/null
+++ b/lib/gitlab/graphql/global_id_compatibility.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module GlobalIDCompatibility
+ # TODO: remove this module once the compatibility layer is no longer needed.
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ def coerce_global_id_arguments!(args)
+ global_id_arguments = self.class.arguments.values.select do |arg|
+ arg.type.is_a?(Class) && arg.type <= ::Types::GlobalIDType
+ end
+
+ global_id_arguments.each do |arg|
+ k = arg.keyword
+ args[k] &&= arg.type.coerce_isolated_input(args[k])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/markdown_field.rb b/lib/gitlab/graphql/markdown_field.rb
index 7be6810f7ba..0b5bde8d8d9 100644
--- a/lib/gitlab/graphql/markdown_field.rb
+++ b/lib/gitlab/graphql/markdown_field.rb
@@ -12,13 +12,19 @@ module Gitlab
end
method_name = kwargs.delete(:method) || name.to_s.sub(/_html$/, '')
- kwargs[:resolve] = Gitlab::Graphql::MarkdownField::Resolver.new(method_name.to_sym).proc
+ resolver_method = "#{name}_resolver".to_sym
+ kwargs[:resolver_method] = resolver_method
kwargs[:description] ||= "The GitLab Flavored Markdown rendering of `#{method_name}`"
# Adding complexity to rendered notes since that could cause queries.
kwargs[:complexity] ||= 5
field name, GraphQL::STRING_TYPE, **kwargs
+
+ define_method resolver_method do
+ # We need to `dup` the context so the MarkdownHelper doesn't modify it
+ ::MarkupHelper.markdown_field(object, method_name.to_sym, context.to_h.dup)
+ end
end
end
end
diff --git a/lib/gitlab/graphql/markdown_field/resolver.rb b/lib/gitlab/graphql/markdown_field/resolver.rb
deleted file mode 100644
index 11a01b95ad1..00000000000
--- a/lib/gitlab/graphql/markdown_field/resolver.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module MarkdownField
- class Resolver
- attr_reader :method_name
-
- def initialize(method_name)
- @method_name = method_name
- end
-
- def proc
- -> (object, _args, ctx) do
- # We need to `dup` the context so the MarkdownHelper doesn't modify it
- ::MarkupHelper.markdown_field(object, method_name, ctx.to_h.dup)
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb
index 17cd22d38ad..252f6371765 100644
--- a/lib/gitlab/graphql/pagination/keyset/connection.rb
+++ b/lib/gitlab/graphql/pagination/keyset/connection.rb
@@ -110,8 +110,7 @@ module Gitlab
end
if last
- # grab one more than we need
- paginated_nodes = sliced_nodes.last(limit_value + 1)
+ paginated_nodes = LastItems.take_items(sliced_nodes, limit_value + 1)
# there is an extra node, so there is a previous page
@has_previous_page = paginated_nodes.count > limit_value
diff --git a/lib/gitlab/graphql/pagination/keyset/last_items.rb b/lib/gitlab/graphql/pagination/keyset/last_items.rb
new file mode 100644
index 00000000000..45bf15236c1
--- /dev/null
+++ b/lib/gitlab/graphql/pagination/keyset/last_items.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Pagination
+ module Keyset
+ # This class handles the last(N) ActiveRecord call even if a special ORDER BY configuration is present.
+ # For the last(N) call, ActiveRecord calls reverse_order, however for some cases it raises
+ # ActiveRecord::IrreversibleOrderError error.
+ class LastItems
+ # rubocop: disable CodeReuse/ActiveRecord
+ def self.take_items(scope, count)
+ if custom_order = lookup_custom_reverse_order(scope.order_values)
+ items = scope.reorder(*custom_order).first(count) # returns a single record when count is nil
+ items.is_a?(Array) ? items.reverse : items
+ else
+ scope.last(count)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # Detect special ordering and provide the reversed order
+ def self.lookup_custom_reverse_order(order_values)
+ if ordering_by_merged_at_and_mr_id_desc?(order_values)
+ [
+ Gitlab::Database.nulls_first_order('merge_request_metrics.merged_at', 'ASC'), # reversing the order
+ MergeRequest.arel_table[:id].asc
+ ]
+ elsif ordering_by_merged_at_and_mr_id_asc?(order_values)
+ [
+ Gitlab::Database.nulls_first_order('merge_request_metrics.merged_at', 'DESC'),
+ MergeRequest.arel_table[:id].asc
+ ]
+ end
+ end
+
+ def self.ordering_by_merged_at_and_mr_id_desc?(order_values)
+ order_values.size == 2 &&
+ order_values.first.to_s == Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', 'DESC') &&
+ order_values.last.is_a?(Arel::Nodes::Descending) &&
+ order_values.last.to_sql == MergeRequest.arel_table[:id].desc.to_sql
+ end
+
+ def self.ordering_by_merged_at_and_mr_id_asc?(order_values)
+ order_values.size == 2 &&
+ order_values.first.to_s == Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', 'ASC') &&
+ order_values.last.is_a?(Arel::Nodes::Descending) &&
+ order_values.last.to_sql == MergeRequest.arel_table[:id].desc.to_sql
+ end
+
+ private_class_method :ordering_by_merged_at_and_mr_id_desc?
+ private_class_method :ordering_by_merged_at_and_mr_id_asc?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb
index f54695ddb9a..f3ce3a10703 100644
--- a/lib/gitlab/graphql/pagination/keyset/order_info.rb
+++ b/lib/gitlab/graphql/pagination/keyset/order_info.rb
@@ -94,6 +94,10 @@ module Gitlab
[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]
+ elsif ordering_by_case?(order_value)
+ ['case_order_value', order_value.direction, order_value.expr]
+ elsif ordering_by_array_position?(order_value)
+ ['array_position', order_value.direction, order_value.expr]
else
[order_value.expr.name, order_value.direction, nil]
end
@@ -104,9 +108,19 @@ module Gitlab
order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'lower'
end
+ # determine if ordering using ARRAY_POSITION, eg. "ORDER BY ARRAY_POSITION(Array[4,3,1,2]::smallint, state)"
+ def ordering_by_array_position?(order_value)
+ order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'array_position'
+ end
+
# determine if ordering using SIMILARITY scoring based on Gitlab::Database::SimilarityScore
def ordering_by_similarity?(order_value)
- order_value.to_sql.match?(/SIMILARITY\(.+\*/)
+ Gitlab::Database::SimilarityScore.order_by_similarity?(order_value)
+ end
+
+ # determine if ordering using CASE
+ def ordering_by_case?(order_value)
+ order_value.expr.is_a?(Arel::Nodes::Case)
end
end
end
diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
index 6b6bb72eb31..1285365376f 100644
--- a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
+++ b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
@@ -6,6 +6,8 @@ module Gitlab
class LoggerAnalyzer
COMPLEXITY_ANALYZER = GraphQL::Analysis::QueryComplexity.new { |query, complexity_value| complexity_value }
DEPTH_ANALYZER = GraphQL::Analysis::QueryDepth.new { |query, depth_value| depth_value }
+ FIELD_USAGE_ANALYZER = GraphQL::Analysis::FieldUsage.new { |query, used_fields, used_deprecated_fields| [used_fields, used_deprecated_fields] }
+ ALL_ANALYZERS = [COMPLEXITY_ANALYZER, DEPTH_ANALYZER, FIELD_USAGE_ANALYZER].freeze
def analyze?(query)
Feature.enabled?(:graphql_logging, default_enabled: true)
@@ -23,18 +25,21 @@ module Gitlab
end
def call(memo, visit_type, irep_node)
- memo
+ RequestStore.store[:graphql_logs] = memo
end
def final_value(memo)
return if memo.nil?
- analyzers = [COMPLEXITY_ANALYZER, DEPTH_ANALYZER]
- complexity, depth = GraphQL::Analysis.analyze_query(memo[:query], analyzers)
+ complexity, depth, field_usages = GraphQL::Analysis.analyze_query(memo[:query], ALL_ANALYZERS)
memo[:depth] = depth
memo[:complexity] = complexity
+ # This duration is not the execution time of the
+ # query but the execution time of the analyzer.
memo[:duration_s] = duration(memo[:time_started]).round(1)
+ memo[:used_fields] = field_usages.first
+ memo[:used_deprecated_fields] = field_usages.second
GraphqlLogger.info(memo.except!(:time_started, :query))
rescue => e
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index 0cc3de297ba..5fec50eecd2 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -4,10 +4,10 @@ module Gitlab
class GroupSearchResults < SearchResults
attr_reader :group
- def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, filters: {})
+ def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, sort: nil, filters: {})
@group = group
- super(current_user, query, limit_projects, default_project_filter: default_project_filter, filters: filters)
+ super(current_user, query, limit_projects, default_project_filter: default_project_filter, sort: sort, filters: filters)
end
# rubocop:disable CodeReuse/ActiveRecord
@@ -38,3 +38,5 @@ module Gitlab
end
end
end
+
+Gitlab::GroupSearchResults.prepend_if_ee('EE::Gitlab::GroupSearchResults')
diff --git a/lib/gitlab/health_checks/unicorn_check.rb b/lib/gitlab/health_checks/unicorn_check.rb
index cdc6d2a7519..f0c6fdab600 100644
--- a/lib/gitlab/health_checks/unicorn_check.rb
+++ b/lib/gitlab/health_checks/unicorn_check.rb
@@ -22,7 +22,7 @@ module Gitlab
def check
return unless http_servers
- http_servers.sum(&:worker_processes) # rubocop: disable CodeReuse/ActiveRecord
+ http_servers.sum(&:worker_processes)
end
# Traversal of ObjectSpace is expensive, on fully loaded application
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
index 018cb36fc58..379a734b19c 100644
--- a/lib/gitlab/import_export/attribute_cleaner.rb
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -20,8 +20,8 @@ module Gitlab
/\Aremote_\w+_(url|urls|request_header)\Z/ # carrierwave automatically creates these attribute methods for uploads
).freeze
- def self.clean(*args)
- new(*args).clean
+ def self.clean(*args, **kwargs)
+ new(*args, **kwargs).clean
end
def initialize(relation_hash:, relation_class:, excluded_keys: [])
diff --git a/lib/gitlab/import_export/base/relation_factory.rb b/lib/gitlab/import_export/base/relation_factory.rb
index 05b69362976..d60bc79df4c 100644
--- a/lib/gitlab/import_export/base/relation_factory.rb
+++ b/lib/gitlab/import_export/base/relation_factory.rb
@@ -31,8 +31,8 @@ module Gitlab
TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
- def self.create(*args)
- new(*args).create
+ def self.create(*args, **kwargs)
+ new(*args, **kwargs).create
end
def self.relation_class(relation_name)
@@ -53,6 +53,7 @@ module Gitlab
@importable = importable
@imported_object_retries = 0
@relation_hash[importable_column_name] = @importable.id
+ @original_user = {}
# Remove excluded keys from relation_hash
# We don't do this in the parsed_relation_hash because of the 'transformed attributes'
@@ -112,6 +113,7 @@ module Gitlab
def update_user_references
self.class::USER_REFERENCES.each do |reference|
if @relation_hash[reference]
+ @original_user[reference] = @relation_hash[reference]
@relation_hash[reference] = @members_mapper.map[@relation_hash[reference]]
end
end
@@ -243,28 +245,20 @@ module Gitlab
# will be used. Otherwise, a note stating the original author name
# is left.
def set_note_author
- old_author_id = @relation_hash['author_id']
+ old_author_id = @original_user['author_id']
author = @relation_hash.delete('author')
- update_note_for_missing_author(author['name']) unless has_author?(old_author_id)
- end
-
- def has_author?(old_author_id)
- admin_user? && @members_mapper.include?(old_author_id)
+ unless @members_mapper.include?(old_author_id)
+ @relation_hash['note'] = "%{note}\n\n %{missing_author_note}" % {
+ note: @relation_hash['note'].presence || '*Blank note*',
+ missing_author_note: missing_author_note(@relation_hash['updated_at'], author['name'])
+ }
+ end
end
def missing_author_note(updated_at, author_name)
timestamp = updated_at.split('.').first
- "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*"
- end
-
- def update_note_for_missing_author(author_name)
- @relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank?
- @relation_hash['note'] = "#{@relation_hash['note']}#{missing_author_note(@relation_hash['updated_at'], author_name)}"
- end
-
- def admin_user?
- @user.admin?
+ "*By #{author_name} on #{timestamp} (imported from GitLab)*"
end
def existing_object?
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index 081745a49f4..5a6f6e017d2 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -10,8 +10,8 @@ module Gitlab
MAX_RETRIES = 8
IGNORED_FILENAMES = %w(. ..).freeze
- def self.import(*args)
- new(*args).import
+ def self.import(*args, **kwargs)
+ new(*args, **kwargs).import
end
def initialize(importable:, archive_file:, shared:)
diff --git a/lib/gitlab/import_export/json/ndjson_reader.rb b/lib/gitlab/import_export/json/ndjson_reader.rb
index e9b05afc7d4..0d9839b86cf 100644
--- a/lib/gitlab/import_export/json/ndjson_reader.rb
+++ b/lib/gitlab/import_export/json/ndjson_reader.rb
@@ -35,6 +35,7 @@ module Gitlab
# This reads from `tree/project/merge_requests.ndjson`
path = file_path(importable_path, "#{key}.ndjson")
+
next unless File.exist?(path)
File.foreach(path, MAX_JSON_DOCUMENT_SIZE).with_index do |line, line_num|
@@ -43,6 +44,11 @@ module Gitlab
end
end
+ # TODO: Move clear logic into main comsume_relation method (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465330)
+ def clear_consumed_relations
+ @consumed_relations.clear
+ end
+
private
def json_decode(string)
diff --git a/lib/gitlab/import_export/lfs_saver.rb b/lib/gitlab/import_export/lfs_saver.rb
index 515fd98630c..4964b8b16f4 100644
--- a/lib/gitlab/import_export/lfs_saver.rb
+++ b/lib/gitlab/import_export/lfs_saver.rb
@@ -21,10 +21,10 @@ module Gitlab
save_lfs_object(lfs_object)
end
- append_lfs_json_for_batch(batch) if write_lfs_json_enabled?
+ append_lfs_json_for_batch(batch)
end
- write_lfs_json if write_lfs_json_enabled?
+ write_lfs_json
true
rescue => e
@@ -35,10 +35,6 @@ module Gitlab
private
- def write_lfs_json_enabled?
- ::Feature.enabled?(:export_lfs_objects_projects, default_enabled: true)
- end
-
def save_lfs_object(lfs_object)
if lfs_object.local_store?
copy_file_for_lfs_object(lfs_object)
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index 31d1f7b48bd..6b37683ea68 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -34,8 +34,8 @@ module Gitlab
@user.id
end
- def include?(old_author_id)
- map.has_key?(old_author_id) && map[old_author_id] != default_user_id
+ def include?(old_user_id)
+ map.has_key?(old_user_id)
end
private
@@ -63,6 +63,8 @@ module Gitlab
end
def add_team_member(member, existing_user = nil)
+ return true if existing_user && @importable.members.exists?(user_id: existing_user.id)
+
member['user'] = existing_user
member_hash = member_hash(member)
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 9ec5df8cde9..a0526ba0414 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -403,9 +403,15 @@ ee:
- issues:
- epic_issue:
- :epic
+ - :issuable_sla
- protected_branches:
- :unprotect_access_levels
- protected_environments:
- :deploy_access_levels
- :service_desk_setting
- :security_setting
+
+ included_attributes:
+ issuable_sla:
+ - :issue
+ - :due_at
diff --git a/lib/gitlab/import_export/project/sample/date_calculator.rb b/lib/gitlab/import_export/project/sample/date_calculator.rb
new file mode 100644
index 00000000000..2d989d21166
--- /dev/null
+++ b/lib/gitlab/import_export/project/sample/date_calculator.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ module Project
+ module Sample
+ class DateCalculator
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(dates)
+ @dates = dates.dup
+ @dates.flatten!
+ @dates.compact!
+ @dates.sort!
+ @dates.map! { |date| date.to_time.to_f }
+ end
+
+ def closest_date_to_average
+ strong_memoize(:closest_date_to_average) do
+ next if @dates.empty?
+
+ average_date = (@dates.first + @dates.last) / 2.0
+ closest_date = @dates.min_by { |date| (date - average_date).abs }
+ Time.zone.at(closest_date)
+ end
+ end
+
+ def calculate_by_closest_date_to_average(date)
+ return date unless closest_date_to_average && closest_date_to_average < Time.current
+
+ date + (Time.current - closest_date_to_average).seconds
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb b/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb
new file mode 100644
index 00000000000..b0c3940b5f9
--- /dev/null
+++ b/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ module Project
+ module Sample
+ class SampleDataRelationTreeRestorer < RelationTreeRestorer
+ DATE_MODELS = %i[issues milestones].freeze
+
+ def initialize(*args)
+ super
+
+ date_calculator
+ end
+
+ private
+
+ def build_relation(relation_key, relation_definition, data_hash)
+ # Override due date attributes in data hash for Sample Data templates
+ # Dates are moved by taking the closest one to average and moving that (and rest around it) to the date of import
+ # TODO: To move this logic to RelationFactory (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465333)
+ override_date_attributes!(relation_key, data_hash)
+ super
+ end
+
+ def override_date_attributes!(relation_key, data_hash)
+ return unless DATE_MODELS.include?(relation_key.to_sym)
+
+ data_hash['start_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['start_date'].to_time) unless data_hash['start_date'].nil?
+ data_hash['due_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['due_date'].to_time) unless data_hash['due_date'].nil?
+ end
+
+ # TODO: Move clear logic into main comsume_relation method (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465330)
+ def dates
+ unless relation_reader.legacy?
+ DATE_MODELS.map do |tag|
+ relation_reader.consume_relation(@importable_path, tag).map { |model| model.first['due_date'] }.tap do
+ relation_reader.clear_consumed_relations
+ end
+ end
+ end
+ end
+
+ def date_calculator
+ @date_calculator ||= Gitlab::ImportExport::Project::Sample::DateCalculator.new(dates)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project/tree_restorer.rb b/lib/gitlab/import_export/project/tree_restorer.rb
index a16ffe36054..b1d647281ab 100644
--- a/lib/gitlab/import_export/project/tree_restorer.rb
+++ b/lib/gitlab/import_export/project/tree_restorer.rb
@@ -70,7 +70,7 @@ module Gitlab
end
def relation_tree_restorer
- @relation_tree_restorer ||= RelationTreeRestorer.new(
+ @relation_tree_restorer ||= relation_tree_restorer_class.new(
user: @user,
shared: @shared,
relation_reader: relation_reader,
@@ -84,6 +84,14 @@ module Gitlab
)
end
+ def relation_tree_restorer_class
+ sample_data_template? ? Sample::SampleDataRelationTreeRestorer : RelationTreeRestorer
+ end
+
+ def sample_data_template?
+ @project&.import_data&.data&.dig('sample_data')
+ end
+
def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
user: @user,
diff --git a/lib/gitlab/import_export/project/tree_saver.rb b/lib/gitlab/import_export/project/tree_saver.rb
index 7cca3596da6..80dacf2eb20 100644
--- a/lib/gitlab/import_export/project/tree_saver.rb
+++ b/lib/gitlab/import_export/project/tree_saver.rb
@@ -36,7 +36,7 @@ module Gitlab
end
def exportable
- @project.present(exportable_params)
+ @project.present(**exportable_params)
end
def exportable_params
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 9e10e7aea13..f808e30bd6e 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -14,10 +14,10 @@ module Gitlab
def restore
return true unless File.exist?(path_to_bundle)
+ ensure_repository_does_not_exist!
+
repository.create_from_bundle(path_to_bundle)
rescue => e
- Repositories::DestroyService.new(repository).execute
-
shared.error(e)
false
end
@@ -25,6 +25,16 @@ module Gitlab
private
attr_accessor :repository, :path_to_bundle, :shared
+
+ def ensure_repository_does_not_exist!
+ if repository.exists?
+ shared.logger.info(
+ message: %Q{Deleting existing "#{repository.path}" to re-import it.}
+ )
+
+ Repositories::DestroyService.new(repository).execute
+ end
+ end
end
end
end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index e4724659eff..045ba2495bf 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -5,8 +5,8 @@ module Gitlab
class Saver
include Gitlab::ImportExport::CommandLineUtil
- def self.save(*args)
- new(*args).save
+ def self.save(*args, **kwargs)
+ new(*args, **kwargs).save
end
def initialize(exportable:, shared:)
diff --git a/lib/gitlab/import_export/uploads_manager.rb b/lib/gitlab/import_export/uploads_manager.rb
index dca8e3a7449..26e7d2cf765 100644
--- a/lib/gitlab/import_export/uploads_manager.rb
+++ b/lib/gitlab/import_export/uploads_manager.rb
@@ -40,7 +40,7 @@ module Gitlab
def add_upload(upload)
uploader_context = FileUploader.extract_dynamic_path(upload).named_captures.symbolize_keys
- UploadService.new(@project, File.open(upload, 'r'), FileUploader, uploader_context).execute.to_h
+ UploadService.new(@project, File.open(upload, 'r'), FileUploader, **uploader_context).execute.to_h
end
def copy_project_uploads
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index 4154d4fe775..48f5b558e52 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -3,8 +3,8 @@
module Gitlab
module ImportExport
class VersionChecker
- def self.check!(*args)
- new(*args).check!
+ def self.check!(*args, **kwargs)
+ new(*args, **kwargs).check!
end
def initialize(shared:)
diff --git a/lib/gitlab/instrumentation/redis.rb b/lib/gitlab/instrumentation/redis.rb
index 4a85a313fd7..d1ac6a55fb7 100644
--- a/lib/gitlab/instrumentation/redis.rb
+++ b/lib/gitlab/instrumentation/redis.rb
@@ -37,7 +37,7 @@ module Gitlab
%i[get_request_count query_time read_bytes write_bytes].each do |method|
define_method method do
- STORAGES.sum(&method) # rubocop:disable CodeReuse/ActiveRecord
+ STORAGES.sum(&method)
end
end
end
diff --git a/lib/gitlab/issuables_count_for_state.rb b/lib/gitlab/issuables_count_for_state.rb
index 7be54a214dd..945ab7f40c2 100644
--- a/lib/gitlab/issuables_count_for_state.rb
+++ b/lib/gitlab/issuables_count_for_state.rb
@@ -16,9 +16,12 @@ module Gitlab
end
# finder - The finder class to use for retrieving the issuables.
- def initialize(finder, project = nil)
+ # fast_fail - restrict counting to a shorter period, degrading gracefully on
+ # failure
+ def initialize(finder, project = nil, fast_fail: false)
@finder = finder
@project = project
+ @fast_fail = fast_fail
@cache = Gitlab::SafeRequestStore[CACHE_KEY] ||= initialize_cache
end
@@ -26,6 +29,10 @@ module Gitlab
self[state || :opened]
end
+ def fast_fail?
+ !!@fast_fail
+ end
+
# Define method for each state
STATES.each do |state|
define_method(state) { self[state] }
@@ -53,7 +60,53 @@ module Gitlab
end
def initialize_cache
- Hash.new { |hash, finder| hash[finder] = finder.count_by_state }
+ Hash.new { |hash, finder| hash[finder] = perform_count(finder) }
+ end
+
+ def perform_count(finder)
+ return finder.count_by_state unless fast_fail?
+
+ fast_count_by_state_attempt!
+
+ # Determining counts when referring to issuable titles or descriptions can
+ # be very expensive, and involve the database reading gigabytes of data
+ # for a relatively minor piece of functionality. This may slow index pages
+ # by seconds in the best case, or lead to a statement timeout in the worst
+ # case.
+ #
+ # In time, we may be able to use elasticsearch or postgresql tsv columns
+ # to perform the calculation more efficiently. Until then, use a shorter
+ # timeout and return -1 as a sentinel value if it is triggered
+ begin
+ ApplicationRecord.with_fast_statement_timeout do
+ finder.count_by_state
+ end
+ rescue ActiveRecord::QueryCanceled => err
+ fast_count_by_state_failure!
+
+ Gitlab::ErrorTracking.track_exception(
+ err,
+ params: finder.params,
+ current_user_id: finder.current_user&.id,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/249180'
+ )
+
+ Hash.new(-1)
+ end
+ end
+
+ def fast_count_by_state_attempt!
+ Gitlab::Metrics.counter(
+ :gitlab_issuable_fast_count_by_state_total,
+ "Count of total calls to IssuableFinder#count_by_state with fast failure"
+ ).increment
+ end
+
+ def fast_count_by_state_failure!
+ Gitlab::Metrics.counter(
+ :gitlab_issuable_fast_count_by_state_failures_total,
+ "Count of failed calls to IssuableFinder#count_by_state with fast failure"
+ ).increment
end
end
end
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index e7a8cc6305a..2cede524cac 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -23,7 +23,15 @@ module Gitlab
TIMEOUTS_METRIC = :gitlab_job_waiter_timeouts_total
def self.notify(key, jid)
- Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) }
+ Gitlab::Redis::SharedState.with do |redis|
+ # Use a Redis MULTI transaction to ensure we always set an expiry
+ redis.multi do |multi|
+ multi.lpush(key, jid)
+ # This TTL needs to be long enough to allow whichever Sidekiq job calls
+ # JobWaiter#wait to reach BLPOP.
+ multi.expire(key, 6.hours.to_i)
+ end
+ end
end
def self.key?(key)
@@ -52,10 +60,6 @@ module Gitlab
increment_counter(STARTED_METRIC)
Gitlab::Redis::SharedState.with do |redis|
- # Fallback key expiry: allow a long grace period to reduce the chance of
- # a job pushing to an expired key and recreating it
- redis.expire(key, [timeout * 2, 10.minutes.to_i].max)
-
while jobs_remaining > 0
# Redis will not take fractional seconds. Prefer waiting too long over
# not waiting long enough
@@ -75,9 +79,6 @@ module Gitlab
@finished << jid
@jobs_remaining -= 1
end
-
- # All jobs have finished, so expire the key immediately
- redis.expire(key, 0) if jobs_remaining == 0
end
finished
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index fa68afd39f5..13cd6dcad3f 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -167,6 +167,21 @@ module Gitlab
end
end
+ # Ingresses resource is currently on the apis/extensions api group
+ # until Kubernetes 1.21. Kubernetest 1.22+ has ingresses resources in
+ # the networking.k8s.io/v1 api group.
+ #
+ # As we still support Kubernetes 1.12+, we will need to support both.
+ def get_ingresses(**args)
+ extensions_client.discover unless extensions_client.discovered
+
+ if extensions_client.respond_to?(:get_ingresses)
+ extensions_client.get_ingresses(**args)
+ else
+ networking_client.get_ingresses(**args)
+ end
+ end
+
def create_or_update_cluster_role_binding(resource)
update_cluster_role_binding(resource)
end
diff --git a/lib/gitlab/kubernetes/pod.rb b/lib/gitlab/kubernetes/pod.rb
index d247662dc3b..a5651f2f184 100644
--- a/lib/gitlab/kubernetes/pod.rb
+++ b/lib/gitlab/kubernetes/pod.rb
@@ -2,13 +2,47 @@
module Gitlab
module Kubernetes
- module Pod
+ class Pod
PENDING = 'Pending'
RUNNING = 'Running'
SUCCEEDED = 'Succeeded'
FAILED = 'Failed'
UNKNOWN = 'Unknown'
PHASES = [PENDING, RUNNING, SUCCEEDED, FAILED, UNKNOWN].freeze
+
+ STABLE_TRACK_VALUE = 'stable'
+
+ def initialize(attributes = {})
+ @attributes = attributes
+ end
+
+ def track
+ attributes.dig('metadata', 'labels', 'track') || STABLE_TRACK_VALUE
+ end
+
+ def name
+ metadata['name'] || metadata['generateName']
+ end
+
+ def stable?
+ track == STABLE_TRACK_VALUE
+ end
+
+ def status
+ attributes.dig('status', 'phase')
+ end
+
+ def order
+ stable? ? 1 : 0
+ end
+
+ private
+
+ attr_reader :attributes
+
+ def metadata
+ attributes.fetch('metadata', {})
+ end
end
end
end
diff --git a/lib/gitlab/lfs/client.rb b/lib/gitlab/lfs/client.rb
index e4d600694c2..825d7399190 100644
--- a/lib/gitlab/lfs/client.rb
+++ b/lib/gitlab/lfs/client.rb
@@ -6,6 +6,14 @@ module Gitlab
# * 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
+ GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs+json'
+ GIT_LFS_USER_AGENT = "GitLab #{Gitlab::VERSION} LFS client"
+ DEFAULT_HEADERS = {
+ 'Accept' => GIT_LFS_CONTENT_TYPE,
+ 'Content-Type' => GIT_LFS_CONTENT_TYPE,
+ 'User-Agent' => GIT_LFS_USER_AGENT
+ }.freeze
+
attr_reader :base_url
def initialize(base_url, credentials:)
@@ -13,19 +21,19 @@ module Gitlab
@credentials = credentials
end
- def batch(operation, objects)
+ 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 } }
+ objects: objects.as_json(only: [:oid, :size])
}
rsp = Gitlab::HTTP.post(
batch_url,
basic_auth: basic_auth,
body: body.to_json,
- headers: { 'Content-Type' => 'application/vnd.git-lfs+json' }
+ headers: build_request_headers
)
raise BatchSubmitError unless rsp.success?
@@ -40,17 +48,19 @@ module Gitlab
body
end
- def upload(object, upload_action, authenticated:)
+ 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'
+ 'Content-Type' => 'application/octet-stream',
+ 'User-Agent' => GIT_LFS_USER_AGENT
}.merge(upload_action['header'] || {})
}
+ authenticated = true if params[:headers].key?('Authorization')
params[:basic_auth] = basic_auth unless authenticated
rsp = Gitlab::HTTP.put(upload_action['href'], params)
@@ -60,8 +70,26 @@ module Gitlab
file&.close
end
+ def verify!(object, verify_action, authenticated:)
+ params = {
+ body: object.to_json(only: [:oid, :size]),
+ headers: build_request_headers(verify_action['header'])
+ }
+
+ authenticated = true if params[:headers].key?('Authorization')
+ params[:basic_auth] = basic_auth unless authenticated
+
+ rsp = Gitlab::HTTP.post(verify_action['href'], params)
+
+ raise ObjectVerifyError unless rsp.success?
+ end
+
private
+ def build_request_headers(extra_headers = nil)
+ DEFAULT_HEADERS.merge(extra_headers || {})
+ end
+
attr_reader :credentials
def batch_url
@@ -96,6 +124,12 @@ module Gitlab
"Failed to upload object"
end
end
+
+ class ObjectVerifyError < StandardError
+ def message
+ "Failed to verify object"
+ end
+ end
end
end
end
diff --git a/lib/gitlab/manifest_import/metadata.rb b/lib/gitlab/manifest_import/metadata.rb
new file mode 100644
index 00000000000..80dff075391
--- /dev/null
+++ b/lib/gitlab/manifest_import/metadata.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ManifestImport
+ class Metadata
+ EXPIRY_TIME = 1.week
+
+ attr_reader :user, :fallback
+
+ def initialize(user, fallback: {})
+ @user = user
+ @fallback = fallback
+ end
+
+ def save(repositories, group_id)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.multi do
+ redis.set(key_for('repositories'), Gitlab::Json.dump(repositories), ex: EXPIRY_TIME)
+ redis.set(key_for('group_id'), group_id, ex: EXPIRY_TIME)
+ end
+ end
+ end
+
+ def repositories
+ redis_get('repositories').then do |repositories|
+ next unless repositories
+
+ Gitlab::Json.parse(repositories).map(&:symbolize_keys)
+ end || fallback[:manifest_import_repositories]
+ end
+
+ def group_id
+ redis_get('group_id')&.to_i || fallback[:manifest_import_group_id]
+ end
+
+ private
+
+ def key_for(field)
+ "manifest_import:metadata:user:#{user.id}:#{field}"
+ end
+
+ def redis_get(field)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(key_for(field))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/marginalia.rb b/lib/gitlab/marginalia.rb
index 24e21a1d512..325a8c5c325 100644
--- a/lib/gitlab/marginalia.rb
+++ b/lib/gitlab/marginalia.rb
@@ -4,8 +4,6 @@ module Gitlab
module Marginalia
cattr_accessor :enabled, default: false
- MARGINALIA_FEATURE_FLAG = :marginalia
-
def self.set_application_name
::Marginalia.application_name = Gitlab.process_name
end
@@ -16,15 +14,11 @@ module Gitlab
end
end
- def self.cached_feature_enabled?
- enabled
- end
-
- def self.set_feature_cache
+ def self.set_enabled_from_feature_flag
# During db:create and db:bootstrap skip feature query as DB is not available yet.
return false unless Gitlab::Database.cached_table_exists?('features')
- self.enabled = Feature.enabled?(MARGINALIA_FEATURE_FLAG)
+ self.enabled = Feature.enabled?(:marginalia, type: :ops)
end
end
end
diff --git a/lib/gitlab/marginalia/active_record_instrumentation.rb b/lib/gitlab/marginalia/active_record_instrumentation.rb
index 3266b9f8336..452f472bf6a 100644
--- a/lib/gitlab/marginalia/active_record_instrumentation.rb
+++ b/lib/gitlab/marginalia/active_record_instrumentation.rb
@@ -5,7 +5,7 @@ module Gitlab
module Marginalia
module ActiveRecordInstrumentation
def annotate_sql(sql)
- Gitlab::Marginalia.cached_feature_enabled? ? super(sql) : sql
+ Gitlab::Marginalia.enabled ? super(sql) : sql
end
end
end
diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb
index da3b597a74e..36e9a6ccef6 100644
--- a/lib/gitlab/markdown_cache.rb
+++ b/lib/gitlab/markdown_cache.rb
@@ -3,7 +3,7 @@
module Gitlab
module MarkdownCache
# Increment this number every time the renderer changes its output
- CACHE_COMMONMARK_VERSION = 25
+ CACHE_COMMONMARK_VERSION = 26
CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError)
diff --git a/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb b/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb
index d1490d5d9b6..8a176be30a2 100644
--- a/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb
+++ b/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb
@@ -13,11 +13,12 @@ module Gitlab
@dashboard_hash = dashboard_hash
@project = project
@dashboard_path = dashboard_path
+ @affected_environment_ids = []
end
def execute
import
- rescue ActiveRecord::RecordInvalid, ::Gitlab::Metrics::Dashboard::Transformers::TransformerError
+ rescue ActiveRecord::RecordInvalid, Dashboard::Transformers::Errors::BaseError
false
end
@@ -32,28 +33,51 @@ module Gitlab
def import
delete_stale_metrics
create_or_update_metrics
+ update_prometheus_environments
end
# rubocop: disable CodeReuse/ActiveRecord
def create_or_update_metrics
# TODO: use upsert and worker for callbacks?
+
+ affected_metric_ids = []
prometheus_metrics_attributes.each do |attributes|
- prometheus_metric = PrometheusMetric.find_or_initialize_by(attributes.slice(:identifier, :project))
+ prometheus_metric = PrometheusMetric.find_or_initialize_by(attributes.slice(:dashboard_path, :identifier, :project))
prometheus_metric.update!(attributes.slice(*ALLOWED_ATTRIBUTES))
+
+ affected_metric_ids << prometheus_metric.id
end
+
+ @affected_environment_ids += find_alerts(affected_metric_ids).get_environment_id
end
# rubocop: enable CodeReuse/ActiveRecord
def delete_stale_metrics
- identifiers = prometheus_metrics_attributes.map { |metric_attributes| metric_attributes[:identifier] }
+ identifiers_from_yml = 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)
+ .not_identifier(identifiers_from_yml)
+
+ return unless stale_metrics.exists?
+
+ delete_stale_alerts(stale_metrics)
+ stale_metrics.each_batch { |batch| batch.delete_all }
+ end
+
+ def delete_stale_alerts(stale_metrics)
+ stale_alerts = find_alerts(stale_metrics)
+
+ affected_environment_ids = stale_alerts.get_environment_id
+ return unless affected_environment_ids.present?
- # TODO: use destroy_all and worker for callbacks?
- stale_metrics.each(&:destroy)
+ @affected_environment_ids += affected_environment_ids
+ stale_alerts.each_batch { |batch| batch.delete_all }
+ end
+
+ def find_alerts(metrics)
+ Projects::Prometheus::AlertsFinder.new(project: project, metric: metrics).execute
end
def prometheus_metrics_attributes
@@ -65,6 +89,19 @@ module Gitlab
).execute
end
end
+
+ def update_prometheus_environments
+ affected_environments = ::Environment.for_id(@affected_environment_ids.flatten.uniq).for_project(project)
+
+ return unless affected_environments.exists?
+
+ affected_environments.each do |affected_environment|
+ ::Clusters::Applications::ScheduleUpdateService.new(
+ affected_environment.cluster_prometheus_adapter,
+ project
+ ).execute
+ end
+ end
end
end
end
diff --git a/lib/gitlab/metrics/dashboard/stages/custom_dashboard_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/custom_dashboard_metrics_inserter.rb
new file mode 100644
index 00000000000..5ed4466f440
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/custom_dashboard_metrics_inserter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ # Acts on metrics which have been ingested from source controlled dashboards
+ class CustomDashboardMetricsInserter < BaseStage
+ # For each metric in the dashboard config, attempts to
+ # find a corresponding database record. If found, includes
+ # the record's id in the dashboard config.
+ def transform!
+ database_metrics = ::PrometheusMetricsFinder.new(common: false, group: :custom, project: project).execute
+
+ for_metrics do |metric|
+ metric_record = database_metrics.find { |m| m.identifier == metric[:id] }
+ metric[:metric_id] = metric_record.id if metric_record
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/url_validator.rb b/lib/gitlab/metrics/dashboard/stages/url_validator.rb
index 9e2bb0d1a70..ad9d78133af 100644
--- a/lib/gitlab/metrics/dashboard/stages/url_validator.rb
+++ b/lib/gitlab/metrics/dashboard/stages/url_validator.rb
@@ -46,7 +46,7 @@ module Gitlab
links&.each do |link|
next unless link.is_a? Hash
- Gitlab::UrlBlocker.validate!(link[:url], blocker_args)
+ Gitlab::UrlBlocker.validate!(link[:url], **blocker_args)
rescue Gitlab::UrlBlocker::BlockedUrlError
link[:url] = ''
end
diff --git a/lib/gitlab/metrics/dashboard/transformers/errors.rb b/lib/gitlab/metrics/dashboard/transformers/errors.rb
index 4d94ab098ae..bc85dc4e131 100644
--- a/lib/gitlab/metrics/dashboard/transformers/errors.rb
+++ b/lib/gitlab/metrics/dashboard/transformers/errors.rb
@@ -4,10 +4,10 @@ module Gitlab
module Metrics
module Dashboard
module Transformers
- TransformerError = Class.new(StandardError)
-
module Errors
- class MissingAttribute < TransformerError
+ BaseError = Class.new(StandardError)
+
+ class MissingAttribute < BaseError
def initialize(attribute_name)
super("Missing attribute: '#{attribute_name}'")
end
diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb
index 15db3999fa4..f6bda0dbea4 100644
--- a/lib/gitlab/metrics/requests_rack_middleware.rb
+++ b/lib/gitlab/metrics/requests_rack_middleware.rb
@@ -3,20 +3,13 @@
module Gitlab
module Metrics
class RequestsRackMiddleware
- HTTP_METHODS = {
- "delete" => %w(200 202 204 303 400 401 403 404 410 422 500 503),
- "get" => %w(200 204 301 302 303 304 307 400 401 403 404 410 412 422 429 500 503),
- "head" => %w(200 204 301 302 303 304 400 401 403 404 410 429 500 503),
- "options" => %w(200 404),
- "patch" => %w(200 202 204 400 403 404 409 416 422 500),
- "post" => %w(200 201 202 204 301 302 303 304 400 401 403 404 406 409 410 412 413 415 422 429 500 503),
- "propfind" => %w(404),
- "put" => %w(200 202 204 400 401 403 404 405 406 409 410 415 422 500),
- "report" => %w(404)
- }.freeze
+ HTTP_METHODS = %w(delete get head options patch post put).to_set.freeze
HEALTH_ENDPOINT = /^\/-\/(liveness|readiness|health|metrics)\/?$/.freeze
+ FEATURE_CATEGORY_HEADER = 'X-Gitlab-Feature-Category'
+ FEATURE_CATEGORY_DEFAULT = 'unknown'
+
def initialize(app)
@app = app
end
@@ -39,33 +32,39 @@ module Gitlab
end
def self.initialize_http_request_duration_seconds
- HTTP_METHODS.each do |method, statuses|
- statuses.each do |status|
- http_request_duration_seconds.get({ method: method, status: status.to_s })
- end
+ HTTP_METHODS.each do |method|
+ http_request_duration_seconds.get({ method: method })
end
end
def call(env)
method = env['REQUEST_METHOD'].downcase
+ method = 'INVALID' unless HTTP_METHODS.include?(method)
started = Time.now.to_f
+ health_endpoint = health_endpoint?(env['PATH_INFO'])
+ status = 'undefined'
+ feature_category = nil
begin
- if health_endpoint?(env['PATH_INFO'])
- RequestsRackMiddleware.http_health_requests_total.increment(method: method)
- else
- RequestsRackMiddleware.http_request_total.increment(method: method)
- end
-
status, headers, body = @app.call(env)
elapsed = Time.now.to_f - started
- RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method, status: status.to_s }, elapsed)
+ feature_category = headers&.fetch(FEATURE_CATEGORY_HEADER, nil)
+
+ unless health_endpoint
+ RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method }, elapsed)
+ end
[status, headers, body]
rescue
RequestsRackMiddleware.rack_uncaught_errors_count.increment
raise
+ ensure
+ if health_endpoint
+ RequestsRackMiddleware.http_health_requests_total.increment(status: status, method: method)
+ else
+ RequestsRackMiddleware.http_request_total.increment(status: status, method: method, feature_category: feature_category || FEATURE_CATEGORY_DEFAULT)
+ end
end
end
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index 8c4d150adad..d7935d65e12 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -54,7 +54,7 @@ module Gitlab
end
def unicorn_workers_count
- http_servers.sum(&:worker_processes) # rubocop: disable CodeReuse/ActiveRecord
+ http_servers.sum(&:worker_processes)
end
# Traversal of ObjectSpace is expensive, on fully loaded application
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index e53ac00e77f..f9ba0a69b0e 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -61,7 +61,7 @@ module Gitlab
end
def current_transaction
- Transaction.current
+ ::Gitlab::Metrics::Transaction.current
end
end
end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 47d0b9ba8cb..4b65bbcc791 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -18,6 +18,15 @@ module Gitlab
request = ActionDispatch::Request.new(env)
render_go_doc(request) || @app.call(env)
+ rescue Gitlab::Auth::IpBlacklisted
+ Gitlab::AuthLogger.error(
+ message: 'Rack_Attack',
+ env: :blocklist,
+ remote_ip: request.ip,
+ request_method: request.request_method,
+ path: request.fullpath
+ )
+ Rack::Response.new('', 403).finish
end
private
diff --git a/lib/gitlab/middleware/handle_null_bytes.rb b/lib/gitlab/middleware/handle_null_bytes.rb
new file mode 100644
index 00000000000..c88dfb6ee0b
--- /dev/null
+++ b/lib/gitlab/middleware/handle_null_bytes.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Middleware
+ # There is no valid reason for a request to contain a null byte (U+0000)
+ # so just return HTTP 400 (Bad Request) if we receive one
+ class HandleNullBytes
+ NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze
+
+ attr_reader :app
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ return [400, {}, ["Bad Request"]] if request_has_null_byte?(env)
+
+ app.call(env)
+ end
+
+ private
+
+ def request_has_null_byte?(request)
+ return false if ENV['REJECT_NULL_BYTES'] == "1"
+
+ request = Rack::Request.new(request)
+
+ request.params.values.any? do |value|
+ param_has_null_byte?(value)
+ end
+ end
+
+ def param_has_null_byte?(value, depth = 0)
+ # Guard against possible attack sending large amounts of nested params
+ # Should be safe as deeply nested params are highly uncommon.
+ return false if depth > 2
+
+ depth += 1
+
+ if value.respond_to?(:match)
+ string_contains_null_byte?(value)
+ elsif value.respond_to?(:values)
+ value.values.any? do |hash_value|
+ param_has_null_byte?(hash_value, depth)
+ end
+ elsif value.is_a?(Array)
+ value.any? do |array_value|
+ param_has_null_byte?(array_value, depth)
+ end
+ else
+ false
+ end
+ end
+
+ def string_contains_null_byte?(string)
+ string.match?(NULL_BYTE_REGEX)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 8e6ac7610f2..7e98f1fc1f7 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -137,6 +137,7 @@ module Gitlab
# TODO this class is meant to replace Handler when the feature flag
# upload_middleware_jwt_params_handler is removed
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/233895#roll-out-steps
class HandlerForJWTParams < Handler
def with_open_files
@rewritten_fields.keys.each do |field|
@@ -228,7 +229,7 @@ module Gitlab
private
def handler_class
- if Feature.enabled?(:upload_middleware_jwt_params_handler)
+ if Feature.enabled?(:upload_middleware_jwt_params_handler, default_enabled: true)
::Gitlab::Middleware::Multipart::HandlerForJWTParams
else
::Gitlab::Middleware::Multipart::Handler
diff --git a/lib/gitlab/pagination/offset_pagination.rb b/lib/gitlab/pagination/offset_pagination.rb
index 8796dd4d7ec..46c74b8fe3c 100644
--- a/lib/gitlab/pagination/offset_pagination.rb
+++ b/lib/gitlab/pagination/offset_pagination.rb
@@ -10,9 +10,9 @@ module Gitlab
@request_context = request_context
end
- def paginate(relation)
+ def paginate(relation, exclude_total_headers: false)
paginate_with_limit_optimization(add_default_order(relation)).tap do |data|
- add_pagination_headers(data)
+ add_pagination_headers(data, exclude_total_headers)
end
end
@@ -27,7 +27,7 @@ module Gitlab
end
return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation)
- return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit)
+ return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit, type: :ops)
limited_total_count = pagination_data.total_count_with_limit
if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT
@@ -47,14 +47,14 @@ module Gitlab
relation
end
- def add_pagination_headers(paginated_data)
+ def add_pagination_headers(paginated_data, exclude_total_headers)
header 'X-Per-Page', paginated_data.limit_value.to_s
header 'X-Page', paginated_data.current_page.to_s
header 'X-Next-Page', paginated_data.next_page.to_s
header 'X-Prev-Page', paginated_data.prev_page.to_s
header 'Link', pagination_links(paginated_data)
- return if data_without_counts?(paginated_data)
+ return if exclude_total_headers || data_without_counts?(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', total_pages(paginated_data).to_s
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index ded8d4ade3f..333564bee01 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, query, project:, repository_ref: nil, filters: {})
+ def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {})
@project = project
@repository_ref = repository_ref.presence
- super(current_user, query, [project], filters: filters)
+ super(current_user, query, [project], sort: sort, filters: filters)
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index e6e599e079d..a830f949b21 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -36,35 +36,37 @@ module Gitlab
name == other.name && title == other.title
end
- def self.localized_templates_table
- [
- ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'),
- ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'),
- ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'),
- ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'),
- ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'),
- ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'),
- ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'),
- ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'),
- ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'),
- ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'),
- ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'),
- ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'),
- ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'),
- ProjectTemplate.new('sse_middleman', 'Static Site Editor/Middleman', _('Middleman project with Static Site Editor support'), 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman'),
- ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
- ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'),
- ProjectTemplate.new('jsonnet', 'Jsonnet for Dynamic Child Pipelines', _('An example showing how to use Jsonnet with GitLab dynamic child pipelines'), 'https://gitlab.com/gitlab-org/project-templates/jsonnet'),
- ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management')
- ].freeze
- end
-
class << self
+ # TODO: Review child inheritance of this table (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430928221)
+ def localized_templates_table
+ [
+ ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'),
+ ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'),
+ ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'),
+ ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'),
+ ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'),
+ ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'),
+ ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'),
+ ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'),
+ ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'),
+ ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'),
+ ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'),
+ ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'),
+ ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'),
+ ProjectTemplate.new('sse_middleman', 'Static Site Editor/Middleman', _('Middleman project with Static Site Editor support'), 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman', 'illustrations/logos/middleman.svg'),
+ ProjectTemplate.new('gitpod_spring_petclinic', 'Gitpod/Spring Petclinic', _('A Gitpod configured Webapplication in Spring and Java'), 'https://gitlab.com/gitlab-org/project-templates/gitpod-spring-petclinic', 'illustrations/logos/gitpod.svg'),
+ ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
+ ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'),
+ ProjectTemplate.new('jsonnet', 'Jsonnet for Dynamic Child Pipelines', _('An example showing how to use Jsonnet with GitLab dynamic child pipelines'), 'https://gitlab.com/gitlab-org/project-templates/jsonnet'),
+ ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management')
+ ].freeze
+ end
+
def all
localized_templates_table
end
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index 6aa3f515ef0..efe07aa8ab2 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -145,9 +145,9 @@ module Gitlab
run_label_command(labels: find_labels(labels_param), command: :relabel, updates_key: :label_ids)
end
- desc _('Add a To Do')
- explanation _('Adds a To Do.')
- execution_message _('Added a To Do.')
+ desc _('Add a to do')
+ explanation _('Adds a to do.')
+ execution_message _('Added a to do.')
types Issuable
condition do
quick_action_target.persisted? &&
@@ -157,9 +157,9 @@ module Gitlab
@updates[:todo_event] = 'add'
end
- desc _('Mark To Do as done')
- explanation _('Marks To Do as done.')
- execution_message _('Marked To Do as done.')
+ desc _('Mark to do as done')
+ explanation _('Marks to do as done.')
+ execution_message _('Marked to do as done.')
types Issuable
condition do
quick_action_target.persisted? &&
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index 98db8ff761e..c8c949a9363 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -121,6 +121,20 @@ module Gitlab
result[:message]
end
end
+
+ desc _('Approve a merge request')
+ explanation _('Approve the current merge request.')
+ types MergeRequest
+ condition do
+ quick_action_target.persisted? && quick_action_target.can_be_approved_by?(current_user)
+ end
+ command :approve do
+ success = MergeRequests::ApprovalService.new(quick_action_target.project, current_user).execute(quick_action_target)
+
+ next unless success
+
+ @execution_message[:approve] = _('Approved the current merge request.')
+ end
end
def merge_orchestration_service
diff --git a/lib/gitlab/redis/hll.rb b/lib/gitlab/redis/hll.rb
index ff5754675e2..010a6b59da5 100644
--- a/lib/gitlab/redis/hll.rb
+++ b/lib/gitlab/redis/hll.rb
@@ -3,15 +3,16 @@
module Gitlab
module Redis
class HLL
+ BATCH_SIZE = 300
KEY_REGEX = %r{\A(\w|-|:)*\{\w*\}(\w|-|:)*\z}.freeze
KeyFormatError = Class.new(StandardError)
def self.count(params)
- self.new.count(params)
+ self.new.count(**params)
end
def self.add(params)
- self.new.add(params)
+ self.new.add(**params)
end
def count(keys:)
@@ -29,17 +30,24 @@ module Gitlab
# 2020-216-{project_action}
# i_{analytics}_dev_ops_score-2020-32
def add(key:, value:, expiry:)
- unless KEY_REGEX.match?(key)
- raise KeyFormatError.new("Invalid key format. #{key} key should have changeable parts in curly braces. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands")
- end
+ validate_key!(key)
Gitlab::Redis::SharedState.with do |redis|
redis.multi do |multi|
- multi.pfadd(key, value)
+ Array.wrap(value).each_slice(BATCH_SIZE) { |batch| multi.pfadd(key, batch) }
+
multi.expire(key, expiry)
end
end
end
+
+ private
+
+ def validate_key!(key)
+ return if KEY_REGEX.match?(key)
+
+ raise KeyFormatError.new("Invalid key format. #{key} key should have changeable parts in curly braces. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands")
+ end
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 8e23ac6aca5..693a10a9de3 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -46,6 +46,12 @@ module Gitlab
maven_app_name_regex
end
+ def nuget_version_regex
+ @nuget_version_regex ||= /
+ \A#{_semver_major_minor_patch_regex}(\.\d*)?#{_semver_prerelease_build_regex}\z
+ /x.freeze
+ end
+
def pypi_version_regex
# See the official regex: https://github.com/pypa/packaging/blob/16.7/packaging/version.py#L159
@@ -61,6 +67,39 @@ module Gitlab
)\z}xi.freeze
end
+ def debian_package_name_regex
+ # See official parser
+ # https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n122
+ # @debian_package_name_regex ||= %r{\A[a-z0-9][-+\._a-z0-9]*\z}i.freeze
+ # But we prefer a more strict version from Lintian
+ # https://salsa.debian.org/lintian/lintian/-/blob/5080c0068ffc4a9ddee92022a91d0c2ff53e56d1/lib/Lintian/Util.pm#L116
+ @debian_package_name_regex ||= %r{\A[a-z0-9][-+\.a-z0-9]+\z}.freeze
+ end
+
+ def debian_version_regex
+ # See official parser: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n205
+ @debian_version_regex ||= %r{
+ \A(?:
+ (?:([0-9]{1,9}):)? (?# epoch)
+ ([0-9][0-9a-z\.+~-]*) (?# version)
+ (?:(-[0-0a-z\.+~]+))? (?# revision)
+ )\z}xi.freeze
+ end
+
+ def debian_architecture_regex
+ # See official parser: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/arch.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n43
+ # But we limit to lower case
+ @debian_architecture_regex ||= %r{\A[a-z0-9][-a-z0-9]*\z}.freeze
+ end
+
+ def debian_distribution_regex
+ @debian_distribution_regex ||= %r{\A[a-z0-9][a-z0-9\.-]*\z}i.freeze
+ end
+
+ def debian_component_regex
+ @debian_component_regex ||= %r{#{debian_distribution_regex}}.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
@@ -68,16 +107,34 @@ module Gitlab
# reordered to be greedy. Without this change, the unbounded regex would
# only partially match "v0.0.0-20201230123456-abcdefabcdef".
@unbounded_semver_regex ||= /
+ #{_semver_major_minor_patch_regex}#{_semver_prerelease_build_regex}
+ /x.freeze
+ end
+
+ def semver_regex
+ @semver_regex ||= Regexp.new("\\A#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
+ end
+
+ # These partial semver regexes are intended for use in composing other
+ # regexes rather than being used alone.
+ def _semver_major_minor_patch_regex
+ @_semver_major_minor_patch_regex ||= /
(?<major>0|[1-9]\d*)
\.(?<minor>0|[1-9]\d*)
\.(?<patch>0|[1-9]\d*)
+ /x.freeze
+ end
+
+ def _semver_prerelease_build_regex
+ @_semver_prerelease_build_regex ||= /
(?:-(?<prerelease>(?:\d*[a-zA-Z-][0-9a-zA-Z-]*|[1-9]\d*|0)(?:\.(?:\d*[a-zA-Z-][0-9a-zA-Z-]*|[1-9]\d*|0))*))?
(?:\+(?<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
/x.freeze
end
- def semver_regex
- @semver_regex ||= Regexp.new("\\A#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
+ def prefixed_semver_regex
+ # identical to semver_regex, except starting with 'v'
+ @prefixed_semver_regex ||= Regexp.new("\\Av#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
end
def go_package_regex
@@ -103,6 +160,14 @@ module Gitlab
def generic_package_version_regex
/\A\d+\.\d+\.\d+\z/
end
+
+ def generic_package_name_regex
+ maven_file_name_regex
+ end
+
+ def generic_package_file_name_regex
+ generic_package_name_regex
+ end
end
extend self
@@ -211,8 +276,27 @@ module Gitlab
"Must start with a letter, and cannot end with '-'"
end
+ # The section start, e.g. section_start:12345678:NAME
+ def logs_section_prefix_regex
+ /section_((?:start)|(?:end)):(\d+):([a-zA-Z0-9_.-]+)/
+ end
+
+ # The optional section options, e.g. [collapsed=true]
+ def logs_section_options_regex
+ /(\[(?:\w+=\w+)(?:, ?(?:\w+=\w+))*\])?/
+ end
+
+ # The region end, always: \r\e\[0K
+ def logs_section_suffix_regex
+ /\r\033\[0K/
+ end
+
def build_trace_section_regex
- @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([a-zA-Z0-9_.-]+)\r\033\[0K/.freeze
+ @build_trace_section_regexp ||= %r{
+ #{logs_section_prefix_regex}
+ #{logs_section_options_regex}
+ #{logs_section_suffix_regex}
+ }x.freeze
end
def markdown_code_or_html_blocks
diff --git a/lib/gitlab/relative_positioning/item_context.rb b/lib/gitlab/relative_positioning/item_context.rb
index cd03a347355..8f5495ece5e 100644
--- a/lib/gitlab/relative_positioning/item_context.rb
+++ b/lib/gitlab/relative_positioning/item_context.rb
@@ -131,12 +131,12 @@ module Gitlab
def shift_left
move_sequence_before(true)
- object.reset
+ object.reset_relative_position
end
def shift_right
move_sequence_after(true)
- object.reset
+ object.reset_relative_position
end
def create_space_left
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 67e23624045..9ee6f67e455 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -35,6 +35,10 @@ module Gitlab
snippet, redirected_path = find_snippet(full_path)
[snippet, snippet&.project, redirected_path]
+ elsif type.wiki?
+ wiki, redirected_path = find_wiki(full_path)
+
+ [wiki, wiki.try(:project), redirected_path]
else
project, redirected_path = find_project(full_path)
@@ -67,6 +71,17 @@ module Gitlab
[Snippet.find_by_id_and_project(id: snippet_id, project: project), redirected_path]
end
+ # Wiki path can be either:
+ # - namespace/project
+ # - group/subgroup/project
+ def self.find_wiki(wiki_path)
+ return [nil, nil] if wiki_path.blank?
+
+ project, redirected_path = find_project(wiki_path)
+
+ [project&.wiki, redirected_path]
+ end
+
def self.extract_snippet_info(snippet_path)
path_segments = snippet_path.split('/')
snippet_id = path_segments.pop
diff --git a/lib/gitlab/repository_size_checker.rb b/lib/gitlab/repository_size_checker.rb
index 82c57f7a07d..03d9f961dd9 100644
--- a/lib/gitlab/repository_size_checker.rb
+++ b/lib/gitlab/repository_size_checker.rb
@@ -6,9 +6,10 @@ module Gitlab
attr_reader :limit
# @param current_size_proc [Proc] returns repository size in bytes
- def initialize(current_size_proc:, limit:, enabled: true)
+ def initialize(current_size_proc:, limit:, namespace:, enabled: true)
@current_size_proc = current_size_proc
@limit = limit
+ @namespace = namespace
@enabled = enabled && limit != 0
end
@@ -40,7 +41,13 @@ module Gitlab
end
def error_message
- @error_message_object ||= Gitlab::RepositorySizeErrorMessage.new(self)
+ @error_message_object ||= ::Gitlab::RepositorySizeErrorMessage.new(self)
end
+
+ private
+
+ attr_reader :namespace
end
end
+
+Gitlab::RepositorySizeChecker.prepend_if_ee('EE::Gitlab::RepositorySizeChecker')
diff --git a/lib/gitlab/sample_data_template.rb b/lib/gitlab/sample_data_template.rb
new file mode 100644
index 00000000000..ae74dc710b7
--- /dev/null
+++ b/lib/gitlab/sample_data_template.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class SampleDataTemplate < ProjectTemplate
+ class << self
+ def localized_templates_table
+ [
+ SampleDataTemplate.new('basic', 'Basic', _('Basic Sample Data template with Issues, Merge Requests and Milestones.'), 'https://gitlab.com/gitlab-org/sample-data-templates/basic'),
+ SampleDataTemplate.new('serenity_valley', 'Serenity Valley', _('Serenity Valley Sample Data template.'), 'https://gitlab.com/gitlab-org/sample-data-templates/serenity-valley')
+ ].freeze
+ end
+
+ def all
+ localized_templates_table
+ end
+
+ def archive_directory
+ Rails.root.join("vendor/sample_data_templates")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search/recent_items.rb b/lib/gitlab/search/recent_items.rb
index 40d96ded275..593148025e1 100644
--- a/lib/gitlab/search/recent_items.rb
+++ b/lib/gitlab/search/recent_items.rb
@@ -6,9 +6,12 @@ module Gitlab
# items. The #type and #finder methods are the only ones needed to be
# implemented by classes inheriting from this.
class RecentItems
- ITEMS_LIMIT = 100
+ ITEMS_LIMIT = 100 # How much history to remember from the user
+ SEARCH_LIMIT = 5 # How many matching items to return from search
EXPIRES_AFTER = 7.days
+ attr_reader :user
+
def initialize(user:, items_limit: ITEMS_LIMIT, expires_after: EXPIRES_AFTER)
@user = user
@items_limit = items_limit
@@ -30,21 +33,25 @@ module Gitlab
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
+ finder.new(user, search: term, in: 'title')
+ .execute
+ .limit(SEARCH_LIMIT).reorder(nil).id_in_ordered(latest_ids) # rubocop: disable CodeReuse/ActiveRecord
end
private
+ def latest_ids
+ with_redis do |redis|
+ redis.zrevrange(key, 0, @items_limit - 1)
+ end.map(&:to_i)
+ end
+
def with_redis(&blk)
Gitlab::Redis::SharedState.with(&blk) # rubocop: disable CodeReuse/ActiveRecord
end
def key
- "recent_items:#{type.name.downcase}:#{@user.id}"
+ "recent_items:#{type.name.downcase}:#{user.id}"
end
def type
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 06d8dca2f70..b81264c5d0c 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, :filters
+ attr_reader :current_user, :query, :sort, :filters
# Limit search results by passed projects
# It allows us to search only for projects user has access to
@@ -19,31 +19,23 @@ module Gitlab
# query
attr_reader :default_project_filter
- def initialize(current_user, query, limit_projects = nil, default_project_filter: false, filters: {})
+ def initialize(current_user, query, limit_projects = nil, sort: nil, default_project_filter: false, filters: {})
@current_user = current_user
@query = query
@limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter
+ @sort = sort
@filters = filters
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true, preload_method: nil)
should_preload = preload_method.present?
- collection = case scope
- when 'projects'
- projects
- when 'issues'
- issues
- when 'merge_requests'
- merge_requests
- when 'milestones'
- milestones
- when 'users'
- users
- else
- should_preload = false
- Kaminari.paginate_array([])
- end
+ collection = collection_for(scope)
+
+ if collection.nil?
+ should_preload = false
+ collection = Kaminari.paginate_array([])
+ end
collection = collection.public_send(preload_method) if should_preload # rubocop:disable GitlabSecurity/PublicSend
collection = collection.page(page).per(per_page)
@@ -116,8 +108,41 @@ module Gitlab
UsersFinder.new(current_user, search: query).execute
end
+ # highlighting is only performed by Elasticsearch backed results
+ def highlight_map(scope)
+ {}
+ end
+
private
+ def collection_for(scope)
+ case scope
+ when 'projects'
+ projects
+ when 'issues'
+ issues
+ when 'merge_requests'
+ merge_requests
+ when 'milestones'
+ milestones
+ when 'users'
+ users
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_sort(scope)
+ case sort
+ when 'oldest'
+ scope.reorder('created_at ASC')
+ when 'newest'
+ scope.reorder('created_at DESC')
+ else
+ scope
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def projects
limit_projects.search(query)
end
@@ -129,7 +154,7 @@ module Gitlab
issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
end
- issues
+ apply_sort(issues)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -149,7 +174,7 @@ module Gitlab
merge_requests = merge_requests.in_projects(project_ids_relation)
end
- merge_requests
+ apply_sort(merge_requests)
end
def default_scope
@@ -193,6 +218,10 @@ module Gitlab
end
params[:state] = filters[:state] if filters.key?(:state)
+
+ if [true, false].include?(filters[:confidential]) && Feature.enabled?(:search_filter_by_confidential)
+ params[:confidential] = filters[:confidential]
+ end
end
end
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index e26d45e1b33..4158cec9b09 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -87,7 +87,9 @@ module Gitlab
SeedFu.quiet = true
- yield
+ without_statement_timeout do
+ yield
+ end
SeedFu.quiet = false
ActiveRecord::Base.logger = old_logger
@@ -114,6 +116,13 @@ module Gitlab
def self.mute_mailer
ActionMailer::MessageDelivery.prepend(DeliverNever)
end
+
+ def self.without_statement_timeout
+ ActiveRecord::Base.connection.execute('SET statement_timeout=0')
+ yield
+ ensure
+ ActiveRecord::Base.connection.execute('RESET statement_timeout')
+ end
end
end
# :nocov:
diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb
index e1a87a77f04..8793a672693 100644
--- a/lib/gitlab/sidekiq_daemon/memory_killer.rb
+++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb
@@ -231,7 +231,7 @@ module Gitlab
def rss_increase_by_jobs
Gitlab::SidekiqDaemon::Monitor.instance.jobs_mutex.synchronize do
- Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord
+ Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job|
rss_increase_by_job(job)
end
end
diff --git a/lib/gitlab/sidekiq_middleware/worker_context/server.rb b/lib/gitlab/sidekiq_middleware/worker_context/server.rb
index d2d84742c17..2d8fd8002d2 100644
--- a/lib/gitlab/sidekiq_middleware/worker_context/server.rb
+++ b/lib/gitlab/sidekiq_middleware/worker_context/server.rb
@@ -12,8 +12,10 @@ module Gitlab
# This is not a worker we know about, perhaps from a gem
return yield unless worker_class.respond_to?(:get_worker_context)
- # Use the context defined on the class level as a base context
- wrap_in_optional_context(worker_class.get_worker_context, &block)
+ Gitlab::ApplicationContext.with_context(feature_category: worker_class.get_feature_category.to_s) do
+ # Use the context defined on the class level as the more specific context
+ wrap_in_optional_context(worker_class.get_worker_context, &block)
+ end
end
end
end
diff --git a/lib/gitlab/slash_commands/presenters/help.rb b/lib/gitlab/slash_commands/presenters/help.rb
index 2d8df2ca204..714ca77c3e5 100644
--- a/lib/gitlab/slash_commands/presenters/help.rb
+++ b/lib/gitlab/slash_commands/presenters/help.rb
@@ -48,7 +48,7 @@ module Gitlab
*Documentation*
For more information about GitLab chatops, refer to its
- documentation: https://docs.gitlab.com/ce/ci/chatops/README.html.
+ documentation: https://docs.gitlab.com/ee/ci/chatops/README.html.
MESSAGE
message
diff --git a/lib/gitlab/static_site_editor/config/file_config.rb b/lib/gitlab/static_site_editor/config/file_config.rb
index f647c85e1c8..315c603c1dd 100644
--- a/lib/gitlab/static_site_editor/config/file_config.rb
+++ b/lib/gitlab/static_site_editor/config/file_config.rb
@@ -3,11 +3,38 @@
module Gitlab
module StaticSiteEditor
module Config
+ #
+ # Base GitLab Static Site Editor Configuration facade
+ #
class FileConfig
- def data
- {
- static_site_generator: 'middleman'
- }
+ ConfigError = Class.new(StandardError)
+
+ def initialize(yaml)
+ content_hash = content_hash(yaml)
+ @global = Entry::Global.new(content_hash)
+ @global.compose!
+ rescue Gitlab::Config::Loader::FormatError => e
+ raise FileConfig::ConfigError, e.message
+ end
+
+ def valid?
+ @global.valid?
+ end
+
+ def errors
+ @global.errors
+ end
+
+ def to_hash_with_defaults
+ # NOTE: The current approach of simply mapping all the descendents' keys and values ('config')
+ # into a flat hash may need to be enhanced as we add more complex, non-scalar entries.
+ @global.descendants.map { |descendant| [descendant.key, descendant.config] }.to_h
+ end
+
+ private
+
+ def content_hash(yaml)
+ Gitlab::Config::Loader::Yaml.new(yaml).load!
end
end
end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/global.rb b/lib/gitlab/static_site_editor/config/file_config/entry/global.rb
new file mode 100644
index 00000000000..c295ccf1d11
--- /dev/null
+++ b/lib/gitlab/static_site_editor/config/file_config/entry/global.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module StaticSiteEditor
+ module Config
+ class FileConfig
+ module Entry
+ ##
+ # This class represents a global entry - root Entry for entire
+ # GitLab StaticSiteEditor Configuration file.
+ #
+ class Global < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
+
+ ALLOWED_KEYS = %i[
+ image_upload_path
+ mounts
+ static_site_generator
+ ].freeze
+
+ attributes ALLOWED_KEYS
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ end
+
+ entry :image_upload_path, Entry::ImageUploadPath,
+ description: 'Configuration of the Static Site Editor image upload path.'
+ entry :mounts, Entry::Mounts,
+ description: 'Configuration of the Static Site Editor mounts.'
+ entry :static_site_generator, Entry::StaticSiteGenerator,
+ description: 'Configuration of the Static Site Editor static site generator.'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb b/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb
new file mode 100644
index 00000000000..6a2b9e10d33
--- /dev/null
+++ b/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module StaticSiteEditor
+ module Config
+ class FileConfig
+ module Entry
+ ##
+ # Entry that represents the path to which images will be uploaded
+ #
+ class ImageUploadPath < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, type: String
+ end
+
+ def self.default
+ 'source/images'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/mount.rb b/lib/gitlab/static_site_editor/config/file_config/entry/mount.rb
new file mode 100644
index 00000000000..b10956e17a5
--- /dev/null
+++ b/lib/gitlab/static_site_editor/config/file_config/entry/mount.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module StaticSiteEditor
+ module Config
+ class FileConfig
+ module Entry
+ ##
+ # Entry that represents the mappings of mounted source directories to target paths
+ #
+ class Mount < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+
+ ALLOWED_KEYS = %i[source target].freeze
+
+ attributes ALLOWED_KEYS
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ validates :source, type: String, presence: true
+ validates :target, type: String, presence: true, allow_blank: true
+ end
+
+ def self.default
+ # NOTE: This is the default for middleman projects. Ideally, this would be determined
+ # based on the defaults for whatever `static_site_generator` is configured.
+ {
+ source: 'source',
+ target: ''
+ }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb b/lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb
new file mode 100644
index 00000000000..10bd377e419
--- /dev/null
+++ b/lib/gitlab/static_site_editor/config/file_config/entry/mounts.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module StaticSiteEditor
+ module Config
+ class FileConfig
+ module Entry
+ ##
+ # Entry that represents the mappings of mounted source directories to target paths
+ #
+ class Mounts < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Validatable
+
+ entry :mount, Entry::Mount, description: 'Configuration of a Static Site Editor mount.'
+
+ validations do
+ validates :config, type: Array, presence: true
+ end
+
+ def skip_config_hash_validation?
+ true
+ end
+
+ def self.default
+ [Entry::Mount.default]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb b/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb
new file mode 100644
index 00000000000..593c0951f93
--- /dev/null
+++ b/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module StaticSiteEditor
+ module Config
+ class FileConfig
+ module Entry
+ ##
+ # Entry that represents the static site generator tool/framework.
+ #
+ class StaticSiteGenerator < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, type: String, inclusion: { in: %w[middleman], message: "should be 'middleman'" }
+ end
+
+ def self.default
+ 'middleman'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/static_site_editor/config/generated_config.rb b/lib/gitlab/static_site_editor/config/generated_config.rb
index f3dce74a32f..ff24ec69ab0 100644
--- a/lib/gitlab/static_site_editor/config/generated_config.rb
+++ b/lib/gitlab/static_site_editor/config/generated_config.rb
@@ -4,8 +4,6 @@ module Gitlab
module StaticSiteEditor
module Config
class GeneratedConfig
- SUPPORTED_EXTENSIONS = %w[.md].freeze
-
def initialize(repository, ref, path, return_url)
@repository = repository
@ref = ref
@@ -23,7 +21,7 @@ module Gitlab
project: project.path,
namespace: project.namespace.full_path,
return_url: sanitize_url(return_url),
- is_supported_content: supported_content?.to_s,
+ is_supported_content: supported_content?,
base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path),
merge_requests_illustration_path: merge_requests_illustration_path
}
@@ -35,8 +33,12 @@ module Gitlab
delegate :project, to: :repository
+ def supported_extensions
+ %w[.md].freeze
+ end
+
def commit_id
- repository.commit(ref)&.id if ref
+ repository.commit(ref)&.id
end
def supported_content?
@@ -50,7 +52,7 @@ module Gitlab
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) }
+ supported_extensions.any? { |ext| path.end_with?(ext) }
end
def file_exists?
diff --git a/lib/gitlab/subscription_portal.rb b/lib/gitlab/subscription_portal.rb
new file mode 100644
index 00000000000..a918e7bec80
--- /dev/null
+++ b/lib/gitlab/subscription_portal.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SubscriptionPortal
+ def self.default_subscriptions_url
+ ::Gitlab.dev_or_test_env? ? 'https://customers.stg.gitlab.com' : 'https://customers.gitlab.com'
+ end
+
+ SUBSCRIPTIONS_URL = ENV.fetch('CUSTOMER_PORTAL_URL', default_subscriptions_url).freeze
+ end
+end
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index bb1e9db55fa..e12af6bf0a4 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -3,7 +3,7 @@
module Gitlab
module Template
class GitlabCiYmlTemplate < BaseTemplate
- BASE_EXCLUDED_PATTERNS = [%r{\.latest$}].freeze
+ BASE_EXCLUDED_PATTERNS = [%r{\.latest\.}].freeze
def content
explanation = "# This file is a template, and might need editing before it works on your project."
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index 72783a2d682..16e7b8a7eca 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -10,21 +10,21 @@ module Gitlab
APPLICATION_DEFAULT = 1
# Struct class representing a single Theme
- Theme = Struct.new(:id, :name, :css_class)
+ Theme = Struct.new(:id, :name, :css_class, :css_filename)
# All available Themes
THEMES = [
- Theme.new(1, 'Indigo', 'ui-indigo'),
- Theme.new(6, 'Light Indigo', 'ui-light-indigo'),
- Theme.new(4, 'Blue', 'ui-blue'),
- Theme.new(7, 'Light Blue', 'ui-light-blue'),
- Theme.new(5, 'Green', 'ui-green'),
- Theme.new(8, 'Light Green', 'ui-light-green'),
- Theme.new(9, 'Red', 'ui-red'),
- Theme.new(10, 'Light Red', 'ui-light-red'),
- Theme.new(2, 'Dark', 'ui-dark'),
- Theme.new(3, 'Light', 'ui-light'),
- Theme.new(11, 'Dark Mode (alpha)', 'gl-dark')
+ Theme.new(1, 'Indigo', 'ui-indigo', 'theme_indigo'),
+ Theme.new(6, 'Light Indigo', 'ui-light-indigo', 'theme_light_indigo'),
+ Theme.new(4, 'Blue', 'ui-blue', 'theme_blue'),
+ Theme.new(7, 'Light Blue', 'ui-light-blue', 'theme_light_blue'),
+ Theme.new(5, 'Green', 'ui-green', 'theme_green'),
+ Theme.new(8, 'Light Green', 'ui-light-green', 'theme_light_green'),
+ Theme.new(9, 'Red', 'ui-red', 'theme_red'),
+ Theme.new(10, 'Light Red', 'ui-light-red', 'theme_light_red'),
+ Theme.new(2, 'Dark', 'ui-dark', 'theme_dark'),
+ Theme.new(3, 'Light', 'ui-light', 'theme_light'),
+ Theme.new(11, 'Dark Mode (alpha)', 'gl-dark', nil)
].freeze
# Convenience method to get a space-separated String of all the theme
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 89605ce5d07..68f24559b1f 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -12,6 +12,19 @@
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
module Gitlab
class UsageData
+ CE_MEMOIZED_VALUES = %i(
+ issue_minimum_id
+ issue_maximum_id
+ project_minimum_id
+ project_maximum_id
+ user_minimum_id
+ user_maximum_id
+ unique_visit_service
+ deployment_minimum_id
+ deployment_maximum_id
+ auth_providers
+ ).freeze
+
class << self
include Gitlab::Utils::UsageData
include Gitlab::Utils::StrongMemoize
@@ -138,8 +151,10 @@ module Gitlab
pages_domains: count(PagesDomain),
pool_repositories: count(PoolRepository),
projects: count(Project),
+ projects_creating_incidents: distinct_count(Issue.incident, :project_id),
projects_imported_from_github: count(Project.where(import_type: 'github')),
projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
+ projects_with_tracing_enabled: count(ProjectTracingSetting),
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
projects_with_alerts_service_enabled: count(AlertsService.active),
projects_with_prometheus_alerts: distinct_count(PrometheusAlert, :project_id),
@@ -166,8 +181,7 @@ module Gitlab
user_preferences_usage,
ingress_modsecurity_usage,
container_expiration_policies_usage,
- service_desk_counts,
- snowplow_event_counts
+ service_desk_counts
).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
@@ -175,7 +189,7 @@ module Gitlab
end
# rubocop: enable Metrics/AbcSize
- def snowplow_event_counts(time_period: {})
+ def snowplow_event_counts(time_period)
return {} unless report_snowplow_events?
{
@@ -200,7 +214,7 @@ module Gitlab
personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)),
project_snippets: count(ProjectSnippet.where(last_28_days_time_period))
}.merge(
- snowplow_event_counts(time_period: last_28_days_time_period(column: :collector_tstamp))
+ snowplow_event_counts(last_28_days_time_period(column: :collector_tstamp))
).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
@@ -242,7 +256,8 @@ module Gitlab
signup_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.allow_signup? },
web_ide_clientside_preview_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? },
ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity),
- grafana_link_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.grafana_enabled? }
+ grafana_link_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.grafana_enabled? },
+ gitpod_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.gitpod_enabled? }
}
end
@@ -264,7 +279,8 @@ module Gitlab
Gitlab::UsageDataCounters::SourceCodeCounter,
Gitlab::UsageDataCounters::MergeRequestCounter,
Gitlab::UsageDataCounters::DesignsCounter,
- Gitlab::UsageDataCounters::KubernetesAgentCounter
+ Gitlab::UsageDataCounters::KubernetesAgentCounter,
+ Gitlab::UsageDataCounters::StaticSiteEditorCounter
]
end
@@ -287,7 +303,8 @@ module Gitlab
},
database: {
adapter: alt_usage_data { Gitlab::Database.adapter_name },
- version: alt_usage_data { Gitlab::Database.version }
+ version: alt_usage_data { Gitlab::Database.version },
+ pg_system_id: alt_usage_data { Gitlab::Database.system_id }
},
mail: {
smtp_server: alt_usage_data { ActionMailer::Base.smtp_settings[:address] }
@@ -385,10 +402,12 @@ module Gitlab
def services_usage
# rubocop: disable UsageData/LargeTable:
Service.available_services_names.each_with_object({}) do |service_name, response|
- response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, instance: false, type: "#{service_name}_service".camelize))
+ response["projects_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where(type: "#{service_name}_service".camelize))
+ response["groups_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where(type: "#{service_name}_service".camelize))
response["templates_#{service_name}_active".to_sym] = count(Service.active.where(template: true, type: "#{service_name}_service".camelize))
response["instances_#{service_name}_active".to_sym] = count(Service.active.where(instance: true, type: "#{service_name}_service".camelize))
- response["projects_inheriting_instance_#{service_name}_active".to_sym] = count(Service.active.where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize))
+ response["projects_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize))
+ response["groups_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize))
end.merge(jira_usage, jira_import_usage)
# rubocop: enable UsageData/LargeTable:
end
@@ -444,8 +463,11 @@ module Gitlab
# rubocop: enable UsageData/LargeTable
# rubocop: enable CodeReuse/ActiveRecord
+ # augmented in EE
def user_preferences_usage
- {} # augmented in EE
+ {
+ user_preferences_user_gitpod_enabled: count(UserPreference.with_user.gitpod_enabled.merge(User.active))
+ }
end
def merge_requests_users(time_period)
@@ -469,7 +491,7 @@ module Gitlab
end
def last_28_days_time_period(column: :created_at)
- { column => 28.days.ago..Time.current }
+ { column => 30.days.ago..2.days.ago }
end
# Source: https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/ping_metrics_to_stage_mapping_data.csv
@@ -541,6 +563,7 @@ module Gitlab
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' },
+ user_auth_by_provider: distinct_count_user_auth_by_provider(time_period),
projects_imported: {
gitlab_project: projects_imported_count('gitlab_project', time_period),
gitlab: projects_imported_count('gitlab', time_period),
@@ -555,7 +578,8 @@ module Gitlab
jira: distinct_count(::JiraImportState.where(time_period), :user_id),
fogbugz: projects_imported_count('fogbugz', time_period),
phabricator: projects_imported_count('phabricator', time_period)
- }
+ },
+ groups_imported: distinct_count(::GroupImportState.where(time_period), :user_id)
}
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -567,7 +591,8 @@ module Gitlab
clusters_applications_prometheus: cluster_applications_user_distinct_count(::Clusters::Applications::Prometheus, time_period),
operations_dashboard_default_dashboard: count(::User.active.with_dashboard('operations').where(time_period),
start: user_minimum_id,
- finish: user_maximum_id)
+ finish: user_maximum_id),
+ projects_with_tracing_enabled: distinct_count(::Project.with_tracing_enabled.where(time_period), :creator_id)
}
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -696,10 +721,10 @@ module Gitlab
counter = Gitlab::UsageDataCounters::EditorUniqueCounter
{
- 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) }
+ 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
@@ -801,17 +826,7 @@ module Gitlab
end
def clear_memoized
- clear_memoization(:issue_minimum_id)
- clear_memoization(:issue_maximum_id)
- clear_memoization(:user_minimum_id)
- clear_memoization(:user_maximum_id)
- clear_memoization(:unique_visit_service)
- clear_memoization(:deployment_minimum_id)
- 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)
+ CE_MEMOIZED_VALUES.each { |v| clear_memoization(v) }
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -843,6 +858,39 @@ module Gitlab
def projects_imported_count(from, time_period)
distinct_count(::Project.imported_from(from).where(time_period), :creator_id) # rubocop: disable CodeReuse/ActiveRecord
end
+
+ # rubocop:disable CodeReuse/ActiveRecord
+ def distinct_count_user_auth_by_provider(time_period)
+ counts = auth_providers_except_ldap.each_with_object({}) do |provider, hash|
+ hash[provider] = distinct_count(
+ ::AuthenticationEvent.success.for_provider(provider).where(time_period), :user_id)
+ end
+
+ if any_ldap_auth_providers?
+ counts['ldap'] = distinct_count(
+ ::AuthenticationEvent.success.ldap.where(time_period), :user_id
+ )
+ end
+
+ counts
+ end
+ # rubocop:enable CodeReuse/ActiveRecord
+
+ # rubocop:disable UsageData/LargeTable
+ def auth_providers
+ strong_memoize(:auth_providers) do
+ ::AuthenticationEvent.providers
+ end
+ end
+ # rubocop:enable UsageData/LargeTable
+
+ def auth_providers_except_ldap
+ auth_providers.reject { |provider| provider.starts_with?('ldap') }
+ end
+
+ def any_ldap_auth_providers?
+ auth_providers.any? { |provider| provider.starts_with?('ldap') }
+ 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 53bf6daea4c..eb132ef0967 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -72,7 +72,8 @@ module Gitlab
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)
+ hash["#{event}_weekly"] = unique_events(event_names: event, start_date: 7.days.ago.to_date, end_date: Date.current)
+ hash["#{event}_monthly"] = unique_events(event_names: event, start_date: 4.weeks.ago.to_date, end_date: Date.current)
end
if eligible_for_totals?(events_names)
diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
index fc1b5a59487..e8839875109 100644
--- a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
@@ -3,14 +3,42 @@
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_CATEGORY = 'issues_edit'
+
ISSUE_ASSIGNEE_CHANGED = 'g_project_management_issue_assignee_changed'
+ ISSUE_CREATED = 'g_project_management_issue_created'
+ ISSUE_CLOSED = 'g_project_management_issue_closed'
+ ISSUE_DESCRIPTION_CHANGED = 'g_project_management_issue_description_changed'
+ ISSUE_ITERATION_CHANGED = 'g_project_management_issue_iteration_changed'
+ ISSUE_LABEL_CHANGED = 'g_project_management_issue_label_changed'
ISSUE_MADE_CONFIDENTIAL = 'g_project_management_issue_made_confidential'
ISSUE_MADE_VISIBLE = 'g_project_management_issue_made_visible'
- ISSUE_CATEGORY = 'issues_edit'
+ ISSUE_MILESTONE_CHANGED = 'g_project_management_issue_milestone_changed'
+ ISSUE_REOPENED = 'g_project_management_issue_reopened'
+ ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed'
+ ISSUE_WEIGHT_CHANGED = 'g_project_management_issue_weight_changed'
+ ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced'
+ ISSUE_MOVED = 'g_project_management_issue_moved'
+ ISSUE_RELATED = 'g_project_management_issue_related'
+ ISSUE_UNRELATED = 'g_project_management_issue_unrelated'
+ ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate'
+ ISSUE_LOCKED = 'g_project_management_issue_locked'
+ ISSUE_UNLOCKED = 'g_project_management_issue_unlocked'
+ ISSUE_ADDED_TO_EPIC = 'g_project_management_issue_added_to_epic'
+ ISSUE_REMOVED_FROM_EPIC = 'g_project_management_issue_removed_from_epic'
+ ISSUE_CHANGED_EPIC = 'g_project_management_issue_changed_epic'
+ ISSUE_DESIGNS_ADDED = 'g_project_management_issue_designs_added'
+ ISSUE_DESIGNS_MODIFIED = 'g_project_management_issue_designs_modified'
+ ISSUE_DESIGNS_REMOVED = 'g_project_management_issue_designs_removed'
+ ISSUE_DUE_DATE_CHANGED = 'g_project_management_issue_due_date_changed'
+ ISSUE_TIME_ESTIMATE_CHANGED = 'g_project_management_issue_time_estimate_changed'
+ ISSUE_TIME_SPENT_CHANGED = 'g_project_management_issue_time_spent_changed'
class << self
+ def track_issue_created_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_CREATED, author, time)
+ end
+
def track_issue_title_changed_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_TITLE_CHANGED, author, time)
end
@@ -31,10 +59,98 @@ module Gitlab
track_unique_action(ISSUE_MADE_VISIBLE, author, time)
end
+ def track_issue_closed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_CLOSED, author, time)
+ end
+
+ def track_issue_reopened_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_REOPENED, author, time)
+ end
+
+ def track_issue_label_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_LABEL_CHANGED, author, time)
+ end
+
+ def track_issue_milestone_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_MILESTONE_CHANGED, author, time)
+ end
+
+ def track_issue_iteration_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_ITERATION_CHANGED, author, time)
+ end
+
+ def track_issue_weight_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_WEIGHT_CHANGED, author, time)
+ end
+
+ def track_issue_cross_referenced_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_CROSS_REFERENCED, author, time)
+ end
+
+ def track_issue_moved_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_MOVED, author, time)
+ end
+
+ def track_issue_related_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_RELATED, author, time)
+ end
+
+ def track_issue_unrelated_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_UNRELATED, author, time)
+ end
+
+ def track_issue_marked_as_duplicate_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_MARKED_AS_DUPLICATE, author, time)
+ end
+
+ def track_issue_locked_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_LOCKED, author, time)
+ end
+
+ def track_issue_unlocked_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_UNLOCKED, author, time)
+ end
+
+ def track_issue_added_to_epic_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_ADDED_TO_EPIC, author, time)
+ end
+
+ def track_issue_removed_from_epic_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_REMOVED_FROM_EPIC, author, time)
+ end
+
+ def track_issue_changed_epic_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_CHANGED_EPIC, author, time)
+ end
+
+ def track_issue_designs_added_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_DESIGNS_ADDED, author, time)
+ end
+
+ def track_issue_designs_modified_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_DESIGNS_MODIFIED, author, time)
+ end
+
+ def track_issue_designs_removed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_DESIGNS_REMOVED, author, time)
+ end
+
+ def track_issue_due_date_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_DUE_DATE_CHANGED, author, time)
+ end
+
+ def track_issue_time_estimate_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_TIME_ESTIMATE_CHANGED, author, time)
+ end
+
+ def track_issue_time_spent_changed_action(author:, time: Time.zone.now)
+ track_unique_action(ISSUE_TIME_SPENT_CHANGED, author, time)
+ end
+
private
def track_unique_action(action, author, time)
- return unless Feature.enabled?(:track_issue_activity_actions)
+ return unless Feature.enabled?(:track_issue_activity_actions, default_enabled: true)
return unless author
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(author.id, action, time)
diff --git a/lib/gitlab/usage_data_counters/known_events.yml b/lib/gitlab/usage_data_counters/known_events.yml
index 25e7f858bb1..bc56c5d6d9b 100644
--- a/lib/gitlab/usage_data_counters/known_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events.yml
@@ -185,6 +185,11 @@
redis_slot: incident_management
category: incident_management
aggregation: weekly
+# Testing category
+- name: i_testing_test_case_parsed
+ category: testing
+ redis_slot: testing
+ aggregation: weekly
# Project Management group
- name: g_project_management_issue_title_changed
category: issues_edit
@@ -206,3 +211,95 @@
category: issues_edit
redis_slot: project_management
aggregation: daily
+- name: g_project_management_issue_created
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_closed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_reopened
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_label_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_milestone_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_iteration_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_weight_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_cross_referenced
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_moved
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_related
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_unrelated
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_marked_as_duplicate
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_locked
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_unlocked
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_added_to_epic
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_removed_from_epic
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_changed_epic
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_designs_added
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_designs_modified
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_designs_removed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_due_date_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_time_estimate_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
+- name: g_project_management_issue_time_spent_changed
+ category: issues_edit
+ redis_slot: project_management
+ aggregation: daily
diff --git a/lib/gitlab/usage_data_counters/static_site_editor_counter.rb b/lib/gitlab/usage_data_counters/static_site_editor_counter.rb
new file mode 100644
index 00000000000..8886a106da8
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/static_site_editor_counter.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ class StaticSiteEditorCounter < BaseCounter
+ KNOWN_EVENTS = %w[views].freeze
+ PREFIX = 'static_site_editor'
+
+ class << self
+ def increment_views_count
+ count(:views)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb
index bacd63ab282..c54e766230e 100644
--- a/lib/gitlab/usage_data_queries.rb
+++ b/lib/gitlab/usage_data_queries.rb
@@ -22,7 +22,7 @@ module Gitlab
end
def sum(relation, column, *rest)
- relation.select(relation.all.table[column].sum).to_sql # rubocop:disable CodeReuse/ActiveRecord
+ relation.select(relation.all.table[column].sum).to_sql
end
private
diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb
index ca6a36c9cea..5267733d220 100644
--- a/lib/gitlab/utils/usage_data.rb
+++ b/lib/gitlab/utils/usage_data.rb
@@ -39,9 +39,9 @@ module Gitlab
FALLBACK = -1
- def count(relation, column = nil, batch: true, start: nil, finish: nil)
+ def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
if batch
- Gitlab::Database::BatchCount.batch_count(relation, column, start: start, finish: finish)
+ Gitlab::Database::BatchCount.batch_count(relation, column, batch_size: batch_size, start: start, finish: finish)
else
relation.count
end
@@ -106,7 +106,6 @@ module Gitlab
# @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
diff --git a/lib/gitlab/view/presenter/factory.rb b/lib/gitlab/view/presenter/factory.rb
index 302697ff8eb..5768bab03c6 100644
--- a/lib/gitlab/view/presenter/factory.rb
+++ b/lib/gitlab/view/presenter/factory.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def fabricate!
- presenter_class.new(subject, attributes)
+ presenter_class.new(subject, **attributes)
end
private
diff --git a/lib/gitlab/visibility_level_checker.rb b/lib/gitlab/visibility_level_checker.rb
index f15f1486a4e..3ffd86c4f8c 100644
--- a/lib/gitlab/visibility_level_checker.rb
+++ b/lib/gitlab/visibility_level_checker.rb
@@ -3,7 +3,7 @@
# Gitlab::VisibilityLevelChecker verifies that:
# - Current @project.visibility_level is not restricted
# - Override visibility param is not restricted
-# - @see https://docs.gitlab.com/ce/api/project_import_export.html#import-a-file
+# - @see https://docs.gitlab.com/ee/api/project_import_export.html#import-a-file
#
# @param current_user [User] Current user object to verify visibility level against
# @param project [Project] Current project that is being created/imported
diff --git a/lib/gitlab/webpack/dev_server_middleware.rb b/lib/gitlab/webpack/dev_server_middleware.rb
index fda41da5a94..069e68e8d29 100644
--- a/lib/gitlab/webpack/dev_server_middleware.rb
+++ b/lib/gitlab/webpack/dev_server_middleware.rb
@@ -11,8 +11,15 @@ module Gitlab
@proxy_host = opts.fetch(:proxy_host, 'localhost')
@proxy_port = opts.fetch(:proxy_port, 3808)
@proxy_path = opts[:proxy_path] if opts[:proxy_path]
+ @proxy_scheme = opts[:proxy_https] ? 'https' : 'http'
- super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
+ super(app, backend: "#{@proxy_scheme}://#{@proxy_host}:#{@proxy_port}", **opts)
+ end
+
+ # disable SSL check since any cert used here will likely be self-signed
+ def rewrite_env(env)
+ env["rack.ssl_verify_none"] = true
+ env
end
def perform_request(env)
diff --git a/lib/gitlab/webpack/manifest.rb b/lib/gitlab/webpack/manifest.rb
index d2c01bbd55e..5873d9c2b99 100644
--- a/lib/gitlab/webpack/manifest.rb
+++ b/lib/gitlab/webpack/manifest.rb
@@ -1,16 +1,33 @@
# frozen_string_literal: true
-require 'webpack/rails/manifest'
+require 'net/http'
+require 'uri'
module Gitlab
module Webpack
- class Manifest < ::Webpack::Rails::Manifest
- # Raised if a supplied asset does not exist in the webpack manifest
+ class Manifest
+ # Raised if we can't read our webpack manifest for whatever reason
+ class ManifestLoadError < StandardError
+ def initialize(message, orig)
+ super "#{message}\n\n(original error #{orig.class.name}: #{orig})"
+ end
+ end
+
+ # Raised if webpack couldn't build one of your entry points
+ class WebpackError < StandardError
+ def initialize(errors)
+ super "Error in webpack compile, details follow below:\n#{errors.join("\n\n")}"
+ end
+ end
+
+ # Raised if a supplied entry point does not exist in the webpack manifest
AssetMissingError = Class.new(StandardError)
class << self
+ include Gitlab::Utils::StrongMemoize
+
def entrypoint_paths(source)
- raise ::Webpack::Rails::Manifest::WebpackError, manifest["errors"] unless manifest_bundled?
+ raise WebpackError, manifest["errors"] unless manifest_bundled?
dll_assets = manifest.fetch("dllAssets", [])
entrypoint = manifest["entrypoints"][source]
@@ -18,12 +35,93 @@ module Gitlab
# Can be either a string or an array of strings.
# Do not include source maps as they are not javascript
[dll_assets, entrypoint["assets"]].flatten.reject { |p| p =~ /.*\.map$/ }.map do |p|
- "/#{::Rails.configuration.webpack.public_path}/#{p}"
+ "/#{Gitlab.config.webpack.public_path}/#{p}"
+ end
+ else
+ raise AssetMissingError, "Can't find asset '#{source}' in webpack manifest"
+ end
+ end
+
+ def asset_paths(source)
+ raise WebpackError, manifest["errors"] unless manifest_bundled?
+
+ paths = manifest["assetsByChunkName"][source]
+ if paths
+ # Can be either a string or an array of strings.
+ # Do not include source maps as they are not javascript
+ [paths].flatten.reject { |p| p =~ /.*\.map$/ }.map do |p|
+ "/#{Gitlab.config.webpack.public_path}/#{p}"
end
else
raise AssetMissingError, "Can't find entry point '#{source}' in webpack manifest"
end
end
+
+ def clear_manifest!
+ clear_memoization(:manifest)
+ end
+
+ private
+
+ def manifest_bundled?
+ !manifest["errors"].any? { |error| error.include? "Module build failed" }
+ end
+
+ def manifest
+ if Gitlab.config.webpack.dev_server.enabled
+ # Don't cache if we're in dev server mode, manifest may change ...
+ load_manifest
+ else
+ # ... otherwise cache at class level, as JSON loading/parsing can be expensive
+ strong_memoize(:manifest) { load_manifest }
+ end
+ end
+
+ def load_manifest
+ data = if Gitlab.config.webpack.dev_server.enabled
+ load_dev_server_manifest
+ else
+ load_static_manifest
+ end
+
+ Gitlab::Json.parse(data)
+ end
+
+ def load_dev_server_manifest
+ host = Gitlab.config.webpack.dev_server.host
+ port = Gitlab.config.webpack.dev_server.port
+ scheme = Gitlab.config.webpack.dev_server.https ? 'https' : 'http'
+ uri = Addressable::URI.new(scheme: scheme, host: host, port: port, path: dev_server_path)
+
+ # localhost could be blocked via Gitlab::HTTP
+ response = HTTParty.get(uri.to_s, verify: false) # rubocop:disable Gitlab/HTTParty
+
+ return response.body if response.code == 200
+
+ raise "HTTP error #{response.code}"
+ rescue OpenSSL::SSL::SSLError, EOFError => e
+ ssl_status = Gitlab.config.webpack.dev_server.https ? ' over SSL' : ''
+ raise ManifestLoadError.new("Could not connect to webpack-dev-server at #{uri}#{ssl_status}.\n\nIs SSL enabled? Check that settings in `gitlab.yml` and webpack-dev-server match.", e)
+ rescue => e
+ raise ManifestLoadError.new("Could not load manifest from webpack-dev-server at #{uri}.\n\nIs webpack-dev-server running? Try running `gdk status webpack` or `gdk tail webpack`.", e)
+ end
+
+ def load_static_manifest
+ File.read(static_manifest_path)
+ rescue => e
+ raise ManifestLoadError.new("Could not load compiled manifest from #{static_manifest_path}.\n\nHave you run `rake gitlab:assets:compile`?", e)
+ end
+
+ def static_manifest_path
+ ::Rails.root.join(
+ Gitlab.config.webpack.output_dir,
+ Gitlab.config.webpack.manifest_filename
+ )
+ end
+
+ def dev_server_path
+ "/#{Gitlab.config.webpack.public_path}/#{Gitlab.config.webpack.manifest_filename}"
+ end
end
end
end
diff --git a/lib/gitlab/whats_new.rb b/lib/gitlab/whats_new.rb
new file mode 100644
index 00000000000..e95ace2c475
--- /dev/null
+++ b/lib/gitlab/whats_new.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WhatsNew
+ CACHE_DURATION = 1.day
+ WHATS_NEW_FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
+
+ private
+
+ def whats_new_most_recent_release_items
+ Rails.cache.fetch('whats_new:release_items', expires_in: CACHE_DURATION) do
+ file = File.read(most_recent_release_file_path)
+
+ items = YAML.safe_load(file, permitted_classes: [Date])
+
+ items if items.is_a?(Array)
+ end
+ rescue => e
+ Gitlab::ErrorTracking.track_exception(e, yaml_file_path: most_recent_release_file_path)
+
+ nil
+ end
+
+ def most_recent_release_file_path
+ @most_recent_release_file_path ||= Dir.glob(WHATS_NEW_FILES_PATH).max
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 8a5acd242d9..eb780a2f7f6 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -269,7 +269,8 @@ module Gitlab
commit_id: metadata['CommitId'],
prefix: metadata['ArchivePrefix'],
format: format,
- path: path.presence || ""
+ path: path.presence || "",
+ include_lfs_blobs: Feature.enabled?(:include_lfs_blobs_in_archive)
).to_proto
)
}