Commit 1a78d2ef authored by Mikołaj Wawrzyniak's avatar Mikołaj Wawrzyniak

Merge branch 'dep-approval-multi-access-levels-api-interface' into 'master'

Implement API interface for Deployment Approval Rules

See merge request gitlab-org/gitlab!84692
parents 2d446d4d 51c52f24
......@@ -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