Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/metrics/dashboard/validator')
-rw-r--r--lib/gitlab/metrics/dashboard/validator/client.rb56
-rw-r--r--lib/gitlab/metrics/dashboard/validator/custom_formats.rb23
-rw-r--r--lib/gitlab/metrics/dashboard/validator/errors.rb60
-rw-r--r--lib/gitlab/metrics/dashboard/validator/post_schema_validator.rb52
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/axis.json14
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json18
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/link.json12
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/metric.json16
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/panel.json24
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/panel_group.json12
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/templating.json7
11 files changed, 294 insertions, 0 deletions
diff --git a/lib/gitlab/metrics/dashboard/validator/client.rb b/lib/gitlab/metrics/dashboard/validator/client.rb
new file mode 100644
index 00000000000..c63415abcfc
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/client.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Validator
+ class Client
+ # @param content [Hash] Representing a raw, unprocessed
+ # dashboard object
+ # @param schema_path [String] Representing path to dashboard schema file
+ # @param dashboard_path[String] Representing path to dashboard content file
+ # @param project [Project] Project to validate dashboard against
+ def initialize(content, schema_path, dashboard_path: nil, project: nil)
+ @content = content
+ @schema_path = schema_path
+ @dashboard_path = dashboard_path
+ @project = project
+ end
+
+ def execute
+ errors = validate_against_schema
+ errors += post_schema_validator.validate
+
+ errors.compact
+ end
+
+ private
+
+ attr_reader :content, :schema_path, :project, :dashboard_path
+
+ def custom_formats
+ @custom_formats ||= CustomFormats.new
+ end
+
+ def post_schema_validator
+ PostSchemaValidator.new(
+ project: project,
+ metric_ids: custom_formats.metric_ids_cache,
+ dashboard_path: dashboard_path
+ )
+ end
+
+ def schemer
+ @schemer ||= ::JSONSchemer.schema(Pathname.new(schema_path), formats: custom_formats.format_handlers)
+ end
+
+ def validate_against_schema
+ schemer.validate(content).map do |error|
+ Errors::SchemaValidationError.new(error)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/validator/custom_formats.rb b/lib/gitlab/metrics/dashboard/validator/custom_formats.rb
new file mode 100644
index 00000000000..485e80ad1b7
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/custom_formats.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Validator
+ class CustomFormats
+ def format_handlers
+ # Key is custom JSON Schema format name. Value is a proc that takes data and schema and handles
+ # validations.
+ @format_handlers ||= {
+ "add_to_metric_id_cache" => ->(data, schema) { metric_ids_cache << data }
+ }
+ end
+
+ def metric_ids_cache
+ @metric_ids_cache ||= []
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/validator/errors.rb b/lib/gitlab/metrics/dashboard/validator/errors.rb
new file mode 100644
index 00000000000..0f6e687d291
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/errors.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Validator
+ module Errors
+ InvalidDashboardError = Class.new(StandardError)
+
+ class SchemaValidationError < InvalidDashboardError
+ def initialize(error = {})
+ super(error_message(error))
+ end
+
+ private
+
+ def error_message(error)
+ if error.is_a?(Hash) && error.present?
+ pretty(error)
+ else
+ "Dashboard failed schema validation"
+ end
+ end
+
+ # based on https://github.com/davishmcclurg/json_schemer/blob/master/lib/json_schemer/errors.rb
+ # with addition ability to translate error messages
+ def pretty(error)
+ data, data_pointer, type, schema = error.values_at('data', 'data_pointer', 'type', 'schema')
+ location = data_pointer.empty? ? 'root' : data_pointer
+
+ case type
+ when 'required'
+ keys = error.fetch('details').fetch('missing_keys').join(', ')
+ _("%{location} is missing required keys: %{keys}") % { location: location, keys: keys }
+ when 'null', 'string', 'boolean', 'integer', 'number', 'array', 'object'
+ _("'%{data}' at %{location} is not of type: %{type}") % { data: data, location: location, type: type }
+ when 'pattern'
+ _("'%{data}' at %{location} does not match pattern: %{pattern}") % { data: data, location: location, pattern: schema.fetch('pattern') }
+ when 'format'
+ _("'%{data}' at %{location} does not match format: %{format}") % { data: data, location: location, format: schema.fetch('format') }
+ when 'const'
+ _("'%{data}' at %{location} is not: %{const}") % { data: data, location: location, const: schema.fetch('const').inspect }
+ when 'enum'
+ _("'%{data}' at %{location} is not one of: %{enum}") % { data: data, location: location, enum: schema.fetch('enum') }
+ else
+ _("'%{data}' at %{location} is invalid: error_type=%{type}") % { data: data, location: location, type: type }
+ end
+ end
+ end
+
+ class DuplicateMetricIds < InvalidDashboardError
+ def initialize
+ super(_("metric_id must be unique across a project"))
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/validator/post_schema_validator.rb b/lib/gitlab/metrics/dashboard/validator/post_schema_validator.rb
new file mode 100644
index 00000000000..73bfc5a6294
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/post_schema_validator.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Validator
+ class PostSchemaValidator
+ def initialize(metric_ids:, project: nil, dashboard_path: nil)
+ @metric_ids = metric_ids
+ @project = project
+ @dashboard_path = dashboard_path
+ end
+
+ def validate
+ errors = []
+ errors << uniq_metric_ids
+ errors.compact
+ end
+
+ private
+
+ attr_reader :project, :metric_ids, :dashboard_path
+
+ def uniq_metric_ids
+ return Validator::Errors::DuplicateMetricIds.new if metric_ids.uniq!
+
+ uniq_metric_ids_across_project if project.present? || dashboard_path.present?
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def uniq_metric_ids_across_project
+ return ArgumentError.new(_('Both project and dashboard_path are required')) unless
+ dashboard_path.present? && project.present?
+
+ # If PrometheusMetric identifier is not unique across project and dashboard_path,
+ # we need to error because we don't know if the user is trying to create a new metric
+ # or update an existing one.
+ identifier_on_other_dashboard = PrometheusMetric.where(
+ project: project,
+ identifier: metric_ids
+ ).where.not(
+ dashboard_path: dashboard_path
+ ).exists?
+
+ Validator::Errors::DuplicateMetricIds.new if identifier_on_other_dashboard
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/axis.json b/lib/gitlab/metrics/dashboard/validator/schemas/axis.json
new file mode 100644
index 00000000000..54334022426
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/axis.json
@@ -0,0 +1,14 @@
+{
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "format": {
+ "type": "string",
+ "default": "engineering"
+ },
+ "precision": {
+ "type": "number",
+ "default": 2
+ }
+ }
+}
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json b/lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json
new file mode 100644
index 00000000000..313f03be7dc
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json
@@ -0,0 +1,18 @@
+{
+ "type": "object",
+ "required": ["dashboard", "panel_groups"],
+ "properties": {
+ "dashboard": { "type": "string" },
+ "panel_groups": {
+ "type": "array",
+ "items": { "$ref": "./panel_group.json" }
+ },
+ "templating": {
+ "$ref": "./templating.json"
+ },
+ "links": {
+ "type": "array",
+ "items": { "$ref": "./link.json" }
+ }
+ }
+}
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/link.json b/lib/gitlab/metrics/dashboard/validator/schemas/link.json
new file mode 100644
index 00000000000..4ea7b5dd324
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/link.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required": ["url"],
+ "properties": {
+ "url": { "type": "string" },
+ "title": { "type": "string" },
+ "type": {
+ "type": "string",
+ "enum": ["grafana"]
+ }
+ }
+}
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/metric.json b/lib/gitlab/metrics/dashboard/validator/schemas/metric.json
new file mode 100644
index 00000000000..13831b77e3e
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/metric.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "required": ["unit"],
+ "oneOf": [{ "required": ["query"] }, { "required": ["query_range"] }],
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "add_to_metric_id_cache"
+ },
+ "unit": { "type": "string" },
+ "label": { "type": "string" },
+ "query": { "type": ["string", "number"] },
+ "query_range": { "type": ["string", "number"] },
+ "step": { "type": "number" }
+ }
+}
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/panel.json b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json
new file mode 100644
index 00000000000..011eef53e40
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": ["title", "metrics"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["area-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap"],
+ "default": "area-chart"
+ },
+ "title": { "type": "string" },
+ "y_label": { "type": "string" },
+ "y_axis": { "$ref": "./axis.json" },
+ "max_value": { "type": "number" },
+ "weight": { "type": "number" },
+ "metrics": {
+ "type": "array",
+ "items": { "$ref": "./metric.json" }
+ },
+ "links": {
+ "type": "array",
+ "items": { "$ref": "./link.json" }
+ }
+ }
+}
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/panel_group.json b/lib/gitlab/metrics/dashboard/validator/schemas/panel_group.json
new file mode 100644
index 00000000000..1306fc475db
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/panel_group.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required": ["group", "panels"],
+ "properties": {
+ "group": { "type": "string" },
+ "priority": { "type": "number" },
+ "panels": {
+ "type": "array",
+ "items": { "$ref": "./panel.json" }
+ }
+ }
+}
diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/templating.json b/lib/gitlab/metrics/dashboard/validator/schemas/templating.json
new file mode 100644
index 00000000000..6f8664c89af
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/validator/schemas/templating.json
@@ -0,0 +1,7 @@
+{
+ "type": "object",
+ "required": ["variables"],
+ "properties": {
+ "variables": { "type": "object" }
+ }
+}