Commit 54617a2a authored by Mikolaj Wawrzyniak's avatar Mikolaj Wawrzyniak

Add Panel Preview service

To process user supplied metrics dashboard panel yaml
we need to add new service to process it.
parent c0adc8ad
...@@ -9,7 +9,13 @@ module Projects ...@@ -9,7 +9,13 @@ module Projects
def panel_preview def panel_preview
respond_to do |format| respond_to do |format|
format.json { render json: render_panel } format.json do
if rendered_panel.success?
render json: rendered_panel.payload
else
render json: { message: rendered_panel.message }, status: :unprocessable_entity
end
end
end end
end end
...@@ -19,25 +25,21 @@ module Projects ...@@ -19,25 +25,21 @@ module Projects
render_404 unless Feature.enabled?(:metrics_dashboard_new_panel_page, project) render_404 unless Feature.enabled?(:metrics_dashboard_new_panel_page, project)
end end
def render_panel def rendered_panel
{ @panel_preview ||= ::Metrics::Dashboard::PanelPreviewService.new(project, panel_yaml, environment).execute
"title": "Memory Usage (Total)", end
"type": "area-chart",
"y_label": "Total Memory Used (GB)", def panel_yaml
"weight": 4, params.require(:panel_yaml)
"metrics": [ end
{
"id": "system_metrics_kubernetes_container_memory_total", def environment
"query_range": "avg(sum(container_memory_usage_bytes{container_name!=\"POD\",pod_name=~\"^{{ci_environment_slug}}-(.*)\",namespace=\"{{kube_namespace}}\"}) by (job)) without (job) /1024/1024/1024", @environment ||=
"label": "Total (GB)", if params[:environment]
"unit": "GB", project.environments.find(params[:environment])
"metric_id": 15, else
"edit_path": nil, project.default_environment
"prometheus_endpoint_path": "/root/autodevops-deploy/-/environments/29/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%7B%7Bci_environment_slug%7D%7D-%28.%2A%29%22%2Cnamespace%3D%22%7B%7Bkube_namespace%7D%7D%22%7D%29+by+%28job%29%29+without+%28job%29++%2F1024%2F1024%2F1024" end
}
],
"id": "4570deed516d0bf93fb42879004117009ab456ced27393ec8dce5b6960438132"
}
end end
end end
end end
......
# frozen_string_literal: true
# Ingest YAML fragment with metrics dashboard panel definition
# https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-panels-properties
# process it and returns renderable json version
module Metrics
module Dashboard
class PanelPreviewService
SEQUENCE = [
::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
::Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter,
::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter,
::Gitlab::Metrics::Dashboard::Stages::AlertsInserter,
::Gitlab::Metrics::Dashboard::Stages::UrlValidator
].freeze
HANDLED_PROCESSING_ERRORS = [
Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError,
Gitlab::Config::Loader::Yaml::NotHashError,
Gitlab::Config::Loader::Yaml::DataTooLargeError,
Gitlab::Config::Loader::FormatError
].freeze
def initialize(project, panel_yaml, environment)
@project, @panel_yaml, @environment = project, panel_yaml, environment
end
def execute
dashboard = ::Gitlab::Metrics::Dashboard::Processor.new(project, dashboard_structure, SEQUENCE, environment: environment).process
ServiceResponse.success(payload: dashboard[:panel_groups][0][:panels][0])
rescue *HANDLED_PROCESSING_ERRORS => error
ServiceResponse.error(message: error.message)
end
private
attr_accessor :project, :panel_yaml, :environment
def dashboard_structure
{
panel_groups: [
{
panels: [panel_hash]
}
]
}
end
def panel_hash
::Gitlab::Config::Loader::Yaml.new(panel_yaml).load_raw!
end
end
end
end
...@@ -6,6 +6,42 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do ...@@ -6,6 +6,42 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) } let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:valid_panel_yml) do
<<~YML
---
title: "Super Chart A1"
type: "area-chart"
y_label: "y_label"
weight: 1
max_value: 1
metrics:
- id: metric_a1
query_range: |+
avg(
sum(
container_memory_usage_bytes{
container_name!="POD",
pod_name=~"^{{ci_environment_slug}}-(.*)",
namespace="{{kube_namespace}}",
user_def_variable="{{user_def_variable}}"
}
) by (job)
) without (job)
/1024/1024/1024
unit: unit
label: Legend Label
YML
end
let_it_be(:invalid_panel_yml) do
<<~YML
---
title: "Super Chart A1"
type: "area-chart"
y_label: "y_label"
weight: 1
max_value: 1
YML
end
def send_request(params = {}) def send_request(params = {})
post namespace_project_metrics_dashboards_builder_path(namespace_id: project.namespace, project_id: project, format: :json, **params) post namespace_project_metrics_dashboards_builder_path(namespace_id: project.namespace, project_id: project, format: :json, **params)
...@@ -17,14 +53,14 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do ...@@ -17,14 +53,14 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do
stub_feature_flags(metrics_dashboard_new_panel_page: true) stub_feature_flags(metrics_dashboard_new_panel_page: true)
end end
it 'redirects to sign in' do it 'redirects user to sign in page' do
send_request send_request
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
end end
context 'as user with reporter access' do context 'as user with guest access' do
before do before do
stub_feature_flags(metrics_dashboard_new_panel_page: true) stub_feature_flags(metrics_dashboard_new_panel_page: true)
project.add_guest(user) project.add_guest(user)
...@@ -49,10 +85,31 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do ...@@ -49,10 +85,31 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do
stub_feature_flags(metrics_dashboard_new_panel_page: true) stub_feature_flags(metrics_dashboard_new_panel_page: true)
end end
context 'valid yaml panel is supplied' do
it 'returns success' do it 'returns success' do
send_request send_request(panel_yaml: valid_panel_yml)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include('title' => 'Super Chart A1', 'type' => 'area-chart')
end
end
context 'invalid yaml panel is supplied' do
it 'returns unprocessable entity' do
send_request(panel_yaml: invalid_panel_yml)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Each "panel" must define an array :metrics')
end
end
context 'invalid panel_yaml is not a yaml string' do
it 'returns unprocessable entity' do
send_request(panel_yaml: 1)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Invalid configuration format')
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Metrics::Dashboard::PanelPreviewService do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:panel_yml) do
<<~YML
---
title: test panel
YML
end
let_it_be(:dashboard) do
{
panel_groups: [
{
panels: [{ 'title' => 'test panel' }]
}
]
}
end
describe '#execute' do
subject(:service_response) { described_class.new(project, panel_yml, environment).execute }
context "valid panel's yaml" do
before do
allow_next_instance_of(::Gitlab::Metrics::Dashboard::Processor) do |processor|
allow(processor).to receive(:process).and_return(dashboard)
end
end
it 'returns success service response' do
expect(service_response.success?).to be_truthy
end
it 'returns processed panel' do
expect(service_response.payload).to eq('title' => 'test panel')
end
it 'uses dashboard processor' do
sequence = [
::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
::Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter,
::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter,
::Gitlab::Metrics::Dashboard::Stages::AlertsInserter,
::Gitlab::Metrics::Dashboard::Stages::UrlValidator
]
processor_params = [project, dashboard, sequence, environment: environment]
expect_next_instance_of(::Gitlab::Metrics::Dashboard::Processor, *processor_params) do |processor|
expect(processor).to receive(:process).and_return(dashboard)
end
service_response
end
end
context "invalid panel's yaml" do
[
Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError,
Gitlab::Config::Loader::Yaml::NotHashError,
Gitlab::Config::Loader::Yaml::DataTooLargeError,
Gitlab::Config::Loader::FormatError
].each do |error_class|
before do
allow_next_instance_of(::Gitlab::Metrics::Dashboard::Processor) do |processor|
allow(processor).to receive(:process).and_raise(error_class.new('error'))
end
end
it 'returns error service response' do
expect(service_response.error?).to be_truthy
end
it 'returns error message' do
expect(service_response.message).to eq('error')
end
end
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