Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a150bd5bac
commit
6d8bbacd13
26 changed files with 899 additions and 30 deletions
|
@ -38,4 +38,11 @@
|
|||
|
||||
For example, if the solution will take a product manager, designer, and engineer two weeks of effort - you may quantify this as 1.5 (based on 0.5 months x 3 people). -->
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] The problem is well understood by the PM to have an understanding summarized in a RICE score
|
||||
- [ ] The problem is well understood by the PM to decide if they want to move forward with this idea or drop it
|
||||
- [ ] The problem is well described and detailed with necessary requirements for product design to understand the problem
|
||||
- [ ] The problem is well described and detailed with necessary requirements for engineering to understand the problem
|
||||
|
||||
/label ~"workflow::validation backlog" ~devops:: ~category: ~group::
|
||||
|
|
40
app/graphql/mutations/groups/update.rb
Normal file
40
app/graphql/mutations/groups/update.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Groups
|
||||
class Update < Mutations::BaseMutation
|
||||
include Mutations::ResolvesGroup
|
||||
|
||||
graphql_name 'GroupUpdate'
|
||||
|
||||
authorize :admin_group
|
||||
|
||||
field :group, Types::GroupType,
|
||||
null: true,
|
||||
description: 'The group after update.'
|
||||
|
||||
argument :full_path, GraphQL::Types::ID,
|
||||
required: true,
|
||||
description: 'Full path of the group that will be updated.'
|
||||
argument :shared_runners_setting, Types::Namespace::SharedRunnersSettingEnum,
|
||||
required: true,
|
||||
description: copy_field_description(Types::GroupType, :shared_runners_setting)
|
||||
|
||||
def resolve(full_path:, **args)
|
||||
group = authorized_find!(full_path: full_path)
|
||||
|
||||
unless ::Groups::UpdateService.new(group, current_user, args).execute
|
||||
return { group: nil, errors: group.errors.full_messages }
|
||||
end
|
||||
|
||||
{ group: group, errors: [] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_group(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -105,6 +105,7 @@ module Types
|
|||
mount_mutation Mutations::Ci::Runner::Delete, feature_flag: :runner_graphql_query
|
||||
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query
|
||||
mount_mutation Mutations::Namespace::PackageSettings::Update
|
||||
mount_mutation Mutations::Groups::Update
|
||||
mount_mutation Mutations::UserCallouts::Create
|
||||
mount_mutation Mutations::Packages::Destroy
|
||||
mount_mutation Mutations::Packages::DestroyFile
|
||||
|
|
16
config/initializers/00_rails_disable_joins.rb
Normal file
16
config/initializers/00_rails_disable_joins.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Backported from Rails 7.0
|
||||
# Initial support for has_many :through was implemented in https://github.com/rails/rails/pull/41937
|
||||
# Support for has_one :through was implemented in https://github.com/rails/rails/pull/42079
|
||||
raise 'DisableJoins patch is only to be used with versions of Rails < 7.0' unless Rails::VERSION::MAJOR < 7
|
||||
|
||||
ActiveRecord::Associations::Association.prepend(GemExtensions::ActiveRecord::Association)
|
||||
# Temporarily allow :disable_joins to accept a lambda argument, to control rollout with feature flags
|
||||
ActiveRecord::Associations::Association.prepend(GemExtensions::ActiveRecord::ConfigurableDisableJoins)
|
||||
ActiveRecord::Associations::Builder::HasOne.prepend(GemExtensions::ActiveRecord::Associations::Builder::HasOne)
|
||||
ActiveRecord::Associations::Builder::HasMany.prepend(GemExtensions::ActiveRecord::Associations::Builder::HasMany)
|
||||
ActiveRecord::Associations::HasOneThroughAssociation.prepend(GemExtensions::ActiveRecord::Associations::HasOneThroughAssociation)
|
||||
ActiveRecord::Associations::HasManyThroughAssociation.prepend(GemExtensions::ActiveRecord::Associations::HasManyThroughAssociation)
|
||||
ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(GemExtensions::ActiveRecord::Associations::Preloader::ThroughAssociation)
|
||||
ActiveRecord::Base.extend(GemExtensions::ActiveRecord::DelegateCache)
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddRuleIndexToSecurityOrchestrationPolicyRuleSchedules < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :security_orchestration_policy_rule_schedules, :rule_index, :integer, null: false, default: 0
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210805085706
Normal file
1
db/schema_migrations/20210805085706
Normal file
|
@ -0,0 +1 @@
|
|||
ec968f1f9fcc5a3551664e74726e1c65b327128e2388e1357ae6d0cf6f05fb95
|
|
@ -18053,6 +18053,7 @@ CREATE TABLE security_orchestration_policy_rule_schedules (
|
|||
user_id bigint NOT NULL,
|
||||
policy_index integer NOT NULL,
|
||||
cron text NOT NULL,
|
||||
rule_index integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT check_915825a76e CHECK ((char_length(cron) <= 255))
|
||||
);
|
||||
|
||||
|
|
|
@ -2360,6 +2360,26 @@ Input type: `GitlabSubscriptionActivateInput`
|
|||
| <a id="mutationgitlabsubscriptionactivateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationgitlabsubscriptionactivatelicense"></a>`license` | [`CurrentLicense`](#currentlicense) | The current license. |
|
||||
|
||||
### `Mutation.groupUpdate`
|
||||
|
||||
Input type: `GroupUpdateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationgroupupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationgroupupdatefullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the group that will be updated. |
|
||||
| <a id="mutationgroupupdatesharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting!`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationgroupupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationgroupupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationgroupupdategroup"></a>`group` | [`Group`](#group) | The group after update. |
|
||||
|
||||
### `Mutation.httpIntegrationCreate`
|
||||
|
||||
Input type: `HttpIntegrationCreateInput`
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
---
|
||||
type: reference, dev
|
||||
stage: none
|
||||
group: Development
|
||||
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
|
||||
info: For assistance with this Style Guide page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-other-projects-and-subjects.
|
||||
description: "GitLab development - how to document features deployed behind feature flags"
|
||||
---
|
||||
|
||||
|
@ -30,31 +28,31 @@ Information about feature flags should be in a **Note** at the start of the topi
|
|||
The note has three parts, and follows this structure:
|
||||
|
||||
```markdown
|
||||
NOTE:
|
||||
FLAG:
|
||||
<Self-managed GitLab availability information.> <GitLab.com availability information.>
|
||||
<This feature is not ready for production use.>
|
||||
```
|
||||
|
||||
### Self-managed GitLab availability information
|
||||
|
||||
|If the feature is... | Use this text |
|
||||
|-|-|
|
||||
|Available|`On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
|Unavailable|`On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
|Available, per-group|`On self-managed GitLab, by default this feature is available. To hide the feature per group, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
|Unavailable, per-group|`On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
|Available, per-project|`On self-managed GitLab, by default this feature is available. To hide the feature per project or for your entire instance, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
|Unavailable, per-project|`On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
|Available, per-user|`On self-managed GitLab, by default this feature is available. To hide the feature per user, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
|Unavailable, per-user|`On self-managed GitLab, by default this feature is not available. To make it available per user, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).`|
|
||||
| If the feature is... | Use this text |
|
||||
|--------------------------|---------------|
|
||||
| Available | `On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
| Unavailable | `On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
| Available, per-group | `On self-managed GitLab, by default this feature is available. To hide the feature per group, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
| Unavailable, per-group | `On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
| Available, per-project | `On self-managed GitLab, by default this feature is available. To hide the feature per project or for your entire instance, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
| Unavailable, per-project | `On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
| Available, per-user | `On self-managed GitLab, by default this feature is available. To hide the feature per user, ask an administrator to [disable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
| Unavailable, per-user | `On self-managed GitLab, by default this feature is not available. To make it available per user, ask an administrator to [enable the <flag name> flag](<path to>/administration/feature_flags.md).` |
|
||||
|
||||
### GitLab.com availability information
|
||||
|
||||
|If the feature is... | Use this text |
|
||||
|-|-|
|
||||
|Available| `On GitLab.com, this feature is available.` |
|
||||
|Available to GitLab.com admins only| `On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.`
|
||||
|Unavailable| `On GitLab.com, this feature is not available.`|
|
||||
| If the feature is... | Use this text |
|
||||
|-------------------------------------|---------------|
|
||||
| Available | `On GitLab.com, this feature is available.` |
|
||||
| Available to GitLab.com admins only | `On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.`
|
||||
| Unavailable | `On GitLab.com, this feature is not available.`|
|
||||
|
||||
### Optional information
|
||||
|
||||
|
@ -82,7 +80,7 @@ The following examples show the progression of a feature flag.
|
|||
```markdown
|
||||
> Introduced in GitLab 13.7.
|
||||
|
||||
NOTE:
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the `forti_token_cloud` flag](../administration/feature_flags.md).`
|
||||
The feature is not ready for production use.
|
||||
|
@ -94,7 +92,7 @@ If it were to be updated in the future to enable its use in production, you can
|
|||
> - Introduced in GitLab 13.7.
|
||||
> - [Enabled with `forti_token_cloud` flag](https://gitlab.com/issue/etc) for self-managed GitLab in GitLab X.X and ready for production use.
|
||||
|
||||
NOTE:
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature per user,
|
||||
ask an administrator to [disable the `forti_token_cloud` flag](../administration/feature_flags.md).
|
||||
```
|
||||
|
|
|
@ -1337,8 +1337,13 @@ example:
|
|||
|
||||
Use alert boxes to call attention to information.
|
||||
|
||||
Alert boxes are generated when the words `NOTE:` or `WARNING:` are followed by a
|
||||
line break. For example:
|
||||
Alert boxes are generated when one of these words is followed by a line break:
|
||||
|
||||
- `FLAG:`
|
||||
- `NOTE:`
|
||||
- `WARNING:`
|
||||
|
||||
For example:
|
||||
|
||||
```markdown
|
||||
NOTE:
|
||||
|
@ -1351,6 +1356,11 @@ To display an alert box for multiple paragraphs, lists, or headers, use
|
|||
Alert boxes render only on the GitLab documentation site (<https://docs.gitlab.com>).
|
||||
In the GitLab product help, alert boxes appear as plain text.
|
||||
|
||||
### Flag
|
||||
|
||||
Use this alert type to describe a feature's availability. For information about how to format
|
||||
`FLAG` alerts, see [Document features deployed behind feature flags](../feature_flags.md).
|
||||
|
||||
### Note
|
||||
|
||||
Use notes sparingly. Too many notes can make topics difficult to scan.
|
||||
|
|
|
@ -38,14 +38,23 @@ for each tier, see the
|
|||
## Subscription seats
|
||||
|
||||
A GitLab self-managed subscription uses a hybrid model. You pay for a subscription
|
||||
according to the maximum number of users enabled during the subscription period.
|
||||
according to the [maximum number](#maximum-users) of users enabled during the subscription period.
|
||||
For instances that aren't offline or on a closed network, the maximum number of
|
||||
simultaneous users in the GitLab self-managed installation is checked each quarter.
|
||||
|
||||
If an instance is unable to generate a quarterly usage report, the existing [true-up model](#users-over-license) is used.
|
||||
If an instance is unable to generate a quarterly usage report, the existing [true-up model](#users-over-license) is used.
|
||||
Prorated charges are not possible without a quarterly usage report.
|
||||
|
||||
### Billable users
|
||||
### View user totals
|
||||
|
||||
You can view users for your license and determine if you've gone over your subscription.
|
||||
|
||||
1. On the top bar, select **Menu >** **{admin}** **Admin**.
|
||||
1. On the left menu, select **Subscription**.
|
||||
|
||||
The lists of users are displayed.
|
||||
|
||||
#### Billable users
|
||||
|
||||
A _billable user_ counts against the number of subscription seats. Every user is considered a
|
||||
billable user, with the following exceptions:
|
||||
|
@ -63,11 +72,28 @@ billable user, with the following exceptions:
|
|||
|
||||
**Billable users** as reported in the `/admin` section is updated once per day.
|
||||
|
||||
### Maximum users
|
||||
#### Maximum users
|
||||
|
||||
GitLab shows the highest number of billable users for the current license period.
|
||||
The number of _maximum users_ reflects the highest number of billable users for the current license period.
|
||||
|
||||
To view this list, on the top bar, select **Menu >** **{admin}** **Admin**. On the left menu, select **Subscription**. In the lower left, the list of **Maximum users** is displayed.
|
||||
#### Users over license
|
||||
|
||||
The number of _users over license_ shows how many users are in excess of the number allowed by the license. This number reflects the current license period.
|
||||
|
||||
For example, if:
|
||||
|
||||
- The license allows 100 users and
|
||||
- **Maximum users** is 150,
|
||||
|
||||
Then this value would be 50.
|
||||
|
||||
If the **Maximum users** value is less than or equal to 100, then this value is 0.
|
||||
|
||||
A trial license always displays zero for **Users over license**.
|
||||
|
||||
If you add more users to your GitLab instance than you are licensed for, payment for the additional users is due [at the time of renewal](../quarterly_reconciliation.md).
|
||||
|
||||
If you do not add these users during the renewal process, your license key will not work.
|
||||
|
||||
### Tips for managing users and subscription seats
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ You can specify a custom URL to which users are directed when they:
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43157) in GitLab 13.5.
|
||||
> - Enabled on GitLab.com and is ready for production use.
|
||||
|
||||
NOTE:
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to
|
||||
[enable the `:help_page_documentation_redirect` flag](../../../administration/feature_flags.md).
|
||||
On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
|
||||
|
|
37
lib/gem_extensions/active_record/association.rb
Normal file
37
lib/gem_extensions/active_record/association.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module Association
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
attr_reader :disable_joins
|
||||
|
||||
def initialize(owner, reflection)
|
||||
super
|
||||
|
||||
@disable_joins = @reflection.options[:disable_joins] || false
|
||||
end
|
||||
|
||||
def scope
|
||||
if disable_joins
|
||||
DisableJoins::Associations::AssociationScope.create.scope(self)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def association_scope
|
||||
if klass
|
||||
@association_scope ||= begin # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
if disable_joins
|
||||
DisableJoins::Associations::AssociationScope.scope(self)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
module Builder
|
||||
module HasMany
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def valid_options(options)
|
||||
valid = super
|
||||
valid += [:disable_joins] if options[:disable_joins] && options[:through]
|
||||
valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
module Builder
|
||||
module HasOne
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def valid_options(options)
|
||||
valid = super
|
||||
valid += [:disable_joins] if options[:disable_joins] && options[:through]
|
||||
valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
module HasManyThroughAssociation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def find_target
|
||||
return [] unless target_reflection_has_associated_record?
|
||||
return scope.to_a if disable_joins
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
module HasOneThroughAssociation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def find_target
|
||||
return scope.first if disable_joins
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
module Preloader
|
||||
module ThroughAssociation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def through_scope
|
||||
scope = through_reflection.klass.unscoped
|
||||
options = reflection.options
|
||||
|
||||
return scope if options[:disable_joins]
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module ConfigurableDisableJoins
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def disable_joins
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
return @disable_joins.call if @disable_joins.is_a?(Proc)
|
||||
|
||||
@disable_joins
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
lib/gem_extensions/active_record/delegate_cache.rb
Normal file
34
lib/gem_extensions/active_record/delegate_cache.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module DelegateCache
|
||||
def relation_delegate_class(klass)
|
||||
@relation_delegate_cache2[klass] || super # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
|
||||
def initialize_relation_delegate_cache_disable_joins
|
||||
@relation_delegate_cache2 = {} # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
[
|
||||
DisableJoins::Relation
|
||||
].each do |klass|
|
||||
delegate = Class.new(klass) do
|
||||
include ::ActiveRecord::Delegation::ClassSpecificRelation
|
||||
end
|
||||
include_relation_methods(delegate)
|
||||
mangled_name = klass.name.gsub("::", "_")
|
||||
const_set mangled_name, delegate
|
||||
private_constant mangled_name
|
||||
|
||||
@relation_delegate_cache2[klass] = delegate # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
end
|
||||
|
||||
def inherited(child_class)
|
||||
child_class.initialize_relation_delegate_cache_disable_joins
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module DisableJoins
|
||||
module Associations
|
||||
class AssociationScope < ::ActiveRecord::Associations::AssociationScope # :nodoc:
|
||||
def scope(association)
|
||||
source_reflection = association.reflection
|
||||
owner = association.owner
|
||||
unscoped = association.klass.unscoped
|
||||
reverse_chain = get_chain(source_reflection, association, unscoped.alias_tracker).reverse
|
||||
|
||||
previous_reflection, last_reflection, last_ordered, last_join_ids = last_scope_chain(reverse_chain, owner)
|
||||
|
||||
add_constraints(last_reflection, last_reflection.join_primary_key, last_join_ids, owner, last_ordered,
|
||||
previous_reflection: previous_reflection)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def last_scope_chain(reverse_chain, owner)
|
||||
# Pulled from https://github.com/rails/rails/pull/42448
|
||||
# Fixes cases where the foreign key is not id
|
||||
first_item = reverse_chain.shift
|
||||
first_scope = [nil, first_item, false, [owner._read_attribute(first_item.join_foreign_key)]]
|
||||
|
||||
reverse_chain.inject(first_scope) do |(previous_reflection, reflection, ordered, join_ids), next_reflection|
|
||||
key = reflection.join_primary_key
|
||||
records = add_constraints(reflection, key, join_ids, owner, ordered, previous_reflection: previous_reflection)
|
||||
foreign_key = next_reflection.join_foreign_key
|
||||
record_ids = records.pluck(foreign_key) # rubocop:disable CodeReuse/ActiveRecord
|
||||
records_ordered = records && records.order_values.any?
|
||||
|
||||
[reflection, next_reflection, records_ordered, record_ids]
|
||||
end
|
||||
end
|
||||
|
||||
def add_constraints(reflection, key, join_ids, owner, ordered, previous_reflection: nil)
|
||||
scope = reflection.build_scope(reflection.aliased_table).where(key => join_ids) # rubocop:disable CodeReuse/ActiveRecord
|
||||
|
||||
# Pulled from https://github.com/rails/rails/pull/42590
|
||||
# Fixes cases where used with an STI type
|
||||
relation = reflection.klass.scope_for_association
|
||||
scope.merge!(
|
||||
relation.except(:select, :create_with, :includes, :preload, :eager_load, :joins, :left_outer_joins)
|
||||
)
|
||||
|
||||
# Attempt to fix use case where we have a polymorphic relationship
|
||||
# Build on an additional scope to filter by the polymorphic type
|
||||
if reflection.type
|
||||
polymorphic_class = previous_reflection.try(:klass) || owner.class
|
||||
|
||||
polymorphic_type = transform_value(polymorphic_class.polymorphic_name)
|
||||
scope = apply_scope(scope, reflection.aliased_table, reflection.type, polymorphic_type)
|
||||
end
|
||||
|
||||
scope = reflection.constraints.inject(scope) do |memo, scope_chain_item|
|
||||
item = eval_scope(reflection, scope_chain_item, owner)
|
||||
scope.unscope!(*item.unscope_values)
|
||||
scope.where_clause += item.where_clause
|
||||
scope.order_values = item.order_values | scope.order_values
|
||||
scope
|
||||
end
|
||||
|
||||
if scope.order_values.empty? && ordered
|
||||
split_scope = DisableJoins::Relation.create(scope.klass, key, join_ids)
|
||||
split_scope.where_clause += scope.where_clause
|
||||
split_scope
|
||||
else
|
||||
scope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
43
lib/gem_extensions/active_record/disable_joins/relation.rb
Normal file
43
lib/gem_extensions/active_record/disable_joins/relation.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GemExtensions
|
||||
module ActiveRecord
|
||||
module DisableJoins
|
||||
class Relation < ::ActiveRecord::Relation
|
||||
attr_reader :ids, :key
|
||||
|
||||
def initialize(klass, key, ids)
|
||||
@ids = ids.uniq
|
||||
@key = key
|
||||
super(klass)
|
||||
end
|
||||
|
||||
def limit(value)
|
||||
records.take(value) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def first(limit = nil)
|
||||
if limit
|
||||
records.limit(limit).first
|
||||
else
|
||||
records.first
|
||||
end
|
||||
end
|
||||
|
||||
def load
|
||||
super
|
||||
records = @records
|
||||
|
||||
records_by_id = records.group_by do |record|
|
||||
record[key]
|
||||
end
|
||||
|
||||
records = ids.flat_map { |id| records_by_id[id.to_i] }
|
||||
records.compact!
|
||||
|
||||
@records = records
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5265,6 +5265,9 @@ msgstr ""
|
|||
msgid "Billing|Enter at least three characters to search."
|
||||
msgstr ""
|
||||
|
||||
msgid "Billing|Export list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billing|Group"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13621,6 +13624,9 @@ msgstr ""
|
|||
msgid "Failed to find import label for Jira import."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to generate export, please try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to generate report, please try again after sometime"
|
||||
msgstr ""
|
||||
|
||||
|
|
74
spec/graphql/mutations/groups/update_spec.rb
Normal file
74
spec/graphql/mutations/groups/update_spec.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Groups::Update do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be_with_reload(:group) { create(:group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:params) { { full_path: group.full_path } }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:admin_group) }
|
||||
|
||||
describe '#resolve' do
|
||||
subject { described_class.new(object: group, context: { current_user: user }, field: nil).resolve(**params) }
|
||||
|
||||
RSpec.shared_examples 'updating the group shared runners setting' do
|
||||
it 'updates the group shared runners setting' do
|
||||
expect { subject }
|
||||
.to change { group.reload.shared_runners_setting }.from('enabled').to('disabled_and_unoverridable')
|
||||
end
|
||||
|
||||
it 'returns no errors' do
|
||||
expect(subject).to eq(errors: [], group: group)
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
let_it_be(:params) { { full_path: group.full_path, shared_runners_setting: 'inexistent_setting' } }
|
||||
|
||||
it 'doesn\'t update the shared_runners_setting' do
|
||||
expect { subject }
|
||||
.not_to change { group.reload.shared_runners_setting }
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
expect(subject).to eq(
|
||||
group: nil,
|
||||
errors: ["Update shared runners state must be one of: #{::Namespace::SHARED_RUNNERS_SETTINGS.join(', ')}"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'denying access to group shared runners setting' do
|
||||
it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'changing shared runners setting' do
|
||||
let_it_be(:params) do
|
||||
{ full_path: group.full_path,
|
||||
shared_runners_setting: 'disabled_and_unoverridable' }
|
||||
end
|
||||
|
||||
where(:user_role, :shared_examples_name) do
|
||||
:owner | 'updating the group shared runners setting'
|
||||
:developer | 'denying access to group shared runners setting'
|
||||
:reporter | 'denying access to group shared runners setting'
|
||||
:guest | 'denying access to group shared runners setting'
|
||||
:anonymous | 'denying access to group shared runners setting'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
group.send("add_#{user_role}", user) unless user_role == :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
288
spec/initializers/00_rails_disable_joins_spec.rb
Normal file
288
spec/initializers/00_rails_disable_joins_spec.rb
Normal file
|
@ -0,0 +1,288 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'DisableJoins' do
|
||||
let(:primary_model) do
|
||||
Class.new(ApplicationRecord) do
|
||||
self.table_name = '_test_primary_records'
|
||||
|
||||
def self.name
|
||||
'TestPrimary'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:bridge_model) do
|
||||
Class.new(ApplicationRecord) do
|
||||
self.table_name = '_test_bridge_records'
|
||||
|
||||
def self.name
|
||||
'TestBridge'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:secondary_model) do
|
||||
Class.new(ApplicationRecord) do
|
||||
self.table_name = '_test_secondary_records'
|
||||
|
||||
def self.name
|
||||
'TestSecondary'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'passing disable_joins as an association option' do
|
||||
context 'when the association is a bare has_one' do
|
||||
it 'disallows the disable_joins option' do
|
||||
expect do
|
||||
primary_model.has_one :test_bridge, disable_joins: true
|
||||
end.to raise_error(ArgumentError, /Unknown key: :disable_joins/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the association is a belongs_to' do
|
||||
it 'disallows the disable_joins option' do
|
||||
expect do
|
||||
bridge_model.belongs_to :test_secondary, disable_joins: true
|
||||
end.to raise_error(ArgumentError, /Unknown key: :disable_joins/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the association is has_one :through' do
|
||||
it 'allows the disable_joins option' do
|
||||
primary_model.has_one :test_bridge
|
||||
bridge_model.belongs_to :test_secondary
|
||||
|
||||
expect do
|
||||
primary_model.has_one :test_secondary, through: :test_bridge, disable_joins: true
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the association is a bare has_many' do
|
||||
it 'disallows the disable_joins option' do
|
||||
expect do
|
||||
primary_model.has_many :test_bridges, disable_joins: true
|
||||
end.to raise_error(ArgumentError, /Unknown key: :disable_joins/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the association is a has_many :through' do
|
||||
it 'allows the disable_joins option' do
|
||||
primary_model.has_many :test_bridges
|
||||
bridge_model.belongs_to :test_secondary
|
||||
|
||||
expect do
|
||||
primary_model.has_many :test_secondaries, through: :test_bridges, disable_joins: true
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'querying has_one :through when disable_joins is set' do
|
||||
before do
|
||||
create_tables(<<~SQL)
|
||||
CREATE TABLE _test_primary_records (
|
||||
id serial NOT NULL PRIMARY KEY);
|
||||
|
||||
CREATE TABLE _test_bridge_records (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
primary_record_id int NOT NULL,
|
||||
secondary_record_id int NOT NULL);
|
||||
|
||||
CREATE TABLE _test_secondary_records (
|
||||
id serial NOT NULL PRIMARY KEY);
|
||||
SQL
|
||||
|
||||
primary_model.has_one :test_bridge, anonymous_class: bridge_model, foreign_key: :primary_record_id
|
||||
bridge_model.belongs_to :test_secondary, anonymous_class: secondary_model, foreign_key: :secondary_record_id
|
||||
primary_model.has_one :test_secondary, through: :test_bridge, anonymous_class: secondary_model,
|
||||
disable_joins: -> { joins_disabled_flag }
|
||||
|
||||
primary_record = primary_model.create!
|
||||
secondary_record = secondary_model.create!
|
||||
bridge_model.create!(primary_record_id: primary_record.id, secondary_record_id: secondary_record.id)
|
||||
end
|
||||
|
||||
context 'when disable_joins evaluates to true' do
|
||||
let(:joins_disabled_flag) { true }
|
||||
|
||||
it 'executes separate queries' do
|
||||
primary_record = primary_model.first
|
||||
|
||||
query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondary }.count
|
||||
|
||||
expect(query_count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disable_joins evalutes to false' do
|
||||
let(:joins_disabled_flag) { false }
|
||||
|
||||
it 'executes a single query' do
|
||||
primary_record = primary_model.first
|
||||
|
||||
query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondary }.count
|
||||
|
||||
expect(query_count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'querying has_many :through when disable_joins is set' do
|
||||
before do
|
||||
create_tables(<<~SQL)
|
||||
CREATE TABLE _test_primary_records (
|
||||
id serial NOT NULL PRIMARY KEY);
|
||||
|
||||
CREATE TABLE _test_bridge_records (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
primary_record_id int NOT NULL);
|
||||
|
||||
CREATE TABLE _test_secondary_records (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
bridge_record_id int NOT NULL);
|
||||
SQL
|
||||
|
||||
primary_model.has_many :test_bridges, anonymous_class: bridge_model, foreign_key: :primary_record_id
|
||||
bridge_model.has_many :test_secondaries, anonymous_class: secondary_model, foreign_key: :bridge_record_id
|
||||
primary_model.has_many :test_secondaries, through: :test_bridges, anonymous_class: secondary_model,
|
||||
disable_joins: -> { disabled_join_flag }
|
||||
|
||||
primary_record = primary_model.create!
|
||||
bridge_record = bridge_model.create!(primary_record_id: primary_record.id)
|
||||
secondary_model.create!(bridge_record_id: bridge_record.id)
|
||||
end
|
||||
|
||||
context 'when disable_joins evaluates to true' do
|
||||
let(:disabled_join_flag) { true }
|
||||
|
||||
it 'executes separate queries' do
|
||||
primary_record = primary_model.first
|
||||
|
||||
query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondaries.first }.count
|
||||
|
||||
expect(query_count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disable_joins evalutes to false' do
|
||||
let(:disabled_join_flag) { false }
|
||||
|
||||
it 'executes a single query' do
|
||||
primary_record = primary_model.first
|
||||
|
||||
query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondaries.first }.count
|
||||
|
||||
expect(query_count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'querying STI relationships' do
|
||||
let(:child_bridge_model) do
|
||||
Class.new(bridge_model) do
|
||||
def self.name
|
||||
'ChildBridge'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:child_secondary_model) do
|
||||
Class.new(secondary_model) do
|
||||
def self.name
|
||||
'ChildSecondary'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
create_tables(<<~SQL)
|
||||
CREATE TABLE _test_primary_records (
|
||||
id serial NOT NULL PRIMARY KEY);
|
||||
|
||||
CREATE TABLE _test_bridge_records (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
primary_record_id int NOT NULL,
|
||||
type text);
|
||||
|
||||
CREATE TABLE _test_secondary_records (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
bridge_record_id int NOT NULL,
|
||||
type text);
|
||||
SQL
|
||||
|
||||
primary_model.has_many :child_bridges, anonymous_class: child_bridge_model, foreign_key: :primary_record_id
|
||||
child_bridge_model.has_one :child_secondary, anonymous_class: child_secondary_model, foreign_key: :bridge_record_id
|
||||
primary_model.has_many :child_secondaries, through: :child_bridges, anonymous_class: child_secondary_model, disable_joins: true
|
||||
|
||||
primary_record = primary_model.create!
|
||||
parent_bridge_record = bridge_model.create!(primary_record_id: primary_record.id)
|
||||
child_bridge_record = child_bridge_model.create!(primary_record_id: primary_record.id)
|
||||
|
||||
secondary_model.create!(bridge_record_id: child_bridge_record.id)
|
||||
child_secondary_model.create!(bridge_record_id: parent_bridge_record.id)
|
||||
child_secondary_model.create!(bridge_record_id: child_bridge_record.id)
|
||||
end
|
||||
|
||||
it 'filters correctly by the STI type across multiple queries' do
|
||||
primary_record = primary_model.first
|
||||
|
||||
query_recorder = ActiveRecord::QueryRecorder.new do
|
||||
expect(primary_record.child_secondaries.count).to eq(1)
|
||||
end
|
||||
|
||||
expect(query_recorder.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'querying polymorphic relationships' do
|
||||
before do
|
||||
create_tables(<<~SQL)
|
||||
CREATE TABLE _test_primary_records (
|
||||
id serial NOT NULL PRIMARY KEY);
|
||||
|
||||
CREATE TABLE _test_bridge_records (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
primaryable_id int NOT NULL,
|
||||
primaryable_type text NOT NULL);
|
||||
|
||||
CREATE TABLE _test_secondary_records (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
bridgeable_id int NOT NULL,
|
||||
bridgeable_type text NOT NULL);
|
||||
SQL
|
||||
|
||||
primary_model.has_many :test_bridges, anonymous_class: bridge_model, foreign_key: :primaryable_id, as: :primaryable
|
||||
bridge_model.has_one :test_secondaries, anonymous_class: secondary_model, foreign_key: :bridgeable_id, as: :bridgeable
|
||||
primary_model.has_many :test_secondaries, through: :test_bridges, anonymous_class: secondary_model, disable_joins: true
|
||||
|
||||
primary_record = primary_model.create!
|
||||
primary_bridge_record = bridge_model.create!(primaryable_id: primary_record.id, primaryable_type: 'TestPrimary')
|
||||
nonprimary_bridge_record = bridge_model.create!(primaryable_id: primary_record.id, primaryable_type: 'NonPrimary')
|
||||
|
||||
secondary_model.create!(bridgeable_id: primary_bridge_record.id, bridgeable_type: 'TestBridge')
|
||||
secondary_model.create!(bridgeable_id: nonprimary_bridge_record.id, bridgeable_type: 'TestBridge')
|
||||
secondary_model.create!(bridgeable_id: primary_bridge_record.id, bridgeable_type: 'NonBridge')
|
||||
end
|
||||
|
||||
it 'filters correctly by the polymorphic type across multiple queries' do
|
||||
primary_record = primary_model.first
|
||||
|
||||
query_recorder = ActiveRecord::QueryRecorder.new do
|
||||
expect(primary_record.test_secondaries.count).to eq(1)
|
||||
end
|
||||
|
||||
expect(query_recorder.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
def create_tables(table_sql)
|
||||
ApplicationRecord.connection.execute(table_sql)
|
||||
|
||||
bridge_model.reset_column_information
|
||||
secondary_model.reset_column_information
|
||||
end
|
||||
end
|
66
spec/requests/api/graphql/mutations/groups/update_spec.rb
Normal file
66
spec/requests/api/graphql/mutations/groups/update_spec.rb
Normal file
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'GroupUpdate' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_reload(:group) { create(:group) }
|
||||
|
||||
let(:variables) do
|
||||
{
|
||||
full_path: group.full_path,
|
||||
shared_runners_setting: 'DISABLED_WITH_OVERRIDE'
|
||||
}
|
||||
end
|
||||
|
||||
let(:mutation) { graphql_mutation(:group_update, variables) }
|
||||
|
||||
context 'when unauthorized' do
|
||||
shared_examples 'unauthorized' do
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_errors).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not a group member' do
|
||||
it_behaves_like 'unauthorized'
|
||||
end
|
||||
|
||||
context 'when a non-admin group member' do
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'unauthorized'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authorized' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'updates shared runners settings' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_errors).to be_nil
|
||||
expect(group.reload.shared_runners_setting).to eq(variables[:shared_runners_setting].downcase)
|
||||
end
|
||||
|
||||
context 'when bad arguments are provided' do
|
||||
let(:variables) { { full_path: '', shared_runners_setting: 'INVALID' } }
|
||||
|
||||
it 'returns the errors' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_errors).not_to be_empty
|
||||
expect(group.reload.shared_runners_setting).to eq('enabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue