Commit 189565fa authored by Shinya Maeda's avatar Shinya Maeda

Persist and control auto stop date for environments

This commit persists and controls auto stop date for environments
parent 46f2255d
...@@ -7,14 +7,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -7,14 +7,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
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_stop_environment!, only: [:stop] before_action :authorize_stop_environment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_update_environment!, only: [:edit, :update, :cancel_auto_stop]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :cancel_auto_stop]
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index] before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:prometheus_computed_alerts)
end end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
def index def index
@environments = project.environments @environments = project.environments
...@@ -104,6 +105,27 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -104,6 +105,27 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def cancel_auto_stop
result = Environments::ResetAutoStopService.new(project, current_user)
.execute(environment)
if result[:status] == :success
respond_to do |format|
message = _('Auto stop successfully canceled.')
format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
format.json { render json: { message: message }, status: :ok }
end
else
respond_to do |format|
message = result[:message]
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: message }) }
format.json { render json: { message: message }, status: :unprocessable_entity }
end
end
end
def terminal def terminal
# Currently, this acts as a hint to load the terminal details into the cache # Currently, this acts as a hint to load the terminal details into the cache
# if they aren't there already. In the future, users will need these details # if they aren't there already. In the future, users will need these details
...@@ -175,8 +197,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -175,8 +197,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
def expire_etag_cache def expire_etag_cache
return if request.format.json?
# this forces to reload json content # this forces to reload json content
Gitlab::EtagCaching::Store.new.tap do |store| Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(project_environments_path(project, format: :json)) store.touch(project_environments_path(project, format: :json))
...@@ -222,6 +242,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -222,6 +242,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def authorize_stop_environment! def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment) access_denied! unless can?(current_user, :stop_environment, environment)
end end
def authorize_update_environment!
access_denied! unless can?(current_user, :update_environment, environment)
end
end end
Projects::EnvironmentsController.prepend_if_ee('EE::Projects::EnvironmentsController') Projects::EnvironmentsController.prepend_if_ee('EE::Projects::EnvironmentsController')
...@@ -17,6 +17,7 @@ module Ci ...@@ -17,6 +17,7 @@ module Ci
delegate :timeout, to: :metadata, prefix: true, allow_nil: true delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true
before_create :ensure_metadata before_create :ensure_metadata
end end
...@@ -47,8 +48,11 @@ module Ci ...@@ -47,8 +48,11 @@ module Ci
def options=(value) def options=(value)
write_metadata_attribute(:options, :config_options, value) write_metadata_attribute(:options, :config_options, value)
ensure_metadata.tap do |metadata|
# Store presence of exposed artifacts in build metadata to make it easier to query # Store presence of exposed artifacts in build metadata to make it easier to query
ensure_metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present? metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present?
metadata.environment_auto_stop_in = value&.dig(:environment, :auto_stop_in)
end
end end
def yaml_variables=(value) def yaml_variables=(value)
......
...@@ -162,6 +162,10 @@ class Environment < ApplicationRecord ...@@ -162,6 +162,10 @@ class Environment < ApplicationRecord
stop_action&.play(current_user) stop_action&.play(current_user)
end end
def reset_auto_stop
update_column(:auto_stop_at, nil)
end
def actions_for(environment) def actions_for(environment)
return [] unless manual_actions return [] unless manual_actions
...@@ -261,6 +265,17 @@ class Environment < ApplicationRecord ...@@ -261,6 +265,17 @@ class Environment < ApplicationRecord
end end
end end
def auto_stop_in
auto_stop_at - Time.now if auto_stop_at
end
def auto_stop_in=(value)
return unless value
return unless parsed_result = ChronicDuration.parse(value)
self.auto_stop_at = parsed_result.seconds.from_now
end
private private
def generate_slug def generate_slug
......
...@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy ...@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :update_container_image enable :update_container_image
enable :destroy_container_image enable :destroy_container_image
enable :create_environment enable :create_environment
enable :update_environment
enable :create_deployment enable :create_deployment
enable :update_deployment enable :update_deployment
enable :create_release enable :create_release
...@@ -278,8 +279,6 @@ class ProjectPolicy < BasePolicy ...@@ -278,8 +279,6 @@ class ProjectPolicy < BasePolicy
enable :admin_board enable :admin_board
enable :push_to_delete_protected_branch enable :push_to_delete_protected_branch
enable :update_project_snippet enable :update_project_snippet
enable :update_environment
enable :update_deployment
enable :admin_project_snippet enable :admin_project_snippet
enable :admin_project_member enable :admin_project_member
enable :admin_note enable :admin_note
......
...@@ -24,6 +24,10 @@ class EnvironmentEntity < Grape::Entity ...@@ -24,6 +24,10 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment) stop_project_environment_path(environment.project, environment)
end end
expose :cancel_auto_stop_path, if: -> (*) { can_update_environment? } do |environment|
cancel_auto_stop_project_environment_path(environment.project, environment)
end
expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment| expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment|
cluster.cluster_type cluster.cluster_type
end end
...@@ -37,6 +41,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -37,6 +41,7 @@ class EnvironmentEntity < Grape::Entity
end end
expose :created_at, :updated_at expose :created_at, :updated_at
expose :auto_stop_at, expose_nil: false
expose :can_stop do |environment| expose :can_stop do |environment|
environment.available? && can?(current_user, :stop_environment, environment) environment.available? && can?(current_user, :stop_environment, environment)
...@@ -54,6 +59,10 @@ class EnvironmentEntity < Grape::Entity ...@@ -54,6 +59,10 @@ class EnvironmentEntity < Grape::Entity
can?(request.current_user, :create_environment_terminal, environment) can?(request.current_user, :create_environment_terminal, environment)
end end
def can_update_environment?
can?(current_user, :update_environment, environment)
end
def cluster_platform_kubernetes? def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes) deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end end
......
...@@ -29,6 +29,7 @@ module Deployments ...@@ -29,6 +29,7 @@ module Deployments
environment.external_url = url environment.external_url = url
end end
renew_auto_stop_in
environment.fire_state_event(action) environment.fire_state_event(action)
if environment.save && !environment.stopped? if environment.save && !environment.stopped?
...@@ -63,6 +64,12 @@ module Deployments ...@@ -63,6 +64,12 @@ module Deployments
def action def action
environment_options[:action] || 'start' environment_options[:action] || 'start'
end end
def renew_auto_stop_in
return unless deployable
environment.auto_stop_in = deployable.environment_auto_stop_in
end
end end
end end
......
# frozen_string_literal: true
module Environments
class ResetAutoStopService < ::BaseService
def execute(environment)
return error(_('Failed to cancel auto stop because you do not have permission to update the environment.')) unless can_update_environment?(environment)
return error(_('Failed to cancel auto stop because the environment is not set as auto stop.')) unless environment.auto_stop_at?
if environment.reset_auto_stop
success
else
error(_('Failed to cancel auto stop because failed to update the environment.'))
end
end
private
def can_update_environment?(environment)
can?(current_user, :update_environment, environment)
end
end
end
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm' = stylesheet_link_tag 'page_bundles/xterm'
- if can?(current_user, :stop_environment, @environment) - if @environment.available? && can?(current_user, :stop_environment, @environment)
#stop-environment-modal.modal.fade{ tabindex: -1 } #stop-environment-modal.modal.fade{ tabindex: -1 }
.modal-dialog .modal-dialog
.modal-content .modal-content
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
= render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment) - if can?(current_user, :update_environment, @environment)
= link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn' = link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- if can?(current_user, :stop_environment, @environment) - if @environment.available? && can?(current_user, :stop_environment, @environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal', = button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do target: '#stop-environment-modal' } do
= sprite_icon('stop') = sprite_icon('stop')
......
...@@ -224,6 +224,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -224,6 +224,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :environments, except: [:destroy] do resources :environments, except: [:destroy] do
member do member do
post :stop post :stop
post :cancel_auto_stop
get :terminal get :terminal
get :metrics get :metrics
get :additional_metrics get :additional_metrics
......
# frozen_string_literal: true
class AddAutoStopInToEnvironments < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :environments, :auto_stop_at, :datetime_with_timezone
end
end
# frozen_string_literal: true
class AddEnvironmentAutoStopInToCiBuildsMetadata < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
add_column :ci_builds_metadata, :environment_auto_stop_in, :string, limit: 255
end
def down
remove_column :ci_builds_metadata, :environment_auto_stop_in
end
end
...@@ -717,6 +717,7 @@ ActiveRecord::Schema.define(version: 2019_12_04_093410) do ...@@ -717,6 +717,7 @@ ActiveRecord::Schema.define(version: 2019_12_04_093410) do
t.jsonb "config_options" t.jsonb "config_options"
t.jsonb "config_variables" t.jsonb "config_variables"
t.boolean "has_exposed_artifacts" t.boolean "has_exposed_artifacts"
t.string "environment_auto_stop_in", limit: 255
t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true
t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts", where: "(has_exposed_artifacts IS TRUE)" t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts", where: "(has_exposed_artifacts IS TRUE)"
t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_interruptible", where: "(interruptible = true)" t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_interruptible", where: "(interruptible = true)"
...@@ -1447,6 +1448,7 @@ ActiveRecord::Schema.define(version: 2019_12_04_093410) do ...@@ -1447,6 +1448,7 @@ ActiveRecord::Schema.define(version: 2019_12_04_093410) do
t.string "environment_type" t.string "environment_type"
t.string "state", default: "available", null: false t.string "state", default: "available", null: false
t.string "slug", null: false t.string "slug", null: false
t.datetime_with_timezone "auto_stop_at"
t.index ["name"], name: "index_environments_on_name_varchar_pattern_ops", opclass: :varchar_pattern_ops t.index ["name"], name: "index_environments_on_name_varchar_pattern_ops", opclass: :varchar_pattern_ops
t.index ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true t.index ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true
t.index ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true t.index ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true
......
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
prevent :create_environment_terminal prevent :create_environment_terminal
prevent :create_deployment prevent :create_deployment
prevent :update_deployment prevent :update_deployment
prevent :update_environment
end end
private private
......
...@@ -282,6 +282,31 @@ describe Projects::EnvironmentsController do ...@@ -282,6 +282,31 @@ describe Projects::EnvironmentsController do
end end
end end
describe 'POST #cancel_auto_stop' do
subject { post :cancel_auto_stop, params: params }
let(:params) { environment_params }
context 'when environment is set as auto-stop' do
let(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
it_behaves_like 'successful response for #cancel_auto_stop'
context 'when the environment is protected' do
before do
stub_licensed_features(protected_environments: true)
create(:protected_environment, name: 'staging', project: project)
end
it 'shows NOT Found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
def environment_params(opts = {}) def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, opts.reverse_merge(namespace_id: project.namespace,
project_id: project, project_id: project,
......
...@@ -71,6 +71,8 @@ ...@@ -71,6 +71,8 @@
}, },
"can_stop": { "can_stop": {
"type": "boolean" "type": "boolean"
} },
"cancel_auto_stop_path": { "type": "string" },
"auto_stop_at": { "type": "string", "format": "date-time" }
} }
} }
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
class Environment < ::Gitlab::Config::Entry::Node class Environment < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[name url action on_stop kubernetes].freeze ALLOWED_KEYS = %i[name url action on_stop auto_stop_in kubernetes].freeze
entry :kubernetes, Entry::Kubernetes, description: 'Kubernetes deployment configuration.' entry :kubernetes, Entry::Kubernetes, description: 'Kubernetes deployment configuration.'
...@@ -49,6 +49,7 @@ module Gitlab ...@@ -49,6 +49,7 @@ module Gitlab
validates :on_stop, type: String, allow_nil: true validates :on_stop, type: String, allow_nil: true
validates :kubernetes, type: Hash, allow_nil: true validates :kubernetes, type: Hash, allow_nil: true
validates :auto_stop_in, duration: true, allow_nil: true
end end
end end
...@@ -80,6 +81,10 @@ module Gitlab ...@@ -80,6 +81,10 @@ module Gitlab
value[:kubernetes] value[:kubernetes]
end end
def auto_stop_in
value[:auto_stop_in]
end
def value def value
case @config case @config
when String then { name: @config, action: 'start' } when String then { name: @config, action: 'start' }
......
...@@ -2312,6 +2312,9 @@ msgstr "" ...@@ -2312,6 +2312,9 @@ msgstr ""
msgid "Auto License Compliance" msgid "Auto License Compliance"
msgstr "" msgstr ""
msgid "Auto stop successfully canceled."
msgstr ""
msgid "Auto-cancel redundant, pending pipelines" msgid "Auto-cancel redundant, pending pipelines"
msgstr "" msgstr ""
...@@ -7251,6 +7254,15 @@ msgstr "" ...@@ -7251,6 +7254,15 @@ msgstr ""
msgid "Failed to assign a user because no user was found." msgid "Failed to assign a user because no user was found."
msgstr "" msgstr ""
msgid "Failed to cancel auto stop because failed to update the environment."
msgstr ""
msgid "Failed to cancel auto stop because the environment is not set as auto stop."
msgstr ""
msgid "Failed to cancel auto stop because you do not have permission to update the environment."
msgstr ""
msgid "Failed to change the owner" msgid "Failed to change the owner"
msgstr "" msgstr ""
......
...@@ -5,16 +5,14 @@ require 'spec_helper' ...@@ -5,16 +5,14 @@ require 'spec_helper'
describe Projects::EnvironmentsController do describe Projects::EnvironmentsController do
include MetricsDashboardHelpers include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:maintainer) { create(:user, name: 'main-dos').tap { |u| project.add_maintainer(u) } }
let_it_be(:reporter) { create(:user, name: 'repo-dos').tap { |u| project.add_reporter(u) } }
let(:user) { maintainer }
let_it_be(:environment) do let!(:environment) { create(:environment, name: 'production', project: project) }
create(:environment, name: 'production', project: project)
end
before do before do
project.add_maintainer(user)
sign_in(user) sign_in(user)
end end
...@@ -245,6 +243,36 @@ describe Projects::EnvironmentsController do ...@@ -245,6 +243,36 @@ describe Projects::EnvironmentsController do
end end
end end
describe 'POST #cancel_auto_stop' do
subject { post :cancel_auto_stop, params: params }
let(:params) { environment_params }
context 'when environment is set as auto-stop' do
let(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
it_behaves_like 'successful response for #cancel_auto_stop'
context 'when user is reporter' do
let(:user) { reporter }
it 'shows NOT Found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when environment is not set as auto-stop' do
let(:environment) { create(:environment, name: 'staging', project: project) }
it_behaves_like 'failed response for #cancel_auto_stop' do
let(:message) { 'the environment is not set as auto stop' }
end
end
end
describe 'GET #terminal' do describe 'GET #terminal' do
context 'with valid id' do context 'with valid id' do
it 'responds with a status code 200' do it 'responds with a status code 200' do
...@@ -320,23 +348,23 @@ describe Projects::EnvironmentsController do ...@@ -320,23 +348,23 @@ describe Projects::EnvironmentsController do
end end
describe 'GET #metrics_redirect' do describe 'GET #metrics_redirect' do
let(:project) { create(:project) }
it 'redirects to environment if it exists' do it 'redirects to environment if it exists' do
environment = create(:environment, name: 'production', project: project)
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project } get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to redirect_to(environment_metrics_path(environment)) expect(response).to redirect_to(environment_metrics_path(environment))
end end
it 'redirects to empty metrics page if no environment exists' do context 'when there are no environments' do
let(:environment) { }
it 'redirects to empty metrics page' do
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project } get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to be_ok expect(response).to be_ok
expect(response).to render_template 'empty_metrics' expect(response).to render_template 'empty_metrics'
end end
end end
end
describe 'GET #metrics' do describe 'GET #metrics' do
before do before do
...@@ -549,6 +577,10 @@ describe Projects::EnvironmentsController do ...@@ -549,6 +577,10 @@ describe Projects::EnvironmentsController do
let(:project) { project_with_dashboard(dashboard_path, dashboard_yml) } let(:project) { project_with_dashboard(dashboard_path, dashboard_yml) }
let(:environment) { create(:environment, name: 'production', project: project) } let(:environment) { create(:environment, name: 'production', project: project) }
before do
project.add_maintainer(user)
end
it_behaves_like 'the specified dashboard', 'Test Dashboard' it_behaves_like 'the specified dashboard', 'Test Dashboard'
end end
......
...@@ -44,5 +44,13 @@ FactoryBot.define do ...@@ -44,5 +44,13 @@ FactoryBot.define do
status { 'created' } status { 'created' }
self.when { 'manual' } self.when { 'manual' }
end end
trait :auto_stopped do
auto_stop_at { 1.day.ago }
end
trait :will_auto_stop do
auto_stop_at { 1.day.from_now }
end
end end
end end
...@@ -144,8 +144,8 @@ describe 'Environments page', :js do ...@@ -144,8 +144,8 @@ describe 'Environments page', :js do
expect(page).to have_content('No deployments yet') expect(page).to have_content('No deployments yet')
end end
it 'does not show stip button when environment is not stoppable' do it 'shows stop button when environment is not stoppable' do
expect(page).not_to have_selector(stop_button_selector) expect(page).to have_selector(stop_button_selector)
end end
end end
...@@ -205,7 +205,7 @@ describe 'Environments page', :js do ...@@ -205,7 +205,7 @@ describe 'Environments page', :js do
end end
it 'shows a stop button' do it 'shows a stop button' do
expect(page).not_to have_selector(stop_button_selector) expect(page).to have_selector(stop_button_selector)
end end
it 'does not show external link button' do it 'does not show external link button' do
......
...@@ -24,9 +24,11 @@ ...@@ -24,9 +24,11 @@
"has_stop_action": { "type": "boolean" }, "has_stop_action": { "type": "boolean" },
"environment_path": { "type": "string" }, "environment_path": { "type": "string" },
"stop_path": { "type": "string" }, "stop_path": { "type": "string" },
"cancel_auto_stop_path": { "type": "string" },
"folder_path": { "type": "string" }, "folder_path": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" },
"auto_stop_at": { "type": "string", "format": "date-time" },
"can_stop": { "type": "boolean" }, "can_stop": { "type": "boolean" },
"cluster_type": { "type": "types/nullable_string.json" }, "cluster_type": { "type": "types/nullable_string.json" },
"terminal_path": { "type": "types/nullable_string.json" }, "terminal_path": { "type": "types/nullable_string.json" },
......
...@@ -206,6 +206,35 @@ describe Gitlab::Ci::Config::Entry::Environment do ...@@ -206,6 +206,35 @@ describe Gitlab::Ci::Config::Entry::Environment do
end end
end end
context 'when auto_stop_in is specified' do
let(:config) do
{
name: 'review/$CI_COMMIT_REF_NAME',
url: 'https://$CI_COMMIT_REF_NAME.review.gitlab.com',
on_stop: 'stop_review',
auto_stop_in: auto_stop_in
}
end
context 'when auto_stop_in is correct format' do
let(:auto_stop_in) { '2 days' }
it 'becomes valid' do
expect(entry).to be_valid
expect(entry.auto_stop_in).to eq(auto_stop_in)
end
end
context 'when auto_stop_in is invalid format' do
let(:auto_stop_in) { 'invalid' }
it 'becomes invalid' do
expect(entry).not_to be_valid
expect(entry.errors).to include 'environment auto stop in should be a duration'
end
end
end
context 'when configuration is invalid' do context 'when configuration is invalid' do
context 'when configuration is an array' do context 'when configuration is an array' do
let(:config) { ['env'] } let(:config) { ['env'] }
......
...@@ -4113,4 +4113,20 @@ describe Ci::Build do ...@@ -4113,4 +4113,20 @@ describe Ci::Build do
end end
end end
end end
describe '#environment_auto_stop_in' do
subject { build.environment_auto_stop_in }
context 'when build option has environment auto_stop_in' do
let(:build) { create(:ci_build, options: { environment: { name: 'test', auto_stop_in: '1 day' } }) }
it { is_expected.to eq('1 day') }
end
context 'when build option does not have environment auto_stop_in' do
let(:build) { create(:ci_build) }
it { is_expected.to be_nil }
end
end
end end
...@@ -441,6 +441,16 @@ describe Environment, :use_clean_rails_memory_store_caching do ...@@ -441,6 +441,16 @@ describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
describe '#reset_auto_stop' do
subject { environment.reset_auto_stop }
let(:environment) { create(:environment, :auto_stopped) }
it 'nullifies the auto_stop_at' do
expect { subject }.to change(environment, :auto_stop_at).from(Time).to(nil)
end
end
describe '#actions_for' do describe '#actions_for' do
let(:deployment) { create(:deployment, :success, environment: environment) } let(:deployment) { create(:deployment, :success, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline } let(:pipeline) { deployment.deployable.pipeline }
...@@ -1088,6 +1098,52 @@ describe Environment, :use_clean_rails_memory_store_caching do ...@@ -1088,6 +1098,52 @@ describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
describe '#auto_stop_in' do
subject { environment.auto_stop_in }
context 'when environment will be expired' do
let(:environment) { build(:environment, :will_auto_stop) }
it 'returns when it will expire' do
Timecop.freeze { is_expected.to eq(1.day.to_i) }
end
end
context 'when environment is not expired' do
let(:environment) { build(:environment) }
it { is_expected.to be_nil }
end
end
describe '#auto_stop_in=' do
subject { environment.auto_stop_in = value }
let(:environment) { build(:environment) }
where(:value, :expected_result) do
'2 days' | 2.days.to_i
'1 week' | 1.week.to_i
'2h20min' | 2.hours.to_i + 20.minutes.to_i
'abcdef' | ChronicDuration::DurationParseError
'' | nil
nil | nil
end
with_them do
it 'sets correct auto_stop_in' do
Timecop.freeze do
if expected_result.is_a?(Integer) || expected_result.nil?
subject
expect(environment.auto_stop_in).to eq(expected_result)
else
expect { subject }.to raise_error(expected_result)
end
end
end
end
end
describe '.find_or_create_by_name' do describe '.find_or_create_by_name' do
it 'finds an existing environment if it exists' do it 'finds an existing environment if it exists' do
env = create(:environment) env = create(:environment)
......
...@@ -68,7 +68,7 @@ describe EnvironmentPolicy do ...@@ -68,7 +68,7 @@ describe EnvironmentPolicy do
nil | false nil | false
:guest | false :guest | false
:reporter | false :reporter | false
:developer | false :developer | true
:maintainer | true :maintainer | true
end end
......
...@@ -42,13 +42,13 @@ describe ProjectPolicy do ...@@ -42,13 +42,13 @@ describe ProjectPolicy do
update_commit_status create_build update_build create_pipeline update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image destroy_container_image resolve_note create_container_image update_container_image destroy_container_image
create_environment create_deployment update_deployment create_release update_release create_environment update_environment create_deployment update_deployment create_release update_release
] ]
end end
let(:base_maintainer_permissions) do let(:base_maintainer_permissions) do
%i[ %i[
push_to_delete_protected_branch update_project_snippet update_environment push_to_delete_protected_branch update_project_snippet
admin_project_snippet admin_project_member admin_note admin_wiki admin_project admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster admin_pipeline admin_environment admin_deployment destroy_release add_cluster
......
...@@ -62,4 +62,12 @@ describe EnvironmentEntity do ...@@ -62,4 +62,12 @@ describe EnvironmentEntity do
end end
end end
end end
context 'with auto_stop_in' do
let(:environment) { create(:environment, :will_auto_stop) }
it 'exposes auto stop related information' do
expect(subject).to include(:cancel_auto_stop_path, :auto_stop_at)
end
end
end end
...@@ -781,6 +781,25 @@ describe Ci::CreatePipelineService do ...@@ -781,6 +781,25 @@ describe Ci::CreatePipelineService do
end end
end end
context 'with environment with auto_stop_in' do
before do
config = YAML.dump(
deploy: {
environment: { name: "review/$CI_COMMIT_REF_NAME", auto_stop_in: '1 day' },
script: 'ls'
})
stub_ci_pipeline_yaml_file(config)
end
it 'creates the environment with auto stop in' do
result = execute_service
expect(result).to be_persisted
expect(result.builds.first.options[:environment][:auto_stop_in]).to eq('1 day')
end
end
context 'with environment name including persisted variables' do context 'with environment name including persisted variables' do
before do before do
config = YAML.dump( config = YAML.dump(
......
...@@ -115,6 +115,21 @@ describe Deployments::AfterCreateService do ...@@ -115,6 +115,21 @@ describe Deployments::AfterCreateService do
expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com')
end end
end end
context 'when auto_stop_in are used' do
let(:options) do
{ name: 'production', auto_stop_in: '1 day' }
end
it 'renews auto stop at' do
Timecop.freeze do
environment.update!(auto_stop_at: nil)
expect { subject.execute }
.to change { environment.reset.auto_stop_at&.round }.from(nil).to(1.day.since.round)
end
end
end
end end
describe '#expanded_environment_url' do describe '#expanded_environment_url' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Environments::ResetAutoStopService do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
let(:user) { developer }
let(:service) { described_class.new(project, user) }
describe '#execute' do
subject { service.execute(environment) }
context 'when environment will be stopped automatically' do
let(:environment) { create(:environment, :will_auto_stop, project: project) }
it 'resets auto stop' do
expect(environment).to receive(:reset_auto_stop).and_call_original
expect(subject[:status]).to eq(:success)
end
context 'when failed to reset auto stop' do
before do
expect(environment).to receive(:reset_auto_stop) { false }
end
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq('Failed to cancel auto stop because failed to update the environment.')
end
end
context 'when user is reporter' do
let(:user) { reporter }
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq('Failed to cancel auto stop because you do not have permission to update the environment.')
end
end
end
context 'when environment will not be stopped automatically' do
let(:environment) { create(:environment, project: project) }
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq('Failed to cancel auto stop because the environment is not set as auto stop.')
end
end
end
end
...@@ -39,12 +39,13 @@ RSpec.shared_context 'ProjectPolicy context' do ...@@ -39,12 +39,13 @@ RSpec.shared_context 'ProjectPolicy context' do
update_pipeline create_merge_request_from create_wiki push_code update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image resolve_note create_container_image update_container_image
create_environment create_deployment update_deployment create_release update_release create_environment create_deployment update_deployment create_release update_release
update_environment
] ]
end end
let(:base_maintainer_permissions) do let(:base_maintainer_permissions) do
%i[ %i[
push_to_delete_protected_branch update_project_snippet update_environment push_to_delete_protected_branch update_project_snippet
admin_project_snippet admin_project_member admin_note admin_wiki admin_project admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster admin_pipeline admin_environment admin_deployment destroy_release add_cluster
......
# frozen_string_literal: true
shared_examples_for 'successful response for #cancel_auto_stop' do
include GitlabRoutingHelper
context 'when request is html' do
let(:params) { environment_params(format: :html) }
it 'redirects to show page' do
subject
expect(response).to redirect_to(environment_path(environment))
expect(flash[:notice]).to eq('Auto stop successfully canceled.')
end
it 'expires etag caching' do
expect_next_instance_of(Gitlab::EtagCaching::Store) do |etag_caching|
expect(etag_caching).to receive(:touch).with(project_environments_path(project, format: :json))
end
subject
end
end
context 'when request is js' do
let(:params) { environment_params(format: :json) }
it 'responds as ok' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['message']).to eq('Auto stop successfully canceled.')
end
it 'expires etag caching' do
expect_next_instance_of(Gitlab::EtagCaching::Store) do |etag_caching|
expect(etag_caching).to receive(:touch).with(project_environments_path(project, format: :json))
end
subject
end
end
end
shared_examples_for 'failed response for #cancel_auto_stop' do
context 'when request is html' do
let(:params) { environment_params(format: :html) }
it 'redirects to show page' do
subject
expect(response).to redirect_to(environment_path(environment))
expect(flash[:alert]).to eq("Failed to cancel auto stop because #{message}.")
end
end
context 'when request is js' do
let(:params) { environment_params(format: :json) }
it 'responds as unprocessable entity' do
subject
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq("Failed to cancel auto stop because #{message}.")
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment