Commit cd4f566a authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents a8368363 ce719f7a
......@@ -165,7 +165,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
project,
current_user,
environment,
embedded: params[:embedded]
dashboard_path: params[:dashboard],
**dashboard_params.to_h.symbolize_keys
)
elsif Feature.enabled?(:environment_metrics_show_multiple_dashboards, project)
result = dashboard_finder.find(
......@@ -233,6 +234,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
params.require([:start, :end])
end
def dashboard_params
params.permit(:embedded, :group, :title, :y_label)
end
def dashboard_finder
Gitlab::Metrics::Dashboard::Finder
end
......
......@@ -32,6 +32,10 @@ class PrometheusMetric < ApplicationRecord
Gitlab::Prometheus::Metric.new(id: id, title: title, required_metrics: required_metrics, weight: 0, y_label: y_label, queries: queries)
end
def to_metric_hash
queries.first.merge(metric_id: id)
end
def queries
[
{
......
......@@ -9,13 +9,17 @@ module PrometheusMetricEnums
aws_elb: -3,
nginx: -4,
kubernetes: -5,
nginx_ingress: -6,
nginx_ingress: -6
}.merge(custom_groups).freeze
end
# custom/user groups
# custom/user groups
def self.custom_groups
{
business: 0,
response: 1,
system: 2
}
}.freeze
end
def self.group_details
......@@ -50,16 +54,20 @@ module PrometheusMetricEnums
group_title: _('System metrics (Kubernetes)'),
required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
priority: 5
}.freeze,
}.freeze
}.merge(custom_group_details).freeze
end
# custom/user groups
# custom/user groups
def self.custom_group_details
{
business: {
group_title: _('Business metrics (Custom)'),
priority: 0
}.freeze,
response: {
group_title: _('Response metrics (Custom)'),
priority: -5
priority: -5
}.freeze,
system: {
group_title: _('System metrics (Custom)'),
......
# frozen_string_literal: true
# Base class for embed services. Contains a few basic helper
# methods that the embed services share.
module Metrics
module Dashboard
class BaseEmbedService < ::Metrics::Dashboard::BaseService
def cache_key
"dynamic_metrics_dashboard_#{identifiers}"
end
protected
def dashboard_path
params[:dashboard_path].presence ||
::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH
end
def group
params[:group]
end
def title
params[:title]
end
def y_label
params[:y_label]
end
def identifiers
[dashboard_path, group, title, y_label].join('|')
end
end
end
end
......@@ -5,17 +5,14 @@
module Metrics
module Dashboard
class BaseService < ::BaseService
PROCESSING_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardProcessingError
NOT_FOUND_ERROR = Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
include Gitlab::Metrics::Dashboard::Errors
def get_dashboard
return error('Insufficient permissions.', :unauthorized) unless allowed?
success(dashboard: process_dashboard)
rescue NOT_FOUND_ERROR
error("#{dashboard_path} could not be found.", :not_found)
rescue PROCESSING_ERROR => e
error(e.message, :unprocessable_entity)
rescue StandardError => e
handle_errors(e)
end
# Summary of all known dashboards for the service.
......
# frozen_string_literal: true
# Responsible for returning a dashboard containing specified
# custom metrics. Creates panels based on the matching metrics
# stored in the database.
#
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
module Metrics
module Dashboard
class CustomMetricEmbedService < ::Metrics::Dashboard::BaseEmbedService
extend ::Gitlab::Utils::Override
include Gitlab::Utils::StrongMemoize
include Gitlab::Metrics::Dashboard::Defaults
class << self
# Determines whether the provided params are sufficient
# to uniquely identify a panel composed of user-defined
# custom metrics from the DB.
def valid_params?(params)
[
params[:embedded],
valid_dashboard?(params[:dashboard_path]),
valid_group_title?(params[:group]),
params[:title].present?,
params.has_key?(:y_label)
].all?
end
private
# A group title is valid if it is one of the limited
# options the user can select in the UI.
def valid_group_title?(group)
PrometheusMetricEnums
.custom_group_details
.map { |_, details| details[:group_title] }
.include?(group)
end
# All custom metrics are displayed on the system dashboard.
# Nil is acceptable as we'll default to the system dashboard.
def valid_dashboard?(dashboard)
dashboard.nil? || SystemDashboardService.system_dashboard?(dashboard)
end
end
# Returns a new dashboard with only the matching
# metrics from the system dashboard, stripped of
# group info.
#
# Note: This overrides the method #raw_dashboard,
# which means the result will not be cached. This
# is because we are inserting DB info into the
# dashboard before post-processing. This ensures
# we aren't acting on deleted or out-of-date metrics.
#
# @return [Hash]
override :raw_dashboard
def raw_dashboard
panels_not_found!(identifiers) if panels.empty?
{ 'panel_groups' => [{ 'panels' => panels }] }
end
private
# Generated dashboard panels for each metric which
# matches the provided input.
# @return [Array<Hash>]
def panels
strong_memoize(:panels) do
metrics.map { |metric| panel_for_metric(metric) }
end
end
# Metrics which match the provided inputs.
# There may be multiple metrics, but they should be
# displayed in a single panel/chart.
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
# rubocop: disable CodeReuse/ActiveRecord
def metrics
project.prometheus_metrics.where(
group: group_key,
title: title,
y_label: y_label
)
end
# rubocop: enable CodeReuse/ActiveRecord
# Returns a symbol representing the group that
# the dashboard's group title belongs to.
# It will be one of the keys found under
# PrometheusMetricEnums.custom_groups.
#
# @return [String]
def group_key
strong_memoize(:group_key) do
PrometheusMetricEnums
.group_details
.find { |_, details| details[:group_title] == group }
.first
.to_s
end
end
# Returns a representation of a PromtheusMetric
# as a dashboard panel. As the panel is generated
# on the fly, we're using default values for info
# not represented in the DB.
#
# @return [Hash]
def panel_for_metric(metric)
{
type: DEFAULT_PANEL_TYPE,
weight: DEFAULT_PANEL_WEIGHT,
title: metric.title,
y_label: metric.y_label,
metrics: [metric.to_metric_hash]
}
end
end
end
end
# frozen_string_literal: true
# Responsible for returning a filtered system dashboard
# containing only the default embedded metrics. In future,
# this class may be updated to support filtering to
# alternate metrics/panels.
# containing only the default embedded metrics. This class
# operates by selecting metrics directly from the system
# dashboard.
#
# Why isn't this filtering in a processing stage? By filtering
# here, we ensure the dynamically-determined dashboard is cached.
......@@ -11,7 +11,7 @@
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
module Metrics
module Dashboard
class DefaultEmbedService < ::Metrics::Dashboard::BaseService
class DefaultEmbedService < ::Metrics::Dashboard::BaseEmbedService
# For the default filtering for embedded metrics,
# uses the 'id' key in dashboard-yml definition for
# identification.
......@@ -33,10 +33,6 @@ module Metrics
{ 'panel_groups' => [{ 'panels' => panels }] }
end
def cache_key
"dynamic_metrics_dashboard_#{metric_identifiers.join('_')}"
end
private
# Returns an array of the panels groups on the
......@@ -58,6 +54,10 @@ module Metrics
def metric_identifiers
DEFAULT_EMBEDDED_METRICS_IDENTIFIERS
end
def identifiers
metric_identifiers.join('|')
end
end
end
end
# frozen_string_literal: true
# Responsible for returning a filtered project dashboard
# containing only the request-provided metrics. The result
# is then cached for future requests. Metrics are identified
# based on a combination of identifiers for now, but the ideal
# would be similar to the approach in DefaultEmbedService, but
# a single unique identifier is not currently available across
# all metric types (custom, project-defined, cluster, or system).
#
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
module Metrics
module Dashboard
class DynamicEmbedService < ::Metrics::Dashboard::BaseEmbedService
include Gitlab::Utils::StrongMemoize
class << self
# Determines whether the provided params are sufficient
# to uniquely identify a panel from a yml-defined dashboard.
#
# See https://docs.gitlab.com/ee/user/project/integrations/prometheus.html#defining-custom-dashboards-per-project
# for additional info on defining custom dashboards.
def valid_params?(params)
[
params[:embedded],
params[:group].present?,
params[:title].present?,
params[:y_label]
].all?
end
end
# Returns a new dashboard with only the matching
# metrics from the system dashboard, stripped of groups.
# @return [Hash]
def get_raw_dashboard
not_found! if panels.empty?
{ 'panel_groups' => [{ 'panels' => panels }] }
end
private
def panels
strong_memoize(:panels) do
not_found! unless base_dashboard
not_found! unless groups = base_dashboard['panel_groups']
not_found! unless matching_group = find_group(groups)
not_found! unless all_panels = matching_group['panels']
find_panels(all_panels)
end
end
def base_dashboard
strong_memoize(:base_dashboard) do
Gitlab::Metrics::Dashboard::Finder.find_raw(project, dashboard_path: dashboard_path)
end
end
def find_group(groups)
groups.find do |candidate_group|
candidate_group['group'] == group
end
end
def find_panels(all_panels)
all_panels.select do |panel|
panel['title'] == title && panel['y_label'] == y_label
end
end
def not_found!
panels_not_found!(identifiers)
end
end
end
end
......@@ -53,6 +53,9 @@ the following documents:
- [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow).
- [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md).
If you're coming over from Jenkins, you can also check out our handy [reference](jenkins/index.md)
for converting your pipelines.
You can also get started by using one of the
[`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates)
available through the UI. You can use them by creating a new file,
......
---
comments: false
type: index, howto
---
# Migrating from Jenkins
A lot of GitLab users have successfully migrated to GitLab CI/CD from Jenkins. To make this
easier if you're just getting started, we've collected several resources here that you might find useful
before diving in.
First of all, our [Quick Start Guide](../quick_start/README.md) contains a good overview of how GitLab CI/CD works.
You may also be interested in [Auto DevOps](../../topics/autodevops/index.md) which can potentially be used to build, test,
and deploy your applications with little to no configuration needed at all.
Otherwise, read on for important information that will help you get the ball rolling. Welcome
to GitLab!
## Important differences
There are some high level differences between the products worth mentioning:
- With GitLab you don't need a root `pipeline` keyword to wrap everything.
- All jobs within a single stage always run in parallel, and all stages run in sequence. We are planning
to allow certain jobs to break this sequencing as needed with our [directed acyclic graph](https://gitlab.com/gitlab-org/gitlab-ce/issues/47063)
feature.
- The `.gitlab-ci.yml` file is checked in to the root of your repository, much like a Jenkinsfile, but
is in the YAML format (see [complete reference](../yaml/README.md)) instead of a Groovy DSL. It's most
analagous to the declarative Jenkinsfile format.
- GitLab comes with a [container registry](../../user/project/container_registry.md), and we recommend using
container images to set up your build environment.
## Artifact publishing
Artifacts may work a bit differently than you've used them with Jenkins. In GitLab, any job can define
a set of artifacts to be saved by using the `artifacts:` keyword. This can be configured to point to a file
or set of files that can then be persisted from job to job. Read more on our detailed [artifacts documentation](../../user/project/pipelines/job_artifacts.html)
```yaml
pdf:
script: xelatex mycv.tex
artifacts:
paths:
- ./mycv.pdf
- ./output/
expire_in: 1 week
```
Additionally, we have package management features like a built-in container, NPM, and Maven registry that you
can leverage. You can see the complete list of packaging features (which includes links to documentation)
in the [Packaging section of our documentation](../../README.md#package).
## Integrated features
Where you may have used plugins to get things like code quality, unit tests, security scanning, and so on working in Jenkins,
GitLab takes advantage of our connected ecosystem to automatically pull these kinds of results into
your Merge Requests, pipeline details pages, and other locations. You may find that you actually don't
need to configure anything to have these appear.
If they aren't working as expected, or if you'd like to see what's available, our [CI feature index](../README.md#feature-set) has the full list
of bundled features and links to the documentation for each.
## Converting Declarative Jenkinsfiles
Declarative Jenkinsfiles contain "Sections" and "Directives" which are used to control the behavior of your
pipelines. There are equivalents for all of these in GitLab, which we've documented below.
This section is based on the [Jenkinsfile syntax documentation](https://jenkins.io/doc/book/pipeline/syntax/)
and is meant to be a mapping of concepts there to concepts in GitLab.
### Sections
#### `agent`
The agent section is used to define how a pipeline will be executed. For GitLab, we use the [GitLab Runner](../runners/README.md)
to provide this capability. You can configure your own runners in Kubernetes or on any host, or take advantage
of our shared runner fleet (note that the shared runner fleet is only available for GitLab.com users.) The link above will bring you to the documenation which will describe how to get
up and running quickly. We also support using [tags](../runners/README.md#using-tags) to direct different jobs
to different Runners (execution agents).
The `agent` section also allows you to define which Docker images should be used for execution, for which we use
the [`image`](../yaml/README.md#image) keyword. The `image` can be set on a single job or at the top level, in which
case it will apply to all jobs in the pipeline.
```yaml
my_job:
image: alpine
...
```
#### `post`
The `post` section defines the actions that should be performed at the end of the pipeline. GitLab also supports
this through the use of stages. You can define your stages as follows, and any jobs assigned to the `before_pipeline`
or `after_pipeline` stages will run as expected. You can call these stages anything you like.
```yaml
stages:
- before_pipeline
- build
- test
- deploy
- after_pipeline
```
Setting a step to be performed before and after any job can be done via the [`before_script` and `after_script` keywords](../yaml/README.md#before_script-and-after_script).
```yaml
default:
before_script:
- echo "I run before any jobs starts in the entire pipeline, and can be responsible for setting up the environment."
```
#### `stages`
GitLab CI also lets you define stages, but is a little bit more free-form to configure. The GitLab [`stages` keyword](../yaml/README.md#stages)
is a top level setting that enumerates the list of stages, but you are not required to nest individual jobs underneath
the `stages` section. Any job defined in the `.gitlab-ci.yml` can be made a part of any stage through use of the
[`stage:` keyword](../yaml/README.md#stage).
Note that, unless otherwise specified, every pipeline is instantiated with a `build`, `test`, and `deploy` stage
which are run in that order. Jobs that have no `stage` defined are placed by default in the `test` stage.
Of course, each job that refers to a stage must refer to a stage that exists in the pipeline configuration.
```yaml
stages:
- build
- test
- deploy
my_job:
stage: build
...
```
#### `steps`
The `steps` section is equivalent to the [`script` section](../yaml/README.md#script) of an individual job. This is
a simple YAML array with each line representing an individual command to be run.
```yaml
my_job:
script:
- echo "hello! the current time is:"
- time
...
```
### Directives
#### `environment`
In GitLab, we use the [`variables` keyword](../yaml/README.md#variables) to define different variables at runtime.
These can also be set up through the GitLab UI, under CI/CD settings. See also our [general documentation on variables](../variables/README.md),
including the section on [protected variables](../variables/README.md#protected-environment-variables) which can be used
to limit access to certain variables to certain environments or runners.
```yaml
variables:
POSTGRES_USER: user
POSTGRES_PASSWORD: testing_password
```
#### `options`
Here, options for different things exist associated with the object in question itself. For example, options related
to jobs are defined in relation to the job itself. If you're looking for a certain option, you should be able to find
where it's located by searching our [complete configuration reference](../yaml/README.md) page.
#### `parameters`
GitLab does not require you to define which variables you want to be available when starting a manual job. A user
can provide any variables they like.
#### `triggers` / `cron`
Because GitLab is integrated tightly with git, SCM polling options for triggers are not needed. We support an easy to use
[syntax for scheduling pipelines](../../user/project/pipelines/schedules.md).
#### `tools`
GitLab does not support a separate `tools` directive. Our best-practice reccomendation is to use pre-built
container images, which can be cached, and can be built to already contain the tools you need for your pipelines. Pipelines can
be set up to automatically build these images as needed and deploy them to the [container registry](../../user/project/container_registry.md).
If you're not using container images with Docker/Kubernetes, for example on Mac or FreeBSD, then the `shell` executor does require you to
set up your environment either in advance or as part of the jobs. You could create a `before_script`
action that handles this for you.
#### `input`
Similar to the `parameters` keyword, this is not needed because a manual job can always be provided runtime
variable entry.
#### `when`
GitLab does support a [`when` keyword](../yaml/README.md#when) which is used to indicate when a job should be
run in case of (or despite) failure, but most of the logic for controlling pipelines can be found in
our very powerful [`only/except` rules system](../yaml/README.md#onlyexcept-basic) (see also our [advanced syntax](../yaml/README.md#onlyexcept-basic))
```yaml
my_job:
only: branches
```
\ No newline at end of file
......@@ -13,6 +13,10 @@ NOTE: **Note:**
Please keep in mind that only project Maintainers and Admin users have
the permissions to access a project's settings.
NOTE: **Note:**
Coming over to GitLab from Jenkins? Check out our [reference](../jenkins/index.md)
for converting your pre-existing pipelines over to our format.
GitLab offers a [continuous integration][ci] service. If you
[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
and configure your GitLab project to use a [Runner], then each commit or
......
# frozen_string_literal: true
# Central point for managing default attributes from within
# the metrics dashboard module.
module Gitlab
module Metrics
module Dashboard
module Defaults
DEFAULT_PANEL_TYPE = 'area-chart'
DEFAULT_PANEL_WEIGHT = 0
end
end
end
end
# frozen_string_literal: true
# Central point for managing errors from within the metrics
# dashboard module. Handles errors from dashboard retrieval
# and processing steps, as well as defines shared error classes.
module Gitlab
module Metrics
module Dashboard
module Errors
PanelNotFoundError = Class.new(StandardError)
PROCESSING_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardProcessingError
NOT_FOUND_ERROR = Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
def handle_errors(error)
case error
when PROCESSING_ERROR
error(error.message, :unprocessable_entity)
when NOT_FOUND_ERROR
error("#{dashboard_path} could not be found.", :not_found)
when PanelNotFoundError
error(error.message, :not_found)
else
raise error
end
end
def panels_not_found!(opts)
raise PanelNotFoundError.new("No panels matching properties #{opts}")
end
end
end
end
end
......@@ -12,21 +12,37 @@ module Gitlab
# @param project [Project]
# @param user [User]
# @param environment [Environment]
# @param opts - dashboard_path [String] Path at which the
# dashboard can be found. Nil values will
# default to the system dashboard.
# @param opts - embedded [Boolean] Determines whether the
# @param options - embedded [Boolean] Determines whether the
# dashboard is to be rendered as part of an
# issue or location other than the primary
# metrics dashboard UI. Returns only the
# Memory/CPU charts of the system dash.
# @param options - dashboard_path [String] Path at which the
# dashboard can be found. Nil values will
# default to the system dashboard.
# @param options - group [String] Title of the group
# to which a panel might belong. Used by
# embedded dashboards.
# @param options - title [String] Title of the panel.
# Used by embedded dashboards.
# @param options - y_label [String] Y-Axis label of
# a panel. Used by embedded dashboards.
# @return [Hash]
def find(project, user, environment, dashboard_path: nil, embedded: false)
service_for_path(dashboard_path, embedded: embedded)
.new(project, user, environment: environment, dashboard_path: dashboard_path)
def find(project, user, environment, options = {})
service_for(options)
.new(project, user, options.merge(environment: environment))
.get_dashboard
end
# Returns a dashboard without any supplemental info.
# Returns only full, yml-defined dashboards.
# @return [Hash]
def find_raw(project, dashboard_path: nil)
service_for(dashboard_path: dashboard_path)
.new(project, nil, dashboard_path: dashboard_path)
.raw_dashboard
end
# Summary of all known dashboards.
# @return [Array<Hash>] ex) [{ path: String,
# display_name: String,
......@@ -46,13 +62,6 @@ module Gitlab
private
def service_for_path(dashboard_path, embedded:)
return embed_service if embedded
return system_service if system_dashboard?(dashboard_path)
project_service
end
def system_service
::Metrics::Dashboard::SystemDashboardService
end
......@@ -61,12 +70,8 @@ module Gitlab
::Metrics::Dashboard::ProjectDashboardService
end
def embed_service
::Metrics::Dashboard::DefaultEmbedService
end
def system_dashboard?(filepath)
!filepath || system_service.system_dashboard?(filepath)
def service_for(options)
Gitlab::Metrics::Dashboard::ServiceSelector.call(options)
end
end
end
......
# frozen_string_literal: true
# Responsible for determining which dashboard service should
# be used to fetch or generate a dashboard hash.
# The services can be considered in two categories - embeds
# and dashboards. Embeds are all portions of dashboards.
module Gitlab
module Metrics
module Dashboard
class ServiceSelector
SERVICES = ::Metrics::Dashboard
class << self
include Gitlab::Utils::StrongMemoize
# Returns a class which inherits from the BaseService
# class that can be used to obtain a dashboard.
# @return [Gitlab::Metrics::Dashboard::Services::BaseService]
def call(params)
return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params)
return SERVICES::DynamicEmbedService if dynamic_embed?(params)
return SERVICES::DefaultEmbedService if params[:embedded]
return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path])
return SERVICES::ProjectDashboardService if params[:dashboard_path]
default_service
end
private
def default_service
SERVICES::SystemDashboardService
end
def system_dashboard?(filepath)
SERVICES::SystemDashboardService.system_dashboard?(filepath)
end
def custom_metric_embed?(params)
SERVICES::CustomMetricEmbedService.valid_params?(params)
end
def dynamic_embed?(params)
SERVICES::DynamicEmbedService.valid_params?(params)
end
end
end
end
end
end
......@@ -5,11 +5,11 @@ module Gitlab
module Dashboard
module Stages
class BaseStage
include Gitlab::Metrics::Dashboard::Defaults
DashboardProcessingError = Class.new(StandardError)
LayoutError = Class.new(DashboardProcessingError)
DEFAULT_PANEL_TYPE = 'area-chart'
attr_reader :project, :environment, :dashboard
def initialize(project, environment, dashboard)
......
......@@ -97,7 +97,7 @@ module Gitlab
end
def new_metric(metric)
metric.queries.first.merge(metric_id: metric.id)
metric.to_metric_hash
end
end
end
......
......@@ -518,10 +518,10 @@ describe Projects::EnvironmentsController do
end
end
shared_examples_for 'the default dynamic dashboard' do
shared_examples_for 'specified dashboard embed' do |expected_titles|
it_behaves_like '200 response'
it 'contains only the Memory and CPU charts' do
it 'contains only the specified charts' do
get :metrics_dashboard, params: environment_params(dashboard_params)
dashboard = json_response['dashboard']
......@@ -531,10 +531,14 @@ describe Projects::EnvironmentsController do
expect(dashboard['dashboard']).to be_nil
expect(dashboard['panel_groups'].length).to eq 1
expect(panel_group['group']).to be_nil
expect(titles).to eq ['Memory Usage (Total)', 'Core Usage (Total)']
expect(titles).to eq expected_titles
end
end
shared_examples_for 'the default dynamic dashboard' do
it_behaves_like 'specified dashboard embed', ['Memory Usage (Total)', 'Core Usage (Total)']
end
shared_examples_for 'dashboard can be specified' do
context 'when dashboard is specified' do
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
......@@ -551,7 +555,7 @@ describe Projects::EnvironmentsController do
end
context 'when the specified dashboard is the default dashboard' do
let(:dashboard_path) { ::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH }
let(:dashboard_path) { system_dashboard_path }
it_behaves_like 'the default dashboard'
end
......@@ -564,12 +568,40 @@ describe Projects::EnvironmentsController do
it_behaves_like 'the default dynamic dashboard'
context 'when the dashboard is specified' do
let(:dashboard_params) { { format: :json, embedded: true, dashboard: '.gitlab/dashboards/fake.yml' } }
context 'when incomplete dashboard params are provided' do
let(:dashboard_params) { { format: :json, embedded: true, title: 'Title' } }
# The title param should be ignored.
it_behaves_like 'the default dynamic dashboard'
end
context 'when invalid params are provided' do
let(:dashboard_params) { { format: :json, embedded: true, metric_id: 16 } }
# The dashboard param should be ignored.
# The superfluous param should be ignored.
it_behaves_like 'the default dynamic dashboard'
end
context 'when the dashboard is correctly specified' do
let(:dashboard_params) do
{
format: :json,
embedded: true,
dashboard: system_dashboard_path,
group: business_metric_title,
title: 'title',
y_label: 'y_label'
}
end
it_behaves_like 'error response', :not_found
context 'and exists' do
let!(:metric) { create(:prometheus_metric, project: project) }
it_behaves_like 'specified dashboard embed', ['title']
end
end
end
end
......
......@@ -16,7 +16,8 @@
"unit": { "type": "string" },
"label": { "type": "string" },
"track": { "type": "string" },
"prometheus_endpoint_path": { "type": "string" }
"prometheus_endpoint_path": { "type": "string" },
"metric_id": { "type": "number" }
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Defaults do
it { is_expected.to be_const_defined(:DEFAULT_PANEL_TYPE) }
it { is_expected.to be_const_defined(:DEFAULT_PANEL_WEIGHT) }
end
......@@ -5,10 +5,9 @@ require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do
include MetricsDashboardHelpers
set(:project) { build(:project) }
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:environment) { create(:environment, project: project) }
let(:system_dashboard_path) { ::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH}
before do
project.add_maintainer(user)
......@@ -52,9 +51,80 @@ describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_cachi
end
context 'when the dashboard is expected to be embedded' do
let(:service_call) { described_class.find(project, user, environment, dashboard_path: nil, embedded: true) }
let(:service_call) { described_class.find(project, user, environment, **params) }
let(:params) { { embedded: true } }
it_behaves_like 'valid embedded dashboard service response'
context 'when params are incomplete' do
let(:params) { { embedded: true, dashboard_path: system_dashboard_path } }
it_behaves_like 'valid embedded dashboard service response'
end
context 'when the panel is specified' do
context 'as a custom metric' do
let(:params) do
{ embedded: true,
dashboard_path: system_dashboard_path,
group: business_metric_title,
title: 'title',
y_label: 'y_label' }
end
it_behaves_like 'misconfigured dashboard service response', :not_found
context 'when the metric exists' do
before do
create(:prometheus_metric, project: project)
end
it_behaves_like 'valid embedded dashboard service response'
end
end
context 'as a project-defined panel' do
let(:dashboard_path) { '.gitlab/dashboard/test.yml' }
let(:params) do
{ embedded: true,
dashboard_path: dashboard_path,
group: 'Group A',
title: 'Super Chart A1',
y_label: 'y_label' }
end
it_behaves_like 'misconfigured dashboard service response', :not_found
context 'when the metric exists' do
let(:project) { project_with_dashboard(dashboard_path) }
it_behaves_like 'valid embedded dashboard service response'
end
end
end
end
end
describe '.find_raw' do
let(:dashboard) { YAML.load_file(Rails.root.join('config', 'prometheus', 'common_metrics.yml')) }
let(:params) { {} }
subject { described_class.find_raw(project, **params) }
it { is_expected.to eq dashboard }
context 'when the system dashboard is specified' do
let(:params) { { dashboard_path: system_dashboard_path } }
it { is_expected.to eq dashboard }
end
context 'when an existing project dashboard is specified' do
let(:dashboard) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
let(:params) { { dashboard_path: '.gitlab/dashboards/test.yml' } }
let(:project) { project_with_dashboard(params[:dashboard_path]) }
it { is_expected.to eq dashboard }
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::ServiceSelector do
include MetricsDashboardHelpers
describe '#call' do
let(:arguments) { {} }
subject { described_class.call(arguments) }
it { is_expected.to be Metrics::Dashboard::SystemDashboardService }
context 'when just the dashboard path is provided' do
let(:arguments) { { dashboard_path: '.gitlab/dashboards/test.yml' } }
it { is_expected.to be Metrics::Dashboard::ProjectDashboardService }
context 'when the path is for the system dashboard' do
let(:arguments) { { dashboard_path: system_dashboard_path } }
it { is_expected.to be Metrics::Dashboard::SystemDashboardService }
end
end
context 'when the embedded flag is provided' do
let(:arguments) { { embedded: true } }
it { is_expected.to be Metrics::Dashboard::DefaultEmbedService }
context 'when an incomplete set of dashboard identifiers are provided' do
let(:arguments) { { embedded: true, dashboard_path: '.gitlab/dashboards/test.yml' } }
it { is_expected.to be Metrics::Dashboard::DefaultEmbedService }
end
context 'when all the chart identifiers are provided' do
let(:arguments) do
{
embedded: true,
dashboard_path: '.gitlab/dashboards/test.yml',
group: 'Important Metrics',
title: 'Total Requests',
y_label: 'req/sec'
}
end
it { is_expected.to be Metrics::Dashboard::DynamicEmbedService }
end
context 'when all chart params expect dashboard_path are provided' do
let(:arguments) do
{
embedded: true,
group: 'Important Metrics',
title: 'Total Requests',
y_label: 'req/sec'
}
end
it { is_expected.to be Metrics::Dashboard::DynamicEmbedService }
end
context 'with a system dashboard and "custom" group' do
let(:arguments) do
{
embedded: true,
dashboard_path: system_dashboard_path,
group: business_metric_title,
title: 'Total Requests',
y_label: 'req/sec'
}
end
it { is_expected.to be Metrics::Dashboard::CustomMetricEmbedService }
end
end
end
end
......@@ -150,4 +150,17 @@ describe PrometheusMetric do
expect(subject.to_query_metric.queries).to eq(queries)
end
end
describe '#to_metric_hash' do
it 'returns a hash suitable for inclusion on a metrics dashboard' do
expected_output = {
query_range: subject.query,
unit: subject.unit,
label: subject.legend,
metric_id: subject.id
}
expect(subject.to_metric_hash).to eq(expected_output)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Metrics::Dashboard::CustomMetricEmbedService do
include MetricsDashboardHelpers
set(:project) { build(:project) }
set(:user) { create(:user) }
set(:environment) { create(:environment, project: project) }
before do
project.add_maintainer(user)
end
let(:dashboard_path) { system_dashboard_path }
let(:group) { business_metric_title }
let(:title) { 'title' }
let(:y_label) { 'y_label' }
describe '.valid_params?' do
let(:valid_params) do
{
embedded: true,
dashboard_path: dashboard_path,
group: group,
title: title,
y_label: y_label
}
end
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 'non-system dashboard' do
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
it { is_expected.to be_falsey }
end
context 'undefined dashboard' do
let(:params) { valid_params.except(:dashboard_path) }
it { is_expected.to be_truthy }
end
context 'non-custom metric group' do
let(:group) { 'Different Group' }
it { is_expected.to be_falsey }
end
context 'missing group' do
let(:group) { nil }
it { is_expected.to be_falsey }
end
context 'missing title' do
let(:title) { nil }
it { is_expected.to be_falsey }
end
context 'undefined y-axis label' do
let(:params) { valid_params.except(:y_label) }
it { is_expected.to be_falsey }
end
end
describe '#get_dashboard' do
let(:service_params) do
[
project,
user,
{
embedded: true,
environment: environment,
dashboard_path: dashboard_path,
group: group,
title: title,
y_label: y_label
}
]
end
let(:service_call) { described_class.new(*service_params).get_dashboard }
it_behaves_like 'misconfigured dashboard service response', :not_found
it_behaves_like 'raises error for users with insufficient permissions'
context 'the custom metric exists' do
let!(:metric) { create(:prometheus_metric, project: project) }
it_behaves_like 'valid embedded dashboard service response'
it 'does not cache the unprocessed dashboard' do
expect(Gitlab::Metrics::Dashboard::Cache).not_to receive(:fetch)
described_class.new(*service_params).get_dashboard
end
context 'multiple metrics meet criteria' do
let!(:metric_2) { create(:prometheus_metric, project: project, query: 'avg(metric_2)') }
it_behaves_like 'valid embedded dashboard service response'
it 'includes both metrics' do
result = service_call
included_queries = all_queries(result[:dashboard])
expect(included_queries).to include('avg(metric_2)', 'avg(metric)')
end
end
end
context 'when the metric exists in another project' do
let!(:metric) { create(:prometheus_metric, project: create(:project)) }
it_behaves_like 'misconfigured dashboard service response', :not_found
end
end
private
def all_queries(dashboard)
dashboard[:panel_groups].flat_map do |group|
group[:panels].flat_map do |panel|
panel[:metrics].map do |metric|
metric[:query_range]
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Metrics::Dashboard::DynamicEmbedService, :use_clean_rails_memory_store_caching do
include MetricsDashboardHelpers
set(:project) { build(:project) }
set(:user) { create(:user) }
set(:environment) { create(:environment, project: project) }
before do
project.add_maintainer(user)
end
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
let(:group) { 'Group A' }
let(:title) { 'Super Chart A1' }
let(:y_label) { 'y_label' }
describe '.valid_params?' do
let(:valid_params) do
{
embedded: true,
dashboard_path: dashboard_path,
group: group,
title: title,
y_label: y_label
}
end
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 dashboard' do
let(:params) { valid_params.except(:dashboard_path) }
it { is_expected.to be_truthy }
end
context 'missing dashboard' do
let(:dashboard) { '' }
it { is_expected.to be_truthy }
end
context 'missing group' do
let(:group) { '' }
it { is_expected.to be_falsey }
end
context 'missing title' do
let(:title) { '' }
it { is_expected.to be_falsey }
end
context 'undefined y-axis label' do
let(:params) { valid_params.except(:y_label) }
it { is_expected.to be_falsey }
end
end
describe '#get_dashboard' do
let(:service_params) do
[
project,
user,
{
environment: environment,
dashboard_path: dashboard_path,
group: group,
title: title,
y_label: y_label
}
]
end
let(:service_call) { described_class.new(*service_params).get_dashboard }
context 'when the dashboard does not exist' do
it_behaves_like 'misconfigured dashboard service response', :not_found
end
context 'when the dashboard is exists' do
let(:project) { project_with_dashboard(dashboard_path) }
it_behaves_like 'valid embedded dashboard service response'
it_behaves_like 'raises error for users with insufficient permissions'
it 'caches the unprocessed dashboard for subsequent calls' do
expect(YAML).to receive(:safe_load).once.and_call_original
described_class.new(*service_params).get_dashboard
described_class.new(*service_params).get_dashboard
end
context 'when the specified group is not present on the dashboard' do
let(:group) { 'Group Not Found' }
it_behaves_like 'misconfigured dashboard service response', :not_found
end
context 'when the specified title is not present on the dashboard' do
let(:title) { 'Title Not Found' }
it_behaves_like 'misconfigured dashboard service response', :not_found
end
context 'when the specified y-axis label is not present on the dashboard' do
let(:y_label) { 'Y-Axis Not Found' }
it_behaves_like 'misconfigured dashboard service response', :not_found
end
end
shared_examples 'uses system dashboard' do
it 'uses the default dashboard' do
expect(Gitlab::Metrics::Dashboard::Finder)
.to receive(:find_raw)
.with(project, dashboard_path: system_dashboard_path)
.once
service_call
end
end
context 'when the dashboard is nil' do
let(:dashboard_path) { nil }
it_behaves_like 'uses system dashboard'
end
context 'when the dashboard is not present' do
let(:dashboard_path) { '' }
it_behaves_like 'uses system dashboard'
end
end
end
......@@ -18,6 +18,14 @@ module MetricsDashboardHelpers
project.repository.refresh_method_caches([:metrics_dashboard])
end
def system_dashboard_path
Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH
end
def business_metric_title
PrometheusMetricEnums.group_details[:business][:group_title]
end
shared_examples_for 'misconfigured dashboard service response' do |status_code|
it 'returns an appropriate message and status code' do
result = service_call
......
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