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>2023-11-07 12:08:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-07 12:08:54 +0300
commitdfa6eac07553d5a3f254ee904e4298bd666b410f (patch)
treea14e04cf628bf1bd28a0c95ed6261461605cfe2e
parent4be549b5ebd354c69fcd2a09e2a2f642490612cf (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/lint/redundant_dir_glob_sort.yml1
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum6
-rw-r--r--Gemfile.lock4
-rw-r--r--app/services/bulk_imports/batched_relation_export_service.rb8
-rw-r--r--app/services/bulk_imports/relation_batch_export_service.rb8
-rw-r--r--app/services/bulk_imports/relation_export_service.rb8
-rw-r--r--app/services/service_desk/custom_email_verifications/update_service.rb6
-rw-r--r--app/workers/bulk_imports/relation_batch_export_worker.rb11
-rw-r--r--app/workers/bulk_imports/relation_export_worker.rb25
-rw-r--r--config/feature_flags/development/oidc_issuer_url.yml8
-rw-r--r--db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb24
-rw-r--r--db/schema_migrations/202310301541171
-rw-r--r--doc/administration/audit_event_streaming/index.md6
-rw-r--r--doc/api/graphql/reference/index.md13
-rw-r--r--doc/subscriptions/bronze_starter.md2
-rw-r--r--doc/user/application_security/container_scanning/index.md103
-rw-r--r--doc/user/packages/container_registry/reduce_container_registry_storage.md51
-rw-r--r--doc/user/read_only_namespaces.md2
-rw-r--r--doc/user/usage_quotas.md137
-rw-r--r--lib/gitlab/ci/jwt_v2.rb2
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb117
-rw-r--r--lib/gitlab/middleware/path_traversal_check.rb44
-rw-r--r--package.json2
-rw-r--r--rubocop/rubocop.rb6
-rw-r--r--spec/lib/gitlab/ci/jwt_v2_spec.rb36
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb481
-rw-r--r--spec/lib/gitlab/middleware/path_traversal_check_spec.rb177
-rw-r--r--spec/rubocop/rubocop_spec.rb13
-rw-r--r--spec/services/bulk_imports/batched_relation_export_service_spec.rb23
-rw-r--r--spec/services/bulk_imports/relation_batch_export_service_spec.rb26
-rw-r--r--spec/services/bulk_imports/relation_export_service_spec.rb35
-rw-r--r--spec/services/service_desk/custom_email_verifications/update_service_spec.rb27
-rw-r--r--spec/workers/bulk_imports/relation_batch_export_worker_spec.rb17
-rw-r--r--spec/workers/bulk_imports/relation_export_worker_spec.rb16
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb4
-rw-r--r--yarn.lock8
37 files changed, 998 insertions, 462 deletions
diff --git a/.rubocop_todo/lint/redundant_dir_glob_sort.yml b/.rubocop_todo/lint/redundant_dir_glob_sort.yml
index 1e4afd562f0..c992f5647a7 100644
--- a/.rubocop_todo/lint/redundant_dir_glob_sort.yml
+++ b/.rubocop_todo/lint/redundant_dir_glob_sort.yml
@@ -6,5 +6,4 @@ Lint/RedundantDirGlobSort:
- 'config/application.rb'
- 'ee/spec/spec_helper.rb'
- 'qa/qa/specs/spec_helper.rb'
- - 'rubocop/rubocop.rb'
- 'spec/spec_helper.rb'
diff --git a/Gemfile b/Gemfile
index 7a6bb68e8bf..d069c116a73 100644
--- a/Gemfile
+++ b/Gemfile
@@ -612,7 +612,7 @@ gem 'cvss-suite', '~> 3.0.1', require: 'cvss_suite' # rubocop:todo Gemfile/Missi
gem 'arr-pm', '~> 0.0.12' # rubocop:todo Gemfile/MissingFeatureCategory
# Remote Development
-gem 'devfile', '~> 0.0.23.pre.alpha1' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'devfile', '~> 0.0.24.pre.alpha1', feature_category: :remote_development
# Apple plist parsing
gem 'CFPropertyList', '~> 3.0.0' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index af51bee0f08..8b2da2ba7c8 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -111,9 +111,9 @@
{"name":"deprecation_toolkit","version":"1.5.1","platform":"ruby","checksum":"a8a1ab1a19ae40ea12560b65010e099f3459ebde390b76621ef0c21c516a04ba"},
{"name":"derailed_benchmarks","version":"2.1.2","platform":"ruby","checksum":"eaadc6206ceeb5538ff8f5e04a0023d54ebdd95d04f33e8960fb95a5f189a14f"},
{"name":"descendants_tracker","version":"0.0.4","platform":"ruby","checksum":"e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897"},
-{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"arm64-darwin","checksum":"eec9ed97436cd5e9d456e270da979faeecbdeef42ee75ef9b39b45001c2399fb"},
-{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"ruby","checksum":"fba2c679cbafb03da153f73f55a346ae01f4921383575e1f7cda269e7e67e40a"},
-{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"x86_64-linux","checksum":"30e31b39599b7823673f5386f8bf19b7cb2b959c7f34a16704893db437d42094"},
+{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"arm64-darwin","checksum":"4954bf498772dbf534da0638bc59023234fed7423c72c85f21b6504ee4c65482"},
+{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"ruby","checksum":"72bbfc26edb519902d5c68e07188e0a3d699a1866392fa1497e5b7f3abb36600"},
+{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"x86_64-linux","checksum":"d121b1094aa3a24c29592a83c629ee640920e0196711dd06f27b6fa9b1ced609"},
{"name":"device_detector","version":"1.0.0","platform":"ruby","checksum":"b800fb3150b00c23e87b6768011808ac1771fffaae74c3238ebaf2b782947a7d"},
{"name":"devise","version":"4.8.1","platform":"ruby","checksum":"fdd48bbe79a89e7c1152236a70479842ede48bea4fa7f4f2d8da1f872559803e"},
{"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 9f2b7608041..6f94a4f275e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -442,7 +442,7 @@ GEM
thor (>= 0.19, < 2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devfile (0.0.23.pre.alpha1)
+ devfile (0.0.24.pre.alpha1)
device_detector (1.0.0)
devise (4.8.1)
bcrypt (~> 3.0)
@@ -1797,7 +1797,7 @@ DEPENDENCIES
declarative_policy (~> 1.1.0)
deprecation_toolkit (~> 1.5.1)
derailed_benchmarks
- devfile (~> 0.0.23.pre.alpha1)
+ devfile (~> 0.0.24.pre.alpha1)
device_detector
devise (~> 4.8.1)
devise-pbkdf2-encryptable (~> 0.0.0)!
diff --git a/app/services/bulk_imports/batched_relation_export_service.rb b/app/services/bulk_imports/batched_relation_export_service.rb
index 778510f2e35..c7c01c80fbf 100644
--- a/app/services/bulk_imports/batched_relation_export_service.rb
+++ b/app/services/bulk_imports/batched_relation_export_service.rb
@@ -26,8 +26,6 @@ module BulkImports
start_export!
export.batches.destroy_all # rubocop: disable Cop/DestroyAll
enqueue_batch_exports
- rescue StandardError => e
- fail_export!(e)
ensure
FinishBatchedRelationExportWorker.perform_async(export.id)
end
@@ -81,11 +79,5 @@ module BulkImports
def find_or_create_batch(batch_number)
export.batches.find_or_create_by!(batch_number: batch_number) # rubocop:disable CodeReuse/ActiveRecord
end
-
- def fail_export!(exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- export.update!(status_event: 'fail_op', error: exception.message.truncate(255))
- end
end
end
diff --git a/app/services/bulk_imports/relation_batch_export_service.rb b/app/services/bulk_imports/relation_batch_export_service.rb
index 20795de54f9..3f98d49aa1b 100644
--- a/app/services/bulk_imports/relation_batch_export_service.rb
+++ b/app/services/bulk_imports/relation_batch_export_service.rb
@@ -19,8 +19,6 @@ module BulkImports
upload_compressed_file
finish_batch!
- rescue StandardError => e
- fail_batch!(e)
ensure
FileUtils.remove_entry(export_path)
end
@@ -72,12 +70,6 @@ module BulkImports
batch.update!(status_event: 'finish', objects_count: exported_objects_count, error: nil)
end
- def fail_batch!(exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
- end
-
def exported_filepath
File.join(export_path, exported_filename)
end
diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb
index 91640496440..6db5ef3e9ec 100644
--- a/app/services/bulk_imports/relation_export_service.rb
+++ b/app/services/bulk_imports/relation_export_service.rb
@@ -42,8 +42,6 @@ module BulkImports
yield export
finish_export!(export)
- rescue StandardError => e
- fail_export!(export, e)
end
def export_service
@@ -87,12 +85,6 @@ module BulkImports
export.update!(status_event: 'finish', batched: false, error: nil)
end
- def fail_export!(export, exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- export&.update(status_event: 'fail_op', error: exception.class, batched: false)
- end
-
def exported_filepath
File.join(export_path, export_service.exported_filename)
end
diff --git a/app/services/service_desk/custom_email_verifications/update_service.rb b/app/services/service_desk/custom_email_verifications/update_service.rb
index 5ef36ce0576..fbd217e3a3e 100644
--- a/app/services/service_desk/custom_email_verifications/update_service.rb
+++ b/app/services/service_desk/custom_email_verifications/update_service.rb
@@ -8,7 +8,7 @@ module ServiceDesk
def execute
return error_feature_flag_disabled unless Feature.enabled?(:service_desk_custom_email, project)
return error_parameter_missing if settings.blank? || verification.blank?
- return error_already_finished if already_finished_and_no_mail?
+ return error_already_finished if verification.finished?
return error_already_failed if already_failed_and_no_mail?
verification_error = verify
@@ -39,10 +39,6 @@ module ServiceDesk
@verification ||= settings.custom_email_verification
end
- def already_finished_and_no_mail?
- verification.finished? && mail.blank?
- end
-
def already_failed_and_no_mail?
verification.failed? && mail.blank?
end
diff --git a/app/workers/bulk_imports/relation_batch_export_worker.rb b/app/workers/bulk_imports/relation_batch_export_worker.rb
index b3acf3ee01b..87ceb775075 100644
--- a/app/workers/bulk_imports/relation_batch_export_worker.rb
+++ b/app/workers/bulk_imports/relation_batch_export_worker.rb
@@ -7,7 +7,16 @@ module BulkImports
idempotent!
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
feature_category :importers
- sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+ sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3
+
+ sidekiq_retries_exhausted do |job, exception|
+ batch = BulkImports::ExportBatch.find(job['args'][1])
+ portable = batch.export.portable
+
+ Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
+
+ batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
+ end
def perform(user_id, batch_id)
@user = User.find(user_id)
diff --git a/app/workers/bulk_imports/relation_export_worker.rb b/app/workers/bulk_imports/relation_export_worker.rb
index 09600b9363a..168626fee85 100644
--- a/app/workers/bulk_imports/relation_export_worker.rb
+++ b/app/workers/bulk_imports/relation_export_worker.rb
@@ -10,12 +10,27 @@ module BulkImports
loggable_arguments 2, 3
data_consistency :always
feature_category :importers
- sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+ sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3
worker_resource_boundary :memory
+ sidekiq_retries_exhausted do |job, exception|
+ _user_id, portable_id, portable_type, relation, batched = job['args']
+ portable = portable(portable_id, portable_type)
+
+ export = portable.bulk_import_exports.find_by_relation(relation)
+
+ Gitlab::ErrorTracking.track_exception(exception, portable_id: portable_id, portable_type: portable.class.name)
+
+ export.update!(status_event: 'fail_op', error: exception.message.truncate(255), batched: batched)
+ end
+
+ def self.portable(portable_id, portable_class)
+ portable_class.classify.constantize.find(portable_id)
+ end
+
def perform(user_id, portable_id, portable_class, relation, batched = false)
user = User.find(user_id)
- portable = portable(portable_id, portable_class)
+ portable = self.class.portable(portable_id, portable_class)
config = BulkImports::FileTransfer.config_for(portable)
log_extra_metadata_on_done(:relation, relation)
@@ -27,11 +42,5 @@ module BulkImports
RelationExportService.new(user, portable, relation, jid).execute
end
end
-
- private
-
- def portable(portable_id, portable_class)
- portable_class.classify.constantize.find(portable_id)
- end
end
end
diff --git a/config/feature_flags/development/oidc_issuer_url.yml b/config/feature_flags/development/oidc_issuer_url.yml
new file mode 100644
index 00000000000..e919d1095d1
--- /dev/null
+++ b/config/feature_flags/development/oidc_issuer_url.yml
@@ -0,0 +1,8 @@
+---
+name: oidc_issuer_url
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135049
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429855
+milestone: '16.6'
+type: development
+group: group::pipeline security
+default_enabled: false
diff --git a/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb b/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb
new file mode 100644
index 00000000000..af589f6337a
--- /dev/null
+++ b/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class InsertNewUltimateTrialPlanIntoPlans < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ execute <<~SQL
+ INSERT INTO plans (name, title, created_at, updated_at)
+ VALUES ('ultimate_trial_paid_customer', 'Ultimate Trial for Paid Customer', current_timestamp, current_timestamp)
+ SQL
+ end
+
+ def down
+ # NOTE: We have a uniqueness constraint for the 'name' column in 'plans'
+ execute <<~SQL
+ DELETE FROM plans
+ WHERE name = 'ultimate_trial_paid_customer'
+ SQL
+ end
+end
diff --git a/db/schema_migrations/20231030154117 b/db/schema_migrations/20231030154117
new file mode 100644
index 00000000000..5380cfa5252
--- /dev/null
+++ b/db/schema_migrations/20231030154117
@@ -0,0 +1 @@
+07c4a447b3888046333b0b8fa237411783fc031ea9943520f716ea0c00ed964f \ No newline at end of file
diff --git a/doc/administration/audit_event_streaming/index.md b/doc/administration/audit_event_streaming/index.md
index 455ddaa5e06..09474db1e08 100644
--- a/doc/administration/audit_event_streaming/index.md
+++ b/doc/administration/audit_event_streaming/index.md
@@ -261,7 +261,11 @@ To delete Google Cloud Logging streaming destinations to a top-level group:
### AWS S3 destinations
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132603) in GitLab 16.6.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132603) in GitLab 16.6 [with a flag](../feature_flags.md) named `allow_streaming_audit_events_to_amazon_s3`. Enabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is available. To hide the feature per group, an administrator can [disable the feature flag](../feature_flags.md) named `allow_streaming_audit_events_to_amazon_s3`.
+On GitLab.com, this feature is available.
Manage AWS S3 destinations for top-level groups.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 91a1460df18..9ef51601468 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -13951,6 +13951,18 @@ Represents the YAML definitions for audit events defined in `ee/config/audit_eve
| <a id="auditeventdefinitionsavedtodatabase"></a>`savedToDatabase` | [`Boolean!`](#boolean) | Indicates if the event is saved to PostgreSQL database. |
| <a id="auditeventdefinitionstreamed"></a>`streamed` | [`Boolean!`](#boolean) | Indicates if the event is streamed to an external destination. |
+### `AuditEventStreamingHTTPNamespaceFilter`
+
+Represents a subgroup or project filter that belongs to an external audit event streaming destination.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="auditeventstreaminghttpnamespacefilterexternalauditeventdestination"></a>`externalAuditEventDestination` | [`ExternalAuditEventDestination!`](#externalauditeventdestination) | Destination to which the filter belongs. |
+| <a id="auditeventstreaminghttpnamespacefilterid"></a>`id` | [`ID!`](#id) | ID of the filter. |
+| <a id="auditeventstreaminghttpnamespacefilternamespace"></a>`namespace` | [`Namespace!`](#namespace) | Group or project namespace the filter belongs to. |
+
### `AuditEventStreamingHeader`
Represents a HTTP header key/value that belongs to an audit streaming destination.
@@ -17621,6 +17633,7 @@ Represents an external resource to send audit events to.
| <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. (see [Connections](#connections)) |
| <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
| <a id="externalauditeventdestinationname"></a>`name` | [`String!`](#string) | Name of the external destination to send audit events to. |
+| <a id="externalauditeventdestinationnamespacefilter"></a>`namespaceFilter` | [`AuditEventStreamingHTTPNamespaceFilter`](#auditeventstreaminghttpnamespacefilter) | List of subgroup or project filters for the destination. |
| <a id="externalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. |
### `ExternalIssue`
diff --git a/doc/subscriptions/bronze_starter.md b/doc/subscriptions/bronze_starter.md
index 3b2ef601136..90e0e77cb9a 100644
--- a/doc/subscriptions/bronze_starter.md
+++ b/doc/subscriptions/bronze_starter.md
@@ -106,7 +106,7 @@ the tiers are no longer mentioned in GitLab documentation:
- [Filtering merge requests](../user/project/merge_requests/index.md#filter-the-list-of-merge-requests) by "approved by"
- [Advanced search (Elasticsearch)](../user/search/advanced_search.md)
- [Service Desk](../user/project/service_desk/index.md)
-- [Storage usage statistics](../user/usage_quotas.md#storage-usage-statistics)
+- [Storage usage statistics](../user/usage_quotas.md)
The following developer features continue to be available to Starter and
Bronze-level subscribers:
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index f3fca8f2def..9b0dbbc5f0e 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -7,11 +7,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container Scanning **(FREE ALL)**
-> - Improved support for FIPS [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263482) in GitLab 13.6 by upgrading `CS_MAJOR_VERSION` from `2` to `3`.
-> - Integration with Trivy [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322656) in GitLab 13.9 by upgrading `CS_MAJOR_VERSION` from `3` to `4`.
-> - Integration with Clair [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/321451) in GitLab 13.9.
-> - Default container scanning with Trivy [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61850) in GitLab 14.0.
-> - Integration with Grype as an alternative scanner [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326279) in GitLab 14.0.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86092) the major analyzer version from `4` to `5` in GitLab 15.0.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86783) from GitLab Ultimate to GitLab Free in 15.0.
> - Container Scanning variables that reference Docker [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/357264) in GitLab 15.4.
@@ -61,21 +56,21 @@ information directly in the merge request.
| Capability | In Free and Premium | In Ultimate |
| --- | ------ | ------ |
-| [Configure Scanners](#configuration) | Yes | Yes |
-| Customize Settings ([Variables](#available-cicd-variables), [Overriding](#overriding-the-container-scanning-template), [offline environment support](#running-container-scanning-in-an-offline-environment), etc) | Yes | Yes |
-| [View JSON Report](#reports-json-format) as a CI job artifact | Yes | Yes |
-| Generation of a JSON report of [dependencies](#dependency-list) as a CI job artifact | Yes | Yes |
-| Ability to enable container scanning via an MR in the GitLab UI | Yes | Yes |
-| [UBI Image Support](#fips-enabled-images) | Yes | Yes |
-| Support for Trivy | Yes | Yes |
-| Support for Grype | Yes | Yes |
+| [Configure Scanners](#configuration) | **{check-circle}** Yes | **{check-circle}** Yes |
+| Customize Settings ([Variables](#available-cicd-variables), [Overriding](#overriding-the-container-scanning-template), [offline environment support](#running-container-scanning-in-an-offline-environment), etc) | **{check-circle}** Yes | **{check-circle}** Yes |
+| [View JSON Report](#reports-json-format) as a CI job artifact | **{check-circle}** Yes | **{check-circle}** Yes |
+| Generation of a JSON report of [dependencies](#dependency-list) as a CI job artifact | **{check-circle}** Yes | **{check-circle}** Yes |
+| Ability to enable container scanning via an MR in the GitLab UI | **{check-circle}** Yes | **{check-circle}** Yes |
+| [UBI Image Support](#fips-enabled-images) | **{check-circle}** Yes | **{check-circle}** Yes |
+| Support for Trivy | **{check-circle}** Yes | **{check-circle}** Yes |
+| Support for Grype | **{check-circle}** Yes | **{check-circle}** Yes |
| Inclusion of GitLab Advisory Database | Limited to the time-delayed content from GitLab [advisories-communities](https://gitlab.com/gitlab-org/advisories-community/) project | Yes - all the latest content from [Gemnasium DB](https://gitlab.com/gitlab-org/security-products/gemnasium-db) |
-| Presentation of Report data in Merge Request and Security tab of the CI pipeline job | No | Yes |
-| [Interaction with Vulnerabilities](#interacting-with-the-vulnerabilities) such as merge request approvals | No | Yes |
-| [Solutions for vulnerabilities (auto-remediation)](#solutions-for-vulnerabilities-auto-remediation) | No | Yes |
-| Support for the [vulnerability allow list](#vulnerability-allowlisting) | No | Yes |
-| [Access to Security Dashboard page](#security-dashboard) | No | Yes |
-| [Access to Dependency List page](../dependency_list/index.md) | No | Yes |
+| Presentation of Report data in Merge Request and Security tab of the CI pipeline job | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Interaction with Vulnerabilities](#interacting-with-the-vulnerabilities) such as merge request approvals | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Solutions for vulnerabilities (auto-remediation)](#solutions-for-vulnerabilities-auto-remediation) | **{dotted-circle}** No | **{check-circle}** Yes |
+| Support for the [vulnerability allow list](#vulnerability-allowlisting) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Access to Security Dashboard page](#security-dashboard) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Access to Dependency List page](../dependency_list/index.md) | **{dotted-circle}** No | **{check-circle}** Yes |
## Prerequisites
@@ -278,28 +273,28 @@ including a large number of false positives.
| `CS_DOCKERFILE_PATH` | `Dockerfile` | The path to the `Dockerfile` to use for generating remediations. By default, the scanner looks for a file named `Dockerfile` in the root directory of the project. You should configure this variable only if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | All |
| `CS_QUIET` | `""` | If set, this variable disables output of the [vulnerabilities table](#container-scanning-job-log-format) in the job log. [Introduced](https://gitlab.com/gitlab-org/security-products/analyzers/container-scanning/-/merge_requests/50) in GitLab 15.1. | All |
| `CS_TRIVY_JAVA_DB` | `"ghcr.io/aquasecurity/trivy-java-db"` | Specify an alternate location for the [trivy-java-db](https://github.com/aquasecurity/trivy-java-db) vulnerability database. | Trivy |
-| `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10880) in GitLab 13.1. | All |
+| `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. | All |
### Supported distributions
Support depends on which scanner is used:
-| Distribution | Grype | Trivy |
-| -------------- | ----- | ----- |
-| Alma Linux | | ✅ |
-| Alpine Linux | ✅ | ✅ |
-| Amazon Linux | ✅ | ✅ |
-| BusyBox | ✅ | |
-| CentOS | ✅ | ✅ |
-| CBL-Mariner | | ✅ |
-| Debian | ✅ | ✅ |
-| Distroless | ✅ | ✅ |
-| Oracle Linux | ✅ | ✅ |
-| Photon OS | | ✅ |
-| Red Hat (RHEL) | ✅ | ✅ |
-| Rocky Linux | | ✅ |
-| SUSE | | ✅ |
-| Ubuntu | ✅ | ✅ |
+| Distribution | Grype | Trivy |
+|----------------|------------------------|------------------------|
+| Alma Linux | **{dotted-circle}** No | **{check-circle}** Yes |
+| Alpine Linux | **{check-circle}** Yes | **{check-circle}** Yes |
+| Amazon Linux | **{check-circle}** Yes | **{check-circle}** Yes |
+| BusyBox | **{check-circle}** Yes | **{dotted-circle}** No |
+| CentOS | **{check-circle}** Yes | **{check-circle}** Yes |
+| CBL-Mariner | **{dotted-circle}** No | **{check-circle}** Yes |
+| Debian | **{check-circle}** Yes | **{check-circle}** Yes |
+| Distroless | **{check-circle}** Yes | **{check-circle}** Yes |
+| Oracle Linux | **{check-circle}** Yes | **{check-circle}** Yes |
+| Photon OS | **{dotted-circle}** No | **{check-circle}** Yes |
+| Red Hat (RHEL) | **{check-circle}** Yes | **{check-circle}** Yes |
+| Rocky Linux | **{dotted-circle}** No | **{check-circle}** Yes |
+| SUSE | **{dotted-circle}** No | **{check-circle}** Yes |
+| Ubuntu | **{check-circle}** Yes | **{check-circle}** Yes |
#### FIPS-enabled images
@@ -747,24 +742,24 @@ All analyzer images are [updated daily](https://gitlab.com/gitlab-org/security-p
The images use data from upstream advisory databases depending on which scanner is used:
-| Data Source | Trivy | Grype |
-| ------------------------------ | ----- | ----- |
-| AlmaLinux Security Advisory | ✅ | ✅ |
-| Amazon Linux Security Center | ✅ | ✅ |
-| Arch Linux Security Tracker | ✅ | |
-| SUSE CVRF | ✅ | ✅ |
-| CWE Advisories | ✅ | |
-| Debian Security Bug Tracker | ✅ | ✅ |
-| GitHub Security Advisory | ✅ | ✅ |
-| Go Vulnerability Database | ✅ | |
-| CBL-Mariner Vulnerability Data | ✅ | |
-| NVD | ✅ | ✅ |
-| OSV | ✅ | |
-| Red Hat OVAL v2 | ✅ | ✅ |
-| Red Hat Security Data API | ✅ | ✅ |
-| Photon Security Advisories | ✅ | |
-| Rocky Linux UpdateInfo | ✅ | |
-| Ubuntu CVE Tracker (only data sources from mid 2021 and later) | ✅ | ✅ |
+| Data Source | Trivy | Grype |
+|----------------------------------------------------------------|------------------------|------------------------|
+| AlmaLinux Security Advisory | **{check-circle}** Yes | **{check-circle}** Yes |
+| Amazon Linux Security Center | **{check-circle}** Yes | **{check-circle}** Yes |
+| Arch Linux Security Tracker | **{check-circle}** Yes | **{dotted-circle}** No |
+| SUSE CVRF | **{check-circle}** Yes | **{check-circle}** Yes |
+| CWE Advisories | **{check-circle}** Yes | **{dotted-circle}** No |
+| Debian Security Bug Tracker | **{check-circle}** Yes | **{check-circle}** Yes |
+| GitHub Security Advisory | **{check-circle}** Yes | **{check-circle}** Yes |
+| Go Vulnerability Database | **{check-circle}** Yes | **{dotted-circle}** No |
+| CBL-Mariner Vulnerability Data | **{check-circle}** Yes | **{dotted-circle}** No |
+| NVD | **{check-circle}** Yes | **{check-circle}** Yes |
+| OSV | **{check-circle}** Yes | **{dotted-circle}** No |
+| Red Hat OVAL v2 | **{check-circle}** Yes | **{check-circle}** Yes |
+| Red Hat Security Data API | **{check-circle}** Yes | **{check-circle}** Yes |
+| Photon Security Advisories | **{check-circle}** Yes | **{dotted-circle}** No |
+| Rocky Linux UpdateInfo | **{check-circle}** Yes | **{dotted-circle}** No |
+| Ubuntu CVE Tracker (only data sources from mid 2021 and later) | **{check-circle}** Yes | **{check-circle}** Yes |
In addition to the sources provided by these scanners, GitLab maintains the following vulnerability databases:
diff --git a/doc/user/packages/container_registry/reduce_container_registry_storage.md b/doc/user/packages/container_registry/reduce_container_registry_storage.md
index 2af16dcc85a..8c4f25af2e1 100644
--- a/doc/user/packages/container_registry/reduce_container_registry_storage.md
+++ b/doc/user/packages/container_registry/reduce_container_registry_storage.md
@@ -15,14 +15,61 @@ if you add a large number of images or tags:
You should delete unnecessary images and tags and set up a [cleanup policy](#cleanup-policy)
to automatically manage your container registry usage.
-## Check Container Registry storage use
+## Check Container Registry storage use **(FREE SAAS)**
The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages.
-This page includes the [Container Registry usage](../../usage_quotas.md#container-registry-usage), which is only available on GitLab.com.
Measuring usage is only possible on the new version of the GitLab Container Registry backed by a
metadata database, which is [available on GitLab.com](https://gitlab.com/groups/gitlab-org/-/epics/5523) since GitLab 15.7.
For information on the planned availability for self-managed instances, see [epic 5521](https://gitlab.com/groups/gitlab-org/-/epics/5521).
+## How container registry usage is calculated
+
+Image layers stored in the Container Registry are deduplicated at the root namespace level.
+
+An image is only counted once if:
+
+- You tag the same image more than once in the same repository.
+- You tag the same image across distinct repositories under the same root namespace.
+
+An image layer is only counted once if:
+
+- You share the image layer across multiple images in the same container repository, project, or group.
+- You share the image layer across different repositories.
+
+Only layers that are referenced by tagged images are accounted for. Untagged images and any layers
+referenced exclusively by them are subject to [online garbage collection](../container_registry/delete_container_registry_images.md#garbage-collection).
+Untagged image layers are automatically deleted after 24 hours if they remain unreferenced during that period.
+
+Image layers are stored on the storage backend in the original (usually compressed) format. This
+means that the measured size for any given image layer should match the size displayed on the
+corresponding [image manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest).
+
+Namespace usage is refreshed a few minutes after a tag is pushed or deleted from any container repository under the namespace.
+
+### Delayed refresh
+
+It is not possible to calculate container registry usage
+with maximum precision in real time for extremely large namespaces (about 1% of namespaces).
+To enable maintainers of these namespaces to see their usage, there is a delayed fallback mechanism.
+See [epic 9413](https://gitlab.com/groups/gitlab-org/-/epics/9413) for more details.
+
+If the usage for a namespace cannot be calculated with precision, GitLab falls back to the delayed method.
+In the delayed method, the displayed usage size is the sum of **all** unique image layers
+in the namespace. Untagged image layers are not ignored. As a result,
+the displayed usage size might not change significantly after deleting tags. Instead,
+the size value only changes when:
+
+- An automated [garbage collection process](../container_registry/delete_container_registry_images.md#garbage-collection)
+ runs and deletes untagged image layers. After a user deletes a tag, a garbage collection run
+ is scheduled to start 24 hours later. During that run, images that were previously tagged
+ are analyzed and their layers deleted if not referenced by any other tagged image.
+ If any layers are deleted, the namespace usage is updated.
+- The namespace's registry usage shrinks enough that GitLab can measure it with maximum precision.
+ As usage for namespaces shrinks to be under the [limits](../../../user/usage_quotas.md#namespace-storage-limit),
+ the measurement switches automatically from delayed to precise usage measurement.
+ There is no place in the UI to determine which measurement method is being used,
+ but [issue 386468](https://gitlab.com/gitlab-org/gitlab/-/issues/386468) proposes to improve this.
+
## Cleanup policy
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/218737) from "expiration policy" to "cleanup policy" in GitLab 13.2.
diff --git a/doc/user/read_only_namespaces.md b/doc/user/read_only_namespaces.md
index 5b302d976dd..d5697ec5a94 100644
--- a/doc/user/read_only_namespaces.md
+++ b/doc/user/read_only_namespaces.md
@@ -27,7 +27,7 @@ To restore a namespace to its standard state, you can:
- [Purchase a paid tier](https://about.gitlab.com/pricing/).
- For exceeded storage quota:
- [Purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
- - [Manage your storage usage](usage_quotas.md#manage-your-storage-usage).
+ - [Manage your storage usage](usage_quotas.md#manage-storage-usage).
## Restricted actions
diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md
index 305a46e1f15..7dea2b97249 100644
--- a/doc/user/usage_quotas.md
+++ b/doc/user/usage_quotas.md
@@ -5,7 +5,7 @@ group: Utilization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Storage usage quota **(FREE ALL)**
+# Storage **(FREE ALL)**
Storage usage statistics are available for projects and namespaces. You can use that information to
manage storage usage within the applicable quotas.
@@ -13,8 +13,8 @@ manage storage usage within the applicable quotas.
Statistics include:
- Storage usage across projects in a namespace.
-- Storage usage that exceeds the storage quota.
-- Available purchased storage.
+- Storage usage that exceeds the storage SaaS limit or [self-managed storage quota](../administration/settings/account_and_limit_settings.md#repository-size-limit).
+- Available purchased storage for SaaS.
Storage and network usage are calculated with the binary measurement system (1024 unit multiples).
Storage usage is displayed in kibibytes (KiB), mebibytes (MiB),
@@ -30,87 +30,33 @@ you might see references to `KB`, `MB`, and `GB` in the UI and documentation.
Prerequisites:
- To view storage usage for a project, you must have at least the Maintainer role for the project or Owner role for the namespace.
-- To view storage usage for a namespace, you must have the Owner role for the namespace.
+- To view storage usage for a group namespace, you must have the Owner role for the namespace.
1. On the left sidebar, select **Search or go to** and find your project or group.
1. On the left sidebar, select **Settings > Usage Quotas**.
-1. Select the **Storage** tab.
+1. Select the **Storage** tab to see namespace storage usage.
+1. To view storage usage for a project, select one of the projects from the table at the bottom of the **Storage** tab of the **Usage Quotas** page.
-Select any title to view details. The information on this page
-is updated every 90 minutes.
+The information on the **Usage Quotas** page is updated every 90 minutes.
If your namespace shows `'Not applicable.'`, push a commit to any project in the
namespace to recalculate the storage.
-### Container Registry usage **(FREE SAAS)**
+### View project fork storage usage **(FREE SAAS)**
-Container Registry usage is available only for GitLab.com. This feature requires a
-[new version](https://about.gitlab.com/blog/2022/04/12/next-generation-container-registry/)
-of the GitLab Container Registry. To learn about the proposed release for self-managed
-installations, see [epic 5521](https://gitlab.com/groups/gitlab-org/-/epics/5521).
-
-#### How container registry usage is calculated
-
-Image layers stored in the Container Registry are deduplicated at the root namespace level.
-
-An image is only counted once if:
-
-- You tag the same image more than once in the same repository.
-- You tag the same image across distinct repositories under the same root namespace.
-
-An image layer is only counted once if:
-
-- You share the image layer across multiple images in the same container repository, project, or group.
-- You share the image layer across different repositories.
-
-Only layers that are referenced by tagged images are accounted for. Untagged images and any layers
-referenced exclusively by them are subject to [online garbage collection](packages/container_registry/delete_container_registry_images.md#garbage-collection).
-Untagged image layers are automatically deleted after 24 hours if they remain unreferenced during that period.
-
-Image layers are stored on the storage backend in the original (usually compressed) format. This
-means that the measured size for any given image layer should match the size displayed on the
-corresponding [image manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest).
-
-Namespace usage is refreshed a few minutes after a tag is pushed or deleted from any container repository under the namespace.
-
-#### Delayed refresh
-
-It is not possible to calculate [container registry usage](#container-registry-usage)
-with maximum precision in real time for extremely large namespaces (about 1% of namespaces).
-To enable maintainers of these namespaces to see their usage, there is a delayed fallback mechanism.
-See [epic 9413](https://gitlab.com/groups/gitlab-org/-/epics/9413) for more details.
-
-If the usage for a namespace cannot be calculated with precision, GitLab falls back to the delayed method.
-In the delayed method, the displayed usage size is the sum of **all** unique image layers
-in the namespace. Untagged image layers are not ignored. As a result,
-the displayed usage size might not change significantly after deleting tags. Instead,
-the size value only changes when:
-
-- An automated [garbage collection process](packages/container_registry/delete_container_registry_images.md#garbage-collection)
- runs and deletes untagged image layers. After a user deletes a tag, a garbage collection run
- is scheduled to start 24 hours later. During that run, images that were previously tagged
- are analyzed and their layers deleted if not referenced by any other tagged image.
- If any layers are deleted, the namespace usage is updated.
-- The namespace's registry usage shrinks enough that GitLab can measure it with maximum precision.
- As usage for namespaces shrinks to be under the [limits](#namespace-storage-limit),
- the measurement switches automatically from delayed to precise usage measurement.
- There is no place in the UI to determine which measurement method is being used,
- but [issue 386468](https://gitlab.com/gitlab-org/gitlab/-/issues/386468) proposes to improve this.
+A cost factor is applied to the storage consumed by project forks so that forks consume less namespace storage than their actual size.
-### Storage usage statistics
+To view the amount of namespace storage the fork has used:
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68898) project-level graph in GitLab 14.4 [with a flag](../administration/feature_flags.md) named `project_storage_ui`. Disabled by default.
-> - Enabled on GitLab.com in GitLab 14.4.
-> - Enabled on self-managed in GitLab 14.5.
-> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71270) in GitLab 14.5.
+1. On the left sidebar, select **Search or go to** and find your project or group.
+1. On the left sidebar, select **Settings > Usage Quotas**.
+1. Select the **Storage** tab. The **Total** column displays the amount of namespace storage used by the fork as a portion of the actual size of the fork on disk.
-The following storage usage statistics are available to a maintainer:
+The cost factor applies to the project repository, LFS objects, job artifacts, packages, snippets, and the wiki.
-- Total namespace storage used: Total amount of storage used across projects in this namespace.
-- Total excess storage used: Total amount of storage used that exceeds their allocated storage.
-- Purchased storage available: Total storage that has been purchased but is not yet used.
+The cost factor does not apply to private forks in namespaces on the Free plan.
-## Manage your storage usage
+## Manage storage usage
To manage your storage, if you are a namespace Owner you can [purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
@@ -126,14 +72,16 @@ Depending on your role, you can also use the following methods to manage or redu
To automate storage usage analysis and management, see the [storage management automation](storage_management_automation.md) documentation.
-## Manage your transfer usage
+## Set usage quotas **(FREE SELF)**
+
+There are no application limits on the amount of storage and transfer for self-managed instances. The administrators are responsible for the underlying infrastructure costs. Administrators can set [repository size limits](../administration/settings/account_and_limit_settings.md#repository-size-limit) to manage your repositories’ size.
-Depending on your role, to manage your transfer usage you can [reduce Container Registry data transfers](packages/container_registry/reduce_container_registry_data_transfer.md).
+## Storage limits **(FREE SAAS)**
-## Project storage limit
+### Project storage limit
-Projects on GitLab SaaS have a 10 GiB storage limit on their Git repository and LFS storage.
-After namespace-level storage limits are applied, the project limit is removed. A namespace has either a namespace-level storage limit or a project-level storage limit, but not both.
+Projects on GitLab SaaS have a 10 GiB storage limit on their Git repository and LFS storage. Limits on project storage
+will be removed before limits are applied to GitLab SaaS namespace storage in the future.
When a project's repository and LFS reaches the quota, the project is set to a read-only state.
You cannot push changes to a read-only project. To monitor the size of each
@@ -141,7 +89,7 @@ repository in a namespace, including a breakdown for each project,
[view storage usage](#view-storage-usage). To allow a project's repository and LFS to exceed the free quota
you must purchase additional storage. For more details, see [Excess storage usage](#excess-storage-usage).
-### Excess storage usage
+#### Excess storage usage
Excess storage usage is the amount that a project's repository and LFS exceeds the [project storage limit](#project-storage-limit). If no
purchased storage is available the project is set to a read-only state. You cannot push changes to a read-only project.
@@ -185,12 +133,19 @@ available decreases. All projects no longer have the read-only status because 40
| Yellow | 5 GiB | 0 GiB | 10 GiB | Not read-only |
| **Totals** | **45 GiB** | **10 GiB** | - | - |
-## Namespace storage limit
+### Namespace storage limit **(FREE SAAS)**
-Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
+GitLab plans to enforce a storage limit for namespaces on GitLab SaaS. For more information, see
+the FAQs for the following tiers:
-After namespace storage limits are enforced, view them in the **Usage quotas** page.
-For more information about the namespace storage limit enforcement, see the FAQ pages for the [Free](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier) and [Paid](https://about.gitlab.com/pricing/faq-paid-storage-transfer/) tiers.
+- [Free tier](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier).
+- [Premium and Ultimate](https://about.gitlab.com/pricing/faq-paid-storage-transfer/).
+
+Namespaces on GitLab SaaS have a [10 GiB project limit](#project-storage-limit) with a soft limit on
+namespace storage. Soft storage limits are limits that have not yet been enforced by GitLab, and will become
+hard limits after namespace storage limits apply. To avoid your namespace from becoming
+[read-only](../user/read_only_namespaces.md) after namespace storage limits apply,
+you should ensure that your namespace storage adheres to the soft storage limit.
Namespace storage limits do not apply to self-managed deployments, but administrators can [manage the repository size](../administration/settings/account_and_limit_settings.md#repository-size-limit).
@@ -209,13 +164,13 @@ If your total namespace storage exceeds the available namespace storage quota, a
To notify you that you have nearly exceeded your namespace storage quota:
-- In the command-line interface, a notification displays after each `git push` action when you've reached 95% and 100% of your namespace storage quota.
-- In the GitLab UI, a notification displays when you've reached 75%, 95%, and 100% of your namespace storage quota.
+- In the command-line interface, a notification displays after each `git push` action when your namespace has reached between 95% and 100%+ of your namespace storage quota.
+- In the GitLab UI, a notification displays when your namespace has reached between 75% and 100%+ of your namespace storage quota.
- GitLab sends an email to members with the Owner role to notify them when namespace storage usage is at 70%, 85%, 95%, and 100%.
To prevent exceeding the namespace storage limit, you can:
-- [Manage your storage usage](#manage-your-storage-usage).
+- [Manage your storage usage](#manage-storage-usage).
- If you meet the eligibility requirements, you can apply for:
- [GitLab for Education](https://about.gitlab.com/solutions/education/join/)
- [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/)
@@ -225,16 +180,8 @@ To prevent exceeding the namespace storage limit, you can:
- [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing/), which include higher limits and features to enable growing teams to ship faster without sacrificing on quality.
- [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) for more information about your options.
-### View project fork storage usage
-
-A cost factor is applied to the storage consumed by project forks so that forks consume less namespace storage than their actual size.
-
-To view the amount of namespace storage the fork has used:
-
-1. On the left sidebar, select **Search or go to** and find your project or group.
-1. On the left sidebar, select **Settings > Usage Quotas**.
-1. Select the **Storage** tab. The **Total** column displays the amount of namespace storage used by the fork as a portion of the actual size of the fork on disk.
-
-The cost factor applies to the project repository, LFS objects, job artifacts, packages, snippets, and the wiki.
+## Related Topics
-The cost factor does not apply to private forks in namespaces on the Free plan.
+- [Automate storage management](storage_management_automation.md)
+- [Purchase storage and transfer](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer)
+- [Transfer usage](packages/container_registry/reduce_container_registry_data_transfer.md)
diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb
index 2c61b0fd236..90db9d13d85 100644
--- a/lib/gitlab/ci/jwt_v2.rb
+++ b/lib/gitlab/ci/jwt_v2.rb
@@ -25,7 +25,7 @@ module Gitlab
def reserved_claims
super.merge({
- iss: Settings.gitlab.base_url,
+ iss: Feature.enabled?(:oidc_issuer_url) ? Gitlab.config.gitlab.url : Settings.gitlab.base_url,
sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}",
aud: aud
}.compact)
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index 1ce0a44e37f..b486ddb8e76 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -8,7 +8,8 @@ module Gitlab
include ::Gitlab::Database::MigrationHelpers
include ::Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers
- ALLOWED_TABLES = %w[audit_events web_hook_logs].freeze
+ ALLOWED_TABLES = %w[audit_events web_hook_logs merge_request_diff_files merge_request_diff_commits].freeze
+
ERROR_SCOPE = 'table partitioning'
MIGRATION_CLASS_NAME = "::#{module_parent_name}::BackfillPartitionedTable"
@@ -16,6 +17,60 @@ module Gitlab
BATCH_INTERVAL = 2.minutes.freeze
BATCH_SIZE = 50_000
SUB_BATCH_SIZE = 2_500
+ PARTITION_BUFFER = 6
+ MIN_ID = 1
+
+ # Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a int/bigint column.
+ # One partition is created per partition_size between 1 and MAX(column_name). Also installs a trigger on
+ # the original table to copy writes into the partitioned table. To copy over historic data from before creation
+ # of the partitioned table, use the `enqueue_partitioning_data_migration` helper in a post-deploy migration.
+ # Note: If the original table is empty the system creates 6 partitions in the new table.
+ #
+ # A copy of the original table is required as PG currently does not support partitioning existing tables.
+ #
+ # Example:
+ #
+ # partition_table_by_int_range :merge_request_diff_commits, :merge_request_diff_id, partition_size: 500, primary_key: ['merge_request_diff_id', 'relative_order']
+ #
+ # Options are:
+ # :partition_size - a int specifying the partition size
+ # :primary_key - a array specifying the primary query of the new table
+ #
+ # Note: The system always adds a buffer of 6 partitions.
+ def partition_table_by_int_range(table_name, column_name, partition_size:, primary_key:)
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_ddl_mode!
+
+ assert_table_is_allowed(table_name)
+
+ assert_not_in_transaction_block(scope: ERROR_SCOPE)
+
+ current_primary_key = Array.wrap(connection.primary_key(table_name))
+ raise "primary key not defined for #{table_name}" if current_primary_key.blank?
+
+ partition_column = find_column_definition(table_name, column_name)
+ raise "partition column #{column_name} does not exist on #{table_name}" if partition_column.nil?
+
+ primary_key = Array.wrap(primary_key).map(&:to_s)
+ raise "the partition column must be part of the primary key" unless primary_key.include?(column_name.to_s)
+
+ primary_key_objects = connection.columns(table_name).select { |column| primary_key.include?(column.name) }
+
+ raise 'partition_size must be greater than 1' unless partition_size > 1
+
+ max_id = Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do
+ Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed do
+ define_batchable_model(table_name, connection: connection).maximum(column_name) || partition_size * PARTITION_BUFFER
+ end
+ end
+
+ partitioned_table_name = make_partitioned_table_name(table_name)
+
+ with_lock_retries do
+ create_range_id_partitioned_copy(table_name, partitioned_table_name, partition_column, primary_key_objects)
+ create_int_range_partitions(partitioned_table_name, partition_size, MIN_ID, max_id)
+ create_trigger_to_sync_tables(table_name, partitioned_table_name, current_primary_key)
+ end
+ end
# Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a timestamp column.
# One partition is created per month between the given `min_date` and `max_date`. Also installs a trigger on
@@ -332,6 +387,34 @@ module Gitlab
connection.columns(table).find { |c| c.name == column.to_s }
end
+ def create_range_id_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_keys)
+ if table_exists?(partitioned_table_name)
+ Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \
+ " (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} "
+ return
+ end
+
+ tmp_partitioning_column_name = "#{partition_column.name}_tmp"
+
+ temporary_columns = primary_keys.map { |key| "#{key.name}_tmp" }.join(", ")
+ temporary_columns_statement = build_temporary_columns_statement(primary_keys)
+
+ transaction do
+ execute(<<~SQL)
+ CREATE TABLE #{partitioned_table_name} (
+ LIKE #{source_table_name} INCLUDING ALL EXCLUDING INDEXES,
+ #{temporary_columns_statement},
+ PRIMARY KEY (#{temporary_columns})
+ ) PARTITION BY RANGE (#{tmp_partitioning_column_name})
+ SQL
+
+ primary_keys.each do |key|
+ remove_column(partitioned_table_name, key.name)
+ rename_column(partitioned_table_name, "#{key.name}_tmp", key.name)
+ end
+ end
+ end
+
def create_range_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_key)
if table_exists?(partitioned_table_name)
Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \
@@ -382,6 +465,20 @@ module Gitlab
end
end
+ def create_int_range_partitions(table_name, partition_size, min_id, max_id)
+ lower_bound = min_id
+ upper_bound = min_id + partition_size
+
+ end_id = max_id + PARTITION_BUFFER * partition_size # Adds a buffer of 6 partitions
+
+ while lower_bound < end_id
+ create_range_partition_safely("#{table_name}_#{lower_bound}", table_name, lower_bound, upper_bound)
+
+ lower_bound += partition_size
+ upper_bound += partition_size
+ end
+ end
+
def to_sql_date_literal(date)
connection.quote(date.strftime('%Y-%m-%d'))
end
@@ -411,19 +508,23 @@ module Gitlab
return
end
+ unique_key = Array.wrap(unique_key)
+
delimiter = ",\n "
column_names = connection.columns(partitioned_table_name).map(&:name)
set_statements = build_set_statements(column_names, unique_key)
insert_values = column_names.map { |name| "NEW.#{name}" }
+ delete_where_statement = unique_key.map { |unique_key| "#{unique_key} = OLD.#{unique_key}" }.join(' AND ')
+ update_where_statement = unique_key.map { |unique_key| "#{partitioned_table_name}.#{unique_key} = NEW.#{unique_key}" }.join(' AND ')
create_trigger_function(name, replace: false) do
<<~SQL
IF (TG_OP = 'DELETE') THEN
- DELETE FROM #{partitioned_table_name} where #{unique_key} = OLD.#{unique_key};
+ DELETE FROM #{partitioned_table_name} where #{delete_where_statement};
ELSIF (TG_OP = 'UPDATE') THEN
UPDATE #{partitioned_table_name}
SET #{set_statements.join(delimiter)}
- WHERE #{partitioned_table_name}.#{unique_key} = NEW.#{unique_key};
+ WHERE #{update_where_statement};
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO #{partitioned_table_name} (#{column_names.join(delimiter)})
VALUES (#{insert_values.join(delimiter)});
@@ -433,8 +534,16 @@ module Gitlab
end
end
+ def build_temporary_columns_statement(columns)
+ columns.map do |column|
+ type = column.name == 'id' || column.name.end_with?('_id') ? 'bigint' : column.sql_type
+
+ "#{column.name}_tmp #{type} NOT NULL"
+ end.join(", ")
+ end
+
def build_set_statements(column_names, unique_key)
- column_names.reject { |name| name == unique_key }.map { |name| "#{name} = NEW.#{name}" }
+ column_names.reject { |name| unique_key.include?(name) }.map { |name| "#{name} = NEW.#{name}" }
end
def create_sync_trigger(table_name, trigger_name, function_name)
diff --git a/lib/gitlab/middleware/path_traversal_check.rb b/lib/gitlab/middleware/path_traversal_check.rb
index 96d0a1c8ff9..6fef247b708 100644
--- a/lib/gitlab/middleware/path_traversal_check.rb
+++ b/lib/gitlab/middleware/path_traversal_check.rb
@@ -5,6 +5,28 @@ module Gitlab
class PathTraversalCheck
PATH_TRAVERSAL_MESSAGE = 'Potential path traversal attempt detected'
+ EXCLUDED_EXACT_PATHS = %w[/search].freeze
+ EXCLUDED_PATH_PREFIXES = %w[/search/].freeze
+
+ EXCLUDED_API_PATHS = %w[/search].freeze
+ EXCLUDED_PROJECT_API_PATHS = %w[/search].freeze
+ EXCLUDED_GROUP_API_PATHS = %w[/search].freeze
+
+ API_PREFIX = %r{/api/[^/]+}
+ API_SUFFIX = %r{(?:\.[^/]+)?}
+
+ EXCLUDED_API_PATHS_REGEX = [
+ EXCLUDED_API_PATHS.map do |path|
+ %r{\A#{API_PREFIX}#{path}#{API_SUFFIX}\z}
+ end.freeze,
+ EXCLUDED_PROJECT_API_PATHS.map do |path|
+ %r{\A#{API_PREFIX}/projects/[^/]+(?:/-)?#{path}#{API_SUFFIX}\z}
+ end.freeze,
+ EXCLUDED_GROUP_API_PATHS.map do |path|
+ %r{\A#{API_PREFIX}/groups/[^/]+(?:/-)?#{path}#{API_SUFFIX}\z}
+ end.freeze
+ ].flatten.freeze
+
def initialize(app)
@app = app
end
@@ -14,7 +36,8 @@ module Gitlab
log_params = {}
execution_time = measure_execution_time do
- check(env, log_params)
+ request = ::Rack::Request.new(env.dup)
+ check(request, log_params) unless excluded?(request)
end
log_params[:duration_ms] = execution_time.round(5) if execution_time
@@ -37,18 +60,25 @@ module Gitlab
end
end
- def check(env, log_params)
- request = ::Rack::Request.new(env.dup)
- fullpath = request.fullpath
- decoded_fullpath = CGI.unescape(fullpath)
+ def check(request, log_params)
+ decoded_fullpath = CGI.unescape(request.fullpath)
::Gitlab::PathTraversal.check_path_traversal!(decoded_fullpath, skip_decoding: true)
-
rescue ::Gitlab::PathTraversal::PathTraversalAttackError
log_params[:method] = request.request_method
- log_params[:fullpath] = fullpath
+ log_params[:fullpath] = request.fullpath
log_params[:message] = PATH_TRAVERSAL_MESSAGE
end
+ def excluded?(request)
+ path = request.path
+
+ return true if path.in?(EXCLUDED_EXACT_PATHS)
+ return true if EXCLUDED_PATH_PREFIXES.any? { |p| path.start_with?(p) }
+ return true if EXCLUDED_API_PATHS_REGEX.any? { |r| path.match?(r) }
+
+ false
+ end
+
def log(payload)
Gitlab::AppLogger.warn(
payload.merge(class_name: self.class.name)
diff --git a/package.json b/package.json
index 799755386db..83ffbb04323 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.69.0",
- "@gitlab/ui": "67.5.1",
+ "@gitlab/ui": "67.5.2",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20231004090414",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 42882966b85..708be988f3a 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,7 +1,11 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
+# Load ActiveSupport to ensure that core extensions like `Enumerable#exclude?`
+# are available in cop rules like `Performance/CollectionLiteralInLoop`.
+require 'active_support/all'
+
# Auto-require all cops under `rubocop/cop/**/*.rb`
-Dir[File.join(__dir__, 'cop', '**', '*.rb')].sort.each { |file| require file }
+Dir[File.join(__dir__, 'cop', '**', '*.rb')].each { |file| require file }
# rubocop:enable Naming/FileName
diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb
index d45d8cacb88..c2ced10620b 100644
--- a/spec/lib/gitlab/ci/jwt_v2_spec.rb
+++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb
@@ -33,14 +33,6 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
describe '#payload' do
subject(:payload) { ci_job_jwt_v2.payload }
- it 'has correct values for the standard JWT attributes' do
- aggregate_failures do
- expect(payload[:iss]).to eq(Settings.gitlab.base_url)
- expect(payload[:aud]).to eq(Settings.gitlab.base_url)
- expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
- end
- end
-
it 'includes user identities when enabled' do
expect(user).to receive(:pass_user_identities_to_ci_jwt).and_return(true)
identities = payload[:user_identities].map { |identity| identity.slice(:extern_uid, :provider) }
@@ -53,6 +45,34 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
expect(payload).not_to include(:user_identities)
end
+ context 'when oidc_issuer_url is disabled' do
+ before do
+ stub_feature_flags(oidc_issuer_url: false)
+ end
+
+ it 'has correct values for the standard JWT attributes' do
+ aggregate_failures do
+ expect(payload[:iss]).to eq(Settings.gitlab.base_url)
+ expect(payload[:aud]).to eq(Settings.gitlab.base_url)
+ expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
+ end
+ end
+ end
+
+ context 'when oidc_issuer_url is enabled' do
+ before do
+ stub_feature_flags(oidc_issuer_url: true)
+ end
+
+ it 'has correct values for the standard JWT attributes' do
+ aggregate_failures do
+ expect(payload[:iss]).to eq(Gitlab.config.gitlab.url)
+ expect(payload[:aud]).to eq(Settings.gitlab.base_url)
+ expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
+ end
+ end
+ end
+
context 'when given an aud' do
let(:aud) { 'AWS' }
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 182b6960384..31c669ff330 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -2,6 +2,173 @@
require 'spec_helper'
+RSpec.shared_examples "a measurable object" do
+ context 'when the table is not allowed' do
+ let(:source_table) { :_test_this_table_is_not_allowed }
+
+ it 'raises an error' do
+ expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
+
+ expect do
+ subject
+ end.to raise_error(/#{source_table} is not allowed for use/)
+ end
+ end
+
+ context 'when run inside a transaction block' do
+ it 'raises an error' do
+ expect(migration).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ subject
+ end.to raise_error(/can not be run inside a transaction/)
+ end
+ end
+
+ context 'when the given table does not have a primary key' do
+ it 'raises an error' do
+ migration.execute(<<~SQL)
+ ALTER TABLE #{source_table}
+ DROP CONSTRAINT #{source_table}_pkey
+ SQL
+
+ expect do
+ subject
+ end.to raise_error(/primary key not defined for #{source_table}/)
+ end
+ end
+
+ it 'creates the partitioned table with the same non-key columns' do
+ subject
+
+ copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
+ original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
+
+ expect(copied_columns).to match_array(original_columns)
+ end
+
+ it 'removes the default from the primary key column' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.default_function).to be_nil
+ end
+
+ describe 'constructing the partitioned table' do
+ it 'creates a table partitioned by the proper column' do
+ subject
+
+ expect(connection.table_exists?(partitioned_table)).to be(true)
+ expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
+
+ expect_table_partitioned_by(partitioned_table, [partition_column_name])
+ end
+
+ it 'requires the migration helper to be run in DDL mode' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
+
+ subject
+
+ expect(connection.table_exists?(partitioned_table)).to be(true)
+ expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
+
+ expect_table_partitioned_by(partitioned_table, [partition_column_name])
+ end
+
+ it 'changes the primary key datatype to bigint' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.sql_type).to eq('bigint')
+ end
+
+ it 'removes the default from the primary key column' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.default_function).to be_nil
+ end
+
+ it 'creates the partitioned table with the same non-key columns' do
+ subject
+
+ copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
+ original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
+
+ expect(copied_columns).to match_array(original_columns)
+ end
+ end
+
+ describe 'keeping data in sync with the partitioned table' do
+ before do
+ partitioned_model.primary_key = :id
+ partitioned_model.table_name = partitioned_table
+ end
+
+ it 'creates a trigger function on the original table' do
+ expect_function_not_to_exist(function_name)
+ expect_trigger_not_to_exist(source_table, trigger_name)
+
+ subject
+
+ expect_function_to_exist(function_name)
+ expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
+ end
+
+ it 'syncs inserts to the partitioned tables' do
+ subject
+
+ expect(partitioned_model.count).to eq(0)
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+ expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
+ expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
+ end
+
+ it 'syncs updates to the partitioned tables' do
+ subject
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+
+ first_copy = partitioned_model.find(first_record.id)
+ second_copy = partitioned_model.find(second_record.id)
+
+ expect(first_copy.attributes).to eq(first_record.attributes)
+ expect(second_copy.attributes).to eq(second_record.attributes)
+
+ first_record.update!(age: 21, updated_at: timestamp + 1.hour, external_id: 3)
+
+ expect(partitioned_model.count).to eq(2)
+ expect(first_copy.reload.attributes).to eq(first_record.attributes)
+ expect(second_copy.reload.attributes).to eq(second_record.attributes)
+ end
+
+ it 'syncs deletes to the partitioned tables' do
+ subject
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+
+ first_record.destroy!
+
+ expect(partitioned_model.count).to eq(1)
+ expect(partitioned_model.find_by_id(first_record.id)).to be_nil
+ expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
+ end
+ end
+end
+
RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers, feature_category: :database do
include Database::PartitioningHelpers
include Database::TriggerHelpers
@@ -18,6 +185,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
let(:partitioned_table) { :_test_migration_partitioned_table }
let(:function_name) { :_test_migration_function_name }
let(:trigger_name) { :_test_migration_trigger_name }
+ let(:partition_column2) { 'external_id' }
let(:partition_column) { 'created_at' }
let(:min_date) { Date.new(2019, 12) }
let(:max_date) { Date.new(2020, 3) }
@@ -29,6 +197,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
migration.create_table source_table do |t|
t.string :name, null: false
t.integer :age, null: false
+ t.integer partition_column2
t.datetime partition_column
t.datetime :updated_at
end
@@ -127,33 +296,190 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
- describe '#partition_table_by_date' do
- let(:partition_column) { 'created_at' }
+ describe '#partition_table_by_int_range' do
let(:old_primary_key) { 'id' }
- let(:new_primary_key) { [old_primary_key, partition_column] }
+ let(:new_primary_key) { ['id', partition_column2] }
+ let(:partition_column_name) { partition_column2 }
+ let(:partitioned_model) { Class.new(ActiveRecord::Base) }
+ let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
+ let(:partition_size) { 500 }
- context 'when the table is not allowed' do
- let(:source_table) { :_test_this_table_is_not_allowed }
+ subject { migration.partition_table_by_int_range(source_table, partition_column2, partition_size: partition_size, primary_key: ['id', partition_column2]) }
- it 'raises an error' do
- expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
+ include_examples "a measurable object"
- expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/#{source_table} is not allowed for use/)
+ context 'simulates the merge_request_diff_commits migration' do
+ let(:table_name) { '_test_merge_request_diff_commits' }
+ let(:partition_column_name) { 'relative_order' }
+ let(:partition_size) { 2 }
+ let(:partitions) do
+ {
+ '1' => %w[1 3],
+ '3' => %w[3 5],
+ '5' => %w[5 7],
+ '7' => %w[7 9],
+ '9' => %w[9 11],
+ '11' => %w[11 13]
+ }
+ end
+
+ let(:buffer_partitions) do
+ {
+ '13' => %w[13 15],
+ '15' => %w[15 17],
+ '17' => %w[17 19],
+ '19' => %w[19 21],
+ '21' => %w[21 23],
+ '23' => %w[23 25]
+ }
+ end
+
+ let(:new_table_defition) do
+ {
+ new_path: { default: 'test', null: true, sql_type: 'text' },
+ merge_request_diff_id: { default: nil, null: false, sql_type: 'bigint' },
+ relative_order: { default: nil, null: false, sql_type: 'integer' }
+ }
+ end
+
+ let(:primary_key) { %w[merge_request_diff_id relative_order] }
+
+ before do
+ migration.create_table table_name, primary_key: primary_key do |t|
+ t.integer :merge_request_diff_id, null: false, default: 1
+ t.integer :relative_order, null: false
+ t.text :new_path, null: true, default: 'test'
+ end
+
+ source_model.table_name = table_name
+ end
+
+ it 'creates the partitions' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions))
+ end
+
+ it 'creates a composite primary key' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect(connection.primary_key(:_test_migration_partitioned_table)).to eql(%w[merge_request_diff_id relative_order])
+ end
+
+ it 'applies the correct column schema for the new table' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ columns = connection.columns(:_test_migration_partitioned_table)
+
+ columns.each do |column|
+ column_name = column.name.to_sym
+
+ expect(column.default).to eql(new_table_defition[column_name][:default])
+ expect(column.null).to eql(new_table_defition[column_name][:null])
+ expect(column.sql_type).to eql(new_table_defition[column_name][:sql_type])
+ end
+ end
+
+ it 'creates multiple partitions' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: 500, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, {
+ '1' => %w[1 501],
+ '501' => %w[501 1001],
+ '1001' => %w[1001 1501],
+ '1501' => %w[1501 2001],
+ '2001' => %w[2001 2501],
+ '2501' => %w[2501 3001],
+ '3001' => %w[3001 3501],
+ '3501' => %w[3501 4001],
+ '4001' => %w[4001 4501],
+ '4501' => %w[4501 5001],
+ '5001' => %w[5001 5501],
+ '5501' => %w[5501 6001]
+ })
+ end
+
+ context 'when the table is not empty' do
+ before do
+ source_model.create!(merge_request_diff_id: 1, relative_order: 7, new_path: 'new_path')
+ end
+
+ let(:partition_size) { 2 }
+
+ let(:partitions) do
+ {
+ '1' => %w[1 3],
+ '3' => %w[3 5],
+ '5' => %w[5 7]
+ }
+ end
+
+ let(:buffer_partitions) do
+ {
+ '7' => %w[7 9],
+ '9' => %w[9 11],
+ '11' => %w[11 13],
+ '13' => %w[13 15],
+ '15' => %w[15 17],
+ '17' => %w[17 19]
+ }
+ end
+
+ it 'defaults the min_id to 1 and the max_id to 7' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions))
+ end
end
end
- context 'when run inside a transaction block' do
+ context 'when an invalid partition column is given' do
+ let(:invalid_column) { :_this_is_not_real }
+
it 'raises an error' do
- expect(migration).to receive(:transaction_open?).and_return(true)
+ expect do
+ migration.partition_table_by_int_range(source_table, invalid_column, partition_size: partition_size, primary_key: ['id'])
+ end.to raise_error(/partition column #{invalid_column} does not exist/)
+ end
+ end
+
+ context 'when partition_size is less than 1' do
+ let(:partition_size) { 1 }
+ it 'raises an error' do
expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/can not be run inside a transaction/)
+ subject
+ end.to raise_error(/partition_size must be greater than 1/)
end
end
+ context 'when the partitioned table already exists' do
+ before do
+ migration.send(:create_range_id_partitioned_copy, source_table,
+ migration.send(:make_partitioned_table_name, source_table),
+ connection.columns(source_table).find { |c| c.name == partition_column2 },
+ connection.columns(source_table).select { |c| new_primary_key.include?(c.name) })
+ end
+
+ it 'raises an error' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(/Partitioned table not created because it already exists/)
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#partition_table_by_date' do
+ let(:partition_column) { 'created_at' }
+ let(:old_primary_key) { 'id' }
+ let(:new_primary_key) { [old_primary_key, partition_column] }
+ let(:partition_column_name) { 'created_at' }
+ let(:partitioned_model) { Class.new(ActiveRecord::Base) }
+ let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
+
+ subject { migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date }
+
+ include_examples "a measurable object"
+
context 'when the the max_date is less than the min_date' do
let(:max_date) { Time.utc(2019, 6) }
@@ -174,19 +500,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
- context 'when the given table does not have a primary key' do
- it 'raises an error' do
- migration.execute(<<~SQL)
- ALTER TABLE #{source_table}
- DROP CONSTRAINT #{source_table}_pkey
- SQL
-
- expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/primary key not defined for #{source_table}/)
- end
- end
-
context 'when an invalid partition column is given' do
let(:invalid_column) { :_this_is_not_real }
@@ -198,34 +511,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
describe 'constructing the partitioned table' do
- it 'creates a table partitioned by the proper column' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(connection.table_exists?(partitioned_table)).to be(true)
- expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
-
- expect_table_partitioned_by(partitioned_table, [partition_column])
- end
-
- it 'requires the migration helper to be run in DDL mode' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
-
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(connection.table_exists?(partitioned_table)).to be(true)
- expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
-
- expect_table_partitioned_by(partitioned_table, [partition_column])
- end
-
- it 'changes the primary key datatype to bigint' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
-
- expect(pk_column.sql_type).to eq('bigint')
- end
-
context 'with a non-integer primary key datatype' do
before do
connection.create_table non_int_table, id: false do |t|
@@ -248,23 +533,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
- it 'removes the default from the primary key column' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
-
- expect(pk_column.default_function).to be_nil
- end
-
- it 'creates the partitioned table with the same non-key columns' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
- original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
-
- expect(copied_columns).to match_array(original_columns)
- end
-
it 'creates a partition spanning over each month in the range given' do
migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
@@ -350,75 +618,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
end
-
- describe 'keeping data in sync with the partitioned table' do
- let(:partitioned_model) { Class.new(ActiveRecord::Base) }
- let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
-
- before do
- partitioned_model.primary_key = :id
- partitioned_model.table_name = partitioned_table
- end
-
- it 'creates a trigger function on the original table' do
- expect_function_not_to_exist(function_name)
- expect_trigger_not_to_exist(source_table, trigger_name)
-
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect_function_to_exist(function_name)
- expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
- end
-
- it 'syncs inserts to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(partitioned_model.count).to eq(0)
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
- expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
- expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
- end
-
- it 'syncs updates to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
-
- first_copy = partitioned_model.find(first_record.id)
- second_copy = partitioned_model.find(second_record.id)
-
- expect(first_copy.attributes).to eq(first_record.attributes)
- expect(second_copy.attributes).to eq(second_record.attributes)
-
- first_record.update!(age: 21, updated_at: timestamp + 1.hour)
-
- expect(partitioned_model.count).to eq(2)
- expect(first_copy.reload.attributes).to eq(first_record.attributes)
- expect(second_copy.reload.attributes).to eq(second_record.attributes)
- end
-
- it 'syncs deletes to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
-
- first_record.destroy!
-
- expect(partitioned_model.count).to eq(1)
- expect(partitioned_model.find_by_id(first_record.id)).to be_nil
- expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
- end
- end
end
describe '#drop_partitioned_table_for' do
diff --git a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
index 44f47236ffa..91081cc88ea 100644
--- a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
+++ b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
@@ -55,6 +55,34 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
end
end
+ shared_examples 'excluded path' do
+ it 'measures and logs the execution time' do
+ expect(::Gitlab::PathTraversal)
+ .not_to receive(:check_path_traversal!)
+ expect(::Gitlab::AppLogger)
+ .to receive(:warn)
+ .with({ class_name: described_class.name, duration_ms: instance_of(Float) })
+ .and_call_original
+
+ expect(subject).to eq(fake_response)
+ end
+
+ context 'with log_execution_time_path_traversal_middleware disabled' do
+ before do
+ stub_feature_flags(log_execution_time_path_traversal_middleware: false)
+ end
+
+ it 'does nothing' do
+ expect(::Gitlab::PathTraversal)
+ .not_to receive(:check_path_traversal!)
+ expect(::Gitlab::AppLogger)
+ .not_to receive(:warn)
+
+ expect(subject).to eq(fake_response)
+ end
+ end
+ end
+
shared_examples 'path traversal' do
it 'logs the problem and measures the execution time' do
expect(::Gitlab::PathTraversal)
@@ -112,23 +140,90 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
let(:method) { 'get' }
where(:path, :query_params, :shared_example_name) do
- '/foo/bar' | {} | 'no issue'
- '/foo/../bar' | {} | 'path traversal'
- '/foo%2Fbar' | {} | 'no issue'
- '/foo%2F..%2Fbar' | {} | 'path traversal'
- '/foo%252F..%252Fbar' | {} | 'no issue'
- '/foo/bar' | { x: 'foo' } | 'no issue'
- '/foo/bar' | { x: 'foo/../bar' } | 'path traversal'
- '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
- '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue'
- '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue'
- '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal'
+ '/foo/bar' | {} | 'no issue'
+ '/foo/../bar' | {} | 'path traversal'
+ '/foo%2Fbar' | {} | 'no issue'
+ '/foo%2F..%2Fbar' | {} | 'path traversal'
+ '/foo%252F..%252Fbar' | {} | 'no issue'
+
+ '/foo/bar' | { x: 'foo' } | 'no issue'
+ '/foo/bar' | { x: 'foo/../bar' } | 'path traversal'
+ '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
+ '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue'
+ '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue'
+ '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal'
end
with_them do
it_behaves_like params[:shared_example_name]
end
+ context 'for global search excluded paths' do
+ excluded_paths = %w[
+ /search
+ /search/count
+ /api/v4/search
+ /api/v4/search.json
+ /api/v4/projects/4/search
+ /api/v4/projects/4/search.json
+ /api/v4/projects/4/-/search
+ /api/v4/projects/4/-/search.json
+ /api/v4/projects/my%2Fproject/search
+ /api/v4/projects/my%2Fproject/search.json
+ /api/v4/projects/my%2Fproject/-/search
+ /api/v4/projects/my%2Fproject/-/search.json
+ /api/v4/groups/4/search
+ /api/v4/groups/4/search.json
+ /api/v4/groups/4/-/search
+ /api/v4/groups/4/-/search.json
+ /api/v4/groups/my%2Fgroup/search
+ /api/v4/groups/my%2Fgroup/search.json
+ /api/v4/groups/my%2Fgroup/-/search
+ /api/v4/groups/my%2Fgroup/-/search.json
+ ]
+ query_params_with_no_path_traversal = [
+ {},
+ { x: 'foo' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%252F..%252Fbar' }
+ ]
+ query_params_with_path_traversal = [
+ { x: 'foo/../bar' }
+ ]
+
+ excluded_paths.each do |excluded_path|
+ [query_params_with_no_path_traversal + query_params_with_path_traversal].flatten.each do |qp|
+ context "for excluded path #{excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path }
+
+ it_behaves_like 'excluded path'
+ end
+ end
+
+ non_excluded_path = excluded_path.gsub('search', 'searchtest')
+
+ query_params_with_no_path_traversal.each do |qp|
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { non_excluded_path }
+
+ it_behaves_like 'no issue'
+ end
+ end
+
+ query_params_with_path_traversal.each do |qp|
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { non_excluded_path }
+
+ it_behaves_like 'path traversal'
+ end
+ end
+ end
+ end
+
context 'with a issues search path' do
let(:query_params) { {} }
let(:path) do
@@ -149,6 +244,7 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
'/foo%2Fbar' | {} | 'no issue'
'/foo%2F..%2Fbar' | {} | 'path traversal'
'/foo%252F..%252Fbar' | {} | 'no issue'
+
'/foo/bar' | { x: 'foo' } | 'no issue'
'/foo/bar' | { x: 'foo/../bar' } | 'no issue'
'/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
@@ -160,6 +256,59 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
with_them do
it_behaves_like params[:shared_example_name]
end
+
+ context 'for global search excluded paths' do
+ excluded_paths = %w[
+ /search
+ /search/count
+ /api/v4/search
+ /api/v4/search.json
+ /api/v4/projects/4/search
+ /api/v4/projects/4/search.json
+ /api/v4/projects/4/-/search
+ /api/v4/projects/4/-/search.json
+ /api/v4/projects/my%2Fproject/search
+ /api/v4/projects/my%2Fproject/search.json
+ /api/v4/projects/my%2Fproject/-/search
+ /api/v4/projects/my%2Fproject/-/search.json
+ /api/v4/groups/4/search
+ /api/v4/groups/4/search.json
+ /api/v4/groups/4/-/search
+ /api/v4/groups/4/-/search.json
+ /api/v4/groups/my%2Fgroup/search
+ /api/v4/groups/my%2Fgroup/search.json
+ /api/v4/groups/my%2Fgroup/-/search
+ /api/v4/groups/my%2Fgroup/-/search.json
+ ]
+ all_query_params = [
+ {},
+ { x: 'foo' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%252F..%252Fbar' },
+ { x: 'foo/../bar' }
+ ]
+
+ excluded_paths.each do |excluded_path|
+ all_query_params.each do |qp|
+ context "for excluded path #{excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path }
+
+ it_behaves_like 'excluded path'
+ end
+
+ non_excluded_path = excluded_path.gsub('search', 'searchtest')
+
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path.gsub('search', 'searchtest') }
+
+ it_behaves_like 'no issue'
+ end
+ end
+ end
+ end
end
end
@@ -179,6 +328,12 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
'/foo/bar' | { x: 'foo%2Fbar' }
'/foo/bar' | { x: 'foo%2F..%2Fbar' }
'/foo/bar' | { x: 'foo%252F..%252Fbar' }
+ '/search' | { x: 'foo/../bar' }
+ '/search' | { x: 'foo%2F..%2Fbar' }
+ '/search' | { x: 'foo%252F..%252Fbar' }
+ '%2Fsearch' | { x: 'foo/../bar' }
+ '%2Fsearch' | { x: 'foo%2F..%2Fbar' }
+ '%2Fsearch' | { x: 'foo%252F..%252Fbar' }
end
with_them do
diff --git a/spec/rubocop/rubocop_spec.rb b/spec/rubocop/rubocop_spec.rb
new file mode 100644
index 00000000000..a80a0f72bdf
--- /dev/null
+++ b/spec/rubocop/rubocop_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# No spec helper is `require`d because `fast_spec_helper` requires
+# `active_support/all` and we want to ensure that `rubocop/rubocop` loads it.
+
+require 'rubocop'
+require_relative '../../rubocop/rubocop'
+
+RSpec.describe 'rubocop/rubocop', feature_category: :tooling do
+ it 'loads activesupport to enhance Enumerable' do
+ expect(Enumerable.instance_methods).to include(:exclude?)
+ end
+end
diff --git a/spec/services/bulk_imports/batched_relation_export_service_spec.rb b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
index c361dfe5052..dd85961befd 100644
--- a/spec/services/bulk_imports/batched_relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
@@ -71,29 +71,6 @@ RSpec.describe BulkImports::BatchedRelationExportService, feature_category: :imp
expect(export.batches.count).to eq(0)
end
end
-
- context 'when exception occurs' do
- it 'tracks exception and marks export as failed' do
- allow_next_instance_of(BulkImports::Export) do |export|
- allow(export).to receive(:update!).and_call_original
-
- allow(export)
- .to receive(:update!)
- .with(status_event: 'finish', total_objects_count: 0, batched: true, batches_count: 0, jid: jid, error: nil)
- .and_raise(StandardError, 'Error!')
- end
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(StandardError, portable_id: portable.id, portable_type: portable.class.name)
-
- service.execute
-
- export = portable.bulk_import_exports.first
-
- expect(export.reload.failed?).to eq(true)
- end
- end
end
describe '.cache_key' do
diff --git a/spec/services/bulk_imports/relation_batch_export_service_spec.rb b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
index 7729c17098a..a18099a4189 100644
--- a/spec/services/bulk_imports/relation_batch_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
@@ -34,10 +34,7 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
end
it 'removes exported contents after export' do
- double = instance_double(BulkImports::FileTransfer::ProjectConfig, export_path: 'foo')
-
- allow(BulkImports::FileTransfer).to receive(:config_for).and_return(double)
- allow(double).to receive(:export_service_for).and_raise(StandardError, 'Error!')
+ allow(subject).to receive(:export_path).and_return('foo')
allow(FileUtils).to receive(:remove_entry)
expect(FileUtils).to receive(:remove_entry).with('foo')
@@ -53,29 +50,10 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
allow(subject).to receive(:export_path).and_return('foo')
allow(FileUtils).to receive(:remove_entry)
- expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson')
+ expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson').and_call_original
subject.execute
end
end
-
- context 'when exception occurs' do
- before do
- allow(service).to receive(:gzip).and_raise(StandardError, 'Error!')
- end
-
- it 'marks batch as failed' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(StandardError, portable_id: project.id, portable_type: 'Project')
-
- service.execute
- batch.reload
-
- expect(batch.failed?).to eq(true)
- expect(batch.objects_count).to eq(0)
- expect(batch.error).to eq('Error!')
- end
- end
end
end
diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb
index bd8447e3d97..b7d6c424277 100644
--- a/spec/services/bulk_imports/relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_export_service_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
let(:relation) { 'milestones' }
it 'creates empty file on disk' do
- expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson")
+ expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson").and_call_original
subject.execute
end
@@ -118,39 +118,6 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
end
end
- context 'when exception occurs during export' do
- shared_examples 'tracks exception' do |exception_class|
- it 'tracks exception' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(exception_class, portable_id: group.id, portable_type: group.class.name)
- .and_call_original
-
- subject.execute
- end
- end
-
- before do
- allow_next_instance_of(BulkImports::ExportUpload) do |upload|
- allow(upload).to receive(:save!).and_raise(StandardError)
- end
- end
-
- it 'marks export as failed' do
- subject.execute
-
- expect(export.reload.failed?).to eq(true)
- end
-
- include_examples 'tracks exception', StandardError
-
- context 'when passed relation is not supported' do
- let(:relation) { 'unsupported' }
-
- include_examples 'tracks exception', ActiveRecord::RecordInvalid
- end
- end
-
context 'when export was batched' do
let(:relation) { 'milestones' }
let(:export) { create(:bulk_import_export, group: group, relation: relation, batched: true, batches_count: 2) }
diff --git a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
index d882cd8635a..f87952d1d0e 100644
--- a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
+++ b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
let(:expected_error_message) { error_parameter_missing }
+ let(:expected_custom_email_enabled) { false }
let(:logger_params) { { category: 'custom_email_verification' } }
before do
@@ -30,7 +31,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
shared_examples 'a failing verification process' do |expected_error_identifier|
- it 'refuses to verify and sends result emails' do
+ it 'refuses to verify and sends result emails', :aggregate_failures do
expect(Notify).to receive(:service_desk_verification_result_email).twice
expect(Gitlab::AppLogger).to receive(:info).with(logger_params.merge(
@@ -52,7 +53,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
shared_examples 'an early exit from the verification process' do |expected_state|
- it 'exits early' do
+ it 'exits early', :aggregate_failures do
expect(Notify).to receive(:service_desk_verification_result_email).exactly(0).times
expect(Gitlab::AppLogger).to receive(:warn).with(logger_params.merge(
@@ -65,7 +66,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
verification.reset
expect(response).to be_error
- expect(settings).not_to be_custom_email_enabled
+ expect(settings.custom_email_enabled).to eq expected_custom_email_enabled
expect(verification.state).to eq expected_state
end
end
@@ -179,6 +180,26 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
it_behaves_like 'a failing verification process', 'mail_not_received_within_timeframe'
end
+
+ context 'when already verified' do
+ let(:expected_error_message) { error_already_finished }
+
+ before do
+ verification.mark_as_finished!
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'finished'
+
+ context 'when enabled' do
+ let(:expected_custom_email_enabled) { true }
+
+ before do
+ settings.update!(custom_email_enabled: true)
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'finished'
+ end
+ end
end
end
end
diff --git a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
index e27e2dc0ac8..8ee55d64a1b 100644
--- a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
@@ -23,4 +23,21 @@ RSpec.describe BulkImports::RelationBatchExportWorker, feature_category: :import
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args } }
+
+ it 'sets export status to failed and tracks the exception' do
+ portable = batch.export.portable
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(kind_of(StandardError), portable_id: portable.id, portable_type: portable.class.name)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300))
+
+ expect(batch.reload.failed?).to eq(true)
+ expect(batch.error.size).to eq(255)
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/relation_export_worker_spec.rb b/spec/workers/bulk_imports/relation_export_worker_spec.rb
index 646af6c2a9c..55e2a238027 100644
--- a/spec/workers/bulk_imports/relation_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_export_worker_spec.rb
@@ -63,4 +63,20 @@ RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers d
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args } }
+ let!(:export) { create(:bulk_import_export, group: group, relation: relation) }
+
+ it 'sets export status to failed and tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(kind_of(StandardError), portable_id: group.id, portable_type: group.class.name)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300))
+
+ expect(export.reload.failed?).to eq(true)
+ expect(export.error.size).to eq(255)
+ end
+ end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 37c878aa16b..ebe7dbcbce1 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -480,7 +480,9 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'X509CertificateRevokeWorker' => 3,
'ComplianceManagement::MergeRequests::ComplianceViolationsWorker' => 3,
'Zoekt::IndexerWorker' => 2,
- 'Issuable::RelatedLinksCreateWorker' => 3
+ 'Issuable::RelatedLinksCreateWorker' => 3,
+ 'BulkImports::RelationBatchExportWorker' => 3,
+ 'BulkImports::RelationExportWorker' => 3
}.merge(extra_retry_exceptions)
end
diff --git a/yarn.lock b/yarn.lock
index ef4fd43d18a..36380d7144d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1274,10 +1274,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.69.0.tgz#bf76b8ffbe72a783807761a38abe8aaedcfe8c12"
integrity sha512-Zu8Fcjhi3Bk26jZOptcD5F4SHWC7/KuAe00NULViCeswKdoda1k19B+9oCSbsbxY7vMoFuD20kiCJdBCpxb3HA==
-"@gitlab/ui@67.5.1":
- version "67.5.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-67.5.1.tgz#e331b34fa920f8f26ab0635dc84d15cbd4ac29e3"
- integrity sha512-nsIlkZlU9Sig/KyVMkiHUp2Mcg/H2h4U5wN7ROnTv5heLhCP5oCvRpzGuazzhR/oemPqnKv/53ySAosDokhlmA==
+"@gitlab/ui@67.5.2":
+ version "67.5.2"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-67.5.2.tgz#9a8d6008353b7250c1686a2a20fcae6630c97213"
+ integrity sha512-Qtkh9AEHjPrST/rl4SeY7Wh/ZB68GQO9jjM4Jll+3JicX3prg55uuFd4PATcq71DbXyIySZuDI/DrltgkhWByw==
dependencies:
"@floating-ui/dom" "1.2.9"
bootstrap-vue "2.23.1"