diff options
33 files changed, 657 insertions, 102 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e0382c791..f4d317fd1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - API: all unknown routing will be handled with 404 Not Found - Add docs for request profiling + - Delete dynamic environments - Make guests unable to view MRs on private projects ## 8.12.7 diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index fcadc4bc515..3ff6851d59b 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -17,6 +17,12 @@ View on <%- external_url_formatted %> </a> </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>`; @@ -205,6 +211,11 @@ if ($(`.mr-state-widget #${ environment.id }`).length) return; const $template = $(DEPLOYMENT_TEMPLATE); 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) { environment.deployed_at = $.timeago(environment.deployed_at) + '.'; } else { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 820cc0fc991..12ee0a5dc3d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -38,6 +38,14 @@ color: $gl-dark-link-color; } + .stop-env-link { + color: $table-text-gray; + + .stop-env-icon { + font-size: 14px; + } + } + .deployment { .build-column { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 101472278e2..35a1877df95 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -183,6 +183,15 @@ .ci-coverage { float: right; } + + .stop-env-container { + color: $gl-text-color; + float: right; + + a { + color: $gl-text-color; + } + } } .mr_source_commit, diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 58678f96879..ea22b2dcc15 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:edit, :update, :destroy] - before_action :environment, only: [:show, :edit, :update, :destroy] + before_action :authorize_create_deployment!, only: [:stop] + before_action :authorize_update_environment!, only: [:edit, :update] + before_action :environment, only: [:show, :edit, :update, :stop] 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 def show @@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end - def destroy - if @environment.destroy - flash[:notice] = 'Environment was successfully removed.' - else - flash[:alert] = 'Failed to remove environment.' - end + def stop + return render_404 unless @environment.stoppable? - 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 private diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a39b47b6d95..88e21b5886d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -422,10 +422,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController project = environment.project 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, name: environment.name, url: namespace_project_environment_path(project.namespace, project, environment), + stop_url: stop_url, external_url: environment.external_url, external_url_formatted: environment.formatted_external_url, deployed_at: deployment.try(:created_at), diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 3d9902d496e..1f8c5fb3d85 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base end def manual_actions - deployable.try(:other_actions) + @manual_actions ||= deployable.try(:other_actions) end def includes_commit?(commit) @@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base take 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 created_at.to_time.in_time_zone.to_s(:medium) end diff --git a/app/models/environment.rb b/app/models/environment.rb index d970bc0a005..d575f1dc73a 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base allow_nil: 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 deployments.last end @@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base external_url.gsub(/\A.*?:\/\//, '') end + + def stoppable? + available? && stop_action.present? + end + + def stop!(current_user) + return unless stoppable? + + stop_action.play(current_user) + end end diff --git a/app/models/project.rb b/app/models/project.rb index aee74c3dba1..db7301219e5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1293,7 +1293,7 @@ class Project < ActiveRecord::Base environment_ids.where(ref: ref) end - environments.where(id: environment_ids).select do |environment| + environments.available.where(id: environment_ids).select do |environment| environment.includes_commit?(commit) end end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index ff9a8310a8c..8ae15ad32f4 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -6,7 +6,13 @@ class CreateDeploymentService < BaseService ActiveRecord::Base.transaction do @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| deployment.update_merge_request_metrics! @@ -27,13 +33,12 @@ class CreateDeploymentService < BaseService tag: params[:tag], sha: params[:sha], user: current_user, - deployable: @deployable) + deployable: @deployable, + on_stop: options[:on_stop]) end - def prepare_environment - project.environments.find_or_create_by(name: expanded_name) do |environment| - environment.external_url = expanded_url - end + def environment + @environment ||= project.environments.find_or_create_by(name: expanded_name) end def expanded_name @@ -61,4 +66,8 @@ class CreateDeploymentService < BaseService def variables params[:variables] || [] end + + def action + options[:action] || 'start' + end end diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 22c4a75d213..58a214bdbd1 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,28 +1,15 @@ -- if can?(current_user, :create_deployment, deployment) && deployment.deployable - .pull-right - - - external_url = deployment.environment.external_url - - if external_url - = link_to external_url, target: '_blank', class: 'btn external-url' do - = icon('external-link') - - - actions = deployment.manual_actions - - if actions.present? - .inline - .dropdown - %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = custom_icon('icon_play') - = 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 can?(current_user, :create_deployment, deployment) + - actions = deployment.manual_actions + - if actions.present? + .inline + .dropdown + %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + = custom_icon('icon_play') + = 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 diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index ca0005abd0c..9238f232c7e 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -17,4 +17,6 @@ #{time_ago_with_tooltip(deployment.created_at)} %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 diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml new file mode 100644 index 00000000000..5941e01c6f1 --- /dev/null +++ b/app/views/projects/deployments/_rollback.haml @@ -0,0 +1,6 @@ +- 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 diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 251694e897c..b75d5df4150 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -28,4 +28,8 @@ #{time_ago_with_tooltip(last_deployment.created_at)} %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 diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml new file mode 100644 index 00000000000..4c8fe1c271b --- /dev/null +++ b/app/views/projects/environments/_external_url.html.haml @@ -0,0 +1,3 @@ +- 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') diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml new file mode 100644 index 00000000000..69848123c17 --- /dev/null +++ b/app/views/projects/environments/_stop.html.haml @@ -0,0 +1,5 @@ +- 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') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 721ba156334..8f555afcf11 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,14 +3,27 @@ = render "projects/pipelines/head" %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 = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment .environments-container - - if @environments.blank? + - if @all_environments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title You don't have any environments right now. diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 90c59223a35..bcac73d3698 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -3,14 +3,16 @@ = render "projects/pipelines/head" %div{ class: container_class } - .top-area + .top-area.adjust .col-md-9 %h3.page-title= @environment.name.capitalize .col-md-3 .nav-controls + = render 'projects/environments/external_url', 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 '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 - if @deployments.blank? diff --git a/config/routes/project.rb b/config/routes/project.rb index 711a59df744..8142e231621 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -319,7 +319,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: end end - resources :environments + resources :environments, except: [:destroy] do + member do + post :stop + end + end resource :cycle_analytics, only: [:show] diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 803cbca584d..08ad3097d34 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -16,7 +16,8 @@ class Gitlab::Seeder::Pipelines { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending }, { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running }, { 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: 'slack', stage: 'notify', when: 'manual', status: :created }, ] diff --git a/db/migrate/20161006104309_add_state_to_environment.rb b/db/migrate/20161006104309_add_state_to_environment.rb new file mode 100644 index 00000000000..ccb546654f9 --- /dev/null +++ b/db/migrate/20161006104309_add_state_to_environment.rb @@ -0,0 +1,15 @@ +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 diff --git a/db/migrate/20161017095000_add_properties_to_deployment.rb b/db/migrate/20161017095000_add_properties_to_deployment.rb new file mode 100644 index 00000000000..f620ee0de1c --- /dev/null +++ b/db/migrate/20161017095000_add_properties_to_deployment.rb @@ -0,0 +1,9 @@ +class AddPropertiesToDeployment < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :deployments, :on_stop, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 51ac0fbaeb5..5ce855fe08f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # 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 enable_extension "plpgsql" @@ -380,6 +380,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do t.string "deployable_type" t.datetime "created_at" t.datetime "updated_at" + t.string "on_stop" end 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 t.datetime "updated_at" t.string "external_url" t.string "environment_type" + t.string "state", default: "available", null: false end add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 2fd1fced65c..3e33c9399e2 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -109,6 +109,7 @@ module Ci validate_job_stage!(name, job) validate_job_dependencies!(name, job) + validate_job_environment!(name, job) end end @@ -150,6 +151,35 @@ module Ci 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) if only_params.present? return false unless matching?(only_params, ref, tag, trigger_request) diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb index d388ab6b879..9a95ef43628 100644 --- a/lib/gitlab/ci/config/node/environment.rb +++ b/lib/gitlab/ci/config/node/environment.rb @@ -8,7 +8,7 @@ module Gitlab class Environment < Entry include Validatable - ALLOWED_KEYS = %i[name url] + ALLOWED_KEYS = %i[name url action on_stop] validations do validate do @@ -35,6 +35,12 @@ module Gitlab length: { maximum: 255 }, addressable_url: 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 @@ -54,9 +60,17 @@ module Gitlab value[:url] end + def action + value[:action] || 'start' + end + + def on_stop + value[:on_stop] + end + def value case @config - when String then { name: @config } + when String then { name: @config, action: 'start' } when Hash then @config else {} end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 68ea4eeae31..b565586ee14 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -19,10 +19,22 @@ feature 'Environments', feature: true do visit namespace_project_environments_path(project.namespace, project) 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 scenario 'does show no environments' do expect(page).to have_content('You don\'t have any environments right now.') 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 context 'with environments' do @@ -32,6 +44,11 @@ feature 'Environments', feature: true do expect(page).to have_link(environment.name) 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 scenario 'does show no deployments' do expect(page).to have_content('No deployments yet') @@ -44,7 +61,7 @@ feature 'Environments', feature: true do scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) end - + scenario 'does show deployment internal id' do expect(page).to have_content(deployment.iid) end @@ -65,20 +82,51 @@ feature 'Environments', feature: true do expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end - + scenario 'does show build name and id' do expect(page).to have_link("#{build.name} (##{build.id})") 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 given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - + scenario 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) 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 @@ -127,6 +175,10 @@ feature 'Environments', feature: true do expect(page).to have_link('Re-deploy') end + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop') + end + context 'with manual action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } @@ -140,16 +192,39 @@ feature 'Environments', feature: true do expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end - + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - + scenario 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) 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 @@ -196,29 +271,4 @@ feature 'Environments', feature: true do 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 diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index bc2b0ff3e2c..c3c3ab33872 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -101,7 +101,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do expect(page).not_to have_link "Merge When Build Succeeds" end end - + def visit_merge_request(merge_request) visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) end diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb index 8e23ec50d4a..6676821b807 100644 --- a/spec/features/merge_requests/widget_deployments_spec.rb +++ b/spec/features/merge_requests/widget_deployments_spec.rb @@ -4,23 +4,58 @@ feature 'Widget Deployments Header', feature: true, js: true do include WaitForAjax describe 'when deployed to an environment' do - let(:project) { merge_request.target_project } - let(:merge_request) { create(:merge_request, :merged) } - let(:environment) { create(:environment, project: project) } - let!(:deployment) do - create(:deployment, environment: environment, sha: project.commit('master').id) - end + given(:user) { create(:user) } + given(:project) { merge_request.target_project } + given(:merge_request) { create(:merge_request, :merged) } + given(:environment) { create(:environment, project: project) } + given(:role) { :developer } + given(:sha) { project.commit('master').id } + given!(:deployment) { create(:deployment, environment: environment, sha: sha) } + given!(:manual) { } - before do - login_as :admin + background do + login_as(user) + project.team << [user, role] visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - it 'displays that the environment is deployed' do + scenario 'displays that the environment is deployed' do wait_for_ajax 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)) 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 diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 6dedd25e9d3..84f21631719 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -754,7 +754,7 @@ module Ci it 'does return production' do expect(builds.size).to eq(1) 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 @@ -796,6 +796,52 @@ module Ci expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") 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 describe "Dependencies" do diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb index df453223da7..df925ff1afd 100644 --- a/spec/lib/gitlab/ci/config/node/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb @@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Node::Environment do describe '#value' do it 'returns valid hash' do - expect(entry.value).to eq(name: 'production') + expect(entry.value).to include(name: 'production') end end @@ -87,6 +87,68 @@ describe Gitlab::Ci::Config::Node::Environment do 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 let(:config) do { name: 'review/$CI_BUILD_REF_NAME', diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 01a4a53a264..ca594a320c0 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -48,4 +48,50 @@ describe Deployment, models: true do 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 diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index e172ee8e590..a94e6d0165f 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -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(:stop_action).to(:last_deployment) } + 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_length_of(:name).is_within(0..255) } @@ -96,4 +98,72 @@ describe Environment, models: true do is_expected.to be_nil 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 diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 0b84c7262c3..cf0a18aacec 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -7,11 +7,13 @@ describe CreateDeploymentService, services: true do let(:service) { described_class.new(project, user, params) } describe '#execute' do + let(:options) { nil } let(:params) do { environment: 'production', ref: 'master', tag: false, sha: '97de212e80737a608d939f648d959671fb0a0142', + options: options } end @@ -28,7 +30,7 @@ describe CreateDeploymentService, services: true do end 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 expect { subject }.not_to change { Environment.count } @@ -37,6 +39,46 @@ describe CreateDeploymentService, services: true do it 'does create a deployment' do expect(subject).to be_persisted 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 context 'for environment with invalid name' do @@ -53,7 +95,7 @@ describe CreateDeploymentService, services: true do end it 'does not create a deployment' do - expect(subject).not_to be_persisted + expect(subject).to be_nil end end @@ -83,6 +125,25 @@ describe CreateDeploymentService, services: true do it 'does create a new deployment' do expect(subject).to be_persisted 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 context 'when project was removed' do |