From 3dc7750091d1665663b205a34475e09b4007c883 Mon Sep 17 00:00:00 2001
From: Mikolaj Wawrzyniak <mwawrzyniak@gitlab.com>
Date: Tue, 30 Jun 2020 13:27:11 +0200
Subject: [PATCH] Extract metrics prometheus api proxy into module

To avoid duplicated code when implementing prometheus api proxy
behavior we need to extrac common logic into dedicated module
---
 .../metrics/dashboard/prometheus_api_proxy.rb |  53 +++++
 .../environments/prometheus_api_controller.rb |  46 +---
 config/routes/project.rb                      |   2 +-
 .../prometheus_api_controller_spec.rb         | 206 +++---------------
 .../prometheus_api_proxy_shared_examples.rb   | 147 +++++++++++++
 5 files changed, 239 insertions(+), 215 deletions(-)
 create mode 100644 app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb
 create mode 100644 spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb

diff --git a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb
new file mode 100644
index 0000000000000..e0e3f628cc580
--- /dev/null
+++ b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Metrics::Dashboard::PrometheusApiProxy
+  extend ActiveSupport::Concern
+  include RenderServiceResults
+
+  included do
+    before_action :authorize_read_prometheus!, only: [:prometheus_proxy]
+  end
+
+  def prometheus_proxy
+    variable_substitution_result =
+      proxy_variable_substitution_service.new(proxyable, permit_params).execute
+
+    if variable_substitution_result[:status] == :error
+      return error_response(variable_substitution_result)
+    end
+
+    prometheus_result = Prometheus::ProxyService.new(
+      proxyable,
+      proxy_method,
+      proxy_path,
+      variable_substitution_result[:params]
+    ).execute
+
+    return continue_polling_response if prometheus_result.nil?
+    return error_response(prometheus_result) if prometheus_result[:status] == :error
+
+    success_response(prometheus_result)
+  end
+
+  private
+
+  def proxyable
+    raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
+  end
+
+  def proxy_variable_substitution_service
+    raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
+  end
+
+  def permit_params
+    params.permit!
+  end
+
+  def proxy_method
+    request.method
+  end
+
+  def proxy_path
+    params[:proxy_path]
+  end
+end
diff --git a/app/controllers/projects/environments/prometheus_api_controller.rb b/app/controllers/projects/environments/prometheus_api_controller.rb
index 98fcc594d6e3c..f0bb5360f8430 100644
--- a/app/controllers/projects/environments/prometheus_api_controller.rb
+++ b/app/controllers/projects/environments/prometheus_api_controller.rb
@@ -1,51 +1,17 @@
 # frozen_string_literal: true
 
 class Projects::Environments::PrometheusApiController < Projects::ApplicationController
-  include RenderServiceResults
+  include Metrics::Dashboard::PrometheusApiProxy
 
-  before_action :authorize_read_prometheus!
-  before_action :environment
-
-  def proxy
-    variable_substitution_result =
-      variable_substitution_service.new(environment, permit_params).execute
-
-    if variable_substitution_result[:status] == :error
-      return error_response(variable_substitution_result)
-    end
-
-    prometheus_result = Prometheus::ProxyService.new(
-      environment,
-      proxy_method,
-      proxy_path,
-      variable_substitution_result[:params]
-    ).execute
-
-    return continue_polling_response if prometheus_result.nil?
-    return error_response(prometheus_result) if prometheus_result[:status] == :error
-
-    success_response(prometheus_result)
-  end
+  before_action :proxyable
 
   private
 
-  def variable_substitution_service
-    Prometheus::ProxyVariableSubstitutionService
-  end
-
-  def permit_params
-    params.permit!
-  end
-
-  def environment
-    @environment ||= project.environments.find(params[:id])
+  def proxyable
+    @proxyable ||= project.environments.find(params[:id])
   end
 
-  def proxy_method
-    request.method
-  end
-
-  def proxy_path
-    params[:proxy_path]
+  def proxy_variable_substitution_service
+    Prometheus::ProxyVariableSubstitutionService
   end
 end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 0ad4d7e0ea097..d2fe2be48e559 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -257,7 +257,7 @@
             # This route is also defined in gitlab-workhorse. Make sure to update accordingly.
             get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', format: false
 
-            get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy', as: :prometheus_api
+            get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#prometheus_proxy', as: :prometheus_api
 
             get '/sample_metrics', to: 'environments/sample_metrics#query'
           end
diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
index 17952aa06832f..68d50cf19f0dc 100644
--- a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
+++ b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
@@ -3,215 +3,73 @@
 require 'spec_helper'
 
 RSpec.describe Projects::Environments::PrometheusApiController do
-  let_it_be(:project) { create(:project) }
-  let_it_be(:environment) { create(:environment, project: project) }
   let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project) }
+  let_it_be(:proxyable) { create(:environment, project: project) }
 
   before do
     project.add_reporter(user)
     sign_in(user)
   end
 
-  describe 'GET #proxy' do
-    let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) }
-
-    let(:expected_params) do
-      ActionController::Parameters.new(
-        environment_params(
-          proxy_path: 'query',
-          controller: 'projects/environments/prometheus_api',
-          action: 'proxy'
-        )
-      ).permit!
-    end
-
-    context 'with valid requests' do
-      before do
-        allow(Prometheus::ProxyService).to receive(:new)
-          .with(environment, 'GET', 'query', expected_params)
-          .and_return(prometheus_proxy_service)
-
-        allow(prometheus_proxy_service).to receive(:execute)
-          .and_return(service_result)
+  describe 'GET #prometheus_proxy' do
+    it_behaves_like 'metrics dashboard prometheus api proxy' do
+      let(:proxyable_params) do
+        {
+          id: proxyable.id.to_s,
+          namespace_id: project.namespace.full_path,
+          project_id: project.name
+        }
       end
 
-      context 'with success result' do
-        let(:service_result) { { status: :success, body: prometheus_body } }
+      context 'with variables' do
         let(:prometheus_body) { '{"status":"success"}' }
-        let(:prometheus_json_body) { Gitlab::Json.parse(prometheus_body) }
+        let(:pod_name) { "pod1" }
 
-        it 'returns prometheus response' do
-          get :proxy, params: environment_params
-
-          expect(Prometheus::ProxyService).to have_received(:new)
-            .with(environment, 'GET', 'query', expected_params)
-          expect(response).to have_gitlab_http_status(:ok)
-          expect(json_response).to eq(prometheus_json_body)
+        before do
+          expected_params[:query] = %{up{pod_name="#{pod_name}"}}
+          expected_params[:variables] = { 'pod_name' => pod_name }
         end
 
-        context 'with format string' do
-          before do
-            expected_params[:query] = %{up{environment="#{environment.slug}"}}
-          end
-
-          it 'replaces variables with values' do
-            get :proxy, params: environment_params.merge(query: 'up{environment="{{ci_environment_slug}}"}')
-
-            expect(Prometheus::ProxyService).to have_received(:new)
-              .with(environment, 'GET', 'query', expected_params)
-          end
-
-          context 'with nil query' do
-            let(:params_without_query) do
-              environment_params.except(:query)
-            end
-
-            before do
-              expected_params.delete(:query)
-            end
-
-            it 'does not raise error' do
-              get :proxy, params: params_without_query
+        it 'replaces variables with values' do
+          get :prometheus_proxy, params: prometheus_proxy_params.merge(
+            query: 'up{pod_name="{{pod_name}}"}', variables: { 'pod_name' => pod_name }
+          )
 
-              expect(Prometheus::ProxyService).to have_received(:new)
-                .with(environment, 'GET', 'query', expected_params)
-            end
-          end
+          expect(response).to have_gitlab_http_status(:success)
+          expect(Prometheus::ProxyService).to have_received(:new)
+                                                .with(proxyable, 'GET', 'query', expected_params)
         end
 
-        context 'with variables' do
-          let(:pod_name) { "pod1" }
-
-          before do
-            expected_params[:query] = %{up{pod_name="#{pod_name}"}}
-            expected_params[:variables] = { 'pod_name' => pod_name }
-          end
-
-          it 'replaces variables with values' do
-            get :proxy, params: environment_params.merge(
-              query: 'up{pod_name="{{pod_name}}"}', variables: { 'pod_name' => pod_name }
+        context 'with invalid variables' do
+          let(:params_with_invalid_variables) do
+            prometheus_proxy_params.merge(
+              query: 'up{pod_name="{{pod_name}}"}', variables: ['a']
             )
-
-            expect(response).to have_gitlab_http_status(:success)
-            expect(Prometheus::ProxyService).to have_received(:new)
-              .with(environment, 'GET', 'query', expected_params)
-          end
-
-          context 'with invalid variables' do
-            let(:params_with_invalid_variables) do
-              environment_params.merge(
-                query: 'up{pod_name="{{pod_name}}"}', variables: ['a']
-              )
-            end
-
-            it 'returns 400' do
-              get :proxy, params: params_with_invalid_variables
-
-              expect(response).to have_gitlab_http_status(:bad_request)
-              expect(Prometheus::ProxyService).not_to receive(:new)
-            end
-          end
-        end
-      end
-
-      context 'with nil result' do
-        let(:service_result) { nil }
-
-        it 'returns 204 no_content' 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(:no_content)
-        end
-      end
-
-      context 'with 404 result' do
-        let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } }
-
-        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 'with error result' do
-        context 'with http_status' do
-          let(:service_result) 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_result) { { status: :error, message: 'error message' } }
 
-          it 'returns bad_request' do
-            get :proxy, params: environment_params
+          it 'returns 400' do
+            get :prometheus_proxy, params: params_with_invalid_variables
 
             expect(response).to have_gitlab_http_status(:bad_request)
-            expect(json_response['status']).to eq('error')
-            expect(json_response['message']).to eq('error message')
+            expect(Prometheus::ProxyService).not_to receive(:new)
           end
         end
       end
-    end
 
-    context 'with inappropriate requests' do
       context 'with anonymous user' do
+        let(:prometheus_body) { nil }
+
         before do
           sign_out(user)
         end
 
         it 'redirects to signin page' do
-          get :proxy, params: environment_params
+          get :prometheus_proxy, params: prometheus_proxy_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
-
-    context 'with invalid environment id' do
-      let(:other_environment) { create(:environment) }
-
-      it 'returns 404' do
-        get :proxy, params: environment_params(id: other_environment.id)
-
-        expect(response).to have_gitlab_http_status(:not_found)
-      end
-    end
-  end
-
-  private
-
-  def environment_params(params = {})
-    {
-      id: environment.id.to_s,
-      namespace_id: project.namespace.full_path,
-      project_id: project.name,
-      proxy_path: 'query',
-      query: '1'
-    }.merge(params)
   end
 end
diff --git a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb
new file mode 100644
index 0000000000000..94cd6971f7c92
--- /dev/null
+++ b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do
+  let(:service_params) { [proxyable, 'GET', 'query', expected_params] }
+  let(:service_result) { { status: :success, body: prometheus_body } }
+  let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) }
+  let(:proxyable_params) do
+    {
+      id: proxyable.id.to_s
+    }
+  end
+  let(:expected_params) do
+    ActionController::Parameters.new(
+      prometheus_proxy_params(
+        proxy_path: 'query',
+        controller: described_class.controller_path,
+        action: 'prometheus_proxy'
+      )
+    ).permit!
+  end
+
+  before do
+    allow_next_instance_of(Prometheus::ProxyService, *service_params) do |proxy_service|
+      allow(proxy_service).to receive(:execute).and_return(service_result)
+    end
+  end
+
+  context 'with valid requests' do
+    context 'with success result' do
+      let(:prometheus_body) { '{"status":"success"}' }
+      let(:prometheus_json_body) { Gitlab::Json.parse(prometheus_body) }
+
+      it 'returns prometheus response' do
+        get :prometheus_proxy, params: prometheus_proxy_params
+
+        expect(Prometheus::ProxyService).to have_received(:new).with(*service_params)
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response).to eq(prometheus_json_body)
+      end
+
+      context 'with nil query' do
+        let(:params_without_query) do
+          prometheus_proxy_params.except(:query)
+        end
+
+        before do
+          expected_params.delete(:query)
+        end
+
+        it 'does not raise error' do
+          get :prometheus_proxy, params: params_without_query
+
+          expect(Prometheus::ProxyService).to have_received(:new).with(*service_params)
+        end
+      end
+    end
+
+    context 'with nil result' do
+      let(:service_result) { nil }
+
+      it 'returns 204 no_content' do
+        get :prometheus_proxy, params: prometheus_proxy_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(:no_content)
+      end
+    end
+
+    context 'with 404 result' do
+      let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } }
+
+      it 'returns body' do
+        get :prometheus_proxy, params: prometheus_proxy_params
+
+        expect(response).to have_gitlab_http_status(:not_found)
+        expect(json_response['body']).to eq('value')
+      end
+    end
+
+    context 'with error result' do
+      context 'with http_status' do
+        let(:service_result) do
+          { http_status: :service_unavailable, status: :error, message: 'error message' }
+        end
+
+        it 'sets the http response status code' do
+          get :prometheus_proxy, params: prometheus_proxy_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_result) { { status: :error, message: 'error message' } }
+
+        it 'returns bad_request' do
+          get :prometheus_proxy, params: prometheus_proxy_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
+  end
+
+  context 'with inappropriate requests' do
+    let(:prometheus_body) { nil }
+
+    context 'without correct permissions' do
+      let(:user2) { create(:user) }
+
+      before do
+        sign_out(user)
+        sign_in(user2)
+      end
+
+      it 'returns 404' do
+        get :prometheus_proxy, params: prometheus_proxy_params
+
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+  end
+
+  context 'with invalid proxyable id' do
+    let(:prometheus_body) { nil }
+
+    it 'returns 404' do
+      get :prometheus_proxy, params: prometheus_proxy_params(id: proxyable.id + 1)
+
+      expect(response).to have_gitlab_http_status(:not_found)
+    end
+  end
+
+  private
+
+  def prometheus_proxy_params(params = {})
+    {
+      proxy_path: 'query',
+      query: '1'
+    }.merge(proxyable_params).merge(params)
+  end
+end
-- 
GitLab