Commit ba1be7e3 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'ali/deployment-approvals-api' into 'master'

Deployment Approvals API

See merge request gitlab-org/gitlab!76689
parents dbac37d4 1badc95c
...@@ -375,3 +375,38 @@ It supports the same parameters as the [Merge Requests API](merge_requests.md#li ...@@ -375,3 +375,38 @@ It supports the same parameters as the [Merge Requests API](merge_requests.md#li
```shell ```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/42/merge_requests" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/42/merge_requests"
``` ```
## Approve or Reject a blocked Deployment
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/343864) in GitLab 14.7 [with a flag](../administration/feature_flags.md) named `deployment_approvals`. Disabled by default. This feature is not ready for production use.
```plaintext
POST /projects/:id/deployments/:deployment_id/approval
```
| Attribute | Type | Required | Description |
|-----------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
| `deployment_id` | integer | yes | The ID of the deployment. |
| `status` | string | yes | The status of the approval (either `approved` or `rejected`). |
```shell
curl --data "status=approved" \
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/1/approval"
```
Example response:
```json
{
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"status": "approved"
}
```
# frozen_string_literal: true
module API
module Entities
module Deployments
class Approval < Grape::Entity
expose :user, using: Entities::UserBasic
expose :status
end
end
end
end
# frozen_string_literal: true
module EE
module API
module Deployments
extend ActiveSupport::Concern
prepended do
params do
requires :id, type: String, desc: 'The project ID'
end
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Approve or reject a blocked deployment' do
detail 'This feature is gated behind the :deployment_approvals feature flag.'
success ::API::Entities::Deployments::Approval
end
params do
requires :deployment_id, type: String, desc: 'The Deployment ID'
requires :status, type: String, values: ::Deployments::Approval.statuses.keys
end
post ':id/deployments/:deployment_id/approval' do
deployment = user_project.deployments.find(params[:deployment_id])
result = ::Deployments::ApprovalService.new(user_project, current_user)
.execute(deployment, params[:status])
if result[:status] == :success
present(result[:approval], with: ::API::Entities::Deployments::Approval, current_user: current_user)
else
render_api_error!(result[:message], 400)
end
end
end
end
end
end
end
{
"type": "object",
"required": [
"status",
"user"
],
"properties": {
"status": {
"type": "string"
},
"user": {
"type": "object",
"items": {
"$ref": "../../../../../../../spec/fixtures/api/schemas/public_api/v4/user/basic.json"
}
}
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Deployments::Approval do
subject { described_class.new(approval).as_json }
let(:approval) { build(:deployment_approval) }
it 'exposes correct attributes' do
expect(subject.keys).to contain_exactly(:user, :status)
end
end
...@@ -180,4 +180,84 @@ RSpec.describe API::Deployments do ...@@ -180,4 +180,84 @@ RSpec.describe API::Deployments do
end end
end end
end end
describe 'POST /projects/:id/deployments/:deployment_id/approval' do
shared_examples_for 'not created' do |approval_status: 'approved', response_status:, message:|
it 'does not create an approval' do
expect { post(api(path, user), params: { status: approval_status }) }.not_to change { Deployments::Approval.count }
expect(response).to have_gitlab_http_status(response_status)
expect(response.body).to include(message)
end
end
let(:deployment) { create(:deployment, :blocked, project: project, environment: environment, deployable: create(:ci_build, :manual, project: project)) }
let(:path) { "/projects/#{project.id}/deployments/#{deployment.id}/approval" }
before do
create(:protected_environment, :maintainers_can_deploy, project: environment.project, name: environment.name, required_approval_count: 1)
end
context 'when user is authorized to read project' do
before do
project.add_developer(user)
end
context 'and Protected Environments feature is available' do
before do
stub_licensed_features(protected_environments: true)
end
context 'and user is authorized to update deployment' do
before do
project.add_maintainer(user)
end
it 'creates an approval' do
expect { post(api(path, user), params: { status: 'approved' }) }.to change { Deployments::Approval.count }.by(1)
expect(response).to have_gitlab_http_status(:success)
expect(response).to match_response_schema('public_api/v4/deployment_approval', dir: 'ee')
expect(json_response['status']).to eq('approved')
expect(json_response.dig('user', 'id')).to eq(user.id)
end
end
context 'and user is not authorized to update deployment' do
include_examples 'not created', response_status: :bad_request, message: 'You do not have permission to approve or reject this deployment'
end
context 'with an invalid status' do
include_examples 'not created', approval_status: 'foo', response_status: :bad_request, message: 'status does not have a valid value'
end
context 'with a deployment that does not belong to the project' do
let(:other_project) { create(:project, :repository) }
let(:user) { other_project.creator }
let(:path) { "/projects/#{other_project.id}/deployments/#{deployment.id}/approval" }
include_examples 'not created', response_status: :not_found, message: '404 Not found'
end
context 'with a deployment that does not exist' do
let(:path) { "/projects/#{project.id}/deployments/0/approval" }
include_examples 'not created', response_status: :not_found, message: '404 Not found'
end
end
context 'when Protected Environments feature is not available' do
before do
stub_licensed_features(protected_environments: false)
end
include_examples 'not created', response_status: :bad_request, message: 'This environment is not protected'
end
end
context 'when user is not authorized to read project' do
include_examples 'not created', response_status: :not_found, message: '404 Project Not Found'
end
end
end end
...@@ -165,3 +165,5 @@ module API ...@@ -165,3 +165,5 @@ module API
end end
end end
end end
API::Deployments.prepend_mod
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