Commit 51c52f24 authored by Shinya Maeda's avatar Shinya Maeda

Implement API interface for Deployment Approval Rules

This commit implements the API interface for the Deployment
Approval Rules.

This change is still behind deployment_approval_rules FF.
parent 7482ed82
......@@ -265,7 +265,7 @@ Example response:
}
```
Deployments created by users on GitLab Premium or higher include the `approvals` and `pending_approval_count` properties:
When the [unified approval setting](../ci/environments/deployment_approvals.md#unified-approval-setting) is configured, deployments created by users on GitLab Premium or higher include the `approvals` and `pending_approval_count` properties:
```json
{
......@@ -290,6 +290,48 @@ Deployments created by users on GitLab Premium or higher include the `approvals`
}
```
When the [multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) is configured, deployments created by users on GitLab Premium or higher include the `approval_summary` property:
```json
{
"approval_summary": {
"rules": [
{
"user_id": null,
"group_id": 134,
"access_level": null,
"access_level_description": "qa-group",
"required_approvals": 1,
"deployment_approvals": []
},
{
"user_id": null,
"group_id": 135,
"access_level": null,
"access_level_description": "security-group",
"required_approvals": 2,
"deployment_approvals": [
{
"user": {
"id": 100,
"username": "security-user-1",
"name": "security user-1",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e130fcd3a1681f41a3de69d10841afa9?s=80&d=identicon",
"web_url": "http://localhost:3000/security-user-1"
},
"status": "approved",
"created_at": "2022-04-11T03:37:03.058Z",
"comment": null
}
]
}
]
}
...
}
```
## Create a deployment
```plaintext
......@@ -455,9 +497,10 @@ POST /projects/:id/deployments/:deployment_id/approval
| `deployment_id` | integer | yes | The ID of the deployment. |
| `status` | string | yes | The status of the approval (either `approved` or `rejected`). |
| `comment` | string | no | A comment to go with the approval |
| `represented_as`| string | no | The name of the User/Group/Role to use for the approval, when the user belongs to [multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules). |
```shell
curl --data "status=approved&comment=Looks good to me" \
curl --data "status=approved&comment=Looks good to me&represented_as=security" \
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/1/approval"
```
......@@ -466,12 +509,12 @@ Example response:
```json
{
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"id": 100,
"username": "security-user-1",
"name": "security user-1",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
"avatar_url": "https://www.gravatar.com/avatar/e130fcd3a1681f41a3de69d10841afa9?s=80&d=identicon",
"web_url": "http://localhost:3000/security-user-1"
},
"status": "approved",
"created_at": "2022-02-24T20:22:30.097Z",
......
......@@ -107,6 +107,7 @@ POST /groups/:id/protected_environments
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. |
| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. This is part of Deployment Approvals, which isn't yet available for use. For details, see [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/343864). |
| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. You can also specify the number of required approvals from the specified entity with `required_approvals` field. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. |
The assignable `user_id` are the users who belong to the given group with the Maintainer role (or above).
The assignable `group_id` are the sub-groups under the given group.
......
......@@ -99,7 +99,7 @@ POST /projects/:id/protected_environments
```shell
curl --header 'Content-Type: application/json' --request POST \
--data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}]}' \
--data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}], "approval_rules": [{"group_id": 134}, {"group_id": 135, "required_approvals": 2}]}' \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/22034114/protected_environments"
```
......@@ -110,8 +110,9 @@ curl --header 'Content-Type: application/json' --request POST \
| `name` | string | yes | The name of the environment. |
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. |
| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. This is part of Deployment Approvals, which isn't yet available for use. For details, see [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/343864). |
| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. |
Elements in the `deploy_access_levels` array should be one of `user_id`, `group_id` or
Elements in the `deploy_access_levels` and `approval_rules` array should be one of `user_id`, `group_id` or
`access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or
`{access_level: integer}`.
Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md).
......@@ -129,7 +130,23 @@ Example response:
"group_id": 9899826
}
],
"required_approval_count": 0
"required_approval_count": 0,
"approval_rules": [
{
"user_id": null,
"group_id": 134,
"access_level": null,
"access_level_description": "qa-group",
"required_approvals": 1
},
{
"user_id": null,
"group_id": 135,
"access_level": null,
"access_level_description": "security-group",
"required_approvals": 2
}
]
}
```
......
......@@ -52,6 +52,19 @@ Example:
### Require approvals for a protected environment
There are two ways to configure the approval requirements:
- [Unified approval setting](#unified-approval-setting) ... You can define who can execute **and** approve deployments.
This is useful when there is no separation of duties between executors and approvers in your oraganization.
- [Multiple approval rules](#multiple-approval-rules) ... You can define who can execute **or** approve deployments.
This is useful when there is a separation of duties between executors and approvers in your oraganization.
NOTE:
Multiple approval rules is a more flexible option than the unified approval setting, thus both configurations shouldn't
co-exist and multiple approval rules takes the precedence over the unified approval setting if it happens.
#### Unified approval setting
NOTE:
At this time, it is not possible to require approvals for an existing protected environment. The workaround is to unprotect the environment and configure approvals when re-protecting the environment.
......@@ -77,6 +90,35 @@ NOTE:
To protect, update, or unprotect an environment, you must have at least the
Maintainer role.
#### Multiple approval rules
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345678) in GitLab 14.10 with a flag named `deployment_approval_rules`. Disabled by default.
1. Using the [REST API](../../api/group_protected_environments.md#protect-an-environment).
1. `deploy_access_levels` represents which entity can execute the deployment job.
1. `approval_rules` represents which entity can approve the deployment job.
After this is configured, all jobs deploying to this environment automatically go into a blocked state and wait for approvals before running. Ensure that the number of required approvals is less than the number of users allowed to deploy.
Example:
```shell
curl --header 'Content-Type: application/json' --request POST \
--data '{"name": "production", "deploy_access_levels": [{"group_id": 138}], "approval_rules": [{"group_id": 134}, {"group_id": 135, "required_approvals": 2}]}' \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/128/protected_environments"
```
With this setup:
- The operator group (`group_id: 138`) has permission to execute the deployment jobs to the `production` environment in the organization (`group_id: 128`).
- The QA tester group (`group_id: 134`) and security group (`group_id: 135`) have permission to approve the deployment jobs to the `production` environment in the organization (`group_id: 128`).
- Unless two approvals from security group and one approval from QA tester group have been collected, the operator group can't execute the deployment jobs.
NOTE:
To protect, update, or unprotect an environment, you must have at least the
Maintainer role.
## Approve or reject a deployment
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342180/) in GitLab 14.9
......@@ -99,6 +141,10 @@ To approve or reject a deployment to a protected environment using the UI:
1. In the deployment's row, select **Approval options** (**{thumb-up}**).
1. Select **Approve** or **Reject**.
NOTE:
This feature might not work as expected when [Multiple approval rules](#multiple-approval-rules) is configured.
See the [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/355708) for planned improvement.
### Approve or reject a deployment using the API
Prerequisites:
......@@ -127,11 +173,14 @@ curl --data "status=approved&comment=Looks good to me" \
### Using the API
Use the [Deployments API](../../api/deployments.md) to see deployments.
Use the [Deployments API](../../api/deployments.md#get-a-specific-deployment) to see deployments.
- The `status` field indicates if a deployment is blocked.
- The `pending_approval_count` field indicates how many approvals are remaining to run a deployment.
- The `approvals` field contains the deployment's approvals.
- When the [unified approval setting](#unified-approval-setting) is configured:
- The `pending_approval_count` field indicates how many approvals are remaining to run a deployment.
- The `approvals` field contains the deployment's approvals.
- When the [multiple approval rules](#multiple-approval-rules) is configured:
- The `approval_summary` field contains the current approval status per rule.
## Related features
......
......@@ -9,6 +9,7 @@ class ProtectedEnvironment < ApplicationRecord
has_many :approval_rules, class_name: 'ProtectedEnvironments::ApprovalRule', inverse_of: :protected_environment
accepts_nested_attributes_for :deploy_access_levels, allow_destroy: true
accepts_nested_attributes_for :approval_rules, allow_destroy: true
validates :deploy_access_levels, length: { minimum: 1 }
validates :name, presence: true
......
......@@ -11,5 +11,7 @@ module ProtectedEnvironments
has_many :deployment_approvals, class_name: 'Deployments::Approval', inverse_of: :approval_rule
validates :access_level, allow_blank: true, inclusion: { in: ALLOWED_ACCESS_LEVELS }
validates :required_approvals,
numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5 }
end
end
......@@ -8,6 +8,9 @@ module EE
def process(build)
if build.persisted_environment.try(:needs_approval?)
build.run_after_commit { |build| build.deployment&.block! }
# To populate the deployment job as manually executable (i.e. `Ci::Build#playable?`),
# we have to set `manual` to `ci_builds.when` as well as `ci_builds.status`.
build.when = 'manual' if ::Feature.enabled?(:deployment_approval_rules, build.project, default_enabled: :yaml)
return build.actionize!
end
......
......@@ -3,18 +3,23 @@ module ProtectedEnvironments
class BaseService < ::BaseContainerService
include Gitlab::Utils::StrongMemoize
SANITIZABLE_KEYS = %i[deploy_access_levels_attributes approval_rules_attributes].freeze
protected
def sanitized_params
params.dup.tap do |sanitized_params|
sanitized_params[:deploy_access_levels_attributes] =
filter_valid_deploy_access_level_attributes(sanitized_params[:deploy_access_levels_attributes])
SANITIZABLE_KEYS.each do |key|
next unless sanitized_params.has_key?(key)
sanitized_params[key] = filter_valid_authorizable_attributes(sanitized_params[key])
end
end
end
private
def filter_valid_deploy_access_level_attributes(attributes)
def filter_valid_authorizable_attributes(attributes)
return unless attributes
attributes.select { |attribute| valid_attribute?(attribute) }
......@@ -51,7 +56,7 @@ module ProtectedEnvironments
def qualified_user_ids
strong_memoize(:qualified_user_ids) do
user_ids = params[:deploy_access_levels_attributes].each.with_object([]) do |attribute, user_ids|
user_ids = all_sanitizable_params.each.with_object([]) do |attribute, user_ids|
user_ids << attribute[:user_id] if attribute[:user_id].present?
user_ids
end
......@@ -64,5 +69,9 @@ module ProtectedEnvironments
end.pluck_user_ids.to_set
end
end
def all_sanitizable_params
params.values_at(*SANITIZABLE_KEYS).flatten.compact
end
end
end
# frozen_string_literal: true
module API
module Entities
module Deployments
class ApprovalSummary < Grape::Entity
expose :rules, using: ::API::Entities::ProtectedEnvironments::ApprovalRuleForSummary
end
end
end
end
# frozen_string_literal: true
module API
module Entities
module ProtectedEnvironments
class ApprovalRule < Grape::Entity
expose :user_id
expose :group_id
expose :access_level
expose :humanize, as: :access_level_description
expose :required_approvals
end
end
end
end
# frozen_string_literal: true
module API
module Entities
module ProtectedEnvironments
class ApprovalRuleForSummary < ApprovalRule
expose :deployment_approvals, using: ::API::Entities::Deployments::Approval
end
end
end
end
......@@ -8,6 +8,18 @@ module API
feature_category :continuous_delivery
helpers do
params :protected_environment_approval_rules do
optional :approval_rules, as: :approval_rules_attributes, type: Array, desc: 'An array of users/groups allowed to approve/reject a deployment' do
optional :access_level, type: Integer, values: ::ProtectedEnvironments::ApprovalRule::ALLOWED_ACCESS_LEVELS
optional :user_id, type: Integer
optional :group_id, type: Integer
optional :required_approvals, type: Integer, default: 1, desc: 'The number of approvals required in this rule'
at_least_one_of :access_level, :user_id, :group_id
end
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
......@@ -58,6 +70,8 @@ module API
optional :user_id, type: Integer
optional :group_id, type: Integer
end
use :protected_environment_approval_rules
end
post ':id/protected_environments' do
protected_environment = user_project.protected_environments.find_by_name(params[:name])
......@@ -145,6 +159,8 @@ module API
optional :user_id, type: Integer
optional :group_id, type: Integer
end
use :protected_environment_approval_rules
end
post ':id/protected_environments' do
protected_environment = user_group.protected_environments.find_by_name(params[:name])
......
......@@ -9,6 +9,7 @@ module EE
prepended do
expose :pending_approval_count
expose :approvals, using: ::API::Entities::Deployments::Approval
expose :approval_summary, using: ::API::Entities::Deployments::ApprovalSummary
end
end
end
......
......@@ -7,6 +7,7 @@ module EE
expose :name
expose :deploy_access_levels, using: ::API::Entities::ProtectedRefAccess
expose :required_approval_count
expose :approval_rules, using: ::API::Entities::ProtectedEnvironments::ApprovalRule
end
end
end
......
......@@ -7,7 +7,8 @@
"properties": {
"name": { "type": "string" },
"deploy_access_levels": { "type": "array", "items": { "$ref": "protected_ref_access.json" } },
"required_approval_count": { "type": "integer" }
"required_approval_count": { "type": "integer" },
"approval_rules": { "type": "array", "items": { "$ref": "protected_environment_approval_rule.json" } }
},
"additionalProperties": false
}
{
"type": "object",
"required": [ "access_level_description" ],
"properties": {
"access_level": { "type": ["integer", "null"] },
"access_level_description": { "type": ["string", "null"] },
"user_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"required_approvals": { "type": ["integer"] }
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Deployments::ApprovalSummary do
subject { described_class.new(approval_summary).as_json }
let(:deployment) { build(:deployment) }
let(:approval_summary) { deployment.approval_summary }
it 'exposes correct attributes' do
expect(subject.keys).to contain_exactly(:rules)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::ProtectedEnvironments::ApprovalRuleForSummary do
subject { described_class.new(approval_rule).as_json }
let(:approval_rule) { build(:protected_environment_approval_rule) }
it 'exposes correct attributes' do
expect(subject.keys).to contain_exactly(:user_id, :group_id, :access_level, :access_level_description,
:required_approvals, :deployment_approvals)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::ProtectedEnvironments::ApprovalRule do
subject { described_class.new(approval_rule).as_json }
let(:approval_rule) { build(:protected_environment_approval_rule) }
it 'exposes correct attributes' do
expect(subject.keys).to contain_exactly(:user_id, :group_id, :access_level, :access_level_description,
:required_approvals)
end
end
......@@ -10,8 +10,9 @@ RSpec.describe ::EE::API::Entities::DeploymentExtended do
before do
stub_licensed_features(protected_environments: true)
create(:protected_environment, project_id: deployment.environment.project_id, name: deployment.environment.name, required_approval_count: 2)
protected_environment = create(:protected_environment, project_id: deployment.environment.project_id, name: deployment.environment.name, required_approval_count: 2)
create(:deployment_approval, :approved, deployment: deployment)
create(:protected_environment_approval_rule, :maintainer_access, protected_environment: protected_environment, required_approvals: 1)
end
it 'includes fields from deployment entity' do
......@@ -26,5 +27,9 @@ RSpec.describe ::EE::API::Entities::DeploymentExtended do
expect(subject[:approvals].length).to eq(1)
expect(subject.dig(:approvals, 0, :status)).to eq("approved")
end
it 'includes approval summary' do
expect(subject[:approval_summary][:rules].first[:required_approvals]).to eq(1)
end
end
end
......@@ -8,4 +8,11 @@ RSpec.describe ProtectedEnvironments::ApprovalRule do
it_behaves_like 'authorizable for protected environments',
factory_name: :protected_environment_approval_rule
describe 'validation' do
it 'has a limit on required_approvals' do
is_expected.to validate_numericality_of(:required_approvals)
.only_integer.is_greater_than_or_equal_to(1).is_less_than_or_equal_to(5)
end
end
end
......@@ -3,16 +3,37 @@
require 'spec_helper'
RSpec.describe API::Deployments do
let_it_be_with_refind(:organization) { create(:group) }
let_it_be_with_refind(:project) { create(:project, :repository, group: organization) }
let(:user) { create(:user) }
let!(:project) { create(:project, :repository) }
let!(:environment) { create(:environment, project: project) }
before do
stub_licensed_features(protected_environments: true)
end
shared_context 'group-level protected environments with multiple approval rules' do
let!(:security_group) { create(:group, name: 'security-group', parent: organization) }
let!(:security_user) { create(:user) }
before do
security_group.add_developer(security_user)
organization.add_reporter(security_user)
end
let!(:group_protected_environment) do
create(:protected_environment, :group_level, group: organization, name: environment.tier)
end
let!(:approval_rule) do
create(:protected_environment_approval_rule, group: security_group,
protected_environment: group_protected_environment, required_approvals: 2)
end
end
describe 'GET /projects/:id/deployments/:id' do
let(:deployment) { create(:deployment, :blocked, project: project) }
let(:deployment) { create(:deployment, :blocked, project: project, environment: environment) }
before do
create(:deployment_approval, :approved, deployment: deployment)
......@@ -25,6 +46,26 @@ RSpec.describe API::Deployments do
expect(response).to have_gitlab_http_status(:success)
expect(response).to match_response_schema('public_api/v4/deployment_extended', dir: 'ee')
end
context 'with multiple approval rules' do
include_context 'group-level protected environments with multiple approval rules'
let!(:deployment_approval) do
create(:deployment_approval, :approved, user: security_user, approval_rule: approval_rule,
deployment: deployment)
end
it 'has approval summary' do
get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['approval_summary']['rules'].count).to eq(1)
expect(json_response['approval_summary']['rules'].first['required_approvals']).to eq(2)
expect(json_response['approval_summary']['rules'].first['deployment_approvals'].count).to eq(1)
expect(json_response['approval_summary']['rules'].first['deployment_approvals'].first["user"]["id"])
.to eq(security_user.id)
end
end
end
describe 'POST /projects/:id/deployments' do
......@@ -283,6 +324,35 @@ RSpec.describe API::Deployments do
end
end
context 'with multiple approval rules' do
include_context 'group-level protected environments with multiple approval rules'
it 'creates an approval' do
expect { post(api(path, security_user), params: { status: 'approved' }) }
.to change { Deployments::Approval.count }.by(1)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['status']).to eq('approved')
end
it 'creates an approval when the user represents the group' do
expect { post(api(path, security_user), params: { status: 'approved', represented_as: 'security' }) }
.to change { Deployments::Approval.count }.by(1)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['status']).to eq('approved')
end
it 'does not create an approval when the user does not represent the group' do
expect { post(api(path, security_user), params: { status: 'approved', represented_as: 'qa' }) }
.not_to change { Deployments::Approval.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include("You don't have permission to review this deployment. " \
"Contact the project or group owner for help.")
end
end
context 'and user is not authorized to update deployment' do
include_examples 'not created', response_status: :bad_request, message: "You don't have permission to review this deployment. Contact the project or group owner for help."
end
......
......@@ -11,9 +11,14 @@ RSpec.describe API::ProtectedEnvironments do
let(:user) { create(:user) }
let(:protected_environment_name) { 'production' }
before do
create(:protected_environment, :maintainers_can_deploy, :project_level, project: project, name: protected_environment_name, required_approval_count: 1)
create(:protected_environment, :maintainers_can_deploy, :group_level, group: group, name: protected_environment_name, required_approval_count: 2)
let!(:project_protected_environment) do
create(:protected_environment, :maintainers_can_deploy, :project_level, project: project,
name: protected_environment_name, required_approval_count: 1)
end
let!(:group_protected_environment) do
create(:protected_environment, :maintainers_can_deploy, :group_level, group: group,
name: protected_environment_name, required_approval_count: 2)
end
shared_examples 'requests for non-maintainers' do
......@@ -67,6 +72,24 @@ RSpec.describe API::ProtectedEnvironments do
expect(json_response['required_approval_count']).to eq(1)
end
context 'with multiple approval rules' do
before do
create(:protected_environment_approval_rule, :maintainer_access,
protected_environment: project_protected_environment, required_approvals: 3)
end
it 'returns the protected environment' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq(protected_environment_name)
expect(json_response['deploy_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
expect(json_response['approval_rules'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
expect(json_response['approval_rules'][0]['required_approvals']).to eq(3)
end
end
context 'when protected environment does not exist' do
let(:requested_environment_name) { 'unknown' }
......@@ -129,6 +152,21 @@ RSpec.describe API::ProtectedEnvironments do
expect(response).to have_gitlab_http_status(:conflict)
end
it 'protects the environment and require approvals' do
deployer = create(:user)
project.add_developer(deployer)
post api_url, params: { name: 'staging', deploy_access_levels: [{ user_id: deployer.id }],
approval_rules: [{ access_level: Gitlab::Access::MAINTAINER }] }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq('staging')
expect(json_response['deploy_access_levels'].first['user_id']).to eq(deployer.id)
expect(json_response['approval_rules'].count).to eq(1)
expect(json_response['approval_rules'].first['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
context 'without deploy_access_levels' do
it_behaves_like '400 response' do
let(:request) { post api_url, params: { name: 'staging' } }
......@@ -212,6 +250,24 @@ RSpec.describe API::ProtectedEnvironments do
expect(json_response['required_approval_count']).to eq(2)
end
context 'with multiple approval rules' do
before do
create(:protected_environment_approval_rule, :maintainer_access,
protected_environment: group_protected_environment, required_approvals: 3)
end
it 'returns the protected environment' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq(protected_environment_name)
expect(json_response['deploy_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
expect(json_response['approval_rules'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
expect(json_response['approval_rules'][0]['required_approvals']).to eq(3)
end
end
context 'when protected environment does not exist' do
let(:requested_environment_name) { 'unknown' }
......@@ -277,6 +333,21 @@ RSpec.describe API::ProtectedEnvironments do
expect(json_response['deploy_access_levels'].first['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
it 'protects the environment and require approvals' do
deployer = create(:user)
project.add_developer(deployer)
post api_url, params: { name: 'staging', deploy_access_levels: [{ access_level: Gitlab::Access::MAINTAINER }],
approval_rules: [{ access_level: Gitlab::Access::DEVELOPER }] }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq('staging')
expect(json_response['deploy_access_levels'].first['access_level']).to eq(Gitlab::Access::MAINTAINER)
expect(json_response['approval_rules'].count).to eq(1)
expect(json_response['approval_rules'].first['access_level']).to eq(Gitlab::Access::DEVELOPER)
end
it 'returns 409 error if environment is already protected' do
deployer = create(:user)
group.add_developer(deployer)
......
......@@ -81,6 +81,20 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
include_examples 'blocked deployment'
it 'sets manual to build.when' do
expect { subject }.to change { ci_build.reload.when }.to('manual')
end
context 'when deployment_approval_rules feature flag is disabled' do
before do
stub_feature_flags(deployment_approval_rules: false)
end
it 'does not set ci_builds.when' do
expect { subject }.not_to change { ci_build.reload.when }
end
end
context 'and the build is schedulable' do
let(:ci_build) { create(:ci_build, :created, :schedulable, environment: environment.name, user: user, project: project) }
......
......@@ -26,6 +26,11 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
{ group_id: group.id },
{ group_id: other_group.id },
{ group_id: child_group.id }
],
approval_rules_attributes: [
{ group_id: group.id },
{ group_id: other_group.id },
{ group_id: child_group.id }
]
}
end
......@@ -35,6 +40,10 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
deploy_access_levels_attributes: [
{ group_id: group.id },
{ group_id: child_group.id }
],
approval_rules_attributes: [
{ group_id: group.id },
{ group_id: child_group.id }
]
)
end
......@@ -48,6 +57,10 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
deploy_access_levels_attributes: [
{ group_id: group.id },
{ group_id: linked_group.id }
],
approval_rules_attributes: [
{ group_id: group.id },
{ group_id: linked_group.id }
]
}
end
......@@ -57,7 +70,11 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
deploy_access_levels_attributes: [
{ group_id: group.id },
{ group_id: linked_group.id }
]
],
approval_rules_attributes: [
{ group_id: group.id },
{ group_id: linked_group.id }
]
)
end
end
......@@ -69,6 +86,11 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
{ group_id: group.id },
{ group_id: other_group.id, '_destroy' => 1 },
{ group_id: child_group.id }
],
approval_rules_attributes: [
{ group_id: group.id },
{ group_id: other_group.id, '_destroy' => 1 },
{ group_id: child_group.id }
]
}
end
......@@ -79,6 +101,11 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
{ group_id: group.id },
{ group_id: other_group.id, '_destroy' => 1 },
{ group_id: child_group.id }
],
approval_rules_attributes: [
{ group_id: group.id },
{ group_id: other_group.id, '_destroy' => 1 },
{ group_id: child_group.id }
]
)
end
......@@ -93,6 +120,12 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
{ user_id: group_developer.id },
{ user_id: other_group_maintainer.id },
{ user_id: child_group_maintainer.id }
],
approval_rules_attributes: [
{ user_id: group_maintainer.id },
{ user_id: group_developer.id },
{ user_id: other_group_maintainer.id },
{ user_id: child_group_maintainer.id }
]
}
end
......@@ -113,6 +146,9 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
is_expected.to eq(
deploy_access_levels_attributes: [
{ user_id: group_maintainer.id }
],
approval_rules_attributes: [
{ user_id: group_maintainer.id }
]
)
end
......@@ -125,6 +161,12 @@ RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
{ user_id: group_developer.id, '_destroy' => 1 },
{ user_id: other_group_maintainer.id, '_destroy' => 1 },
{ user_id: child_group_maintainer.id, '_destroy' => 1 }
],
approval_rules_attributes: [
{ user_id: group_maintainer.id },
{ user_id: group_developer.id, '_destroy' => 1 },
{ user_id: other_group_maintainer.id, '_destroy' => 1 },
{ user_id: child_group_maintainer.id, '_destroy' => 1 }
]
}
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