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
@deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment)
end
# rubocop: disable CodeReuse/ActiveRecord
def deployment
@deployment ||= environment.deployments.find_by(iid: params[:id])
@deployment ||= environment.deployments.find_successful_deployment!(params[:id])
end
# rubocop: enable CodeReuse/ActiveRecord
def environment
@environment ||= project.environments.find(params[:environment_id])
......
......@@ -18,12 +18,16 @@ module EnvironmentHelper
end
end
def deployment_path(deployment)
[deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
end
def deployment_link(deployment, text: nil)
return unless deployment
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
def last_deployment_link_for_environment_build(project, build)
......@@ -32,4 +36,31 @@ module EnvironmentHelper
deployment_link(environment.last_deployment)
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
......@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord
belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
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
Deployment.where(project: s.project).maximum(:iid) if s&.project
......@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord
scope :for_environment, -> (environment) { where(environment_id: environment) }
scope :visible, -> { where(status: %i[running success failed canceled]) }
state_machine :status, initial: :created do
event :run do
transition created: :running
......@@ -73,6 +75,10 @@ class Deployment < ApplicationRecord
find(ids)
end
def self.find_successful_deployment!(iid)
success.find_by!(iid: iid)
end
def commit
project.commit(sha)
end
......
......@@ -6,7 +6,8 @@ class Environment < ApplicationRecord
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'
......@@ -81,6 +82,10 @@ class Environment < ApplicationRecord
pluck(:name)
end
def self.find_or_create_by_name(name)
find_or_create_by(name: name)
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name)
......
......@@ -281,7 +281,7 @@ class Project < ApplicationRecord
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
has_many :deployments, -> { success }
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
......
......@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy
can?(:update_build, @subject.deployable)
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 :update_deployment
end
rule { ~can_update_deployment }.policy do
prevent :update_deployment
end
end
......@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image
enable :create_environment
enable :create_deployment
enable :update_deployment
enable :create_release
enable :update_release
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' }
.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-mobile-header{ role: 'rowheader' }= _("ID")
%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")
= 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")
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
.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})
- if deployment.deployed_by
%div
by
= user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none")
- else
.badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
= s_('Deployment|API')
.table-section.section-15{ role: 'gridcell' }
.table-section.section-10{ role: 'gridcell' }
.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
%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
= render 'projects/deployments/actions', 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')
= 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?
......
......@@ -60,10 +60,13 @@
.table-holder
.ci-table.environments{ role: 'grid' }
.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-30{ role: 'columnheader' }= _('Commit')
.table-section.section-25{ role: 'columnheader' }= _('Job')
.table-section.section-15{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
= render @deployments
......
......@@ -10,7 +10,7 @@ module Deployments
Deployment.find_by_id(deployment_id).try do |deployment|
break unless deployment.success?
UpdateDeploymentService.new(deployment).execute
Deployments::AfterCreateService.new(deployment).execute
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
}
}
```
## 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
.on(join_conditions)
model
.joins(:deployments)
.joins(:successful_deployments)
.joins(join.join_sources)
.where(later_deployments[:id].eq(nil))
.where(deployments[:cluster_id].eq(cluster.id))
......
......@@ -9,6 +9,8 @@ module EE
rule { ~deployable_by_user }.policy do
prevent :stop_environment
prevent :create_environment_terminal
prevent :create_deployment
prevent :update_deployment
end
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 @@
require 'spec_helper'
describe UpdateDeploymentService do
describe Deployments::AfterCreateService do
include ::EE::GeoHelpers
let(:primary) { create(:geo_node, :primary) }
......
......@@ -42,6 +42,88 @@ module 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 @@ module Gitlab
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 @@ describe Projects::DeploymentsController do
}
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 @@ describe Projects::DeploymentsController do
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 @@ describe 'Environment' do
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 @@ describe 'Environment' do
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 @@ describe Deployment do
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 @@ describe Environment, :use_clean_rails_memory_store_caching do
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 @@ describe ProjectPolicy do
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 @@ describe API::Deployments do
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 @@ RSpec.shared_context 'ProjectPolicy context' do
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 @@ describe Deployments::SuccessWorker do
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 @@ describe Deployments::SuccessWorker do
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 @@ describe Deployments::SuccessWorker do
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
......
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