Commit f0c7e671 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '22191-delete-dynamic-envs-mr' into 'master'

Delete dynamic environments

- Adds "close environment" action to a merge request
- Adds tabs to environments list
- Adds close button to each environment row in environments list
- Replaces Destroy button with Close button inside an environment
- Adds close button to builds list inside an environment

#### Configuration

In order to enable stopping environments a valid `.gitlab-ci.yml` syntax has to be used:

```
review:
  environment:
    name: review/$app
    on_stop: stop_review

stop_review:
  script: echo Delete My App
  when: manual
  environment:
    name: review/$app
    action: stop
```

This MR requires that `stop_review` has to have: `when`, `environment:name` and `environment:action` defined.
The next MR after this one will verify that and enforce that these settings are configured.

It will also implicitly configure these settings, making it possible to define it like this:

```
review:
  environment:
    name: review/$app
    on_stop: stop_review

stop_review:
  script: echo Delete My App
```

Closes #22191 

See merge request !6669
parents f64e36c4 19300a1a
...@@ -125,6 +125,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -125,6 +125,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix a typo in doc/api/labels.md - Fix a typo in doc/api/labels.md
- API: all unknown routing will be handled with 404 Not Found - API: all unknown routing will be handled with 404 Not Found
- Add docs for request profiling - Add docs for request profiling
- Delete dynamic environments
- Make guests unable to view MRs on private projects - Make guests unable to view MRs on private projects
## 8.12.7 ## 8.12.7
......
...@@ -17,6 +17,12 @@ ...@@ -17,6 +17,12 @@
View on <%- external_url_formatted %> View on <%- external_url_formatted %>
</a> </a>
</span> </span>
<span class="stop-env-container js-stop-env-link">
<a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?">
<i class="fa fa-stop-circle-o"/>
Stop environment
</a>
</span>
</div> </div>
</div>`; </div>`;
...@@ -205,6 +211,11 @@ ...@@ -205,6 +211,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return; if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE); const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove();
}
if (environment.deployed_at && environment.deployed_at_formatted) { if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = $.timeago(environment.deployed_at) + '.'; environment.deployed_at = $.timeago(environment.deployed_at) + '.';
} else { } else {
......
...@@ -38,6 +38,14 @@ ...@@ -38,6 +38,14 @@
color: $gl-dark-link-color; color: $gl-dark-link-color;
} }
.stop-env-link {
color: $table-text-gray;
.stop-env-icon {
font-size: 14px;
}
}
.deployment { .deployment {
.build-column { .build-column {
......
...@@ -183,6 +183,15 @@ ...@@ -183,6 +183,15 @@
.ci-coverage { .ci-coverage {
float: right; float: right;
} }
.stop-env-container {
color: $gl-text-color;
float: right;
a {
color: $gl-text-color;
}
}
} }
.mr_source_commit, .mr_source_commit,
......
...@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project' layout 'project'
before_action :authorize_read_environment! before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_update_environment!, only: [:edit, :update, :destroy] before_action :authorize_create_deployment!, only: [:stop]
before_action :environment, only: [:show, :edit, :update, :destroy] before_action :authorize_update_environment!, only: [:edit, :update]
before_action :environment, only: [:show, :edit, :update, :stop]
def index def index
@environments = project.environments @scope = params[:scope]
@all_environments = project.environments
@environments =
if @scope == 'stopped'
@all_environments.stopped
else
@all_environments.available
end
end end
def show def show
...@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def destroy def stop
if @environment.destroy return render_404 unless @environment.stoppable?
flash[:notice] = 'Environment was successfully removed.'
else
flash[:alert] = 'Failed to remove environment.'
end
redirect_to namespace_project_environments_path(project.namespace, project) new_action = @environment.stop!(current_user)
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end end
private private
......
...@@ -422,10 +422,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -422,10 +422,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
project = environment.project project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit) deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
if environment.stoppable? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
{ {
id: environment.id, id: environment.id,
name: environment.name, name: environment.name,
url: namespace_project_environment_path(project.namespace, project, environment), url: namespace_project_environment_path(project.namespace, project, environment),
stop_url: stop_url,
external_url: environment.external_url, external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url, external_url_formatted: environment.formatted_external_url,
deployed_at: deployment.try(:created_at), deployed_at: deployment.try(:created_at),
......
...@@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base ...@@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base
end end
def manual_actions def manual_actions
deployable.try(:other_actions) @manual_actions ||= deployable.try(:other_actions)
end end
def includes_commit?(commit) def includes_commit?(commit)
...@@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base ...@@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base
take take
end end
def stop_action
return nil unless on_stop.present?
return nil unless manual_actions
@stop_action ||= manual_actions.find_by(name: on_stop)
end
def stoppable?
stop_action.present?
end
def formatted_deployment_time def formatted_deployment_time
created_at.to_time.in_time_zone.to_s(:medium) created_at.to_time.in_time_zone.to_s(:medium)
end end
......
...@@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base ...@@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base
allow_nil: true, allow_nil: true,
addressable_url: true addressable_url: true
delegate :stop_action, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
state_machine :state, initial: :available do
event :start do
transition stopped: :available
end
event :stop do
transition available: :stopped
end
state :available
state :stopped
end
def last_deployment def last_deployment
deployments.last deployments.last
end end
...@@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base ...@@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '') external_url.gsub(/\A.*?:\/\//, '')
end end
def stoppable?
available? && stop_action.present?
end
def stop!(current_user)
return unless stoppable?
stop_action.play(current_user)
end
end end
...@@ -1293,7 +1293,7 @@ class Project < ActiveRecord::Base ...@@ -1293,7 +1293,7 @@ class Project < ActiveRecord::Base
environment_ids.where(ref: ref) environment_ids.where(ref: ref)
end end
environments.where(id: environment_ids).select do |environment| environments.available.where(id: environment_ids).select do |environment|
environment.includes_commit?(commit) environment.includes_commit?(commit)
end end
end end
......
...@@ -6,7 +6,13 @@ class CreateDeploymentService < BaseService ...@@ -6,7 +6,13 @@ class CreateDeploymentService < BaseService
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@deployable = deployable @deployable = deployable
@environment = prepare_environment
@environment = environment
@environment.external_url = expanded_url if expanded_url
@environment.fire_state_event(action)
return unless @environment.save
return if @environment.stopped?
deploy.tap do |deployment| deploy.tap do |deployment|
deployment.update_merge_request_metrics! deployment.update_merge_request_metrics!
...@@ -27,13 +33,12 @@ class CreateDeploymentService < BaseService ...@@ -27,13 +33,12 @@ class CreateDeploymentService < BaseService
tag: params[:tag], tag: params[:tag],
sha: params[:sha], sha: params[:sha],
user: current_user, user: current_user,
deployable: @deployable) deployable: @deployable,
on_stop: options[:on_stop])
end end
def prepare_environment def environment
project.environments.find_or_create_by(name: expanded_name) do |environment| @environment ||= project.environments.find_or_create_by(name: expanded_name)
environment.external_url = expanded_url
end
end end
def expanded_name def expanded_name
...@@ -61,4 +66,8 @@ class CreateDeploymentService < BaseService ...@@ -61,4 +66,8 @@ class CreateDeploymentService < BaseService
def variables def variables
params[:variables] || [] params[:variables] || []
end end
def action
options[:action] || 'start'
end
end end
- if can?(current_user, :create_deployment, deployment) && deployment.deployable - if can?(current_user, :create_deployment, deployment)
.pull-right - actions = deployment.manual_actions
- if actions.present?
- external_url = deployment.environment.external_url .inline
- if external_url .dropdown
= link_to external_url, target: '_blank', class: 'btn external-url' do %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= icon('external-link') = custom_icon('icon_play')
= icon('caret-down')
- actions = deployment.manual_actions %ul.dropdown-menu.dropdown-menu-align-right
- if actions.present? - actions.each do |action|
.inline %li
.dropdown = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = custom_icon('icon_play')
= custom_icon('icon_play') %span= action.name.humanize
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= action.name.humanize
- if local_assigns.fetch(:allow_rollback, false)
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last?
Re-deploy
- else
Rollback
...@@ -17,4 +17,6 @@ ...@@ -17,4 +17,6 @@
#{time_ago_with_tooltip(deployment.created_at)} #{time_ago_with_tooltip(deployment.created_at)}
%td.hidden-xs %td.hidden-xs
= render 'projects/deployments/actions', deployment: deployment, allow_rollback: true .pull-right
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last?
Re-deploy
- else
Rollback
...@@ -28,4 +28,8 @@ ...@@ -28,4 +28,8 @@
#{time_ago_with_tooltip(last_deployment.created_at)} #{time_ago_with_tooltip(last_deployment.created_at)}
%td.hidden-xs %td.hidden-xs
= render 'projects/deployments/actions', deployment: last_deployment .pull-right
= render 'projects/environments/external_url', environment: environment
= render 'projects/deployments/actions', deployment: last_deployment
= render 'projects/environments/stop', environment: environment
= render 'projects/deployments/rollback', deployment: last_deployment
- if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', class: 'btn external-url' do
= icon('external-link')
- if can?(current_user, :create_deployment, environment) && environment.stoppable?
.inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
= icon('stop', class: 'stop-env-icon')
...@@ -3,14 +3,27 @@ ...@@ -3,14 +3,27 @@
= render "projects/pipelines/head" = render "projects/pipelines/head"
%div{ class: container_class } %div{ class: container_class }
- if can?(current_user, :create_environment, @project) && !@environments.blank? .top-area
.top-area %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_environments_path(@project) do
Available
%span.badge.js-available-environments-count
= number_with_delimiter(@all_environments.available.count)
%li{class: ('active' if @scope == 'stopped')}
= link_to project_environments_path(@project, scope: :stopped) do
Stopped
%span.badge.js-stopped-environments-count
= number_with_delimiter(@all_environments.stopped.count)
- if can?(current_user, :create_environment, @project) && !@all_environments.blank?
.nav-controls .nav-controls
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
New environment New environment
.environments-container .environments-container
- if @environments.blank? - if @all_environments.blank?
.blank-state.blank-state-no-icon .blank-state.blank-state-no-icon
%h2.blank-state-title %h2.blank-state-title
You don't have any environments right now. You don't have any environments right now.
......
...@@ -3,14 +3,16 @@ ...@@ -3,14 +3,16 @@
= render "projects/pipelines/head" = render "projects/pipelines/head"
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area.adjust
.col-md-9 .col-md-9
%h3.page-title= @environment.name.capitalize %h3.page-title= @environment.name.capitalize
.col-md-3 .col-md-3
.nav-controls .nav-controls
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment) - if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
= link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container .deployments-container
- if @deployments.blank? - if @deployments.blank?
......
...@@ -319,7 +319,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: ...@@ -319,7 +319,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end end
end end
resources :environments resources :environments, except: [:destroy] do
member do
post :stop
end
end
resource :cycle_analytics, only: [:show] resource :cycle_analytics, only: [:show]
......
...@@ -16,7 +16,8 @@ class Gitlab::Seeder::Pipelines ...@@ -16,7 +16,8 @@ class Gitlab::Seeder::Pipelines
{ name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending }, { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
{ name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running }, { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
{ name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled }, { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
{ name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success }, { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } },
{ name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped }, { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
{ name: 'slack', stage: 'notify', when: 'manual', status: :created }, { name: 'slack', stage: 'notify', when: 'manual', status: :created },
] ]
......
class AddStateToEnvironment < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
add_column_with_default(:environments, :state, :string, default: :available)
end
def down
remove_column(:environments, :state)
end
end
class AddPropertiesToDeployment < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :deployments, :on_stop, :string
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161012180455) do ActiveRecord::Schema.define(version: 20161017095000) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -380,6 +380,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do ...@@ -380,6 +380,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do
t.string "deployable_type" t.string "deployable_type"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "on_stop"
end end
add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
...@@ -404,6 +405,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do ...@@ -404,6 +405,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do
t.datetime "updated_at" t.datetime "updated_at"
t.string "external_url" t.string "external_url"
t.string "environment_type" t.string "environment_type"
t.string "state", default: "available", null: false
end end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
......
...@@ -109,6 +109,7 @@ module Ci ...@@ -109,6 +109,7 @@ module Ci
validate_job_stage!(name, job) validate_job_stage!(name, job)
validate_job_dependencies!(name, job) validate_job_dependencies!(name, job)
validate_job_environment!(name, job)
end end
end end
...@@ -150,6 +151,35 @@ module Ci ...@@ -150,6 +151,35 @@ module Ci
end end
end end
def validate_job_environment!(name, job)
return unless job[:environment]
return unless job[:environment].is_a?(Hash)
environment = job[:environment]
validate_on_stop_job!(name, environment, environment[:on_stop])
end
def validate_on_stop_job!(name, environment, on_stop)
return unless on_stop
on_stop_job = @jobs[on_stop.to_sym]
unless on_stop_job
raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
end
unless on_stop_job[:environment]
raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
end
unless on_stop_job[:environment][:name] == environment[:name]
raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
end
unless on_stop_job[:environment][:action] == 'stop'
raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
end
end
def process?(only_params, except_params, ref, tag, trigger_request) def process?(only_params, except_params, ref, tag, trigger_request)
if only_params.present? if only_params.present?
return false unless matching?(only_params, ref, tag, trigger_request) return false unless matching?(only_params, ref, tag, trigger_request)
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
class Environment < Entry class Environment < Entry
include Validatable include Validatable
ALLOWED_KEYS = %i[name url] ALLOWED_KEYS = %i[name url action on_stop]
validations do validations do
validate do validate do
...@@ -35,6 +35,12 @@ module Gitlab ...@@ -35,6 +35,12 @@ module Gitlab
length: { maximum: 255 }, length: { maximum: 255 },
addressable_url: true, addressable_url: true,
allow_nil: true allow_nil: true
validates :action,
inclusion: { in: %w[start stop], message: 'should be start or stop' },
allow_nil: true
validates :on_stop, type: String, allow_nil: true
end end
end end
...@@ -54,9 +60,17 @@ module Gitlab ...@@ -54,9 +60,17 @@ module Gitlab
value[:url] value[:url]
end end
def action
value[:action] || 'start'
end
def on_stop
value[:on_stop]
end
def value def value
case @config case @config
when String then { name: @config } when String then { name: @config, action: 'start' }
when Hash then @config when Hash then @config
else {} else {}
end end
......
...@@ -19,10 +19,22 @@ feature 'Environments', feature: true do ...@@ -19,10 +19,22 @@ feature 'Environments', feature: true do
visit namespace_project_environments_path(project.namespace, project) visit namespace_project_environments_path(project.namespace, project)
end end
context 'shows two tabs' do
scenario 'shows "Available" and "Stopped" tab with links' do
expect(page).to have_link('Available')
expect(page).to have_link('Stopped')
end
end
context 'without environments' do context 'without environments' do
scenario 'does show no environments' do scenario 'does show no environments' do
expect(page).to have_content('You don\'t have any environments right now.') expect(page).to have_content('You don\'t have any environments right now.')
end end
scenario 'does show 0 as counter for environments in both tabs' do
expect(page.find('.js-available-environments-count').text).to eq('0')
expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
end end
context 'with environments' do context 'with environments' do
...@@ -32,6 +44,11 @@ feature 'Environments', feature: true do ...@@ -32,6 +44,11 @@ feature 'Environments', feature: true do
expect(page).to have_link(environment.name) expect(page).to have_link(environment.name)
end end
scenario 'does show number of available and stopped environments' do
expect(page.find('.js-available-environments-count').text).to eq('1')
expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
context 'without deployments' do context 'without deployments' do
scenario 'does show no deployments' do scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet') expect(page).to have_content('No deployments yet')
...@@ -44,7 +61,7 @@ feature 'Environments', feature: true do ...@@ -44,7 +61,7 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha) expect(page).to have_link(deployment.short_sha)
end end
scenario 'does show deployment internal id' do scenario 'does show deployment internal id' do
expect(page).to have_content(deployment.iid) expect(page).to have_content(deployment.iid)
end end
...@@ -65,20 +82,51 @@ feature 'Environments', feature: true do ...@@ -65,20 +82,51 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name) expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending expect(manual.reload).to be_pending
end end
scenario 'does show build name and id' do scenario 'does show build name and id' do
expect(page).to have_link("#{build.name} (##{build.id})") expect(page).to have_link("#{build.name} (##{build.id})")
end end
scenario 'does not show stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
scenario 'does not show external link button' do
expect(page).not_to have_css('external-url')
end
context 'with external_url' do context 'with external_url' do
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
given(:build) { create(:ci_build, pipeline: pipeline) } given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) } given(:deployment) { create(:deployment, environment: environment, deployable: build) }
scenario 'does show an external link button' do scenario 'does show an external link button' do
expect(page).to have_link(nil, href: environment.external_url) expect(page).to have_link(nil, href: environment.external_url)
end end
end end
context 'with stop action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
scenario 'does show stop button' do
expect(page).to have_selector('.stop-env-link')
end
scenario 'starts build when stop button clicked' do
first('.stop-env-link').click
expect(page).to have_content('close_app')
end
context 'for reporter' do
let(:role) { :reporter }
scenario 'does not show stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
end
end
end end
end end
end end
...@@ -127,6 +175,10 @@ feature 'Environments', feature: true do ...@@ -127,6 +175,10 @@ feature 'Environments', feature: true do
expect(page).to have_link('Re-deploy') expect(page).to have_link('Re-deploy')
end end
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
end
context 'with manual action' do context 'with manual action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
...@@ -140,16 +192,39 @@ feature 'Environments', feature: true do ...@@ -140,16 +192,39 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name) expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending expect(manual.reload).to be_pending
end end
context 'with external_url' do context 'with external_url' do
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
given(:build) { create(:ci_build, pipeline: pipeline) } given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) } given(:deployment) { create(:deployment, environment: environment, deployable: build) }
scenario 'does show an external link button' do scenario 'does show an external link button' do
expect(page).to have_link(nil, href: environment.external_url) expect(page).to have_link(nil, href: environment.external_url)
end end
end end
context 'with stop action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
scenario 'does show stop button' do
expect(page).to have_link('Stop')
end
scenario 'does allow to stop environment' do
click_link('Stop')
expect(page).to have_content('close_app')
end
context 'for reporter' do
let(:role) { :reporter }
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
end
end
end
end end
end end
end end
...@@ -196,29 +271,4 @@ feature 'Environments', feature: true do ...@@ -196,29 +271,4 @@ feature 'Environments', feature: true do
end end
end end
end end
describe 'when deleting existing environment' do
given(:environment) { create(:environment, project: project) }
before do
visit namespace_project_environment_path(project.namespace, project, environment)
end
context 'when logged as master' do
given(:role) { :master }
scenario 'does delete environment' do
click_link 'Destroy'
expect(page).not_to have_link(environment.name)
end
end
context 'when logged as developer' do
given(:role) { :developer }
scenario 'does not have a Destroy link' do
expect(page).not_to have_link('Destroy')
end
end
end
end end
...@@ -101,7 +101,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do ...@@ -101,7 +101,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
expect(page).not_to have_link "Merge When Build Succeeds" expect(page).not_to have_link "Merge When Build Succeeds"
end end
end end
def visit_merge_request(merge_request) def visit_merge_request(merge_request)
visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
end end
......
...@@ -4,23 +4,58 @@ feature 'Widget Deployments Header', feature: true, js: true do ...@@ -4,23 +4,58 @@ feature 'Widget Deployments Header', feature: true, js: true do
include WaitForAjax include WaitForAjax
describe 'when deployed to an environment' do describe 'when deployed to an environment' do
let(:project) { merge_request.target_project } given(:user) { create(:user) }
let(:merge_request) { create(:merge_request, :merged) } given(:project) { merge_request.target_project }
let(:environment) { create(:environment, project: project) } given(:merge_request) { create(:merge_request, :merged) }
let!(:deployment) do given(:environment) { create(:environment, project: project) }
create(:deployment, environment: environment, sha: project.commit('master').id) given(:role) { :developer }
end given(:sha) { project.commit('master').id }
given!(:deployment) { create(:deployment, environment: environment, sha: sha) }
given!(:manual) { }
before do background do
login_as :admin login_as(user)
project.team << [user, role]
visit namespace_project_merge_request_path(project.namespace, project, merge_request) visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
it 'displays that the environment is deployed' do scenario 'displays that the environment is deployed' do
wait_for_ajax wait_for_ajax
expect(page).to have_content("Deployed to #{environment.name}") expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
end end
context 'with stop action' do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) do
create(:deployment, environment: environment, ref: merge_request.target_branch,
sha: sha, deployable: build, on_stop: 'close_app')
end
background do
wait_for_ajax
end
scenario 'does show stop button' do
expect(page).to have_link('Stop environment')
end
scenario 'does start build when stop button clicked' do
click_link('Stop environment')
expect(page).to have_content('close_app')
end
context 'for reporter' do
given(:role) { :reporter }
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop environment')
end
end
end
end end
end end
...@@ -754,7 +754,7 @@ module Ci ...@@ -754,7 +754,7 @@ module Ci
it 'does return production' do it 'does return production' do
expect(builds.size).to eq(1) expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment) expect(builds.first[:environment]).to eq(environment)
expect(builds.first[:options]).to include(environment: { name: environment }) expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
end end
end end
...@@ -796,6 +796,52 @@ module Ci ...@@ -796,6 +796,52 @@ module Ci
expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end end
end end
context 'when on_stop is specified' do
let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
let(:config) { { review: review, close_review: close_review }.compact }
context 'with matching job' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
it 'does return a list of builds' do
expect(builds.size).to eq(2)
expect(builds.first[:environment]).to eq('review')
end
end
context 'without matching job' do
let(:close_review) { nil }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
end
end
context 'with close job without environment' do
let(:close_review) { { stage: 'deploy', script: 'test' } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
end
end
context 'with close job for different environment' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
end
end
context 'with close job without stop action' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
end
end
end
end end
describe "Dependencies" do describe "Dependencies" do
......
...@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Node::Environment do ...@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Node::Environment do
describe '#value' do describe '#value' do
it 'returns valid hash' do it 'returns valid hash' do
expect(entry.value).to eq(name: 'production') expect(entry.value).to include(name: 'production')
end end
end end
...@@ -87,6 +87,68 @@ describe Gitlab::Ci::Config::Node::Environment do ...@@ -87,6 +87,68 @@ describe Gitlab::Ci::Config::Node::Environment do
end end
end end
context 'when valid action is used' do
let(:config) do
{ name: 'production',
action: 'start' }
end
it 'is valid' do
expect(entry).to be_valid
end
end
context 'when invalid action is used' do
let(:config) do
{ name: 'production',
action: 'invalid' }
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
describe '#errors' do
it 'contains error about invalid action' do
expect(entry.errors)
.to include 'environment action should be start or stop'
end
end
end
context 'when on_stop is used' do
let(:config) do
{ name: 'production',
on_stop: 'close_app' }
end
it 'is valid' do
expect(entry).to be_valid
end
end
context 'when invalid on_stop is used' do
let(:config) do
{ name: 'production',
on_stop: false }
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
describe '#errors' do
it 'contains error about invalid action' do
expect(entry.errors)
.to include 'environment on stop should be a string'
end
end
end
context 'when variables are used for environment' do context 'when variables are used for environment' do
let(:config) do let(:config) do
{ name: 'review/$CI_BUILD_REF_NAME', { name: 'review/$CI_BUILD_REF_NAME',
......
...@@ -48,4 +48,50 @@ describe Deployment, models: true do ...@@ -48,4 +48,50 @@ describe Deployment, models: true do
end end
end end
end end
describe '#stop_action' do
let(:build) { create(:ci_build) }
subject { deployment.stop_action }
context 'when no other actions' do
let(:deployment) { FactoryGirl.build(:deployment, deployable: build) }
it { is_expected.to be_nil }
end
context 'with other actions' do
let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
context 'when matching action is defined' do
let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') }
it { is_expected.to be_nil }
end
context 'when no matching action is defined' do
let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
it { is_expected.to eq(close_action) }
end
end
end
describe '#stoppable?' do
subject { deployment.stoppable? }
context 'when no other actions' do
let(:deployment) { build(:deployment) }
it { is_expected.to be_falsey }
end
context 'when matching action is defined' do
let(:build) { create(:ci_build) }
let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
it { is_expected.to be_truthy }
end
end
end end
...@@ -8,6 +8,8 @@ describe Environment, models: true do ...@@ -8,6 +8,8 @@ describe Environment, models: true do
it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_within(0..255) } it { is_expected.to validate_length_of(:name).is_within(0..255) }
...@@ -96,4 +98,72 @@ describe Environment, models: true do ...@@ -96,4 +98,72 @@ describe Environment, models: true do
is_expected.to be_nil is_expected.to be_nil
end end
end end
describe '#stoppable?' do
subject { environment.stoppable? }
context 'when no other actions' do
it { is_expected.to be_falsey }
end
context 'when matching action is defined' do
let(:build) { create(:ci_build) }
let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
context 'when environment is available' do
before do
environment.start
end
it { is_expected.to be_truthy }
end
context 'when environment is stopped' do
before do
environment.stop
end
it { is_expected.to be_falsey }
end
end
end
describe '#stop!' do
let(:user) { create(:user) }
subject { environment.stop!(user) }
before do
expect(environment).to receive(:stoppable?).and_call_original
end
context 'when no other actions' do
it { is_expected.to be_nil }
end
context 'when matching action is defined' do
let(:build) { create(:ci_build) }
let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
context 'when action did not yet finish' do
let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
it 'returns the same action' do
expect(subject).to eq(close_action)
expect(subject.user).to eq(user)
end
end
context 'if action did finish' do
let!(:close_action) { create(:ci_build, :manual, :success, pipeline: build.pipeline, name: 'close_app') }
it 'returns a new action of the same type' do
is_expected.to be_persisted
expect(subject.name).to eq(close_action.name)
expect(subject.user).to eq(user)
end
end
end
end
end end
...@@ -7,11 +7,13 @@ describe CreateDeploymentService, services: true do ...@@ -7,11 +7,13 @@ describe CreateDeploymentService, services: true do
let(:service) { described_class.new(project, user, params) } let(:service) { described_class.new(project, user, params) }
describe '#execute' do describe '#execute' do
let(:options) { nil }
let(:params) do let(:params) do
{ environment: 'production', { environment: 'production',
ref: 'master', ref: 'master',
tag: false, tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142', sha: '97de212e80737a608d939f648d959671fb0a0142',
options: options
} }
end end
...@@ -28,7 +30,7 @@ describe CreateDeploymentService, services: true do ...@@ -28,7 +30,7 @@ describe CreateDeploymentService, services: true do
end end
context 'when environment exist' do context 'when environment exist' do
before { create(:environment, project: project, name: 'production') } let!(:environment) { create(:environment, project: project, name: 'production') }
it 'does not create a new environment' do it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count } expect { subject }.not_to change { Environment.count }
...@@ -37,6 +39,46 @@ describe CreateDeploymentService, services: true do ...@@ -37,6 +39,46 @@ describe CreateDeploymentService, services: true do
it 'does create a deployment' do it 'does create a deployment' do
expect(subject).to be_persisted expect(subject).to be_persisted
end end
context 'and start action is defined' do
let(:options) { { action: 'start' } }
context 'and environment is stopped' do
before do
environment.stop
end
it 'makes environment available' do
subject
expect(environment.reload).to be_available
end
it 'does create a deployment' do
expect(subject).to be_persisted
end
end
end
context 'and stop action is defined' do
let(:options) { { action: 'stop' } }
context 'and environment is available' do
before do
environment.start
end
it 'makes environment stopped' do
subject
expect(environment.reload).to be_stopped
end
it 'does not create a deployment' do
expect(subject).to be_nil
end
end
end
end end
context 'for environment with invalid name' do context 'for environment with invalid name' do
...@@ -53,7 +95,7 @@ describe CreateDeploymentService, services: true do ...@@ -53,7 +95,7 @@ describe CreateDeploymentService, services: true do
end end
it 'does not create a deployment' do it 'does not create a deployment' do
expect(subject).not_to be_persisted expect(subject).to be_nil
end end
end end
...@@ -83,6 +125,25 @@ describe CreateDeploymentService, services: true do ...@@ -83,6 +125,25 @@ describe CreateDeploymentService, services: true do
it 'does create a new deployment' do it 'does create a new deployment' do
expect(subject).to be_persisted expect(subject).to be_persisted
end end
context 'and environment exist' do
let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') }
it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count }
end
it 'updates external url' do
subject
expect(subject.environment.name).to eq('review-apps/feature-review-apps')
expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
end
it 'does create a new deployment' do
expect(subject).to be_persisted
end
end
end end
context 'when project was removed' do context 'when project was removed' do
......
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