diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index a02740373da..ddfdf72cf99 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -32,4 +32,5 @@ lib/gitlab/github_import/ @gitlab-org/maintainers/database /.gitlab/ci/ @gl-quality/eng-prod Dangerfile @gl-quality/eng-prod /danger/ @gl-quality/eng-prod +/lib/gitlab/danger/ @gl-quality/eng-prod /scripts/ @gl-quality/eng-prod diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index ad516aba7c3..09cf38908a6 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -97,7 +97,10 @@ schedule:review-build-cng: variables: HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" 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" environment: name: review/${CI_COMMIT_REF_NAME} diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 3e634de4f0c..e06a6fb0cff 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -29,7 +29,7 @@ Set the title to: `Description of the original issue` #### 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 - [ ] 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) diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 110e589e30d..d58cb0f9e2b 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -14,7 +14,7 @@ class CreateBranchService < BaseService if new_branch success(new_branch) else - error('Invalid reference name') + error("Invalid reference name: #{branch_name}") end rescue Gitlab::Git::PreReceiveError => ex error(ex.message) diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb new file mode 100644 index 00000000000..f302a786ba8 --- /dev/null +++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb @@ -0,0 +1,157 @@ +# 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\/(?\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 diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml index 55aea0296e7..3d77a439d61 100644 --- a/app/views/admin/sessions/_new_base.html.haml +++ b/app/views/admin/sessions/_new_base.html.haml @@ -4,4 +4,4 @@ = 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_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' } diff --git a/app/views/admin/sessions/_tabs_normal.html.haml b/app/views/admin/sessions/_tabs_normal.html.haml index f5dedb5ad76..20830051d31 100644 --- a/app/views/admin/sessions/_tabs_normal.html.haml +++ b/app/views/admin/sessions/_tabs_normal.html.haml @@ -1,3 +1,3 @@ %ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' } %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') diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml index ee06b4a1741..73028e78ea5 100644 --- a/app/views/admin/sessions/new.html.haml +++ b/app/views/admin/sessions/new.html.haml @@ -1,5 +1,5 @@ - @hide_breadcrumbs = true -- page_title _('Enter admin mode') +- page_title _('Enter Admin Mode') .row.justify-content-center .col-6.new-session-forms-container diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5122c2517aa..d339751848b 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -55,15 +55,15 @@ = nav_link(controller: 'admin/dashboard') do = link_to admin_root_path, class: 'admin-icon qa-admin-area-link d-xl-none' do = _('Admin Area') - - if Feature.enabled?(:user_mode_in_session) - - if header_link?(:admin_mode) - = nav_link(controller: 'admin/sessions') do - = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do - = _('Leave admin mode') - - elsif current_user.admin? - = nav_link(controller: 'admin/sessions') do - = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do - = _('Enter admin mode') + - if Feature.enabled?(:user_mode_in_session) + - if header_link?(:admin_mode) + = nav_link(controller: 'admin/sessions') do + = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do + = _('Leave Admin Mode') + - elsif current_user.admin? + = nav_link(controller: 'admin/sessions') do + = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do + = _('Enter Admin Mode') - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, class: 'admin-icon' do @@ -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 = 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 - if dashboard_nav_link?(:projects) diff --git a/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml new file mode 100644 index 00000000000..546e6bc6b63 --- /dev/null +++ b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml @@ -0,0 +1,5 @@ +--- +title: Enable the color chip in AsciiDoc documents +merge_request: 18723 +author: +type: added diff --git a/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml new file mode 100644 index 00000000000..b727bc7f85e --- /dev/null +++ b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Error when uploading a few designs in a row +merge_request: 18811 +author: +type: fixed diff --git a/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml new file mode 100644 index 00000000000..4014b5e7ab2 --- /dev/null +++ b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml @@ -0,0 +1,5 @@ +--- +title: Fix missing admin mode UI buttons on bigger screen sizes +merge_request: 18585 +author: Diego Louzán +type: fixed diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb index 82b99d3de4a..2e8d2bd23b0 100644 --- a/lib/banzai/pipeline/ascii_doc_pipeline.rb +++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb @@ -10,6 +10,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::ExternalLinkFilter, Filter::PlantumlFilter, + Filter::ColorFilter, Filter::AsciiDocPostProcessingFilter ] end diff --git a/lib/gitlab/metrics/dashboard/errors.rb b/lib/gitlab/metrics/dashboard/errors.rb index d41bd2c43c7..264ea0488e7 100644 --- a/lib/gitlab/metrics/dashboard/errors.rb +++ b/lib/gitlab/metrics/dashboard/errors.rb @@ -9,6 +9,7 @@ module Gitlab module Errors DashboardProcessingError = Class.new(StandardError) PanelNotFoundError = Class.new(StandardError) + MissingIntegrationError = Class.new(StandardError) LayoutError = Class.new(DashboardProcessingError) MissingQueryError = Class.new(DashboardProcessingError) @@ -22,6 +23,10 @@ module Gitlab error("#{dashboard_path} could not be found.", :not_found) when PanelNotFoundError 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 raise error end diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb index bfdee76a818..9566e5afb9a 100644 --- a/lib/gitlab/metrics/dashboard/processor.rb +++ b/lib/gitlab/metrics/dashboard/processor.rb @@ -17,7 +17,10 @@ module Gitlab # Returns a new dashboard hash with the results of # running transforms on the dashboard. + # @return [Hash, nil] def process + return unless @dashboard + @dashboard.deep_symbolize_keys.tap do |dashboard| @sequence.each do |stage| stage.new(@project, dashboard, @params).transform! diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb new file mode 100644 index 00000000000..ce75c54d014 --- /dev/null +++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb @@ -0,0 +1,224 @@ +# 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 diff --git a/lib/grafana/client.rb b/lib/grafana/client.rb index 0765630f9bb..b419f79bace 100644 --- a/lib/grafana/client.rb +++ b/lib/grafana/client.rb @@ -11,6 +11,18 @@ module Grafana @token = token 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 proxy_path [String] Path to proxy - ex) 'api/v1/query_range' def proxy_datasource(datasource_id:, proxy_path:, query: {}) @@ -57,7 +69,7 @@ module Grafana def handle_response(response) 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 def raise_error(message) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 74d52dad839..e3860825b73 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6099,15 +6099,15 @@ msgstr "" msgid "Ensure your %{linkStart}environment is part of the deploy stage%{linkEnd} of your CI pipeline to track deployments to your cluster." msgstr "" +msgid "Enter Admin Mode" +msgstr "" + msgid "Enter IP address range" msgstr "" msgid "Enter a number" msgstr "" -msgid "Enter admin mode" -msgstr "" - msgid "Enter at least three characters to search" msgstr "" @@ -9680,7 +9680,7 @@ msgstr "" msgid "Leave" msgstr "" -msgid "Leave admin mode" +msgid "Leave Admin Mode" msgstr "" msgid "Leave edit mode? All unsaved changes will be lost." diff --git a/spec/factories/grafana_integrations.rb b/spec/factories/grafana_integrations.rb index c19417f5a90..4eb3bee8b28 100644 --- a/spec/factories/grafana_integrations.rb +++ b/spec/factories/grafana_integrations.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :grafana_integration, class: GrafanaIntegration do project - grafana_url { 'https://grafana.com' } + grafana_url { 'https://grafana.example.com' } token { SecureRandom.hex(10) } end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index e1c9364067a..99a6165cfc9 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do include StubENV include TermsHelper + include MobileHelpers let(:admin) { create(:admin) } @@ -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) 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 context 'when in admin_mode' do @@ -462,7 +489,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc it 'can leave admin mode' do page.within('.navbar-sub-nav') do # 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) end @@ -481,7 +508,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc before do page.within('.navbar-sub-nav') do # 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 diff --git a/spec/fixtures/grafana/dashboard_response.json b/spec/fixtures/grafana/dashboard_response.json new file mode 100644 index 00000000000..4743ec39b44 --- /dev/null +++ b/spec/fixtures/grafana/dashboard_response.json @@ -0,0 +1,764 @@ +{ + "meta": { + "type": "db", + "canSave": true, + "canEdit": true, + "canAdmin": true, + "canStar": true, + "slug": "gitlab-omnibus-redis", + "url": "/-/grafana/d/XDaNK6amz/gitlab-omnibus-redis", + "expires": "0001-01-01T00:00:00Z", + "created": "2019-10-04T13:43:20Z", + "updated": "2019-10-04T13:43:20Z", + "updatedBy": "Anonymous", + "createdBy": "Anonymous", + "version": 1, + "hasAcl": false, + "isFolder": false, + "folderId": 1, + "folderTitle": "GitLab Omnibus", + "folderUrl": "/-/grafana/dashboards/f/l2EpNh2Zk/gitlab-omnibus", + "provisioned": true, + "provisionedExternalId": "redis.json" + }, + "dashboard": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "description": "GitLab Omnibus dashboard for Redis servers", + "editable": true, + "gnetId": 763, + "graphTooltip": 0, + "id": 3, + "iteration": 1556027798221, + "links": [], + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "GitLab Omnibus", + "decimals": 0, + "editable": true, + "error": false, + "format": "dtdurations", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { "h": 3, "w": 4, "x": 0, "y": 0 }, + "id": 9, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { "name": "value to text", "value": 1 }, + { "name": "range to text", "value": 2 } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "addr", + "targets": [ + { + "expr": "avg(time() - redis_start_time_seconds{instance=~\"$instance\"})", + "format": "time_series", + "instant": true, + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 1800 + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "GitLab Omnibus", + "decimals": 0, + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { "h": 3, "w": 4, "x": 4, "y": 0 }, + "hideTimeOverride": true, + "id": 12, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { "name": "value to text", "value": 1 }, + { "name": "range to text", "value": 2 } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(\n avg_over_time(redis_connected_clients{instance=~\"$instance\"}[$__interval])\n)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "1m", + "timeShift": null, + "title": "Clients", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }], + "valueName": "avg" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "editable": true, + "error": false, + "fill": 1, + "grid": {}, + "gridPos": { "h": 6, "w": 8, "x": 8, "y": 0 }, + "id": 2, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(\n rate(redis_commands_processed_total{instance=~\"$instance\"}[$__interval])\n)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "", + "metric": "A", + "refId": "A", + "step": 240, + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Commands Executed", + "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "reqps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "grid": {}, + "gridPos": { "h": 6, "w": 8, "x": 16, "y": 0 }, + "id": 1, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(\n rate(redis_keyspace_hits_total{instance=~\"$instance\"}[$__interval])\n)", + "format": "time_series", + "hide": false, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "hits", + "metric": "", + "refId": "A", + "step": 240, + "target": "" + }, + { + "expr": "sum(\n rate(redis_keyspace_misses_total{instance=~\"$instance\"}[$__interval])\n)", + "format": "time_series", + "hide": false, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "misses", + "metric": "", + "refId": "B", + "step": 240, + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Hits, Misses per Second", + "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + }, + { + "aliasColors": { "max": "#BF1B00" }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "editable": true, + "error": false, + "fill": 1, + "grid": {}, + "gridPos": { "h": 10, "w": 8, "x": 0, "y": 3 }, + "id": 7, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [{ "alias": "/max - .*/", "dashes": true }], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "redis_memory_used_bytes{instance=~\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used - {{instance}}", + "metric": "", + "refId": "A", + "step": 240, + "target": "" + }, + { + "expr": "redis_config_maxmemory{instance=~\"$instance\"} \u003e 0", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "max - {{instance}}", + "refId": "B", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Memory Usage", + "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + }, + { + "aliasColors": { + "evicts": "#890F02", + "memcached_items_evicted_total{instance=\"172.17.0.1:9150\",job=\"prometheus\"}": "#890F02", + "reclaims": "#3F6833" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "editable": true, + "error": false, + "fill": 1, + "grid": {}, + "gridPos": { "h": 7, "w": 8, "x": 8, "y": 6 }, + "id": 8, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [{ "alias": "reclaims", "yaxis": 2 }], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(redis_expired_keys_total{instance=~\"$instance\"}[$__interval]))", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "expired", + "metric": "", + "refId": "A", + "step": 240, + "target": "" + }, + { + "expr": "sum(rate(redis_evicted_keys_total{instance=~\"$instance\"}[$__interval]))", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "evicted", + "refId": "B", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Expired / Evicted", + "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "editable": true, + "error": false, + "fill": 1, + "grid": {}, + "gridPos": { "h": 7, "w": 8, "x": 16, "y": 6 }, + "id": 10, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(\n rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "In", + "refId": "A", + "step": 240 + }, + { + "expr": "sum(\n rate(redis_net_output_bytes_total{instance=~\"$instance\"}[$__interval])\n)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "Out", + "refId": "B", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network I/O", + "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "Bps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "editable": true, + "error": false, + "fill": 8, + "grid": {}, + "gridPos": { "h": 7, "w": 16, "x": 0, "y": 13 }, + "id": 14, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum without (instance) (\n rate(redis_commands_total{instance=~\"$instance\"}[$__interval])\n) \u003e 0", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{ cmd }}", + "metric": "redis_command_calls_total", + "refId": "A", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Command Calls / sec", + "tooltip": { "msResolution": true, "shared": true, "sort": 2, "value_type": "individual" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "editable": true, + "error": false, + "fill": 7, + "grid": {}, + "gridPos": { "h": 7, "w": 8, "x": 16, "y": 13 }, + "id": 13, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(redis_db_keys{instance=~\"$instance\"} - redis_db_keys_expiring{instance=~\"$instance\"}) ", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "not expiring", + "refId": "A", + "step": 240, + "target": "" + }, + { + "expr": "sum(redis_db_keys_expiring{instance=~\"$instance\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "expiring", + "metric": "", + "refId": "B", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Expiring vs Not-Expiring Keys", + "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "GitLab Omnibus", + "editable": true, + "error": false, + "fill": 7, + "grid": {}, + "gridPos": { "h": 7, "w": 16, "x": 0, "y": 20 }, + "id": 5, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (db) (\n redis_db_keys{instance=~\"$instance\"}\n)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{ db }} ", + "refId": "A", + "step": 240, + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Items per DB", + "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, + "type": "graph", + "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, + "yaxes": [ + { "format": "none", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, + { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } + ], + "yaxis": { "align": false, "alignLevel": null } + } + ], + "refresh": "1m", + "schemaVersion": 18, + "style": "dark", + "tags": ["redis"], + "templating": { + "list": [ + { + "allValue": null, + "current": { "tags": [], "text": "All", "value": "$__all" }, + "datasource": "GitLab Omnibus", + "definition": "", + "hide": 0, + "includeAll": true, + "label": null, + "multi": false, + "name": "instance", + "options": [], + "query": "label_values(up{job=\"redis\"}, instance)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { "from": "now-24h", "to": "now" }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "", + "title": "GitLab Omnibus - Redis", + "uid": "XDaNK6amz", + "version": 1 + } +} diff --git a/spec/fixtures/grafana/datasource_response.json b/spec/fixtures/grafana/datasource_response.json new file mode 100644 index 00000000000..07c075beb35 --- /dev/null +++ b/spec/fixtures/grafana/datasource_response.json @@ -0,0 +1,21 @@ +{ + "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 +} diff --git a/spec/fixtures/grafana/expected_grafana_embed.json b/spec/fixtures/grafana/expected_grafana_embed.json new file mode 100644 index 00000000000..72fb5477b9e --- /dev/null +++ b/spec/fixtures/grafana/expected_grafana_embed.json @@ -0,0 +1,27 @@ +{ + "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" + } + ] + } + ] + } + ] +} diff --git a/spec/fixtures/grafana/simplified_dashboard_response.json b/spec/fixtures/grafana/simplified_dashboard_response.json new file mode 100644 index 00000000000..b450fda082b --- /dev/null +++ b/spec/fixtures/grafana/simplified_dashboard_response.json @@ -0,0 +1,40 @@ +{ + "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" + } + ] + } + } +} diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json index 9c1be32645a..ac40f2dcd13 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json @@ -1,7 +1,6 @@ { "type": "object", "required": [ - "unit", "label", "prometheus_endpoint_path" ], diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json index 1548daacd64..a16f1ef592f 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json @@ -3,7 +3,6 @@ "required": [ "title", "y_label", - "weight", "metrics" ], "properties": { diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb index e2ce1869810..4fa136bc405 100644 --- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb @@ -25,6 +25,14 @@ describe Gitlab::Metrics::Dashboard::Processor do 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 let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') } diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb new file mode 100644 index 00000000000..5c2ec6dae6b --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb @@ -0,0 +1,106 @@ +# 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 diff --git a/spec/lib/grafana/client_spec.rb b/spec/lib/grafana/client_spec.rb index bd93a3c59a2..699344e940e 100644 --- a/spec/lib/grafana/client_spec.rb +++ b/spec/lib/grafana/client_spec.rb @@ -35,7 +35,7 @@ describe Grafana::Client do it 'does not follow redirects' do expect { subject }.to raise_exception( Grafana::Client::Error, - 'Grafana response status code: 302' + 'Grafana response status code: 302, Message: {}' ) expect(redirect_req_stub).to have_been_requested @@ -67,6 +67,30 @@ describe Grafana::Client do 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 let(:grafana_api_url) do 'https://grafanatest.com/-/grafana-project/' \ diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index f9c8b42afa8..d1e20cb1770 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -602,7 +602,7 @@ describe API::Branches do post api(route, user), params: { branch: 'new_design3', ref: 'foo' } 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 diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb index 0d34c7f9a82..9661173c9e7 100644 --- a/spec/services/create_branch_service_spec.rb +++ b/spec/services/create_branch_service_spec.rb @@ -22,5 +22,20 @@ describe CreateBranchService do expect(project.repository.branch_exists?('my-feature')).to be_truthy 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 diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb new file mode 100644 index 00000000000..f200c636aac --- /dev/null +++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb @@ -0,0 +1,177 @@ +# 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 diff --git a/spec/support/helpers/grafana_api_helpers.rb b/spec/support/helpers/grafana_api_helpers.rb new file mode 100644 index 00000000000..b212cbf2943 --- /dev/null +++ b/spec/support/helpers/grafana_api_helpers.rb @@ -0,0 +1,32 @@ +# 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 diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 7d5896e4eeb..1d42f26ad3e 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -53,7 +53,7 @@ module LoginHelpers fill_in 'password', with: user.password - click_button 'Enter admin mode' + click_button 'Enter Admin Mode' end def gitlab_sign_in_via(provider, user, uid, saml_response = nil)