Commit 8911a759 authored by Allison Browne's avatar Allison Browne Committed by Achilleas Pipinellis

Add Cluster Embed Metrics Dashboard Endpoint

Add an endpoint to query to get a single panel in
the format of metric embeds
parent fb67968c
......@@ -695,7 +695,7 @@ Prometheus server.
> [Introduced][ce-29691] in GitLab 12.2.
It is possible to display metrics charts within [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm). The maximum number of embeds allowed in a GitLab Flavored Markdown field is 100.
It is possible to display metrics charts within [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm) fields such as issue or merge request descriptions. The maximum number of embedded charts allowed in a GitLab Flavored Markdown field is 100.
This can be useful if you are sharing an application incident or performance
metrics to others and want to have relevant information directly available.
......@@ -733,6 +733,25 @@ It is also possible to embed either the default dashboard metrics or individual
![Embedded Metrics in issue templates](img/embed_metrics_issue_template.png)
### Embedding Cluster Health Charts **(ULTIMATE)**
> [Introduced](<https://gitlab.com/gitlab-org/gitlab/issues/40997>) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.9.
[Cluster Health Metrics](../clusters/index.md#monitoring-your-kubernetes-cluster-ultimate) can also be embedded in [GitLab-flavored Markdown](../../markdown.md).
To embed a metric chart, include a link to that chart in the form `https://<root_url>/<project>/-/cluster/<cluster_id>?<query_params>` anywhere that GitLab-flavored Markdown is supported. To generate and copy a link to the chart, follow the instructions in the [Cluster Health Metric documentation](../clusters/index.md#monitoring-your-kubernetes-cluster-ultimate).
The following requirements must be met for the metric to unfurl:
- The `<cluster_id>` must correspond to a real cluster.
- Prometheus must be monitoring the cluster.
- The user must be allowed access to the project cluster metrics.
- The dashboards must be reporting data on the [Cluster Health Page](../clusters/index.md#monitoring-your-kubernetes-cluster-ultimate)
If the above requirements are met, then the metric will unfurl as seen below.
![Embedded Cluster Metric in issue descriptions](img/prometheus_cluster_health_embed_v12_9.png)
### Embedding Grafana charts
Grafana metrics can be embedded in [GitLab Flavored Markdown](../../markdown.md).
......
......@@ -9,6 +9,7 @@ module EE
prepended do
before_action :expire_etag_cache, only: [:show]
before_action :authorize_read_prometheus!, only: :prometheus_proxy
before_action :authorize_read_cluster_health!, only: [:metrics_dashboard]
end
def metrics
......@@ -70,6 +71,10 @@ module EE
private
def authorize_read_cluster_health!
access_denied! unless can?(current_user, :read_cluster_health, cluster)
end
def expire_etag_cache
return if request.format.json? || !clusterable.environments_cluster_path(cluster)
......
......@@ -4,10 +4,12 @@ module EE
module Projects
module ClustersController
def metrics_dashboard_params
{
cluster: cluster,
cluster_type: :project
}
params.permit(:embedded, :group, :title, :y_label).merge(
{
cluster: cluster,
cluster_type: :project
}
)
end
end
end
......
......@@ -6,12 +6,20 @@ module EE
extend ActiveSupport::Concern
prepended do
with_scope :global
condition(:cluster_deployments_available) do
License.feature_available?(:cluster_deployments)
end
with_scope :global
condition(:cluster_health_available) do
License.feature_available?(:cluster_health)
end
rule { can?(:read_cluster) & cluster_deployments_available }
.enable :read_cluster_environments
rule { can?(:read_cluster) & cluster_health_available }.enable :read_cluster_health
end
end
end
......
......@@ -58,6 +58,11 @@ module EE
@subject.feature_available?(:group_timelogs)
end
with_scope :global
condition(:cluster_health_available) do
License.feature_available?(:cluster_health)
end
rule { reporter }.policy do
enable :admin_list
enable :admin_board
......@@ -163,6 +168,8 @@ module EE
end
rule { ~group_timelogs_available }.prevent :read_group_timelogs
rule { can?(:read_cluster) & cluster_health_available }.enable :read_cluster_health
end
override :lookup_access_level!
......
......@@ -67,6 +67,11 @@ module EE
.prevent_merge_requests_committers_approval
end
with_scope :global
condition(:cluster_health_available) do
License.feature_available?(:cluster_health)
end
with_scope :subject
condition(:commit_committer_check_available) do
@subject.feature_available?(:commit_committer_check)
......@@ -335,6 +340,8 @@ module EE
prevent :modify_merge_request_committer_setting
end
rule { can?(:read_cluster) & cluster_health_available }.enable :read_cluster_health
rule { owner_cannot_modify_approvers_rules & ~admin }.policy do
prevent :modify_approvers_list
end
......
......@@ -9,14 +9,15 @@ module Metrics
DASHBOARD_NAME = 'Cluster'
SEQUENCE = [
STAGES::CommonMetricsInserter,
STAGES::ClusterEndpointInserter,
STAGES::Sorter
].freeze
class << self
def valid_params?(params)
params[:cluster].present?
# support selecting this service by cluster id via .find
# Use super to support selecting this service by dashboard_path via .find_raw
(params[:cluster].present? && params[:embedded] != 'true') || super
end
end
......
# frozen_string_literal: true
#
module Metrics
module Dashboard
class ClusterMetricsEmbedService < Metrics::Dashboard::DynamicEmbedService
class << self
def valid_params?(params)
[
params[:cluster],
params[:embedded] == 'true',
params[:group].present?,
params[:title].present?,
params[:y_label].present?
].all?
end
end
private
# Permissions are handled at the controller level
def allowed?
true
end
def dashboard_path
::Metrics::Dashboard::ClusterDashboardService::DASHBOARD_PATH
end
def sequence
[
STAGES::ClusterEndpointInserter
]
end
end
end
end
---
title: Embed cluster health metrics in GitLab-flavored Markdown
merge_request: 25739
author:
type: added
......@@ -11,7 +11,8 @@ module EE
override :permissions_by_route
def permissions_by_route
super.concat([
ROUTE.new(::Gitlab::Metrics::Dashboard::Url.alert_regex, :read_prometheus_alerts)
ROUTE.new(::Gitlab::Metrics::Dashboard::Url.alert_regex, :read_prometheus_alerts),
ROUTE.new(::Gitlab::Metrics::Dashboard::Url.clusters_regex, :read_cluster_health)
])
end
end
......
......@@ -8,6 +8,7 @@ module EE
extend ActiveSupport::Concern
EE_SERVICES = [
::Metrics::Dashboard::ClusterMetricsEmbedService,
::Metrics::Dashboard::ClusterDashboardService,
::Metrics::Dashboard::GitlabAlertEmbedService
].freeze
......
......@@ -57,7 +57,25 @@ describe Admin::ClustersController do
end
describe 'GET #metrics_dashboard' do
it_behaves_like 'the default dashboard'
context 'with license' do
before do
stub_licensed_features(cluster_health: true)
end
it_behaves_like 'the default dashboard'
end
context 'without license' do
before do
stub_licensed_features(cluster_health: false)
end
it 'has status not found' do
get :metrics_dashboard, params: metrics_params, format: :json
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......
......@@ -87,7 +87,25 @@ describe Groups::ClustersController do
sign_in(user)
end
it_behaves_like 'the default dashboard'
context 'with license' do
before do
stub_licensed_features(cluster_health: true)
end
it_behaves_like 'the default dashboard'
end
context 'without license' do
before do
stub_licensed_features(cluster_health: false)
end
it 'has status not found' do
get :metrics_dashboard, params: metrics_params, format: :json
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......
......@@ -79,7 +79,25 @@ describe Projects::ClustersController do
sign_in(user)
end
it_behaves_like 'the default dashboard'
context 'with license' do
before do
stub_licensed_features(cluster_health: true)
end
it_behaves_like 'the default dashboard'
end
context 'without license' do
before do
stub_licensed_features(cluster_health: false)
end
it 'has status not found' do
get :metrics_dashboard, params: metrics_params, format: :json
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
include PrometheusHelpers
include KubernetesHelpers
include MetricsDashboardUrlHelpers
let_it_be(:user) { create(:user) }
......@@ -18,7 +19,7 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek
.to receive(:url)
.and_return(urls.root_url.chomp('/'))
stub_licensed_features(prometheus_alerts: true)
stub_licensed_features(prometheus_alerts: true, cluster_health: true)
project.add_maintainer(user)
sign_in(user)
......@@ -31,14 +32,13 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek
clear_host_from_memoized_variables
end
# This context block can be moved as-is to inside the
# While migrating alerting to CE.
# this context block can be moved after removing the 'unlicensed' context to inside the
# 'internal metrics embeds' block in spec/features/markdown/metrics_spec.rb
# while migrating alerting to CE. Add `:alert_regex` to
# clear_host_from_memoized_variables
# Add `:alert_regex` to clear_host_from_memoized_variables
context 'for GitLab-managed alerting rules' do
let(:metric) { PrometheusMetric.last }
let!(:alert) { create(:prometheus_alert, project: project, prometheus_metric: metric) }
let(:description) { "# Summary \n[](#{metrics_url})" }
let(:metrics_url) do
urls.metrics_dashboard_project_prometheus_alert_url(
......@@ -57,6 +57,57 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek
expect(page).to have_text(metric.y_label)
expect(page).not_to have_text(metrics_url)
end
# Delete when moving to CE
context 'unlicensed' do
before do
stub_licensed_features(prometheus_alerts: false)
end
it 'shows no embedded metrics' do
visit project_issue_path(project, issue)
expect(page).to have_no_css('div.prometheus-graph')
end
end
end
context 'for GitLab embedded cluster health metrics' do
before do
create(:clusters_applications_prometheus, :installed, cluster: cluster)
stub_kubeclient_discover(cluster.platform.api_url)
stub_prometheus_request(/prometheus-prometheus-server/, body: prometheus_values_body)
stub_prometheus_request(/prometheus\/api\/v1/, body: prometheus_values_body)
end
let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [project], user: user) }
let(:params) { [project.namespace.path, project.path, cluster.id] }
let(:query_params) { { group: 'Cluster Health', title: 'CPU Usage', y_label: 'CPU (cores)' } }
let(:metrics_url) { urls.metrics_namespace_project_cluster_url(*params, **query_params) }
let(:description) { "# Summary \n[](#{metrics_url})" }
it 'shows embedded metrics' do
visit project_issue_path(project, issue)
expect(page).to have_css('div.metrics-embed')
expect(page).to have_text(query_params[:title])
expect(page).to have_text(query_params[:y_label])
expect(page).not_to have_text(metrics_url)
end
# Delete when moving to CE
context 'unlicensed' do
before do
stub_licensed_features(cluster_health: false)
end
it 'shows no embedded metrics' do
visit project_issue_path(project, issue)
expect(page).to have_no_css('div.metrics-embed')
expect(page).to have_no_css('div.js-render-metrics')
end
end
end
def import_common_metrics
......
......@@ -9,13 +9,12 @@ describe Banzai::Filter::InlineClusterMetricsFilter do
let!(:project) { create(:project) }
let(:params) { [project.namespace.path, project.path, cluster.id] }
let(:query_params) { { group: 'Food metrics', title: 'Pizza Consumption', y_label: 'Slice Count' } }
let(:trigger_url) { urls.metrics_namespace_project_cluster_url(*params, **query_params) }
let(:dashboard_url) do
urls.metrics_dashboard_namespace_project_cluster_url(
*params,
**{
embedded: true,
embedded: 'true',
cluster_type: 'project',
format: :json
}.merge(query_params)
......
......@@ -27,4 +27,32 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do
it_behaves_like 'redacts the embed placeholder'
it_behaves_like 'retains the embed placeholder when applicable'
end
context 'for a cluster metric embed' do
let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [project]) }
let(:params) { [project.namespace.path, project.path, cluster.id] }
let(:query_params) { { group: 'Cluster Health', title: 'CPU Usage', y_label: 'CPU (cores)' } }
let(:url) { urls.metrics_namespace_project_cluster_url(*params, **query_params) }
context 'with cluster health license' do
before do
stub_licensed_features(cluster_health: true)
end
it_behaves_like 'redacts the embed placeholder'
it_behaves_like 'retains the embed placeholder when applicable'
end
context 'without cluster health license' do
let(:doc) { filter(input, current_user: project.owner) }
before do
stub_licensed_features(cluster_health: false)
end
it 'redacts the embed placeholder' do
expect(doc.to_s).to be_empty
end
end
end
end
......@@ -14,6 +14,34 @@ describe Gitlab::Metrics::Dashboard::ServiceSelector do
it { is_expected.to be Metrics::Dashboard::ClusterDashboardService }
end
context 'when cluster is provided and embedded is not true' do
let(:arguments) { { cluster: "some cluster", embedded: 'false' } }
it { is_expected.to be Metrics::Dashboard::ClusterDashboardService }
end
context 'when cluster dashboard_path is provided' do
let(:arguments) { { dashboard_path: ::Metrics::Dashboard::ClusterDashboardService::DASHBOARD_PATH } }
it { is_expected.to be Metrics::Dashboard::ClusterDashboardService }
end
context 'when cluster is provided and embed params' do
let(:arguments) do
{
cluster: "some cluster",
embedded: 'true',
cluster_type: 'project',
format: :json,
group: 'Food metrics',
title: 'Pizza Consumption',
y_label: 'Slice Count'
}
end
it { is_expected.to be Metrics::Dashboard::ClusterMetricsEmbedService }
end
context 'when metrics embed is for an alert' do
let(:arguments) { { embedded: true, prometheus_alert_id: 5 } }
......
......@@ -3,9 +3,10 @@
require 'spec_helper'
describe Clusters::InstancePolicy do
let(:user) { create(:admin) }
let(:user) { build(:admin) }
let(:instance) { Clusters::Instance.new }
subject { described_class.new(user, Clusters::Instance.new) }
subject { described_class.new(user, instance) }
context 'when cluster deployments is available' do
before do
......@@ -15,11 +16,49 @@ describe Clusters::InstancePolicy do
it { is_expected.to be_allowed(:read_cluster_environments) }
end
context 'when cluster deployments is not available' do
context 'when cluster deployments is unavailable' do
before do
stub_licensed_features(cluster_deployments: false)
end
it { is_expected.not_to be_allowed(:read_cluster_environments) }
end
context 'when cluster is readable' do
context 'and cluster health is available' do
before do
stub_licensed_features(cluster_health: true)
end
it { is_expected.to be_allowed(:read_cluster_health) }
end
context 'and cluster health is unavailable' do
before do
stub_licensed_features(cluster_health: false)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
end
context 'when cluster is not readable to user' do
let(:user) { build(:user) }
context 'when cluster health is available' do
before do
stub_licensed_features(cluster_health: true)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
context 'when cluster health is unavailable' do
before do
stub_licensed_features(cluster_health: false)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
end
end
......@@ -635,4 +635,46 @@ describe GroupPolicy do
end
end
end
describe 'read_cluster_health' do
let(:current_user) { owner }
context 'when cluster is readable' do
context 'and cluster health is available' do
before do
stub_licensed_features(cluster_health: true)
end
it { is_expected.to be_allowed(:read_cluster_health) }
end
context 'and cluster health is unavailable' do
before do
stub_licensed_features(cluster_health: false)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
end
context 'when cluster is not readable to user' do
let(:current_user) { build(:user) }
context 'when cluster health is available' do
before do
stub_licensed_features(cluster_health: true)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
context 'when cluster health is unavailable' do
before do
stub_licensed_features(cluster_health: false)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
end
end
end
......@@ -1215,6 +1215,48 @@ describe ProjectPolicy do
end
end
describe 'read_cluster_health' do
let(:current_user) { owner }
context 'when cluster is readable' do
context 'and cluster health is available' do
before do
stub_licensed_features(cluster_health: true)
end
it { is_expected.to be_allowed(:read_cluster_health) }
end
context 'and cluster health is unavailable' do
before do
stub_licensed_features(cluster_health: false)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
end
context 'when cluster is not readable to user' do
let(:current_user) { build(:user) }
context 'when cluster health is available' do
before do
stub_licensed_features(cluster_health: true)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
context 'when cluster health is unavailable' do
before do
stub_licensed_features(cluster_health: false)
end
it { is_expected.to be_disallowed(:read_cluster_health) }
end
end
end
shared_examples 'merge request rules' do
let(:project) { create(:project, namespace: owner.namespace) }
......
......@@ -15,13 +15,19 @@ describe Metrics::Dashboard::ClusterDashboardService, :use_clean_rails_memory_st
end
describe '.valid_params?' do
let(:params) { { cluster: cluster } }
let(:params) { { cluster: cluster, embedded: 'false' } }
subject { described_class.valid_params?(params) }
it { is_expected.to be_truthy }
context 'missing cluster' do
context 'with matching dashboard_path' do
let(:params) { { dashboard_path: ::Metrics::Dashboard::ClusterDashboardService::DASHBOARD_PATH } }
it { is_expected.to be_truthy }
end
context 'missing cluster without dashboard_path' do
let(:params) { {} }
it { is_expected.to be_falsey }
......
# frozen_string_literal: true
require 'spec_helper'
describe Metrics::Dashboard::ClusterMetricsEmbedService, :use_clean_rails_memory_store_caching do
include MetricsDashboardHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:cluster_project) { create(:cluster_project) }
let_it_be(:cluster) { cluster_project.cluster }
let_it_be(:project) { cluster_project.project }
before do
project.add_maintainer(user)
end
describe '.valid_params?' do
let(:valid_params) { { cluster: 1, embedded: 'true', group: 'hello', title: 'world', y_label: 'countries' } }
subject { described_class }
it { expect(subject.valid_params?(valid_params)).to be_truthy }
context 'missing all params' do
let(:params) { {} }
it { expect(subject.valid_params?(params)).to be_falsy }
end
[:cluster, :embedded, :group, :title, :y_label].each do |param_key|
it 'returns false with missing param' do
params = valid_params.except(param_key)
expect(subject.valid_params?(params)).to be_falsy
end
end
end
describe '#get_dashboard' do
let(:service_params) do
[
project,
user,
{
cluster: cluster,
cluster_type: :project,
embedded: 'true',
group: 'Cluster Health',
title: 'CPU Usage',
y_label: 'CPU (cores)'
}
]
end
let(:service_call) { described_class.new(*service_params).get_dashboard }
let(:panel_groups) { service_call[:dashboard][:panel_groups] }
let(:panel) { panel_groups.first[:panels].first }
it_behaves_like 'valid embedded dashboard service response'
it_behaves_like 'caches the unprocessed dashboard for subsequent calls'
it 'returns one panel' do
expect(panel_groups.size).to eq 1
expect(panel_groups.first[:panels].size).to eq 1
end
it 'returns panel by title and y_label' do
expect(panel[:title]).to eq(service_params.last[:title])
expect(panel[:y_label]).to eq(service_params.last[:y_label])
end
end
end
......@@ -29,9 +29,11 @@ module Gitlab
# Used by embedded dashboards.
# @param options - y_label [String] Y-Axis label of
# a panel. Used by embedded dashboards.
# @param options - cluster [Cluster]
# @param options - cluster [Cluster]. Used by
# embedded and un-embedded dashboards.
# @param options - cluster_type [Symbol] The level of
# cluster, one of [:admin, :project, :group]
# cluster, one of [:admin, :project, :group]. Used by
# embedded and un-embedded dashboards.
# @param options - grafana_url [String] URL pointing
# to a grafana dashboard panel
# @param options - prometheus_alert_id [Integer] ID of
......
......@@ -3,7 +3,8 @@
# Responsible for determining which dashboard service should
# be used to fetch or generate a dashboard hash.
# The services can be considered in two categories - embeds
# and dashboards. Embeds are all portions of dashboards.
# and dashboards. Embed hashes are identical to dashboard hashes except
# that they contain a subset of panels.
module Gitlab
module Metrics
module Dashboard
......
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