Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									224d2fe167
								
							
						
					
					
						commit
						3fbfc0075a
					
				|  | @ -50,6 +50,8 @@ include: | ||||||
|         --tag ~orchestrated \ |         --tag ~orchestrated \ | ||||||
|         --tag ~transient \ |         --tag ~transient \ | ||||||
|         --tag ~skip_signup_disabled \ |         --tag ~skip_signup_disabled \ | ||||||
|  |         --tag ~requires_git_protocol_v2 \ | ||||||
|  |         --tag ~requires_praefect \ | ||||||
|         --force-color \ |         --force-color \ | ||||||
|         --order random \ |         --order random \ | ||||||
|         --format documentation \ |         --format documentation \ | ||||||
|  |  | ||||||
|  | @ -1001,7 +1001,6 @@ Layout/LineLength: | ||||||
|     - 'db/migrate/20220310101118_update_holder_name_limit.rb' |     - 'db/migrate/20220310101118_update_holder_name_limit.rb' | ||||||
|     - 'db/migrate/20220314184209_add_group_fk_to_protected_environment_approval_rules.rb' |     - 'db/migrate/20220314184209_add_group_fk_to_protected_environment_approval_rules.rb' | ||||||
|     - 'db/migrate/20220314204009_add_approval_rule_fk_to_deployment_approvals.rb' |     - 'db/migrate/20220314204009_add_approval_rule_fk_to_deployment_approvals.rb' | ||||||
|     - 'db/optional_migrations/composite_primary_keys.rb' |  | ||||||
|     - 'db/post_migrate/20210328214434_remove_temporary_index_from_vulnerabilities_table.rb' |     - 'db/post_migrate/20210328214434_remove_temporary_index_from_vulnerabilities_table.rb' | ||||||
|     - 'db/post_migrate/20210401131948_move_container_registry_enabled_to_project_features2.rb' |     - 'db/post_migrate/20210401131948_move_container_registry_enabled_to_project_features2.rb' | ||||||
|     - 'db/post_migrate/20210402005225_add_source_and_level_index_on_notification_settings.rb' |     - 'db/post_migrate/20210402005225_add_source_and_level_index_on_notification_settings.rb' | ||||||
|  |  | ||||||
|  | @ -229,7 +229,6 @@ Style/PercentLiteralDelimiters: | ||||||
|     - 'db/migrate/20210621044000_rename_services_indexes_to_integrations.rb' |     - 'db/migrate/20210621044000_rename_services_indexes_to_integrations.rb' | ||||||
|     - 'db/migrate/20210709085759_index_batched_migration_jobs_by_max_value.rb' |     - 'db/migrate/20210709085759_index_batched_migration_jobs_by_max_value.rb' | ||||||
|     - 'db/migrate/20210928155022_improve_index_for_error_tracking.rb' |     - 'db/migrate/20210928155022_improve_index_for_error_tracking.rb' | ||||||
|     - 'db/optional_migrations/composite_primary_keys.rb' |  | ||||||
|     - 'db/post_migrate/20210329102724_add_new_trail_plans.rb' |     - 'db/post_migrate/20210329102724_add_new_trail_plans.rb' | ||||||
|     - 'db/post_migrate/20210420121149_backfill_conversion_of_ci_job_artifacts.rb' |     - 'db/post_migrate/20210420121149_backfill_conversion_of_ci_job_artifacts.rb' | ||||||
|     - 'db/post_migrate/20210426094549_backfill_ci_builds_for_bigint_conversion.rb' |     - 'db/post_migrate/20210426094549_backfill_ci_builds_for_bigint_conversion.rb' | ||||||
|  |  | ||||||
|  | @ -1 +1 @@ | ||||||
| 2106629e3af3e8949b23f20825d6bfee62c10992 | d7eedd059daf9059990a95e53c76e567eac64899 | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.6 KiB | 
|  | @ -1,5 +1,5 @@ | ||||||
| <script> | <script> | ||||||
| import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; | import { GlSkeletonLoader } from '@gitlab/ui'; | ||||||
| import { mapState, mapGetters } from 'vuex'; | import { mapState, mapGetters } from 'vuex'; | ||||||
| import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants'; | import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants'; | ||||||
| import ActivityBar from './activity_bar.vue'; | import ActivityBar from './activity_bar.vue'; | ||||||
|  | @ -10,7 +10,7 @@ import ResizablePanel from './resizable_panel.vue'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     GlSkeletonLoading, |     GlSkeletonLoader, | ||||||
|     ResizablePanel, |     ResizablePanel, | ||||||
|     ActivityBar, |     ActivityBar, | ||||||
|     IdeTree, |     IdeTree, | ||||||
|  | @ -38,7 +38,7 @@ export default { | ||||||
|     <template v-if="loading"> |     <template v-if="loading"> | ||||||
|       <div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner"> |       <div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner"> | ||||||
|         <div v-for="n in 3" :key="n" class="multi-file-loading-container"> |         <div v-for="n in 3" :key="n" class="multi-file-loading-container"> | ||||||
|           <gl-skeleton-loading /> |           <gl-skeleton-loader /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
|  |  | ||||||
|  | @ -132,6 +132,7 @@ export default { | ||||||
|         <div |         <div | ||||||
|           v-gl-tooltip="{ title: tag.name }" |           v-gl-tooltip="{ title: tag.name }" | ||||||
|           data-testid="name" |           data-testid="name" | ||||||
|  |           data-qa-selector="tag_name_content" | ||||||
|           class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap" |           class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap" | ||||||
|           :class="mobileClasses" |           :class="mobileClasses" | ||||||
|         > |         > | ||||||
|  |  | ||||||
|  | @ -7,6 +7,9 @@ class PwaController < ApplicationController # rubocop:disable Gitlab/NamespacedC | ||||||
| 
 | 
 | ||||||
|   skip_before_action :authenticate_user! |   skip_before_action :authenticate_user! | ||||||
| 
 | 
 | ||||||
|  |   def manifest | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def offline |   def offline | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Tooling | ||||||
|  |   module VisualReviewHelper | ||||||
|  |     # Since we only use the visual review toolbar for the gitlab project, | ||||||
|  |     # we can hardcode the project ID and project path for now. | ||||||
|  |     # | ||||||
|  |     # If we need to extend the review apps to other applications in the future, | ||||||
|  |     # we should create REVIEW_APPS_PROJECT_ID and REVIEW_APPS_PROJECT_PATH | ||||||
|  |     # environment variables (mapped to CI_PROJECT_ID and CI_PROJECT_PATH respectively), | ||||||
|  |     # as well as setting `data-require-auth` according to the project visibility. | ||||||
|  |     GITLAB_INSTANCE_URL            = 'https://gitlab.com' | ||||||
|  |     GITLAB_ORG_GITLAB_PROJECT_ID   = '278964' | ||||||
|  |     GITLAB_ORG_GITLAB_PROJECT_PATH = 'gitlab-org/gitlab' | ||||||
|  | 
 | ||||||
|  |     def visual_review_toolbar_options | ||||||
|  |       { 'data-merge-request-id': "#{ENV['REVIEW_APPS_MERGE_REQUEST_IID']}", | ||||||
|  |         'data-mr-url': "#{GITLAB_INSTANCE_URL}", | ||||||
|  |         'data-project-id': "#{GITLAB_ORG_GITLAB_PROJECT_ID}", | ||||||
|  |         'data-project-path': "#{GITLAB_ORG_GITLAB_PROJECT_PATH}", | ||||||
|  |         'data-require-auth': false, | ||||||
|  |         'id': 'review-app-toolbar-script', | ||||||
|  |         'src': 'https://gitlab.com/assets/webpack/visual_review_toolbar.js' } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -28,7 +28,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated | ||||||
|       commits_anchor_data, |       commits_anchor_data, | ||||||
|       branches_anchor_data, |       branches_anchor_data, | ||||||
|       tags_anchor_data, |       tags_anchor_data, | ||||||
|       files_anchor_data, |  | ||||||
|       storage_anchor_data, |       storage_anchor_data, | ||||||
|       releases_anchor_data |       releases_anchor_data | ||||||
|     ].compact.select(&:is_link) |     ].compact.select(&:is_link) | ||||||
|  | @ -161,26 +160,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated | ||||||
|     can_current_user_push_to_branch?(default_branch) |     can_current_user_push_to_branch?(default_branch) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def files_anchor_data |  | ||||||
|     AnchorData.new(true, |  | ||||||
|                    statistic_icon('doc-code') + |  | ||||||
|                    _('%{strong_start}%{human_size}%{strong_end} Files').html_safe % { |  | ||||||
|                      human_size: storage_counter(statistics.total_repository_size), |  | ||||||
|                      strong_start: '<strong class="project-stat-value">'.html_safe, |  | ||||||
|                      strong_end: '</strong>'.html_safe |  | ||||||
|                    }, |  | ||||||
|                    empty_repo? ? nil : project_tree_path(project)) |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def storage_anchor_data |   def storage_anchor_data | ||||||
|  |     can_show_quota = can?(current_user, :admin_project, project) && !empty_repo? | ||||||
|     AnchorData.new(true, |     AnchorData.new(true, | ||||||
|                    statistic_icon('disk') + |                    statistic_icon('disk') + | ||||||
|                    _('%{strong_start}%{human_size}%{strong_end} Storage').html_safe % { |                    _('%{strong_start}%{human_size}%{strong_end} Project Storage').html_safe % { | ||||||
|                      human_size: storage_counter(statistics.storage_size), |                      human_size: storage_counter(statistics.storage_size), | ||||||
|                      strong_start: '<strong class="project-stat-value">'.html_safe, |                      strong_start: '<strong class="project-stat-value">'.html_safe, | ||||||
|                      strong_end: '</strong>'.html_safe |                      strong_end: '</strong>'.html_safe | ||||||
|                    }, |                    }, | ||||||
|                    empty_repo? ? nil : project_tree_path(project)) |                    can_show_quota ? project_usage_quotas_path(project) : nil) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def releases_anchor_data |   def releases_anchor_data | ||||||
|  |  | ||||||
|  | @ -68,6 +68,7 @@ | ||||||
| 
 | 
 | ||||||
|   %meta{ name: "description", content: page_description } |   %meta{ name: "description", content: page_description } | ||||||
| 
 | 
 | ||||||
|  |   %link{ rel: 'manifest', href: manifest_path(format: :json) } | ||||||
|   %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' } |   %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' } | ||||||
|   %meta{ name: 'theme-color', content: user_theme_primary_color } |   %meta{ name: 'theme-color', content: user_theme_primary_color } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | = javascript_tag "", visual_review_toolbar_options | ||||||
|  | @ -8,6 +8,7 @@ | ||||||
|   %body{ class: body_classes, data: body_data } |   %body{ class: body_classes, data: body_data } | ||||||
|     = render "layouts/init_auto_complete" if @gfm_form |     = render "layouts/init_auto_complete" if @gfm_form | ||||||
|     = render "layouts/init_client_detection_flags" |     = render "layouts/init_client_detection_flags" | ||||||
|  |     = render "layouts/visual_review" if ENV['REVIEW_APPS_ENABLED'] | ||||||
|     = render 'peek/bar' |     = render 'peek/bar' | ||||||
|     = header_message |     = header_message | ||||||
|     = render partial: "layouts/header/default", locals: { project: @project, group: @group } |     = render partial: "layouts/header/default", locals: { project: @project, group: @group } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | { | ||||||
|  |   "name": "GitLab", | ||||||
|  |   "short_name": "GitLab", | ||||||
|  |   "description": "<%= _("The complete DevOps platform. One application with endless possibilities. Organizations rely on GitLab’s source code management, CI/CD, security, and more to deliver software rapidly.") %>", | ||||||
|  |   "start_url": "<%= explore_projects_path %>", | ||||||
|  |   "scope": "<%= root_path %>", | ||||||
|  |   "display": "browser", | ||||||
|  |   "orientation": "any", | ||||||
|  |   "background_color": "#fff", | ||||||
|  |   "theme_color": "<%= user_theme_primary_color %>", | ||||||
|  |   "icons": [{ | ||||||
|  |     "src": "<%= Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/pwa-icons/logo-192.png') %>", | ||||||
|  |     "sizes": "192x192", | ||||||
|  |     "type": "image/png" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "src": "<%= Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/pwa-icons/logo-512.png') %>", | ||||||
|  |     "sizes": "512x512", | ||||||
|  |     "type": "image/png" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "src": "<%= Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/pwa-icons/maskable-logo.png') %>", | ||||||
|  |     "sizes": "512x512", | ||||||
|  |     "type": "image/png", | ||||||
|  |     "purpose": "maskable" | ||||||
|  |   }] | ||||||
|  | } | ||||||
|  | @ -9,6 +9,6 @@ | ||||||
|     .note-form-actions.clearfix |     .note-form-actions.clearfix | ||||||
|       .settings-message.note-edit-warning.js-finish-edit-warning |       .settings-message.note-edit-warning.js-finish-edit-warning | ||||||
|         = _("Finish editing this message first!") |         = _("Finish editing this message first!") | ||||||
|       = submit_tag _('Save comment'), class: 'gl-button btn btn-success js-comment-save-button', data: { qa_selector: 'save_comment_button' } |       = submit_tag _('Save comment'), class: 'gl-button btn btn-confirm js-comment-save-button', data: { qa_selector: 'save_comment_button' } | ||||||
|       %button.btn.gl-button.btn-cancel.note-edit-cancel{ type: 'button' } |       %button.btn.gl-button.btn-cancel.note-edit-cancel{ type: 'button' } | ||||||
|         = _("Cancel") |         = _("Cancel") | ||||||
|  |  | ||||||
|  | @ -629,6 +629,9 @@ Settings.cron_jobs['projects_schedule_refresh_build_artifacts_size_statistics_wo | ||||||
| Settings.cron_jobs['inactive_projects_deletion_cron_worker'] ||= Settingslogic.new({}) | Settings.cron_jobs['inactive_projects_deletion_cron_worker'] ||= Settingslogic.new({}) | ||||||
| Settings.cron_jobs['inactive_projects_deletion_cron_worker']['cron'] ||= '0 1 * * *' | Settings.cron_jobs['inactive_projects_deletion_cron_worker']['cron'] ||= '0 1 * * *' | ||||||
| Settings.cron_jobs['inactive_projects_deletion_cron_worker']['job_class'] = 'Projects::InactiveProjectsDeletionCronWorker' | Settings.cron_jobs['inactive_projects_deletion_cron_worker']['job_class'] = 'Projects::InactiveProjectsDeletionCronWorker' | ||||||
|  | Settings.cron_jobs['loose_foreign_keys_cleanup_worker'] ||= Settingslogic.new({}) | ||||||
|  | Settings.cron_jobs['loose_foreign_keys_cleanup_worker']['cron'] ||= '*/1 * * * *' | ||||||
|  | Settings.cron_jobs['loose_foreign_keys_cleanup_worker']['job_class'] = 'LooseForeignKeys::CleanupWorker' | ||||||
| 
 | 
 | ||||||
| Gitlab.ee do | Gitlab.ee do | ||||||
|   Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({}) |   Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({}) | ||||||
|  | @ -760,9 +763,6 @@ Gitlab.ee do | ||||||
|   Settings.cron_jobs['app_sec_dast_profile_schedule_worker'] ||= Settingslogic.new({}) |   Settings.cron_jobs['app_sec_dast_profile_schedule_worker'] ||= Settingslogic.new({}) | ||||||
|   Settings.cron_jobs['app_sec_dast_profile_schedule_worker']['cron'] ||= '7-59/15 * * * *' |   Settings.cron_jobs['app_sec_dast_profile_schedule_worker']['cron'] ||= '7-59/15 * * * *' | ||||||
|   Settings.cron_jobs['app_sec_dast_profile_schedule_worker']['job_class'] = 'AppSec::Dast::ProfileScheduleWorker' |   Settings.cron_jobs['app_sec_dast_profile_schedule_worker']['job_class'] = 'AppSec::Dast::ProfileScheduleWorker' | ||||||
|   Settings.cron_jobs['loose_foreign_keys_cleanup_worker'] ||= Settingslogic.new({}) |  | ||||||
|   Settings.cron_jobs['loose_foreign_keys_cleanup_worker']['cron'] ||= '*/1 * * * *' |  | ||||||
|   Settings.cron_jobs['loose_foreign_keys_cleanup_worker']['job_class'] = 'LooseForeignKeys::CleanupWorker' |  | ||||||
|   Settings.cron_jobs['ci_namespace_mirrors_consistency_check_worker'] ||= Settingslogic.new({}) |   Settings.cron_jobs['ci_namespace_mirrors_consistency_check_worker'] ||= Settingslogic.new({}) | ||||||
|   Settings.cron_jobs['ci_namespace_mirrors_consistency_check_worker']['cron'] ||= '*/4 * * * *' |   Settings.cron_jobs['ci_namespace_mirrors_consistency_check_worker']['cron'] ||= '*/4 * * * *' | ||||||
|   Settings.cron_jobs['ci_namespace_mirrors_consistency_check_worker']['job_class'] = 'Database::CiNamespaceMirrorsConsistencyCheckWorker' |   Settings.cron_jobs['ci_namespace_mirrors_consistency_check_worker']['job_class'] = 'Database::CiNamespaceMirrorsConsistencyCheckWorker' | ||||||
|  |  | ||||||
|  | @ -117,6 +117,7 @@ Rails.application.routes.draw do | ||||||
|     get '/whats_new' => 'whats_new#index' |     get '/whats_new' => 'whats_new#index' | ||||||
| 
 | 
 | ||||||
|     get 'offline' => "pwa#offline" |     get 'offline' => "pwa#offline" | ||||||
|  |     get 'manifest' => "pwa#manifest", constraints: lambda { |req| req.format == :json } | ||||||
| 
 | 
 | ||||||
|     # '/-/health' implemented by BasicHealthCheck middleware |     # '/-/health' implemented by BasicHealthCheck middleware | ||||||
|     get 'liveness' => 'health#liveness' |     get 'liveness' => 'health#liveness' | ||||||
|  |  | ||||||
|  | @ -1,63 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| # This migration adds a primary key constraint to tables |  | ||||||
| # that only have a composite unique key. |  | ||||||
| # |  | ||||||
| # This is not strictly relevant to Rails (v4 does not |  | ||||||
| # support composite primary keys). However this becomes |  | ||||||
| # useful for e.g. PostgreSQL's logical replication (pglogical) |  | ||||||
| # which requires all tables to have a primary key constraint. |  | ||||||
| # |  | ||||||
| # In that sense, the migration is optional and not strictly needed. |  | ||||||
| class CompositePrimaryKeysMigration < ActiveRecord::Migration[4.2] |  | ||||||
|   include Gitlab::Database::MigrationHelpers |  | ||||||
| 
 |  | ||||||
|   DOWNTIME = false |  | ||||||
| 
 |  | ||||||
|   Index = Struct.new(:table, :name, :columns) |  | ||||||
| 
 |  | ||||||
|   TABLES = [ |  | ||||||
|     Index.new(:issue_assignees, 'index_issue_assignees_on_issue_id_and_user_id', %i(issue_id user_id)), |  | ||||||
|     Index.new(:user_interacted_projects, 'index_user_interacted_projects_on_project_id_and_user_id', %i(project_id user_id)), |  | ||||||
|     Index.new(:merge_request_diff_files, 'index_merge_request_diff_files_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)), |  | ||||||
|     Index.new(:merge_request_diff_commits, 'index_merge_request_diff_commits_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)), |  | ||||||
|     Index.new(:project_authorizations, 'index_project_authorizations_on_user_id_project_id_access_level', %i(user_id project_id access_level)), |  | ||||||
|     Index.new(:push_event_payloads, 'index_push_event_payloads_on_event_id', %i(event_id)), |  | ||||||
|     Index.new(:schema_migrations, 'unique_schema_migrations', %(version)) |  | ||||||
|   ].freeze |  | ||||||
| 
 |  | ||||||
|   disable_ddl_transaction! |  | ||||||
| 
 |  | ||||||
|   def up |  | ||||||
|     disable_statement_timeout do |  | ||||||
|       TABLES.each do |index| |  | ||||||
|         add_primary_key(index) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def down |  | ||||||
|     disable_statement_timeout do |  | ||||||
|       TABLES.each do |index| |  | ||||||
|         remove_primary_key(index) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   private |  | ||||||
| 
 |  | ||||||
|   def add_primary_key(index) |  | ||||||
|     execute "ALTER TABLE #{index.table} ADD PRIMARY KEY USING INDEX #{index.name}" |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def remove_primary_key(index) |  | ||||||
|     temp_index_name = "#{index.name[0..58]}_old" |  | ||||||
|     rename_index index.table, index.name, temp_index_name if index_exists_by_name?(index.table, index.name) |  | ||||||
| 
 |  | ||||||
|     # re-create unique key index |  | ||||||
|     add_concurrent_index index.table, index.columns, unique: true, name: index.name |  | ||||||
| 
 |  | ||||||
|     # This also drops the `temp_index_name` as this is owned by the constraint |  | ||||||
|     execute "ALTER TABLE #{index.table} DROP CONSTRAINT IF EXISTS #{temp_index_name}" |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -127,10 +127,10 @@ POST /features/:name | ||||||
| | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | | | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | | ||||||
| | `key` | string | no | `percentage_of_actors` or `percentage_of_time` (default) | | | `key` | string | no | `percentage_of_actors` or `percentage_of_time` (default) | | ||||||
| | `feature_group` | string | no | A Feature group name | | | `feature_group` | string | no | A Feature group name | | ||||||
| | `user` | string | no | A GitLab username | | | `user` | string | no | A GitLab username or comma-separated multiple usernames | | ||||||
| | `group` | string | no | A GitLab group's path, for example `gitlab-org` | | | `group` | string | no | A GitLab group's path, for example `gitlab-org`, or comma-separated multiple group paths | | ||||||
| | `namespace` | string | no | A GitLab group or user namespace's path, for example `gitlab-org` or username path. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/353117) in GitLab 15.0. | | | `namespace` | string | no | A GitLab group or user namespace's path, for example `john-doe`, or comma-separated multiple namespace paths. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/353117) in GitLab 15.0. | | ||||||
| | `project` | string | no | A projects path, for example `gitlab-org/gitlab-foss` | | | `project` | string | no | A projects path, for example `gitlab-org/gitlab-foss`, or comma-separated multiple project paths | | ||||||
| | `force` | boolean | no | Skip feature flag validation checks, such as a YAML definition | | | `force` | boolean | no | Skip feature flag validation checks, such as a YAML definition | | ||||||
| 
 | 
 | ||||||
| You can enable or disable a feature for a `feature_group`, a `user`, | You can enable or disable a feature for a `feature_group`, a `user`, | ||||||
|  |  | ||||||
|  | @ -74,6 +74,8 @@ If you set a quota for a subgroup, it is not used. | ||||||
| 
 | 
 | ||||||
| ## View CI/CD minutes used by a group | ## View CI/CD minutes used by a group | ||||||
| 
 | 
 | ||||||
|  | > Displaying shared runners duration per project [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355666) in GitLab 15.0. | ||||||
|  | 
 | ||||||
| You can view the number of CI/CD minutes being used by a group. | You can view the number of CI/CD minutes being used by a group. | ||||||
| 
 | 
 | ||||||
| Prerequisite: | Prerequisite: | ||||||
|  |  | ||||||
|  | @ -226,33 +226,47 @@ A fork *does* copy the CI/CD settings of the cloned repository. | ||||||
| ### Create a specific runner | ### Create a specific runner | ||||||
| 
 | 
 | ||||||
| You can create a specific runner for your self-managed GitLab instance or for GitLab.com. | You can create a specific runner for your self-managed GitLab instance or for GitLab.com. | ||||||
| You must have the Owner role for the project. | 
 | ||||||
|  | Prerequisite: | ||||||
|  | 
 | ||||||
|  | - You must have at least the Maintainer role for the project. | ||||||
| 
 | 
 | ||||||
| To create a specific runner: | To create a specific runner: | ||||||
| 
 | 
 | ||||||
| 1. [Install runner](https://docs.gitlab.com/runner/install/). | 1. [Install GitLab Runner](https://docs.gitlab.com/runner/install/). | ||||||
| 1. Go to the project's **Settings > CI/CD** and expand the **Runners** section. | 1. On the top bar, select **Menu > Projects** and find the project where you want to use the runner. | ||||||
| 1. Note the URL and token. | 1. On the left sidebar, select **Settings > CI/CD**. | ||||||
|  | 1. Expand **Runners**. | ||||||
|  | 1. In the **Specific runners** section, note the URL and token. | ||||||
| 1. [Register the runner](https://docs.gitlab.com/runner/register/). | 1. [Register the runner](https://docs.gitlab.com/runner/register/). | ||||||
| 
 | 
 | ||||||
| ### Enable a specific runner for a specific project | The runner is now enabled for the project. | ||||||
| 
 | 
 | ||||||
| A specific runner is available in the project it was created for. An administrator can | ### Enable a specific runner for a different project | ||||||
| enable a specific runner to apply to additional projects. |  | ||||||
| 
 | 
 | ||||||
| - You must have the Owner role for the | After a specific runner is created, you can enable it for other projects. | ||||||
|   project. | 
 | ||||||
|  | Prerequisites: | ||||||
|  | You must have at least the Maintainer role for: | ||||||
|  | 
 | ||||||
|  | - The project where the runner is already enabled. | ||||||
|  | - The project where you want to enable the runner. | ||||||
| - The specific runner must not be [locked](#prevent-a-specific-runner-from-being-enabled-for-other-projects). | - The specific runner must not be [locked](#prevent-a-specific-runner-from-being-enabled-for-other-projects). | ||||||
| 
 | 
 | ||||||
| To enable or disable a specific runner for a project: | To enable a specific runner for a project: | ||||||
| 
 | 
 | ||||||
| 1. Go to the project's **Settings > CI/CD** and expand the **Runners** section. | 1. On the top bar, select **Menu > Projects** and find the project where you want to enable the runner. | ||||||
| 1. Click **Enable for this project** or **Disable for this project**. | 1. On the left sidebar, select **Settings > CI/CD**. | ||||||
|  | 1. Expand **General pipelines**. | ||||||
|  | 1. Expand **Runners**. | ||||||
|  | 1. By the runner you want, select **Enable for this project**. | ||||||
| 
 | 
 | ||||||
| You can edit a specific runner from any of the projects it's enabled for. | You can edit a specific runner from any of the projects it's enabled for. | ||||||
| The modifications, which include unlocking, editing tags and the description, | The modifications, which include unlocking and editing tags and the description, | ||||||
| affect all projects that use the runner. | affect all projects that use the runner. | ||||||
| 
 | 
 | ||||||
|  | An administrator can [enable the runner for multiple projects](../../user/admin_area/settings/continuous_integration.md#enable-a-specific-runner-for-multiple-projects). | ||||||
|  | 
 | ||||||
| ### Prevent a specific runner from being enabled for other projects | ### Prevent a specific runner from being enabled for other projects | ||||||
| 
 | 
 | ||||||
| You can configure a specific runner so it is "locked" and cannot be enabled for other projects. | You can configure a specific runner so it is "locked" and cannot be enabled for other projects. | ||||||
|  | @ -261,8 +275,9 @@ but can also be changed later. | ||||||
| 
 | 
 | ||||||
| To lock or unlock a specific runner: | To lock or unlock a specific runner: | ||||||
| 
 | 
 | ||||||
| 1. Go to the project's **Settings > CI/CD** and expand the **Runners** section. | 1. Go to the project's **Settings > CI/CD**. | ||||||
|  | 1. Expand the **Runners** section. | ||||||
| 1. Find the specific runner you want to lock or unlock. Make sure it's enabled. You cannot lock shared or group runners. | 1. Find the specific runner you want to lock or unlock. Make sure it's enabled. You cannot lock shared or group runners. | ||||||
| 1. Click the pencil button. | 1. Select **Edit** (**{pencil}**). | ||||||
| 1. Check the **Lock to current projects** option. | 1. Check the **Lock to current projects** option. | ||||||
| 1. Click **Save changes**. | 1. Select **Save changes**. | ||||||
|  |  | ||||||
|  | @ -39,6 +39,23 @@ You can set all new projects to have the instance's shared runners available by | ||||||
| 
 | 
 | ||||||
| Any time a new project is created, the shared runners are available. | Any time a new project is created, the shared runners are available. | ||||||
| 
 | 
 | ||||||
|  | ## Shared runners CI/CD minutes | ||||||
|  | 
 | ||||||
|  | As an administrator you can set either a global or namespace-specific | ||||||
|  | limit on the number of [CI/CD minutes](../../../ci/pipelines/cicd_minutes.md) you can use. | ||||||
|  | 
 | ||||||
|  | ## Enable a specific runner for multiple projects | ||||||
|  | 
 | ||||||
|  | To enable a specific runner for one or more projects: | ||||||
|  | 
 | ||||||
|  | 1. On the top bar, select **Menu > Admin**. | ||||||
|  | 1. From the left sidebar, select **Overview > Runners**. | ||||||
|  | 1. Select the runner you want to edit. | ||||||
|  | 1. In the top right, select **Edit** (**{pencil}**). | ||||||
|  | 1. Under **Restrict projects for this runner**, search for a project. | ||||||
|  | 1. To the left of the project, select **Enable**. | ||||||
|  | 1. Repeat this process for each additional project. | ||||||
|  | 
 | ||||||
| ## Add a message for shared runners | ## Add a message for shared runners | ||||||
| 
 | 
 | ||||||
| To display details about the instance's shared runners in all projects' | To display details about the instance's shared runners in all projects' | ||||||
|  | @ -143,10 +160,6 @@ A new pipeline must run before the latest artifacts can expire and be deleted. | ||||||
| NOTE: | NOTE: | ||||||
| All application settings have a [customizable cache expiry interval](../../../administration/application_settings_cache.md) which can delay the settings affect. | All application settings have a [customizable cache expiry interval](../../../administration/application_settings_cache.md) which can delay the settings affect. | ||||||
| 
 | 
 | ||||||
| ## Shared runners CI/CD minutes |  | ||||||
| 
 |  | ||||||
| As an administrator you can set either a global or namespace-specific limit on the number of [CI/CD minutes](../../../ci/pipelines/cicd_minutes.md) you can use. |  | ||||||
| 
 |  | ||||||
| ## Archive jobs | ## Archive jobs | ||||||
| 
 | 
 | ||||||
| Archiving jobs is useful for reducing the CI/CD footprint on the system by removing some | Archiving jobs is useful for reducing the CI/CD footprint on the system by removing some | ||||||
|  |  | ||||||
|  | @ -22,16 +22,15 @@ module API | ||||||
|         use :pagination |         use :pagination | ||||||
|         optional :name, type: String, desc: 'Returns the environment with this name' |         optional :name, type: String, desc: 'Returns the environment with this name' | ||||||
|         optional :search, type: String, desc: 'Returns list of environments matching the search criteria' |         optional :search, type: String, desc: 'Returns list of environments matching the search criteria' | ||||||
|  |         optional :states, type: String, values: Environment.valid_states.map(&:to_s), desc: 'List all environments that match a specific state' | ||||||
|         mutually_exclusive :name, :search, message: 'cannot be used together' |         mutually_exclusive :name, :search, message: 'cannot be used together' | ||||||
|       end |       end | ||||||
|       get ':id/environments' do |       get ':id/environments' do | ||||||
|         authorize! :read_environment, user_project |         authorize! :read_environment, user_project | ||||||
| 
 | 
 | ||||||
|         environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, params).execute |         environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, declared_params(include_missing: false)).execute | ||||||
| 
 | 
 | ||||||
|         present paginate(environments), with: Entities::Environment, current_user: current_user |         present paginate(environments), with: Entities::Environment, current_user: current_user | ||||||
|       rescue ::Environments::EnvironmentsFinder::InvalidStatesError => exception |  | ||||||
|         bad_request!(exception.message) |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       desc 'Creates a new environment' do |       desc 'Creates a new environment' do | ||||||
|  |  | ||||||
|  | @ -68,10 +68,13 @@ module API | ||||||
|         requires :value, type: String, desc: '`true` or `false` to enable/disable, a float for percentage of time' |         requires :value, type: String, desc: '`true` or `false` to enable/disable, a float for percentage of time' | ||||||
|         optional :key, type: String, desc: '`percentage_of_actors` or the default `percentage_of_time`' |         optional :key, type: String, desc: '`percentage_of_actors` or the default `percentage_of_time`' | ||||||
|         optional :feature_group, type: String, desc: 'A Feature group name' |         optional :feature_group, type: String, desc: 'A Feature group name' | ||||||
|         optional :user, type: String, desc: 'A GitLab username' |         optional :user, type: String, desc: 'A GitLab username or comma-separated multiple usernames' | ||||||
|         optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'" |         optional :group, type: String, | ||||||
|         optional :namespace, type: String, desc: "A GitLab group or user namespace path, such as 'gitlab-org'" |           desc: "A GitLab group's path, such as 'gitlab-org', or comma-separated multiple group paths" | ||||||
|         optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' |         optional :namespace, type: String, | ||||||
|  |           desc: "A GitLab group or user namespace path, such as 'john-doe', or comma-separated multiple namespace paths" | ||||||
|  |         optional :project, type: String, | ||||||
|  |           desc: "A projects path, such as `gitlab-org/gitlab-ce`, or comma-separated multiple project paths" | ||||||
|         optional :force, type: Boolean, desc: 'Skip feature flag validation checks, ie. YAML definition' |         optional :force, type: Boolean, desc: 'Skip feature flag validation checks, ie. YAML definition' | ||||||
| 
 | 
 | ||||||
|         mutually_exclusive :key, :feature_group |         mutually_exclusive :key, :feature_group | ||||||
|  | @ -110,6 +113,8 @@ module API | ||||||
| 
 | 
 | ||||||
|         present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet |         present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet | ||||||
|           with: Entities::Feature, current_user: current_user |           with: Entities::Feature, current_user: current_user | ||||||
|  |       rescue Feature::Target::UnknowTargetError => e | ||||||
|  |         bad_request!(e.message) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       desc 'Remove the gate value for the given feature' |       desc 'Remove the gate value for the given feature' | ||||||
|  |  | ||||||
|  | @ -41,29 +41,8 @@ module Backup | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def create |     def create | ||||||
|       if incremental? |       unpack(ENV.fetch('PREVIOUS_BACKUP', ENV['BACKUP'])) if incremental? | ||||||
|         unpack(ENV.fetch('PREVIOUS_BACKUP', ENV['BACKUP'])) |       run_all_create_tasks | ||||||
|         read_backup_information |  | ||||||
|         verify_backup_version |  | ||||||
|         update_backup_information |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       build_backup_information |  | ||||||
| 
 |  | ||||||
|       definitions.keys.each do |task_name| |  | ||||||
|         run_create_task(task_name) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       write_backup_information |  | ||||||
| 
 |  | ||||||
|       if skipped?('tar') |  | ||||||
|         upload |  | ||||||
|       else |  | ||||||
|         pack |  | ||||||
|         upload |  | ||||||
|         cleanup |  | ||||||
|         remove_old |  | ||||||
|       end |  | ||||||
| 
 | 
 | ||||||
|       puts_time "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ |       puts_time "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ | ||||||
|            "and are not included in this backup. You will need these files to restore a backup.\n" \ |            "and are not included in this backup. You will need these files to restore a backup.\n" \ | ||||||
|  | @ -95,22 +74,8 @@ module Backup | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def restore |     def restore | ||||||
|       cleanup_required = unpack(ENV['BACKUP']) |       unpack(ENV['BACKUP']) | ||||||
|       read_backup_information |       run_all_restore_tasks | ||||||
|       verify_backup_version |  | ||||||
| 
 |  | ||||||
|       definitions.keys.each do |task_name| |  | ||||||
|         run_restore_task(task_name) if !skipped?(task_name) && enabled_task?(task_name) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       Rake::Task['gitlab:shell:setup'].invoke |  | ||||||
|       Rake::Task['cache:clear'].invoke |  | ||||||
| 
 |  | ||||||
|       if cleanup_required |  | ||||||
|         cleanup |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       remove_tmp |  | ||||||
| 
 | 
 | ||||||
|       puts_time "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ |       puts_time "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ | ||||||
|         "and are not included in this backup. You will need to restore these files manually.".color(:red) |         "and are not included in this backup. You will need to restore these files manually.".color(:red) | ||||||
|  | @ -232,6 +197,48 @@ module Backup | ||||||
|       Files.new(progress, app_files_dir, excludes: excludes) |       Files.new(progress, app_files_dir, excludes: excludes) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     def run_all_create_tasks | ||||||
|  |       if incremental? | ||||||
|  |         read_backup_information | ||||||
|  |         verify_backup_version | ||||||
|  |         update_backup_information | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       build_backup_information | ||||||
|  | 
 | ||||||
|  |       definitions.keys.each do |task_name| | ||||||
|  |         run_create_task(task_name) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       write_backup_information | ||||||
|  | 
 | ||||||
|  |       unless skipped?('tar') | ||||||
|  |         pack | ||||||
|  |         upload | ||||||
|  |         remove_old | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |     ensure | ||||||
|  |       cleanup unless skipped?('tar') | ||||||
|  |       remove_tmp | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def run_all_restore_tasks | ||||||
|  |       read_backup_information | ||||||
|  |       verify_backup_version | ||||||
|  | 
 | ||||||
|  |       definitions.keys.each do |task_name| | ||||||
|  |         run_restore_task(task_name) if !skipped?(task_name) && enabled_task?(task_name) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       Rake::Task['gitlab:shell:setup'].invoke | ||||||
|  |       Rake::Task['cache:clear'].invoke | ||||||
|  | 
 | ||||||
|  |     ensure | ||||||
|  |       cleanup unless skipped?('tar') | ||||||
|  |       remove_tmp | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     def incremental? |     def incremental? | ||||||
|       @incremental |       @incremental | ||||||
|     end |     end | ||||||
|  | @ -299,7 +306,7 @@ module Backup | ||||||
| 
 | 
 | ||||||
|     def upload |     def upload | ||||||
|       connection_settings = Gitlab.config.backup.upload.connection |       connection_settings = Gitlab.config.backup.upload.connection | ||||||
|       if connection_settings.blank? || skipped?('remote') |       if connection_settings.blank? || skipped?('remote') || skipped?('tar') | ||||||
|         puts_time "Uploading backup archive to remote storage #{remote_directory} ... ".color(:blue) + "[SKIPPED]".color(:cyan) |         puts_time "Uploading backup archive to remote storage #{remote_directory} ... ".color(:blue) + "[SKIPPED]".color(:cyan) | ||||||
|         return |         return | ||||||
|       end |       end | ||||||
|  | @ -405,8 +412,7 @@ module Backup | ||||||
|     def unpack(source_backup_id) |     def unpack(source_backup_id) | ||||||
|       if source_backup_id.blank? && non_tarred_backup? |       if source_backup_id.blank? && non_tarred_backup? | ||||||
|         puts_time "Non tarred backup found in #{backup_path}, using that" |         puts_time "Non tarred backup found in #{backup_path}, using that" | ||||||
| 
 |         return | ||||||
|         return false |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       Dir.chdir(backup_path) do |       Dir.chdir(backup_path) do | ||||||
|  |  | ||||||
|  | @ -19,7 +19,12 @@ module Banzai | ||||||
|         def find_object(project, id) |         def find_object(project, id) | ||||||
|           return unless project.is_a?(Project) && project.valid_repo? |           return unless project.is_a?(Project) && project.valid_repo? | ||||||
| 
 | 
 | ||||||
|  |           # Optimization: try exact commit hash match first | ||||||
|  |           record = reference_cache.records_per_parent[project].fetch(id, nil) | ||||||
|  | 
 | ||||||
|  |           unless record | ||||||
|             _, record = reference_cache.records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) } |             _, record = reference_cache.records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) } | ||||||
|  |           end | ||||||
| 
 | 
 | ||||||
|           record |           record | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -281,6 +281,8 @@ class Feature | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   class Target |   class Target | ||||||
|  |     UnknowTargetError = Class.new(StandardError) | ||||||
|  | 
 | ||||||
|     attr_reader :params |     attr_reader :params | ||||||
| 
 | 
 | ||||||
|     def initialize(params) |     def initialize(params) | ||||||
|  | @ -292,7 +294,7 @@ class Feature | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def targets |     def targets | ||||||
|       [feature_group, user, project, group, namespace].compact |       [feature_group, users, projects, groups, namespaces].flatten.compact | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
|  | @ -305,29 +307,37 @@ class Feature | ||||||
|     end |     end | ||||||
|     # rubocop: enable CodeReuse/ActiveRecord |     # rubocop: enable CodeReuse/ActiveRecord | ||||||
| 
 | 
 | ||||||
|     def user |     def users | ||||||
|       return unless params.key?(:user) |       return unless params.key?(:user) | ||||||
| 
 | 
 | ||||||
|       UserFinder.new(params[:user]).find_by_username! |       params[:user].split(',').map do |arg| | ||||||
|  |         UserFinder.new(arg).find_by_username || (raise UnknowTargetError, "#{arg} is not found!") | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def project |     def projects | ||||||
|       return unless params.key?(:project) |       return unless params.key?(:project) | ||||||
| 
 | 
 | ||||||
|       Project.find_by_full_path(params[:project]) |       params[:project].split(',').map do |arg| | ||||||
|  |         Project.find_by_full_path(arg) || (raise UnknowTargetError, "#{arg} is not found!") | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def group |     def groups | ||||||
|       return unless params.key?(:group) |       return unless params.key?(:group) | ||||||
| 
 | 
 | ||||||
|       Group.find_by_full_path(params[:group]) |       params[:group].split(',').map do |arg| | ||||||
|  |         Group.find_by_full_path(arg) || (raise UnknowTargetError, "#{arg} is not found!") | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def namespace |     def namespaces | ||||||
|       return unless params.key?(:namespace) |       return unless params.key?(:namespace) | ||||||
| 
 | 
 | ||||||
|  |       params[:namespace].split(',').map do |arg| | ||||||
|         # We are interested in Group or UserNamespace |         # We are interested in Group or UserNamespace | ||||||
|       Namespace.without_project_namespaces.find_by_full_path(params[:namespace]) |         Namespace.without_project_namespaces.find_by_full_path(arg) || (raise UnknowTargetError, "#{arg} is not found!") | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -44,6 +44,7 @@ module Gitlab | ||||||
|         allow_sentry(directives) if Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn |         allow_sentry(directives) if Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn | ||||||
|         allow_framed_gitlab_paths(directives) |         allow_framed_gitlab_paths(directives) | ||||||
|         allow_customersdot(directives) if ENV['CUSTOMER_PORTAL_URL'].present? |         allow_customersdot(directives) if ENV['CUSTOMER_PORTAL_URL'].present? | ||||||
|  |         allow_review_apps(directives) if ENV['REVIEW_APPS_ENABLED'] | ||||||
| 
 | 
 | ||||||
|         # The follow section contains workarounds to patch Safari's lack of support for CSP Level 3 |         # The follow section contains workarounds to patch Safari's lack of support for CSP Level 3 | ||||||
|         # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579 |         # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579 | ||||||
|  | @ -154,6 +155,11 @@ module Gitlab | ||||||
|           append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path)) |           append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path)) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | 
 | ||||||
|  |       def self.allow_review_apps(directives) | ||||||
|  |         # Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions | ||||||
|  |         append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/') | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -87,8 +87,13 @@ module Gitlab | ||||||
|         length = [sha1.length, sha2.length].min |         length = [sha1.length, sha2.length].min | ||||||
|         return false if length < Gitlab::Git::Commit::MIN_SHA_LENGTH |         return false if length < Gitlab::Git::Commit::MIN_SHA_LENGTH | ||||||
| 
 | 
 | ||||||
|  |         # Optimization: prevent unnecessary substring creation | ||||||
|  |         if sha1.length == sha2.length | ||||||
|  |           sha1 == sha2 | ||||||
|  |         else | ||||||
|           sha1[0, length] == sha2[0, length] |           sha1[0, length] == sha2[0, length] | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,85 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | namespace :gitlab do | ||||||
|  |   namespace :db do | ||||||
|  |     TRIGGER_FUNCTION_NAME = 'gitlab_schema_prevent_write' | ||||||
|  | 
 | ||||||
|  |     desc "GitLab | DB | Install prevent write triggers on all databases" | ||||||
|  |     task lock_writes: [:environment, 'gitlab:db:validate_config'] do | ||||||
|  |       Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name| | ||||||
|  |         create_write_trigger_function(connection) | ||||||
|  | 
 | ||||||
|  |         schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection) | ||||||
|  |         Gitlab::Database::GitlabSchema.tables_to_schema.each do |table_name, schema_name| | ||||||
|  |           connection.transaction do | ||||||
|  |             if schemas_for_connection.include?(schema_name.to_sym) | ||||||
|  |               drop_write_trigger(database_name, connection, table_name) | ||||||
|  |             else | ||||||
|  |               create_write_trigger(database_name, connection, table_name) | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     desc "GitLab | DB | Remove all triggers that prevents writes from all databases" | ||||||
|  |     task unlock_writes: :environment do | ||||||
|  |       Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name| | ||||||
|  |         Gitlab::Database::GitlabSchema.tables_to_schema.each do |table_name, schema_name| | ||||||
|  |           drop_write_trigger(database_name, connection, table_name) | ||||||
|  |         end | ||||||
|  |         drop_write_trigger_function(connection) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def create_write_trigger_function(connection) | ||||||
|  |       sql = <<-SQL | ||||||
|  |         CREATE OR REPLACE FUNCTION #{TRIGGER_FUNCTION_NAME}() | ||||||
|  |           RETURNS TRIGGER AS | ||||||
|  |           $$ | ||||||
|  |           BEGIN | ||||||
|  |             RAISE EXCEPTION 'Table: "%" is write protected within this Gitlab database.', TG_TABLE_NAME | ||||||
|  |               USING ERRCODE = 'modifying_sql_data_not_permitted', | ||||||
|  |               HINT = 'Make sure you are using the right database connection'; | ||||||
|  |           END | ||||||
|  |           $$ LANGUAGE PLPGSQL | ||||||
|  |       SQL | ||||||
|  | 
 | ||||||
|  |       connection.execute(sql) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def drop_write_trigger_function(connection) | ||||||
|  |       sql = <<-SQL | ||||||
|  |         DROP FUNCTION IF EXISTS #{TRIGGER_FUNCTION_NAME}() | ||||||
|  |       SQL | ||||||
|  | 
 | ||||||
|  |       connection.execute(sql) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def create_write_trigger(database_name, connection, table_name) | ||||||
|  |       puts "#{database_name}: '#{table_name}'... Lock Writes".color(:red) | ||||||
|  |       sql = <<-SQL | ||||||
|  |           DROP TRIGGER IF EXISTS #{write_trigger_name(table_name)} ON #{table_name}; | ||||||
|  |           CREATE TRIGGER #{write_trigger_name(table_name)} | ||||||
|  |             BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE | ||||||
|  |             ON #{table_name} | ||||||
|  |             FOR EACH STATEMENT EXECUTE FUNCTION #{TRIGGER_FUNCTION_NAME}(); | ||||||
|  |       SQL | ||||||
|  | 
 | ||||||
|  |       connection.execute(sql) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def drop_write_trigger(database_name, connection, table_name) | ||||||
|  |       puts "#{database_name}: '#{table_name}'... Allow Writes".color(:green) | ||||||
|  |       sql = <<-SQL | ||||||
|  |         DROP TRIGGER IF EXISTS #{write_trigger_name(table_name)} ON #{table_name} | ||||||
|  |       SQL | ||||||
|  | 
 | ||||||
|  |       connection.execute(sql) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def write_trigger_name(table_name) | ||||||
|  |       "gitlab_schema_write_trigger_for_#{table_name}" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -1,17 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| namespace :gitlab do |  | ||||||
|   namespace :db do |  | ||||||
|     desc 'GitLab | DB | Adds primary keys to tables that only have composite unique keys' |  | ||||||
|     task composite_primary_keys_add: :environment do |  | ||||||
|       require Rails.root.join('db/optional_migrations/composite_primary_keys') |  | ||||||
|       CompositePrimaryKeysMigration.new.up |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     desc 'GitLab | DB | Removes previously added composite primary keys' |  | ||||||
|     task composite_primary_keys_drop: :environment do |  | ||||||
|       require Rails.root.join('db/optional_migrations/composite_primary_keys') |  | ||||||
|       CompositePrimaryKeysMigration.new.down |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1008,10 +1008,7 @@ msgid_plural "%{strong_start}%{count} members%{strong_end} must approve to merge | ||||||
| msgstr[0] "" | msgstr[0] "" | ||||||
| msgstr[1] "" | msgstr[1] "" | ||||||
| 
 | 
 | ||||||
| msgid "%{strong_start}%{human_size}%{strong_end} Files" | msgid "%{strong_start}%{human_size}%{strong_end} Project Storage" | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "%{strong_start}%{human_size}%{strong_end} Storage" |  | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "%{strong_start}%{release_count}%{strong_end} Release" | msgid "%{strong_start}%{release_count}%{strong_end} Release" | ||||||
|  | @ -37702,6 +37699,9 @@ msgstr "" | ||||||
| msgid "The comparison view may be inaccurate due to merge conflicts." | msgid "The comparison view may be inaccurate due to merge conflicts." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "The complete DevOps platform. One application with endless possibilities. Organizations rely on GitLab’s source code management, CI/CD, security, and more to deliver software rapidly." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "The compliance report shows the merge request violations merged in protected environments." | msgid "The compliance report shows the merge request violations merged in protected environments." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.8 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.5 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.9 KiB | 
|  | @ -11,10 +11,8 @@ module QA | ||||||
| 
 | 
 | ||||||
|           view 'app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue' do |           view 'app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue' do | ||||||
|             element :more_actions_menu |             element :more_actions_menu | ||||||
|           end |  | ||||||
| 
 |  | ||||||
|           view 'app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue' do |  | ||||||
|             element :tag_delete_button |             element :tag_delete_button | ||||||
|  |             element :tag_name_content | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def has_registry_repository?(name) |           def has_registry_repository?(name) | ||||||
|  | @ -26,11 +24,11 @@ module QA | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def has_tag?(tag_name) |           def has_tag?(tag_name) | ||||||
|             has_button?(tag_name) |             has_element?(:tag_name_content, text: tag_name) | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def has_no_tag?(tag_name) |           def has_no_tag?(tag_name) | ||||||
|             has_no_button?(tag_name) |             has_no_element?(:tag_name_content, text: tag_name) | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def click_delete |           def click_delete | ||||||
|  |  | ||||||
|  | @ -7,14 +7,15 @@ module QA | ||||||
|       include Members |       include Members | ||||||
|       include Visibility |       include Visibility | ||||||
| 
 | 
 | ||||||
|       attr_accessor :repository_storage, # requires admin access |       attr_accessor :initialize_with_readme, | ||||||
|                     :initialize_with_readme, |  | ||||||
|                     :auto_devops_enabled, |                     :auto_devops_enabled, | ||||||
|                     :github_personal_access_token, |                     :github_personal_access_token, | ||||||
|                     :github_repository_path, |                     :github_repository_path, | ||||||
|                     :gitlab_repository_path, |                     :gitlab_repository_path, | ||||||
|                     :personal_namespace |                     :personal_namespace | ||||||
| 
 | 
 | ||||||
|  |       attr_reader :repository_storage | ||||||
|  | 
 | ||||||
|       attributes :id, |       attributes :id, | ||||||
|                  :name, |                  :name, | ||||||
|                  :path, |                  :path, | ||||||
|  | @ -70,6 +71,15 @@ module QA | ||||||
|         @name = @add_name_uuid ? "#{raw_name}-#{SecureRandom.hex(8)}" : raw_name |         @name = @add_name_uuid ? "#{raw_name}-#{SecureRandom.hex(8)}" : raw_name | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       # Sets the project's repository storage | ||||||
|  |       # This feature requires admin access so be sure to fabricate the project as an admin user, and add the metadata | ||||||
|  |       # `:requires_admin` to the test it's used in. | ||||||
|  |       def repository_storage=(name) | ||||||
|  |         raise ArgumentError, "Please provide a valid repository storage name" if name.to_s.empty? | ||||||
|  | 
 | ||||||
|  |         @repository_storage = name | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       def fabricate! |       def fabricate! | ||||||
|         return if @import |         return if @import | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -297,12 +297,21 @@ function deploy() { | ||||||
| 
 | 
 | ||||||
|   create_application_secret |   create_application_secret | ||||||
| 
 | 
 | ||||||
|  | cat > review_apps.values.yml <<EOF | ||||||
|  |   gitlab: | ||||||
|  |     webservice: | ||||||
|  |       extraEnv: | ||||||
|  |         REVIEW_APPS_ENABLED: "true" | ||||||
|  |         REVIEW_APPS_MERGE_REQUEST_IID: "${CI_MERGE_REQUEST_IID}" | ||||||
|  | EOF | ||||||
|  | 
 | ||||||
| HELM_CMD=$(cat << EOF | HELM_CMD=$(cat << EOF | ||||||
|   helm upgrade \ |   helm upgrade \ | ||||||
|     --namespace="${namespace}" \ |     --namespace="${namespace}" \ | ||||||
|     --create-namespace \ |     --create-namespace \ | ||||||
|     --install \ |     --install \ | ||||||
|     --wait \ |     --wait \ | ||||||
|  |     -f review_apps.values.yml \ | ||||||
|     --timeout "${HELM_INSTALL_TIMEOUT:-20m}" \ |     --timeout "${HELM_INSTALL_TIMEOUT:-20m}" \ | ||||||
|     --set ci.branch="${CI_COMMIT_REF_NAME}" \ |     --set ci.branch="${CI_COMMIT_REF_NAME}" \ | ||||||
|     --set ci.commit.sha="${CI_COMMIT_SHORT_SHA}" \ |     --set ci.commit.sha="${CI_COMMIT_SHORT_SHA}" \ | ||||||
|  |  | ||||||
|  | @ -124,7 +124,7 @@ RSpec.describe 'Comments on personal snippets', :js do | ||||||
| 
 | 
 | ||||||
|       page.within('.current-note-edit-form') do |       page.within('.current-note-edit-form') do | ||||||
|         fill_in 'note[note]', with: 'new content' |         fill_in 'note[note]', with: 'new content' | ||||||
|         find('.btn-success').click |         find('.btn-confirm').click | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       page.within("#notes-list li#note_#{snippet_notes[0].id}") do |       page.within("#notes-list li#note_#{snippet_notes[0].id}") do | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; | import { GlSkeletonLoader } from '@gitlab/ui'; | ||||||
| import { mount } from '@vue/test-utils'; | import { mount } from '@vue/test-utils'; | ||||||
| import Vue, { nextTick } from 'vue'; | import Vue, { nextTick } from 'vue'; | ||||||
| import Vuex from 'vuex'; | import Vuex from 'vuex'; | ||||||
|  | @ -47,7 +47,7 @@ describe('IdeSidebar', () => { | ||||||
| 
 | 
 | ||||||
|     await nextTick(); |     await nextTick(); | ||||||
| 
 | 
 | ||||||
|     expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(3); |     expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(3); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('deferred rendering components', () => { |   describe('deferred rendering components', () => { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Tooling::VisualReviewHelper do | ||||||
|  |   describe '#visual_review_toolbar_options' do | ||||||
|  |     subject(:result) { helper.visual_review_toolbar_options } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       stub_env('REVIEW_APPS_MERGE_REQUEST_IID', '123') | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns the correct params' do | ||||||
|  |       expect(result).to eq( | ||||||
|  |         'data-merge-request-id': '123', | ||||||
|  |         'data-mr-url': 'https://gitlab.com', | ||||||
|  |         'data-project-id': '278964', | ||||||
|  |         'data-project-path': 'gitlab-org/gitlab', | ||||||
|  |         'data-require-auth': false, | ||||||
|  |         'id': 'review-app-toolbar-script', | ||||||
|  |         'src': 'https://gitlab.com/assets/webpack/visual_review_toolbar.js' | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -15,6 +15,7 @@ RSpec.describe Backup::Manager do | ||||||
|     # is trying to display a diff and `File.exist?` is stubbed. Adding a |     # is trying to display a diff and `File.exist?` is stubbed. Adding a | ||||||
|     # default stub fixes this. |     # default stub fixes this. | ||||||
|     allow(File).to receive(:exist?).and_call_original |     allow(File).to receive(:exist?).and_call_original | ||||||
|  |     allow(FileUtils).to receive(:rm_rf).and_call_original | ||||||
| 
 | 
 | ||||||
|     allow(progress).to receive(:puts) |     allow(progress).to receive(:puts) | ||||||
|     allow(progress).to receive(:print) |     allow(progress).to receive(:print) | ||||||
|  | @ -171,12 +172,14 @@ RSpec.describe Backup::Manager do | ||||||
|       allow(task2).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz'), full_backup_id) |       allow(task2).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz'), full_backup_id) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'executes tar' do |     it 'creates a backup tar' do | ||||||
|       travel_to(backup_time) do |       travel_to(backup_time) do | ||||||
|         subject.create # rubocop:disable Rails/SaveBang |         subject.create # rubocop:disable Rails/SaveBang | ||||||
|  |       end | ||||||
| 
 | 
 | ||||||
|       expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) |       expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) | ||||||
|       end |       expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) | ||||||
|  |       expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when BACKUP is set' do |     context 'when BACKUP is set' do | ||||||
|  | @ -203,6 +206,8 @@ RSpec.describe Backup::Manager do | ||||||
|           end.to raise_error(Backup::Error, 'Backup failed') |           end.to raise_error(Backup::Error, 'Backup failed') | ||||||
| 
 | 
 | ||||||
|           expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") |           expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") | ||||||
|  |           expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) | ||||||
|  |           expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -597,6 +602,7 @@ RSpec.describe Backup::Manager do | ||||||
|           skipped: 'tar', |           skipped: 'tar', | ||||||
|           tar_version: be_a(String) |           tar_version: be_a(String) | ||||||
|         ) |         ) | ||||||
|  |         expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -697,6 +703,8 @@ RSpec.describe Backup::Manager do | ||||||
| 
 | 
 | ||||||
|           expect(Kernel).to have_received(:system).with(*unpack_tar_cmdline) |           expect(Kernel).to have_received(:system).with(*unpack_tar_cmdline) | ||||||
|           expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) |           expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) | ||||||
|  |           expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) | ||||||
|  |           expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'untar fails' do |         context 'untar fails' do | ||||||
|  | @ -724,6 +732,8 @@ RSpec.describe Backup::Manager do | ||||||
|             end.to raise_error(Backup::Error, 'Backup failed') |             end.to raise_error(Backup::Error, 'Backup failed') | ||||||
| 
 | 
 | ||||||
|             expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") |             expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") | ||||||
|  |             expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) | ||||||
|  |             expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -786,6 +796,8 @@ RSpec.describe Backup::Manager do | ||||||
| 
 | 
 | ||||||
|           expect(Kernel).to have_received(:system).with(*unpack_tar_cmdline) |           expect(Kernel).to have_received(:system).with(*unpack_tar_cmdline) | ||||||
|           expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) |           expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) | ||||||
|  |           expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) | ||||||
|  |           expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'untar fails' do |         context 'untar fails' do | ||||||
|  | @ -817,6 +829,8 @@ RSpec.describe Backup::Manager do | ||||||
|             end.to raise_error(Backup::Error, 'Backup failed') |             end.to raise_error(Backup::Error, 'Backup failed') | ||||||
| 
 | 
 | ||||||
|             expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") |             expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") | ||||||
|  |             expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) | ||||||
|  |             expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -1001,6 +1015,8 @@ RSpec.describe Backup::Manager do | ||||||
|         subject.restore |         subject.restore | ||||||
| 
 | 
 | ||||||
|         expect(Kernel).to have_received(:system).with(*tar_cmdline) |         expect(Kernel).to have_received(:system).with(*tar_cmdline) | ||||||
|  |         expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) | ||||||
|  |         expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'tar fails' do |       context 'tar fails' do | ||||||
|  | @ -1031,22 +1047,6 @@ RSpec.describe Backup::Manager do | ||||||
|             .with(a_string_matching('GitLab version mismatch')) |             .with(a_string_matching('GitLab version mismatch')) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 |  | ||||||
|       describe 'tmp files' do |  | ||||||
|         let(:path) { File.join(Gitlab.config.backup.path, 'tmp') } |  | ||||||
| 
 |  | ||||||
|         before do |  | ||||||
|           allow(FileUtils).to receive(:rm_rf).and_call_original |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it 'removes backups/tmp dir' do |  | ||||||
|           expect(FileUtils).to receive(:rm_rf).with(path).and_call_original |  | ||||||
| 
 |  | ||||||
|           subject.restore |  | ||||||
| 
 |  | ||||||
|           expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting backups/tmp ... ') |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when there is a non-tarred backup in the directory' do |     context 'when there is a non-tarred backup in the directory' do | ||||||
|  | @ -1066,6 +1066,7 @@ RSpec.describe Backup::Manager do | ||||||
| 
 | 
 | ||||||
|         expect(progress).to have_received(:puts) |         expect(progress).to have_received(:puts) | ||||||
|           .with(a_string_matching('Non tarred backup found ')) |           .with(a_string_matching('Non tarred backup found ')) | ||||||
|  |         expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'on version mismatch' do |       context 'on version mismatch' do | ||||||
|  | @ -1082,22 +1083,6 @@ RSpec.describe Backup::Manager do | ||||||
|             .with(a_string_matching('GitLab version mismatch')) |             .with(a_string_matching('GitLab version mismatch')) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 |  | ||||||
|       describe 'tmp files' do |  | ||||||
|         let(:path) { File.join(Gitlab.config.backup.path, 'tmp') } |  | ||||||
| 
 |  | ||||||
|         before do |  | ||||||
|           allow(FileUtils).to receive(:rm_rf).and_call_original |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it 'removes backups/tmp dir' do |  | ||||||
|           expect(FileUtils).to receive(:rm_rf).with(path).and_call_original |  | ||||||
| 
 |  | ||||||
|           subject.restore |  | ||||||
| 
 |  | ||||||
|           expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting backups/tmp ... ') |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -178,6 +178,16 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do | ||||||
|             expect(directives['connect_src']).not_to include(snowplow_micro_url) |             expect(directives['connect_src']).not_to include(snowplow_micro_url) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         context 'when REVIEW_APPS_ENABLED is set' do | ||||||
|  |           before do | ||||||
|  |             stub_env('REVIEW_APPS_ENABLED', 'true') | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it 'adds gitlab-org/gitlab merge requests API endpoint to CSP' do | ||||||
|  |             expect(directives['connect_src']).to include('https://gitlab.com/api/v4/projects/278964/merge_requests/') | ||||||
|  |           end | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -211,16 +211,6 @@ RSpec.describe ProjectPresenter do | ||||||
|   context 'statistics anchors (empty repo)' do |   context 'statistics anchors (empty repo)' do | ||||||
|     let_it_be(:project) { create(:project, :empty_repo) } |     let_it_be(:project) { create(:project, :empty_repo) } | ||||||
| 
 | 
 | ||||||
|     describe '#files_anchor_data' do |  | ||||||
|       it 'returns files data' do |  | ||||||
|         expect(presenter.files_anchor_data).to have_attributes( |  | ||||||
|           is_link: true, |  | ||||||
|           label:  a_string_including('0 Bytes'), |  | ||||||
|           link: nil |  | ||||||
|         ) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     describe '#storage_anchor_data' do |     describe '#storage_anchor_data' do | ||||||
|       it 'returns storage data' do |       it 'returns storage data' do | ||||||
|         expect(presenter.storage_anchor_data).to have_attributes( |         expect(presenter.storage_anchor_data).to have_attributes( | ||||||
|  | @ -275,22 +265,22 @@ RSpec.describe ProjectPresenter do | ||||||
| 
 | 
 | ||||||
|     let(:presenter) { described_class.new(project, current_user: user) } |     let(:presenter) { described_class.new(project, current_user: user) } | ||||||
| 
 | 
 | ||||||
|     describe '#files_anchor_data' do |  | ||||||
|       it 'returns files data' do |  | ||||||
|         expect(presenter.files_anchor_data).to have_attributes( |  | ||||||
|           is_link: true, |  | ||||||
|           label:  a_string_including('0 Bytes'), |  | ||||||
|           link: presenter.project_tree_path(project) |  | ||||||
|         ) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     describe '#storage_anchor_data' do |     describe '#storage_anchor_data' do | ||||||
|       it 'returns storage data' do |       it 'returns storage data without usage quotas link for non-admin users' do | ||||||
|         expect(presenter.storage_anchor_data).to have_attributes( |         expect(presenter.storage_anchor_data).to have_attributes( | ||||||
|           is_link: true, |           is_link: true, | ||||||
|           label:  a_string_including('0 Bytes'), |           label:  a_string_including('0 Bytes'), | ||||||
|           link: presenter.project_tree_path(project) |           link: nil | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'returns storage data with usage quotas link for admin users' do | ||||||
|  |         project.add_owner(user) | ||||||
|  | 
 | ||||||
|  |         expect(presenter.storage_anchor_data).to have_attributes( | ||||||
|  |           is_link: true, | ||||||
|  |           label:  a_string_including('0 Bytes'), | ||||||
|  |           link: presenter.project_usage_quotas_path(project) | ||||||
|         ) |         ) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -113,7 +113,7 @@ RSpec.describe API::Environments do | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when filtering' do |       context 'when filtering' do | ||||||
|         let_it_be(:environment2) { create(:environment, project: project) } |         let_it_be(:stopped_environment) { create(:environment, :stopped, project: project) } | ||||||
| 
 | 
 | ||||||
|         it 'returns environment by name' do |         it 'returns environment by name' do | ||||||
|           get api("/projects/#{project.id}/environments?name=#{environment.name}", user) |           get api("/projects/#{project.id}/environments?name=#{environment.name}", user) | ||||||
|  | @ -152,11 +152,32 @@ RSpec.describe API::Environments do | ||||||
|           expect(json_response.size).to eq(0) |           expect(json_response.size).to eq(0) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'returns a 400 status code with invalid states' do |         it 'returns environment by valid state' do | ||||||
|  |           get api("/projects/#{project.id}/environments?states=available", user) | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:ok) | ||||||
|  |           expect(response).to include_pagination_headers | ||||||
|  |           expect(json_response).to be_an Array | ||||||
|  |           expect(json_response.size).to eq(1) | ||||||
|  |           expect(json_response.first['name']).to eq(environment.name) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'returns all environments when state is not specified' do | ||||||
|  |           get api("/projects/#{project.id}/environments", user) | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:ok) | ||||||
|  |           expect(response).to include_pagination_headers | ||||||
|  |           expect(json_response).to be_an Array | ||||||
|  |           expect(json_response.size).to eq(2) | ||||||
|  |           expect(json_response.first['name']).to eq(environment.name) | ||||||
|  |           expect(json_response.last['name']).to eq(stopped_environment.name) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'returns a 400 when filtering by invalid state' do | ||||||
|           get api("/projects/#{project.id}/environments?states=test", user) |           get api("/projects/#{project.id}/environments?states=test", user) | ||||||
| 
 | 
 | ||||||
|           expect(response).to have_gitlab_http_status(:bad_request) |           expect(response).to have_gitlab_http_status(:bad_request) | ||||||
|           expect(json_response['message']).to include('Requested states are invalid') |           expect(json_response['error']).to eq('states does not have a valid value') | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -168,19 +168,15 @@ RSpec.describe API::Features, stub_feature_flags: false do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       shared_examples 'does not enable the flag' do |actor_type, actor_path| |       shared_examples 'does not enable the flag' do |actor_type| | ||||||
|  |         let(:actor_path) { raise NotImplementedError } | ||||||
|  |         let(:expected_inexistent_path) { actor_path } | ||||||
|  | 
 | ||||||
|         it 'returns the current state of the flag without changes' do |         it 'returns the current state of the flag without changes' do | ||||||
|           post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor_path } |           post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor_path } | ||||||
| 
 | 
 | ||||||
|           expect(response).to have_gitlab_http_status(:created) |           expect(response).to have_gitlab_http_status(:bad_request) | ||||||
|           expect(json_response).to match( |           expect(json_response['message']).to eq("400 Bad request - #{expected_inexistent_path} is not found!") | ||||||
|             "name" => feature_name, |  | ||||||
|             "state" => "off", |  | ||||||
|             "gates" => [ |  | ||||||
|               { "key" => "boolean", "value" => false } |  | ||||||
|             ], |  | ||||||
|             'definition' => known_feature_flag_definition_hash |  | ||||||
|           ) |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -201,6 +197,19 @@ RSpec.describe API::Features, stub_feature_flags: false do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       shared_examples 'creates an enabled feature for the specified entries' do | ||||||
|  |         it do | ||||||
|  |           post api("/features/#{feature_name}", admin), params: { value: 'true', **gate_params } | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:created) | ||||||
|  |           expect(json_response['name']).to eq(feature_name) | ||||||
|  |           expect(json_response['gates']).to contain_exactly( | ||||||
|  |             { 'key' => 'boolean', 'value' => false }, | ||||||
|  |             { 'key' => 'actors', 'value' => array_including(expected_gate_params) } | ||||||
|  |           ) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       context 'when enabling for a project by path' do |       context 'when enabling for a project by path' do | ||||||
|         context 'when the project exists' do |         context 'when the project exists' do | ||||||
|           it_behaves_like 'enables the flag for the actor', :project do |           it_behaves_like 'enables the flag for the actor', :project do | ||||||
|  | @ -209,7 +218,9 @@ RSpec.describe API::Features, stub_feature_flags: false do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'when the project does not exist' do |         context 'when the project does not exist' do | ||||||
|           it_behaves_like 'does not enable the flag', :project, 'mep/to/the/mep/mep' |           it_behaves_like 'does not enable the flag', :project do | ||||||
|  |             let(:actor_path) { 'mep/to/the/mep/mep' } | ||||||
|  |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -221,7 +232,9 @@ RSpec.describe API::Features, stub_feature_flags: false do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'when the group does not exist' do |         context 'when the group does not exist' do | ||||||
|           it_behaves_like 'does not enable the flag', :group, 'not/a/group' |           it_behaves_like 'does not enable the flag', :group do | ||||||
|  |             let(:actor_path) { 'not/a/group' } | ||||||
|  |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -239,7 +252,9 @@ RSpec.describe API::Features, stub_feature_flags: false do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'when the user namespace does not exist' do |         context 'when the user namespace does not exist' do | ||||||
|           it_behaves_like 'does not enable the flag', :namespace, 'not/a/group' |           it_behaves_like 'does not enable the flag', :namespace do | ||||||
|  |             let(:actor_path) { 'not/a/group' } | ||||||
|  |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'when a project namespace exists' do |         context 'when a project namespace exists' do | ||||||
|  | @ -251,6 +266,98 @@ RSpec.describe API::Features, stub_feature_flags: false do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       context 'with multiple users' do | ||||||
|  |         let_it_be(:users) { create_list(:user, 3) } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |           let(:gate_params) { { user: users.map(&:username).join(',') } } | ||||||
|  |           let(:expected_gate_params) { users.map(&:flipper_id) } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when empty value exists between comma' do | ||||||
|  |           it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |             let(:gate_params) { { user: "#{users.first.username},,,," } } | ||||||
|  |             let(:expected_gate_params) { users.first.flipper_id } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when one of the users does not exist' do | ||||||
|  |           it_behaves_like 'does not enable the flag', :user do | ||||||
|  |             let(:actor_path) { "#{users.first.username},inexistent-entry" } | ||||||
|  |             let(:expected_inexistent_path) { "inexistent-entry" } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with multiple projects' do | ||||||
|  |         let_it_be(:projects) { create_list(:project, 3) } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |           let(:gate_params) { { project: projects.map(&:full_path).join(',') } } | ||||||
|  |           let(:expected_gate_params) { projects.map(&:flipper_id) } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when empty value exists between comma' do | ||||||
|  |           it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |             let(:gate_params) { { project: "#{projects.first.full_path},,,," } } | ||||||
|  |             let(:expected_gate_params) { projects.first.flipper_id } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when one of the projects does not exist' do | ||||||
|  |           it_behaves_like 'does not enable the flag', :project do | ||||||
|  |             let(:actor_path) { "#{projects.first.full_path},inexistent-entry" } | ||||||
|  |             let(:expected_inexistent_path) { "inexistent-entry" } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with multiple groups' do | ||||||
|  |         let_it_be(:groups) { create_list(:group, 3) } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |           let(:gate_params) { { group: groups.map(&:full_path).join(',') } } | ||||||
|  |           let(:expected_gate_params) { groups.map(&:flipper_id) } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when empty value exists between comma' do | ||||||
|  |           it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |             let(:gate_params) { { group: "#{groups.first.full_path},,,," } } | ||||||
|  |             let(:expected_gate_params) { groups.first.flipper_id } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when one of the groups does not exist' do | ||||||
|  |           it_behaves_like 'does not enable the flag', :group do | ||||||
|  |             let(:actor_path) { "#{groups.first.full_path},inexistent-entry" } | ||||||
|  |             let(:expected_inexistent_path) { "inexistent-entry" } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with multiple namespaces' do | ||||||
|  |         let_it_be(:namespaces) { create_list(:namespace, 3) } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |           let(:gate_params) { { namespace: namespaces.map(&:full_path).join(',') } } | ||||||
|  |           let(:expected_gate_params) { namespaces.map(&:flipper_id) } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when empty value exists between comma' do | ||||||
|  |           it_behaves_like 'creates an enabled feature for the specified entries' do | ||||||
|  |             let(:gate_params) { { namespace: "#{namespaces.first.full_path},,,," } } | ||||||
|  |             let(:expected_gate_params) { namespaces.first.flipper_id } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when one of the namespaces does not exist' do | ||||||
|  |           it_behaves_like 'does not enable the flag', :namespace do | ||||||
|  |             let(:actor_path) { "#{namespaces.first.full_path},inexistent-entry" } | ||||||
|  |             let(:expected_inexistent_path) { "inexistent-entry" } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       it 'creates a feature with the given percentage of time if passed an integer' do |       it 'creates a feature with the given percentage of time if passed an integer' do | ||||||
|         post api("/features/#{feature_name}", admin), params: { value: '50' } |         post api("/features/#{feature_name}", admin), params: { value: '50' } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,6 +3,15 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe PwaController do | RSpec.describe PwaController do | ||||||
|  |   describe 'GET #manifest' do | ||||||
|  |     it 'responds with json' do | ||||||
|  |       get manifest_path(format: :json) | ||||||
|  | 
 | ||||||
|  |       expect(response.body).to include('The complete DevOps platform.') | ||||||
|  |       expect(response).to have_gitlab_http_status(:success) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe 'GET #offline' do |   describe 'GET #offline' do | ||||||
|     it 'responds with static HTML page' do |     it 'responds with static HTML page' do | ||||||
|       get offline_path |       get offline_path | ||||||
|  |  | ||||||
|  | @ -239,6 +239,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     let(:gitlab_schema) { "gitlab_#{tracking_database}" } | ||||||
|     let!(:migration) do |     let!(:migration) do | ||||||
|       create( |       create( | ||||||
|         :batched_background_migration, |         :batched_background_migration, | ||||||
|  | @ -249,10 +250,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d | ||||||
|         batch_size: batch_size, |         batch_size: batch_size, | ||||||
|         sub_batch_size: sub_batch_size, |         sub_batch_size: sub_batch_size, | ||||||
|         job_class_name: 'ExampleDataMigration', |         job_class_name: 'ExampleDataMigration', | ||||||
|         job_arguments: [1] |         job_arguments: [1], | ||||||
|  |         gitlab_schema: gitlab_schema | ||||||
|       ) |       ) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     let(:base_model) { Gitlab::Database.database_base_models[tracking_database] } | ||||||
|     let(:table_name) { 'example_data' } |     let(:table_name) { 'example_data' } | ||||||
|     let(:batch_size) { 5 } |     let(:batch_size) { 5 } | ||||||
|     let(:sub_batch_size) { 2 } |     let(:sub_batch_size) { 2 } | ||||||
|  | @ -289,7 +292,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d | ||||||
|         WHERE some_column = #{migration_records - 5}; |         WHERE some_column = #{migration_records - 5}; | ||||||
|       SQL |       SQL | ||||||
| 
 | 
 | ||||||
|       stub_feature_flags(execute_batched_migrations_on_schedule: true) |       stub_feature_flags(feature_flag => true) | ||||||
| 
 | 
 | ||||||
|       stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class) |       stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,152 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'rake_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_record_base do | ||||||
|  |   before :all do | ||||||
|  |     Rake.application.rake_require 'active_record/railties/databases' | ||||||
|  |     Rake.application.rake_require 'tasks/seed_fu' | ||||||
|  |     Rake.application.rake_require 'tasks/gitlab/db/validate_config' | ||||||
|  |     Rake.application.rake_require 'tasks/gitlab/db/lock_writes' | ||||||
|  | 
 | ||||||
|  |     # empty task as env is already loaded | ||||||
|  |     Rake::Task.define_task :environment | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   let!(:project) { create(:project) } | ||||||
|  |   let!(:ci_build) { create(:ci_build) } | ||||||
|  |   let(:main_connection) { ApplicationRecord.connection } | ||||||
|  |   let(:ci_connection) { Ci::ApplicationRecord.connection } | ||||||
|  | 
 | ||||||
|  |   context 'single database' do | ||||||
|  |     before do | ||||||
|  |       skip_if_multiple_databases_are_setup | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when locking writes' do | ||||||
|  |       it 'does not add any triggers to the main schema tables' do | ||||||
|  |         expect do | ||||||
|  |           run_rake_task('gitlab:db:lock_writes') | ||||||
|  |         end.to change { | ||||||
|  |           number_of_triggers(main_connection) | ||||||
|  |         }.by(0) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'will be still able to modify tables that belong to the main two schemas' do | ||||||
|  |         run_rake_task('gitlab:db:lock_writes') | ||||||
|  |         expect do | ||||||
|  |           Project.last.touch | ||||||
|  |           Ci::Build.last.touch | ||||||
|  |         end.not_to raise_error | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'multiple databases' do | ||||||
|  |     before do | ||||||
|  |       skip_if_multiple_databases_not_setup | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when locking writes' do | ||||||
|  |       it 'adds 3 triggers to the ci schema tables on the main database' do | ||||||
|  |         expect do | ||||||
|  |           run_rake_task('gitlab:db:lock_writes') | ||||||
|  |         end.to change { | ||||||
|  |           number_of_triggers_on(main_connection, Ci::Build.table_name) | ||||||
|  |         }.by(3) # Triggers to block INSERT / UPDATE / DELETE | ||||||
|  |         # Triggers on TRUNCATE are not added to the information_schema.triggers | ||||||
|  |         # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'adds 3 triggers to the main schema tables on the ci database' do | ||||||
|  |         expect do | ||||||
|  |           run_rake_task('gitlab:db:lock_writes') | ||||||
|  |         end.to change { | ||||||
|  |           number_of_triggers_on(ci_connection, Project.table_name) | ||||||
|  |         }.by(3) # Triggers to block INSERT / UPDATE / DELETE | ||||||
|  |         # Triggers on TRUNCATE are not added to the information_schema.triggers | ||||||
|  |         # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'still allows writes on the tables with the correct connections' do | ||||||
|  |         Project.update_all(updated_at: Time.now) | ||||||
|  |         Ci::Build.update_all(updated_at: Time.now) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'still allows writing to gitlab_shared schema on any connection' do | ||||||
|  |         connections = [main_connection, ci_connection] | ||||||
|  |         connections.each do |connection| | ||||||
|  |           Gitlab::Database::SharedModel.using_connection(connection) do | ||||||
|  |             LooseForeignKeys::DeletedRecord.create!( | ||||||
|  |               fully_qualified_table_name: "public.projects", | ||||||
|  |               primary_key_value: 1, | ||||||
|  |               cleanup_attempts: 0 | ||||||
|  |             ) | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'prevents writes on the main tables on the ci database' do | ||||||
|  |         run_rake_task('gitlab:db:lock_writes') | ||||||
|  |         expect do | ||||||
|  |           ci_connection.execute("delete from projects") | ||||||
|  |         end.to raise_error(ActiveRecord::StatementInvalid, /Table: "projects" is write protected/) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'prevents writes on the ci tables on the main database' do | ||||||
|  |         run_rake_task('gitlab:db:lock_writes') | ||||||
|  |         expect do | ||||||
|  |           main_connection.execute("delete from ci_builds") | ||||||
|  |         end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_builds" is write protected/) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'prevents truncating a ci table on the main database' do | ||||||
|  |         run_rake_task('gitlab:db:lock_writes') | ||||||
|  |         expect do | ||||||
|  |           main_connection.execute("truncate ci_build_needs") | ||||||
|  |         end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_build_needs" is write protected/) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when unlocking writes' do | ||||||
|  |       before do | ||||||
|  |         run_rake_task('gitlab:db:lock_writes') | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'removes the write protection triggers from the gitlab_main tables on the ci database' do | ||||||
|  |         expect do | ||||||
|  |           run_rake_task('gitlab:db:unlock_writes') | ||||||
|  |         end.to change { | ||||||
|  |           number_of_triggers_on(ci_connection, Project.table_name) | ||||||
|  |         }.by(-3) # Triggers to block INSERT / UPDATE / DELETE | ||||||
|  |         # Triggers on TRUNCATE are not added to the information_schema.triggers | ||||||
|  |         # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us | ||||||
|  | 
 | ||||||
|  |         expect do | ||||||
|  |           ci_connection.execute("delete from projects") | ||||||
|  |         end.not_to raise_error | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'removes the write protection triggers from the gitlab_ci tables on the main database' do | ||||||
|  |         expect do | ||||||
|  |           run_rake_task('gitlab:db:unlock_writes') | ||||||
|  |         end.to change { | ||||||
|  |           number_of_triggers_on(main_connection, Ci::Build.table_name) | ||||||
|  |         }.by(-3) | ||||||
|  | 
 | ||||||
|  |         expect do | ||||||
|  |           main_connection.execute("delete from ci_builds") | ||||||
|  |         end.not_to raise_error | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def number_of_triggers(connection) | ||||||
|  |     connection.select_value("SELECT count(*) FROM information_schema.triggers") | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def number_of_triggers_on(connection, table_name) | ||||||
|  |     connection | ||||||
|  |       .select_value("SELECT count(*) FROM information_schema.triggers WHERE event_object_table=$1", nil, [table_name]) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -14,6 +14,35 @@ RSpec.describe 'layouts/application' do | ||||||
|     allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user)) |     allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user)) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe "visual review toolbar" do | ||||||
|  |     context "ENV['REVIEW_APPS_ENABLED'] is set to true" do | ||||||
|  |       before do | ||||||
|  |         stub_env( | ||||||
|  |           'REVIEW_APPS_ENABLED' => true, | ||||||
|  |           'REVIEW_APPS_MERGE_REQUEST_IID' => '123' | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'renders the visual review toolbar' do | ||||||
|  |         render | ||||||
|  | 
 | ||||||
|  |         expect(rendered).to include('review-app-toolbar-script') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context "ENV['REVIEW_APPS_ENABLED'] is set to false" do | ||||||
|  |       before do | ||||||
|  |         stub_env('REVIEW_APPS_ENABLED', false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'does not render the visual review toolbar' do | ||||||
|  |         render | ||||||
|  | 
 | ||||||
|  |         expect(rendered).not_to include('review-app-toolbar-script') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   context 'body data elements for pageview context' do |   context 'body data elements for pageview context' do | ||||||
|     let(:body_data) do |     let(:body_data) do | ||||||
|       { |       { | ||||||
|  |  | ||||||
|  | @ -2,6 +2,6 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Database::BatchedBackgroundMigration::CiDatabaseWorker, :clean_gitlab_redis_shared_state, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/362821' do | RSpec.describe Database::BatchedBackgroundMigration::CiDatabaseWorker, :clean_gitlab_redis_shared_state do | ||||||
|   it_behaves_like 'it runs batched background migration jobs', 'ci', feature_flag: :execute_batched_migrations_on_schedule_ci_database |   it_behaves_like 'it runs batched background migration jobs', 'ci', feature_flag: :execute_batched_migrations_on_schedule_ci_database | ||||||
| end | end | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue