Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									f7406657b9
								
							
						
					
					
						commit
						81a37f0581
					
				
							
								
								
									
										21
									
								
								.stylelintrc
								
								
								
								
							
							
						
						
									
										21
									
								
								.stylelintrc
								
								
								
								
							|  | @ -13,27 +13,6 @@ | |||
|       "./scripts/frontend/stylelint/stylelint-utility-classes.js", | ||||
|    ], | ||||
|    "rules":{ | ||||
|       "at-rule-disallowed-list": ["extend"], | ||||
|       "max-nesting-depth": [ | ||||
|          3, | ||||
|          { | ||||
|             "ignoreAtRules":[ | ||||
|                "each", | ||||
|                "media", | ||||
|                "supports", | ||||
|                "include" | ||||
|             ], | ||||
|             "severity":"warning" | ||||
|          } | ||||
|       ], | ||||
|       "selector-max-compound-selectors":[3, { "severity": "warning" }], | ||||
|       "stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }], | ||||
|       "declaration-block-no-duplicate-properties": [ | ||||
|         true, | ||||
|         { | ||||
|           "ignore": ["consecutive-duplicates"] | ||||
|         } | ||||
|       ], | ||||
|       "no-eol-whitespace": true, | ||||
|    } | ||||
| } | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| c959b634bd98035355259ffcd421d7a43e015982 | ||||
| 7b9cd199b0851fd1b6615e0798f2aafddafd63cb | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										2
									
								
								Gemfile
								
								
								
								
							|  | @ -538,4 +538,4 @@ gem 'ipaddress', '~> 0.8.3' | |||
| 
 | ||||
| gem 'parslet', '~> 1.8' | ||||
| 
 | ||||
| gem 'ipynbdiff', '0.3.6' | ||||
| gem 'ipynbdiff', '0.3.7' | ||||
|  |  | |||
|  | @ -640,7 +640,7 @@ GEM | |||
|     invisible_captcha (1.1.0) | ||||
|       rails (>= 4.2) | ||||
|     ipaddress (0.8.3) | ||||
|     ipynbdiff (0.3.6) | ||||
|     ipynbdiff (0.3.7) | ||||
|       diffy (= 3.3.0) | ||||
|       json (= 2.5.1) | ||||
|     jaeger-client (1.1.0) | ||||
|  | @ -1504,7 +1504,7 @@ DEPENDENCIES | |||
|   icalendar | ||||
|   invisible_captcha (~> 1.1.0) | ||||
|   ipaddress (~> 0.8.3) | ||||
|   ipynbdiff (= 0.3.6) | ||||
|   ipynbdiff (= 0.3.7) | ||||
|   jira-ruby (~> 2.1.4) | ||||
|   js_regex (~> 3.7) | ||||
|   json (~> 2.5.1) | ||||
|  |  | |||
|  | @ -1,16 +1,29 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Projects::GoogleCloudController < Projects::ApplicationController | ||||
|   before_action :authorize_can_manage_google_cloud_deployments! | ||||
|   feature_category :google_cloud | ||||
| 
 | ||||
|   feature_category :release_orchestration | ||||
|   before_action :admin_project_google_cloud? | ||||
|   before_action :google_oauth2_enabled? | ||||
|   before_action :feature_flag_enabled? | ||||
| 
 | ||||
|   def index | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def authorize_can_manage_google_cloud_deployments! | ||||
|     access_denied! unless can?(current_user, :manage_project_google_cloud, project) | ||||
|   def admin_project_google_cloud? | ||||
|     access_denied! unless can?(current_user, :admin_project_google_cloud, project) | ||||
|   end | ||||
| 
 | ||||
|   def google_oauth2_enabled? | ||||
|     config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2') | ||||
|     if config.app_id.blank? || config.app_secret.blank? | ||||
|       access_denied! 'This GitLab instance not configured for Google Oauth2.' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def feature_flag_enabled? | ||||
|     access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -161,7 +161,7 @@ module Namespaces | |||
|       def lineage(top: nil, bottom: nil, hierarchy_order: nil) | ||||
|         raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom | ||||
| 
 | ||||
|         skope = self.class.without_sti_condition | ||||
|         skope = self.class | ||||
| 
 | ||||
|         if top | ||||
|           skope = skope.where("traversal_ids @> ('{?}')", top.id) | ||||
|  | @ -181,7 +181,6 @@ module Namespaces | |||
|           # standard SELECT to avoid mismatched attribute errors when trying to | ||||
|           # chain future ActiveRelation commands, and retain the ordering. | ||||
|           skope = self.class | ||||
|             .without_sti_condition | ||||
|             .from(skope, self.class.table_name) | ||||
|             .select(skope.arel_table[Arel.star]) | ||||
|             .order(depth: hierarchy_order) | ||||
|  |  | |||
|  | @ -19,8 +19,7 @@ module Namespaces | |||
|           return super unless use_traversal_ids_for_ancestor_scopes? | ||||
| 
 | ||||
|           records = unscoped | ||||
|             .without_sti_condition | ||||
|             .where(id: without_sti_condition.select('unnest(traversal_ids)')) | ||||
|             .where(id: select('unnest(traversal_ids)')) | ||||
|             .order_by_depth(hierarchy_order) | ||||
|             .normal_select | ||||
| 
 | ||||
|  | @ -60,16 +59,6 @@ module Namespaces | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         # Make sure we drop the STI `type = 'Group'` condition for better performance. | ||||
|         # Logically equivalent so long as hierarchies remain homogeneous. | ||||
|         def without_sti_condition | ||||
|           if Feature.enabled?(:include_sti_condition, default_enabled: :yaml) | ||||
|             all | ||||
|           else | ||||
|             unscope(where: :type) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def order_by_depth(hierarchy_order) | ||||
|           return all unless hierarchy_order | ||||
| 
 | ||||
|  | @ -85,7 +74,7 @@ module Namespaces | |||
|         # When we have queries that break this SELECT * format we can run in to errors. | ||||
|         # For example `SELECT DISTINCT on(...)` will fail when we chain a `.count` c | ||||
|         def normal_select | ||||
|           unscoped.without_sti_condition.from(all, :namespaces) | ||||
|           unscoped.from(all, :namespaces) | ||||
|         end | ||||
| 
 | ||||
|         private | ||||
|  | @ -108,7 +97,6 @@ module Namespaces | |||
| 
 | ||||
|           namespaces = Arel::Table.new(:namespaces) | ||||
|           records = unscoped | ||||
|             .without_sti_condition | ||||
|             .with(cte.to_arel) | ||||
|             .from([cte.table, namespaces]) | ||||
| 
 | ||||
|  | @ -136,7 +124,6 @@ module Namespaces | |||
|           base_ids = select(:id) | ||||
| 
 | ||||
|           records = unscoped | ||||
|             .without_sti_condition | ||||
|             .from("namespaces, (#{base_ids.to_sql}) base") | ||||
|             .where('namespaces.traversal_ids @> ARRAY[base.id]') | ||||
| 
 | ||||
|  |  | |||
|  | @ -439,7 +439,7 @@ class ProjectPolicy < BasePolicy | |||
|     enable :destroy_freeze_period | ||||
|     enable :admin_feature_flags_client | ||||
|     enable :update_runners_registration_token | ||||
|     enable :manage_project_google_cloud | ||||
|     enable :admin_project_google_cloud | ||||
|   end | ||||
| 
 | ||||
|   rule { public_project & metrics_dashboard_allowed }.policy do | ||||
|  |  | |||
|  | @ -16,14 +16,9 @@ module Ci | |||
|       private | ||||
| 
 | ||||
|       def create_pipeline_for(pull_request) | ||||
|         if ::Feature.enabled?(:ci_create_external_pr_pipeline_async, project, default_enabled: :yaml) | ||||
|           Ci::ExternalPullRequests::CreatePipelineWorker.perform_async( | ||||
|             project.id, current_user.id, pull_request.id | ||||
|           ) | ||||
|         else | ||||
|           Ci::CreatePipelineService.new(project, current_user, create_params(pull_request)) | ||||
|             .execute(:external_pull_request_event, external_pull_request: pull_request) | ||||
|         end | ||||
|         Ci::ExternalPullRequests::CreatePipelineWorker.perform_async( | ||||
|           project.id, current_user.id, pull_request.id | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       def create_params(pull_request) | ||||
|  |  | |||
|  | @ -1,70 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Projects | ||||
|   module ContainerRepository | ||||
|     class CacheTagsCreatedAtService | ||||
|       def initialize(container_repository) | ||||
|         @container_repository = container_repository | ||||
|         @cached_tag_names = Set.new | ||||
|       end | ||||
| 
 | ||||
|       def populate(tags) | ||||
|         return if tags.empty? | ||||
| 
 | ||||
|         # This will load all tags in one Redis roundtrip | ||||
|         # the maximum number of tags is configurable and is set to 200 by default. | ||||
|         # https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/packages/container_registry/index.md#set-cleanup-limits-to-conserve-resources | ||||
|         keys = tags.map(&method(:cache_key)) | ||||
|         cached_tags_count = 0 | ||||
| 
 | ||||
|         ::Gitlab::Redis::Cache.with do |redis| | ||||
|           tags.zip(redis.mget(keys)).each do |tag, created_at| | ||||
|             next unless created_at | ||||
| 
 | ||||
|             tag.created_at = DateTime.rfc3339(created_at) | ||||
|             @cached_tag_names << tag.name | ||||
|             cached_tags_count += 1 | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         cached_tags_count | ||||
|       end | ||||
| 
 | ||||
|       def insert(tags, max_ttl_in_seconds) | ||||
|         return unless max_ttl_in_seconds | ||||
|         return if tags.empty? | ||||
| 
 | ||||
|         # tags with nil created_at are not cacheable | ||||
|         # tags already cached don't need to be cached again | ||||
|         cacheable_tags = tags.select do |tag| | ||||
|           tag.created_at.present? && !tag.name.in?(@cached_tag_names) | ||||
|         end | ||||
| 
 | ||||
|         return if cacheable_tags.empty? | ||||
| 
 | ||||
|         now = Time.zone.now | ||||
| 
 | ||||
|         ::Gitlab::Redis::Cache.with do |redis| | ||||
|           # we use a pipeline instead of a MSET because each tag has | ||||
|           # a specific ttl | ||||
|           redis.pipelined do | ||||
|             cacheable_tags.each do |tag| | ||||
|               created_at = tag.created_at | ||||
|               # ttl is the max_ttl_in_seconds reduced by the number | ||||
|               # of seconds that the tag has already existed | ||||
|               ttl = max_ttl_in_seconds - (now - created_at).seconds | ||||
|               ttl = ttl.to_i | ||||
|               redis.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0 | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def cache_key(tag) | ||||
|         "container_repository:{#{@container_repository.id}}:tag:#{tag.name}:created_at" | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -140,7 +140,7 @@ module Projects | |||
| 
 | ||||
|       def cache | ||||
|         strong_memoize(:cache) do | ||||
|           ::Projects::ContainerRepository::CacheTagsCreatedAtService.new(@container_repository) | ||||
|           ::Gitlab::ContainerRepository::Tags::Cache.new(@container_repository) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,80 +4,3 @@ | |||
| - @content_class = "limit-container-width" unless fluid_layout | ||||
| 
 | ||||
| #js-google-cloud | ||||
| 
 | ||||
|   %h1.gl-font-size-h1 Google Cloud | ||||
| 
 | ||||
|   %section#js-section-google-cloud-service-accounts | ||||
| 
 | ||||
|     %h2.gl-font-size-h2 Service Accounts | ||||
| 
 | ||||
|     %p= _('Service Accounts keys are required to authorize GitLab to deploy your Google Cloud project.') | ||||
| 
 | ||||
|     %table.table.b-table.gl-table | ||||
| 
 | ||||
|       %thead | ||||
|         %tr | ||||
|           %th Environment | ||||
|           %th GCP Project ID | ||||
|           %th Service Account Key | ||||
| 
 | ||||
|       %tbody | ||||
| 
 | ||||
|         %tr | ||||
|           %td * | ||||
|           %td serving-salutes-453 | ||||
|           %td ..... | ||||
| 
 | ||||
|         %tr | ||||
|           %td production | ||||
|           %td crimson-corey-234 | ||||
|           %td ..... | ||||
| 
 | ||||
|         %tr | ||||
|           %td review/* | ||||
|           %td roving-river-379 | ||||
|           %td ..... | ||||
| 
 | ||||
|     %a.gl-button.btn.btn-primary= _('Add new service account') | ||||
| 
 | ||||
|   %br | ||||
| 
 | ||||
|   %section#js-section-google-cloud-deployments | ||||
| 
 | ||||
|     .row.row-fluid | ||||
| 
 | ||||
|       .col-lg-4 | ||||
|         %h2.gl-font-size-h2 Deployments | ||||
|         %p= _('Google Cloud offers several deployment targets. Select the one most suitable for your project.') | ||||
|         %p | ||||
|           = _('Deployments to Google Kubernetes Engine can be ') | ||||
|           %a{ href: '#' }= _('managed') | ||||
|           = _('in Infrastructure :: Kubernetes clusters') | ||||
| 
 | ||||
|       .col-lg-8 | ||||
| 
 | ||||
|         %br | ||||
| 
 | ||||
|         .gl-card.gl-mb-6 | ||||
|           .gl-card-body | ||||
|             .gl-display-flex.gl-align-items-baseline | ||||
|               %strong.gl-font-lg App Engine | ||||
|               .gl-ml-auto.gl-text-gray-500 Disabled | ||||
|             %p= _('App Engine description and apps that are suitable for this deployment target') | ||||
|             %button.gl-button.btn.btn-default= _('Configure via Merge Request') | ||||
| 
 | ||||
|         .gl-card.gl-mb-6 | ||||
|           .gl-card-body | ||||
|             .gl-display-flex.gl-align-items-baseline | ||||
|               %strong.gl-font-lg Cloud Functions | ||||
|               .gl-ml-auto.gl-text-gray-500 Disabled | ||||
|             %p= _('Cloud Functions description and apps that are suitable for this deployment target') | ||||
|             %button.gl-button.btn.btn-default= _('Configure via Merge Request') | ||||
| 
 | ||||
|         .gl-card.gl-mb-6 | ||||
|           .gl-card-body | ||||
|             .gl-display-flex.gl-align-items-baseline | ||||
|               %strong.gl-font-lg Cloud Run | ||||
|               .gl-ml-auto.gl-text-gray-500 Disabled | ||||
|             %p= _('Cloud Run description and apps that are suitable for this deployment target') | ||||
|             %button.gl-button.btn.btn-default= _('Configure via Merge Request') | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ | |||
| - gitaly | ||||
| - gitlab_docs | ||||
| - global_search | ||||
| - google_cloud | ||||
| - helm_chart_registry | ||||
| - horse | ||||
| - importers | ||||
|  |  | |||
|  | @ -1,8 +0,0 @@ | |||
| --- | ||||
| name: ci_create_external_pr_pipeline_async | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68567 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338908 | ||||
| milestone: '14.3' | ||||
| type: development | ||||
| group: group::pipeline authoring | ||||
| default_enabled: true | ||||
|  | @ -1,8 +0,0 @@ | |||
| --- | ||||
| name: include_sti_condition | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72119 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343412 | ||||
| milestone: '14.5' | ||||
| type: development | ||||
| group: group::workspace | ||||
| default_enabled: false | ||||
|  | @ -1,8 +0,0 @@ | |||
| --- | ||||
| name: jira_issue_details_edit_labels | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65298 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335069 | ||||
| milestone: '14.1' | ||||
| type: development | ||||
| group: group::integrations | ||||
| default_enabled: false | ||||
|  | @ -133,6 +133,40 @@ Before adding a new variable for a color or a size, guarantee: | |||
| - There isn't an existing one. | ||||
| - There isn't a similar one we can use instead. | ||||
| 
 | ||||
| ### Using `extend` at-rule | ||||
| 
 | ||||
| Usage of the `extend` at-rule is prohibited due to [memory leaks](https://gitlab.com/gitlab-org/gitlab/-/issues/323021) and [the rule doesn't work as it should to](https://sass-lang.com/documentation/breaking-changes/extend-compound). Use mixins instead: | ||||
| 
 | ||||
| ```scss | ||||
| // Bad | ||||
| .gl-pt-3 { | ||||
|   padding-top: 12px; | ||||
| } | ||||
| 
 | ||||
| .my-element { | ||||
|   @extend .gl-pt-3; | ||||
| } | ||||
| 
 | ||||
| // compiles to | ||||
| .gl-pt-3, .my-element { | ||||
|   padding-top: 12px; | ||||
| } | ||||
| 
 | ||||
| // Good | ||||
| @mixing gl-pt-3 { | ||||
|   padding-top: 12px; | ||||
| } | ||||
| 
 | ||||
| .my-element { | ||||
|   @include gl-pt-3; | ||||
| } | ||||
| 
 | ||||
| // compiles to | ||||
| .my-element { | ||||
|   padding-top: 12px; | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Linting | ||||
| 
 | ||||
| We use [stylelint](https://stylelint.io) to check for style guide conformity. It uses the | ||||
|  |  | |||
|  | @ -38,11 +38,11 @@ there isn't any context selected. | |||
| Contexts are named in the following format: `<agent-configuration-project-path>:<agent-name>`. | ||||
| To get the list of available contexts, run `kubectl config get-contexts`. | ||||
| 
 | ||||
| ## Share the CI/CD Tunnel provided by an Agent with other projects and group | ||||
| ## Share the CI/CD Tunnel provided by an Agent with other projects and groups | ||||
| 
 | ||||
| The Agent can be configured to enable access to the CI/CD Tunnel to other projects or all the projects under a given group. This way you can have a single agent serving all the requests for several projects saving on resources and maintenance. | ||||
| 
 | ||||
| You can read more on how to [authorize access to groups in the Agent configuration reference](repository.md#authorize-groups-to-use-an-agent). | ||||
| You can read more on how to [authorize access in the Agent configuration reference](repository.md#authorize-projects-and-groups-to-use-an-agent). | ||||
| 
 | ||||
| ## Example for a `kubectl` command using the CI/CD Tunnel | ||||
| 
 | ||||
|  |  | |||
|  | @ -350,16 +350,17 @@ Additional management interfaces are planned for the GitLab Kubernetes Agent. | |||
| 
 | ||||
| ## Upgrades and version compatibility | ||||
| 
 | ||||
| As the GitLab Kubernetes Agent is a new product, we are constantly adding new features | ||||
| to it. As a result, while shipped features are production ready, its internal API is | ||||
| neither stable nor versioned yet. For this reason, GitLab only guarantees compatibility | ||||
| between corresponding major.minor (X.Y) versions of GitLab and its cluster side | ||||
| component, `agentk`. | ||||
| The GitLab Kubernetes Agent is comprised of two major components: `agentk` and `kas`.  | ||||
| As we provide `kas` installers built into the various GitLab installation methods, the required `kas` version corresponds to the GitLab `major.minor` (X.Y) versions. | ||||
| 
 | ||||
| Upgrade your agent installations together with GitLab upgrades. To decide which version of `agentk` to install follow: | ||||
| At the same time, `agentk` and `kas` can differ by 1 minor version in either direction. For example, | ||||
| `agentk` 14.4 supports `kas` 14.3, 14.4, and 14.5 (regardless of the patch). | ||||
| 
 | ||||
| 1. Open the [`GITLAB_KAS_VERSION`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/GITLAB_KAS_VERSION) file from the GitLab Repository, which contains the latest `agentk` version associated with the `master` branch. | ||||
| 1. Change the `master` branch and select the Git tag associated with your version. For instance, you could change it to GitLab [v13.5.3-ee release](https://gitlab.com/gitlab-org/gitlab/-/blob/v13.5.3-ee/GITLAB_KAS_VERSION) | ||||
| A feature introduced in a given GitLab minor version might work with other `agentk` or `kas` versions. | ||||
| To make sure that it works, use at least the same `agentk` and `kas` minor version. For example, | ||||
| if your GitLab version is 14.2, use at least `agentk` 14.2 and `kas` 14.2. | ||||
| 
 | ||||
| We recommend upgrading your `kas` installations together with GitLab instances' upgrades, and to upgrade the `agentk` installations after upgrading GitLab. | ||||
| 
 | ||||
| The available `agentk` and `kas` versions can be found in | ||||
| [the container registry](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/container_registry/). | ||||
|  |  | |||
|  | @ -149,39 +149,52 @@ gitops: | |||
|     - glob: '/**/*.yaml' | ||||
| ``` | ||||
| 
 | ||||
| ## Authorize groups to use an Agent | ||||
| ## Authorize projects and groups to use an Agent | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3. | ||||
| > - Group authorization [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3. | ||||
| > - Project authorization [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327850) in GitLab 14.4. | ||||
| 
 | ||||
| If you use the same cluster across multiple projects, you can set up the CI/CD Tunnel | ||||
| to grant the Agent access to one or more groups. This way, all the projects that belong | ||||
| to the authorized groups can access the same Agent. This enables you to save resources and | ||||
| have a scalable setup. | ||||
| If you use the same cluster across multiple projects, you can set up the [CI/CD Tunnel](ci_cd_tunnel.md) | ||||
| to grant access to an Agent from one or more projects or groups. This way, all the authorized | ||||
| projects can access the same Agent, which facilitates you to save resources and have a scalable setup. | ||||
| 
 | ||||
| When you authorize a group, the agent's Kubernetes context is automatically injected | ||||
| into every project of the authorized group, and users can select the connection as | ||||
| described in the [CI/CD Tunnel documentation](ci_cd_tunnel.md). | ||||
| To authorize a group to access the Agent through the [CI/CD Tunnel](ci_cd_tunnel.md), | ||||
| use the `ci_access` attribute in your `config.yaml` configuration file. | ||||
| When you authorize a project to use an agent through the [CI/CD Tunnel](ci_cd_tunnel.md), | ||||
| the selected Kubernetes context is automatically injected into CI/CD jobs, allowing you to | ||||
| run Kubernetes commands from your authorized projects' scripts. When you authorize a group, | ||||
| all the projects that belong to that group can access the selected agent. | ||||
| 
 | ||||
| An Agent can only authorize groups in the same group hierarchy as the Agent's configuration project. At most | ||||
| 100 groups can be authorized per Agent. | ||||
| An Agent can only authorize projects or groups in the same group hierarchy as the Agent's configuration | ||||
| project. You can authorize up to 100 projects and 100 groups per Agent. | ||||
| 
 | ||||
| To authorize a group: | ||||
| ### Authorize projects to use an Agent | ||||
| 
 | ||||
| 1. Edit your `config.yaml` file under the `.gitlab/agents/<agent name>` directory. | ||||
| 1. Add the `ci_access` root attribute. | ||||
| To grant projects access to the Agent through the [CI/CD Tunnel](ci_cd_tunnel.md): | ||||
| 
 | ||||
| 1. Go to your Agent's configuration project. | ||||
| 1. Edit the Agent's configuration file (`config.yaml`). | ||||
| 1. Add the `projects` attribute into `ci_access`. | ||||
| 1. Identify the new project through its path: | ||||
| 
 | ||||
|    ```yaml | ||||
|    ci_access: | ||||
|      projects: | ||||
|      - id: path/to/project | ||||
|    ``` | ||||
| 
 | ||||
| ### Authorize groups to use an Agent | ||||
| 
 | ||||
| To grant access to all projects within a group: | ||||
| 
 | ||||
| 1. Go to your Agent's configuration project. | ||||
| 1. Edit the Agent's configuration file (`config.yaml`). | ||||
| 1. Add the `groups` attribute into `ci_access`. | ||||
| 1. Add the group `id` into `groups`, identifying the authorized group through its path. | ||||
| 1. Identify the group or subgroup through its path: | ||||
| 
 | ||||
| For example: | ||||
| 
 | ||||
| ```yaml | ||||
| ci_access: | ||||
|   # This agent is accessible from CI jobs in projects in these groups | ||||
|   groups: | ||||
|   - id: group/subgroup | ||||
| ``` | ||||
|    ```yaml | ||||
|    ci_access: | ||||
|      groups: | ||||
|      - id: path/to/group/subgroup | ||||
|    ``` | ||||
| 
 | ||||
| ## Surface network security alerts from cluster to GitLab **(ULTIMATE)** | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ installation, such as an Ingress controller. | |||
| ## RBAC compatibility | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/29398) in GitLab 11.4. | ||||
| > - [Project namespace restriction](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51716) was introduced in GitLab 11.5. | ||||
| > - Project namespace restriction was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51716) in GitLab 11.5. | ||||
| 
 | ||||
| For each project under a group with a Kubernetes cluster, GitLab creates a restricted | ||||
| service account with [`edit` privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) | ||||
|  | @ -49,7 +49,7 @@ to the project, provided the cluster is not disabled. | |||
| 
 | ||||
| ## Multiple Kubernetes clusters | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35094) in GitLab Free 13.2. | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35094) in GitLab 13.2. | ||||
| 
 | ||||
| You can associate more than one Kubernetes cluster to your group, and maintain different clusters | ||||
| for different environments, such as development, staging, and production. | ||||
|  |  | |||
|  | @ -68,7 +68,6 @@ The concept of [project-level](../../project/clusters/index.md), | |||
| [instance-level](../../instance/clusters/index.md) clusters becomes | ||||
| extinct in the new model, although the functionality remains to some extent. | ||||
| 
 | ||||
| The Agent is always configured in a GitLab project, but you can: | ||||
| 
 | ||||
| - [Grant your cluster's access to GitLab groups through the Agent](../../clusters/agent/repository.md#authorize-groups-to-use-an-agent). | ||||
| - [Share access to the Agent with other projects and groups through the CI/CD Tunnel](../../clusters/agent/ci_cd_tunnel.md#share-the-cicd-tunnel-provided-by-an-agent-with-other-projects-and-group). | ||||
| The Agent is always configured in a single GitLab project, but you can use the CI/CD Tunnel to | ||||
| [authorize other projects and groups to use the same Agent](../../clusters/agent/repository.md#authorize-projects-and-groups-to-use-an-agent). | ||||
| By doing so, you are granting these projects and groups access to the same cluster, which is similar to group-level clusters' use case. | ||||
|  |  | |||
|  | @ -0,0 +1,72 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module ContainerRepository | ||||
|     module Tags | ||||
|       class Cache | ||||
|         def initialize(container_repository) | ||||
|           @container_repository = container_repository | ||||
|           @cached_tag_names = Set.new | ||||
|         end | ||||
| 
 | ||||
|         def populate(tags) | ||||
|           return if tags.empty? | ||||
| 
 | ||||
|           # This will load all tags in one Redis roundtrip | ||||
|           # the maximum number of tags is configurable and is set to 200 by default. | ||||
|           # https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/packages/container_registry/index.md#set-cleanup-limits-to-conserve-resources | ||||
|           keys = tags.map(&method(:cache_key)) | ||||
|           cached_tags_count = 0 | ||||
| 
 | ||||
|           ::Gitlab::Redis::Cache.with do |redis| | ||||
|             tags.zip(redis.mget(keys)).each do |tag, created_at| | ||||
|               next unless created_at | ||||
| 
 | ||||
|               tag.created_at = DateTime.rfc3339(created_at) | ||||
|               @cached_tag_names << tag.name | ||||
|               cached_tags_count += 1 | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|           cached_tags_count | ||||
|         end | ||||
| 
 | ||||
|         def insert(tags, max_ttl_in_seconds) | ||||
|           return unless max_ttl_in_seconds | ||||
|           return if tags.empty? | ||||
| 
 | ||||
|           # tags with nil created_at are not cacheable | ||||
|           # tags already cached don't need to be cached again | ||||
|           cacheable_tags = tags.select do |tag| | ||||
|             tag.created_at.present? && !tag.name.in?(@cached_tag_names) | ||||
|           end | ||||
| 
 | ||||
|           return if cacheable_tags.empty? | ||||
| 
 | ||||
|           now = Time.zone.now | ||||
| 
 | ||||
|           ::Gitlab::Redis::Cache.with do |redis| | ||||
|             # we use a pipeline instead of a MSET because each tag has | ||||
|             # a specific ttl | ||||
|             redis.pipelined do | ||||
|               cacheable_tags.each do |tag| | ||||
|                 created_at = tag.created_at | ||||
|                 # ttl is the max_ttl_in_seconds reduced by the number | ||||
|                 # of seconds that the tag has already existed | ||||
|                 ttl = max_ttl_in_seconds - (now - created_at).seconds | ||||
|                 ttl = ttl.to_i | ||||
|                 redis.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0 | ||||
|               end | ||||
|             end | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         private | ||||
| 
 | ||||
|         def cache_key(tag) | ||||
|           "container_repository:{#{@container_repository.id}}:tag:#{tag.name}:created_at" | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -91,7 +91,7 @@ module Sidebars | |||
| 
 | ||||
|         def google_cloud_menu_item | ||||
|           feature_is_enabled = Feature.enabled?(:incubation_5mp_google_cloud) | ||||
|           user_has_permissions = can?(context.current_user, :manage_project_google_cloud, context.project) | ||||
|           user_has_permissions = can?(context.current_user, :admin_project_google_cloud, context.project) | ||||
| 
 | ||||
|           unless feature_is_enabled && user_has_permissions | ||||
|             return ::Sidebars::NilMenuItem.new(item_id: :incubation_5mp_google_cloud) | ||||
|  |  | |||
|  | @ -2047,9 +2047,6 @@ msgstr "" | |||
| msgid "Add new directory" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Add new service account" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Add or remove previously merged commits" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -3987,9 +3984,6 @@ msgstr "" | |||
| msgid "Any namespace" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "App Engine description and apps that are suitable for this deployment target" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "App ID" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -7283,12 +7277,6 @@ msgstr "" | |||
| msgid "Closes this %{quick_action_target}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Cloud Functions description and apps that are suitable for this deployment target" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Cloud Run description and apps that are suitable for this deployment target" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Cluster" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -8728,9 +8716,6 @@ msgstr "" | |||
| msgid "Configure the way a user creates a new account." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Configure via Merge Request" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Configure which lists are shown for anyone who visits this board" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -11554,9 +11539,6 @@ msgstr "" | |||
| msgid "Deployments" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Deployments to Google Kubernetes Engine can be " | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Deployments|%{deployments} environment impacted." | ||||
| msgid_plural "Deployments|%{deployments} environments impacted." | ||||
| msgstr[0] "" | ||||
|  | @ -16052,9 +16034,6 @@ msgstr "" | |||
| msgid "Google Cloud" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Google Cloud offers several deployment targets. Select the one most suitable for your project." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Google authentication is not %{link_start}properly configured%{link_end}. Ask your GitLab administrator if you want to use this service." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -19607,9 +19586,6 @@ msgstr "" | |||
| msgid "JiraService|Failed to load Jira issue. View the issue in Jira, or reload the page." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "JiraService|Failed to update Jira issue labels. View the issue in Jira, or reload the page." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "JiraService|Failed to update Jira issue status. View the issue in Jira, or reload the page." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -31306,9 +31282,6 @@ msgstr "" | |||
| msgid "Service" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Service Accounts keys are required to authorize GitLab to deploy your Google Cloud project." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Service Desk" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -40769,9 +40742,6 @@ msgstr "" | |||
| msgid "in" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "in Infrastructure :: Kubernetes clusters" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "in all GitLab" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -40936,9 +40906,6 @@ msgstr "" | |||
| msgid "log in" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "managed" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "manual" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob do | ||||
|   let(:table_name) { :copy_primary_key_test } | ||||
|   let(:table_name) { :_test_copy_primary_key_test } | ||||
|   let(:test_table) { table(table_name) } | ||||
|   let(:sub_batch_size) { 1000 } | ||||
|   let(:pause_ms) { 0 } | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe ::Projects::ContainerRepository::CacheTagsCreatedAtService, :clean_gitlab_redis_cache do | ||||
| RSpec.describe ::Gitlab::ContainerRepository::Tags::Cache, :clean_gitlab_redis_cache do | ||||
|   let_it_be(:dummy_tag_class) { Struct.new(:name, :created_at) } | ||||
|   let_it_be(:repository) { create(:container_repository) } | ||||
| 
 | ||||
|  | @ -286,7 +286,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do | |||
|     let(:migration_wrapper) { Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper.new } | ||||
| 
 | ||||
|     let(:migration_helpers) { ActiveRecord::Migration.new } | ||||
|     let(:table_name) { :_batched_migrations_test_table } | ||||
|     let(:table_name) { :_test_batched_migrations_test_table } | ||||
|     let(:column_name) { :some_id } | ||||
|     let(:job_arguments) { [:some_id, :some_id_convert_to_bigint] } | ||||
| 
 | ||||
|  |  | |||
|  | @ -85,7 +85,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do | |||
|   describe '.insert_all!' do | ||||
|     before do | ||||
|       ActiveRecord::Schema.define do | ||||
|         create_table :connection_proxy_bulk_insert, force: true do |t| | ||||
|         create_table :_test_connection_proxy_bulk_insert, force: true do |t| | ||||
|           t.string :name, null: true | ||||
|         end | ||||
|       end | ||||
|  | @ -93,13 +93,13 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do | |||
| 
 | ||||
|     after do | ||||
|       ActiveRecord::Schema.define do | ||||
|         drop_table :connection_proxy_bulk_insert, force: true | ||||
|         drop_table :_test_connection_proxy_bulk_insert, force: true | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     let(:model_class) do | ||||
|       Class.new(ApplicationRecord) do | ||||
|         self.table_name = "connection_proxy_bulk_insert" | ||||
|         self.table_name = "_test_connection_proxy_bulk_insert" | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -105,7 +105,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do | |||
|   describe 'LoadBalancing integration tests', :database_replica, :delete do | ||||
|     before(:all) do | ||||
|       ActiveRecord::Schema.define do | ||||
|         create_table :load_balancing_test, force: true do |t| | ||||
|         create_table :_test_load_balancing_test, force: true do |t| | ||||
|           t.string :name, null: true | ||||
|         end | ||||
|       end | ||||
|  | @ -113,13 +113,13 @@ RSpec.describe Gitlab::Database::LoadBalancing do | |||
| 
 | ||||
|     after(:all) do | ||||
|       ActiveRecord::Schema.define do | ||||
|         drop_table :load_balancing_test, force: true | ||||
|         drop_table :_test_load_balancing_test, force: true | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     let(:model) do | ||||
|       Class.new(ApplicationRecord) do | ||||
|         self.table_name = "load_balancing_test" | ||||
|         self.table_name = "_test_load_balancing_test" | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  | @ -443,7 +443,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do | |||
|             elsif payload[:name] == 'SQL' # Custom query | ||||
|               true | ||||
|             else | ||||
|               keywords = %w[load_balancing_test] | ||||
|               keywords = %w[_test_load_balancing_test] | ||||
|               keywords += %w[begin commit] if include_transaction | ||||
|               keywords.any? { |keyword| payload[:sql].downcase.include?(keyword) } | ||||
|             end | ||||
|  |  | |||
|  | @ -9,18 +9,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers do | |||
| 
 | ||||
|   let(:model) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_test_table' | ||||
|       self.table_name = '_test_loose_fk_test_table' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   before(:all) do | ||||
|     migration.create_table :loose_fk_test_table do |t| | ||||
|     migration.create_table :_test_loose_fk_test_table do |t| | ||||
|       t.timestamps | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   after(:all) do | ||||
|     migration.drop_table :loose_fk_test_table | ||||
|     migration.drop_table :_test_loose_fk_test_table | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|  | @ -37,7 +37,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers do | |||
| 
 | ||||
|   context 'when the record deletion tracker trigger is installed' do | ||||
|     before do | ||||
|       migration.track_record_deletions(:loose_fk_test_table) | ||||
|       migration.track_record_deletions(:_test_loose_fk_test_table) | ||||
|     end | ||||
| 
 | ||||
|     it 'stores the record deletion' do | ||||
|  | @ -50,7 +50,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers do | |||
|       deleted_record = LooseForeignKeys::DeletedRecord.all.first | ||||
| 
 | ||||
|       expect(deleted_record.primary_key_value).to eq(record_to_be_deleted.id) | ||||
|       expect(deleted_record.fully_qualified_table_name).to eq('public.loose_fk_test_table') | ||||
|       expect(deleted_record.fully_qualified_table_name).to eq('public._test_loose_fk_test_table') | ||||
|       expect(deleted_record.partition).to eq(1) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|     let(:model) { Class.new(ActiveRecord::Base) } | ||||
| 
 | ||||
|     before do | ||||
|       model.table_name = :test_table | ||||
|       model.table_name = :_test_table | ||||
|     end | ||||
| 
 | ||||
|     context 'when called inside a transaction block' do | ||||
|  | @ -30,19 +30,19 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
| 
 | ||||
|       it 'raises an error' do | ||||
|         expect do | ||||
|           migration.public_send(operation, :test_table, :original, :renamed) | ||||
|           migration.public_send(operation, :_test_table, :original, :renamed) | ||||
|         end.to raise_error("#{operation} can not be run inside a transaction") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the existing column has a default value' do | ||||
|       before do | ||||
|         migration.change_column_default :test_table, existing_column, 'default value' | ||||
|         migration.change_column_default :_test_table, existing_column, 'default value' | ||||
|       end | ||||
| 
 | ||||
|       it 'raises an error' do | ||||
|         expect do | ||||
|           migration.public_send(operation, :test_table, :original, :renamed) | ||||
|           migration.public_send(operation, :_test_table, :original, :renamed) | ||||
|         end.to raise_error("#{operation} does not currently support columns with default values") | ||||
|       end | ||||
|     end | ||||
|  | @ -51,18 +51,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|       context 'when the batch column does not exist' do | ||||
|         it 'raises an error' do | ||||
|           expect do | ||||
|             migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :missing) | ||||
|           end.to raise_error('Column missing does not exist on test_table') | ||||
|             migration.public_send(operation, :_test_table, :original, :renamed, batch_column_name: :missing) | ||||
|           end.to raise_error('Column missing does not exist on _test_table') | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when the batch column does exist' do | ||||
|         it 'passes it when creating the column' do | ||||
|           expect(migration).to receive(:create_column_from) | ||||
|             .with(:test_table, existing_column, added_column, type: nil, batch_column_name: :status) | ||||
|             .with(:_test_table, existing_column, added_column, type: nil, batch_column_name: :status) | ||||
|             .and_call_original | ||||
| 
 | ||||
|           migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :status) | ||||
|           migration.public_send(operation, :_test_table, :original, :renamed, batch_column_name: :status) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | @ -71,17 +71,17 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|       existing_record_1 = model.create!(status: 0, existing_column => 'existing') | ||||
|       existing_record_2 = model.create!(status: 0, existing_column => nil) | ||||
| 
 | ||||
|       migration.send(operation, :test_table, :original, :renamed) | ||||
|       migration.send(operation, :_test_table, :original, :renamed) | ||||
|       model.reset_column_information | ||||
| 
 | ||||
|       expect(migration.column_exists?(:test_table, added_column)).to eq(true) | ||||
|       expect(migration.column_exists?(:_test_table, added_column)).to eq(true) | ||||
| 
 | ||||
|       expect(existing_record_1.reload).to have_attributes(status: 0, original: 'existing', renamed: 'existing') | ||||
|       expect(existing_record_2.reload).to have_attributes(status: 0, original: nil, renamed: nil) | ||||
|     end | ||||
| 
 | ||||
|     it 'installs triggers to sync new data' do | ||||
|       migration.public_send(operation, :test_table, :original, :renamed) | ||||
|       migration.public_send(operation, :_test_table, :original, :renamed) | ||||
|       model.reset_column_information | ||||
| 
 | ||||
|       new_record_1 = model.create!(status: 1, original: 'first') | ||||
|  | @ -102,7 +102,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|     before do | ||||
|       allow(migration).to receive(:transaction_open?).and_return(false) | ||||
| 
 | ||||
|       migration.create_table :test_table do |t| | ||||
|       migration.create_table :_test_table do |t| | ||||
|         t.integer :status, null: false | ||||
|         t.text :original | ||||
|         t.text :other_column | ||||
|  | @ -118,8 +118,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|     context 'when the column to rename does not exist' do | ||||
|       it 'raises an error' do | ||||
|         expect do | ||||
|           migration.rename_column_concurrently :test_table, :missing_column, :renamed | ||||
|         end.to raise_error('Column missing_column does not exist on test_table') | ||||
|           migration.rename_column_concurrently :_test_table, :missing_column, :renamed | ||||
|         end.to raise_error('Column missing_column does not exist on _test_table') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | @ -128,7 +128,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|     before do | ||||
|       allow(migration).to receive(:transaction_open?).and_return(false) | ||||
| 
 | ||||
|       migration.create_table :test_table do |t| | ||||
|       migration.create_table :_test_table do |t| | ||||
|         t.integer :status, null: false | ||||
|         t.text :other_column | ||||
|         t.text :renamed | ||||
|  | @ -144,8 +144,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|     context 'when the renamed column does not exist' do | ||||
|       it 'raises an error' do | ||||
|         expect do | ||||
|           migration.undo_cleanup_concurrent_column_rename :test_table, :original, :missing_column | ||||
|         end.to raise_error('Column missing_column does not exist on test_table') | ||||
|           migration.undo_cleanup_concurrent_column_rename :_test_table, :original, :missing_column | ||||
|         end.to raise_error('Column missing_column does not exist on _test_table') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | @ -156,25 +156,25 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|     before do | ||||
|       allow(migration).to receive(:transaction_open?).and_return(false) | ||||
| 
 | ||||
|       migration.create_table :test_table do |t| | ||||
|       migration.create_table :_test_table do |t| | ||||
|         t.integer :status, null: false | ||||
|         t.text :original | ||||
|         t.text :other_column | ||||
|       end | ||||
| 
 | ||||
|       migration.rename_column_concurrently :test_table, :original, :renamed | ||||
|       migration.rename_column_concurrently :_test_table, :original, :renamed | ||||
|     end | ||||
| 
 | ||||
|     context 'when the helper is called repeatedly' do | ||||
|       before do | ||||
|         migration.public_send(operation, :test_table, :original, :renamed) | ||||
|         migration.public_send(operation, :_test_table, :original, :renamed) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not make repeated attempts to cleanup' do | ||||
|         expect(migration).not_to receive(:remove_column) | ||||
| 
 | ||||
|         expect do | ||||
|           migration.public_send(operation, :test_table, :original, :renamed) | ||||
|           migration.public_send(operation, :_test_table, :original, :renamed) | ||||
|         end.not_to raise_error | ||||
|       end | ||||
|     end | ||||
|  | @ -182,26 +182,26 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|     context 'when the renamed column exists' do | ||||
|       let(:triggers) do | ||||
|         [ | ||||
|           ['trigger_7cc71f92fd63', 'function_for_trigger_7cc71f92fd63', before: 'insert'], | ||||
|           ['trigger_f1a1f619636a', 'function_for_trigger_f1a1f619636a', before: 'update'], | ||||
|           ['trigger_769a49938884', 'function_for_trigger_769a49938884', before: 'update'] | ||||
|           ['trigger_020dbcb8cdd0', 'function_for_trigger_020dbcb8cdd0', before: 'insert'], | ||||
|           ['trigger_6edaca641d03', 'function_for_trigger_6edaca641d03', before: 'update'], | ||||
|           ['trigger_a3fb9f3add34', 'function_for_trigger_a3fb9f3add34', before: 'update'] | ||||
|         ] | ||||
|       end | ||||
| 
 | ||||
|       it 'removes the sync triggers and renamed columns' do | ||||
|         triggers.each do |(trigger_name, function_name, event)| | ||||
|           expect_function_to_exist(function_name) | ||||
|           expect_valid_function_trigger(:test_table, trigger_name, function_name, event) | ||||
|           expect_valid_function_trigger(:_test_table, trigger_name, function_name, event) | ||||
|         end | ||||
| 
 | ||||
|         expect(migration.column_exists?(:test_table, added_column)).to eq(true) | ||||
|         expect(migration.column_exists?(:_test_table, added_column)).to eq(true) | ||||
| 
 | ||||
|         migration.public_send(operation, :test_table, :original, :renamed) | ||||
|         migration.public_send(operation, :_test_table, :original, :renamed) | ||||
| 
 | ||||
|         expect(migration.column_exists?(:test_table, added_column)).to eq(false) | ||||
|         expect(migration.column_exists?(:_test_table, added_column)).to eq(false) | ||||
| 
 | ||||
|         triggers.each do |(trigger_name, function_name, _)| | ||||
|           expect_trigger_not_to_exist(:test_table, trigger_name) | ||||
|           expect_trigger_not_to_exist(:_test_table, trigger_name) | ||||
|           expect_function_not_to_exist(function_name) | ||||
|         end | ||||
|       end | ||||
|  | @ -223,7 +223,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|   end | ||||
| 
 | ||||
|   describe '#create_table' do | ||||
|     let(:table_name) { :test_table } | ||||
|     let(:table_name) { :_test_table } | ||||
|     let(:column_attributes) do | ||||
|       [ | ||||
|         { name: 'id',         sql_type: 'bigint',                   null: false, default: nil    }, | ||||
|  | @ -245,7 +245,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do | |||
|         end | ||||
| 
 | ||||
|         expect_table_columns_to_match(column_attributes, table_name) | ||||
|         expect_check_constraint(table_name, 'check_cda6f69506', 'char_length(name) <= 100') | ||||
|         expect_check_constraint(table_name, 'check_e9982cf9da', 'char_length(name) <= 100') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
| 
 | ||||
|     let(:model) { double('model', table_name: table_name) } | ||||
|     let(:partitioning_key) { double } | ||||
|     let(:table_name) { :partitioned_test } | ||||
|     let(:table_name) { :_test_partitioned_test } | ||||
| 
 | ||||
|     before do | ||||
|       connection.execute(<<~SQL) | ||||
|  | @ -18,11 +18,11 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
|           (id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at)) | ||||
|           PARTITION BY RANGE (created_at); | ||||
| 
 | ||||
|         CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_000000 | ||||
|         CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000 | ||||
|         PARTITION OF #{table_name} | ||||
|         FOR VALUES FROM (MINVALUE) TO ('2020-05-01'); | ||||
| 
 | ||||
|         CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202005 | ||||
|         CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005 | ||||
|         PARTITION OF #{table_name} | ||||
|         FOR VALUES FROM ('2020-05-01') TO ('2020-06-01'); | ||||
|       SQL | ||||
|  | @ -30,8 +30,8 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
| 
 | ||||
|     it 'detects both partitions' do | ||||
|       expect(subject).to eq([ | ||||
|         Gitlab::Database::Partitioning::TimePartition.new(table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000'), | ||||
|         Gitlab::Database::Partitioning::TimePartition.new(table_name, '2020-05-01', '2020-06-01', partition_name: 'partitioned_test_202005') | ||||
|         Gitlab::Database::Partitioning::TimePartition.new(table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'), | ||||
|         Gitlab::Database::Partitioning::TimePartition.new(table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005') | ||||
|     ]) | ||||
|     end | ||||
|   end | ||||
|  | @ -41,7 +41,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
| 
 | ||||
|     let(:model) do | ||||
|       Class.new(ActiveRecord::Base) do | ||||
|         self.table_name = 'partitioned_test' | ||||
|         self.table_name = '_test_partitioned_test' | ||||
|         self.primary_key = :id | ||||
|       end | ||||
|     end | ||||
|  | @ -59,11 +59,11 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
|             (id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at)) | ||||
|             PARTITION BY RANGE (created_at); | ||||
| 
 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_000000 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000 | ||||
|           PARTITION OF #{model.table_name} | ||||
|           FOR VALUES FROM (MINVALUE) TO ('2020-05-01'); | ||||
| 
 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202006 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006 | ||||
|           PARTITION OF #{model.table_name} | ||||
|           FOR VALUES FROM ('2020-06-01') TO ('2020-07-01'); | ||||
|         SQL | ||||
|  | @ -166,7 +166,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
|             (id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at)) | ||||
|             PARTITION BY RANGE (created_at); | ||||
| 
 | ||||
|             CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202006 | ||||
|             CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006 | ||||
|             PARTITION OF #{model.table_name} | ||||
|             FOR VALUES FROM ('2020-06-01') TO ('2020-07-01'); | ||||
|         SQL | ||||
|  | @ -181,13 +181,13 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
|   describe '#extra_partitions' do | ||||
|     let(:model) do | ||||
|       Class.new(ActiveRecord::Base) do | ||||
|         self.table_name = 'partitioned_test' | ||||
|         self.table_name = '_test_partitioned_test' | ||||
|         self.primary_key = :id | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     let(:partitioning_key) { :created_at } | ||||
|     let(:table_name) { :partitioned_test } | ||||
|     let(:table_name) { :_test_partitioned_test } | ||||
| 
 | ||||
|     around do |example| | ||||
|       travel_to(Date.parse('2020-08-22')) { example.run } | ||||
|  | @ -200,15 +200,15 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
|             (id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at)) | ||||
|             PARTITION BY RANGE (created_at); | ||||
| 
 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_000000 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000 | ||||
|           PARTITION OF #{table_name} | ||||
|           FOR VALUES FROM (MINVALUE) TO ('2020-05-01'); | ||||
| 
 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202005 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005 | ||||
|           PARTITION OF #{table_name} | ||||
|           FOR VALUES FROM ('2020-05-01') TO ('2020-06-01'); | ||||
| 
 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202006 | ||||
|           CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006 | ||||
|           PARTITION OF #{table_name} | ||||
|           FOR VALUES FROM ('2020-06-01') TO ('2020-07-01') | ||||
|         SQL | ||||
|  | @ -235,7 +235,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
| 
 | ||||
|         it 'prunes the unbounded partition ending 2020-05-01' do | ||||
|           min_value_to_may = Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', | ||||
|                                                                                partition_name: 'partitioned_test_000000') | ||||
|                                                                                partition_name: '_test_partitioned_test_000000') | ||||
| 
 | ||||
|           expect(subject).to contain_exactly(min_value_to_may) | ||||
|         end | ||||
|  | @ -246,8 +246,8 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
| 
 | ||||
|         it 'prunes the unbounded partition and the partition for May-June' do | ||||
|           expect(subject).to contain_exactly( | ||||
|             Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000'), | ||||
|                                Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: 'partitioned_test_202005') | ||||
|             Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'), | ||||
|                                Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005') | ||||
|           ) | ||||
|         end | ||||
| 
 | ||||
|  | @ -256,16 +256,16 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do | |||
| 
 | ||||
|           it 'prunes empty partitions' do | ||||
|             expect(subject).to contain_exactly( | ||||
|               Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000'), | ||||
|                                  Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: 'partitioned_test_202005') | ||||
|               Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'), | ||||
|                                  Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005') | ||||
|             ) | ||||
|           end | ||||
| 
 | ||||
|           it 'does not prune non-empty partitions' do | ||||
|             connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))") # inserting one record into partitioned_test_202005 | ||||
|             connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))") # inserting one record into _test_partitioned_test_202005 | ||||
| 
 | ||||
|             expect(subject).to contain_exactly( | ||||
|               Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000') | ||||
|               Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000') | ||||
|             ) | ||||
|           end | ||||
|         end | ||||
|  |  | |||
|  | @ -11,12 +11,12 @@ RSpec.describe Gitlab::Database::SchemaCacheWithRenamedTable do | |||
| 
 | ||||
|   let(:new_model) do | ||||
|     Class.new(ActiveRecord::Base) do | ||||
|       self.table_name = 'projects_new' | ||||
|       self.table_name = '_test_projects_new' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     stub_const('Gitlab::Database::TABLES_TO_BE_RENAMED', { 'projects' => 'projects_new' }) | ||||
|     stub_const('Gitlab::Database::TABLES_TO_BE_RENAMED', { 'projects' => '_test_projects_new' }) | ||||
|   end | ||||
| 
 | ||||
|   context 'when table is not renamed yet' do | ||||
|  | @ -32,8 +32,8 @@ RSpec.describe Gitlab::Database::SchemaCacheWithRenamedTable do | |||
| 
 | ||||
|   context 'when table is renamed' do | ||||
|     before do | ||||
|       ActiveRecord::Base.connection.execute("ALTER TABLE projects RENAME TO projects_new") | ||||
|       ActiveRecord::Base.connection.execute("CREATE VIEW projects AS SELECT * FROM projects_new") | ||||
|       ActiveRecord::Base.connection.execute("ALTER TABLE projects RENAME TO _test_projects_new") | ||||
|       ActiveRecord::Base.connection.execute("CREATE VIEW projects AS SELECT * FROM _test_projects_new") | ||||
| 
 | ||||
|       old_model.reset_column_information | ||||
|       ActiveRecord::Base.connection.schema_cache.clear! | ||||
|  | @ -54,14 +54,14 @@ RSpec.describe Gitlab::Database::SchemaCacheWithRenamedTable do | |||
| 
 | ||||
|     it 'has the same indexes' do | ||||
|       indexes_for_old_table = ActiveRecord::Base.connection.schema_cache.indexes('projects') | ||||
|       indexes_for_new_table = ActiveRecord::Base.connection.schema_cache.indexes('projects_new') | ||||
|       indexes_for_new_table = ActiveRecord::Base.connection.schema_cache.indexes('_test_projects_new') | ||||
| 
 | ||||
|       expect(indexes_for_old_table).to eq(indexes_for_new_table) | ||||
|     end | ||||
| 
 | ||||
|     it 'has the same column_hash' do | ||||
|       columns_hash_for_old_table = ActiveRecord::Base.connection.schema_cache.columns_hash('projects') | ||||
|       columns_hash_for_new_table = ActiveRecord::Base.connection.schema_cache.columns_hash('projects_new') | ||||
|       columns_hash_for_new_table = ActiveRecord::Base.connection.schema_cache.columns_hash('_test_projects_new') | ||||
| 
 | ||||
|       expect(columns_hash_for_old_table).to eq(columns_hash_for_new_table) | ||||
|     end | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ RSpec.describe ::Gitlab::Graphql::Pagination::Connections do | |||
| 
 | ||||
|   before(:all) do | ||||
|     ActiveRecord::Schema.define do | ||||
|       create_table :testing_pagination_nodes, force: true do |t| | ||||
|       create_table :_test_testing_pagination_nodes, force: true do |t| | ||||
|         t.integer :value, null: false | ||||
|       end | ||||
|     end | ||||
|  | @ -16,13 +16,13 @@ RSpec.describe ::Gitlab::Graphql::Pagination::Connections do | |||
| 
 | ||||
|   after(:all) do | ||||
|     ActiveRecord::Schema.define do | ||||
|       drop_table :testing_pagination_nodes, force: true | ||||
|       drop_table :_test_testing_pagination_nodes, force: true | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:node_model) do | ||||
|     Class.new(ActiveRecord::Base) do | ||||
|       self.table_name = 'testing_pagination_nodes' | ||||
|       self.table_name = '_test_testing_pagination_nodes' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,42 +5,42 @@ require 'spec_helper' | |||
| RSpec.describe BulkInsertSafe do | ||||
|   before(:all) do | ||||
|     ActiveRecord::Schema.define do | ||||
|       create_table :bulk_insert_parent_items, force: true do |t| | ||||
|       create_table :_test_bulk_insert_parent_items, force: true do |t| | ||||
|         t.string :name, null: false | ||||
|       end | ||||
| 
 | ||||
|       create_table :bulk_insert_items, force: true do |t| | ||||
|       create_table :_test_bulk_insert_items, force: true do |t| | ||||
|         t.string :name, null: true | ||||
|         t.integer :enum_value, null: false | ||||
|         t.text :encrypted_secret_value, null: false | ||||
|         t.string :encrypted_secret_value_iv, null: false | ||||
|         t.binary :sha_value, null: false, limit: 20 | ||||
|         t.jsonb :jsonb_value, null: false | ||||
|         t.belongs_to :bulk_insert_parent_item, foreign_key: true, null: true | ||||
|         t.belongs_to :bulk_insert_parent_item, foreign_key: { to_table: :_test_bulk_insert_parent_items }, null: true | ||||
|         t.timestamps null: true | ||||
| 
 | ||||
|         t.index :name, unique: true | ||||
|       end | ||||
| 
 | ||||
|       create_table :bulk_insert_items_with_composite_pk, id: false, force: true do |t| | ||||
|       create_table :_test_bulk_insert_items_with_composite_pk, id: false, force: true do |t| | ||||
|         t.integer :id, null: true | ||||
|         t.string :name, null: true | ||||
|       end | ||||
| 
 | ||||
|       execute("ALTER TABLE bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);") | ||||
|       execute("ALTER TABLE _test_bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);") | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   after(:all) do | ||||
|     ActiveRecord::Schema.define do | ||||
|       drop_table :bulk_insert_items, force: true | ||||
|       drop_table :bulk_insert_parent_items, force: true | ||||
|       drop_table :bulk_insert_items_with_composite_pk, force: true | ||||
|       drop_table :_test_bulk_insert_items, force: true | ||||
|       drop_table :_test_bulk_insert_parent_items, force: true | ||||
|       drop_table :_test_bulk_insert_items_with_composite_pk, force: true | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   BulkInsertParentItem = Class.new(ActiveRecord::Base) do | ||||
|     self.table_name = :bulk_insert_parent_items | ||||
|     self.table_name = :_test_bulk_insert_parent_items | ||||
|     self.inheritance_column = :_type_disabled | ||||
| 
 | ||||
|     def self.name | ||||
|  | @ -54,7 +54,7 @@ RSpec.describe BulkInsertSafe do | |||
| 
 | ||||
|   let_it_be(:bulk_insert_item_class) do | ||||
|     Class.new(ActiveRecord::Base) do | ||||
|       self.table_name = 'bulk_insert_items' | ||||
|       self.table_name = '_test_bulk_insert_items' | ||||
| 
 | ||||
|       include BulkInsertSafe | ||||
|       include ShaAttribute | ||||
|  | @ -247,7 +247,7 @@ RSpec.describe BulkInsertSafe do | |||
|     context 'when a model with composite primary key is inserted' do | ||||
|       let_it_be(:bulk_insert_items_with_composite_pk_class) do | ||||
|         Class.new(ActiveRecord::Base) do | ||||
|           self.table_name = 'bulk_insert_items_with_composite_pk' | ||||
|           self.table_name = '_test_bulk_insert_items_with_composite_pk' | ||||
| 
 | ||||
|           include BulkInsertSafe | ||||
|         end | ||||
|  |  | |||
|  | @ -6,42 +6,50 @@ RSpec.describe BulkInsertableAssociations do | |||
|   class BulkFoo < ApplicationRecord | ||||
|     include BulkInsertSafe | ||||
| 
 | ||||
|     self.table_name = '_test_bulk_foos' | ||||
| 
 | ||||
|     validates :name, presence: true | ||||
|   end | ||||
| 
 | ||||
|   class BulkBar < ApplicationRecord | ||||
|     include BulkInsertSafe | ||||
| 
 | ||||
|     self.table_name = '_test_bulk_bars' | ||||
|   end | ||||
| 
 | ||||
|   SimpleBar = Class.new(ApplicationRecord) | ||||
|   SimpleBar = Class.new(ApplicationRecord) do | ||||
|     self.table_name = '_test_simple_bars' | ||||
|   end | ||||
| 
 | ||||
|   class BulkParent < ApplicationRecord | ||||
|     include BulkInsertableAssociations | ||||
| 
 | ||||
|     has_many :bulk_foos | ||||
|     self.table_name = '_test_bulk_parents' | ||||
| 
 | ||||
|     has_many :bulk_foos, class_name: 'BulkFoo' | ||||
|     has_many :bulk_hunks, class_name: 'BulkFoo' | ||||
|     has_many :bulk_bars | ||||
|     has_many :simple_bars # not `BulkInsertSafe` | ||||
|     has_many :bulk_bars, class_name: 'BulkBar' | ||||
|     has_many :simple_bars, class_name: 'SimpleBar' # not `BulkInsertSafe` | ||||
|     has_one :bulk_foo # not supported | ||||
|   end | ||||
| 
 | ||||
|   before(:all) do | ||||
|     ActiveRecord::Schema.define do | ||||
|       create_table :bulk_parents, force: true do |t| | ||||
|       create_table :_test_bulk_parents, force: true do |t| | ||||
|         t.string :name, null: true | ||||
|       end | ||||
| 
 | ||||
|       create_table :bulk_foos, force: true do |t| | ||||
|       create_table :_test_bulk_foos, force: true do |t| | ||||
|         t.string :name, null: true | ||||
|         t.belongs_to :bulk_parent, null: false | ||||
|       end | ||||
| 
 | ||||
|       create_table :bulk_bars, force: true do |t| | ||||
|       create_table :_test_bulk_bars, force: true do |t| | ||||
|         t.string :name, null: true | ||||
|         t.belongs_to :bulk_parent, null: false | ||||
|       end | ||||
| 
 | ||||
|       create_table :simple_bars, force: true do |t| | ||||
|       create_table :_test_simple_bars, force: true do |t| | ||||
|         t.string :name, null: true | ||||
|         t.belongs_to :bulk_parent, null: false | ||||
|       end | ||||
|  | @ -50,10 +58,10 @@ RSpec.describe BulkInsertableAssociations do | |||
| 
 | ||||
|   after(:all) do | ||||
|     ActiveRecord::Schema.define do | ||||
|       drop_table :bulk_foos, force: true | ||||
|       drop_table :bulk_bars, force: true | ||||
|       drop_table :simple_bars, force: true | ||||
|       drop_table :bulk_parents, force: true | ||||
|       drop_table :_test_bulk_foos, force: true | ||||
|       drop_table :_test_bulk_bars, force: true | ||||
|       drop_table :_test_simple_bars, force: true | ||||
|       drop_table :_test_bulk_parents, force: true | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ require 'spec_helper' | |||
| 
 | ||||
| RSpec.describe WhereComposite do | ||||
|   describe '.where_composite' do | ||||
|     let_it_be(:test_table_name) { "test_table_#{SecureRandom.hex(10)}" } | ||||
|     let_it_be(:test_table_name) { "_test_table_#{SecureRandom.hex(10)}" } | ||||
| 
 | ||||
|     let(:model) do | ||||
|       tbl_name = test_table_name | ||||
|  |  | |||
|  | @ -571,16 +571,6 @@ RSpec.describe Group do | |||
|         it 'filters out project namespace' do | ||||
|           expect(group.descendants.find_by_id(project_namespace.id)).to be_nil | ||||
|         end | ||||
| 
 | ||||
|         context 'when include_sti_condition is disabled' do | ||||
|           before do | ||||
|             stub_feature_flags(include_sti_condition: false) | ||||
|           end | ||||
| 
 | ||||
|           it 'raises an exception' do | ||||
|             expect { group.descendants.find_by_id(project_namespace.id)}.to raise_error(ActiveRecord::SubclassNotFound) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -955,6 +955,28 @@ RSpec.describe ProjectPolicy do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'infrastructure google cloud feature' do | ||||
|     %w(guest reporter developer).each do |role| | ||||
|       context role do | ||||
|         let(:current_user) { send(role) } | ||||
| 
 | ||||
|         it 'disallows managing google cloud' do | ||||
|           expect_disallowed(:admin_project_google_cloud) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     %w(maintainer owner).each do |role| | ||||
|       context role do | ||||
|         let(:current_user) { send(role) } | ||||
| 
 | ||||
|         it 'allows managing google cloud' do | ||||
|           expect_allowed(:admin_project_google_cloud) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'design permissions' do | ||||
|     include DesignManagementTestHelpers | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,48 +2,106 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| # Mock Types | ||||
| MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret) | ||||
| 
 | ||||
| RSpec.describe Projects::GoogleCloudController do | ||||
|   let_it_be(:project) { create(:project, :public) } | ||||
| 
 | ||||
|   describe 'GET index' do | ||||
|     let_it_be(:url) { "#{project_google_cloud_index_path(project)}" } | ||||
| 
 | ||||
|     let(:subject) { get url } | ||||
|     context 'when a public request is made' do | ||||
|       it 'returns not found' do | ||||
|         get url | ||||
| 
 | ||||
|     context 'when user is authorized' do | ||||
|       let(:user) { project.creator } | ||||
| 
 | ||||
|       before do | ||||
|         sign_in(user) | ||||
|         subject | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when a project.guest makes request' do | ||||
|       let(:user) { create(:user) } | ||||
| 
 | ||||
|       it 'returns not found' do | ||||
|         project.add_guest(user) | ||||
|         sign_in(user) | ||||
| 
 | ||||
|         get url | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when project.developer makes request' do | ||||
|       let(:user) { create(:user) } | ||||
| 
 | ||||
|       it 'returns not found' do | ||||
|         project.add_developer(user) | ||||
|         sign_in(user) | ||||
| 
 | ||||
|         get url | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when project.maintainer makes request' do | ||||
|       let(:user) { create(:user) } | ||||
| 
 | ||||
|       it 'returns successful' do | ||||
|         project.add_maintainer(user) | ||||
|         sign_in(user) | ||||
| 
 | ||||
|         get url | ||||
| 
 | ||||
|       it 'renders content' do | ||||
|         expect(response).to be_successful | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when user is unauthorized' do | ||||
|       let(:user) { create(:user) } | ||||
|     context 'when project.creator makes request' do | ||||
|       let(:user) { project.creator } | ||||
| 
 | ||||
|       before do | ||||
|         project.add_guest(user) | ||||
|       it 'returns successful' do | ||||
|         sign_in(user) | ||||
|         subject | ||||
|       end | ||||
| 
 | ||||
|       it 'shows 404' do | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|         get url | ||||
| 
 | ||||
|         expect(response).to be_successful | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when no user is present' do | ||||
|       before do | ||||
|         subject | ||||
|     describe 'when authorized user makes request' do | ||||
|       let(:user) { project.creator } | ||||
| 
 | ||||
|       context 'but gitlab instance is not configured for google oauth2' do | ||||
|         before do | ||||
|           unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '') | ||||
|           allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) | ||||
|                                                     .with('google_oauth2') | ||||
|                                                     .and_return(unconfigured_google_oauth2) | ||||
|         end | ||||
| 
 | ||||
|         it 'returns forbidden' do | ||||
|           sign_in(user) | ||||
| 
 | ||||
|           get url | ||||
| 
 | ||||
|           expect(response).to have_gitlab_http_status(:forbidden) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'shows 404' do | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|       context 'but feature flag is disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(incubation_5mp_google_cloud: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'returns not found' do | ||||
|           sign_in(user) | ||||
| 
 | ||||
|           get url | ||||
| 
 | ||||
|           expect(response).to have_gitlab_http_status(:not_found) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -26,28 +26,6 @@ RSpec.describe Ci::ExternalPullRequests::CreatePipelineService do | |||
|           pull_request.update!(source_branch: source_branch.name, source_sha: source_branch.target) | ||||
|         end | ||||
| 
 | ||||
|         context 'when the FF ci_create_external_pr_pipeline_async is disabled' do | ||||
|           before do | ||||
|             stub_feature_flags(ci_create_external_pr_pipeline_async: false) | ||||
|           end | ||||
| 
 | ||||
|           it 'creates a pipeline for external pull request', :aggregate_failures do | ||||
|             pipeline = execute.payload | ||||
| 
 | ||||
|             expect(execute).to be_success | ||||
|             expect(pipeline).to be_valid | ||||
|             expect(pipeline).to be_persisted | ||||
|             expect(pipeline).to be_external_pull_request_event | ||||
|             expect(pipeline).to eq(project.ci_pipelines.last) | ||||
|             expect(pipeline.external_pull_request).to eq(pull_request) | ||||
|             expect(pipeline.user).to eq(user) | ||||
|             expect(pipeline.status).to eq('created') | ||||
|             expect(pipeline.ref).to eq(pull_request.source_branch) | ||||
|             expect(pipeline.sha).to eq(pull_request.source_sha) | ||||
|             expect(pipeline.source_sha).to eq(pull_request.source_sha) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         it 'enqueues Ci::ExternalPullRequests::CreatePipelineWorker' do | ||||
|           expect { execute } | ||||
|             .to change { ::Ci::ExternalPullRequests::CreatePipelineWorker.jobs.count } | ||||
|  |  | |||
|  | @ -8,44 +8,44 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do | |||
|   def create_table_structure | ||||
|     migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers) | ||||
| 
 | ||||
|     migration.create_table :loose_fk_parent_table | ||||
|     migration.create_table :_test_loose_fk_parent_table | ||||
| 
 | ||||
|     migration.create_table :loose_fk_child_table_1 do |t| | ||||
|     migration.create_table :_test_loose_fk_child_table_1 do |t| | ||||
|       t.bigint :parent_id | ||||
|     end | ||||
| 
 | ||||
|     migration.create_table :loose_fk_child_table_2 do |t| | ||||
|     migration.create_table :_test_loose_fk_child_table_2 do |t| | ||||
|       t.bigint :parent_id_with_different_column | ||||
|     end | ||||
| 
 | ||||
|     migration.track_record_deletions(:loose_fk_parent_table) | ||||
|     migration.track_record_deletions(:_test_loose_fk_parent_table) | ||||
|   end | ||||
| 
 | ||||
|   let(:parent_model) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_parent_table' | ||||
|       self.table_name = '_test_loose_fk_parent_table' | ||||
| 
 | ||||
|       include LooseForeignKey | ||||
| 
 | ||||
|       loose_foreign_key :loose_fk_child_table_1, :parent_id, on_delete: :async_delete | ||||
|       loose_foreign_key :loose_fk_child_table_2, :parent_id_with_different_column, on_delete: :async_nullify | ||||
|       loose_foreign_key :_test_loose_fk_child_table_1, :parent_id, on_delete: :async_delete | ||||
|       loose_foreign_key :_test_loose_fk_child_table_2, :parent_id_with_different_column, on_delete: :async_nullify | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let(:child_model_1) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_child_table_1' | ||||
|       self.table_name = '_test_loose_fk_child_table_1' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let(:child_model_2) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_child_table_2' | ||||
|       self.table_name = '_test_loose_fk_child_table_2' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let(:loose_fk_child_table_1) { table(:loose_fk_child_table_1) } | ||||
|   let(:loose_fk_child_table_2) { table(:loose_fk_child_table_2) } | ||||
|   let(:loose_fk_child_table_1) { table(:_test_loose_fk_child_table_1) } | ||||
|   let(:loose_fk_child_table_2) { table(:_test_loose_fk_child_table_2) } | ||||
|   let(:parent_record_1) { parent_model.create! } | ||||
|   let(:other_parent_record) { parent_model.create! } | ||||
| 
 | ||||
|  | @ -73,9 +73,9 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do | |||
| 
 | ||||
|   after(:all) do | ||||
|     migration = ActiveRecord::Migration.new | ||||
|     migration.drop_table :loose_fk_parent_table | ||||
|     migration.drop_table :loose_fk_child_table_1 | ||||
|     migration.drop_table :loose_fk_child_table_2 | ||||
|     migration.drop_table :_test_loose_fk_parent_table | ||||
|     migration.drop_table :_test_loose_fk_child_table_1 | ||||
|     migration.drop_table :_test_loose_fk_child_table_2 | ||||
|   end | ||||
| 
 | ||||
|   context 'when parent records are deleted' do | ||||
|  | @ -90,8 +90,8 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do | |||
|       described_class.new(parent_klass: parent_model, | ||||
|                           deleted_parent_records: LooseForeignKeys::DeletedRecord.status_pending.all, | ||||
|                           models_by_table_name: { | ||||
|                             'loose_fk_child_table_1' => child_model_1, | ||||
|                             'loose_fk_child_table_2' => child_model_2 | ||||
|                             '_test_loose_fk_child_table_1' => child_model_1, | ||||
|                             '_test_loose_fk_child_table_2' => child_model_2 | ||||
|                           }).execute | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,26 +25,6 @@ RSpec.shared_examples 'namespace traversal scopes' do | |||
|     it { is_expected.to contain_exactly(group_1.id, group_2.id) } | ||||
|   end | ||||
| 
 | ||||
|   describe '.without_sti_condition' do | ||||
|     subject { described_class.where(type: 'Group').without_sti_condition } | ||||
| 
 | ||||
|     context 'when include_sti_condition is enabled' do | ||||
|       before do | ||||
|         stub_feature_flags(include_sti_condition: true) | ||||
|       end | ||||
| 
 | ||||
|       it { expect(subject.where_values_hash).to have_key('type') } | ||||
|     end | ||||
| 
 | ||||
|     context 'when include_sti_condition is disabled' do | ||||
|       before do | ||||
|         stub_feature_flags(include_sti_condition: false) | ||||
|       end | ||||
| 
 | ||||
|       it { expect(subject.where_values_hash).not_to have_key('type') } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.order_by_depth' do | ||||
|     subject { described_class.where(id: [group_1, nested_group_1, deep_nested_group_1]).order_by_depth(direction) } | ||||
| 
 | ||||
|  |  | |||
|  | @ -215,7 +215,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do | |||
|         stub_feature_flags(database_async_index_creation: true) | ||||
| 
 | ||||
|         expect(Gitlab::Database::AsyncIndexes).to receive(:create_pending_indexes!).ordered.exactly(databases_count).times | ||||
|         expect(Gitlab::Database::Reindexing).to receive(:automatic_reindexing).ordered.once | ||||
|         expect(Gitlab::Database::Reindexing).to receive(:automatic_reindexing).ordered.exactly(databases_count).times | ||||
| 
 | ||||
|         run_rake_task('gitlab:db:reindex') | ||||
|       end | ||||
|  | @ -233,7 +233,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do | |||
| 
 | ||||
|     context 'calls automatic reindexing' do | ||||
|       it 'uses all candidate indexes' do | ||||
|         expect(Gitlab::Database::Reindexing).to receive(:automatic_reindexing).once | ||||
|         expect(Gitlab::Database::Reindexing).to receive(:automatic_reindexing).exactly(databases_count).times | ||||
| 
 | ||||
|         run_rake_task('gitlab:db:reindex') | ||||
|       end | ||||
|  |  | |||
|  | @ -8,69 +8,69 @@ RSpec.describe LooseForeignKeys::CleanupWorker do | |||
|   def create_table_structure | ||||
|     migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers) | ||||
| 
 | ||||
|     migration.create_table :loose_fk_parent_table_1 | ||||
|     migration.create_table :loose_fk_parent_table_2 | ||||
|     migration.create_table :_test_loose_fk_parent_table_1 | ||||
|     migration.create_table :_test_loose_fk_parent_table_2 | ||||
| 
 | ||||
|     migration.create_table :loose_fk_child_table_1_1 do |t| | ||||
|     migration.create_table :_test_loose_fk_child_table_1_1 do |t| | ||||
|       t.bigint :parent_id | ||||
|     end | ||||
| 
 | ||||
|     migration.create_table :loose_fk_child_table_1_2 do |t| | ||||
|     migration.create_table :_test_loose_fk_child_table_1_2 do |t| | ||||
|       t.bigint :parent_id_with_different_column | ||||
|     end | ||||
| 
 | ||||
|     migration.create_table :loose_fk_child_table_2_1 do |t| | ||||
|     migration.create_table :_test_loose_fk_child_table_2_1 do |t| | ||||
|       t.bigint :parent_id | ||||
|     end | ||||
| 
 | ||||
|     migration.track_record_deletions(:loose_fk_parent_table_1) | ||||
|     migration.track_record_deletions(:loose_fk_parent_table_2) | ||||
|     migration.track_record_deletions(:_test_loose_fk_parent_table_1) | ||||
|     migration.track_record_deletions(:_test_loose_fk_parent_table_2) | ||||
|   end | ||||
| 
 | ||||
|   let!(:parent_model_1) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_parent_table_1' | ||||
|       self.table_name = '_test_loose_fk_parent_table_1' | ||||
| 
 | ||||
|       include LooseForeignKey | ||||
| 
 | ||||
|       loose_foreign_key :loose_fk_child_table_1_1, :parent_id, on_delete: :async_delete | ||||
|       loose_foreign_key :loose_fk_child_table_1_2, :parent_id_with_different_column, on_delete: :async_nullify | ||||
|       loose_foreign_key :_test_loose_fk_child_table_1_1, :parent_id, on_delete: :async_delete | ||||
|       loose_foreign_key :_test_loose_fk_child_table_1_2, :parent_id_with_different_column, on_delete: :async_nullify | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let!(:parent_model_2) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_parent_table_2' | ||||
|       self.table_name = '_test_loose_fk_parent_table_2' | ||||
| 
 | ||||
|       include LooseForeignKey | ||||
| 
 | ||||
|       loose_foreign_key :loose_fk_child_table_2_1, :parent_id, on_delete: :async_delete | ||||
|       loose_foreign_key :_test_loose_fk_child_table_2_1, :parent_id, on_delete: :async_delete | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let!(:child_model_1) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_child_table_1_1' | ||||
|       self.table_name = '_test_loose_fk_child_table_1_1' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let!(:child_model_2) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_child_table_1_2' | ||||
|       self.table_name = '_test_loose_fk_child_table_1_2' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let!(:child_model_3) do | ||||
|     Class.new(ApplicationRecord) do | ||||
|       self.table_name = 'loose_fk_child_table_2_1' | ||||
|       self.table_name = '_test_loose_fk_child_table_2_1' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let(:loose_fk_parent_table_1) { table(:loose_fk_parent_table_1) } | ||||
|   let(:loose_fk_parent_table_2) { table(:loose_fk_parent_table_2) } | ||||
|   let(:loose_fk_child_table_1_1) { table(:loose_fk_child_table_1_1) } | ||||
|   let(:loose_fk_child_table_1_2) { table(:loose_fk_child_table_1_2) } | ||||
|   let(:loose_fk_child_table_2_1) { table(:loose_fk_child_table_2_1) } | ||||
|   let(:loose_fk_parent_table_1) { table(:_test_loose_fk_parent_table_1) } | ||||
|   let(:loose_fk_parent_table_2) { table(:_test_loose_fk_parent_table_2) } | ||||
|   let(:loose_fk_child_table_1_1) { table(:_test_loose_fk_child_table_1_1) } | ||||
|   let(:loose_fk_child_table_1_2) { table(:_test_loose_fk_child_table_1_2) } | ||||
|   let(:loose_fk_child_table_2_1) { table(:_test_loose_fk_child_table_2_1) } | ||||
| 
 | ||||
|   before(:all) do | ||||
|     create_table_structure | ||||
|  | @ -79,11 +79,11 @@ RSpec.describe LooseForeignKeys::CleanupWorker do | |||
|   after(:all) do | ||||
|     migration = ActiveRecord::Migration.new | ||||
| 
 | ||||
|     migration.drop_table :loose_fk_parent_table_1 | ||||
|     migration.drop_table :loose_fk_parent_table_2 | ||||
|     migration.drop_table :loose_fk_child_table_1_1 | ||||
|     migration.drop_table :loose_fk_child_table_1_2 | ||||
|     migration.drop_table :loose_fk_child_table_2_1 | ||||
|     migration.drop_table :_test_loose_fk_parent_table_1 | ||||
|     migration.drop_table :_test_loose_fk_parent_table_2 | ||||
|     migration.drop_table :_test_loose_fk_child_table_1_1 | ||||
|     migration.drop_table :_test_loose_fk_child_table_1_2 | ||||
|     migration.drop_table :_test_loose_fk_child_table_2_1 | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue