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 ~transient \ | ||||
|         --tag ~skip_signup_disabled \ | ||||
|         --tag ~requires_git_protocol_v2 \ | ||||
|         --tag ~requires_praefect \ | ||||
|         --force-color \ | ||||
|         --order random \ | ||||
|         --format documentation \ | ||||
|  |  | |||
|  | @ -1001,7 +1001,6 @@ Layout/LineLength: | |||
|     - 'db/migrate/20220310101118_update_holder_name_limit.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/optional_migrations/composite_primary_keys.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/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/20210709085759_index_batched_migration_jobs_by_max_value.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/20210420121149_backfill_conversion_of_ci_job_artifacts.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> | ||||
| import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; | ||||
| import { GlSkeletonLoader } from '@gitlab/ui'; | ||||
| import { mapState, mapGetters } from 'vuex'; | ||||
| import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants'; | ||||
| import ActivityBar from './activity_bar.vue'; | ||||
|  | @ -10,7 +10,7 @@ import ResizablePanel from './resizable_panel.vue'; | |||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     GlSkeletonLoading, | ||||
|     GlSkeletonLoader, | ||||
|     ResizablePanel, | ||||
|     ActivityBar, | ||||
|     IdeTree, | ||||
|  | @ -38,7 +38,7 @@ export default { | |||
|     <template v-if="loading"> | ||||
|       <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"> | ||||
|           <gl-skeleton-loading /> | ||||
|           <gl-skeleton-loader /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </template> | ||||
|  |  | |||
|  | @ -132,6 +132,7 @@ export default { | |||
|         <div | ||||
|           v-gl-tooltip="{ title: tag.name }" | ||||
|           data-testid="name" | ||||
|           data-qa-selector="tag_name_content" | ||||
|           class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap" | ||||
|           :class="mobileClasses" | ||||
|         > | ||||
|  |  | |||
|  | @ -7,6 +7,9 @@ class PwaController < ApplicationController # rubocop:disable Gitlab/NamespacedC | |||
| 
 | ||||
|   skip_before_action :authenticate_user! | ||||
| 
 | ||||
|   def manifest | ||||
|   end | ||||
| 
 | ||||
|   def offline | ||||
|   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, | ||||
|       branches_anchor_data, | ||||
|       tags_anchor_data, | ||||
|       files_anchor_data, | ||||
|       storage_anchor_data, | ||||
|       releases_anchor_data | ||||
|     ].compact.select(&:is_link) | ||||
|  | @ -161,26 +160,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated | |||
|     can_current_user_push_to_branch?(default_branch) | ||||
|   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 | ||||
|     can_show_quota = can?(current_user, :admin_project, project) && !empty_repo? | ||||
|     AnchorData.new(true, | ||||
|                    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), | ||||
|                      strong_start: '<strong class="project-stat-value">'.html_safe, | ||||
|                      strong_end: '</strong>'.html_safe | ||||
|                    }, | ||||
|                    empty_repo? ? nil : project_tree_path(project)) | ||||
|                    can_show_quota ? project_usage_quotas_path(project) : nil) | ||||
|   end | ||||
| 
 | ||||
|   def releases_anchor_data | ||||
|  |  | |||
|  | @ -68,6 +68,7 @@ | |||
| 
 | ||||
|   %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: '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 } | ||||
|     = render "layouts/init_auto_complete" if @gfm_form | ||||
|     = render "layouts/init_client_detection_flags" | ||||
|     = render "layouts/visual_review" if ENV['REVIEW_APPS_ENABLED'] | ||||
|     = render 'peek/bar' | ||||
|     = header_message | ||||
|     = 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 | ||||
|       .settings-message.note-edit-warning.js-finish-edit-warning | ||||
|         = _("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' } | ||||
|         = _("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']['cron'] ||= '0 1 * * *' | ||||
| 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 | ||||
|   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']['cron'] ||= '7-59/15 * * * *' | ||||
|   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']['cron'] ||= '*/4 * * * *' | ||||
|   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 'offline' => "pwa#offline" | ||||
|     get 'manifest' => "pwa#manifest", constraints: lambda { |req| req.format == :json } | ||||
| 
 | ||||
|     # '/-/health' implemented by BasicHealthCheck middleware | ||||
|     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 | | ||||
| | `key` | string | no | `percentage_of_actors` or `percentage_of_time` (default) | | ||||
| | `feature_group` | string | no | A Feature group name | | ||||
| | `user` | string | no | A GitLab username | | ||||
| | `group` | string | no | A GitLab group's path, for example `gitlab-org` | | ||||
| | `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. | | ||||
| | `project` | string | no | A projects path, for example `gitlab-org/gitlab-foss` | | ||||
| | `user` | string | no | A GitLab username or comma-separated multiple usernames | | ||||
| | `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 `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`, or comma-separated multiple project paths | | ||||
| | `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`, | ||||
|  |  | |||
|  | @ -74,6 +74,8 @@ If you set a quota for a subgroup, it is not used. | |||
| 
 | ||||
| ## 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. | ||||
| 
 | ||||
| Prerequisite: | ||||
|  |  | |||
|  | @ -226,33 +226,47 @@ A fork *does* copy the CI/CD settings of the cloned repository. | |||
| ### Create a specific runner | ||||
| 
 | ||||
| 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: | ||||
| 
 | ||||
| 1. [Install runner](https://docs.gitlab.com/runner/install/). | ||||
| 1. Go to the project's **Settings > CI/CD** and expand the **Runners** section. | ||||
| 1. Note the URL and token. | ||||
| 1. [Install GitLab Runner](https://docs.gitlab.com/runner/install/). | ||||
| 1. On the top bar, select **Menu > Projects** and find the project where you want to use the runner. | ||||
| 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/). | ||||
| 
 | ||||
| ### 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 to apply to additional projects. | ||||
| ### Enable a specific runner for a different project | ||||
| 
 | ||||
| - You must have the Owner role for the | ||||
|   project. | ||||
| After a specific runner is created, you can enable it for other projects. | ||||
| 
 | ||||
| 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). | ||||
| 
 | ||||
| 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. Click **Enable for this project** or **Disable for this project**. | ||||
| 1. On the top bar, select **Menu > Projects** and find the project where you want to enable the runner. | ||||
| 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. | ||||
| 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. | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| 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: | ||||
| 
 | ||||
| 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. Click the pencil button. | ||||
| 1. Select **Edit** (**{pencil}**). | ||||
| 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. | ||||
| 
 | ||||
| ## 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 | ||||
| 
 | ||||
| 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: | ||||
| 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 | ||||
| 
 | ||||
| Archiving jobs is useful for reducing the CI/CD footprint on the system by removing some | ||||
|  |  | |||
|  | @ -22,16 +22,15 @@ module API | |||
|         use :pagination | ||||
|         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 :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' | ||||
|       end | ||||
|       get ':id/environments' do | ||||
|         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 | ||||
|       rescue ::Environments::EnvironmentsFinder::InvalidStatesError => exception | ||||
|         bad_request!(exception.message) | ||||
|       end | ||||
| 
 | ||||
|       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' | ||||
|         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 :user, type: String, desc: 'A GitLab username' | ||||
|         optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'" | ||||
|         optional :namespace, type: String, desc: "A GitLab group or user namespace path, such as 'gitlab-org'" | ||||
|         optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' | ||||
|         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', or comma-separated multiple group paths" | ||||
|         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' | ||||
| 
 | ||||
|         mutually_exclusive :key, :feature_group | ||||
|  | @ -110,6 +113,8 @@ module API | |||
| 
 | ||||
|         present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet | ||||
|           with: Entities::Feature, current_user: current_user | ||||
|       rescue Feature::Target::UnknowTargetError => e | ||||
|         bad_request!(e.message) | ||||
|       end | ||||
| 
 | ||||
|       desc 'Remove the gate value for the given feature' | ||||
|  |  | |||
|  | @ -41,29 +41,8 @@ module Backup | |||
|     end | ||||
| 
 | ||||
|     def create | ||||
|       if incremental? | ||||
|         unpack(ENV.fetch('PREVIOUS_BACKUP', ENV['BACKUP'])) | ||||
|         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 | ||||
|       unpack(ENV.fetch('PREVIOUS_BACKUP', ENV['BACKUP'])) if incremental? | ||||
|       run_all_create_tasks | ||||
| 
 | ||||
|       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" \ | ||||
|  | @ -95,22 +74,8 @@ module Backup | |||
|     end | ||||
| 
 | ||||
|     def restore | ||||
|       cleanup_required = unpack(ENV['BACKUP']) | ||||
|       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 | ||||
| 
 | ||||
|       if cleanup_required | ||||
|         cleanup | ||||
|       end | ||||
| 
 | ||||
|       remove_tmp | ||||
|       unpack(ENV['BACKUP']) | ||||
|       run_all_restore_tasks | ||||
| 
 | ||||
|       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) | ||||
|  | @ -232,6 +197,48 @@ module Backup | |||
|       Files.new(progress, app_files_dir, excludes: excludes) | ||||
|     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? | ||||
|       @incremental | ||||
|     end | ||||
|  | @ -299,7 +306,7 @@ module Backup | |||
| 
 | ||||
|     def upload | ||||
|       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) | ||||
|         return | ||||
|       end | ||||
|  | @ -405,8 +412,7 @@ module Backup | |||
|     def unpack(source_backup_id) | ||||
|       if source_backup_id.blank? && non_tarred_backup? | ||||
|         puts_time "Non tarred backup found in #{backup_path}, using that" | ||||
| 
 | ||||
|         return false | ||||
|         return | ||||
|       end | ||||
| 
 | ||||
|       Dir.chdir(backup_path) do | ||||
|  |  | |||
|  | @ -19,7 +19,12 @@ module Banzai | |||
|         def find_object(project, id) | ||||
|           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) } | ||||
|           end | ||||
| 
 | ||||
|           record | ||||
|         end | ||||
|  |  | |||
|  | @ -281,6 +281,8 @@ class Feature | |||
|   end | ||||
| 
 | ||||
|   class Target | ||||
|     UnknowTargetError = Class.new(StandardError) | ||||
| 
 | ||||
|     attr_reader :params | ||||
| 
 | ||||
|     def initialize(params) | ||||
|  | @ -292,7 +294,7 @@ class Feature | |||
|     end | ||||
| 
 | ||||
|     def targets | ||||
|       [feature_group, user, project, group, namespace].compact | ||||
|       [feature_group, users, projects, groups, namespaces].flatten.compact | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
|  | @ -305,29 +307,37 @@ class Feature | |||
|     end | ||||
|     # rubocop: enable CodeReuse/ActiveRecord | ||||
| 
 | ||||
|     def user | ||||
|     def users | ||||
|       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 | ||||
| 
 | ||||
|     def project | ||||
|     def projects | ||||
|       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 | ||||
| 
 | ||||
|     def group | ||||
|     def groups | ||||
|       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 | ||||
| 
 | ||||
|     def namespace | ||||
|     def namespaces | ||||
|       return unless params.key?(:namespace) | ||||
| 
 | ||||
|       params[:namespace].split(',').map do |arg| | ||||
|         # 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 | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ module Gitlab | |||
|         allow_sentry(directives) if Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn | ||||
|         allow_framed_gitlab_paths(directives) | ||||
|         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 | ||||
|         # 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)) | ||||
|         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 | ||||
|  |  | |||
|  | @ -87,8 +87,13 @@ module Gitlab | |||
|         length = [sha1.length, sha2.length].min | ||||
|         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] | ||||
|         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[1] "" | ||||
| 
 | ||||
| msgid "%{strong_start}%{human_size}%{strong_end} Files" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "%{strong_start}%{human_size}%{strong_end} Storage" | ||||
| msgid "%{strong_start}%{human_size}%{strong_end} Project Storage" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "%{strong_start}%{release_count}%{strong_end} Release" | ||||
|  | @ -37702,6 +37699,9 @@ msgstr "" | |||
| msgid "The comparison view may be inaccurate due to merge conflicts." | ||||
| 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." | ||||
| 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 | ||||
|             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_name_content | ||||
|           end | ||||
| 
 | ||||
|           def has_registry_repository?(name) | ||||
|  | @ -26,11 +24,11 @@ module QA | |||
|           end | ||||
| 
 | ||||
|           def has_tag?(tag_name) | ||||
|             has_button?(tag_name) | ||||
|             has_element?(:tag_name_content, text: tag_name) | ||||
|           end | ||||
| 
 | ||||
|           def has_no_tag?(tag_name) | ||||
|             has_no_button?(tag_name) | ||||
|             has_no_element?(:tag_name_content, text: tag_name) | ||||
|           end | ||||
| 
 | ||||
|           def click_delete | ||||
|  |  | |||
|  | @ -7,14 +7,15 @@ module QA | |||
|       include Members | ||||
|       include Visibility | ||||
| 
 | ||||
|       attr_accessor :repository_storage, # requires admin access | ||||
|                     :initialize_with_readme, | ||||
|       attr_accessor :initialize_with_readme, | ||||
|                     :auto_devops_enabled, | ||||
|                     :github_personal_access_token, | ||||
|                     :github_repository_path, | ||||
|                     :gitlab_repository_path, | ||||
|                     :personal_namespace | ||||
| 
 | ||||
|       attr_reader :repository_storage | ||||
| 
 | ||||
|       attributes :id, | ||||
|                  :name, | ||||
|                  :path, | ||||
|  | @ -70,6 +71,15 @@ module QA | |||
|         @name = @add_name_uuid ? "#{raw_name}-#{SecureRandom.hex(8)}" : raw_name | ||||
|       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! | ||||
|         return if @import | ||||
| 
 | ||||
|  |  | |||
|  | @ -297,12 +297,21 @@ function deploy() { | |||
| 
 | ||||
|   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 upgrade \ | ||||
|     --namespace="${namespace}" \ | ||||
|     --create-namespace \ | ||||
|     --install \ | ||||
|     --wait \ | ||||
|     -f review_apps.values.yml \ | ||||
|     --timeout "${HELM_INSTALL_TIMEOUT:-20m}" \ | ||||
|     --set ci.branch="${CI_COMMIT_REF_NAME}" \ | ||||
|     --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 | ||||
|         fill_in 'note[note]', with: 'new content' | ||||
|         find('.btn-success').click | ||||
|         find('.btn-confirm').click | ||||
|       end | ||||
| 
 | ||||
|       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 Vue, { nextTick } from 'vue'; | ||||
| import Vuex from 'vuex'; | ||||
|  | @ -47,7 +47,7 @@ describe('IdeSidebar', () => { | |||
| 
 | ||||
|     await nextTick(); | ||||
| 
 | ||||
|     expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(3); | ||||
|     expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(3); | ||||
|   }); | ||||
| 
 | ||||
|   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 | ||||
|     # default stub fixes this. | ||||
|     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(: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) | ||||
|     end | ||||
| 
 | ||||
|     it 'executes tar' do | ||||
|     it 'creates a backup tar' do | ||||
|       travel_to(backup_time) do | ||||
|         subject.create # rubocop:disable Rails/SaveBang | ||||
|       end | ||||
| 
 | ||||
|       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 | ||||
| 
 | ||||
|     context 'when BACKUP is set' do | ||||
|  | @ -203,6 +206,8 @@ RSpec.describe Backup::Manager do | |||
|           end.to raise_error(Backup::Error, 'Backup 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 | ||||
| 
 | ||||
|  | @ -597,6 +602,7 @@ RSpec.describe Backup::Manager do | |||
|           skipped: 'tar', | ||||
|           tar_version: be_a(String) | ||||
|         ) | ||||
|         expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||
|       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(*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 | ||||
| 
 | ||||
|         context 'untar fails' do | ||||
|  | @ -724,6 +732,8 @@ RSpec.describe Backup::Manager do | |||
|             end.to raise_error(Backup::Error, 'Backup 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 | ||||
| 
 | ||||
|  | @ -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(*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 | ||||
| 
 | ||||
|         context 'untar fails' do | ||||
|  | @ -817,6 +829,8 @@ RSpec.describe Backup::Manager do | |||
|             end.to raise_error(Backup::Error, 'Backup 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 | ||||
| 
 | ||||
|  | @ -1001,6 +1015,8 @@ RSpec.describe Backup::Manager do | |||
|         subject.restore | ||||
| 
 | ||||
|         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 | ||||
| 
 | ||||
|       context 'tar fails' do | ||||
|  | @ -1031,22 +1047,6 @@ RSpec.describe Backup::Manager do | |||
|             .with(a_string_matching('GitLab version mismatch')) | ||||
|         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 | ||||
| 
 | ||||
|     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) | ||||
|           .with(a_string_matching('Non tarred backup found ')) | ||||
|         expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp')) | ||||
|       end | ||||
| 
 | ||||
|       context 'on version mismatch' do | ||||
|  | @ -1082,22 +1083,6 @@ RSpec.describe Backup::Manager do | |||
|             .with(a_string_matching('GitLab version mismatch')) | ||||
|         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 | ||||
|  |  | |||
|  | @ -178,6 +178,16 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do | |||
|             expect(directives['connect_src']).not_to include(snowplow_micro_url) | ||||
|           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 | ||||
|  |  | |||
|  | @ -211,16 +211,6 @@ RSpec.describe ProjectPresenter do | |||
|   context 'statistics anchors (empty repo)' do | ||||
|     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 | ||||
|       it 'returns storage data' do | ||||
|         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) } | ||||
| 
 | ||||
|     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 | ||||
|       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( | ||||
|           is_link: true, | ||||
|           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 | ||||
|  |  | |||
|  | @ -113,7 +113,7 @@ RSpec.describe API::Environments do | |||
|       end | ||||
| 
 | ||||
|       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 | ||||
|           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) | ||||
|         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) | ||||
| 
 | ||||
|           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 | ||||
|  |  | |||
|  | @ -168,19 +168,15 @@ RSpec.describe API::Features, stub_feature_flags: false do | |||
|         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 | ||||
|           post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor_path } | ||||
| 
 | ||||
|           expect(response).to have_gitlab_http_status(:created) | ||||
|           expect(json_response).to match( | ||||
|             "name" => feature_name, | ||||
|             "state" => "off", | ||||
|             "gates" => [ | ||||
|               { "key" => "boolean", "value" => false } | ||||
|             ], | ||||
|             'definition' => known_feature_flag_definition_hash | ||||
|           ) | ||||
|           expect(response).to have_gitlab_http_status(:bad_request) | ||||
|           expect(json_response['message']).to eq("400 Bad request - #{expected_inexistent_path} is not found!") | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  | @ -201,6 +197,19 @@ RSpec.describe API::Features, stub_feature_flags: false do | |||
|         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 the project exists' 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 | ||||
| 
 | ||||
|         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 | ||||
| 
 | ||||
|  | @ -221,7 +232,9 @@ RSpec.describe API::Features, stub_feature_flags: false do | |||
|         end | ||||
| 
 | ||||
|         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 | ||||
| 
 | ||||
|  | @ -239,7 +252,9 @@ RSpec.describe API::Features, stub_feature_flags: false do | |||
|         end | ||||
| 
 | ||||
|         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 | ||||
| 
 | ||||
|         context 'when a project namespace exists' do | ||||
|  | @ -251,6 +266,98 @@ RSpec.describe API::Features, stub_feature_flags: false do | |||
|         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 | ||||
|         post api("/features/#{feature_name}", admin), params: { value: '50' } | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,6 +3,15 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| 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 | ||||
|     it 'responds with static HTML page' do | ||||
|       get offline_path | ||||
|  |  | |||
|  | @ -239,6 +239,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     let(:gitlab_schema) { "gitlab_#{tracking_database}" } | ||||
|     let!(:migration) do | ||||
|       create( | ||||
|         :batched_background_migration, | ||||
|  | @ -249,10 +250,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d | |||
|         batch_size: batch_size, | ||||
|         sub_batch_size: sub_batch_size, | ||||
|         job_class_name: 'ExampleDataMigration', | ||||
|         job_arguments: [1] | ||||
|         job_arguments: [1], | ||||
|         gitlab_schema: gitlab_schema | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     let(:base_model) { Gitlab::Database.database_base_models[tracking_database] } | ||||
|     let(:table_name) { 'example_data' } | ||||
|     let(:batch_size) { 5 } | ||||
|     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}; | ||||
|       SQL | ||||
| 
 | ||||
|       stub_feature_flags(execute_batched_migrations_on_schedule: true) | ||||
|       stub_feature_flags(feature_flag => true) | ||||
| 
 | ||||
|       stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class) | ||||
|     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)) | ||||
|   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 | ||||
|     let(:body_data) do | ||||
|       { | ||||
|  |  | |||
|  | @ -2,6 +2,6 @@ | |||
| 
 | ||||
| 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 | ||||
| end | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue