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
......@@ -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
......@@ -104,6 +105,27 @@ class Projects::EnvironmentsController < Projects::ApplicationController
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
......@@ -175,8 +197,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
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))
......@@ -222,6 +242,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
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')
......@@ -17,6 +17,7 @@ module Ci
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
 
......@@ -47,8 +48,11 @@ module Ci
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)
......
......@@ -162,6 +162,10 @@ class Environment < ApplicationRecord
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
 
......@@ -261,6 +265,17 @@ class Environment < ApplicationRecord
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
......
......@@ -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
......@@ -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
......
......@@ -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
......@@ -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)
......@@ -54,6 +59,10 @@ class EnvironmentEntity < Grape::Entity
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
......
......@@ -29,6 +29,7 @@ module Deployments
environment.external_url = url
end
 
renew_auto_stop_in
environment.fire_state_event(action)
 
if environment.save && !environment.stopped?
......@@ -63,6 +64,12 @@ module Deployments
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
 
......
# 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 @@
- 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
......@@ -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')
......
......@@ -224,6 +224,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :environments, except: [:destroy] do
member do
post :stop
post :cancel_auto_stop
get :terminal
get :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
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)"
......@@ -1447,6 +1448,7 @@ ActiveRecord::Schema.define(version: 2019_12_04_093410) do
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
......
......@@ -11,6 +11,7 @@ module EE
prevent :create_environment_terminal
prevent :create_deployment
prevent :update_deployment
prevent :update_environment
end
 
private
......
......@@ -282,6 +282,31 @@ describe Projects::EnvironmentsController do
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,
......
......@@ -71,6 +71,8 @@
},
"can_stop": {
"type": "boolean"
}
},
"cancel_auto_stop_path": { "type": "string" },
"auto_stop_at": { "type": "string", "format": "date-time" }
}
}
......@@ -10,7 +10,7 @@ module Gitlab
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.'
 
......@@ -49,6 +49,7 @@ module Gitlab
 
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
 
......@@ -80,6 +81,10 @@ module Gitlab
value[:kubernetes]
end
 
def auto_stop_in
value[:auto_stop_in]
end
def value
case @config
when String then { name: @config, action: 'start' }
......
......@@ -2312,6 +2312,9 @@ msgstr ""
msgid "Auto License Compliance"
msgstr ""
 
msgid "Auto stop successfully canceled."
msgstr ""
msgid "Auto-cancel redundant, pending pipelines"
msgstr ""
 
......@@ -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 ""
 
......
......@@ -5,16 +5,14 @@ require 'spec_helper'
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
 
......@@ -245,6 +243,36 @@ describe Projects::EnvironmentsController do
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
......@@ -320,21 +348,21 @@ describe Projects::EnvironmentsController do
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
 
......@@ -549,6 +577,10 @@ describe Projects::EnvironmentsController do
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
 
......
......@@ -44,5 +44,13 @@ FactoryBot.define do
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
......@@ -144,8 +144,8 @@ describe 'Environments page', :js do
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
 
......@@ -205,7 +205,7 @@ describe 'Environments page', :js do
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
......
Supports Markdown
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