Commit 69afb4c7 authored by Robert Speicher's avatar Robert Speicher

Merge branch '10io-graphql-container-repository-details-api' into 'master'

Container repository details GraphQL API

See merge request gitlab-org/gitlab!46560
parents 1aa7f24b 8e2d2906
# frozen_string_literal: true
module Types
class ContainerRepositoryDetailsType < Types::ContainerRepositoryType
graphql_name 'ContainerRepositoryDetails'
description 'Details of a container repository'
authorize :read_container_image
field :tags,
Types::ContainerRepositoryTagType.connection_type,
null: true,
description: 'Tags of the container repository',
max_page_size: 20
def can_delete
Ability.allowed?(current_user, :destroy_container_image, object)
end
end
end
# frozen_string_literal: true
module Types
class ContainerRepositoryTagType < BaseObject
graphql_name 'ContainerRepositoryTag'
description 'A tag from a container repository'
authorize :read_container_image
field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the tag.'
field :path, GraphQL::STRING_TYPE, null: false, description: 'Path of the tag.'
field :location, GraphQL::STRING_TYPE, null: false, description: 'URL of the tag.'
field :digest, GraphQL::STRING_TYPE, null: false, description: 'Digest of the tag.'
field :revision, GraphQL::STRING_TYPE, null: false, description: 'Revision of the tag.'
field :short_revision, GraphQL::STRING_TYPE, null: false, description: 'Short revision of the tag.'
field :total_size, GraphQL::INT_TYPE, null: false, description: 'The size of the tag.'
field :created_at, Types::TimeType, null: false, description: 'Timestamp when the tag was created.'
field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete this tag.'
def can_delete
Ability.allowed?(current_user, :destroy_container_image, object)
end
end
end
...@@ -296,8 +296,7 @@ module Types ...@@ -296,8 +296,7 @@ module Types
Types::ContainerRepositoryType.connection_type, Types::ContainerRepositoryType.connection_type,
null: true, null: true,
description: 'Container repositories of the project', description: 'Container repositories of the project',
resolver: Resolvers::ContainerRepositoriesResolver, resolver: Resolvers::ContainerRepositoriesResolver
authorize: :read_container_image
field :label, field :label,
Types::LabelType, Types::LabelType,
......
...@@ -50,10 +50,14 @@ module Types ...@@ -50,10 +50,14 @@ module Types
field :milestone, ::Types::MilestoneType, field :milestone, ::Types::MilestoneType,
null: true, null: true,
description: 'Find a milestone' do description: 'Find a milestone' do
argument :id, ::Types::GlobalIDType[Milestone], argument :id, ::Types::GlobalIDType[Milestone], required: true, description: 'Find a milestone by its ID'
required: true, end
description: 'Find a milestone by its ID'
end field :container_repository, Types::ContainerRepositoryDetailsType,
null: true,
description: 'Find a container repository' do
argument :id, ::Types::GlobalIDType[::ContainerRepository], required: true, description: 'The global ID of the container repository'
end
field :user, Types::UserType, field :user, Types::UserType,
null: true, null: true,
...@@ -105,6 +109,13 @@ module Types ...@@ -105,6 +109,13 @@ module Types
id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id) id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id) GitlabSchema.find_by_gid(id)
end end
def container_repository(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::ContainerRepository].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end end
end end
......
# frozen_string_literal: true
module ContainerRegistry
class TagPolicy < BasePolicy
delegate { @subject.repository }
end
end
---
title: Container repository details GraphQL API
merge_request: 46560
author:
type: added
...@@ -3344,6 +3344,86 @@ type ContainerRepositoryConnection { ...@@ -3344,6 +3344,86 @@ type ContainerRepositoryConnection {
pageInfo: PageInfo! pageInfo: PageInfo!
} }
"""
Details of a container repository
"""
type ContainerRepositoryDetails {
"""
Can the current user delete the container repository.
"""
canDelete: Boolean!
"""
Timestamp when the container repository was created.
"""
createdAt: Time!
"""
Timestamp when the cleanup done by the expiration policy was started on the container repository.
"""
expirationPolicyStartedAt: Time
"""
ID of the container repository.
"""
id: ID!
"""
URL of the container repository.
"""
location: String!
"""
Name of the container repository.
"""
name: String!
"""
Path of the container repository.
"""
path: String!
"""
Status of the container repository.
"""
status: ContainerRepositoryStatus
"""
Tags of the container repository
"""
tags(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ContainerRepositoryTagConnection
"""
Number of tags associated with this image.
"""
tagsCount: Int!
"""
Timestamp when the container repository was updated.
"""
updatedAt: Time!
}
""" """
An edge in a connection. An edge in a connection.
""" """
...@@ -3359,6 +3439,11 @@ type ContainerRepositoryEdge { ...@@ -3359,6 +3439,11 @@ type ContainerRepositoryEdge {
node: ContainerRepository node: ContainerRepository
} }
"""
Identifier of ContainerRepository
"""
scalar ContainerRepositoryID
""" """
Status of a container repository Status of a container repository
""" """
...@@ -3374,6 +3459,91 @@ enum ContainerRepositoryStatus { ...@@ -3374,6 +3459,91 @@ enum ContainerRepositoryStatus {
DELETE_SCHEDULED DELETE_SCHEDULED
} }
"""
A tag from a container repository
"""
type ContainerRepositoryTag {
"""
Can the current user delete this tag.
"""
canDelete: Boolean!
"""
Timestamp when the tag was created.
"""
createdAt: Time!
"""
Digest of the tag.
"""
digest: String!
"""
URL of the tag.
"""
location: String!
"""
Name of the tag.
"""
name: String!
"""
Path of the tag.
"""
path: String!
"""
Revision of the tag.
"""
revision: String!
"""
Short revision of the tag.
"""
shortRevision: String!
"""
The size of the tag.
"""
totalSize: Int!
}
"""
The connection type for ContainerRepositoryTag.
"""
type ContainerRepositoryTagConnection {
"""
A list of edges.
"""
edges: [ContainerRepositoryTagEdge]
"""
A list of nodes.
"""
nodes: [ContainerRepositoryTag]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ContainerRepositoryTagEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ContainerRepositoryTag
}
""" """
Autogenerated input type of CreateAlertIssue Autogenerated input type of CreateAlertIssue
""" """
...@@ -16810,6 +16980,16 @@ type PromoteToEpicPayload { ...@@ -16810,6 +16980,16 @@ type PromoteToEpicPayload {
} }
type Query { type Query {
"""
Find a container repository
"""
containerRepository(
"""
The global ID of the container repository
"""
id: ContainerRepositoryID!
): ContainerRepositoryDetails
""" """
Get information about current user Get information about current user
""" """
......
...@@ -541,6 +541,40 @@ A container repository. ...@@ -541,6 +541,40 @@ A container repository.
| `tagsCount` | Int! | Number of tags associated with this image. | | `tagsCount` | Int! | Number of tags associated with this image. |
| `updatedAt` | Time! | Timestamp when the container repository was updated. | | `updatedAt` | Time! | Timestamp when the container repository was updated. |
### ContainerRepositoryDetails
Details of a container repository.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `canDelete` | Boolean! | Can the current user delete the container repository. |
| `createdAt` | Time! | Timestamp when the container repository was created. |
| `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
| `id` | ID! | ID of the container repository. |
| `location` | String! | URL of the container repository. |
| `name` | String! | Name of the container repository. |
| `path` | String! | Path of the container repository. |
| `status` | ContainerRepositoryStatus | Status of the container repository. |
| `tags` | ContainerRepositoryTagConnection | Tags of the container repository |
| `tagsCount` | Int! | Number of tags associated with this image. |
| `updatedAt` | Time! | Timestamp when the container repository was updated. |
### ContainerRepositoryTag
A tag from a container repository.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `canDelete` | Boolean! | Can the current user delete this tag. |
| `createdAt` | Time! | Timestamp when the tag was created. |
| `digest` | String! | Digest of the tag. |
| `location` | String! | URL of the tag. |
| `name` | String! | Name of the tag. |
| `path` | String! | Path of the tag. |
| `revision` | String! | Revision of the tag. |
| `shortRevision` | String! | Short revision of the tag. |
| `totalSize` | Int! | The size of the tag. |
### CreateAlertIssuePayload ### CreateAlertIssuePayload
Autogenerated return type of CreateAlertIssue. Autogenerated return type of CreateAlertIssue.
......
{
"type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "tags"],
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"location": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"expirationPolicyStartedAt": {
"type": ["string", "null"]
},
"status": {
"type": ["string", "null"]
},
"tagsCount": {
"type": "integer"
},
"canDelete": {
"type": "boolean"
},
"tags": {
"type": "object",
"required": ["nodes"],
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "path", "location", "digest", "revision", "shortRevision", "totalSize", "createdAt", "canDelete"],
"properties": {
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"location": {
"type": "string"
},
"digest": {
"type": "string"
},
"revision": {
"type": "string"
},
"shortRevision": {
"type": "string"
},
"totalSize": {
"type": "integer"
},
"createdAt": {
"type": "string"
},
"canDelete": {
"type": "boolean"
}
}
}
}
}
}
}
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete tags]
it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') }
it { expect(described_class.description).to eq('Details of a container repository') }
it { expect(described_class).to require_graphql_authorizations(:read_container_image) }
it { expect(described_class).to have_graphql_fields(fields) }
describe 'tags field' do
subject { described_class.fields['tags'] }
it 'returns tags connection type' do
is_expected.to have_graphql_type(Types::ContainerRepositoryTagType.connection_type)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryTag'] do
fields = %i[name path location digest revision short_revision total_size created_at can_delete]
it { expect(described_class.graphql_name).to eq('ContainerRepositoryTag') }
it { expect(described_class.description).to eq('A tag from a container repository') }
it { expect(described_class).to require_graphql_authorizations(:read_container_image) }
it { expect(described_class).to have_graphql_fields(fields) }
end
...@@ -88,4 +88,10 @@ RSpec.describe GitlabSchema.types['Query'] do ...@@ -88,4 +88,10 @@ RSpec.describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::Ci::RunnerSetupType) is_expected.to have_graphql_type(Types::Ci::RunnerSetupType)
end end
end end
describe 'container_repository field' do
subject { described_class.fields['containerRepository'] }
it { is_expected.to have_graphql_type(Types::ContainerRepositoryDetailsType) }
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'container repository details' do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:container_repository) { create(:container_repository, project: project) }
let(:query) do
graphql_query_for(
'containerRepository',
{ id: container_repository_global_id },
all_graphql_fields_for('ContainerRepositoryDetails')
)
end
let(:user) { project.owner }
let(:variables) { {} }
let(:tags) { %w(latest tag1 tag2 tag3 tag4 tag5) }
let(:container_repository_global_id) { container_repository.to_global_id.to_s }
let(:container_repository_details_response) { graphql_data.dig('containerRepository') }
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: container_repository.path, tags: tags, with_manifest: true)
end
subject { post_graphql(query, current_user: user, variables: variables) }
it_behaves_like 'a working graphql query' do
before do
subject
end
it 'matches the expected schema' do
expect(container_repository_details_response).to match_schema('graphql/container_repository_details')
end
end
context 'with different permissions' do
let_it_be(:user) { create(:user) }
let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') }
where(:project_visibility, :role, :access_granted, :can_delete) do
:private | :maintainer | true | true
:private | :developer | true | true
:private | :reporter | true | false
:private | :guest | false | false
:private | :anonymous | false | false
:public | :maintainer | true | true
:public | :developer | true | true
:public | :reporter | true | false
:public | :guest | true | false
:public | :anonymous | true | false
end
with_them do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false))
project.add_user(user, role) unless role == :anonymous
end
it 'return the proper response' do
subject
if access_granted
expect(tags_response.size).to eq(tags.size)
expect(container_repository_details_response.dig('canDelete')).to eq(can_delete)
else
expect(container_repository_details_response).to eq(nil)
end
end
end
end
context 'limiting the number of tags' do
let(:limit) { 2 }
let(:tags_response) { container_repository_details_response.dig('tags', 'edges') }
let(:variables) do
{ id: container_repository_global_id, n: limit }
end
let(:query) do
<<~GQL
query($id: ID!, $n: Int) {
containerRepository(id: $id) {
tags(first: $n) {
edges {
node {
#{all_graphql_fields_for('ContainerRepositoryTag')}
}
}
}
}
}
GQL
end
it 'only returns n tags' do
subject
expect(tags_response.size).to eq(limit)
end
end
end
...@@ -92,9 +92,9 @@ RSpec.describe 'getting container repositories in a group' do ...@@ -92,9 +92,9 @@ RSpec.describe 'getting container repositories in a group' do
end end
context 'limiting the number of repositories' do context 'limiting the number of repositories' do
let(:issue_limit) { 1 } let(:limit) { 1 }
let(:variables) do let(:variables) do
{ path: group.full_path, n: issue_limit } { path: group.full_path, n: limit }
end end
let(:query) do let(:query) do
...@@ -107,10 +107,10 @@ RSpec.describe 'getting container repositories in a group' do ...@@ -107,10 +107,10 @@ RSpec.describe 'getting container repositories in a group' do
GQL GQL
end end
it 'only returns N issues' do it 'only returns N repositories' do
subject subject
expect(container_repositories_response.size).to eq(issue_limit) expect(container_repositories_response.size).to eq(limit)
end end
end end
......
...@@ -87,9 +87,9 @@ RSpec.describe 'getting container repositories in a project' do ...@@ -87,9 +87,9 @@ RSpec.describe 'getting container repositories in a project' do
end end
context 'limiting the number of repositories' do context 'limiting the number of repositories' do
let(:issue_limit) { 1 } let(:limit) { 1 }
let(:variables) do let(:variables) do
{ path: project.full_path, n: issue_limit } { path: project.full_path, n: limit }
end end
let(:query) do let(:query) do
...@@ -102,10 +102,10 @@ RSpec.describe 'getting container repositories in a project' do ...@@ -102,10 +102,10 @@ RSpec.describe 'getting container repositories in a project' do
GQL GQL
end end
it 'only returns N issues' do it 'only returns N repositories' do
subject subject
expect(container_repositories_response.size).to eq(issue_limit) expect(container_repositories_response.size).to eq(limit)
end end
end end
......
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