Skip to content
Snippets Groups Projects
Commit 189565fa authored by Shinya Maeda's avatar Shinya Maeda
Browse files

Persist and control auto stop date for environments

This commit persists and controls auto stop date for environments
parent 46f2255d
No related branches found
No related tags found
No related merge requests found
Showing
with 220 additions and 30 deletions
Loading
Loading
@@ -7,14 +7,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
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 :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 :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
push_frontend_feature_flag(:prometheus_computed_alerts)
end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
 
def index
@environments = project.environments
Loading
Loading
@@ -104,6 +105,27 @@ def stop
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
# 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
Loading
Loading
@@ -175,8 +197,6 @@ def verify_api_request!
end
 
def expire_etag_cache
return if request.format.json?
# this forces to reload json content
Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(project_environments_path(project, format: :json))
Loading
Loading
@@ -222,6 +242,10 @@ def serialize_environments(request, response, nested = false)
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
def authorize_update_environment!
access_denied! unless can?(current_user, :update_environment, environment)
end
end
 
Projects::EnvironmentsController.prepend_if_ee('EE::Projects::EnvironmentsController')
Loading
Loading
@@ -17,6 +17,7 @@ module Metadatable
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :interruptible, 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
end
 
Loading
Loading
@@ -47,8 +48,11 @@ def yaml_variables
def options=(value)
write_metadata_attribute(:options, :config_options, value)
 
# 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?
ensure_metadata.tap do |metadata|
# Store presence of exposed artifacts in build metadata to make it easier to query
metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present?
metadata.environment_auto_stop_in = value&.dig(:environment, :auto_stop_in)
end
end
 
def yaml_variables=(value)
Loading
Loading
Loading
Loading
@@ -162,6 +162,10 @@ def stop_with_action!(current_user)
stop_action&.play(current_user)
end
 
def reset_auto_stop
update_column(:auto_stop_at, nil)
end
def actions_for(environment)
return [] unless manual_actions
 
Loading
Loading
@@ -261,6 +265,17 @@ def knative_services_finder
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
 
def generate_slug
Loading
Loading
Loading
Loading
@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :update_container_image
enable :destroy_container_image
enable :create_environment
enable :update_environment
enable :create_deployment
enable :update_deployment
enable :create_release
Loading
Loading
@@ -278,8 +279,6 @@ class ProjectPolicy < BasePolicy
enable :admin_board
enable :push_to_delete_protected_branch
enable :update_project_snippet
enable :update_environment
enable :update_deployment
enable :admin_project_snippet
enable :admin_project_member
enable :admin_note
Loading
Loading
Loading
Loading
@@ -24,6 +24,10 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment)
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|
cluster.cluster_type
end
Loading
Loading
@@ -37,6 +41,7 @@ class EnvironmentEntity < Grape::Entity
end
 
expose :created_at, :updated_at
expose :auto_stop_at, expose_nil: false
 
expose :can_stop do |environment|
environment.available? && can?(current_user, :stop_environment, environment)
Loading
Loading
@@ -54,6 +59,10 @@ def can_access_terminal?
can?(request.current_user, :create_environment_terminal, environment)
end
 
def can_update_environment?
can?(current_user, :update_environment, environment)
end
def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end
Loading
Loading
Loading
Loading
@@ -29,6 +29,7 @@ def update_environment(deployment)
environment.external_url = url
end
 
renew_auto_stop_in
environment.fire_state_event(action)
 
if environment.save && !environment.stopped?
Loading
Loading
@@ -63,6 +64,12 @@ def environment_url
def action
environment_options[:action] || 'start'
end
def renew_auto_stop_in
return unless deployable
environment.auto_stop_in = deployable.environment_auto_stop_in
end
end
end
 
Loading
Loading
# 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
Loading
Loading
@@ -5,7 +5,7 @@
- content_for :page_specific_javascripts do
= 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 }
.modal-dialog
.modal-content
Loading
Loading
@@ -40,7 +40,7 @@
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= 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',
target: '#stop-environment-modal' } do
= sprite_icon('stop')
Loading
Loading
Loading
Loading
@@ -224,6 +224,7 @@
resources :environments, except: [:destroy] do
member do
post :stop
post :cancel_auto_stop
get :terminal
get :metrics
get :additional_metrics
Loading
Loading
# 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
Loading
Loading
@@ -717,6 +717,7 @@
t.jsonb "config_options"
t.jsonb "config_variables"
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_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)"
Loading
Loading
@@ -1447,6 +1448,7 @@
t.string "environment_type"
t.string "state", default: "available", 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 ["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
Loading
Loading
Loading
Loading
@@ -11,6 +11,7 @@ module EnvironmentPolicy
prevent :create_environment_terminal
prevent :create_deployment
prevent :update_deployment
prevent :update_environment
end
 
private
Loading
Loading
Loading
Loading
@@ -282,6 +282,31 @@
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 = {})
opts.reverse_merge(namespace_id: project.namespace,
project_id: project,
Loading
Loading
Loading
Loading
@@ -71,6 +71,8 @@
},
"can_stop": {
"type": "boolean"
}
},
"cancel_auto_stop_path": { "type": "string" },
"auto_stop_at": { "type": "string", "format": "date-time" }
}
}
Loading
Loading
@@ -10,7 +10,7 @@ module Entry
class Environment < ::Gitlab::Config::Entry::Node
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.'
 
Loading
Loading
@@ -49,6 +49,7 @@ class Environment < ::Gitlab::Config::Entry::Node
 
validates :on_stop, type: String, allow_nil: true
validates :kubernetes, type: Hash, allow_nil: true
validates :auto_stop_in, duration: true, allow_nil: true
end
end
 
Loading
Loading
@@ -80,6 +81,10 @@ def kubernetes
value[:kubernetes]
end
 
def auto_stop_in
value[:auto_stop_in]
end
def value
case @config
when String then { name: @config, action: 'start' }
Loading
Loading
Loading
Loading
@@ -2312,6 +2312,9 @@ msgstr ""
msgid "Auto License Compliance"
msgstr ""
 
msgid "Auto stop successfully canceled."
msgstr ""
msgid "Auto-cancel redundant, pending pipelines"
msgstr ""
 
Loading
Loading
@@ -7251,6 +7254,15 @@ msgstr ""
msgid "Failed to assign a user because no user was found."
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"
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -5,16 +5,14 @@
describe Projects::EnvironmentsController do
include MetricsDashboardHelpers
 
let_it_be(:user) { create(:user) }
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
create(:environment, name: 'production', project: project)
end
let!(:environment) { create(:environment, name: 'production', project: project) }
 
before do
project.add_maintainer(user)
sign_in(user)
end
 
Loading
Loading
@@ -245,6 +243,36 @@
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
context 'with valid id' do
it 'responds with a status code 200' do
Loading
Loading
@@ -320,21 +348,21 @@
end
 
describe 'GET #metrics_redirect' do
let(:project) { create(:project) }
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 }
 
expect(response).to redirect_to(environment_metrics_path(environment))
end
 
it 'redirects to empty metrics page if no environment exists' do
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
context 'when there are no environments' do
let(:environment) { }
 
expect(response).to be_ok
expect(response).to render_template 'empty_metrics'
it 'redirects to empty metrics page' do
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to be_ok
expect(response).to render_template 'empty_metrics'
end
end
end
 
Loading
Loading
@@ -549,6 +577,10 @@
let(:project) { project_with_dashboard(dashboard_path, dashboard_yml) }
let(:environment) { create(:environment, name: 'production', project: project) }
 
before do
project.add_maintainer(user)
end
it_behaves_like 'the specified dashboard', 'Test Dashboard'
end
 
Loading
Loading
Loading
Loading
@@ -44,5 +44,13 @@
status { 'created' }
self.when { 'manual' }
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
Loading
Loading
@@ -144,8 +144,8 @@ def stop_button_selector
expect(page).to have_content('No deployments yet')
end
 
it 'does not show stip button when environment is not stoppable' do
expect(page).not_to have_selector(stop_button_selector)
it 'shows stop button when environment is not stoppable' do
expect(page).to have_selector(stop_button_selector)
end
end
 
Loading
Loading
@@ -205,7 +205,7 @@ def stop_button_selector
end
 
it 'shows a stop button' do
expect(page).not_to have_selector(stop_button_selector)
expect(page).to have_selector(stop_button_selector)
end
 
it 'does not show external link button' do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment