Commit 8dc67118 authored by Yorick Peterse's avatar Yorick Peterse

Merge canonical into security master

parents f65f0914 593568df
......@@ -395,8 +395,6 @@ class IssuableFinder
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
if not_params.assignees.present?
items.not_assigned_to(not_params.assignees)
elsif not_params.assignee_id? || not_params.assignee_username? # assignee not found
items.none
else
items
end
......
......@@ -12,10 +12,10 @@ module IssueResolverArguments
argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'List of IIDs of issues. For example, [1, 2].'
argument :label_name, GraphQL::STRING_TYPE.to_list_type,
argument :label_name, [GraphQL::STRING_TYPE, null: true],
required: false,
description: 'Labels applied to this issue.'
argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
argument :milestone_title, [GraphQL::STRING_TYPE, null: true],
required: false,
description: 'Milestone applied to this issue.'
argument :author_username, GraphQL::STRING_TYPE,
......@@ -55,6 +55,10 @@ module IssueResolverArguments
as: :issue_types,
description: 'Filter issues by the given issue types.',
required: false
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'List of negated params.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
required: false
end
def resolve_with_lookahead(**args)
......@@ -69,11 +73,22 @@ module IssueResolverArguments
args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
args[:attempt_project_search_optimizations] = true if args[:search].present?
prepare_assignee_username_params(args)
finder = IssuesFinder.new(current_user, args)
continue_issue_resolve(parent, finder, **args)
end
def ready?(**args)
if args.slice(*mutually_exclusive_assignee_username_args).compact.size > 1
arg_str = mutually_exclusive_assignee_username_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
raise Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time."
end
super
end
class_methods do
def resolver_complexity(args, child_complexity:)
complexity = super
......@@ -82,4 +97,15 @@ module IssueResolverArguments
complexity
end
end
private
def prepare_assignee_username_params(args)
args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present?
args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present?
end
def mutually_exclusive_assignee_username_args
[:assignee_usernames, :assignee_username]
end
end
......@@ -4,7 +4,7 @@ module Types
module Boards
# Common arguments that we can be used to filter boards epics and issues
class BoardIssuableInputBaseType < BaseInputObject
argument :label_name, GraphQL::STRING_TYPE.to_list_type,
argument :label_name, [GraphQL::STRING_TYPE, null: true],
required: false,
description: 'Filter by label name.'
......
......@@ -8,7 +8,7 @@ module Types
required: false,
description: 'Filter by milestone title.'
argument :assignee_username, GraphQL::STRING_TYPE.to_list_type,
argument :assignee_username, [GraphQL::STRING_TYPE, null: true],
required: false,
description: 'Filter by assignee username.'
......
# frozen_string_literal: true
module Types
module Issues
class NegatedIssueFilterInputType < BaseInputObject
graphql_name 'NegatedIssueFilterInput'
argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'List of IIDs of issues to exclude. For example, [1, 2].'
argument :label_name, [GraphQL::STRING_TYPE],
required: false,
description: 'Labels not applied to this issue.'
argument :milestone_title, [GraphQL::STRING_TYPE],
required: false,
description: 'Milestone not applied to this issue.'
argument :assignee_usernames, [GraphQL::STRING_TYPE],
required: false,
description: 'Usernames of users not assigned to the issue.'
argument :assignee_id, GraphQL::STRING_TYPE,
required: false,
description: 'ID of a user not assigned to the issues.'
end
end
end
Types::Issues::NegatedIssueFilterInputType.prepend_if_ee('::EE::Types::Issues::NegatedIssueFilterInputType')
......@@ -52,8 +52,6 @@ module Pages
return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project, default_enabled: :yaml)
return if deployment.migrated? && !Feature.enabled?(:pages_serve_from_migrated_zip, project, default_enabled: true)
global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
{
......
......@@ -22,29 +22,29 @@
= f.label :scopes, _('Scopes [Select 1 or more]'), class: 'label-bold'
%fieldset.form-group.form-check
= f.check_box :read_repository, class: 'form-check-input qa-deploy-token-read-repository'
= label_tag ("deploy_token_read_repository"), 'read_repository', class: 'label-bold form-check-label'
= f.label :read_repository, 'read_repository', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read-only access to the repository.')
- if container_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
= f.check_box :read_registry, class: 'form-check-input qa-deploy-token-read-registry'
= label_tag ("deploy_token_read_registry"), 'read_registry', class: 'label-bold form-check-label'
= f.label :read_registry, 'read_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read-only access to registry images.')
%fieldset.form-group.form-check
= f.check_box :write_registry, class: 'form-check-input'
= label_tag ("deploy_token_write_registry"), 'write_registry', class: 'label-bold form-check-label'
= f.label :write_registry, 'write_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows write access to registry images.')
- if packages_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
= f.check_box :read_package_registry, class: 'form-check-input'
= label_tag ("deploy_token_read_package_registry"), 'read_package_registry', class: 'label-bold form-check-label'
= f.label :read_package_registry, 'read_package_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read access to the package registry.')
%fieldset.form-group.form-check
= f.check_box :write_package_registry, class: 'form-check-input'
= label_tag ("deploy_token_write_package_registry"), 'write_package_registry', class: 'label-bold form-check-label'
= f.label :write_package_registry, 'write_package_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows write access to the package registry.')
.gl-mt-3
......
---
title: Remove pages_serve_from_migrated_zip feature flag
merge_request: 59002
author:
type: added
---
title: Support negated filtering of issues by iids, label_name, milestone_title, assignee_usernames and assignee_id in GraphQL
merge_request: 58154
author:
type: added
---
name: pages_serve_from_migrated_zip
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52573
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300021
milestone: '13.9'
type: development
group: group::release
default_enabled: true
......@@ -5,16 +5,16 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: howto
---
# Docker Registry for a secondary node **(PREMIUM SELF)**
# Docker Registry for a secondary site **(PREMIUM SELF)**
You can set up a [Docker Registry](https://docs.docker.com/registry/) on your
**secondary** Geo node that mirrors the one on the **primary** Geo node.
**secondary** Geo site that mirrors the one on the **primary** Geo site.
## Storage support
Docker Registry currently supports a few types of storage. If you choose a
distributed storage (`azure`, `gcs`, `s3`, `swift`, or `oss`) for your Docker
Registry on the **primary** node, you can use the same storage for a **secondary**
Registry on the **primary** site, you can use the same storage for a **secondary**
Docker Registry as well. For more information, read the
[Load balancing considerations](https://docs.docker.com/registry/deploying/#load-balancing-considerations)
when deploying the Registry, and how to set up the storage driver for the GitLab
......@@ -24,22 +24,22 @@ integrated [Container Registry](../../packages/container_registry.md#use-object-
You can enable a storage-agnostic replication so it
can be used for cloud or local storage. Whenever a new image is pushed to the
**primary** node, each **secondary** node will pull it to its own container
**primary** site, each **secondary** site will pull it to its own container
repository.
To configure Docker Registry replication:
1. Configure the [**primary** node](#configure-primary-node).
1. Configure the [**secondary** node](#configure-secondary-node).
1. Configure the [**primary** site](#configure-primary-site).
1. Configure the [**secondary** site](#configure-secondary-site).
1. Verify Docker Registry [replication](#verify-replication).
### Configure **primary** node
### Configure **primary** site
Make sure that you have Container Registry set up and working on
the **primary** node before following the next steps.
the **primary** site before following the next steps.
We need to make Docker Registry send notification events to the
**primary** node.
**primary** site.
1. SSH into your GitLab **primary** server and login as root:
......@@ -85,27 +85,29 @@ We need to make Docker Registry send notification events to the
gitlab-ctl reconfigure
```
### Configure **secondary** node
### Configure **secondary** site
Make sure you have Container Registry set up and working on
the **secondary** node before following the next steps.
the **secondary** site before following the next steps.
The following steps should be done on each **secondary** node you're
The following steps should be done on each **secondary** site you're
expecting to see the Docker images replicated.
Because we need to allow the **secondary** node to communicate securely with
the **primary** node Container Registry, we need to have a single key
pair for all the nodes. The **secondary** node will use this key to
Because we need to allow the **secondary** site to communicate securely with
the **primary** site Container Registry, we need to have a single key
pair for all the sites. The **secondary** site will use this key to
generate a short-lived JWT that is pull-only-capable to access the
**primary** node Container Registry.
**primary** site Container Registry.
1. SSH into the **secondary** node and login as the `root` user:
For each application node on the **secondary** site:
1. SSH into the node and login as the `root` user:
```shell
sudo -i
```
1. Copy `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` from the **primary** to the **secondary** node.
1. Copy `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` from the **primary** to the node.
1. Edit `/etc/gitlab/gitlab.rb`:
......@@ -114,7 +116,7 @@ generate a short-lived JWT that is pull-only-capable to access the
gitlab_rails['geo_registry_replication_primary_api_url'] = 'https://primary.example.com:5050/' # Primary registry address, it will be used by the secondary node to directly communicate to primary registry
```
1. Reconfigure the **secondary** node for the change to take effect:
1. Reconfigure the node for the change to take effect:
```shell
gitlab-ctl reconfigure
......@@ -123,6 +125,6 @@ generate a short-lived JWT that is pull-only-capable to access the
### Verify replication
To verify Container Registry replication is working, go to **Admin Area > Geo**
(`/admin/geo/nodes`) on the **secondary** node.
(`/admin/geo/nodes`) on the **secondary** site.
The initial replication, or "backfill", will probably still be in progress.
You can monitor the synchronization process on each Geo node from the **primary** node's **Geo Nodes** dashboard in your browser.
You can monitor the synchronization process on each Geo site from the **primary** site's **Geo Nodes** dashboard in your browser.
......@@ -19,7 +19,7 @@ module EE
end
def weights?
params[:weight].present? && params[:weight] != ::Issue::WEIGHT_ALL
params[:weight].present? && params[:weight].to_s.casecmp(::Issue::WEIGHT_ALL) != 0
end
def filter_by_no_weight?
......
......@@ -7,13 +7,15 @@ module EE
extend ::Gitlab::Utils::Override
prepended do
argument :iteration_id, ::GraphQL::ID_TYPE.to_list_type,
argument :iteration_id, [::GraphQL::ID_TYPE, null: true],
required: false,
description: 'Iterations applied to the issue.'
argument :epic_id, GraphQL::STRING_TYPE,
required: false,
description: 'ID of an epic associated with the issues, "none" and "any" values are supported.'
argument :weight, GraphQL::STRING_TYPE,
required: false,
description: 'Weight applied to the issue, "none" and "any" values are supported.'
end
private
......
# frozen_string_literal: true
module EE
module Types
module Issues
module NegatedIssueFilterInputType
extend ActiveSupport::Concern
prepended do
argument :epic_id, GraphQL::STRING_TYPE,
required: false,
description: 'ID of an epic not associated with the issues.'
argument :weight, GraphQL::STRING_TYPE,
required: false,
description: 'Weight not applied to the issue.'
end
end
end
end
end
---
title: Support negated filtering of issues by epic_id and weight in GraphQL
merge_request: 58154
author:
type: added
......@@ -11,36 +11,39 @@ RSpec.describe Resolvers::IssuesResolver do
context "with a project" do
describe '#resolve' do
let_it_be(:epic1) { create :epic, group: group }
let_it_be(:epic2) { create :epic, group: group }
let_it_be(:iteration1) { create(:iteration, group: group, start_date: 2.weeks.ago, due_date: 1.week.ago) }
let_it_be(:current_iteration) { create(:iteration, :started, group: group, start_date: Date.today, due_date: 1.day.from_now) }
let_it_be(:issue1) { create :issue, project: project, epic: epic1, iteration: iteration1 }
let_it_be(:issue2) { create :issue, project: project, epic: epic2, weight: 1 }
let_it_be(:issue3) { create :issue, project: project, weight: 3, iteration: current_iteration }
let_it_be(:issue4) { create :issue, :published, project: project }
before do
project.add_developer(current_user)
end
describe 'sorting' do
context 'when sorting by weight' do
let_it_be(:weight_issue1) { create(:issue, project: project, weight: 5) }
let_it_be(:weight_issue2) { create(:issue, project: project, weight: nil) }
let_it_be(:weight_issue3) { create(:issue, project: project, weight: 1) }
let_it_be(:weight_issue4) { create(:issue, project: project, weight: nil) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :weight_asc).to_a).to eq [weight_issue3, weight_issue1, weight_issue4, weight_issue2]
expect(resolve_issues(sort: :weight_asc).to_a).to eq [issue2, issue3, issue4, issue1]
end
it 'sorts issues descending' do
expect(resolve_issues(sort: :weight_desc).to_a).to eq [weight_issue1, weight_issue3, weight_issue4, weight_issue2]
expect(resolve_issues(sort: :weight_desc).to_a).to eq [issue3, issue2, issue4, issue1]
end
end
context 'when sorting by published' do
let_it_be(:not_published) { create(:issue, project: project) }
let_it_be(:published) { create(:issue, :published, project: project) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :published_asc).to_a).to eq [not_published, published]
expect(resolve_issues(sort: :published_asc).to_a).to eq [issue3, issue2, issue1, issue4]
end
it 'sorts issues descending' do
expect(resolve_issues(sort: :published_desc).to_a).to eq [published, not_published]
expect(resolve_issues(sort: :published_desc).to_a).to eq [issue4, issue3, issue2, issue1]
end
end
......@@ -64,32 +67,56 @@ RSpec.describe Resolvers::IssuesResolver do
end
describe 'filtering by iteration' do
let_it_be(:iteration1) { create(:iteration, group: group) }
let_it_be(:issue_with_iteration) { create(:issue, project: project, iteration: iteration1) }
let_it_be(:issue_without_iteration) { create(:issue, project: project) }
it 'returns issues with iteration' do
expect(resolve_issues(iteration_id: [iteration1.id])).to contain_exactly(issue_with_iteration)
expect(resolve_issues(iteration_id: [iteration1.id.to_s])).to contain_exactly(issue1)
end
end
describe 'filter by epic' do
let_it_be(:epic) { create :epic, group: group }
let_it_be(:epic2) { create :epic, group: group }
let_it_be(:issue1) { create :issue, project: project, epic: epic }
let_it_be(:issue2) { create :issue, project: project, epic: epic2 }
let_it_be(:issue3) { create :issue, project: project }
it 'returns issues without epic when epic_id is "none"' do
expect(resolve_issues(epic_id: 'none')).to match_array([issue3])
expect(resolve_issues(epic_id: 'none')).to contain_exactly(issue4, issue3)
end
it 'returns issues with any epic when epic_id is "any"' do
expect(resolve_issues(epic_id: 'any')).to match_array([issue1, issue2])
expect(resolve_issues(epic_id: 'any')).to contain_exactly(issue1, issue2)
end
it 'returns issues with any epic when epic_id is specific' do
expect(resolve_issues(epic_id: epic.id)).to match_array([issue1])
expect(resolve_issues(epic_id: epic1.id.to_s)).to contain_exactly(issue1)
end
end
describe 'filter by weight' do
context 'when filtering by any weight' do
it 'only returns issues that have a weight assigned' do
expect(resolve_issues(weight: 'any')).to contain_exactly(issue2, issue3)
end
end
context 'when filtering by no weight' do
it 'only returns issues that have no weight assigned' do
expect(resolve_issues(weight: 'none')).to contain_exactly(issue1, issue4)
end
end
context 'when filtering by specific weight' do
it 'only returns issues that have the specified weight assigned' do
expect(resolve_issues(weight: '3')).to contain_exactly(issue3)
end
end
end
describe 'filtering by negated params' do
describe 'filter by negated epic' do
it 'returns issues without the specified epic_id' do
expect(resolve_issues(not: { epic_id: epic2.id.to_s })).to contain_exactly(issue1, issue3, issue4)
end
end
describe 'filtering by negated weight' do
it 'only returns issues that do not have the specified weight assigned' do
expect(resolve_issues(not: { weight: '3' })).to contain_exactly(issue1, issue2, issue4)
end
end
end
end
......
......@@ -49,6 +49,13 @@ RSpec.describe IssuesFinder do
let(:expected_issuables) { [issue3, issue4] }
end
context 'when assignee_id does not exist' do
it_behaves_like 'assignee NOT ID filter' do
let(:params) { { not: { assignee_id: -100 } } }
let(:expected_issuables) { [issue1, issue2, issue3, issue4, issue5] }
end
end
context 'filter by username' do
let_it_be(:user3) { create(:user) }
......@@ -71,6 +78,17 @@ RSpec.describe IssuesFinder do
let(:params) { { not: { assignee_username: [user.username, user2.username] } } }
let(:expected_issuables) { [issue3, issue4] }
end
context 'when assignee_username does not exist' do
it_behaves_like 'assignee NOT username filter' do
before do
issue2.assignees = [user2]
end
let(:params) { { not: { assignee_username: 'non_existent_username' } } }
let(:expected_issuables) { [issue1, issue2, issue3, issue4, issue5] }
end
end
end
it_behaves_like 'no assignee filter' do
......
......@@ -69,6 +69,14 @@ RSpec.describe Resolvers::IssueStatusCountsResolver do
expect(result.closed).to eq 1
end
context 'when both assignee_username and assignee_usernames are provided' do
it 'raises a mutually exclusive filter error' do
expect do
resolve_issue_status_counts(assignee_usernames: [current_user.username], assignee_username: current_user.username)
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
end
end
private
def resolve_issue_status_counts(args = {}, context = { current_user: current_user })
......
......@@ -46,10 +46,6 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(milestone_title: [milestone.title])).to contain_exactly(issue1)
end
it 'filters by assignee_username' do
expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2)
end
it 'filters by two assignees' do
assignee2 = create(:user)
issue2.update!(assignees: [assignee, assignee2])
......@@ -78,6 +74,24 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
end
describe 'filters by assignee_username' do
it 'filters by assignee_username' do
expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2)
end
it 'filters by assignee_usernames' do
expect(resolve_issues(assignee_usernames: [assignee.username])).to contain_exactly(issue2)
end
context 'when both assignee_username and assignee_usernames are provided' do
it 'raises a mutually exclusive filter error' do
expect do
resolve_issues(assignee_usernames: [assignee.username], assignee_username: assignee.username)
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
end
end
end
describe 'filters by created_at' do
it 'filters by created_before' do
expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
......@@ -144,6 +158,29 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
describe 'filters by negated params' do
it 'returns issues without the specified iids' do
expect(resolve_issues(not: { iids: [issue1.iid] })).to contain_exactly(issue2)
end
it 'returns issues without the specified label names' do
expect(resolve_issues(not: { label_name: [label1.title] })).to be_empty
expect(resolve_issues(not: { label_name: [label2.title] })).to contain_exactly(issue1)
end
it 'returns issues without the specified milestone' do
expect(resolve_issues(not: { milestone_title: [milestone.title] })).to contain_exactly(issue2)
end
it 'returns issues without the specified assignee_usernames' do
expect(resolve_issues(not: { assignee_usernames: [assignee.username] })).to contain_exactly(issue1)
end
it 'returns issues without the specified assignee_id' do
expect(resolve_issues(not: { assignee_id: [assignee.id] })).to contain_exactly(issue1)
end
end
describe 'sorting' do
context 'when sorting by created' do
it 'sorts issues ascending' do
......
......@@ -136,14 +136,6 @@ RSpec.describe Pages::LookupPath do
)
end
end
context 'when pages_serve_from_migrated_zip feature flag is disabled' do
before do
stub_feature_flags(pages_serve_from_migrated_zip: false)
end
include_examples 'uses disk storage'
end
end
end
end
......
......@@ -12,6 +12,7 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let(:issue_filter_params) { {} }
let(:fields) do
<<~QUERY
......@@ -27,7 +28,7 @@ RSpec.describe 'getting an issue list for a project' do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('issues', {}, fields)
query_graphql_field('issues', issue_filter_params, fields)
)
end
......@@ -50,6 +51,16 @@ RSpec.describe 'getting an issue list for a project' do
expect(issues_data[1]['node']['discussionLocked']).to eq(true)
end
context 'when both assignee_username filters are provided' do
let(:issue_filter_params) { { assignee_username: current_user.username, assignee_usernames: [current_user.username] } }
it 'returns a mutually exclusive param error' do
post_graphql(query, current_user: current_user)
expect_graphql_errors_to_include('only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
end
end
context 'when limiting the number of results' do
let(:query) do
<<~GQL
......
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