Commit 2569cb80 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 680decd0 93374331
# frozen_string_literal: true
class GroupMembersFinder < UnionFinder
RELATIONS = %i(direct inherited descendants).freeze
DEFAULT_RELATIONS = %i(direct inherited).freeze
include CreatedAtFilter
# Params can be any of the following:
......@@ -17,7 +20,7 @@ class GroupMembersFinder < UnionFinder
@params = params
end
def execute(include_relations: [:inherited, :direct])
def execute(include_relations: DEFAULT_RELATIONS)
group_members = group_members_list
relations = []
......
# frozen_string_literal: true
class MembersFinder
RELATIONS = %i(direct inherited descendants invited_groups).freeze
DEFAULT_RELATIONS = %i(direct inherited).freeze
# Params can be any of the following:
# sort: string
# search: string
......@@ -13,7 +16,7 @@ class MembersFinder
@params = params
end
def execute(include_relations: [:inherited, :direct])
def execute(include_relations: DEFAULT_RELATIONS)
members = find_members(include_relations)
filter_members(members)
......@@ -56,7 +59,7 @@ class MembersFinder
def group_union_members(include_relations)
[].tap do |members|
members << direct_group_members(include_relations.include?(:descendants)) if group
members << project_invited_groups_members if include_relations.include?(:invited_groups_members)
members << project_invited_groups if include_relations.include?(:invited_groups)
end
end
......@@ -66,7 +69,7 @@ class MembersFinder
GroupMembersFinder.new(group).execute(include_relations: requested_relations).non_invite.non_minimal_access # rubocop: disable CodeReuse/Finder
end
def project_invited_groups_members
def project_invited_groups
invited_groups_ids_including_ancestors = Gitlab::ObjectHierarchy
.new(project.invited_groups)
.base_and_ancestors
......
......@@ -6,6 +6,11 @@ module Resolvers
authorize :read_group_member
argument :relations, [Types::GroupMemberRelationEnum],
description: 'Filter members by the given member relations',
required: false,
default_value: GroupMembersFinder::DEFAULT_RELATIONS
private
def preloads
......
......@@ -14,7 +14,9 @@ module Resolvers
def resolve_with_lookahead(**args)
authorize!(object)
apply_lookahead(finder_class.new(object, current_user, params: args).execute)
relations = args.delete(:relations)
apply_lookahead(finder_class.new(object, current_user, params: args).execute(include_relations: relations))
end
private
......
......@@ -5,6 +5,11 @@ module Resolvers
class ProjectMembersResolver < MembersResolver
authorize :read_project_member
argument :relations, [Types::ProjectMemberRelationEnum],
description: 'Filter members by the given member relations',
required: false,
default_value: MembersFinder::DEFAULT_RELATIONS
private
def finder_class
......
# frozen_string_literal: true
module Types
class GroupMemberRelationEnum < BaseEnum
graphql_name 'GroupMemberRelation'
description 'Group member relation'
::GroupMembersFinder::RELATIONS.each do |member_relation|
value member_relation.to_s.upcase, value: member_relation, description: "#{member_relation.to_s.titleize} members"
end
end
end
# frozen_string_literal: true
module Types
class ProjectMemberRelationEnum < BaseEnum
graphql_name 'ProjectMemberRelation'
description 'Project member relation'
::MembersFinder::RELATIONS.each do |member_relation|
value member_relation.to_s.upcase, value: member_relation, description: "#{member_relation.to_s.titleize} members"
end
end
end
......@@ -263,3 +263,5 @@ module AlertManagement
end
end
end
AlertManagement::Alert.prepend_if_ee('EE::AlertManagement::Alert')
---
title: Allow filtering project and group members by relationship in GraphQL
merge_request: 48372
author:
type: changed
......@@ -9103,6 +9103,11 @@ type Group {
"""
last: Int
"""
Filter members by the given member relations
"""
relations: [GroupMemberRelation!] = [DIRECT, INHERITED]
"""
Search query
"""
......@@ -10007,6 +10012,26 @@ type GroupMemberEdge {
node: GroupMember
}
"""
Group member relation
"""
enum GroupMemberRelation {
"""
Descendants members
"""
DESCENDANTS
"""
Direct members
"""
DIRECT
"""
Inherited members
"""
INHERITED
}
type GroupPermissions {
"""
Indicates the user can perform `read_group` on this resource
......@@ -16625,6 +16650,11 @@ type Project {
"""
last: Int
"""
Filter members by the given member relations
"""
relations: [ProjectMemberRelation!] = [DIRECT, INHERITED]
"""
Search query
"""
......@@ -17262,6 +17292,31 @@ type ProjectMemberEdge {
node: ProjectMember
}
"""
Project member relation
"""
enum ProjectMemberRelation {
"""
Descendants members
"""
DESCENDANTS
"""
Direct members
"""
DIRECT
"""
Inherited members
"""
INHERITED
"""
Invited Groups members
"""
INVITED_GROUPS
}
type ProjectPermissions {
"""
Indicates the user can perform `admin_operations` on this resource
......
......@@ -25128,6 +25128,24 @@
},
"defaultValue": null
},
{
"name": "relations",
"description": "Filter members by the given member relations",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "GroupMemberRelation",
"ofType": null
}
}
},
"defaultValue": "[DIRECT, INHERITED]"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -27424,6 +27442,35 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "GroupMemberRelation",
"description": "Group member relation",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "DIRECT",
"description": "Direct members",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INHERITED",
"description": "Inherited members",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DESCENDANTS",
"description": "Descendants members",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GroupPermissions",
......@@ -48635,6 +48682,24 @@
},
"defaultValue": null
},
{
"name": "relations",
"description": "Filter members by the given member relations",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ProjectMemberRelation",
"ofType": null
}
}
},
"defaultValue": "[DIRECT, INHERITED]"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -50363,6 +50428,41 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ProjectMemberRelation",
"description": "Project member relation",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "DIRECT",
"description": "Direct members",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INHERITED",
"description": "Inherited members",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DESCENDANTS",
"description": "Descendants members",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INVITED_GROUPS",
"description": "Invited Groups members",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ProjectPermissions",
......@@ -4078,6 +4078,16 @@ Epic ID wildcard values.
| `ANY` | Any epic is assigned |
| `NONE` | No epic is assigned |
### GroupMemberRelation
Group member relation.
| Value | Description |
| ----- | ----------- |
| `DESCENDANTS` | Descendants members |
| `DIRECT` | Direct members |
| `INHERITED` | Inherited members |
### HealthStatus
Health status of an issue or epic.
......@@ -4366,6 +4376,17 @@ Values for sorting projects.
| `SUCCESS` | |
| `WAITING_FOR_RESOURCE` | |
### ProjectMemberRelation
Project member relation.
| Value | Description |
| ----- | ----------- |
| `DESCENDANTS` | Descendants members |
| `DIRECT` | Direct members |
| `INHERITED` | Inherited members |
| `INVITED_GROUPS` | Invited Groups members |
### RegistryState
State of a Geo registry.
......
......@@ -894,6 +894,32 @@ longer visible on the environment page.
If the alert requires a [rollback](#retrying-and-rolling-back), you can select the
deployment tab from the environment page and select which deployment to roll back to.
#### Auto Rollback **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35404) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.7.
In a typical Continuous Deployment workflow, the CI pipeline tests every commit before deploying to
production. However, problematic code can still make it to production. For example, inefficient code
that is logically correct can pass tests even though it causes severe performance degradation.
Operators and SREs monitor the system to catch such problems as soon as possible. If they find a
problematic deployment, they can roll back to a previous stable version.
GitLab Auto Rollback eases this workflow by automatically triggering a rollback when a
[critical alert](../../operations/incident_management/alerts.md)
is detected. GitLab selects and redeploys the most recent successful deployment.
Limitations of GitLab Auto Rollback:
- The rollback is skipped if a deployment is running when the alert is detected.
- A rollback can happen only once in three minutes. If multiple alerts are detected at once, only
one rollback is performed.
GitLab Auto Rollback is turned off by default. To turn it on:
1. Visit **Project > Settings > CI/CD > Automatic deployment rollbacks**.
1. Select the checkbox for **Enable automatic rollbacks**.
1. Click **Save changes**.
### Monitoring environments
If you have enabled [Prometheus for monitoring system and response metrics](../../user/project/integrations/prometheus.md),
......
......@@ -115,7 +115,7 @@ Add the following to your `.gitlab-ci.yml` file:
```yaml
include:
- template: Secret-Detection.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
```
The included template creates Secret Detection jobs in your CI/CD pipeline and scans
......@@ -153,7 +153,7 @@ override the `secret_detection` job with the `SECRET_DETECTION_HISTORIC_SCAN` va
```yaml
include:
- template: Secret-Detection.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
secret_detection:
variables:
......
# frozen_string_literal: true
module EE
module AlertManagement
module Alert
extend ActiveSupport::Concern
prepended do
include AfterCommitQueue
after_create do |alert|
run_after_commit { alert.trigger_auto_rollback }
end
end
def trigger_auto_rollback
return unless triggered? && critical? && environment&.auto_rollback_enabled?
::Deployments::AutoRollbackWorker.perform_async(environment.id)
end
end
end
end
......@@ -23,7 +23,6 @@
= s_('AutoRollback|Enable automatic rollbacks')
%small.form-text.text-gl-muted
= s_('AutoRollback|Automatic rollbacks start when a critical alert is triggered. If the last successful deployment fails to roll back automatically, it can still be done manually.')
-# This will be added once the documentation page has been created
-# = link_to _('More information'), help_page_path('topics/auto_rollback/index.md'), target: '_blank'
= link_to _('More information'), help_page_path('ci/environments/index.md', anchor: 'auto-rollback'), target: '_blank'
= f.submit _('Save changes'), class: "gl-button btn btn-success gl-mt-5", data: { qa_selector: 'save_changes_button' }
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::AlertManagement::Alert do
let_it_be(:project, refind: true) { create(:project) }
let_it_be(:environment, refind: true) { create(:environment, project: project) }
describe 'after_create' do
it 'attempts to trigger auto rollback' do
alert = build(:alert_management_alert, :triggered, :critical)
expect(alert).to receive(:trigger_auto_rollback)
alert.save!
end
end
describe '#trigger_auto_rollback' do
subject { alert.trigger_auto_rollback }
let!(:alert) { create(:alert_management_alert, :triggered, :critical, project: project, environment: environment) }
before do
stub_licensed_features(auto_rollback: true)
environment.project.auto_rollback_enabled = true
end
it 'executes AutoRollbackWorker' do
expect(Deployments::AutoRollbackWorker).to receive(:perform_async).with(environment.id)
subject
end
context 'when status is not triggered' do
let!(:alert) { create(:alert_management_alert, :acknowledged, :critical, project: project, environment: environment) }
it 'does not execute AutoRollbackWorker' do
expect(Deployments::AutoRollbackWorker).not_to receive(:perform_async)
subject
end
end
context 'when severity is not critical' do
let!(:alert) { create(:alert_management_alert, :triggered, :high, project: project, environment: environment) }
it 'does not execute AutoRollbackWorker' do
expect(Deployments::AutoRollbackWorker).not_to receive(:perform_async)
subject
end
end
context 'when project does not enable auto rollback' do
before do
environment.project.auto_rollback_enabled = false
end
it 'does not execute AutoRollbackWorker' do
expect(Deployments::AutoRollbackWorker).not_to receive(:perform_async)
subject
end
end
context 'when project does not have a license for auto rollback' do
before do
stub_licensed_features(auto_rollback: false)
end
it 'does not execute AutoRollbackWorker' do
expect(Deployments::AutoRollbackWorker).not_to receive(:perform_async)
subject
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(cd_auto_rollback: false)
end
it 'does not execute AutoRollbackWorker' do
expect(Deployments::AutoRollbackWorker).not_to receive(:perform_async)
subject
end
end
end
end
......@@ -45,7 +45,7 @@ module API
end
def find_all_members_for_project(project)
MembersFinder.new(project, current_user).execute(include_relations: [:inherited, :direct, :invited_groups_members])
MembersFinder.new(project, current_user).execute(include_relations: [:inherited, :direct, :invited_groups])
end
def find_all_members_for_group(group)
......
......@@ -160,8 +160,8 @@ RSpec.describe MembersFinder, '#execute' do
expect(result).to eq([member3, member2, member1])
end
context 'when include_invited_groups_members == true' do
subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups_members]) }
context 'when :invited_groups is passed' do
subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups]) }
let_it_be(:linked_group) { create(:group, :public) }
let_it_be(:nested_linked_group) { create(:group, parent: linked_group) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::GroupMemberRelationEnum do
specify { expect(described_class.graphql_name).to eq('GroupMemberRelation') }
it 'exposes all the existing group member relation type values' do
expect(described_class.values.keys).to contain_exactly('DIRECT', 'INHERITED', 'DESCENDANTS')
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::ProjectMemberRelationEnum do
specify { expect(described_class.graphql_name).to eq('ProjectMemberRelation') }
it 'exposes all the existing project member relation type values' do
expect(described_class.values.keys).to contain_exactly('DIRECT', 'INHERITED', 'DESCENDANTS', 'INVITED_GROUPS')
end
end
......@@ -99,7 +99,8 @@ RSpec.describe AlertManagement::Alert do
describe 'fingerprint' do
let_it_be(:fingerprint) { 'fingerprint' }
let(:new_alert) { build(:alert_management_alert, fingerprint: fingerprint, project: project) }
let_it_be(:project3, refind: true) { create(:project) }
let(:new_alert) { build(:alert_management_alert, fingerprint: fingerprint, project: project3) }
subject { new_alert }
......@@ -107,7 +108,7 @@ RSpec.describe AlertManagement::Alert do
context 'same project, various states' do
using RSpec::Parameterized::TableSyntax
let_it_be(:existing_alert) { create(:alert_management_alert, fingerprint: fingerprint, project: project) }
let_it_be(:existing_alert, refind: true) { create(:alert_management_alert, fingerprint: fingerprint, project: project3) }
# We are only validating uniqueness for non-resolved alerts
where(:existing_status, :new_status, :valid) do
......@@ -130,7 +131,7 @@ RSpec.describe AlertManagement::Alert do
end
with_them do
let(:new_alert) { build(:alert_management_alert, new_status, fingerprint: fingerprint, project: project) }
let(:new_alert) { build(:alert_management_alert, new_status, fingerprint: fingerprint, project: project3) }
before do
existing_alert.change_status_to(existing_status)
......
......@@ -5,44 +5,95 @@ require 'spec_helper'
RSpec.describe 'getting group members information' do
include GraphqlHelpers
let_it_be(:group) { create(:group, :public) }
let_it_be(:parent_group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:user_1) { create(:user, username: 'user') }
let_it_be(:user_2) { create(:user, username: 'test') }
let(:member_data) { graphql_data['group']['groupMembers']['edges'] }
before do
[user_1, user_2].each { |user| group.add_guest(user) }
before_all do
[user_1, user_2].each { |user| parent_group.add_guest(user) }
end
context 'when the request is correct' do
it_behaves_like 'a working graphql query' do
before do
fetch_members(user)
before_all do
fetch_members
end
end
it 'returns group members successfully' do
fetch_members(user)
fetch_members
expect(graphql_errors).to be_nil
expect_array_response(user_1.to_global_id.to_s, user_2.to_global_id.to_s)
expect_array_response(user_1, user_2)
end
it 'returns members that match the search query' do
fetch_members(user, { search: 'test' })
fetch_members(args: { search: 'test' })
expect(graphql_errors).to be_nil
expect_array_response(user_2.to_global_id.to_s)
expect_array_response(user_2)
end
end
def fetch_members(user = nil, args = {})
post_graphql(members_query(args), current_user: user)
context 'member relations' do
let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) }
let_it_be(:child_user) { create(:user) }
let_it_be(:grandchild_user) { create(:user) }
before_all do
child_group.add_guest(child_user)
grandchild_group.add_guest(grandchild_user)
end
it 'returns direct members' do
fetch_members(group: child_group, args: { relations: [:DIRECT] })
expect(graphql_errors).to be_nil
expect_array_response(child_user)
end
it 'returns direct and inherited members' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] })
expect(graphql_errors).to be_nil
expect_array_response(child_user, user_1, user_2)
end
it 'returns direct, inherited, and descendant members' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS] })
expect(graphql_errors).to be_nil
expect_array_response(child_user, user_1, user_2, grandchild_user)
end
it 'returns an error for an invalid member relation' do
fetch_members(group: child_group, args: { relations: [:OBLIQUE] })
expect(graphql_errors.first)
.to include('path' => %w[query group groupMembers relations],
'message' => a_string_including('invalid value ([OBLIQUE])'))
end
end
context 'when unauthenticated' do
it 'returns nothing' do
fetch_members(current_user: nil)
expect(graphql_errors).to be_nil
expect(response).to have_gitlab_http_status(:success)
expect(member_data).to be_empty
end
end
def fetch_members(group: parent_group, current_user: user, args: {})
post_graphql(members_query(group.full_path, args), current_user: current_user)
end
def members_query(args = {})
def members_query(group_path, args = {})
members_node = <<~NODE
edges {
node {
......@@ -54,7 +105,7 @@ RSpec.describe 'getting group members information' do
NODE
graphql_query_for("group",
{ full_path: group.full_path },
{ full_path: group_path },
[query_graphql_field("groupMembers", args, members_node)]
)
end
......@@ -62,6 +113,7 @@ RSpec.describe 'getting group members information' do
def expect_array_response(*items)
expect(response).to have_gitlab_http_status(:success)
expect(member_data).to be_an Array
expect(member_data.map { |node| node["node"]["user"]["id"] }).to match_array(items)
expect(member_data.map { |node| node["node"]["user"]["id"] })
.to match_array(items.map { |u| global_id_of(u) })
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting project members information' do
include GraphqlHelpers
let_it_be(:parent_group) { create(:group, :public) }
let_it_be(:parent_project) { create(:project, :public, group: parent_group) }
let_it_be(:user) { create(:user) }
let_it_be(:user_1) { create(:user, username: 'user') }
let_it_be(:user_2) { create(:user, username: 'test') }
let(:member_data) { graphql_data['project']['projectMembers']['edges'] }
before_all do
[user_1, user_2].each { |user| parent_group.add_guest(user) }
end
context 'when the request is correct' do
it_behaves_like 'a working graphql query' do
before_all do
fetch_members(project: parent_project)
end
end
it 'returns project members successfully' do
fetch_members(project: parent_project)
expect(graphql_errors).to be_nil
expect_array_response(user_1, user_2)
end
it 'returns members that match the search query' do
fetch_members(project: parent_project, args: { search: 'test' })
expect(graphql_errors).to be_nil
expect_array_response(user_2)
end
end
context 'member relations' do
let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
let_it_be(:child_project) { create(:project, :public, group: child_group) }
let_it_be(:invited_group) { create(:group, :public) }
let_it_be(:child_user) { create(:user) }
let_it_be(:invited_user) { create(:user) }
let_it_be(:group_link) { create(:project_group_link, project: child_project, group: invited_group) }
before_all do
child_project.add_guest(child_user)
invited_group.add_guest(invited_user)
end
it 'returns direct members' do
fetch_members(project: child_project, args: { relations: [:DIRECT] })
expect(graphql_errors).to be_nil
expect_array_response(child_user)
end
it 'returns invited members plus inherited members' do
fetch_members(project: child_project, args: { relations: [:INVITED_GROUPS] })
expect(graphql_errors).to be_nil
expect_array_response(invited_user, user_1, user_2)
end
it 'returns direct, inherited, descendant, and invited members' do
fetch_members(project: child_project, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS, :INVITED_GROUPS] })
expect(graphql_errors).to be_nil
expect_array_response(child_user, user_1, user_2, invited_user)
end
it 'returns an error for an invalid member relation' do
fetch_members(project: child_project, args: { relations: [:OBLIQUE] })
expect(graphql_errors.first)
.to include('path' => %w[query project projectMembers relations],
'message' => a_string_including('invalid value ([OBLIQUE])'))
end
end
context 'when unauthenticated' do
it 'returns members' do
fetch_members(current_user: nil, project: parent_project)
expect(graphql_errors).to be_nil
expect_array_response(user_1, user_2)
end
end
def fetch_members(project:, current_user: user, args: {})
post_graphql(members_query(project.full_path, args), current_user: current_user)
end
def members_query(group_path, args = {})
members_node = <<~NODE
edges {
node {
user {
id
}
}
}
NODE
graphql_query_for('project',
{ full_path: group_path },
[query_graphql_field('projectMembers', args, members_node)]
)
end
def expect_array_response(*items)
expect(response).to have_gitlab_http_status(:success)
expect(member_data).to be_an Array
expect(member_data.map { |node| node['node']['user']['id'] })
.to match_array(items.map { |u| global_id_of(u) })
end
end
......@@ -36,9 +36,10 @@ RSpec.shared_examples 'querying members with a group' do
let_it_be(:group_2_member) { create(:group_member, user: user_3, group: group_2) }
let(:args) { {} }
let(:base_args) { { relations: described_class.arguments['relations'].default_value } }
subject do
resolve(described_class, obj: resource, args: args, ctx: { current_user: user_4 })
resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: user_4 })
end
describe '#resolve' do
......@@ -72,7 +73,7 @@ RSpec.shared_examples 'querying members with a group' do
let_it_be(:other_user) { create(:user) }
subject do
resolve(described_class, obj: resource, args: args, ctx: { current_user: other_user })
resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: other_user })
end
it 'raises an error' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment