Commit 77831f78 authored by Yorick Peterse's avatar Yorick Peterse

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
...@@ -47,11 +47,9 @@ class Projects::DeploymentsController < Projects::ApplicationController ...@@ -47,11 +47,9 @@ class Projects::DeploymentsController < Projects::ApplicationController
@deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment) @deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment)
end end
# rubocop: disable CodeReuse/ActiveRecord
def deployment def deployment
@deployment ||= environment.deployments.find_by(iid: params[:id]) @deployment ||= environment.deployments.find_successful_deployment!(params[:id])
end end
# rubocop: enable CodeReuse/ActiveRecord
def environment def environment
@environment ||= project.environments.find(params[:environment_id]) @environment ||= project.environments.find(params[:environment_id])
......
...@@ -18,12 +18,16 @@ module EnvironmentHelper ...@@ -18,12 +18,16 @@ module EnvironmentHelper
end end
end end
def deployment_path(deployment)
[deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
end
def deployment_link(deployment, text: nil) def deployment_link(deployment, text: nil)
return unless deployment return unless deployment
link_label = text ? text : "##{deployment.iid}" link_label = text ? text : "##{deployment.iid}"
link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] link_to link_label, deployment_path(deployment)
end end
def last_deployment_link_for_environment_build(project, build) def last_deployment_link_for_environment_build(project, build)
...@@ -32,4 +36,31 @@ module EnvironmentHelper ...@@ -32,4 +36,31 @@ module EnvironmentHelper
deployment_link(environment.last_deployment) deployment_link(environment.last_deployment)
end end
def render_deployment_status(deployment)
status = deployment.status
status_text =
case status
when 'created'
s_('Deployment|created')
when 'running'
s_('Deployment|running')
when 'success'
s_('Deployment|success')
when 'failed'
s_('Deployment|failed')
when 'canceled'
s_('Deployment|canceled')
end
klass = "ci-status ci-#{status.dasherize}"
text = "#{ci_icon_for_status(status)} #{status_text}".html_safe
if deployment.deployable
link_to(text, deployment_path(deployment), class: klass)
else
content_tag(:span, text, class: klass)
end
end
end end
...@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord ...@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord
belongs_to :environment, required: true belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
has_internal_id :iid, scope: :project, init: ->(s) do has_internal_id :iid, scope: :project, init: ->(s) do
Deployment.where(project: s.project).maximum(:iid) if s&.project Deployment.where(project: s.project).maximum(:iid) if s&.project
...@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord ...@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord
scope :for_environment, -> (environment) { where(environment_id: environment) } scope :for_environment, -> (environment) { where(environment_id: environment) }
scope :visible, -> { where(status: %i[running success failed canceled]) }
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :run do event :run do
transition created: :running transition created: :running
...@@ -73,6 +75,10 @@ class Deployment < ApplicationRecord ...@@ -73,6 +75,10 @@ class Deployment < ApplicationRecord
find(ids) find(ids)
end end
def self.find_successful_deployment!(iid)
success.find_by!(iid: iid)
end
def commit def commit
project.commit(sha) project.commit(sha)
end end
......
...@@ -6,7 +6,8 @@ class Environment < ApplicationRecord ...@@ -6,7 +6,8 @@ class Environment < ApplicationRecord
belongs_to :project, required: true belongs_to :project, required: true
has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
...@@ -81,6 +82,10 @@ class Environment < ApplicationRecord ...@@ -81,6 +82,10 @@ class Environment < ApplicationRecord
pluck(:name) pluck(:name)
end end
def self.find_or_create_by_name(name)
find_or_create_by(name: name)
end
def predefined_variables def predefined_variables
Gitlab::Ci::Variables::Collection.new Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name) .append(key: 'CI_ENVIRONMENT_NAME', value: name)
......
...@@ -281,7 +281,7 @@ class Project < ApplicationRecord ...@@ -281,7 +281,7 @@ class Project < ApplicationRecord
has_many :variables, class_name: 'Ci::Variable' has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger' has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments has_many :environments
has_many :deployments, -> { success } has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens
......
...@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy ...@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy
can?(:update_build, @subject.deployable) can?(:update_build, @subject.deployable)
end end
rule { ~can_retry_deployable }.policy do condition(:has_deployable) do
@subject.deployable.present?
end
condition(:can_update_deployment) do
can?(:update_deployment, @subject.environment)
end
rule { has_deployable & ~can_retry_deployable }.policy do
prevent :create_deployment prevent :create_deployment
prevent :update_deployment prevent :update_deployment
end end
rule { ~can_update_deployment }.policy do
prevent :update_deployment
end
end end
...@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy ...@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image enable :destroy_container_image
enable :create_environment enable :create_environment
enable :create_deployment enable :create_deployment
enable :update_deployment
enable :create_release enable :create_release
enable :update_release enable :update_release
end end
......
# frozen_string_literal: true
module Deployments
class AfterCreateService
attr_reader :deployment
attr_reader :deployable
delegate :environment, to: :deployment
delegate :variables, to: :deployable
delegate :options, to: :deployable, allow_nil: true
def initialize(deployment)
@deployment = deployment
@deployable = deployment.deployable
end
def execute
deployment.create_ref
deployment.invalidate_cache
update_environment(deployment)
deployment
end
def update_environment(deployment)
ActiveRecord::Base.transaction do
if (url = expanded_environment_url)
environment.external_url = url
end
environment.fire_state_event(action)
if environment.save && !environment.stopped?
deployment.update_merge_request_metrics!
end
end
end
private
def environment_options
options&.dig(:environment) || {}
end
def expanded_environment_url
ExpandVariables.expand(environment_url, -> { variables }) if environment_url
end
def environment_url
environment_options[:url]
end
def action
environment_options[:action] || 'start'
end
end
end
Deployments::AfterCreateService.prepend_if_ee('EE::Deployments::AfterCreateService')
# frozen_string_literal: true
module Deployments
class CreateService
attr_reader :environment, :current_user, :params
def initialize(environment, current_user, params)
@environment = environment
@current_user = current_user
@params = params
end
def execute
create_deployment.tap do |deployment|
AfterCreateService.new(deployment).execute if deployment.persisted?
end
end
def create_deployment
environment.deployments.create(deployment_attributes)
end
def deployment_attributes
# We use explicit parameters here so we never by accident allow parameters
# to be set that one should not be able to set (e.g. the row ID).
{
cluster_id: environment.deployment_platform&.cluster_id,
project_id: environment.project_id,
environment_id: environment.id,
ref: params[:ref],
tag: params[:tag],
sha: params[:sha],
user: current_user,
on_stop: params[:on_stop],
status: params[:status]
}
end
end
end
# frozen_string_literal: true
module Deployments
class UpdateService
attr_reader :deployment, :params
def initialize(deployment, params)
@deployment = deployment
@params = params
end
def execute
deployment.update(status: params[:status])
end
end
end
# frozen_string_literal: true
class UpdateDeploymentService
attr_reader :deployment
attr_reader :deployable
delegate :environment, to: :deployment
delegate :variables, to: :deployable
def initialize(deployment)
@deployment = deployment
@deployable = deployment.deployable
end
def execute
deployment.create_ref
deployment.invalidate_cache
ActiveRecord::Base.transaction do
environment.external_url = expanded_environment_url if
expanded_environment_url
environment.fire_state_event(action)
break unless environment.save
break if environment.stopped?
deployment.tap(&:update_merge_request_metrics!)
end
deployment
end
private
def environment_options
@environment_options ||= deployable.options&.dig(:environment) || {}
end
def expanded_environment_url
return @expanded_environment_url if defined?(@expanded_environment_url)
return unless environment_url
@expanded_environment_url =
ExpandVariables.expand(environment_url, -> { variables })
end
def environment_url
environment_options[:url]
end
def action
environment_options[:action] || 'start'
end
end
UpdateDeploymentService.prepend_if_ee('EE::UpdateDeploymentService')
.gl-responsive-table-row.deployment{ role: 'row' } .gl-responsive-table-row.deployment{ role: 'row' }
.table-section.section-15{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Status")
.table-mobile-content
= render_deployment_status(deployment)
.table-section.section-10{ role: 'gridcell' } .table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("ID") .table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid} %strong.table-mobile-content ##{deployment.iid}
.table-section.section-30{ role: 'gridcell' } .table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Triggerer")
.table-mobile-content
- if deployment.deployed_by
= user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
.table-section.section-25{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Commit") .table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment = render 'projects/deployments/commit', deployment: deployment
.table-section.section-25.build-column{ role: 'gridcell' } .table-section.section-10.build-column{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Job") .table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable - if deployment.deployable
.table-mobile-content .table-mobile-content
.flex-truncate-parent .flex-truncate-parent
.flex-truncate-child .flex-truncate-child
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do = link_to deployment_path(deployment), class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id}) #{deployment.deployable.name} (##{deployment.deployable.id})
- if deployment.deployed_by - else
%div .badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
by = s_('Deployment|API')
= user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none")
.table-section.section-15{ role: 'gridcell' } .table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Created") .table-mobile-header{ role: 'rowheader' }= _("Created")
%span.table-mobile-content.flex-truncate-parent
%span.flex-truncate-child
= time_ago_with_tooltip(deployment.created_at)
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Deployed")
- if deployment.deployed_at - if deployment.deployed_at
%span.table-mobile-content= time_ago_with_tooltip(deployment.deployed_at) %span.table-mobile-content.flex-truncate-parent
%span.flex-truncate-child
= time_ago_with_tooltip(deployment.deployed_at)
.table-section.section-20.table-button-footer{ role: 'gridcell' } .table-section.section-10.table-button-footer{ role: 'gridcell' }
.btn-group.table-action-buttons .btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment
- if can?(current_user, :create_deployment, deployment) - if deployment.deployable && can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= button_tag class: 'btn btn-default btn-build has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do = button_tag class: 'btn btn-default btn-build has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
- if deployment.last? - if deployment.last?
......
...@@ -60,10 +60,13 @@ ...@@ -60,10 +60,13 @@
.table-holder .table-holder
.ci-table.environments{ role: 'grid' } .ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' } .gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID') .table-section.section-10{ role: 'columnheader' }= _('ID')
.table-section.section-30{ role: 'columnheader' }= _('Commit') .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Job') .table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-15{ role: 'columnheader' }= _('Created') .table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
= render @deployments = render @deployments
......
...@@ -10,7 +10,7 @@ module Deployments ...@@ -10,7 +10,7 @@ module Deployments
Deployment.find_by_id(deployment_id).try do |deployment| Deployment.find_by_id(deployment_id).try do |deployment|
break unless deployment.success? break unless deployment.success?
UpdateDeploymentService.new(deployment).execute Deployments::AfterCreateService.new(deployment).execute
end end
end end
end end
......
---
title: Add API for manually creating and updating deployments
merge_request: 17620
author:
type: added
...@@ -223,3 +223,100 @@ Example of response ...@@ -223,3 +223,100 @@ Example of response
} }
} }
``` ```
## Create a deployment
```
POST /projects/:id/deployments
```
| Attribute | Type | Required | Description |
|------------------|----------------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `environment` | string | yes | The name of the environment to create the deployment for |
| `sha` | string | yes | The SHA of the commit that is deployed |
| `ref` | string | yes | The name of the branch or tag that is deployed |
| `tag` | boolean | yes | A boolean that indicates if the deployed ref is a tag (true) or not (false) |
| `status` | string | yes | The status of the deployment |
The status can be one of the following values:
- created
- running
- success
- failed
- canceled
```bash
curl --data "environment=production&sha=a91957a858320c0e17f3a0eca7cfacbff50ea29a&ref=master&tag=false&status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments"
```
Example of a response:
```json
{
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"created_at": "2016-08-11T11:32:35.444Z",
"status": "success",
"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"
},
"environment": {
"id": 9,
"name": "production",
"external_url": "https://about.gitlab.com"
},
"deployable": null
}
```
## Updating a deployment
```
PUT /projects/:id/deployments/:deployment_id
```
| Attribute | Type | Required | Description |
|------------------|----------------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `deployment_id` | integer | yes | The ID of the deployment to update |
| `status` | string | yes | The new status of the deployment |
```bash
curl --request PUT --data "status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/42"
```
Example of a response:
```json
{
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"created_at": "2016-08-11T11:32:35.444Z",
"status": "success",
"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"
},
"environment": {
"id": 9,
"name": "production",
"external_url": "https://about.gitlab.com"
},
"deployable": null
}
```
...@@ -25,7 +25,7 @@ module EE ...@@ -25,7 +25,7 @@ module EE
.on(join_conditions) .on(join_conditions)
model model
.joins(:deployments) .joins(:successful_deployments)
.joins(join.join_sources) .joins(join.join_sources)
.where(later_deployments[:id].eq(nil)) .where(later_deployments[:id].eq(nil))
.where(deployments[:cluster_id].eq(cluster.id)) .where(deployments[:cluster_id].eq(cluster.id))
......
...@@ -9,6 +9,8 @@ module EE ...@@ -9,6 +9,8 @@ module EE
rule { ~deployable_by_user }.policy do rule { ~deployable_by_user }.policy do
prevent :stop_environment prevent :stop_environment
prevent :create_environment_terminal prevent :create_environment_terminal
prevent :create_deployment
prevent :update_deployment
end end
private private
......
# frozen_string_literal: true
module EE
module Deployments
module AfterCreateService
extend ::Gitlab::Utils::Override
override :execute
def execute
super.tap do |deployment|
deployment.project.repository.log_geo_updated_event
end
end
end
end
end
# 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 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe UpdateDeploymentService do describe Deployments::AfterCreateService do
include ::EE::GeoHelpers include ::EE::GeoHelpers
let(:primary) { create(:geo_node, :primary) } let(:primary) { create(:geo_node, :primary) }
......
...@@ -42,6 +42,88 @@ module API ...@@ -42,6 +42,88 @@ module API
present deployment, with: Entities::Deployment present deployment, with: Entities::Deployment
end 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 end
end end
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
def value def value
strong_memoize(:value) do 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 = query.where("created_at <= ?", @to) if @to
query.count query.count
end end
......
...@@ -5409,6 +5409,27 @@ msgstr "" ...@@ -5409,6 +5409,27 @@ msgstr ""
msgid "Deploying to" msgid "Deploying to"
msgstr "" 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" msgid "Deprioritize label"
msgstr "" msgstr ""
......
...@@ -75,15 +75,13 @@ describe Projects::DeploymentsController do ...@@ -75,15 +75,13 @@ describe Projects::DeploymentsController do
} }
end end
before do it 'returns a metrics JSON document' do
expect_next_instance_of(DeploymentMetrics) do |deployment_metrics| expect_next_instance_of(DeploymentMetrics) do |deployment_metrics|
allow(deployment_metrics).to receive(:has_metrics?).and_return(true) allow(deployment_metrics).to receive(:has_metrics?).and_return(true)
expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics) expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics)
end end
end
it 'returns a metrics JSON document' do
get :metrics, params: deployment_params(id: deployment.to_param) get :metrics, params: deployment_params(id: deployment.to_param)
expect(response).to be_ok expect(response).to be_ok
...@@ -91,6 +89,19 @@ describe Projects::DeploymentsController do ...@@ -91,6 +89,19 @@ describe Projects::DeploymentsController do
expect(json_response['metrics']).to eq({}) expect(json_response['metrics']).to eq({})
expect(json_response['last_update']).to eq(42) expect(json_response['last_update']).to eq(42)
end 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 end
end end
......
...@@ -66,8 +66,8 @@ describe 'Environment' do ...@@ -66,8 +66,8 @@ describe 'Environment' do
create(:deployment, :running, environment: environment, deployable: build) create(:deployment, :running, environment: environment, deployable: build)
end end
it 'does not show deployments' do it 'does show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.') expect(page).to have_link("#{build.name} (##{build.id})")
end end
end end
...@@ -79,8 +79,8 @@ describe 'Environment' do ...@@ -79,8 +79,8 @@ describe 'Environment' do
create(:deployment, :failed, environment: environment, deployable: build) create(:deployment, :failed, environment: environment, deployable: build)
end end
it 'does not show deployments' do it 'does show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.') expect(page).to have_link("#{build.name} (##{build.id})")
end end
end end
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
"type": "array", "type": "array",
"items": { "$ref": "job/job.json" } "items": { "$ref": "job/job.json" }
}, },
"status": { "type": "string" } "status": { "type": "string" }
}, },
"additionalProperties": false "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 @@ describe Deployment do ...@@ -348,4 +348,17 @@ describe Deployment do
expect(deployment.deployed_by).to eq(build_user) expect(deployment.deployed_by).to eq(build_user)
end end
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 end
...@@ -882,4 +882,19 @@ describe Environment, :use_clean_rails_memory_store_caching do ...@@ -882,4 +882,19 @@ describe Environment, :use_clean_rails_memory_store_caching do
end end
end 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 end
...@@ -40,14 +40,14 @@ describe ProjectPolicy do ...@@ -40,14 +40,14 @@ describe ProjectPolicy do
update_commit_status create_build update_build create_pipeline update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image destroy_container_image 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 end
let(:base_maintainer_permissions) do let(:base_maintainer_permissions) do
%i[ %i[
push_to_delete_protected_branch update_project_snippet update_environment 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_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics daily_statistics
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe API::Deployments do describe API::Deployments do
...@@ -96,4 +98,164 @@ describe API::Deployments do ...@@ -96,4 +98,164 @@ describe API::Deployments do
end end
end 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 end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe UpdateDeploymentService do describe Deployments::AfterCreateService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:options) { { name: 'production' } } 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 @@ RSpec.shared_context 'ProjectPolicy context' do ...@@ -38,14 +38,14 @@ RSpec.shared_context 'ProjectPolicy context' do
update_commit_status create_build update_build create_pipeline update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image 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 end
let(:base_maintainer_permissions) do let(:base_maintainer_permissions) do
%i[ %i[
push_to_delete_protected_branch update_project_snippet update_environment 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_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics daily_statistics
......
...@@ -8,8 +8,8 @@ describe Deployments::SuccessWorker do ...@@ -8,8 +8,8 @@ describe Deployments::SuccessWorker do
context 'when successful deployment' do context 'when successful deployment' do
let(:deployment) { create(:deployment, :success) } let(:deployment) { create(:deployment, :success) }
it 'executes UpdateDeploymentService' do it 'executes Deployments::AfterCreateService' do
expect(UpdateDeploymentService) expect(Deployments::AfterCreateService)
.to receive(:new).with(deployment).and_call_original .to receive(:new).with(deployment).and_call_original
subject subject
...@@ -19,8 +19,8 @@ describe Deployments::SuccessWorker do ...@@ -19,8 +19,8 @@ describe Deployments::SuccessWorker do
context 'when canceled deployment' do context 'when canceled deployment' do
let(:deployment) { create(:deployment, :canceled) } let(:deployment) { create(:deployment, :canceled) }
it 'does not execute UpdateDeploymentService' do it 'does not execute Deployments::AfterCreateService' do
expect(UpdateDeploymentService).not_to receive(:new) expect(Deployments::AfterCreateService).not_to receive(:new)
subject subject
end end
...@@ -29,8 +29,8 @@ describe Deployments::SuccessWorker do ...@@ -29,8 +29,8 @@ describe Deployments::SuccessWorker do
context 'when deploy record does not exist' do context 'when deploy record does not exist' do
let(:deployment) { nil } let(:deployment) { nil }
it 'does not execute UpdateDeploymentService' do it 'does not execute Deployments::AfterCreateService' do
expect(UpdateDeploymentService).not_to receive(:new) expect(Deployments::AfterCreateService).not_to receive(:new)
subject subject
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