Commit b5698d89 authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'deployments-api' into 'master'

Add API for manually creating deployments

Closes #25176 and #32579

See merge request gitlab-org/gitlab!17620
parents 2c6bf8d0 77831f78
...@@ -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