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

Merge branch 'deployments-api' into 'master'

Add API for manually creating deployments

Closes #25176 and #32579

See merge request !17620
parents 2c6bf8d0 77831f78
No related branches found
No related tags found
1 merge request!17620Add API for manually creating deployments
Pipeline #89257717 failed
Showing
with 333 additions and 81 deletions
......@@ -47,11 +47,9 @@ def deployment_metrics
@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 @@ def environment_link_for_build(project, build)
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 @@ def last_deployment_link_for_environment_build(project, build)
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 @@ def self.last_for_environment(environment)
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 @@ def self.pluck_names
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 @@ def perform(deployment_id)
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 Environment
.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 EnvironmentPolicy
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment