diff --git a/app/controllers/projects/environments/prometheus_api_controller.rb b/app/controllers/projects/environments/prometheus_api_controller.rb new file mode 100644 index 00000000000..337b7fdf65d --- /dev/null +++ b/app/controllers/projects/environments/prometheus_api_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class Projects::Environments::PrometheusApiController < Projects::ApplicationController + before_action :authorize_read_prometheus! + before_action :environment + + def proxy + permitted = permit_params + + result = Prometheus::ProxyService.new( + environment, + request.method, + permitted[:proxy_path], + permitted.except(:proxy_path) # rubocop: disable CodeReuse/ActiveRecord + ).execute + + if result.nil? + render status: :accepted, json: { + status: 'processing', + message: 'Not ready yet. Try again later.' + } + return + end + + if result[:status] == :success + render status: result[:http_status], json: result[:body] + else + render status: result[:http_status] || :bad_request, json: { + status: result[:status], + message: result[:message] + } + end + end + + private + + def permit_params + params.permit([ + :proxy_path, :query, :time, :timeout, :start, :end, :step, { match: [] }, + :match_target, :metric, :limit + ]) + end + + def environment + @environment ||= project.environments.find(params[:id]) + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 75825c8fac0..26d7d6e84c4 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -204,6 +204,7 @@ class ProjectPolicy < BasePolicy enable :read_merge_request enable :read_sentry_issue enable :read_release + enable :read_prometheus end # We define `:public_user_access` separately because there are cases in gitlab-ee diff --git a/changelogs/unreleased/58375-api-controller.yml b/changelogs/unreleased/58375-api-controller.yml new file mode 100644 index 00000000000..60f21b37ae7 --- /dev/null +++ b/changelogs/unreleased/58375-api-controller.yml @@ -0,0 +1,5 @@ +--- +title: Add a Prometheus API per environment +merge_request: 26841 +author: +type: added diff --git a/config/routes/project.rb b/config/routes/project.rb index d60a5cc9ae8..1cb8f331f6f 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -219,6 +219,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get :metrics get :additional_metrics get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } + + get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy' end collection do diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb new file mode 100644 index 00000000000..d943d006ae1 --- /dev/null +++ b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::Environments::PrometheusApiController do + set(:project) { create(:project) } + set(:environment) { create(:environment, project: project) } + set(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + end + + describe 'GET #proxy' do + let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) } + let(:prometheus_response) { { status: :success, body: response_body } } + let(:json_response_body) { JSON.parse(response_body) } + + let(:response_body) do + "{\"status\":\"success\",\"data\":{\"resultType\":\"scalar\",\"result\":[1553864609.117,\"1\"]}}" + end + + before do + allow(Prometheus::ProxyService).to receive(:new) + .with(environment, 'GET', 'query', anything) + .and_return(prometheus_proxy_service) + + allow(prometheus_proxy_service).to receive(:execute) + .and_return(prometheus_response) + end + + it 'returns prometheus response' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq(json_response_body) + end + + it 'filters params' do + get :proxy, params: environment_params({ extra_param: 'dangerous value' }) + + expect(Prometheus::ProxyService).to have_received(:new) + .with(environment, 'GET', 'query', ActionController::Parameters.new({ 'query' => '1' }).permit!) + end + + context 'Prometheus::ProxyService returns nil' do + before do + allow(prometheus_proxy_service).to receive(:execute) + .and_return(nil) + end + + it 'returns 202 accepted' do + get :proxy, params: environment_params + + expect(json_response['status']).to eq('processing') + expect(json_response['message']).to eq('Not ready yet. Try again later.') + expect(response).to have_gitlab_http_status(:accepted) + end + end + + context 'Prometheus::ProxyService returns status success' do + let(:service_response) { { http_status: 404, status: :success, body: '{"body": "value"}' } } + + before do + allow(prometheus_proxy_service).to receive(:execute) + .and_return(service_response) + end + + it 'returns body' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['body']).to eq('value') + end + end + + context 'Prometheus::ProxyService returns status error' do + before do + allow(prometheus_proxy_service).to receive(:execute) + .and_return(service_response) + end + + context 'with http_status' do + let(:service_response) do + { http_status: :service_unavailable, status: :error, message: 'error message' } + end + + it 'sets the http response status code' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + + context 'without http_status' do + let(:service_response) { { status: :error, message: 'error message' } } + + it 'returns message' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + end + + context 'with anonymous user' do + before do + sign_out(user) + end + + it 'redirects to signin page' do + get :proxy, params: environment_params + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'without correct permissions' do + before do + project.team.truncate + end + + it 'returns 404' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + private + + def environment_params(params = {}) + { + id: environment.id, + namespace_id: project.namespace, + project_id: project, + proxy_path: 'query', + query: '1' + }.merge(params) + end +end diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb index 3ad6e067674..ee5cfcd850d 100644 --- a/spec/support/shared_context/policies/project_policy_shared_context.rb +++ b/spec/support/shared_context/policies/project_policy_shared_context.rb @@ -25,6 +25,7 @@ RSpec.shared_context 'ProjectPolicy context' do admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment read_merge_request download_wiki_code read_sentry_issue read_release + read_prometheus ] end