Commit 97eab290 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'lm-be-pipeline-mutations' into 'master'

GraphQL: Pipeline mutations for retry, cancel, and destroy

See merge request gitlab-org/gitlab!39780
parents 8e80c580 d1cbcaf1
# frozen_string_literal: true
module Mutations
module Ci
class Base < BaseMutation
argument :id, ::Types::GlobalIDType[::Ci::Pipeline],
required: true,
description: 'The id of the pipeline to mutate'
private
def find_object(id:)
GlobalID::Locator.locate(id)
end
end
end
end
# frozen_string_literal: true
module Mutations
module Ci
class PipelineCancel < BaseMutation
graphql_name 'PipelineCancel'
authorize :update_pipeline
def resolve
result = ::Ci::CancelUserPipelinesService.new.execute(current_user)
{
success: result.success?,
errors: [result&.message]
}
end
end
end
end
# frozen_string_literal: true
module Mutations
module Ci
class PipelineDestroy < Base
graphql_name 'PipelineDestroy'
authorize :destroy_pipeline
def resolve(id:)
pipeline = authorized_find!(id: id)
project = pipeline.project
result = ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
{
success: result.success?,
errors: result.errors
}
end
end
end
end
# frozen_string_literal: true
module Mutations
module Ci
class PipelineRetry < Base
graphql_name 'PipelineRetry'
field :pipeline,
Types::Ci::PipelineType,
null: true,
description: 'The pipeline after mutation'
authorize :update_pipeline
def resolve(id:)
pipeline = authorized_find!(id: id)
project = pipeline.project
::Ci::RetryPipelineService.new(project, current_user).execute(pipeline)
{
pipeline: pipeline,
errors: errors_on_object(pipeline)
}
end
end
end
end
...@@ -62,6 +62,9 @@ module Types ...@@ -62,6 +62,9 @@ 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::Ci::PipelineCancel
mount_mutation Mutations::Ci::PipelineDestroy
mount_mutation Mutations::Ci::PipelineRetry
end end
end end
......
...@@ -7,6 +7,10 @@ module Ci ...@@ -7,6 +7,10 @@ module Ci
# https://gitlab.com/gitlab-org/gitlab/issues/32332 # https://gitlab.com/gitlab-org/gitlab/issues/32332
def execute(user) def execute(user)
user.pipelines.cancelable.find_each(&:cancel_running) user.pipelines.cancelable.find_each(&:cancel_running)
ServiceResponse.success(message: 'Pipeline canceled')
rescue ActiveRecord::StaleObjectError
ServiceResponse.error(message: 'Error canceling pipeline')
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
......
...@@ -8,6 +8,10 @@ module Ci ...@@ -8,6 +8,10 @@ module Ci
Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true) Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true)
pipeline.destroy! pipeline.destroy!
ServiceResponse.success(message: 'Pipeline not found')
rescue ActiveRecord::RecordNotFound
ServiceResponse.error(message: 'Pipeline not found')
end end
end end
end end
---
title: 'GraphQL: Pipeline mutations for retry, cancel, and destroy'
merge_request: 39780
author:
type: added
...@@ -1596,6 +1596,11 @@ type CiJobEdge { ...@@ -1596,6 +1596,11 @@ type CiJobEdge {
node: CiJob node: CiJob
} }
"""
Identifier of Ci::Pipeline
"""
scalar CiPipelineID
type CiStage { type CiStage {
""" """
Group of jobs for the stage Group of jobs for the stage
...@@ -9795,6 +9800,9 @@ type Mutation { ...@@ -9795,6 +9800,9 @@ type Mutation {
""" """
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2") removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2")
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
runDastScan(input: RunDASTScanInput!): RunDASTScanPayload runDastScan(input: RunDASTScanInput!): RunDASTScanPayload
...@@ -10589,6 +10597,31 @@ type Pipeline { ...@@ -10589,6 +10597,31 @@ type Pipeline {
userPermissions: PipelinePermissions! userPermissions: PipelinePermissions!
} }
"""
Autogenerated input type of PipelineCancel
"""
input PipelineCancelInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
}
"""
Autogenerated return type of PipelineCancel
"""
type PipelineCancelPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
enum PipelineConfigSourceEnum { enum PipelineConfigSourceEnum {
AUTO_DEVOPS_SOURCE AUTO_DEVOPS_SOURCE
BRIDGE_SOURCE BRIDGE_SOURCE
...@@ -10625,6 +10658,36 @@ type PipelineConnection { ...@@ -10625,6 +10658,36 @@ type PipelineConnection {
pageInfo: PageInfo! pageInfo: PageInfo!
} }
"""
Autogenerated input type of PipelineDestroy
"""
input PipelineDestroyInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The id of the pipeline to mutate
"""
id: CiPipelineID!
}
"""
Autogenerated return type of PipelineDestroy
"""
type PipelineDestroyPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
""" """
An edge in a connection. An edge in a connection.
""" """
...@@ -10657,6 +10720,41 @@ type PipelinePermissions { ...@@ -10657,6 +10720,41 @@ type PipelinePermissions {
updatePipeline: Boolean! updatePipeline: Boolean!
} }
"""
Autogenerated input type of PipelineRetry
"""
input PipelineRetryInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The id of the pipeline to mutate
"""
id: CiPipelineID!
}
"""
Autogenerated return type of PipelineRetry
"""
type PipelineRetryPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The pipeline after mutation
"""
pipeline: Pipeline
}
enum PipelineStatusEnum { enum PipelineStatusEnum {
CANCELED CANCELED
CREATED CREATED
......
...@@ -1635,6 +1635,24 @@ Information about pagination in a connection. ...@@ -1635,6 +1635,24 @@ Information about pagination in a connection.
| `user` | User | Pipeline user | | `user` | User | Pipeline user |
| `userPermissions` | PipelinePermissions! | Permissions for the current user on the resource | | `userPermissions` | PipelinePermissions! | Permissions for the current user on the resource |
## PipelineCancelPayload
Autogenerated return type of PipelineCancel
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## PipelineDestroyPayload
Autogenerated return type of PipelineDestroy
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## PipelinePermissions ## PipelinePermissions
| Name | Type | Description | | Name | Type | Description |
...@@ -1643,6 +1661,16 @@ Information about pagination in a connection. ...@@ -1643,6 +1661,16 @@ Information about pagination in a connection.
| `destroyPipeline` | Boolean! | Indicates the user can perform `destroy_pipeline` on this resource | | `destroyPipeline` | Boolean! | Indicates the user can perform `destroy_pipeline` on this resource |
| `updatePipeline` | Boolean! | Indicates the user can perform `update_pipeline` on this resource | | `updatePipeline` | Boolean! | Indicates the user can perform `update_pipeline` on this resource |
## PipelineRetryPayload
Autogenerated return type of PipelineRetry
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipeline` | Pipeline | The pipeline after mutation |
## Project ## Project
| Name | Type | Description | | Name | Type | Description |
......
...@@ -29,7 +29,7 @@ module Mutations ...@@ -29,7 +29,7 @@ module Mutations
dast_site_profile = find_dast_site_profile(project: project, dast_site_profile_id: dast_site_profile_id) dast_site_profile = find_dast_site_profile(project: project, dast_site_profile_id: dast_site_profile_id)
dast_site = dast_site_profile.dast_site dast_site = dast_site_profile.dast_site
service = Ci::RunDastScanService.new(project, current_user) service = ::Ci::RunDastScanService.new(project, current_user)
result = service.execute(branch: project.default_branch, target_url: dast_site.url) result = service.execute(branch: project.default_branch, target_url: dast_site.url)
if result.success? if result.success?
......
...@@ -32,7 +32,7 @@ module Mutations ...@@ -32,7 +32,7 @@ module Mutations
def resolve(project_path:, target_url:, branch:, scan_type:) def resolve(project_path:, target_url:, branch:, scan_type:)
project = authorized_find!(full_path: project_path) project = authorized_find!(full_path: project_path)
service = Ci::RunDastScanService.new(project, current_user) service = ::Ci::RunDastScanService.new(project, current_user)
result = service.execute(branch: branch, target_url: target_url) result = service.execute(branch: branch, target_url: target_url)
if result.success? if result.success?
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'PipelineCancel' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, :running, project: project, user: user) }
let(:mutation) { graphql_mutation(:pipeline_cancel, {}, 'errors') }
let(:mutation_response) { graphql_mutation_response(:pipeline_cancel) }
before_all do
project.add_maintainer(user)
end
it 'reports the service-level error' do
service = double(execute: ServiceResponse.error(message: 'Error canceling pipeline'))
allow(::Ci::CancelUserPipelinesService).to receive(:new).and_return(service)
post_graphql_mutation(mutation, current_user: create(:user))
expect(mutation_response).to include('errors' => ['Error canceling pipeline'])
end
it 'does not change any pipelines not owned by the current user' do
build = create(:ci_build, :running, pipeline: pipeline)
post_graphql_mutation(mutation, current_user: create(:user))
expect(build).not_to be_canceled
end
it "cancels all of the current user's cancelable pipelines" do
build = create(:ci_build, :running, pipeline: pipeline)
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(build.reload).to be_canceled
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'PipelineDestroy' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.owner }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, user: user) }
let(:mutation) do
variables = {
id: pipeline.to_global_id.to_s
}
graphql_mutation(:pipeline_destroy, variables, 'errors')
end
it 'returns an error if the user is not allowed to destroy the pipeline' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'destroys a pipeline' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'PipelineRetry' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
let(:mutation) do
variables = {
id: pipeline.to_global_id.to_s
}
graphql_mutation(:pipeline_retry, variables,
<<-QL
errors
pipeline {
id
}
QL
)
end
let(:mutation_response) { graphql_mutation_response(:pipeline_retry) }
before_all do
project.add_maintainer(user)
end
it 'returns an error if the user is not allowed to retry the pipeline' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'retries a pipeline' do
pipeline_id = ::Gitlab::GlobalId.build(pipeline, id: pipeline.id).to_s
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['pipeline']['id']).to eq(pipeline_id)
end
end
...@@ -19,5 +19,17 @@ RSpec.describe Ci::CancelUserPipelinesService do ...@@ -19,5 +19,17 @@ RSpec.describe Ci::CancelUserPipelinesService do
expect(build.reload).to be_canceled expect(build.reload).to be_canceled
end end
end end
context 'when an error ocurrs' do
it 'raises a service level error' do
service = double(execute: ServiceResponse.error(message: 'Error canceling pipeline'))
allow(::Ci::CancelUserPipelinesService).to receive(:new).and_return(service)
result = subject
expect(result).to be_a(ServiceResponse)
expect(result).to be_error
end
end
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