Commit 7a4612cd authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '10io-graphql-destroy-mutation-for-container-repositories' into 'master'

Add container repository destroy GraphQL mutation

See merge request gitlab-org/gitlab!47175
parents cc608759 65566bbc
# frozen_string_literal: true
module Mutations
module PackageEventable
extend ActiveSupport::Concern
private
def track_event(event, scope)
::Packages::CreateEventService.new(nil, current_user, event_name: event, scope: scope).execute
::Gitlab::Tracking.event(event.to_s, scope.to_s)
end
end
end
# frozen_string_literal: true
module Mutations
module ContainerRepositories
class Destroy < Mutations::BaseMutation
include ::Mutations::PackageEventable
graphql_name 'DestroyContainerRepository'
authorize :destroy_container_image
argument :id,
::Types::GlobalIDType[::ContainerRepository],
required: true,
description: 'ID of the container repository.'
field :container_repository,
Types::ContainerRepositoryType,
null: false,
description: 'The container repository policy after scheduling the deletion.'
def resolve(id:)
container_repository = authorized_find!(id: id)
container_repository.delete_scheduled!
DeleteContainerRepositoryWorker.perform_async(current_user.id, container_repository.id)
track_event(:delete_repository, :container)
{
container_repository: container_repository,
errors: []
}
end
private
def find_object(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
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Resolvers module Resolvers
class ContainerRepositoriesResolver < BaseResolver class ContainerRepositoriesResolver < BaseResolver
include ::Mutations::PackageEventable
type Types::ContainerRepositoryType, null: true type Types::ContainerRepositoryType, null: true
argument :name, GraphQL::STRING_TYPE, argument :name, GraphQL::STRING_TYPE,
...@@ -13,12 +15,5 @@ module Resolvers ...@@ -13,12 +15,5 @@ module Resolvers
.execute .execute
.tap { track_event(:list_repositories, :container) } .tap { track_event(:list_repositories, :container) }
end end
private
def track_event(event, scope)
::Packages::CreateEventService.new(nil, current_user, event_name: event, scope: scope).execute
::Gitlab::Tracking.event(event.to_s, scope.to_s)
end
end end
end end
...@@ -82,6 +82,7 @@ module Types ...@@ -82,6 +82,7 @@ module Types
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Move mount_mutation Mutations::DesignManagement::Move
mount_mutation Mutations::ContainerExpirationPolicies::Update mount_mutation Mutations::ContainerExpirationPolicies::Update
mount_mutation Mutations::ContainerRepositories::Destroy
mount_mutation Mutations::Ci::PipelineCancel mount_mutation Mutations::Ci::PipelineCancel
mount_mutation Mutations::Ci::PipelineDestroy mount_mutation Mutations::Ci::PipelineDestroy
mount_mutation Mutations::Ci::PipelineRetry mount_mutation Mutations::Ci::PipelineRetry
......
---
title: Add container repository destroy GraphQL mutation
merge_request: 47175
author:
type: added
...@@ -6093,6 +6093,41 @@ type DestroyBoardPayload { ...@@ -6093,6 +6093,41 @@ type DestroyBoardPayload {
errors: [String!]! errors: [String!]!
} }
"""
Autogenerated input type of DestroyContainerRepository
"""
input DestroyContainerRepositoryInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
ID of the container repository.
"""
id: ContainerRepositoryID!
}
"""
Autogenerated return type of DestroyContainerRepository
"""
type DestroyContainerRepositoryPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The container repository policy after scheduling the deletion.
"""
containerRepository: ContainerRepository!
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
""" """
Autogenerated input type of DestroyNote Autogenerated input type of DestroyNote
""" """
...@@ -13470,6 +13505,7 @@ type Mutation { ...@@ -13470,6 +13505,7 @@ type Mutation {
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyBoard(input: DestroyBoardInput!): DestroyBoardPayload destroyBoard(input: DestroyBoardInput!): DestroyBoardPayload
destroyBoardList(input: DestroyBoardListInput!): DestroyBoardListPayload destroyBoardList(input: DestroyBoardListInput!): DestroyBoardListPayload
destroyContainerRepository(input: DestroyContainerRepositoryInput!): DestroyContainerRepositoryPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
......
...@@ -16765,6 +16765,112 @@ ...@@ -16765,6 +16765,112 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "DestroyContainerRepositoryInput",
"description": "Autogenerated input type of DestroyContainerRepository",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "ID of the container repository.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ContainerRepositoryID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DestroyContainerRepositoryPayload",
"description": "Autogenerated return type of DestroyContainerRepository",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "containerRepository",
"description": "The container repository policy after scheduling the deletion.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ContainerRepository",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "DestroyNoteInput", "name": "DestroyNoteInput",
...@@ -38177,6 +38283,33 @@ ...@@ -38177,6 +38283,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "destroyContainerRepository",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DestroyContainerRepositoryInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DestroyContainerRepositoryPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "destroyNote", "name": "destroyNote",
"description": null, "description": null,
...@@ -1002,6 +1002,16 @@ Autogenerated return type of DestroyBoard. ...@@ -1002,6 +1002,16 @@ Autogenerated return type of DestroyBoard.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DestroyContainerRepositoryPayload
Autogenerated return type of DestroyContainerRepository.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `containerRepository` | ContainerRepository! | The container repository policy after scheduling the deletion. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DestroyNotePayload ### DestroyNotePayload
Autogenerated return type of DestroyNote. Autogenerated return type of DestroyNote.
......
{
"type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete"],
"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"
}
}
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::ContainerRepositories::Destroy do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:container_repository) { create(:container_repository) }
let_it_be(:user) { create(:user) }
let(:project) { container_repository.project }
let(:id) { container_repository.to_global_id.to_s }
specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
describe '#resolve' do
subject do
described_class.new(object: nil, context: { current_user: user }, field: nil)
.resolve(id: id)
end
shared_examples 'destroying the container repository' do
it 'destroys the container repistory' do
expect(::Packages::CreateEventService)
.to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
expect(DeleteContainerRepositoryWorker)
.to receive(:perform_async).with(user.id, container_repository.id)
expect { subject }.to change { ::Packages::Event.count }.by(1)
expect(container_repository.reload.delete_scheduled?).to be true
end
end
shared_examples 'denying access to container respository' do
it 'raises an error' do
expect(DeleteContainerRepositoryWorker)
.not_to receive(:perform_async).with(user.id, container_repository.id)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'with valid id' do
where(:user_role, :shared_examples_name) do
:maintainer | 'destroying the container repository'
:developer | 'destroying the container repository'
:reporter | 'denying access to container respository'
:guest | 'denying access to container respository'
:anonymous | 'denying access to container respository'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'with invalid id' do
let(:id) { 'gid://gitlab/ContainerRepository/5555' }
it_behaves_like 'denying access to container respository'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Destroying a container repository' do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
let_it_be_with_reload(:container_repository) { create(:container_repository) }
let_it_be(:user) { create(:user) }
let(:project) { container_repository.project }
let(:id) { container_repository.to_global_id.to_s }
let(:query) do
<<~GQL
containerRepository {
#{all_graphql_fields_for('ContainerRepository')}
}
errors
GQL
end
let(:params) { { id: container_repository.to_global_id.to_s } }
let(:mutation) { graphql_mutation(:destroy_container_repository, params, query) }
let(:mutation_response) { graphql_mutation_response(:destroyContainerRepository) }
let(:container_repository_mutation_response) { mutation_response['containerRepository'] }
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(tags: %w[a b c])
end
shared_examples 'destroying the container repository' do
it 'destroy the container repository' do
expect(::Packages::CreateEventService)
.to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
expect(DeleteContainerRepositoryWorker)
.to receive(:perform_async).with(user.id, container_repository.id)
expect { subject }.to change { ::Packages::Event.count }.by(1)
expect(container_repository_mutation_response).to match_schema('graphql/container_repository')
expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED')
end
it_behaves_like 'returning response status', :success
end
shared_examples 'denying the mutation request' do
it 'does not destroy the container repository' do
expect(DeleteContainerRepositoryWorker)
.not_to receive(:perform_async).with(user.id, container_repository.id)
expect { subject }.not_to change { ::Packages::Event.count }
expect(mutation_response).to be_nil
end
it_behaves_like 'returning response status', :success
end
describe 'post graphql mutation' do
subject { post_graphql_mutation(mutation, current_user: user) }
context 'with valid id' do
where(:user_role, :shared_examples_name) do
:maintainer | 'destroying the container repository'
:developer | 'destroying the container repository'
:reporter | 'denying the mutation request'
:guest | 'denying the mutation request'
:anonymous | 'denying the mutation request'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'with invalid id' do
let(:params) { { id: 'gid://gitlab/ContainerRepository/5555' } }
it_behaves_like 'denying the mutation request'
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