Commit 664c4c7b authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 6791eefe
...@@ -32,4 +32,5 @@ lib/gitlab/github_import/ @gitlab-org/maintainers/database ...@@ -32,4 +32,5 @@ lib/gitlab/github_import/ @gitlab-org/maintainers/database
/.gitlab/ci/ @gl-quality/eng-prod /.gitlab/ci/ @gl-quality/eng-prod
Dangerfile @gl-quality/eng-prod Dangerfile @gl-quality/eng-prod
/danger/ @gl-quality/eng-prod /danger/ @gl-quality/eng-prod
/lib/gitlab/danger/ @gl-quality/eng-prod
/scripts/ @gl-quality/eng-prod /scripts/ @gl-quality/eng-prod
...@@ -97,7 +97,10 @@ schedule:review-build-cng: ...@@ -97,7 +97,10 @@ schedule:review-build-cng:
variables: variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "v2.3.7" # v2.3.7 + some stability improvements not yet released:
# - sidekiq readinessProbe should be `pgrep -f sidekiq`: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/991
# - Allows livenessProbe and readinessProbe to be configured for unicorn: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/985
GITLAB_HELM_CHART_REF: "df7c52dc69df441909880b8f2fd15e938cdb2047"
GITLAB_EDITION: "ce" GITLAB_EDITION: "ce"
environment: environment:
name: review/${CI_COMMIT_REF_NAME} name: review/${CI_COMMIT_REF_NAME}
......
...@@ -29,7 +29,7 @@ Set the title to: `Description of the original issue` ...@@ -29,7 +29,7 @@ Set the title to: `Description of the original issue`
#### Documentation and final details #### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links) - [ ] Check the topic on #releases to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Add links to this issue and your MRs in the description of the security release issue - [ ] Add links to this issue and your MRs in the description of the security release issue
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details) - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
......
...@@ -14,7 +14,7 @@ class CreateBranchService < BaseService ...@@ -14,7 +14,7 @@ class CreateBranchService < BaseService
if new_branch if new_branch
success(new_branch) success(new_branch)
else else
error('Invalid reference name') error("Invalid reference name: #{branch_name}")
end end
rescue Gitlab::Git::PreReceiveError => ex rescue Gitlab::Git::PreReceiveError => ex
error(ex.message) error(ex.message)
......
# frozen_string_literal: true
# Responsible for returning a gitlab-compatible dashboard
# containing info based on a grafana dashboard and datasource.
#
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
module Metrics
module Dashboard
class GrafanaMetricEmbedService < ::Metrics::Dashboard::BaseService
include ReactiveCaching
SEQUENCE = [
::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter
].freeze
self.reactive_cache_key = ->(service) { service.cache_key }
self.reactive_cache_lease_timeout = 30.seconds
self.reactive_cache_refresh_interval = 30.minutes
self.reactive_cache_lifetime = 30.minutes
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
class << self
# Determines whether the provided params are sufficient
# to uniquely identify a grafana dashboard.
def valid_params?(params)
[
params[:embedded],
params[:grafana_url]
].all?
end
def from_cache(project_id, user_id, grafana_url)
project = Project.find(project_id)
user = User.find(user_id)
new(project, user, grafana_url: grafana_url)
end
end
def get_dashboard
with_reactive_cache(*cache_key) { |result| result }
end
# Inherits the primary logic from the parent class and
# maintains the service's API while including ReactiveCache
def calculate_reactive_cache(*)
::Metrics::Dashboard::BaseService
.instance_method(:get_dashboard)
.bind(self)
.call() # rubocop:disable Style/MethodCallWithoutArgsParentheses
end
def cache_key(*args)
[project.id, current_user.id, grafana_url]
end
# Required for ReactiveCaching; Usage overridden by
# self.reactive_cache_worker_finder
def id
nil
end
private
def get_raw_dashboard
raise MissingIntegrationError unless client
grafana_dashboard = fetch_dashboard
datasource = fetch_datasource(grafana_dashboard)
params.merge!(grafana_dashboard: grafana_dashboard, datasource: datasource)
{}
end
def fetch_dashboard
uid = GrafanaUidParser.new(grafana_url, project).parse
raise DashboardProcessingError.new('Dashboard uid not found') unless uid
response = client.get_dashboard(uid: uid)
parse_json(response.body)
end
def fetch_datasource(dashboard)
name = DatasourceNameParser.new(grafana_url, dashboard).parse
raise DashboardProcessingError.new('Datasource name not found') unless name
response = client.get_datasource(name: name)
parse_json(response.body)
end
def grafana_url
params[:grafana_url]
end
def client
project.grafana_integration&.client
end
def allowed?
Ability.allowed?(current_user, :read_project, project)
end
def sequence
SEQUENCE
end
def parse_json(json)
JSON.parse(json, symbolize_names: true)
rescue JSON::ParserError
raise DashboardProcessingError.new('Grafana response contains invalid json')
end
end
# Identifies the uid of the dashboard based on url format
class GrafanaUidParser
def initialize(grafana_url, project)
@grafana_url, @project = grafana_url, project
end
def parse
@grafana_url.match(uid_regex) { |m| m.named_captures['uid'] }
end
private
# URLs are expected to look like https://domain.com/d/:uid/other/stuff
def uid_regex
base_url = @project.grafana_integration.grafana_url.chomp('/')
%r{(#{Regexp.escape(base_url)}\/d\/(?<uid>\w+)\/)}x
end
end
# Identifies the name of the datasource for a dashboard
# based on the panelId query parameter found in the url
class DatasourceNameParser
def initialize(grafana_url, grafana_dashboard)
@grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard
end
def parse
@grafana_dashboard[:dashboard][:panels]
.find { |panel| panel[:id].to_s == query_params[:panelId] }
.try(:[], :datasource)
end
private
def query_params
Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url)
end
end
end
end
...@@ -4,4 +4,4 @@ ...@@ -4,4 +4,4 @@
= password_field_tag :password, nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' } = password_field_tag :password, nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
.submit-container.move-submit-down .submit-container.move-submit-down
= submit_tag _('Enter admin mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' } = submit_tag _('Enter Admin Mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' } %ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
%li.nav-item{ role: 'presentation' } %li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter admin mode') %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter Admin Mode')
- @hide_breadcrumbs = true - @hide_breadcrumbs = true
- page_title _('Enter admin mode') - page_title _('Enter Admin Mode')
.row.justify-content-center .row.justify-content-center
.col-6.new-session-forms-container .col-6.new-session-forms-container
......
...@@ -55,15 +55,15 @@ ...@@ -55,15 +55,15 @@
= nav_link(controller: 'admin/dashboard') do = nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link d-xl-none' do = link_to admin_root_path, class: 'admin-icon qa-admin-area-link d-xl-none' do
= _('Admin Area') = _('Admin Area')
- if Feature.enabled?(:user_mode_in_session) - if Feature.enabled?(:user_mode_in_session)
- if header_link?(:admin_mode) - if header_link?(:admin_mode)
= nav_link(controller: 'admin/sessions') do = nav_link(controller: 'admin/sessions') do
= link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
= _('Leave admin mode') = _('Leave Admin Mode')
- elsif current_user.admin? - elsif current_user.admin?
= nav_link(controller: 'admin/sessions') do = nav_link(controller: 'admin/sessions') do
= link_to new_admin_session_path, class: 'd-lg-none lock-icon' do = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
= _('Enter admin mode') = _('Enter Admin Mode')
- if Gitlab::Sherlock.enabled? - if Gitlab::Sherlock.enabled?
%li %li
= link_to sherlock_transactions_path, class: 'admin-icon' do = link_to sherlock_transactions_path, class: 'admin-icon' do
...@@ -74,6 +74,15 @@ ...@@ -74,6 +74,15 @@
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin', size: 18) = sprite_icon('admin', size: 18)
- if Feature.enabled?(:user_mode_in_session)
- if header_link?(:admin_mode)
= nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
= link_to destroy_admin_session_path, title: _('Leave Admin Mode'), aria: { label: _('Leave Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= sprite_icon('lock-open', size: 18)
- elsif current_user.admin?
= nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
= link_to new_admin_session_path, title: _('Enter Admin Mode'), aria: { label: _('Enter Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= sprite_icon('lock', size: 18)
-# Shortcut to Dashboard > Projects -# Shortcut to Dashboard > Projects
- if dashboard_nav_link?(:projects) - if dashboard_nav_link?(:projects)
......
---
title: Enable the color chip in AsciiDoc documents
merge_request: 18723
author:
type: added
---
title: Resolve Error when uploading a few designs in a row
merge_request: 18811
author:
type: fixed
---
title: Fix missing admin mode UI buttons on bigger screen sizes
merge_request: 18585
author: Diego Louzán
type: fixed
...@@ -10,6 +10,7 @@ module Banzai ...@@ -10,6 +10,7 @@ module Banzai
Filter::SyntaxHighlightFilter, Filter::SyntaxHighlightFilter,
Filter::ExternalLinkFilter, Filter::ExternalLinkFilter,
Filter::PlantumlFilter, Filter::PlantumlFilter,
Filter::ColorFilter,
Filter::AsciiDocPostProcessingFilter Filter::AsciiDocPostProcessingFilter
] ]
end end
......
...@@ -9,6 +9,7 @@ module Gitlab ...@@ -9,6 +9,7 @@ module Gitlab
module Errors module Errors
DashboardProcessingError = Class.new(StandardError) DashboardProcessingError = Class.new(StandardError)
PanelNotFoundError = Class.new(StandardError) PanelNotFoundError = Class.new(StandardError)
MissingIntegrationError = Class.new(StandardError)
LayoutError = Class.new(DashboardProcessingError) LayoutError = Class.new(DashboardProcessingError)
MissingQueryError = Class.new(DashboardProcessingError) MissingQueryError = Class.new(DashboardProcessingError)
...@@ -22,6 +23,10 @@ module Gitlab ...@@ -22,6 +23,10 @@ module Gitlab
error("#{dashboard_path} could not be found.", :not_found) error("#{dashboard_path} could not be found.", :not_found)
when PanelNotFoundError when PanelNotFoundError
error(error.message, :not_found) error(error.message, :not_found)
when ::Grafana::Client::Error
error(error.message, :service_unavailable)
when MissingIntegrationError
error('Proxy support for this API is not available currently', :bad_request)
else else
raise error raise error
end end
......
...@@ -17,7 +17,10 @@ module Gitlab ...@@ -17,7 +17,10 @@ module Gitlab
# Returns a new dashboard hash with the results of # Returns a new dashboard hash with the results of
# running transforms on the dashboard. # running transforms on the dashboard.
# @return [Hash, nil]
def process def process
return unless @dashboard
@dashboard.deep_symbolize_keys.tap do |dashboard| @dashboard.deep_symbolize_keys.tap do |dashboard|
@sequence.each do |stage| @sequence.each do |stage|
stage.new(@project, dashboard, @params).transform! stage.new(@project, dashboard, @params).transform!
......
# frozen_string_literal: true
module Gitlab
module Metrics
module Dashboard
module Stages
class GrafanaFormatter < BaseStage
include Gitlab::Utils::StrongMemoize
CHART_TYPE = 'area-chart'
PROXY_PATH = 'api/v1/query_range'
# Reformats the specified panel in the Gitlab
# dashboard-yml format
def transform!
InputFormatValidator.new(
grafana_dashboard,
datasource,
panel,
query_params
).validate!
new_dashboard = formatted_dashboard
dashboard.clear
dashboard.merge!(new_dashboard)
end
private
def formatted_dashboard
{ panel_groups: [{ panels: [formatted_panel] }] }
end
def formatted_panel
{
title: panel[:title],
type: CHART_TYPE,
y_label: '', # Grafana panels do not include a Y-Axis label
metrics: panel[:targets].map.with_index do |target, idx|
formatted_metric(target, idx)
end
}
end
def formatted_metric(metric, idx)
{
id: "#{metric[:legendFormat]}_#{idx}",
query_range: format_query(metric),
label: replace_variables(metric[:legendFormat]),
prometheus_endpoint_path: prometheus_endpoint_for_metric(metric)
}.compact
end
# Panel specified by the url from the Grafana dashboard
def panel
strong_memoize(:panel) do
grafana_dashboard[:dashboard][:panels].find do |panel|
panel[:id].to_s == query_params[:panelId]
end
end
end
# Grafana url query parameters. Includes information
# on which panel to select and time range.
def query_params
strong_memoize(:query_params) do
Gitlab::Metrics::Dashboard::Url.parse_query(grafana_url)
end
end
# Endpoint which will return prometheus metric data
# for the metric
def prometheus_endpoint_for_metric(metric)
Gitlab::Routing.url_helpers.project_grafana_api_path(
project,
datasource_id: datasource[:id],
proxy_path: PROXY_PATH,
query: format_query(metric)
)
end
# Reformats query for compatibility with prometheus api.
def format_query(metric)
expression = remove_new_lines(metric[:expr])
expression = replace_variables(expression)
expression = replace_global_variables(expression, metric)
expression
end
# Accomodates instance-defined Grafana variables.
# These are variables defined by users, and values
# must be provided in the query parameters.
def replace_variables(expression)
return expression unless grafana_dashboard[:dashboard][:templating]
grafana_dashboard[:dashboard][:templating][:list]
.sort_by { |variable| variable[:name].length }
.each do |variable|
variable_value = query_params[:"var-#{variable[:name]}"]
expression = expression.gsub("$#{variable[:name]}", variable_value)
expression = expression.gsub("[[#{variable[:name]}]]", variable_value)
expression = expression.gsub("{{#{variable[:name]}}}", variable_value)
end
expression
end
# Replaces Grafana global built-in variables with values.
# Only $__interval and $__from and $__to are supported.
#
# See https://grafana.com/docs/reference/templating/#global-built-in-variables
def replace_global_variables(expression, metric)
expression = expression.gsub('$__interval', metric[:interval]) if metric[:interval]
expression = expression.gsub('$__from', query_params[:from])
expression = expression.gsub('$__to', query_params[:to])
expression
end
# Removes new lines from expression.
def remove_new_lines(expression)
expression.gsub(/\R+/, '')
end
# Grafana datasource object corresponding to the
# specified dashboard
def datasource
params[:datasource]
end
# The specified Grafana dashboard
def grafana_dashboard
params[:grafana_dashboard]
end
# The URL specifying which Grafana panel to embed
def grafana_url
params[:grafana_url]
end
end
class InputFormatValidator
include ::Gitlab::Metrics::Dashboard::Errors
attr_reader :grafana_dashboard, :datasource, :panel, :query_params
UNSUPPORTED_GRAFANA_GLOBAL_VARS = %w(
$__interval_ms
$__timeFilter
$__name
$timeFilter
$interval
).freeze
def initialize(grafana_dashboard, datasource, panel, query_params)
@grafana_dashboard = grafana_dashboard
@datasource = datasource
@panel = panel
@query_params = query_params
end
def validate!
validate_query_params!
validate_datasource!
validate_panel_type!
validate_variable_definitions!
validate_global_variables!
end
private
def validate_datasource!
return if datasource[:access] == 'proxy' && datasource[:type] == 'prometheus'
raise_error 'Only Prometheus datasources with proxy access in Grafana are supported.'
end
def validate_query_params!
return if [:panelId, :from, :to].all? { |param| query_params.include?(param) }
raise_error 'Grafana query parameters must include panelId, from, and to.'
end
def validate_panel_type!
return if panel[:type] == 'graph' && panel[:lines]
raise_error 'Panel type must be a line graph.'
end
def validate_variable_definitions!
return unless grafana_dashboard[:dashboard][:templating]
return if grafana_dashboard[:dashboard][:templating][:list].all? do |variable|
query_params[:"var-#{variable[:name]}"].present?
end
raise_error 'All Grafana variables must be defined in the query parameters.'
end
def validate_global_variables!
return unless panel_contains_unsupported_vars?
raise_error 'Prometheus must not include'
end
def panel_contains_unsupported_vars?
panel[:targets].any? do |target|
UNSUPPORTED_GRAFANA_GLOBAL_VARS.any? do |variable|
target[:expr].include?(variable)
end
end
end
def raise_error(message)
raise DashboardProcessingError.new(message)
end
end
end
end
end
end
...@@ -11,6 +11,18 @@ module Grafana ...@@ -11,6 +11,18 @@ module Grafana
@token = token @token = token
end end
# @param uid [String] Unique identifier for a Grafana dashboard
def get_dashboard(uid:)
http_get("#{@api_url}/api/dashboards/uid/#{uid}")
end
# @param name [String] Unique identifier for a Grafana datasource
def get_datasource(name:)
# CGI#escape formats strings such that the Grafana endpoint
# will not recognize the dashboard name. Preferring URI#escape.
http_get("#{@api_url}/api/datasources/name/#{URI.escape(name)}") # rubocop:disable Lint/UriEscapeUnescape
end
# @param datasource_id [String] Grafana ID for the datasource # @param datasource_id [String] Grafana ID for the datasource
# @param proxy_path [String] Path to proxy - ex) 'api/v1/query_range' # @param proxy_path [String] Path to proxy - ex) 'api/v1/query_range'
def proxy_datasource(datasource_id:, proxy_path:, query: {}) def proxy_datasource(datasource_id:, proxy_path:, query: {})
...@@ -57,7 +69,7 @@ module Grafana ...@@ -57,7 +69,7 @@ module Grafana
def handle_response(response) def handle_response(response)
return response if response.code == 200 return response if response.code == 200
raise_error "Grafana response status code: #{response.code}" raise_error "Grafana response status code: #{response.code}, Message: #{response.body}"
end end
def raise_error(message) def raise_error(message)
......
...@@ -6099,13 +6099,13 @@ msgstr "" ...@@ -6099,13 +6099,13 @@ msgstr ""
msgid "Ensure your %{linkStart}environment is part of the deploy stage%{linkEnd} of your CI pipeline to track deployments to your cluster." msgid "Ensure your %{linkStart}environment is part of the deploy stage%{linkEnd} of your CI pipeline to track deployments to your cluster."
msgstr "" msgstr ""
msgid "Enter IP address range" msgid "Enter Admin Mode"
msgstr "" msgstr ""
msgid "Enter a number" msgid "Enter IP address range"
msgstr "" msgstr ""
msgid "Enter admin mode" msgid "Enter a number"
msgstr "" msgstr ""
msgid "Enter at least three characters to search" msgid "Enter at least three characters to search"
...@@ -9680,7 +9680,7 @@ msgstr "" ...@@ -9680,7 +9680,7 @@ msgstr ""
msgid "Leave" msgid "Leave"
msgstr "" msgstr ""
msgid "Leave admin mode" msgid "Leave Admin Mode"
msgstr "" msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost." msgid "Leave edit mode? All unsaved changes will be lost."
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
FactoryBot.define do FactoryBot.define do
factory :grafana_integration, class: GrafanaIntegration do factory :grafana_integration, class: GrafanaIntegration do
project project
grafana_url { 'https://grafana.com' } grafana_url { 'https://grafana.example.com' }
token { SecureRandom.hex(10) } token { SecureRandom.hex(10) }
end end
end end
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include StubENV include StubENV
include TermsHelper include TermsHelper
include MobileHelpers
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
...@@ -450,6 +451,32 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc ...@@ -450,6 +451,32 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(page).to have_link(text: 'Support', href: new_support_url) expect(page).to have_link(text: 'Support', href: new_support_url)
end end
end end
it 'Shows admin dashboard links on bigger screen' do
visit root_dashboard_path
page.within '.navbar' do
expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
end
end
it 'Relocates admin dashboard links to dropdown list on smaller screen', :js do
resize_screen_xs
visit root_dashboard_path
page.within '.navbar' do
expect(page).not_to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
expect(page).not_to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
end
find('.header-more').click
page.within '.navbar' do
expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
end
end
end end
context 'when in admin_mode' do context 'when in admin_mode' do
...@@ -462,7 +489,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc ...@@ -462,7 +489,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
it 'can leave admin mode' do it 'can leave admin mode' do
page.within('.navbar-sub-nav') do page.within('.navbar-sub-nav') do
# Select first, link is also included in mobile view list # Select first, link is also included in mobile view list
click_on 'Leave admin mode', match: :first click_on 'Leave Admin Mode', match: :first
expect(page).to have_link(href: new_admin_session_path) expect(page).to have_link(href: new_admin_session_path)
end end
...@@ -481,7 +508,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc ...@@ -481,7 +508,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
before do before do
page.within('.navbar-sub-nav') do page.within('.navbar-sub-nav') do
# Select first, link is also included in mobile view list # Select first, link is also included in mobile view list
click_on 'Leave admin mode', match: :first click_on 'Leave Admin Mode', match: :first
end end
end end
......
This diff is collapsed.
{
"id": 1,
"orgId": 1,
"name": "GitLab Omnibus",
"type": "prometheus",
"typeLogoUrl": "",
"access": "proxy",
"url": "http://localhost:9090",
"password": "",
"user": "",
"database": "",
"basicAuth": false,
"basicAuthUser": "",
"basicAuthPassword": "",
"withCredentials": false,
"isDefault": true,
"jsonData": {},
"secureJsonFields": {},
"version": 1,
"readOnly": true
}
{
"panel_groups": [
{
"panels": [
{
"title": "Network I/O",
"type": "area-chart",
"y_label": "",
"metrics": [
{
"id": "In_0",
"query_range": "sum( rate(redis_net_input_bytes_total{instance=~\"localhost:9121\"}[1m]))",
"label": "In",
"prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_input_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
},
{
"id": "Out_1",
"query_range": "sum( rate(redis_net_output_bytes_total{instance=~\"localhost:9121\"}[1m]))",
"label": "Out",
"prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_output_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
}
]
}
]
}
]
}
{
"dashboard": {
"panels": [
{
"datasource": "GitLab Omnibus",
"id": 8,
"lines": true,
"targets": [
{
"expr": "sum(\n rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
"format": "time_series",
"interval": "1m",
"legendFormat": "In",
"refId": "A"
},
{
"expr": "sum(\n rate(redis_net_output_bytes_total{instance=~\"[[instance]]\"}[$__interval])\n)",
"format": "time_series",
"interval": "1m",
"legendFormat": "Out",
"refId": "B"
}
],
"title": "Network I/O",
"type": "graph",
"yaxes": [{ "format": "Bps" }, { "format": "short" }]
}
],
"templating": {
"list": [
{
"current": {
"value": "localhost:9121"
},
"name": "instance"
}
]
}
}
}
{ {
"type": "object", "type": "object",
"required": [ "required": [
"unit",
"label", "label",
"prometheus_endpoint_path" "prometheus_endpoint_path"
], ],
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
"required": [ "required": [
"title", "title",
"y_label", "y_label",
"weight",
"metrics" "metrics"
], ],
"properties": { "properties": {
......
...@@ -25,6 +25,14 @@ describe Gitlab::Metrics::Dashboard::Processor do ...@@ -25,6 +25,14 @@ describe Gitlab::Metrics::Dashboard::Processor do
end end
end end
context 'when the dashboard is not present' do
let(:dashboard_yml) { nil }
it 'returns nil' do
expect(dashboard).to be_nil
end
end
context 'when dashboard config corresponds to common metrics' do context 'when dashboard config corresponds to common metrics' do
let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') } let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do
include GrafanaApiHelpers
let_it_be(:namespace) { create(:namespace, name: 'foo') }
let_it_be(:project) { create(:project, namespace: namespace, name: 'bar') }
describe '#transform!' do
let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) }
let(:datasource) { JSON.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) }
let(:dashboard) { described_class.new(project, {}, params).transform! }
let(:params) do
{
grafana_dashboard: grafana_dashboard,
datasource: datasource,
grafana_url: valid_grafana_dashboard_link('https://grafana.example.com')
}
end
context 'when the query and resources are configured correctly' do
let(:expected_dashboard) { JSON.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) }
it 'generates a gitlab-yml formatted dashboard' do
expect(dashboard).to eq(expected_dashboard)
end
end
context 'when the inputs are invalid' do
shared_examples_for 'processing error' do
it 'raises a processing error' do
expect { dashboard }
.to raise_error(Gitlab::Metrics::Dashboard::Stages::InputFormatValidator::DashboardProcessingError)
end
end
context 'when the datasource is not proxyable' do
before do
params[:datasource][:access] = 'not-proxy'
end
it_behaves_like 'processing error'
end
context 'when query param "panelId" is not specified' do
before do
params[:grafana_url].gsub!('panelId=8', '')
end
it_behaves_like 'processing error'
end
context 'when query param "from" is not specified' do
before do
params[:grafana_url].gsub!('from=1570397739557', '')
end
it_behaves_like 'processing error'
end
context 'when query param "to" is not specified' do
before do
params[:grafana_url].gsub!('to=1570484139557', '')
end
it_behaves_like 'processing error'
end
context 'when the panel is not a graph' do
before do
params[:grafana_dashboard][:dashboard][:panels][0][:type] = 'singlestat'
end
it_behaves_like 'processing error'
end
context 'when the panel is not a line graph' do
before do
params[:grafana_dashboard][:dashboard][:panels][0][:lines] = false
end
it_behaves_like 'processing error'
end
context 'when the query dashboard includes undefined variables' do
before do
params[:grafana_url].gsub!('&var-instance=localhost:9121', '')
end
it_behaves_like 'processing error'
end
context 'when the expression contains unsupported global variables' do
before do
params[:grafana_dashboard][:dashboard][:panels][0][:targets][0][:expr] = 'sum(important_metric[$__interval_ms])'
end
it_behaves_like 'processing error'
end
end
end
end
...@@ -35,7 +35,7 @@ describe Grafana::Client do ...@@ -35,7 +35,7 @@ describe Grafana::Client do
it 'does not follow redirects' do it 'does not follow redirects' do
expect { subject }.to raise_exception( expect { subject }.to raise_exception(
Grafana::Client::Error, Grafana::Client::Error,
'Grafana response status code: 302' 'Grafana response status code: 302, Message: {}'
) )
expect(redirect_req_stub).to have_been_requested expect(redirect_req_stub).to have_been_requested
...@@ -67,6 +67,30 @@ describe Grafana::Client do ...@@ -67,6 +67,30 @@ describe Grafana::Client do
end end
end end
describe '#get_dashboard' do
let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/dashboards/uid/FndfgnX' }
subject do
client.get_dashboard(uid: 'FndfgnX')
end
it_behaves_like 'calls grafana api'
it_behaves_like 'no redirects'
it_behaves_like 'handles exceptions'
end
describe '#get_datasource' do
let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/datasources/name/Test%20Name' }
subject do
client.get_datasource(name: 'Test Name')
end
it_behaves_like 'calls grafana api'
it_behaves_like 'no redirects'
it_behaves_like 'handles exceptions'
end
describe '#proxy_datasource' do describe '#proxy_datasource' do
let(:grafana_api_url) do let(:grafana_api_url) do
'https://grafanatest.com/-/grafana-project/' \ 'https://grafanatest.com/-/grafana-project/' \
......
...@@ -602,7 +602,7 @@ describe API::Branches do ...@@ -602,7 +602,7 @@ describe API::Branches do
post api(route, user), params: { branch: 'new_design3', ref: 'foo' } post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Invalid reference name') expect(json_response['message']).to eq('Invalid reference name: new_design3')
end end
end end
......
...@@ -22,5 +22,20 @@ describe CreateBranchService do ...@@ -22,5 +22,20 @@ describe CreateBranchService do
expect(project.repository.branch_exists?('my-feature')).to be_truthy expect(project.repository.branch_exists?('my-feature')).to be_truthy
end end
end end
context 'when creating a branch fails' do
let(:project) { create(:project_empty_repo) }
before do
allow(project.repository).to receive(:add_branch).and_return(false)
end
it 'retruns an error with the branch name' do
result = service.execute('my-feature', 'master')
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Invalid reference name: my-feature")
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Metrics::Dashboard::GrafanaMetricEmbedService do
include MetricsDashboardHelpers
include ReactiveCachingHelpers
include GrafanaApiHelpers
let_it_be(:project) { build(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
let(:grafana_url) do
valid_grafana_dashboard_link(grafana_integration.grafana_url)
end
before do
project.add_maintainer(user)
end
describe '.valid_params?' do
let(:valid_params) { { embedded: true, grafana_url: grafana_url } }
subject { described_class.valid_params?(params) }
let(:params) { valid_params }
it { is_expected.to be_truthy }
context 'not embedded' do
let(:params) { valid_params.except(:embedded) }
it { is_expected.to be_falsey }
end
context 'undefined grafana_url' do
let(:params) { valid_params.except(:grafana_url) }
it { is_expected.to be_falsey }
end
end
describe '.from_cache' do
let(:params) { [project.id, user.id, grafana_url] }
subject { described_class.from_cache(*params) }
it 'initializes an instance of GrafanaMetricEmbedService' do
expect(subject).to be_an_instance_of(described_class)
expect(subject.project).to eq(project)
expect(subject.current_user).to eq(user)
expect(subject.params[:grafana_url]).to eq(grafana_url)
end
end
describe '#get_dashboard', :use_clean_rails_memory_store_caching do
let(:service_params) do
[
project,
user,
{
embedded: true,
grafana_url: grafana_url
}
]
end
let(:service) { described_class.new(*service_params) }
let(:service_call) { service.get_dashboard }
context 'without caching' do
before do
synchronous_reactive_cache(service)
end
it_behaves_like 'raises error for users with insufficient permissions'
context 'without a grafana integration' do
before do
allow(project).to receive(:grafana_integration).and_return(nil)
end
it_behaves_like 'misconfigured dashboard service response', :bad_request
end
context 'when grafana cannot be reached' do
before do
allow(grafana_integration.client).to receive(:get_dashboard).and_raise(::Grafana::Client::Error)
end
it_behaves_like 'misconfigured dashboard service response', :service_unavailable
end
context 'when panelId is missing' do
let(:grafana_url) do
grafana_integration.grafana_url +
'/d/XDaNK6amz/gitlab-omnibus-redis' \
'?from=1570397739557&to=1570484139557'
end
before do
stub_dashboard_request(grafana_integration.grafana_url)
end
it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
end
context 'when uid is missing' do
let(:grafana_url) { grafana_integration.grafana_url + '/d/' }
before do
stub_dashboard_request(grafana_integration.grafana_url)
end
it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
end
context 'when the dashboard response contains misconfigured json' do
before do
stub_dashboard_request(grafana_integration.grafana_url, body: '')
end
it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
end
context 'when the datasource response contains misconfigured json' do
before do
stub_dashboard_request(grafana_integration.grafana_url)
stub_datasource_request(grafana_integration.grafana_url, body: '')
end
it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
end
context 'when the embed was created successfully' do
before do
stub_dashboard_request(grafana_integration.grafana_url)
stub_datasource_request(grafana_integration.grafana_url)
end
it_behaves_like 'valid embedded dashboard service response'
end
end
context 'with caching', :use_clean_rails_memory_store_caching do
let(:cache_params) { [project.id, user.id, grafana_url] }
context 'when value not present in cache' do
it 'returns nil' do
expect(ReactiveCachingWorker)
.to receive(:perform_async)
.with(service.class, service.id, *cache_params)
expect(service_call).to eq(nil)
end
end
context 'when value present in cache' do
let(:return_value) { { 'http_status' => :ok, 'dashboard' => '{}' } }
before do
stub_reactive_cache(service, return_value, cache_params)
end
it 'returns cached value' do
expect(ReactiveCachingWorker)
.not_to receive(:perform_async)
.with(service.class, service.id, *cache_params)
expect(service_call[:http_status]).to eq(return_value[:http_status])
expect(service_call[:dashboard]).to eq(return_value[:dashboard])
end
end
end
end
end
# frozen_string_literal: true
module GrafanaApiHelpers
def valid_grafana_dashboard_link(base_url)
base_url +
'/d/XDaNK6amz/gitlab-omnibus-redis' \
'?from=1570397739557&to=1570484139557' \
'&var-instance=localhost:9121&panelId=8'
end
def stub_dashboard_request(base_url, path: '/api/dashboards/uid/XDaNK6amz', body: nil)
body ||= fixture_file('grafana/dashboard_response.json')
stub_request(:get, "#{base_url}#{path}")
.to_return(
status: 200,
body: body,
headers: { 'Content-Type' => 'application/json' }
)
end
def stub_datasource_request(base_url, path: '/api/datasources/name/GitLab%20Omnibus', body: nil)
body ||= fixture_file('grafana/datasource_response.json')
stub_request(:get, "#{base_url}#{path}")
.to_return(
status: 200,
body: body,
headers: { 'Content-Type' => 'application/json' }
)
end
end
...@@ -53,7 +53,7 @@ module LoginHelpers ...@@ -53,7 +53,7 @@ module LoginHelpers
fill_in 'password', with: user.password fill_in 'password', with: user.password
click_button 'Enter admin mode' click_button 'Enter Admin Mode'
end end
def gitlab_sign_in_via(provider, user, uid, saml_response = nil) def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
......
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