Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-19 21:10:26 +00:00
parent 1e1012d3d2
commit b6bf52d3e2
29 changed files with 1620 additions and 14 deletions

View File

@ -5231,7 +5231,6 @@ RSpec/FeatureCategory:
- 'spec/rubocop/cop/migration/schema_addition_methods_no_post_spec.rb'
- 'spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb'
- 'spec/rubocop/cop/migration/timestamps_spec.rb'
- 'spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb'
- 'spec/rubocop/cop/migration/with_lock_retries_with_change_spec.rb'
- 'spec/rubocop/cop/performance/active_record_subtransaction_methods_spec.rb'
- 'spec/rubocop/cop/performance/active_record_subtransactions_spec.rb'

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Mutations
module Ci
module Catalog
module Resources
class Create < BaseMutation
graphql_name 'CatalogResourcesCreate'
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project to convert to a catalog resource.'
authorize :add_catalog_resource
def resolve(project_path:)
project = authorized_find!(project_path: project_path)
response = ::Ci::Catalog::AddResourceService.new(project, current_user).execute
errors = response.success? ? [] : [response.message]
{
errors: errors
}
end
private
def find_object(project_path:)
Project.find_by_full_path(project_path)
end
end
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Resolvers
module Ci
module Catalog
class ResourceResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
authorize :read_code
type ::Types::Ci::Catalog::ResourceType, null: true
argument :id, ::Types::GlobalIDType[::Ci::Catalog::Resource],
required: true,
description: 'CI/CD Catalog resource global ID.'
def resolve(id:)
catalog_resource = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(id))
authorize!(catalog_resource&.project)
catalog_resource
end
end
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Resolvers
module Ci
module Catalog
class ResourcesResolver < BaseResolver
include LooksAhead
type ::Types::Ci::Catalog::ResourceType.connection_type, null: true
argument :sort, ::Types::Ci::Catalog::ResourceSortEnum,
required: false,
description: 'Sort Catalog Resources by given criteria.'
argument :project_path, GraphQL::Types::ID,
required: false,
description: 'Project with the namespace catalog.'
def resolve_with_lookahead(project_path:, sort: nil)
project = Project.find_by_full_path(project_path)
apply_lookahead(
::Ci::Catalog::Listing.new(project.root_namespace, context[:current_user]).resources(sort: sort)
)
end
private
def preloads
{
web_path: { project: { namespace: :route } },
readme_html: { project: :route }
}
end
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Resolvers
module Ci
module Catalog
class VersionsResolver < ::Resolvers::ReleasesResolver
type Types::ReleaseType.connection_type, null: true
# This allows a maximum of 1 call to the field that uses this resolver. If the
# field is evaluated on more than one node, it causes performance degradation.
extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
private
def get_project
object.respond_to?(:project) ? object.project : object
end
# Override the aliased method in ReleasesResolver
alias_method :project, :get_project
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Types
module Ci
module Catalog
class ResourceSortEnum < SortEnum
graphql_name 'CiCatalogResourceSort'
description 'Values for sorting catalog resources'
value 'NAME_ASC', 'Name by ascending order.', value: :name_asc
value 'NAME_DESC', 'Name by descending order.', value: :name_desc
value 'LATEST_RELEASED_AT_ASC', 'Latest release date by ascending order.', value: :latest_released_at_asc
value 'LATEST_RELEASED_AT_DESC', 'Latest release date by descending order.', value: :latest_released_at_desc
end
end
end
end

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
module Types
module Ci
module Catalog
# rubocop: disable Graphql/AuthorizeTypes
class ResourceType < BaseObject
graphql_name 'CiCatalogResource'
connection_type_class Types::CountableConnectionType
field :open_issues_count, GraphQL::Types::Int, null: false,
description: 'Count of open issues that belong to the the catalog resource.',
alpha: { milestone: '16.3' }
field :open_merge_requests_count, GraphQL::Types::Int, null: false,
description: 'Count of open merge requests that belong to the the catalog resource.',
alpha: { milestone: '16.3' }
field :id, GraphQL::Types::ID, null: false, description: 'ID of the catalog resource.',
alpha: { milestone: '15.11' }
field :name, GraphQL::Types::String, null: true, description: 'Name of the catalog resource.',
alpha: { milestone: '15.11' }
field :description, GraphQL::Types::String, null: true, description: 'Description of the catalog resource.',
alpha: { milestone: '15.11' }
field :icon, GraphQL::Types::String, null: true, description: 'Icon for the catalog resource.',
method: :avatar_path, alpha: { milestone: '15.11' }
field :web_path, GraphQL::Types::String, null: true, description: 'Web path of the catalog resource.',
alpha: { milestone: '16.1' }
field :versions, Types::ReleaseType.connection_type, null: true,
description: 'Versions of the catalog resource. This field can only be ' \
'resolved for one catalog resource in any single request.',
resolver: Resolvers::Ci::Catalog::VersionsResolver,
alpha: { milestone: '16.2' }
field :latest_version, Types::ReleaseType, null: true, description: 'Latest version of the catalog resource.',
alpha: { milestone: '16.1' }
field :latest_released_at, Types::TimeType, null: true,
description: "Release date of the catalog resource's latest version.",
alpha: { milestone: '16.5' }
field :star_count, GraphQL::Types::Int, null: false,
description: 'Number of times the catalog resource has been starred.',
alpha: { milestone: '16.1' }
field :forks_count, GraphQL::Types::Int, null: false, calls_gitaly: true,
description: 'Number of times the catalog resource has been forked.',
alpha: { milestone: '16.1' }
field :root_namespace, Types::NamespaceType, null: true,
description: 'Root namespace of the catalog resource.',
alpha: { milestone: '16.1' }
markdown_field :readme_html, null: false,
alpha: { milestone: '16.1' }
def open_issues_count
BatchLoader::GraphQL.wrap(object.project.open_issues_count)
end
def open_merge_requests_count
BatchLoader::GraphQL.wrap(object.project.open_merge_requests_count)
end
def web_path
::Gitlab::Routing.url_helpers.project_path(object.project)
end
def latest_version
BatchLoader::GraphQL.for(object.project).batch do |projects, loader|
latest_releases = ReleasesFinder.new(projects, current_user, latest: true).execute
latest_releases.index_by(&:project).each do |project, latest_release|
loader.call(project, latest_release)
end
end
end
def forks_count
BatchLoader::GraphQL.wrap(object.forks_count)
end
def root_namespace
BatchLoader::GraphQL.for(object.project_id).batch do |project_ids, loader|
projects = Project.id_in(project_ids)
# This preloader uses traversal_ids to obtain Group-type root namespaces.
# It also preloads each project's immediate parent namespace, which effectively
# preloads the User-type root namespaces since they cannot be nested (parent == root).
Preloaders::ProjectRootAncestorPreloader.new(projects, :group).execute
root_namespaces = projects.map(&:root_ancestor)
# NamespaceType requires the `:read_namespace` ability. We must preload the policy for
# Group-type namespaces to avoid N+1 queries caused by the authorization requests.
group_root_namespaces = root_namespaces.select { |n| n.type == ::Group.sti_name }
Preloaders::GroupPolicyPreloader.new(group_root_namespaces, current_user).execute
# For User-type namespaces, the authorization request requires preloading the owner objects.
user_root_namespaces = root_namespaces.select { |n| n.type == ::Namespaces::UserNamespace.sti_name }
ActiveRecord::Associations::Preloader.new(records: user_root_namespaces, associations: :owner).call
projects.each { |project| loader.call(project.id, project.root_ancestor) }
end
end
def readme_html_resolver
markdown_context = context.to_h.dup.merge(project: object.project)
::MarkupHelper.markdown(object.project.repository.readme&.data, markdown_context)
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
end

View File

@ -39,6 +39,7 @@ module Types
mount_mutation Mutations::Boards::Lists::Update
mount_mutation Mutations::Boards::Lists::Destroy
mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Ci::Catalog::Resources::Create, alpha: { milestone: '15.11' }
mount_mutation Mutations::Clusters::Agents::Create
mount_mutation Mutations::Clusters::Agents::Delete
mount_mutation Mutations::Clusters::AgentTokens::Create

View File

@ -21,6 +21,20 @@ module Types
required: true, description: 'Global ID of the CI stage.'
end
field :ci_catalog_resources,
::Types::Ci::Catalog::ResourceType.connection_type,
null: true,
alpha: { milestone: '15.11' },
description: 'All CI/CD Catalog resources under a common namespace, visible to an authorized user',
resolver: ::Resolvers::Ci::Catalog::ResourcesResolver
field :ci_catalog_resource,
::Types::Ci::Catalog::ResourceType,
null: true,
alpha: { milestone: '16.1' },
description: 'A single CI/CD Catalog resource visible to an authorized user',
resolver: ::Resolvers::Ci::Catalog::ResourceResolver
field :ci_variables,
Types::Ci::InstanceVariableType.connection_type,
null: true,

View File

@ -303,6 +303,8 @@ class ProjectPolicy < BasePolicy
enable :set_show_diff_preview_in_email
enable :set_warn_about_potentially_unwanted_characters
enable :manage_owners
enable :add_catalog_resource
end
rule { can?(:guest_access) }.policy do
@ -908,10 +910,6 @@ class ProjectPolicy < BasePolicy
enable :read_namespace_catalog
end
rule { can?(:owner_access) & namespace_catalog_available }.policy do
enable :add_catalog_resource
end
rule { model_registry_enabled }.policy do
enable :read_model_registry
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Ci
module Catalog
class AddResourceService
include Gitlab::Allowable
attr_reader :project, :current_user
def initialize(project, user)
@current_user = user
@project = project
end
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)
validation_response = Ci::Catalog::Resources::ValidateService.new(project, project.default_branch).execute
if validation_response.success?
create_catalog_resource
else
ServiceResponse.error(message: validation_response.message)
end
end
private
def create_catalog_resource
catalog_resource = Ci::Catalog::Resource.new(project: project)
if catalog_resource.valid?
catalog_resource.save!
ServiceResponse.success(payload: catalog_resource)
else
ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
end
end
end
end
end

View File

@ -2142,9 +2142,14 @@ Example response:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131923) in GitLab 16.5.
Use this API to create a new personal access token for the currently authenticated user.
For security purposes, the scopes are limited to only `k8s_proxy` and by default the token will expire by
the end of the day it was created at.
Token values are returned once so, make sure you save it as you can't access it again.
For security purposes, the token:
- Is limited to the [`k8s_proxy` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
This scope grants permission to perform Kubernetes API calls using the agent for Kubernetes.
- By default, expires at the end of the day it was created on.
Token values are returned once, so make sure you save the token as you cannot access
it again.
```plaintext
POST /user/personal_access_tokens

View File

@ -26,6 +26,7 @@ The following GitLab projects use TypeScript:
- [`gitlab-web-ide`](https://gitlab.com/gitlab-org/gitlab-web-ide/)
- [`gitlab-vscode-extension`](https://gitlab.com/gitlab-org/gitlab-vscode-extension/)
- [`gitlab-language-server-for-code-suggestions`](https://gitlab.com/gitlab-org/editor-extensions/gitlab-language-server-for-code-suggestions)
- [`gitlab-org/cluster-integration/javascript-client`](https://gitlab.com/gitlab-org/cluster-integration/javascript-client)
## Recommended configurations

60
doc/devsecops.md Normal file
View File

@ -0,0 +1,60 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
---
# GitLab: The DevSecOps platform
DevSecOps is a combination of development, security, and operations.
It is an approach to software development that integrates security throughout the development lifecycle.
## DevSecOps compared to DevOps
DevOps combines development and operations, with the intent to increase the efficiency,
speed, and security of software development and delivery.
DevOps means working together to conceive, build, and deliver secure software at top speed.
DevOps practices include automation, collaboration, fast feedback, and iterative improvement.
DevSecOps is an evolution of DevOps. DevSecOps includes application security practices in every stage of software development.
Throughout the development process, tools and methods protect and monitor your live applications.
New attack surfaces, like containers and orchestrators, must also be monitored and protected.
DevSecOps tools automate security workflows to create an adaptable process for your development
and security teams, improving collaboration and breaking down silos.
By embedding security into the software development lifecycle, you can consistently secure fast-moving
and iterative processes, improving efficiency without sacrificing quality.
## DevSecOps fundamentals
DevSecOps fundamentals include:
- Automation
- Collaboration
- Policy guardrails
- Visibility
For details, see [this article about DevSecOps](https://about.gitlab.com/topics/devsecops/).
## Is DevSecOps right for you?
If your organization is facing any of the following challenges, a DevSecOps approach might be for you.
- **Development, security, and operations teams are siloed.**
If development and operations are isolated from security issues,
they can't build secure software. And if security teams aren't part of the development process,
they can't identify risks proactively. DevSecOps brings teams together to improve workflows
and share ideas. Organizations might even see improved employee morale and retention.
- **Long development cycles are making it difficult to meet customer or stakeholder demands.**
One reason for the struggle could be security. DevSecOps implements security at every step of
the development lifecycle, meaning that solid security doesnt require the whole process to come to a halt.
- **Youre migrating to the cloud (or considering it).**
Moving to the cloud often means bringing on new development processes, tools, and systems.
Its a great time to make processes faster and more secure — and DevSecOps could make that a lot easier.
To get started with DevSecOps,
[learn more, and try GitLab Ultimate for free](https://about.gitlab.com/solutions/security-compliance/).

View File

@ -1,5 +1,5 @@
variables:
DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -1,5 +1,5 @@
variables:
AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -1,5 +1,5 @@
variables:
AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -29,6 +29,12 @@ module RuboCop
index_exists?
column_exists?
create_trigger_to_sync_tables
lock_tables
swap_columns
swap_columns_default
swap_foreign_keys
swap_indexes
reset_trigger_function
].sort.freeze
MSG = "The method is not allowed to be called within the `with_lock_retries` block, the only allowed methods are: #{ALLOWED_MIGRATION_METHODS.join(', ')}".freeze

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::Catalog::ResourceResolver, feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:namespace) { create(:group) }
let_it_be(:project) { create(:project, :private, namespace: namespace) }
let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
let_it_be(:user) { create(:user) }
describe '#resolve' do
context 'when the user can read code on the catalog resource project' do
before_all do
namespace.add_developer(user)
end
context 'when resource is found' do
it 'returns a single CI/CD Catalog resource' do
result = resolve(described_class, ctx: { current_user: user },
args: { id: resource.to_global_id.to_s })
expect(result.id).to eq(resource.id)
expect(result.class).to eq(Ci::Catalog::Resource)
end
end
context 'when resource does not exist' do
it 'raises ResourceNotAvailable error' do
result = resolve(described_class, ctx: { current_user: user },
args: { id: "gid://gitlab/Ci::Catalog::Resource/not-a-real-id" })
expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'when the user cannot read code on the catalog resource project' do
it 'raises ResourceNotAvailable error' do
result = resolve(described_class, ctx: { current_user: user },
args: { id: resource.to_global_id.to_s })
expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:namespace) { create(:group) }
let_it_be(:project_1) { create(:project, name: 'Z', namespace: namespace) }
let_it_be(:project_2) { create(:project, name: 'A', namespace: namespace) }
let_it_be(:project_3) { create(:project, name: 'L', namespace: namespace) }
let_it_be(:resource_1) { create(:ci_catalog_resource, project: project_1) }
let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2) }
let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3) }
let_it_be(:user) { create(:user) }
describe '#resolve' do
context 'with an authorized user' do
before_all do
namespace.add_owner(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
it 'returns all CI Catalog resources visible to the current user in the namespace' do
result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path })
expect(result.items.count).to be(3)
expect(result.items.pluck(:name)).to contain_exactly('Z', 'A', 'L')
end
it 'returns all resources sorted by descending created date when given no sort param' do
result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path })
expect(result.items.pluck(:name)).to eq(%w[L A Z])
end
it 'returns all CI Catalog resources sorted by descending name when there is a sort parameter' do
result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path, sort:
'NAME_DESC' })
expect(result.items.pluck(:name)).to eq(%w[Z L A])
end
end
context 'when the current user cannot read the namespace catalog' do
it 'raises ResourceNotAvailable' do
stub_licensed_features(ci_namespace_catalog: true)
namespace.add_guest(user)
result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path })
expect(result).to be_empty
end
end
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
# In this context, a `version` is equivalent to a `release`
RSpec.describe Resolvers::Ci::Catalog::VersionsResolver, feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:today) { Time.now }
let_it_be(:yesterday) { today - 1.day }
let_it_be(:tomorrow) { today + 1.day }
let_it_be(:project) { create(:project, :private) }
# rubocop: disable Layout/LineLength
let_it_be(:version1) { create(:release, project: project, tag: 'v1.0.0', released_at: yesterday, created_at: tomorrow) }
let_it_be(:version2) { create(:release, project: project, tag: 'v2.0.0', released_at: today, created_at: yesterday) }
let_it_be(:version3) { create(:release, project: project, tag: 'v3.0.0', released_at: tomorrow, created_at: today) }
# rubocop: enable Layout/LineLength
let_it_be(:developer) { create(:user) }
let_it_be(:public_user) { create(:user) }
let(:args) { { sort: :released_at_desc } }
let(:all_releases) { [version1, version2, version3] }
before_all do
project.add_developer(developer)
end
describe '#resolve' do
it_behaves_like 'releases and group releases resolver'
describe 'when order_by is created_at' do
let(:current_user) { developer }
context 'with sort: desc' do
let(:args) { { sort: :created_desc } }
it 'returns the releases ordered by created_at in descending order' do
expect(resolve_releases.to_a)
.to match_array(all_releases)
.and be_sorted(:created_at, :desc)
end
end
context 'with sort: asc' do
let(:args) { { sort: :created_asc } }
it 'returns the releases ordered by created_at in ascending order' do
expect(resolve_releases.to_a)
.to match_array(all_releases)
.and be_sorted(:created_at, :asc)
end
end
end
end
private
def resolve_versions
context = { current_user: current_user }
resolve(described_class, obj: project, args: args, ctx: context, arg_style: :internal)
end
# Required for shared examples
alias_method :resolve_releases, :resolve_versions
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['CiCatalogResourceSort'], feature_category: :pipeline_composition do
it { expect(described_class.graphql_name).to eq('CiCatalogResourceSort') }
it 'exposes all the existing catalog resource sort orders' do
expect(described_class.values.keys).to include(
*%w[NAME_ASC NAME_DESC LATEST_RELEASED_AT_ASC LATEST_RELEASED_AT_DESC]
)
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::Catalog::ResourceType, feature_category: :pipeline_composition do
specify { expect(described_class.graphql_name).to eq('CiCatalogResource') }
it 'exposes the expected fields' do
expected_fields = %i[
id
name
description
icon
web_path
versions
latest_version
latest_released_at
star_count
forks_count
root_namespace
readme_html
open_issues_count
open_merge_requests_count
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end

View File

@ -3212,9 +3212,23 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
describe 'add_catalog_resource' do
let(:current_user) { owner }
using RSpec::Parameterized::TableSyntax
specify { is_expected.to be_disallowed(:read_namespace_catalog) }
let(:current_user) { public_send(role) }
where(:role, :allowed) do
:owner | true
:maintainer | false
:developer | false
:reporter | false
:guest | false
end
with_them do
it do
expect(subject.can?(:add_catalog_resource)).to be(allowed)
end
end
end
describe 'read_model_registry' do

View File

@ -0,0 +1,341 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) }
let_it_be(:project) do
create(
:project, :with_avatar, :custom_repo,
name: 'Component Repository',
description: 'A simple component',
namespace: namespace,
star_count: 1,
files: { 'README.md' => '[link](README.md)' }
)
end
let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
let(:query) do
<<~GQL
query {
ciCatalogResource(id: "#{resource.to_global_id}") {
#{all_graphql_fields_for('CiCatalogResource', max_depth: 1)}
}
}
GQL
end
subject(:post_query) { post_graphql(query, current_user: user) }
context 'when the current user has permission to read the namespace catalog' do
it 'returns the resource with the expected data' do
namespace.add_developer(user)
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(
resource, :name, :description,
icon: project.avatar_path,
webPath: "/#{project.full_path}",
starCount: project.star_count,
forksCount: project.forks_count,
readmeHtml: a_string_including(
"#{project.full_path}/-/blob/#{project.default_branch}/README.md"
)
)
)
end
end
context 'when the current user does not have permission to read the namespace catalog' do
it 'returns nil' do
namespace.add_guest(user)
post_query
expect(graphql_data_at(:ciCatalogResource)).to be_nil
end
end
describe 'versions' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
let(:query) do
<<~GQL
query {
ciCatalogResource(id: "#{resource.to_global_id}") {
id
versions {
nodes {
id
tagName
releasedAt
author {
id
name
webUrl
}
}
}
}
}
GQL
end
context 'when the resource has versions' do
let_it_be(:author) { create(:user, name: 'author') }
let_it_be(:version1) do
create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
end
let_it_be(:version2) do
create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
end
it 'returns the resource with the versions data' do
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(resource)
)
expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly(
a_graphql_entity_for(
version1,
tagName: version1.tag,
releasedAt: version1.released_at,
author: a_graphql_entity_for(author, :name)
),
a_graphql_entity_for(
version2,
tagName: version2.tag,
releasedAt: version2.released_at,
author: a_graphql_entity_for(author, :name)
)
)
end
end
context 'when the resource does not have a version' do
it 'returns versions as an empty array' do
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(resource, versions: { 'nodes' => [] })
)
end
end
end
describe 'latestVersion' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
let(:query) do
<<~GQL
query {
ciCatalogResource(id: "#{resource.to_global_id}") {
id
latestVersion {
id
tagName
releasedAt
author {
id
name
webUrl
}
}
}
}
GQL
end
context 'when the resource has versions' do
let_it_be(:author) { create(:user, name: 'author') }
let_it_be(:latest_version) do
create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
end
before_all do
# Previous version of the project
create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
end
it 'returns the resource with the latest version data' do
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(
resource,
latestVersion: a_graphql_entity_for(
latest_version,
tagName: latest_version.tag,
releasedAt: latest_version.released_at,
author: a_graphql_entity_for(author, :name)
)
)
)
end
end
context 'when the resource does not have a version' do
it 'returns nil' do
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(resource, latestVersion: nil)
)
end
end
end
describe 'rootNamespace' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
let(:query) do
<<~GQL
query {
ciCatalogResource(id: "#{resource.to_global_id}") {
id
rootNamespace {
id
name
path
}
}
}
GQL
end
it 'returns the correct root namespace data' do
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(
resource,
rootNamespace: a_graphql_entity_for(namespace, :name, :path)
)
)
end
end
describe 'openIssuesCount' do
before do
stub_licensed_features(ci_namespace_catalog: true)
end
context 'when open_issue_count is requested' do
let(:query) do
<<~GQL
query {
ciCatalogResource(id: "#{resource.to_global_id}") {
openIssuesCount
}
}
GQL
end
it 'returns the correct count' do
create(:issue, :opened, project: project)
create(:issue, :opened, project: project)
namespace.add_developer(user)
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(
open_issues_count: 2
)
)
end
context 'when open_issue_count is zero' do
it 'returns zero' do
namespace.add_developer(user)
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(
open_issues_count: 0
)
)
end
end
end
end
describe 'openMergeRequestsCount' do
before do
stub_licensed_features(ci_namespace_catalog: true)
end
context 'when merge_requests_count is requested' do
let(:query) do
<<~GQL
query {
ciCatalogResource(id: "#{resource.to_global_id}") {
openMergeRequestsCount
}
}
GQL
end
it 'returns the correct count' do
create(:merge_request, :opened, source_project: project)
namespace.add_developer(user)
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(
open_merge_requests_count: 1
)
)
end
context 'when open merge_requests_count is zero' do
it 'returns zero' do
namespace.add_developer(user)
post_query
expect(graphql_data_at(:ciCatalogResource)).to match(
a_graphql_entity_for(
open_merge_requests_count: 0
)
)
end
end
end
end
end

View File

@ -0,0 +1,533 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) }
let_it_be(:project2) { create(:project, namespace: namespace) }
let_it_be(:project1) do
create(
:project, :with_avatar, :custom_repo,
name: 'Component Repository',
description: 'A simple component',
namespace: namespace,
star_count: 1,
files: { 'README.md' => '**Test**' }
)
end
let_it_be(:resource1) { create(:ci_catalog_resource, project: project1, latest_released_at: '2023-01-01T00:00:00Z') }
let(:query) do
<<~GQL
query {
ciCatalogResources(projectPath: "#{project1.full_path}") {
nodes {
#{all_graphql_fields_for('CiCatalogResource', max_depth: 1)}
}
}
}
GQL
end
subject(:post_query) { post_graphql(query, current_user: user) }
shared_examples 'avoids N+1 queries' do
it do
ctx = { current_user: user }
control_count = ActiveRecord::QueryRecorder.new do
run_with_clean_state(query, context: ctx)
end
create(:ci_catalog_resource, project: project2)
expect do
run_with_clean_state(query, context: ctx)
end.not_to exceed_query_limit(control_count)
end
end
context 'when the current user has permission to read the namespace catalog' do
before_all do
namespace.add_developer(user)
end
it 'returns the resource with the expected data' do
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(
resource1, :name, :description,
icon: project1.avatar_path,
webPath: "/#{project1.full_path}",
starCount: project1.star_count,
forksCount: project1.forks_count,
readmeHtml: a_string_including('Test</strong>'),
latestReleasedAt: resource1.latest_released_at
)
)
end
context 'when there are two resources visible to the current user in the namespace' do
it 'returns both resources with the expected data' do
resource2 = create(:ci_catalog_resource, project: project2)
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(resource1),
a_graphql_entity_for(
resource2, :name, :description,
icon: project2.avatar_path,
webPath: "/#{project2.full_path}",
starCount: project2.star_count,
forksCount: project2.forks_count,
readmeHtml: '',
latestReleasedAt: resource2.latest_released_at
)
)
end
it_behaves_like 'avoids N+1 queries'
end
end
context 'when the current user does not have permission to read the namespace catalog' do
it 'returns no resources' do
namespace.add_guest(user)
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to be_empty
end
end
describe 'versions' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
let(:query) do
<<~GQL
query {
ciCatalogResources(projectPath: "#{project1.full_path}") {
nodes {
id
versions {
nodes {
id
tagName
releasedAt
author {
id
name
webUrl
}
}
}
}
}
}
GQL
end
context 'when there is a single resource visible to the current user in the namespace' do
context 'when the resource has versions' do
let_it_be(:author) { create(:user, name: 'author') }
let_it_be(:version1) do
create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author)
end
let_it_be(:version2) do
create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author)
end
it 'returns the resource with the versions data' do
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(resource1)
)
expect(graphql_data_at(:ciCatalogResources, :nodes, 0, :versions, :nodes)).to contain_exactly(
a_graphql_entity_for(
version1,
tagName: version1.tag,
releasedAt: version1.released_at,
author: a_graphql_entity_for(author, :name)
),
a_graphql_entity_for(
version2,
tagName: version2.tag,
releasedAt: version2.released_at,
author: a_graphql_entity_for(author, :name)
)
)
end
end
context 'when the resource does not have a version' do
it 'returns versions as an empty array' do
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(resource1, versions: { 'nodes' => [] })
)
end
end
end
context 'when there are multiple resources visible to the current user in the namespace' do
it 'limits the request to 1 resource at a time' do
create(:ci_catalog_resource, project: project2)
post_query
expect_graphql_errors_to_include \
[/"versions" field can be requested only for 1 CiCatalogResource\(s\) at a time./]
end
it_behaves_like 'avoids N+1 queries'
end
end
describe 'latestVersion' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
let(:query) do
<<~GQL
query {
ciCatalogResources(projectPath: "#{project1.full_path}") {
nodes {
id
latestVersion {
id
tagName
releasedAt
author {
id
name
webUrl
}
}
}
}
}
GQL
end
context 'when the resource has versions' do
let_it_be(:author1) { create(:user, name: 'author1') }
let_it_be(:author2) { create(:user, name: 'author2') }
let_it_be(:latest_version1) do
create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author1)
end
let_it_be(:latest_version2) do
create(:release, project: project2, released_at: '2023-02-01T00:00:00Z', author: author2)
end
before_all do
# Previous versions of the projects
create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author1)
create(:release, project: project2, released_at: '2023-01-01T00:00:00Z', author: author2)
end
it 'returns the resource with the latest version data' do
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(
resource1,
latestVersion: a_graphql_entity_for(
latest_version1,
tagName: latest_version1.tag,
releasedAt: latest_version1.released_at,
author: a_graphql_entity_for(author1, :name)
)
)
)
end
context 'when there are multiple resources visible to the current user in the namespace' do
let_it_be(:project0) { create(:project, namespace: namespace) }
let_it_be(:resource0) { create(:ci_catalog_resource, project: project0) }
let_it_be(:author0) { create(:user, name: 'author0') }
let_it_be(:version0) do
create(:release, project: project0, released_at: '2023-01-01T00:00:00Z', author: author0)
end
it 'returns all resources with the latest version data' do
resource2 = create(:ci_catalog_resource, project: project2)
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(
resource0,
latestVersion: a_graphql_entity_for(
version0,
tagName: version0.tag,
releasedAt: version0.released_at,
author: a_graphql_entity_for(author0, :name)
)
),
a_graphql_entity_for(
resource1,
latestVersion: a_graphql_entity_for(
latest_version1,
tagName: latest_version1.tag,
releasedAt: latest_version1.released_at,
author: a_graphql_entity_for(author1, :name)
)
),
a_graphql_entity_for(
resource2,
latestVersion: a_graphql_entity_for(
latest_version2,
tagName: latest_version2.tag,
releasedAt: latest_version2.released_at,
author: a_graphql_entity_for(author2, :name)
)
)
)
end
it_behaves_like 'avoids N+1 queries'
end
end
context 'when the resource does not have a version' do
it 'returns nil' do
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(resource1, latestVersion: nil)
)
end
end
end
describe 'rootNamespace' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
let(:query) do
<<~GQL
query {
ciCatalogResources(projectPath: "#{project1.full_path}") {
nodes {
id
rootNamespace {
id
name
path
}
}
}
}
GQL
end
it 'returns the correct root namespace data' do
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(
resource1,
rootNamespace: a_graphql_entity_for(namespace, :name, :path)
)
)
end
shared_examples 'returns the correct root namespace for both resources' do
it do
resource2 = create(:ci_catalog_resource, project: project2)
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(resource1, rootNamespace: a_graphql_entity_for(namespace)),
a_graphql_entity_for(resource2, rootNamespace: a_graphql_entity_for(namespace2))
)
end
end
shared_examples 'when there are two resources visible to the current user' do
it_behaves_like 'returns the correct root namespace for both resources'
it_behaves_like 'avoids N+1 queries'
context 'when a resource is within a nested namespace' do
let_it_be(:nested_namespace) { create(:group, parent: namespace2) }
let_it_be(:project2) { create(:project, namespace: nested_namespace) }
it_behaves_like 'returns the correct root namespace for both resources'
it_behaves_like 'avoids N+1 queries'
end
end
context 'when there are multiple resources visible to the current user from the same root namespace' do
let_it_be(:namespace2) { namespace }
it_behaves_like 'when there are two resources visible to the current user'
end
# We expect the resources resolver will eventually support returning resources from multiple root namespaces.
context 'when there are multiple resources visible to the current user from different root namespaces' do
before do
# In order to mock this scenario, we allow the resolver to return
# all existing resources without scoping to a specific namespace.
allow_next_instance_of(::Ci::Catalog::Listing) do |instance|
allow(instance).to receive(:resources).and_return(::Ci::Catalog::Resource.includes(:project))
end
end
# Make the current user an Admin so it has `:read_namespace` ability on all namespaces
let_it_be(:user) { create(:admin) }
let_it_be(:namespace2) { create(:group) }
let_it_be(:project2) { create(:project, namespace: namespace2) }
it_behaves_like 'when there are two resources visible to the current user'
context 'when a resource is within a User namespace' do
let_it_be(:namespace2) { create(:user).namespace }
let_it_be(:project2) { create(:project, namespace: namespace2) }
# A response containing any number of 'User' type root namespaces will always execute 1 extra
# query than a response with only 'Group' type root namespaces. This is due to their different
# policies. Here we preemptively create another resource with a 'User' type root namespace so
# that the control_count in the N+1 test includes this extra query.
let_it_be(:namespace3) { create(:user).namespace }
let_it_be(:resource3) { create(:ci_catalog_resource, project: create(:project, namespace: namespace3)) }
it 'returns the correct root namespace for all resources' do
resource2 = create(:ci_catalog_resource, project: project2)
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(resource1, rootNamespace: a_graphql_entity_for(namespace)),
a_graphql_entity_for(resource2, rootNamespace: a_graphql_entity_for(namespace2)),
a_graphql_entity_for(resource3, rootNamespace: a_graphql_entity_for(namespace3))
)
end
it_behaves_like 'avoids N+1 queries'
end
end
end
describe 'openIssuesCount' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
context 'when open_issues_count is requested' do
before_all do
create(:issue, :opened, project: project1)
create(:issue, :opened, project: project1)
create(:issue, :opened, project: project2)
end
let(:query) do
<<~GQL
query {
ciCatalogResources(projectPath: "#{project1.full_path}") {
nodes {
openIssuesCount
}
}
}
GQL
end
it 'returns the correct count' do
create(:ci_catalog_resource, project: project2)
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(
openIssuesCount: 2),
a_graphql_entity_for(
openIssuesCount: 1)
)
end
it_behaves_like 'avoids N+1 queries'
end
end
describe 'openMergeRequestsCount' do
before_all do
namespace.add_developer(user)
end
before do
stub_licensed_features(ci_namespace_catalog: true)
end
context 'when open_merge_requests_count is requested' do
before_all do
create(:merge_request, :opened, source_project: project1)
create(:merge_request, :opened, source_project: project2)
end
let(:query) do
<<~GQL
query {
ciCatalogResources(projectPath: "#{project1.full_path}") {
nodes {
openMergeRequestsCount
}
}
}
GQL
end
it 'returns the correct count' do
create(:ci_catalog_resource, project: project2)
post_query
expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
a_graphql_entity_for(
openMergeRequestsCount: 1),
a_graphql_entity_for(
openMergeRequestsCount: 1)
)
end
it_behaves_like 'avoids N+1 queries'
end
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'CatalogResourcesCreate', feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project, :repository, description: 'our components') }
let(:mutation) do
variables = {
project_path: project.full_path
}
graphql_mutation(:catalog_resources_create, variables,
<<-QL.strip_heredoc
errors
QL
)
end
context 'when unauthorized' do
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when authorized' do
context 'with a valid project' do
before_all do
project.add_owner(current_user)
end
it 'creates a catalog resource' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_mutation_response(:catalog_resources_create)['errors']).to be_empty
expect(response).to have_gitlab_http_status(:success)
end
end
context 'with an invalid project' do
let_it_be(:project) { create(:project, :repository) }
before_all do
project.add_owner(current_user)
end
it 'returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_mutation_response(:catalog_resources_create)['errors']).not_to be_empty
end
end
end
end

View File

@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/with_lock_retries_disallowed_method'
RSpec.describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod do
RSpec.describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod, feature_category: :database do
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :repository, description: 'Our components') }
let_it_be(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
before do
stub_licensed_features(ci_namespace_catalog: true)
end
describe '#execute' do
context 'with an unauthorized user' do
it 'raises an AccessDeniedError' do
expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
context 'with an authorized user' do
before_all do
project.add_owner(user)
end
context 'and a valid project' do
it 'creates a catalog resource' do
response = service.execute
expect(response.payload.project).to eq(project)
end
end
context 'with an invalid project' do
let_it_be(:project) { create(:project, :repository) }
before_all do
project.add_owner(user)
end
it 'does not create a catalog resource' do
response = service.execute
expect(response.message).to eq('Project must have a description')
end
end
context 'with an invalid catalog resource' do
it 'does not save the catalog resource' do
catalog_resource = instance_double(::Ci::Catalog::Resource,
valid?: false,
errors: instance_double(ActiveModel::Errors, full_messages: ['not valid']))
allow(::Ci::Catalog::Resource).to receive(:new).and_return(catalog_resource)
response = service.execute
expect(response.message).to eq('not valid')
end
end
end
end
end