diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/banzai/filter/inline_grafana_metrics_filter.rb | 78 | ||||
-rw-r--r-- | lib/banzai/filter/inline_metrics_redactor_filter.rb | 89 | ||||
-rw-r--r-- | lib/banzai/pipeline/gfm_pipeline.rb | 1 | ||||
-rw-r--r-- | lib/gitlab/metrics/dashboard/finder.rb | 3 | ||||
-rw-r--r-- | lib/gitlab/metrics/dashboard/service_selector.rb | 5 | ||||
-rw-r--r-- | lib/gitlab/metrics/dashboard/url.rb | 46 | ||||
-rw-r--r-- | lib/gitlab/workhorse.rb | 1 |
7 files changed, 185 insertions, 38 deletions
diff --git a/lib/banzai/filter/inline_grafana_metrics_filter.rb b/lib/banzai/filter/inline_grafana_metrics_filter.rb new file mode 100644 index 00000000000..34d93a59f48 --- /dev/null +++ b/lib/banzai/filter/inline_grafana_metrics_filter.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML filter that inserts a placeholder element for each + # reference to a grafana dashboard. + class InlineGrafanaMetricsFilter < Banzai::Filter::InlineEmbedsFilter + # Placeholder element for the frontend to use as an + # injection point for charts. + def create_element(params) + begin_loading_dashboard(params[:url]) + + doc.document.create_element( + 'div', + class: 'js-render-metrics', + 'data-dashboard-url': metrics_dashboard_url(params) + ) + end + + def embed_params(node) + return unless Feature.enabled?(:gfm_grafana_integration) + + query_params = Gitlab::Metrics::Dashboard::Url.parse_query(node['href']) + return unless [:panelId, :from, :to].all? do |param| + query_params.include?(param) + end + + { url: node['href'], start: query_params[:from], end: query_params[:to] } + end + + # Selects any links with an href contains the configured + # grafana domain for the project + def xpath_search + return unless grafana_url.present? + + %(descendant-or-self::a[starts-with(@href, '#{grafana_url}')]) + end + + private + + def project + context[:project] + end + + def grafana_url + project&.grafana_integration&.grafana_url + end + + def metrics_dashboard_url(params) + Gitlab::Routing.url_helpers.project_grafana_api_metrics_dashboard_url( + project, + embedded: true, + grafana_url: params[:url], + start: format_time(params[:start]), + end: format_time(params[:end]) + ) + end + + # Formats a timestamp from Grafana for compatibility with + # parsing in JS via `new Date(timestamp)` + # + # @param time [String] Represents miliseconds since epoch + def format_time(time) + Time.at(time.to_i / 1000).utc.strftime('%FT%TZ') + end + + # Fetches a dashboard and caches the result for the + # FE to fetch quickly while rendering charts + def begin_loading_dashboard(url) + ::Gitlab::Metrics::Dashboard::Finder.find( + project, + embedded: true, + grafana_url: url + ) + end + end + end +end diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb index 4d8a5028898..e84ba83e03e 100644 --- a/lib/banzai/filter/inline_metrics_redactor_filter.rb +++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb @@ -8,14 +8,17 @@ module Banzai include Gitlab::Utils::StrongMemoize METRICS_CSS_CLASS = '.js-render-metrics' + URL = Gitlab::Metrics::Dashboard::Url + + Embed = Struct.new(:project_path, :permission) # Finds all embeds based on the css class the FE # uses to identify the embedded content, removing # only unnecessary nodes. def call nodes.each do |node| - path = paths_by_node[node] - user_has_access = user_access_by_path[path] + embed = embeds_by_node[node] + user_has_access = user_access_by_embed[embed] node.remove unless user_has_access end @@ -30,40 +33,69 @@ module Banzai end # Returns all nodes which the FE will identify as - # a metrics dashboard placeholder element + # a metrics embed placeholder element # # @return [Nokogiri::XML::NodeSet] def nodes @nodes ||= doc.css(METRICS_CSS_CLASS) end - # Maps a node to the full path of a project. + # Maps a node to key properties of an embed. # Memoized so we only need to run the regex to get # the project full path from the url once per node. # - # @return [Hash<Nokogiri::XML::Node, String>] - def paths_by_node - strong_memoize(:paths_by_node) do - nodes.each_with_object({}) do |node, paths| - paths[node] = path_for_node(node) + # @return [Hash<Nokogiri::XML::Node, Embed>] + def embeds_by_node + strong_memoize(:embeds_by_node) do + nodes.each_with_object({}) do |node, embeds| + embed = Embed.new + url = node.attribute('data-dashboard-url').to_s + + set_path_and_permission(embed, url, URL.regex, :read_environment) + set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission + + embeds[node] = embed if embed.permission end end end - # Gets a project's full_path from the dashboard url - # in the placeholder node. The FE will use the attr - # `data-dashboard-url`, so we want to check against that - # attribute directly in case a user has manually - # created a metrics element (rather than supporting - # an alternate attr in InlineMetricsFilter). + # Attempts to determine the path and permission attributes + # of a url based on expected dashboard url formats and + # sets the attributes on an Embed object # - # @return [String] - def path_for_node(node) - url = node.attribute('data-dashboard-url').to_s - - Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m| + # @param embed [Embed] + # @param url [String] + # @param regex [RegExp] + # @param permission [Symbol] + def set_path_and_permission(embed, url, regex, permission) + return unless path = regex.match(url) do |m| "#{$~[:namespace]}/#{$~[:project]}" end + + embed.project_path = path + embed.permission = permission + end + + # Returns a mapping representing whether the current user + # has permission to view the embed for the project. + # Determined in a batch + # + # @return [Hash<Embed, Boolean>] + def user_access_by_embed + strong_memoize(:user_access_by_embed) do + unique_embeds.each_with_object({}) do |embed, access| + project = projects_by_path[embed.project_path] + + access[embed] = Ability.allowed?(user, embed.permission, project) + end + end + end + + # Returns a unique list of embeds + # + # @return [Array<Embed>] + def unique_embeds + embeds_by_node.values.uniq end # Maps a project's full path to a Project object. @@ -74,22 +106,17 @@ module Banzai def projects_by_path strong_memoize(:projects_by_path) do Project.eager_load(:route, namespace: [:route]) - .where_full_path_in(paths_by_node.values.uniq) + .where_full_path_in(unique_project_paths) .index_by(&:full_path) end end - # Returns a mapping representing whether the current user - # has permission to view the metrics for the project. - # Determined in a batch + # Returns a list of the full_paths of every project which + # has an embed in the doc # - # @return [Hash<Project, Boolean>] - def user_access_by_path - strong_memoize(:user_access_by_path) do - projects_by_path.each_with_object({}) do |(path, project), access| - access[path] = Ability.allowed?(user, :read_environment, project) - end - end + # @return [Array<String>] + def unique_project_paths + embeds_by_node.values.map(&:project_path).uniq end end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 08e27257fdf..f6c12cdb53b 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -30,6 +30,7 @@ module Banzai Filter::ImageLazyLoadFilter, Filter::ImageLinkFilter, Filter::InlineMetricsFilter, + Filter::InlineGrafanaMetricsFilter, Filter::TableOfContentsFilter, Filter::AutolinkFilter, Filter::ExternalLinkFilter, diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb index 297f109ff81..268112f33a9 100644 --- a/lib/gitlab/metrics/dashboard/finder.rb +++ b/lib/gitlab/metrics/dashboard/finder.rb @@ -12,6 +12,7 @@ module Gitlab # @param project [Project] # @param user [User] # @param environment [Environment] + # @param options [Hash<Symbol,Any>] # @param options - embedded [Boolean] Determines whether the # dashboard is to be rendered as part of an # issue or location other than the primary @@ -31,6 +32,8 @@ module Gitlab # @param options - cluster [Cluster] # @param options - cluster_type [Symbol] The level of # cluster, one of [:admin, :project, :group] + # @param options - grafana_url [String] URL pointing + # to a grafana dashboard panel # @return [Hash] def find(project, user, options = {}) service_for(options) diff --git a/lib/gitlab/metrics/dashboard/service_selector.rb b/lib/gitlab/metrics/dashboard/service_selector.rb index 10b686fbb81..aee7f6685ad 100644 --- a/lib/gitlab/metrics/dashboard/service_selector.rb +++ b/lib/gitlab/metrics/dashboard/service_selector.rb @@ -18,6 +18,7 @@ module Gitlab # @return [Gitlab::Metrics::Dashboard::Services::BaseService] def call(params) return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params) + return SERVICES::GrafanaMetricEmbedService if grafana_metric_embed?(params) return SERVICES::DynamicEmbedService if dynamic_embed?(params) return SERVICES::DefaultEmbedService if params[:embedded] return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path]) @@ -40,6 +41,10 @@ module Gitlab SERVICES::CustomMetricEmbedService.valid_params?(params) end + def grafana_metric_embed?(params) + SERVICES::GrafanaMetricEmbedService.valid_params?(params) + end + def dynamic_embed?(params) SERVICES::DynamicEmbedService.valid_params?(params) end diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb index 94f8b2e02b1..712f769bbeb 100644 --- a/lib/gitlab/metrics/dashboard/url.rb +++ b/lib/gitlab/metrics/dashboard/url.rb @@ -14,17 +14,31 @@ module Gitlab def regex %r{ (?<url> - #{Regexp.escape(Gitlab.config.gitlab.url)} - \/#{Project.reference_pattern} + #{gitlab_pattern} + #{project_pattern} (?:\/\-)? \/environments \/(?<environment>\d+) \/metrics - (?<query> - \?[a-zA-Z0-9%.()+_=-]+ - (&[a-zA-Z0-9%.()+_=-]+)* - )? - (?<anchor>\#[a-z0-9_-]+)? + #{query_pattern} + #{anchor_pattern} + ) + }x + end + + # Matches dashboard urls for a Grafana embed. + # + # EX - https://<host>/<namespace>/<project>/grafana/metrics_dashboard + def grafana_regex + %r{ + (?<url> + #{gitlab_pattern} + #{project_pattern} + (?:\/\-)? + \/grafana + \/metrics_dashboard + #{query_pattern} + #{anchor_pattern} ) }x end @@ -45,6 +59,24 @@ module Gitlab def build_dashboard_url(*args) Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args) end + + private + + def gitlab_pattern + Regexp.escape(Gitlab.config.gitlab.url) + end + + def project_pattern + "\/#{Project.reference_pattern}" + end + + def query_pattern + '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?' + end + + def anchor_pattern + '(?<anchor>\#[a-z0-9_-]+)?' + end end end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index db67e4fd479..713ca31bbc5 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -14,6 +14,7 @@ module Gitlab NOTIFICATION_CHANNEL = 'workhorse:notifications' ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type' + ARCHIVE_FORMATS = %w(zip tar.gz tar.bz2 tar).freeze include JwtAuthenticatable |