Commit 476ee015 authored by Mikolaj Wawrzyniak's avatar Mikolaj Wawrzyniak

Expand dashboard schema validation to full list

In order to provide user with complete information we need to expand
dashboard validation to return exhaustive list of errors
parent 498292b1
...@@ -14,7 +14,8 @@ module Resolvers ...@@ -14,7 +14,8 @@ module Resolvers
def resolve(**args) def resolve(**args)
return unless environment return unless environment
::PerformanceMonitoring::PrometheusDashboard.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment }) ::PerformanceMonitoring::PrometheusDashboard
.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
end end
end end
end end
......
...@@ -16,6 +16,13 @@ module Types ...@@ -16,6 +16,13 @@ module Types
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true, field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
description: 'Annotations added to the dashboard', description: 'Annotations added to the dashboard',
resolver: Resolvers::Metrics::Dashboards::AnnotationResolver resolver: Resolvers::Metrics::Dashboards::AnnotationResolver
# In order to maintain backward compatibility we need to return NULL when there are no warnings
# and dashboard validation returns an empty array when there are no issues.
def schema_validation_warnings
warnings = object.schema_validation_warnings
warnings unless warnings.empty?
end
end end
# rubocop: enable Graphql/AuthorizeTypes # rubocop: enable Graphql/AuthorizeTypes
end end
......
...@@ -25,20 +25,30 @@ module BlobViewer ...@@ -25,20 +25,30 @@ module BlobViewer
private private
def parse_blob_data def parse_blob_data
yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw! if Feature.enabled?(:metrics_dashboard_exhaustive_validations, project)
exhaustive_metrics_dashboard_validation
else
old_metrics_dashboard_validation
end
end
def old_metrics_dashboard_validation
yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
::PerformanceMonitoring::PrometheusDashboard.from_json(yaml) ::PerformanceMonitoring::PrometheusDashboard.from_json(yaml)
nil []
rescue Gitlab::Config::Loader::FormatError => error rescue Gitlab::Config::Loader::FormatError => error
wrap_yml_syntax_error(error) ["YAML syntax: #{error.message}"]
rescue ActiveModel::ValidationError => invalid rescue ActiveModel::ValidationError => invalid
invalid.model.errors invalid.model.errors.messages.map { |messages| messages.join(': ') }
end end
def wrap_yml_syntax_error(error) def exhaustive_metrics_dashboard_validation
::PerformanceMonitoring::PrometheusDashboard.new.errors.tap do |errors| yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
errors.add(:'YAML syntax', error.message) Gitlab::Metrics::Dashboard::Validator
end .errors(yaml, dashboard_path: blob.path, project: project)
.map(&:message)
rescue Gitlab::Config::Loader::FormatError => error
[error.message]
end end
end end
end end
...@@ -53,14 +53,23 @@ module PerformanceMonitoring ...@@ -53,14 +53,23 @@ module PerformanceMonitoring
# This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398 # This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398
# implementation. For new existing logic was reused to faster deliver MVC # implementation. For new existing logic was reused to faster deliver MVC
def schema_validation_warnings def schema_validation_warnings
return run_custom_validation.map(&:message) if Feature.enabled?(:metrics_dashboard_exhaustive_validations, environment&.project)
self.class.from_json(reload_schema) self.class.from_json(reload_schema)
nil []
rescue Gitlab::Metrics::Dashboard::Errors::LayoutError => error
[error.message]
rescue ActiveModel::ValidationError => exception rescue ActiveModel::ValidationError => exception
exception.model.errors.map { |attr, error| "#{attr}: #{error}" } exception.model.errors.map { |attr, error| "#{attr}: #{error}" }
end end
private private
def run_custom_validation
Gitlab::Metrics::Dashboard::Validator
.errors(reload_schema, dashboard_path: path, project: environment&.project)
end
# dashboard finder methods are somehow limited, #find includes checking if # dashboard finder methods are somehow limited, #find includes checking if
# user is authorised to view selected dashboard, but modifies schema, which in some cases may # user is authorised to view selected dashboard, but modifies schema, which in some cases may
# cause false positives returned from validation, and #find_raw does not authorise users # cause false positives returned from validation, and #find_raw does not authorise users
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= icon('warning fw') = icon('warning fw')
= _('Metrics Dashboard YAML definition is invalid:') = _('Metrics Dashboard YAML definition is invalid:')
%ul %ul
- viewer.errors.messages.each do |error| - viewer.errors.each do |error|
%li= error.join(': ') %li= error
= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md') = link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md')
---
name: metrics_dashboard_exhaustive_validations
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40103
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241697
group: group::apm
type: development
default_enabled: false
\ No newline at end of file
...@@ -4,24 +4,22 @@ module Gitlab ...@@ -4,24 +4,22 @@ module Gitlab
module Metrics module Metrics
module Dashboard module Dashboard
module Validator module Validator
DASHBOARD_SCHEMA_PATH = 'lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json'.freeze DASHBOARD_SCHEMA_PATH = Rails.root.join(*%w[lib gitlab metrics dashboard validator schemas dashboard.json]).freeze
class << self class << self
def validate(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) def validate(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project) errors(content, schema_path, dashboard_path: dashboard_path, project: project).empty?
errors.empty?
end end
def validate!(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) def validate!(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project) errors = errors(content, schema_path, dashboard_path: dashboard_path, project: project)
errors.empty? || raise(errors.first) errors.empty? || raise(errors.first)
end end
private def errors(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil)
Validator::Client
def _validate(content, schema_path, dashboard_path: nil, project: nil) .new(content, schema_path, dashboard_path: dashboard_path, project: project)
client = Validator::Client.new(content, schema_path, dashboard_path: dashboard_path, project: project) .execute
client.execute
end end
end end
end end
......
...@@ -46,7 +46,7 @@ module Gitlab ...@@ -46,7 +46,7 @@ module Gitlab
def validate_against_schema def validate_against_schema
schemer.validate(content).map do |error| schemer.validate(content).map do |error|
Errors::SchemaValidationError.new(error) ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new(error)
end end
end end
end end
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"enum": ["area-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap"], "enum": ["area-chart", "line-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap", "gauge"],
"default": "area-chart" "default": "area-chart"
}, },
"title": { "type": "string" }, "title": { "type": "string" },
......
...@@ -564,35 +564,76 @@ RSpec.describe 'File blob', :js do ...@@ -564,35 +564,76 @@ RSpec.describe 'File blob', :js do
file_path: '.gitlab/dashboards/custom-dashboard.yml', file_path: '.gitlab/dashboards/custom-dashboard.yml',
file_content: file_content file_content: file_content
).execute ).execute
visit_blob('.gitlab/dashboards/custom-dashboard.yml')
end end
context 'valid dashboard file' do context 'with metrics_dashboard_exhaustive_validations feature flag off' do
let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
visit_blob('.gitlab/dashboards/custom-dashboard.yml')
end
it 'displays an auxiliary viewer' do context 'valid dashboard file' do
aggregate_failures do let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
# shows that dashboard yaml is valid
expect(page).to have_content('Metrics Dashboard YAML definition is valid.') it 'displays an auxiliary viewer' do
aggregate_failures do
# shows that dashboard yaml is valid
expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
# shows a learn more link
expect(page).to have_link('Learn more')
end
end
end
context 'invalid dashboard file' do
let(:file_content) { "dashboard: 'invalid'" }
# shows a learn more link it 'displays an auxiliary viewer' do
expect(page).to have_link('Learn more') aggregate_failures do
# shows that dashboard yaml is invalid
expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
expect(page).to have_content("panel_groups: should be an array of panel_groups objects")
# shows a learn more link
expect(page).to have_link('Learn more')
end
end end
end end
end end
context 'invalid dashboard file' do context 'with metrics_dashboard_exhaustive_validations feature flag on' do
let(:file_content) { "dashboard: 'invalid'" } before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
visit_blob('.gitlab/dashboards/custom-dashboard.yml')
end
it 'displays an auxiliary viewer' do context 'valid dashboard file' do
aggregate_failures do let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
# shows that dashboard yaml is invalid
expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
expect(page).to have_content("panel_groups: should be an array of panel_groups objects")
# shows a learn more link it 'displays an auxiliary viewer' do
expect(page).to have_link('Learn more') aggregate_failures do
# shows that dashboard yaml is valid
expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
# shows a learn more link
expect(page).to have_link('Learn more')
end
end
end
context 'invalid dashboard file' do
let(:file_content) { "dashboard: 'invalid'" }
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows that dashboard yaml is invalid
expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
expect(page).to have_content("root is missing required keys: panel_groups")
# shows a learn more link
expect(page).to have_link('Learn more')
end
end end
end end
end end
......
dashboard: 'Test Dashboard'
panel_groups:
- group: Group B
panels:
- title: "Super Chart B"
type: "area-chart"
y_label: "y_label"
metrics:
- id: metric_b
query_range: 'query'
unit: unit
label: Legend Label
- group: Group A
...@@ -34,6 +34,17 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do ...@@ -34,6 +34,17 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
it { is_expected.to eq 'root is missing required keys: one' } it { is_expected.to eq 'root is missing required keys: one' }
end end
context 'when there is type mismatch' do
%w(null string boolean integer number array object).each do |expected_type|
context "on type: #{expected_type}" do
let(:type) { expected_type }
let(:details) { nil }
it { is_expected.to eq "'property_name' at root is not of type: #{expected_type}" }
end
end
end
end end
context 'for nested object' do context 'for nested object' do
...@@ -52,8 +63,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do ...@@ -52,8 +63,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
let(:type) { expected_type } let(:type) { expected_type }
let(:details) { nil } let(:details) { nil }
subject { described_class.new(error_hash).message }
it { is_expected.to eq "'property_name' at /nested_objects/0 is not of type: #{expected_type}" } it { is_expected.to eq "'property_name' at /nested_objects/0 is not of type: #{expected_type}" }
end end
end end
......
...@@ -143,4 +143,56 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do ...@@ -143,4 +143,56 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
end end
end end
end end
describe '#errors' do
context 'valid dashboard schema' do
it 'returns no errors' do
expect(described_class.errors(valid_dashboard)).to eq []
end
context 'with duplicate metric_ids' do
it 'returns errors' do
expect(described_class.errors(duplicate_id_dashboard)).to eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new]
end
end
context 'with dashboard_path and project' do
subject { described_class.errors(valid_dashboard, dashboard_path: 'test/path.yml', project: project) }
context 'with no conflicting metric identifiers in db' do
it { is_expected.to eq [] }
end
context 'with metric identifier present in current dashboard' do
before do
create(:prometheus_metric,
identifier: 'metric_a1',
dashboard_path: 'test/path.yml',
project: project
)
end
it { is_expected.to eq [] }
end
context 'with metric identifier present in another dashboard' do
before do
create(:prometheus_metric,
identifier: 'metric_a1',
dashboard_path: 'some/other/dashboard/path.yml',
project: project
)
end
it { is_expected.to eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new] }
end
end
end
context 'invalid dashboard schema' do
it 'returns collection of validation errors' do
expect(described_class.errors(invalid_dashboard)).to all be_kind_of(Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError)
end
end
end
end end
...@@ -9,119 +9,228 @@ RSpec.describe BlobViewer::MetricsDashboardYml do ...@@ -9,119 +9,228 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) } let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) }
let(:sha) { sample_commit.id } let(:sha) { sample_commit.id }
let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
subject(:viewer) { described_class.new(blob) } subject(:viewer) { described_class.new(blob) }
context 'when the definition is valid' do context 'with metrics_dashboard_exhaustive_validations feature flag on' do
let(:data) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
end
context 'when the definition is valid' do
before do
allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([])
end
describe '#valid?' do
it 'calls prepare! on the viewer' do
expect(viewer).to receive(:prepare!)
viewer.valid?
end
it 'processes dashboard yaml and returns true', :aggregate_failures do
yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
expect(loader).to receive(:load_raw!).and_call_original
end
expect(Gitlab::Metrics::Dashboard::Validator)
.to receive(:errors)
.with(yml, dashboard_path: '.gitlab/dashboards/custom-dashboard.yml', project: project)
.and_call_original
expect(viewer.valid?).to be true
end
end
describe '#errors' do
it 'returns empty array' do
expect(viewer.errors).to eq []
end
end
end
context 'when definition is invalid' do
let(:error) { ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new }
let(:data) do
<<~YAML
dashboard:
YAML
end
before do
allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([error])
end
describe '#valid?' do describe '#valid?' do
it 'calls prepare! on the viewer' do it 'returns false' do
allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) expect(viewer.valid?).to be false
end
end
expect(viewer).to receive(:prepare!) describe '#errors' do
it 'returns validation errors' do
expect(viewer.errors).to eq ["Dashboard failed schema validation"]
end
end
end
viewer.valid? context 'when YAML syntax is invalid' do
let(:data) do
<<~YAML
dashboard: 'empty metrics'
panel_groups:
- group: 'Group Title'
YAML
end end
it 'returns true', :aggregate_failures do describe '#valid?' do
yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw! it 'returns false' do
expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
expect(viewer.valid?).to be false
end
end
expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader| describe '#errors' do
expect(loader).to receive(:load_raw!).and_call_original it 'returns validation errors' do
expect(viewer.errors).to all be_kind_of String
end end
expect(PerformanceMonitoring::PrometheusDashboard)
.to receive(:from_json)
.with(yml)
.and_call_original
expect(viewer.valid?).to be_truthy
end end
end end
describe '#errors' do context 'when YAML loader raises error' do
it 'returns nil' do let(:data) do
allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) <<~YAML
large yaml file
YAML
end
before do
allow(::Gitlab::Config::Loader::Yaml).to receive(:new)
.and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
end
expect(viewer.errors).to be nil it 'is invalid' do
expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
expect(viewer.valid?).to be false
end
it 'returns validation errors' do
expect(viewer.errors).to eq ['The parsed YAML is too big']
end end
end end
end end
context 'when definition is invalid' do context 'with metrics_dashboard_exhaustive_validations feature flag off' do
let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) } before do
let(:data) do stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
<<~YAML
dashboard:
YAML
end end
describe '#valid?' do context 'when the definition is valid' do
it 'returns false' do describe '#valid?' do
expect(PerformanceMonitoring::PrometheusDashboard) before do
.to receive(:from_json).and_raise(error) allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
end
it 'calls prepare! on the viewer' do
expect(viewer).to receive(:prepare!)
expect(viewer.valid?).to be_falsey viewer.valid?
end
it 'processes dashboard yaml and returns true', :aggregate_failures do
yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
expect(loader).to receive(:load_raw!).and_call_original
end
expect(PerformanceMonitoring::PrometheusDashboard)
.to receive(:from_json)
.with(yml)
.and_call_original
expect(viewer.valid?).to be true
end
end
describe '#errors' do
it 'returns empty array' do
expect(viewer.errors).to eq []
end
end end
end end
describe '#errors' do context 'when definition is invalid' do
it 'returns validation errors' do let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) }
allow(PerformanceMonitoring::PrometheusDashboard) let(:data) do
.to receive(:from_json).and_raise(error) <<~YAML
dashboard:
YAML
end
describe '#valid?' do
it 'returns false' do
expect(PerformanceMonitoring::PrometheusDashboard)
.to receive(:from_json).and_raise(error)
expect(viewer.errors).to be error.model.errors expect(viewer.valid?).to be false
end
end
describe '#errors' do
it 'returns validation errors' do
allow(PerformanceMonitoring::PrometheusDashboard)
.to receive(:from_json).and_raise(error)
expect(viewer.errors).to eq error.model.errors.messages.map { |messages| messages.join(': ') }
end
end end
end end
end
context 'when YAML syntax is invalid' do context 'when YAML syntax is invalid' do
let(:data) do let(:data) do
<<~YAML <<~YAML
dashboard: 'empty metrics' dashboard: 'empty metrics'
panel_groups: panel_groups:
- group: 'Group Title' - group: 'Group Title'
YAML YAML
end
describe '#valid?' do
it 'returns false' do
expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
expect(viewer.valid?).to be_falsey
end end
end
describe '#errors' do describe '#valid?' do
it 'returns validation errors' do it 'returns false' do
yaml_wrapped_errors = { 'YAML syntax': ["(<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"] } expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
expect(viewer.valid?).to be false
end
end
expect(viewer.errors).to be_kind_of ActiveModel::Errors describe '#errors' do
expect(viewer.errors.messages).to eql(yaml_wrapped_errors) it 'returns validation errors' do
expect(viewer.errors).to eq ["YAML syntax: (<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"]
end
end end
end end
end
context 'when YAML loader raises error' do context 'when YAML loader raises error' do
let(:data) do let(:data) do
<<~YAML <<~YAML
large yaml file large yaml file
YAML YAML
end end
before do
allow(::Gitlab::Config::Loader::Yaml).to receive(:new)
.and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
end
it 'is invalid' do before do
expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) allow(::Gitlab::Config::Loader::Yaml).to(
expect(viewer.valid?).to be(false) receive(:new).and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
end )
end
it 'returns validation errors' do it 'is invalid' do
yaml_wrapped_errors = { 'YAML syntax': ["The parsed YAML is too big"] } expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
expect(viewer.valid?).to be false
end
expect(viewer.errors).to be_kind_of(ActiveModel::Errors) it 'returns validation errors' do
expect(viewer.errors.messages).to eq(yaml_wrapped_errors) expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"]
end
end end
end end
end end
...@@ -219,20 +219,93 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do ...@@ -219,20 +219,93 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
end end
describe '#schema_validation_warnings' do describe '#schema_validation_warnings' do
context 'when schema is valid' do let(:environment) { create(:environment, project: project) }
it 'returns nil' do let(:path) { '.gitlab/dashboards/test.yml' }
expect(described_class).to receive(:from_json) let(:project) { create(:project, :repository, :custom_repo, files: { path => dashboard_schema.to_yaml }) }
expect(described_class.new.schema_validation_warnings).to be_nil
subject(:schema_validation_warnings) { described_class.new(dashboard_schema.merge(path: path, environment: environment)).schema_validation_warnings }
before do
allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find_raw).with(project, dashboard_path: path).and_call_original
end
context 'metrics_dashboard_exhaustive_validations is on' do
before do
stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
end
context 'when schema is valid' do
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
it 'returns empty array' do
expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([])
expect(schema_validation_warnings).to eq []
end
end
context 'when schema is invalid' do
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
it 'returns array with errors messages' do
error = ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new
expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([error])
expect(schema_validation_warnings).to eq [error.message]
end
end
context 'when YAML has wrong syntax' do
let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
it 'returns array with errors messages' do
expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
expect(schema_validation_warnings).to eq ['Invalid yaml']
end
end end
end end
context 'when schema is invalid' do context 'metrics_dashboard_exhaustive_validations is off' do
it 'returns array with errors messages' do before do
instance = described_class.new stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
instance.errors.add(:test, 'test error') end
context 'when schema is valid' do
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
it 'returns empty array' do
expect(described_class).to receive(:from_json).with(dashboard_schema)
expect(schema_validation_warnings).to eq []
end
end
context 'when schema is invalid' do
let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
it 'returns array with errors messages' do
instance = described_class.new
instance.errors.add(:test, 'test error')
expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance))
expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
end
end
expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance)) context 'when YAML has wrong syntax' do
expect(described_class.new.schema_validation_warnings).to eq ['test: test error'] let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
it 'returns array with errors messages' do
expect(described_class).not_to receive(:from_json)
expect(schema_validation_warnings).to eq ['Invalid yaml']
end
end end
end end
end end
......
...@@ -7,7 +7,7 @@ RSpec.describe 'Getting Metrics Dashboard' do ...@@ -7,7 +7,7 @@ RSpec.describe 'Getting Metrics Dashboard' do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:environment) { create(:environment, project: project) } let(:environment) { create(:environment, project: project) }
let(:query) do let(:query) do
graphql_query_for( graphql_query_for(
...@@ -25,73 +25,156 @@ RSpec.describe 'Getting Metrics Dashboard' do ...@@ -25,73 +25,156 @@ RSpec.describe 'Getting Metrics Dashboard' do
) )
end end
context 'for anonymous user' do context 'with metrics_dashboard_exhaustive_validations feature flag off' do
before do before do
post_graphql(query, current_user: current_user) stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
end end
context 'requested dashboard is available' do context 'for anonymous user' do
let(:path) { 'config/prometheus/common_metrics.yml' } before do
post_graphql(query, current_user: current_user)
end
context 'requested dashboard is available' do
let(:path) { 'config/prometheus/common_metrics.yml' }
it_behaves_like 'a working graphql query'
it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')
expect(dashboard).to be_nil
end
end
end
context 'for user with developer access' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
context 'requested dashboard is available' do
let(:path) { 'config/prometheus/common_metrics.yml' }
it_behaves_like 'a working graphql query'
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
end
context 'invalid dashboard' do
let(:path) { '.gitlab/dashboards/metrics.yml' }
let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"])
end
end
context 'empty dashboard' do
let(:path) { '.gitlab/dashboards/metrics.yml' }
let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
it_behaves_like 'a working graphql query' expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"])
end
end
end
context 'requested dashboard can not be found' do
let(:path) { 'config/prometheus/i_am_not_here.yml' }
it 'returns nil' do it_behaves_like 'a working graphql query'
dashboard = graphql_data.dig('project', 'environments', 'nodes')
expect(dashboard).to be_nil it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to be_nil
end
end end
end end
end end
context 'for user with developer access' do context 'with metrics_dashboard_exhaustive_validations feature flag on' do
before do before do
project.add_developer(current_user) stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
post_graphql(query, current_user: current_user)
end end
context 'requested dashboard is available' do context 'for anonymous user' do
let(:path) { 'config/prometheus/common_metrics.yml' } before do
post_graphql(query, current_user: current_user)
end
context 'requested dashboard is available' do
let(:path) { 'config/prometheus/common_metrics.yml' }
it_behaves_like 'a working graphql query'
it_behaves_like 'a working graphql query' it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')
it 'returns metrics dashboard' do expect(dashboard).to be_nil
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard'] end
end
end
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) context 'for user with developer access' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end end
context 'invalid dashboard' do context 'requested dashboard is available' do
let(:path) { '.gitlab/dashboards/metrics.yml' } let(:path) { 'config/prometheus/common_metrics.yml' }
let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
it_behaves_like 'a working graphql query'
it 'returns metrics dashboard' do it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"]) expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
end end
end
context 'empty dashboard' do context 'invalid dashboard' do
let(:path) { '.gitlab/dashboards/metrics.yml' } let(:path) { '.gitlab/dashboards/metrics.yml' }
let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
it 'returns metrics dashboard' do it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"]) expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: panel_groups"])
end
end
context 'empty dashboard' do
let(:path) { '.gitlab/dashboards/metrics.yml' }
let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: dashboard, panel_groups"])
end
end end
end end
end
context 'requested dashboard can not be found' do context 'requested dashboard can not be found' do
let(:path) { 'config/prometheus/i_am_not_here.yml' } let(:path) { 'config/prometheus/i_am_not_here.yml' }
it_behaves_like 'a working graphql query' it_behaves_like 'a working graphql query'
it 'returns nil' do it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard'] dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
expect(dashboard).to be_nil expect(dashboard).to be_nil
end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment