Commit e009ae1b authored by rpereira2's avatar rpereira2

API to create self monitoring project

* Create an admin API to trigger an async job that will create the self
monitoring project.

* Also create an API to check status of the async job.
parent 15a86e9d
...@@ -6,10 +6,21 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -6,10 +6,21 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data] before_action :whitelist_query_limiting, only: [:usage_data]
before_action do
push_frontend_feature_flag(:self_monitoring_project)
end
VALID_SETTING_PANELS = %w(general integrations repository VALID_SETTING_PANELS = %w(general integrations repository
ci_cd reporting metrics_and_profiling ci_cd reporting metrics_and_profiling
network preferences).freeze network preferences).freeze
# The current size of a sidekiq job's jid is 24 characters. The size of the
# jid is an internal detail of Sidekiq, and they do not guarantee that it'll
# stay the same. We chose 50 to give us room in case the size of the jid
# increases. The jid is alphanumeric, so 50 is very generous. There is a spec
# that ensures that the constant value is more than the size of an actual jid.
PARAM_JOB_ID_MAX_SIZE = 50
VALID_SETTING_PANELS.each do |action| VALID_SETTING_PANELS.each do |action|
define_method(action) { perform_update if submitted? } define_method(action) { perform_update if submitted? }
end end
...@@ -62,8 +73,87 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -62,8 +73,87 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
end end
def create_self_monitoring_project
return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
job_id = SelfMonitoringProjectCreateWorker.perform_async
render status: :accepted, json: {
job_id: job_id,
monitor_status: status_create_self_monitoring_project_admin_application_settings_path
}
end
def status_create_self_monitoring_project
return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
job_id = params[:job_id]
unless job_id.present? && job_id.is_a?(String)
return render status: :bad_request, json: {
message: _('"job_id" must be an alphanumeric value')
}
end
unless job_id.length <= PARAM_JOB_ID_MAX_SIZE
return render status: :bad_request, json: {
message: _('Parameter "job_id" cannot exceed length of %{job_id_max_size}' %
{ job_id_max_size: PARAM_JOB_ID_MAX_SIZE })
}
end
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
result = SelfMonitoringProjectCreateWorker.status(job_id)
if [:in_progress, :unknown].include?(result[:status])
render status: :accepted, json: result
elsif result[:status] == :completed
data = result[:output].merge(self_monitoring_data)
render status: :ok, json: data
else
message = _('SelfMonitoringProjectCreateWorker#status returned unknown status "%{status}"') %
{ status: result[:status] }
raise_exception_for_dev(
message,
{ return_value: result }
)
render(
status: :internal_server_error,
json: { message: message }
)
end
end
private private
def self_monitoring_data
{
project_id: Gitlab::CurrentSettings.instance_administration_project_id,
project_full_path: Gitlab::CurrentSettings.instance_administration_project&.full_path
}
end
def raise_exception_for_dev(message, extra = {})
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
StandardError.new(message),
extra
)
end
def self_monitoring_project_not_implemented
render(
status: :not_implemented,
json: {
message: _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'),
documentation_url: help_page_path('administration/monitoring/gitlab_instance_administration_project/index')
}
)
end
def set_application_setting def set_application_setting
@application_setting = ApplicationSetting.current_without_cache @application_setting = ApplicationSetting.current_without_cache
end end
......
...@@ -334,6 +334,22 @@ module ApplicationSettingsHelper ...@@ -334,6 +334,22 @@ module ApplicationSettingsHelper
def omnibus_protected_paths_throttle? def omnibus_protected_paths_throttle?
Rack::Attack.throttles.key?('protected paths') Rack::Attack.throttles.key?('protected paths')
end end
def self_monitoring_project_data
{
'create_self_monitoring_project_path' =>
create_self_monitoring_project_admin_application_settings_path,
'status_create_self_monitoring_project_path' =>
status_create_self_monitoring_project_admin_application_settings_path,
'self_monitoring_project_exists' =>
Gitlab::CurrentSettings.instance_administration_project.present?,
'self_monitoring_project_full_path' =>
Gitlab::CurrentSettings.instance_administration_project&.full_path
}
end
end end
ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
......
...@@ -116,6 +116,9 @@ namespace :admin do ...@@ -116,6 +116,9 @@ namespace :admin do
put :clear_repository_check_states put :clear_repository_check_states
match :general, :integrations, :repository, :ci_cd, :reporting, :metrics_and_profiling, :network, :preferences, via: [:get, :patch] match :general, :integrations, :repository, :ci_cd, :reporting, :metrics_and_profiling, :network, :preferences, via: [:get, :patch]
get :lets_encrypt_terms_of_service get :lets_encrypt_terms_of_service
post :create_self_monitoring_project
get :status_create_self_monitoring_project
end end
resources :labels resources :labels
......
...@@ -65,6 +65,9 @@ msgstr "" ...@@ -65,6 +65,9 @@ msgstr ""
msgid "\"%{path}\" did not exist on \"%{ref}\"" msgid "\"%{path}\" did not exist on \"%{ref}\""
msgstr "" msgstr ""
msgid "\"job_id\" must be an alphanumeric value"
msgstr ""
msgid "%d comment" msgid "%d comment"
msgid_plural "%d comments" msgid_plural "%d comments"
msgstr[0] "" msgstr[0] ""
...@@ -12708,6 +12711,9 @@ msgstr "" ...@@ -12708,6 +12711,9 @@ msgstr ""
msgid "Parameter" msgid "Parameter"
msgstr "" msgstr ""
msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}"
msgstr ""
msgid "Parent epic doesn't exist." msgid "Parent epic doesn't exist."
msgstr "" msgstr ""
...@@ -16211,6 +16217,12 @@ msgstr "" ...@@ -16211,6 +16217,12 @@ msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user." msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr "" msgstr ""
msgid "Self-monitoring is not enabled on this GitLab server, contact your administrator."
msgstr ""
msgid "SelfMonitoringProjectCreateWorker#status returned unknown status \"%{status}\""
msgstr ""
msgid "Send a separate email notification to Developers." msgid "Send a separate email notification to Developers."
msgstr "" msgstr ""
......
...@@ -59,4 +59,54 @@ describe ApplicationSettingsHelper do ...@@ -59,4 +59,54 @@ describe ApplicationSettingsHelper do
expect(helper.integration_expanded?('plantuml_')).to be_falsey expect(helper.integration_expanded?('plantuml_')).to be_falsey
end end
end end
describe '.self_monitoring_project_data' do
context 'when self monitoring project does not exist' do
it 'returns create_self_monitoring_project_path' do
expect(helper.self_monitoring_project_data).to include(
'create_self_monitoring_project_path' =>
create_self_monitoring_project_admin_application_settings_path
)
end
it 'returns status_create_self_monitoring_project_path' do
expect(helper.self_monitoring_project_data).to include(
'status_create_self_monitoring_project_path' =>
status_create_self_monitoring_project_admin_application_settings_path
)
end
it 'returns self_monitoring_project_exists false' do
expect(helper.self_monitoring_project_data).to include(
'self_monitoring_project_exists' => false
)
end
it 'returns nil for project full_path' do
expect(helper.self_monitoring_project_data).to include(
'self_monitoring_project_full_path' => nil
)
end
end
context 'when self monitoring project exists' do
let(:project) { build(:project) }
before do
stub_application_setting(instance_administration_project: project)
end
it 'returns self_monitoring_project_exists true' do
expect(helper.self_monitoring_project_data).to include(
'self_monitoring_project_exists' => true
)
end
it 'returns project full_path' do
expect(helper.self_monitoring_project_data).to include(
'self_monitoring_project_full_path' => project.full_path
)
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe 'Self-Monitoring project requests' do
let(:admin) { create(:admin) }
describe 'POST #create_self_monitoring_project' do
let(:worker_class) { SelfMonitoringProjectCreateWorker }
subject { post create_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'not accessible to non-admin users'
context 'with admin user' do
before do
login_as(admin)
end
context 'with feature flag disabled' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
it 'returns sidekiq job_id of expected length' do
subject
job_id = json_response['job_id']
aggregate_failures do
expect(job_id).to be_present
expect(job_id.length).to be <= Admin::ApplicationSettingsController::PARAM_JOB_ID_MAX_SIZE
end
end
it 'triggers async worker' do
expect(worker_class).to receive(:perform_async)
subject
end
it 'returns accepted response' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:accepted)
expect(json_response.keys).to contain_exactly('job_id', 'monitor_status')
expect(json_response).to include(
'monitor_status' => status_create_self_monitoring_project_admin_application_settings_path
)
end
end
it 'returns job_id' do
fake_job_id = 'b5b28910d97563e58c2fe55f'
expect(worker_class).to receive(:perform_async).and_return(fake_job_id)
subject
response_job_id = json_response['job_id']
expect(response_job_id).to eq fake_job_id
end
end
end
end
describe 'GET #status_create_self_monitoring_project' do
let(:worker_class) { SelfMonitoringProjectCreateWorker }
let(:job_id) { 'job_id' }
subject do
get status_create_self_monitoring_project_admin_application_settings_path,
params: { job_id: job_id }
end
it_behaves_like 'not accessible to non-admin users'
context 'with admin user' do
before do
login_as(admin)
end
context 'with feature flag disabled' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
it 'calls .status' do
expect(worker_class).to receive(:status).with(job_id).and_call_original
subject
end
it 'sets polling header' do
expect(::Gitlab::PollingInterval).to receive(:set_header)
subject
end
context 'with invalid job_id' do
it_behaves_like 'handles missing or invalid job_id', nil
it_behaves_like 'handles missing or invalid job_id', job_id: nil
it_behaves_like 'handles missing or invalid job_id', job_id: [2]
it 'returns bad_request if job_id too long' do
get status_create_self_monitoring_project_admin_application_settings_path,
params: { job_id: 'a' * 51 }
aggregate_failures do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('message' => 'Parameter "job_id" cannot ' \
"exceed length of #{Admin::ApplicationSettingsController::PARAM_JOB_ID_MAX_SIZE}")
end
end
end
context 'different status values' do
before do
allow(worker_class).to receive(:status).and_return(status)
end
context 'when status returns in_progress' do
let(:status) { { status: :in_progress } }
it 'returns status accepted' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:accepted)
expect(json_response).to eq('status' => 'in_progress')
end
end
end
context 'when status returns unknown' do
let(:status) { { status: :unknown, message: 'message' } }
it 'returns accepted' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:accepted)
expect(json_response).to eq(
'status' => 'unknown',
'message' => 'message'
)
end
end
end
context 'when status returns completed' do
let(:status) { { status: :completed, output: { status: :success } } }
let(:project) { build(:project) }
before do
stub_application_setting(instance_administration_project_id: 2)
stub_application_setting(instance_administration_project: project)
end
it 'returns output' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq(
'status' => 'success',
'project_id' => 2,
'project_full_path' => project.full_path
)
end
end
end
context 'when unexpected status is returned' do
let(:status) { { status: :unexpected_status } }
it 'raises error' do
expect { subject }.to raise_error(
StandardError, 'SelfMonitoringProjectCreateWorker#status returned ' \
'unknown status "unexpected_status"'
)
end
context 'in production' do
before do
stub_rails_env('production')
end
it 'returns internal_server_error' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response).to eq(
'message' => 'SelfMonitoringProjectCreateWorker#status returned ' \
'unknown status "unexpected_status"'
)
end
end
end
end
end
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'not accessible if feature flag is disabled' do
before do
stub_feature_flags(self_monitoring_project: false)
end
it 'returns not_implemented' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:not_implemented)
expect(json_response).to eq(
'message' => _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'),
'documentation_url' => help_page_path('administration/monitoring/gitlab_instance_administration_project/index')
)
end
end
end
RSpec.shared_examples 'handles missing or invalid job_id' do |job_id_param|
it "returns bad_request for #{job_id_param}" do
get status_create_self_monitoring_project_admin_application_settings_path, params: job_id_param
aggregate_failures do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('message' => '"job_id" must be an alphanumeric value')
end
end
end
RSpec.shared_examples 'not accessible to non-admin users' do
context 'with unauthenticated user' do
it 'redirects to signin page' do
subject
expect(response).to redirect_to(new_user_session_path)
end
end
context 'with authenticated non-admin user' do
before do
login_as(create(:user))
end
it 'returns status not_found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
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