Skip to content
Snippets Groups Projects
Verified Commit 77831f78 authored by Yorick Peterse's avatar Yorick Peterse
Browse files

Add API for manually creating deployments

This API can be used for manually creating deployments, instead of being
limited to creating deployments using CI pipelines.

As part of these changes, I refactored some parts of the existing
deployments code. This ensures we reuse the same logic for creating
deployments in different places. We also move the deployments service
classes to a Deployments namespace, now that there are two service
classes.

We also make some changes to which deployments are displayed. Prior to
these changes, only deployments that were successful were displayed.
This can get very confusing when using deployments from external
systems, so we now show deployments regardless of their status. The
table to display deployments has also had some style/content changes to
display the deployments data in a more meaningful way.
parent c7c4ac07
No related branches found
No related tags found
1 merge request!17620Add API for manually creating deployments
Pipeline #89222902 passed with warnings
Showing
with 641 additions and 35 deletions
# frozen_string_literal: true
module EE
module UpdateDeploymentService
extend ::Gitlab::Utils::Override
override :execute
def execute
super.tap do |deployment|
deployment.project.repository.log_geo_updated_event
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::Deployments do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository) }
let!(:environment) { create(:environment, project: project) }
before do
stub_licensed_features(protected_environments: true)
end
describe 'POST /projects/:id/deployments' do
context 'when deploying to a protected environment that requires maintainer access' do
before do
create(
:protected_environment,
:maintainers_can_deploy,
project: environment.project,
name: environment.name
)
end
it 'returns a 403 when the user is a developer' do
project.add_developer(user)
post(
api("/projects/#{project.id}/deployments", user),
params: {
environment: environment.name,
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(403)
end
it 'creates the deployment when the user is a maintainer' do
project.add_maintainer(user)
post(
api("/projects/#{project.id}/deployments", user),
params: {
environment: environment.name,
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(201)
end
end
context 'when deploying to a protected environment that requires developer access' do
before do
create(
:protected_environment,
:developers_can_deploy,
project: environment.project,
name: environment.name
)
end
it 'returns a 403 when the user is a guest' do
project.add_guest(user)
post(
api("/projects/#{project.id}/deployments", user),
params: {
environment: environment.name,
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(403)
end
it 'creates the deployment when the user is a developer' do
project.add_developer(user)
post(
api("/projects/#{project.id}/deployments", user),
params: {
environment: environment.name,
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(201)
end
end
end
describe 'PUT /projects/:id/deployments/:deployment_id' do
let(:project) { create(:project) }
let(:deploy) do
create(
:deployment,
:running,
project: project,
deployable: nil,
environment: environment
)
end
context 'when updating a deployment for a protected environment that requires maintainer access' do
before do
create(
:protected_environment,
:maintainers_can_deploy,
project: environment.project,
name: environment.name
)
end
it 'returns a 403 when the user is a developer' do
project.add_developer(user)
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", user),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(403)
end
it 'updates the deployment when the user is a maintainer' do
project.add_maintainer(user)
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", user),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(200)
end
end
context 'when updating a deployment for a protected environment that requires developer access' do
before do
create(
:protected_environment,
:developers_can_deploy,
project: environment.project,
name: environment.name
)
end
it 'returns a 403 when the user is a guest' do
project.add_guest(user)
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", user),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(403)
end
it 'updates the deployment when the user is a developer' do
project.add_developer(user)
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", user),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(200)
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe UpdateDeploymentService do
describe Deployments::AfterCreateService do
include ::EE::GeoHelpers
let(:primary) { create(:geo_node, :primary) }
......
......@@ -42,6 +42,88 @@ class Deployments < Grape::API
present deployment, with: Entities::Deployment
end
desc 'Creates a new deployment' do
detail 'This feature was introduced in GitLab 12.4'
success Entities::Deployment
end
params do
requires :environment,
type: String,
desc: 'The name of the environment to deploy to'
requires :sha,
type: String,
desc: 'The SHA of the commit that was deployed'
requires :ref,
type: String,
desc: 'The name of the branch or tag that was deployed'
requires :tag,
type: Boolean,
desc: 'A boolean indicating if the deployment ran for a tag'
requires :status,
type: String,
desc: 'The status of the deployment',
values: %w[running success failed canceled]
end
post ':id/deployments' do
authorize!(:create_deployment, user_project)
authorize!(:create_environment, user_project)
environment = user_project
.environments
.find_or_create_by_name(params[:environment])
unless environment.persisted?
render_validation_error!(deployment)
end
authorize!(:create_deployment, environment)
service = ::Deployments::CreateService
.new(environment, current_user, declared_params)
deployment = service.execute
if deployment.persisted?
present(deployment, with: Entities::Deployment, current_user: current_user)
else
render_validation_error!(deployment)
end
end
desc 'Updates an existing deployment' do
detail 'This feature was introduced in GitLab 12.4'
success Entities::Deployment
end
params do
requires :status,
type: String,
desc: 'The new status of the deployment',
values: %w[running success failed canceled]
end
put ':id/deployments/:deployment_id' do
authorize!(:read_deployment, user_project)
deployment = user_project.deployments.find(params[:deployment_id])
authorize!(:update_deployment, deployment)
if deployment.deployable
forbidden!('Deployments created using GitLab CI can not be updated using the API')
end
service = ::Deployments::UpdateService.new(deployment, declared_params)
if service.execute
present(deployment, with: Entities::Deployment, current_user: current_user)
else
render_validation_error!(deployment)
end
end
end
end
end
......@@ -12,7 +12,7 @@ def title
def value
strong_memoize(:value) do
query = @project.deployments.where("created_at >= ?", @from)
query = @project.deployments.success.where("created_at >= ?", @from)
query = query.where("created_at <= ?", @to) if @to
query.count
end
......
......@@ -5409,6 +5409,27 @@ msgstr ""
msgid "Deploying to"
msgstr ""
msgid "Deployment|API"
msgstr ""
msgid "Deployment|This deployment was created using the API"
msgstr ""
msgid "Deployment|canceled"
msgstr ""
msgid "Deployment|created"
msgstr ""
msgid "Deployment|failed"
msgstr ""
msgid "Deployment|running"
msgstr ""
msgid "Deployment|success"
msgstr ""
msgid "Deprioritize label"
msgstr ""
......
......@@ -75,15 +75,13 @@
}
end
before do
it 'returns a metrics JSON document' do
expect_next_instance_of(DeploymentMetrics) do |deployment_metrics|
allow(deployment_metrics).to receive(:has_metrics?).and_return(true)
expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics)
end
end
it 'returns a metrics JSON document' do
get :metrics, params: deployment_params(id: deployment.to_param)
expect(response).to be_ok
......@@ -91,6 +89,19 @@
expect(json_response['metrics']).to eq({})
expect(json_response['last_update']).to eq(42)
end
it 'returns a 404 if the deployment failed' do
failed_deployment = create(
:deployment,
:failed,
project: project,
environment: environment
)
get :metrics, params: deployment_params(id: failed_deployment.to_param)
expect(response).to have_gitlab_http_status(404)
end
end
end
end
......
......@@ -66,8 +66,8 @@
create(:deployment, :running, environment: environment, deployable: build)
end
it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
it 'does show deployments' do
expect(page).to have_link("#{build.name} (##{build.id})")
end
end
......@@ -79,8 +79,8 @@
create(:deployment, :failed, environment: environment, deployable: build)
end
it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
it 'does show deployments' do
expect(page).to have_link("#{build.name} (##{build.id})")
end
end
......
......@@ -61,7 +61,7 @@
"type": "array",
"items": { "$ref": "job/job.json" }
},
"status": { "type": "string" }
"status": { "type": "string" }
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
describe EnvironmentHelper do
describe '#render_deployment_status' do
context 'when using a manual deployment' do
it 'renders a span tag' do
deploy = build(:deployment, deployable: nil, status: :success)
html = helper.render_deployment_status(deploy)
expect(html).to have_css('span.ci-status.ci-success')
end
end
context 'when using a deployment from a build' do
it 'renders a link tag' do
deploy = build(:deployment, status: :success)
html = helper.render_deployment_status(deploy)
expect(html).to have_css('a.ci-status.ci-success')
end
end
end
end
......@@ -348,4 +348,17 @@
expect(deployment.deployed_by).to eq(build_user)
end
end
describe '.find_successful_deployment!' do
it 'returns a successful deployment' do
deploy = create(:deployment, :success)
expect(described_class.find_successful_deployment!(deploy.iid)).to eq(deploy)
end
it 'raises when no deployment is found' do
expect { described_class.find_successful_deployment!(-1) }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
......@@ -882,4 +882,19 @@
end
end
end
describe '.find_or_create_by_name' do
it 'finds an existing environment if it exists' do
env = create(:environment)
expect(described_class.find_or_create_by_name(env.name)).to eq(env)
end
it 'creates an environment if it does not exist' do
env = project.environments.find_or_create_by_name('kittens')
expect(env).to be_an_instance_of(described_class)
expect(env).to be_persisted
end
end
end
......@@ -40,14 +40,14 @@
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image destroy_container_image
create_environment create_deployment create_release update_release
create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
......
# frozen_string_literal: true
require 'spec_helper'
describe API::Deployments do
......@@ -96,4 +98,164 @@ def expect_deployments(ordered_deployments)
end
end
end
describe 'POST /projects/:id/deployments' do
let!(:project) { create(:project, :repository) }
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
context 'as a maintainer' do
it 'creates a new deployment' do
post(
api("/projects/#{project.id}/deployments", user),
params: {
environment: 'production',
sha: sha,
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(201)
expect(json_response['sha']).to eq(sha)
expect(json_response['ref']).to eq('master')
expect(json_response['environment']['name']).to eq('production')
end
it 'errors when creating a deployment with an invalid name' do
post(
api("/projects/#{project.id}/deployments", user),
params: {
environment: 'a' * 300,
sha: sha,
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(500)
end
end
context 'as a developer' do
it 'creates a new deployment' do
developer = create(:user)
project.add_developer(developer)
post(
api("/projects/#{project.id}/deployments", developer),
params: {
environment: 'production',
sha: sha,
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(201)
expect(json_response['sha']).to eq(sha)
expect(json_response['ref']).to eq('master')
end
end
context 'as non member' do
it 'returns a 404 status code' do
post(
api( "/projects/#{project.id}/deployments", non_member),
params: {
environment: 'production',
sha: '123',
ref: 'master',
tag: false,
status: 'success'
}
)
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'PUT /projects/:id/deployments/:deployment_id' do
let(:project) { create(:project) }
let(:build) { create(:ci_build, :failed, project: project) }
let(:environment) { create(:environment, project: project) }
let(:deploy) do
create(
:deployment,
:failed,
project: project,
environment: environment,
deployable: nil
)
end
context 'as a maintainer' do
it 'returns a 403 when updating a deployment with a build' do
deploy.update(deployable: build)
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", user),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(403)
end
it 'updates a deployment without an associated build' do
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", user),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq('success')
end
end
context 'as a developer' do
let(:developer) { create(:user) }
before do
project.add_developer(developer)
end
it 'returns a 403 when updating a deployment with a build' do
deploy.update(deployable: build)
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(403)
end
it 'updates a deployment without an associated build' do
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq('success')
end
end
context 'as non member' do
it 'returns a 404 status code' do
put(
api("/projects/#{project.id}/deployments/#{deploy.id}", non_member),
params: { status: 'success' }
)
expect(response).to have_gitlab_http_status(404)
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe UpdateDeploymentService do
describe Deployments::AfterCreateService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:options) { { name: 'production' } }
......
# frozen_string_literal: true
require 'spec_helper'
describe Deployments::CreateService do
let(:environment) do
double(
:environment,
deployment_platform: double(:platform, cluster_id: 1),
project_id: 2,
id: 3
)
end
let(:user) { double(:user) }
describe '#execute' do
let(:service) { described_class.new(environment, user, {}) }
it 'does not run the AfterCreateService service if the deployment is not persisted' do
deploy = double(:deployment, persisted?: false)
expect(service)
.to receive(:create_deployment)
.and_return(deploy)
expect(Deployments::AfterCreateService)
.not_to receive(:new)
expect(service.execute).to eq(deploy)
end
it 'runs the AfterCreateService service if the deployment is persisted' do
deploy = double(:deployment, persisted?: true)
after_service = double(:after_create_service)
expect(service)
.to receive(:create_deployment)
.and_return(deploy)
expect(Deployments::AfterCreateService)
.to receive(:new)
.with(deploy)
.and_return(after_service)
expect(after_service)
.to receive(:execute)
expect(service.execute).to eq(deploy)
end
end
describe '#create_deployment' do
it 'creates a deployment' do
environment = build(:environment)
service = described_class.new(environment, user, {})
expect(environment.deployments)
.to receive(:create)
.with(an_instance_of(Hash))
service.create_deployment
end
end
describe '#deployment_attributes' do
it 'only includes attributes that we want to persist' do
service = described_class.new(
environment,
user,
ref: 'master',
tag: true,
sha: '123',
foo: 'bar',
on_stop: 'stop',
status: 'running'
)
expect(service.deployment_attributes).to eq(
cluster_id: 1,
project_id: 2,
environment_id: 3,
ref: 'master',
tag: true,
sha: '123',
user: user,
on_stop: 'stop',
status: 'running'
)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Deployments::UpdateService do
let(:deploy) { create(:deployment, :running) }
let(:service) { described_class.new(deploy, status: 'success') }
describe '#execute' do
it 'updates the status of a deployment' do
expect(service.execute).to eq(true)
expect(deploy.status).to eq('success')
end
end
end
......@@ -38,14 +38,14 @@
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image
create_environment create_deployment create_release update_release
create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
......
......@@ -8,8 +8,8 @@
context 'when successful deployment' do
let(:deployment) { create(:deployment, :success) }
it 'executes UpdateDeploymentService' do
expect(UpdateDeploymentService)
it 'executes Deployments::AfterCreateService' do
expect(Deployments::AfterCreateService)
.to receive(:new).with(deployment).and_call_original
subject
......@@ -19,8 +19,8 @@
context 'when canceled deployment' do
let(:deployment) { create(:deployment, :canceled) }
it 'does not execute UpdateDeploymentService' do
expect(UpdateDeploymentService).not_to receive(:new)
it 'does not execute Deployments::AfterCreateService' do
expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
......@@ -29,8 +29,8 @@
context 'when deploy record does not exist' do
let(:deployment) { nil }
it 'does not execute UpdateDeploymentService' do
expect(UpdateDeploymentService).not_to receive(:new)
it 'does not execute Deployments::AfterCreateService' do
expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment