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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-11 15:09:26 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-11 15:09:26 +0300
commitc9687bdf58e9d4a9c3942f587bd4841f42e3b5de (patch)
treea60a2e20f152483be6a92bacdf10564bbc96c664
parent3f3e4bcc50a3280d03299c2c263eafd9c8e3bd7b (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue26
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/controllers/dashboard/projects_controller.rb5
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/service.rb9
-rw-r--r--app/services/boards/issues/list_service.rb2
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb20
-rw-r--r--changelogs/unreleased/19880-sort-closed-issues-by-recently-closed.yml5
-rw-r--r--changelogs/unreleased/204801-add-instance-to-services.yml5
-rw-r--r--changelogs/unreleased/206899-move-system-metrics-chart-group-to-the-top-of-the-default-dashbord.yml5
-rw-r--r--changelogs/unreleased/208887-optimize-project-counters-projects_with_repositories_enabled.yml5
-rw-r--r--changelogs/unreleased/fix-missing-rss-feed-events.yml5
-rw-r--r--changelogs/unreleased/sy-grafana-default-panel.yml5
-rw-r--r--changelogs/unreleased/udpate-cluster-application-image-to-0-11.yml6
-rw-r--r--config/prometheus/common_metrics.yml144
-rw-r--r--db/migrate/20200309140540_add_index_on_project_id_and_repository_access_level_to_project_features.rb18
-rw-r--r--db/migrate/20200310132654_add_instance_to_services.rb17
-rw-r--r--db/migrate/20200310135823_add_index_to_service_unique_instance_per_type.rb17
-rw-r--r--db/schema.rb5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql25
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json69
-rw-r--r--doc/api/graphql/reference/index.md11
-rw-r--r--doc/development/documentation/styleguide.md61
-rw-r--r--doc/user/group/epics/img/epic_view_roadmap_v12.3.pngbin50491 -> 0 bytes
-rw-r--r--doc/user/group/epics/img/epic_view_roadmap_v12_9.pngbin0 -> 434985 bytes
-rw-r--r--doc/user/group/epics/index.md2
-rw-r--r--doc/user/group/roadmap/img/roadmap_view.pngbin49757 -> 0 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_view_v12_9.pngbin0 -> 414880 bytes
-rw-r--r--doc/user/group/roadmap/index.md13
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--lib/banzai/filter/inline_grafana_metrics_filter.rb2
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml3
-rw-r--r--lib/gitlab/import_export/project/import_export.yml1
-rw-r--r--lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb111
-rw-r--r--lib/grafana/validator.rb96
-rw-r--r--locale/gitlab.pot19
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb12
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb51
-rw-r--r--spec/factories/services.rb5
-rw-r--r--spec/features/boards/issue_ordering_spec.rb25
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/project.json22
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js2
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js16
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js16
-rw-r--r--spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb82
-rw-r--r--spec/lib/grafana/validator_spec.rb119
-rw-r--r--spec/models/service_spec.rb24
-rw-r--r--spec/services/boards/issues/list_service_spec.rb20
-rw-r--r--spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb1
53 files changed, 800 insertions, 347 deletions
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index b90f441b8ec..5e41a155ef6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -122,9 +122,14 @@ export default {
this.$store.subscribeAction({
after: this.handleVuexActionDispatch,
});
+
+ document.addEventListener('click', this.handleDocumentClick);
+ },
+ beforeDestroy() {
+ document.removeEventListener('click', this.handleDocumentClick);
},
methods: {
- ...mapActions(['setInitialState']),
+ ...mapActions(['setInitialState', 'toggleDropdownContents']),
/**
* This method differentiates between
* dispatched actions and calls necessary method.
@@ -138,6 +143,22 @@ export default {
this.handleDropdownClose(state.labels.filter(label => label.touched));
}
},
+ /**
+ * This method listens for document-wide click event
+ * and toggle dropdown if user clicks anywhere outside
+ * the dropdown while dropdown is visible.
+ */
+ handleDocumentClick({ target }) {
+ if (
+ this.showDropdownButton &&
+ this.showDropdownContents &&
+ !target?.classList.contains('js-sidebar-dropdown-toggle') &&
+ !this.$refs.dropdownButtonCollapsed?.$el.contains(target) &&
+ !this.$refs.dropdownContents?.$el.contains(target)
+ ) {
+ this.toggleDropdownContents();
+ }
+ },
handleDropdownClose(labels) {
// Only emit label updates if there are any labels to update
// on UI.
@@ -156,6 +177,7 @@ export default {
<div v-if="!dropdownOnly">
<dropdown-value-collapsed
v-if="allowLabelCreate"
+ ref="dropdownButtonCollapsed"
:labels="selectedLabels"
@onValueClick="handleCollapsedValueClick"
/>
@@ -167,7 +189,7 @@ export default {
<slot></slot>
</dropdown-value>
<dropdown-button v-show="showDropdownButton" />
- <dropdown-contents v-if="showDropdownButton && showDropdownContents" />
+ <dropdown-contents v-if="showDropdownButton && showDropdownContents" ref="dropdownContents" />
</div>
</div>
</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 408ca249be2..20846502e85 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -413,6 +413,7 @@ img.emoji {
.prepend-left-20 { margin-left: 20px; }
.prepend-left-32 { margin-left: 32px; }
.prepend-left-64 { margin-left: 64px; }
+.append-right-2 { margin-right: 2px; }
.append-right-4 { margin-right: 4px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
@@ -424,6 +425,7 @@ img.emoji {
.append-right-48 { margin-right: 48px; }
.prepend-right-32 { margin-right: 32px; }
.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-2 { margin-bottom: 2px; }
.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 711be67f8f9..039991e07a2 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -33,7 +33,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def starred
@projects = load_projects(params.merge(starred: true))
- .includes(:forked_from_project, :tags).page(params[:page])
+ .includes(:forked_from_project, :tags)
@groups = []
@@ -51,7 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def projects
- @projects ||= load_projects(params.merge(non_public: true)).page(params[:page])
+ @projects ||= load_projects(params.merge(non_public: true))
end
def render_projects
@@ -73,6 +73,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
.execute
.includes(:route, :creator, :group, namespace: [:route, :owner])
.preload(:project_feature)
+ .page(finder_params[:page])
prepare_projects_for_rendering(projects)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d3f597c0bda..ef65b002816 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -67,6 +67,7 @@ class Issue < ApplicationRecord
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
+ scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :preload_associated_models, -> { preload(:labels, project: :namespace) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
diff --git a/app/models/service.rb b/app/models/service.rb
index 91597c51fca..8f1772e67f9 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -32,9 +32,12 @@ class Service < ApplicationRecord
belongs_to :project, inverse_of: :services
has_one :service_hook
- validates :project_id, presence: true, unless: -> { template? }
+ validates :project_id, presence: true, unless: -> { template? || instance? }
+ validates :project_id, absence: true, if: -> { instance? }
validates :type, presence: true
validates :template, uniqueness: { scope: :type }, if: -> { template? }
+ validates :instance, uniqueness: { scope: :type }, if: -> { instance? }
+ validate :validate_is_instance_or_template
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') }
@@ -326,6 +329,10 @@ class Service < ApplicationRecord
private
+ def validate_is_instance_or_template
+ errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance?
+ end
+
def cache_project_has_external_issue_tracker
if project && !project.destroyed?
project.cache_has_external_issue_tracker
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 699fa17cb65..337710b60e0 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -10,6 +10,8 @@ module Boards
end
def execute
+ return fetch_issues.order_closed_date_desc if list&.closed?
+
fetch_issues.order_by_position_and_priority(with_cte: can_attempt_search_optimization?)
end
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
index 3ad3a2c609e..274057b8262 100644
--- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -138,7 +138,9 @@ module Metrics
end
# Identifies the name of the datasource for a dashboard
- # based on the panelId query parameter found in the url
+ # based on the panelId query parameter found in the url.
+ #
+ # If no panel is specified, defaults to the first valid panel.
class DatasourceNameParser
def initialize(grafana_url, grafana_dashboard)
@grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard
@@ -146,15 +148,29 @@ module Metrics
def parse
@grafana_dashboard[:dashboard][:panels]
- .find { |panel| panel[:id].to_s == query_params[:panelId] }
+ .find { |panel| panel_id ? matching_panel?(panel) : valid_panel?(panel) }
.try(:[], :datasource)
end
private
+ def panel_id
+ query_params[:panelId]
+ end
+
def query_params
Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url)
end
+
+ def matching_panel?(panel)
+ panel[:id].to_s == panel_id
+ end
+
+ def valid_panel?(panel)
+ ::Grafana::Validator
+ .new(@grafana_dashboard, nil, panel, query_params)
+ .valid?
+ end
end
end
end
diff --git a/changelogs/unreleased/19880-sort-closed-issues-by-recently-closed.yml b/changelogs/unreleased/19880-sort-closed-issues-by-recently-closed.yml
new file mode 100644
index 00000000000..a015ef1b132
--- /dev/null
+++ b/changelogs/unreleased/19880-sort-closed-issues-by-recently-closed.yml
@@ -0,0 +1,5 @@
+---
+title: Sort closed issues on issue boards using time of closing
+merge_request: 23442
+author: briankabiro
+type: changed
diff --git a/changelogs/unreleased/204801-add-instance-to-services.yml b/changelogs/unreleased/204801-add-instance-to-services.yml
new file mode 100644
index 00000000000..458644d7dd6
--- /dev/null
+++ b/changelogs/unreleased/204801-add-instance-to-services.yml
@@ -0,0 +1,5 @@
+---
+title: Add instance column to services table
+merge_request: 25714
+author:
+type: other
diff --git a/changelogs/unreleased/206899-move-system-metrics-chart-group-to-the-top-of-the-default-dashbord.yml b/changelogs/unreleased/206899-move-system-metrics-chart-group-to-the-top-of-the-default-dashbord.yml
new file mode 100644
index 00000000000..afba03b584a
--- /dev/null
+++ b/changelogs/unreleased/206899-move-system-metrics-chart-group-to-the-top-of-the-default-dashbord.yml
@@ -0,0 +1,5 @@
+---
+title: Put System Metrics chart group first in default dashboard
+merge_request: 26355
+author:
+type: other
diff --git a/changelogs/unreleased/208887-optimize-project-counters-projects_with_repositories_enabled.yml b/changelogs/unreleased/208887-optimize-project-counters-projects_with_repositories_enabled.yml
new file mode 100644
index 00000000000..e2014464d8c
--- /dev/null
+++ b/changelogs/unreleased/208887-optimize-project-counters-projects_with_repositories_enabled.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize Project counters with respository enabled counter
+merge_request: 26698
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-missing-rss-feed-events.yml b/changelogs/unreleased/fix-missing-rss-feed-events.yml
deleted file mode 100644
index 9fbacc6d739..00000000000
--- a/changelogs/unreleased/fix-missing-rss-feed-events.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix missing RSS feed events
-merge_request: 19524
-author:
-type: fixed
diff --git a/changelogs/unreleased/sy-grafana-default-panel.yml b/changelogs/unreleased/sy-grafana-default-panel.yml
new file mode 100644
index 00000000000..9ad5c824b32
--- /dev/null
+++ b/changelogs/unreleased/sy-grafana-default-panel.yml
@@ -0,0 +1,5 @@
+---
+title: Default to first valid panel in unspecified Grafana embeds
+merge_request: 21932
+author:
+type: changed
diff --git a/changelogs/unreleased/udpate-cluster-application-image-to-0-11.yml b/changelogs/unreleased/udpate-cluster-application-image-to-0-11.yml
new file mode 100644
index 00000000000..27e11028441
--- /dev/null
+++ b/changelogs/unreleased/udpate-cluster-application-image-to-0-11.yml
@@ -0,0 +1,6 @@
+---
+title: Update cluster-applications image to v0.11 with a runner bugfix, updated cert-manager,
+ and vault as a new app
+merge_request: 26842
+author:
+type: changed
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 6eae29c3906..85833cc1968 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -1,6 +1,74 @@
dashboard: 'Environment metrics'
priority: 1
panel_groups:
+- group: System metrics (Kubernetes)
+ priority: 15
+ panels:
+ - title: "Memory Usage (Total)"
+ type: "area-chart"
+ y_label: "Total Memory Used (GB)"
+ weight: 4
+ metrics:
+ - id: system_metrics_kubernetes_container_memory_total
+ query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024'
+ label: Total (GB)
+ unit: GB
+ - title: "Core Usage (Total)"
+ type: "area-chart"
+ y_label: "Total Cores"
+ weight: 3
+ metrics:
+ - id: system_metrics_kubernetes_container_cores_total
+ query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)'
+ label: Total (cores)
+ unit: "cores"
+ - title: "Memory Usage (Pod average)"
+ type: "line-chart"
+ y_label: "Memory Used per Pod (MB)"
+ weight: 2
+ metrics:
+ - id: system_metrics_kubernetes_container_memory_average
+ query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
+ label: Pod average (MB)
+ unit: MB
+ - title: "Canary: Memory Usage (Pod Average)"
+ type: "line-chart"
+ y_label: "Memory Used per Pod (MB)"
+ weight: 2
+ metrics:
+ - id: system_metrics_kubernetes_container_memory_average_canary
+ query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
+ label: Pod average (MB)
+ unit: MB
+ track: canary
+ - title: "Core Usage (Pod Average)"
+ type: "line-chart"
+ y_label: "Cores per Pod"
+ weight: 1
+ metrics:
+ - id: system_metrics_kubernetes_container_core_usage
+ query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
+ label: Pod average (cores)
+ unit: "cores"
+ - title: "Canary: Core Usage (Pod Average)"
+ type: "line-chart"
+ y_label: "Cores per Pod"
+ weight: 1
+ metrics:
+ - id: system_metrics_kubernetes_container_core_usage_canary
+ query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
+ label: Pod average (cores)
+ unit: "cores"
+ track: canary
+ - title: "Knative function invocations"
+ type: "area-chart"
+ y_label: "Invocations"
+ weight: 1
+ metrics:
+ - id: system_metrics_knative_function_invocation_count
+ query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))'
+ label: invocations / minute
+ unit: requests
# NGINX Ingress metrics for pre-0.16.0 versions
- group: Response metrics (NGINX Ingress VTS)
priority: 10
@@ -150,79 +218,3 @@ panel_groups:
query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors
unit: "errors / sec"
-- group: System metrics (Kubernetes)
- priority: 5
- panels:
- - title: "Memory Usage (Total)"
- type: "area-chart"
- y_label: "Total Memory Used (GB)"
- y_axis:
- format: "gibibytes"
- weight: 4
- metrics:
- - id: system_metrics_kubernetes_container_memory_total
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024'
- label: Total (GB)
- unit: GB
- - title: "Core Usage (Total)"
- type: "area-chart"
- y_label: "Total Cores"
- weight: 3
- metrics:
- - id: system_metrics_kubernetes_container_cores_total
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)'
- label: Total (cores)
- unit: "cores"
- - title: "Memory Usage (Pod average)"
- type: "line-chart"
- y_label: "Memory Used per Pod (MB)"
- y_axis:
- format: "mebibytes"
- weight: 2
- metrics:
- - id: system_metrics_kubernetes_container_memory_average
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
- label: Pod average (MB)
- unit: MB
- - title: "Canary: Memory Usage (Pod Average)"
- type: "line-chart"
- y_label: "Memory Used per Pod (MB)"
- y_axis:
- format: "mebibytes"
- weight: 2
- metrics:
- - id: system_metrics_kubernetes_container_memory_average_canary
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
- label: Pod average (MB)
- unit: MB
- track: canary
- - title: "Core Usage (Pod Average)"
- type: "line-chart"
- y_label: "Cores per Pod"
- weight: 1
- metrics:
- - id: system_metrics_kubernetes_container_core_usage
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
- label: Pod average (cores)
- unit: "cores"
- - title: "Canary: Core Usage (Pod Average)"
- type: "line-chart"
- y_label: "Cores per Pod"
- weight: 1
- metrics:
- - id: system_metrics_kubernetes_container_core_usage_canary
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
- label: Pod average (cores)
- unit: "cores"
- track: canary
- - title: "Knative function invocations"
- type: "area-chart"
- y_label: "Invocations"
- y_axis:
- precision: 0
- weight: 1
- metrics:
- - id: system_metrics_knative_function_invocation_count
- query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))'
- label: invocations / minute
- unit: requests
diff --git a/db/migrate/20200309140540_add_index_on_project_id_and_repository_access_level_to_project_features.rb b/db/migrate/20200309140540_add_index_on_project_id_and_repository_access_level_to_project_features.rb
new file mode 100644
index 00000000000..a2093db0b3b
--- /dev/null
+++ b/db/migrate/20200309140540_add_index_on_project_id_and_repository_access_level_to_project_features.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnProjectIdAndRepositoryAccessLevelToProjectFeatures < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_project_features_on_project_id_ral_20'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :project_features, :project_id, where: 'repository_access_level = 20', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :project_features, INDEX_NAME
+ end
+end
diff --git a/db/migrate/20200310132654_add_instance_to_services.rb b/db/migrate/20200310132654_add_instance_to_services.rb
new file mode 100644
index 00000000000..85b16a4094c
--- /dev/null
+++ b/db/migrate/20200310132654_add_instance_to_services.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddInstanceToServices < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:services, :instance, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:services, :instance)
+ end
+end
diff --git a/db/migrate/20200310135823_add_index_to_service_unique_instance_per_type.rb b/db/migrate/20200310135823_add_index_to_service_unique_instance_per_type.rb
new file mode 100644
index 00000000000..1a60c521b71
--- /dev/null
+++ b/db/migrate/20200310135823_add_index_to_service_unique_instance_per_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToServiceUniqueInstancePerType < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:services, [:type, :instance], unique: true, where: 'instance IS TRUE')
+ end
+
+ def down
+ remove_concurrent_index(:services, [:type, :instance])
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 62cce205424..2b726073035 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_03_09_195710) do
+ActiveRecord::Schema.define(version: 2020_03_10_135823) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -3265,6 +3265,7 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do
t.integer "forking_access_level"
t.index ["project_id"], name: "index_project_features_on_project_id", unique: true
t.index ["project_id"], name: "index_project_features_on_project_id_bal_20", where: "(builds_access_level = 20)"
+ t.index ["project_id"], name: "index_project_features_on_project_id_ral_20", where: "(repository_access_level = 20)"
end
create_table "project_group_links", id: :serial, force: :cascade do |t|
@@ -3939,8 +3940,10 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do
t.string "description", limit: 500
t.boolean "comment_on_event_enabled", default: true, null: false
t.boolean "template", default: false
+ t.boolean "instance", default: false, null: false
t.index ["project_id"], name: "index_services_on_project_id"
t.index ["template"], name: "index_services_on_template"
+ t.index ["type", "instance"], name: "index_services_on_type_and_instance", unique: true, where: "(instance IS TRUE)"
t.index ["type", "template"], name: "index_services_on_type_and_template", unique: true, where: "(template IS TRUE)"
t.index ["type"], name: "index_services_on_type"
end
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index f47f07953d7..b6caff283fc 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2024,6 +2024,11 @@ type Epic implements Noteable {
hasIssues: Boolean!
"""
+ Current health status of the epic
+ """
+ healthStatus: EpicHealthStatus
+
+ """
ID of the epic
"""
id: ID!
@@ -2350,6 +2355,26 @@ type EpicEdge {
}
"""
+Health status of child issues
+"""
+type EpicHealthStatus {
+ """
+ Number of issues at risk
+ """
+ issuesAtRisk: Int
+
+ """
+ Number of issues that need attention
+ """
+ issuesNeedingAttention: Int
+
+ """
+ Number of issues on track
+ """
+ issuesOnTrack: Int
+}
+
+"""
Relationship between an epic and an issue
"""
type EpicIssue implements Noteable {
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 3472a8bf742..0f0abf25047 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -5187,6 +5187,20 @@
"deprecationReason": null
},
{
+ "name": "healthStatus",
+ "description": "Current health status of the epic",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicHealthStatus",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "id",
"description": "ID of the epic",
"args": [
@@ -13086,6 +13100,61 @@
},
{
"kind": "OBJECT",
+ "name": "EpicHealthStatus",
+ "description": "Health status of child issues",
+ "fields": [
+ {
+ "name": "issuesAtRisk",
+ "description": "Number of issues at risk",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issuesNeedingAttention",
+ "description": "Number of issues that need attention",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issuesOnTrack",
+ "description": "Number of issues on track",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "TimelogConnection",
"description": "The connection type for Timelog.",
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index df85e13d194..a85553ce4aa 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -327,6 +327,7 @@ Represents an epic.
| `group` | Group! | Group to which the epic belongs |
| `hasChildren` | Boolean! | Indicates if the epic has children |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues |
+| `healthStatus` | EpicHealthStatus | Current health status of the epic |
| `id` | ID! | ID of the epic |
| `iid` | ID! | Internal ID of the epic |
| `parent` | Epic | Parent epic of the epic |
@@ -377,6 +378,16 @@ Total weight of open and closed descendant issues
| `closedIssues` | Int | Total weight of completed (closed) issues in this epic, including epic descendants |
| `openedIssues` | Int | Total weight of opened issues in this epic, including epic descendants |
+## EpicHealthStatus
+
+Health status of child issues
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `issuesAtRisk` | Int | Number of issues at risk |
+| `issuesNeedingAttention` | Int | Number of issues that need attention |
+| `issuesOnTrack` | Int | Number of issues on track |
+
## EpicIssue
Relationship between an epic and an issue
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index f769560d67f..67b1f7eb1f1 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -261,15 +261,6 @@ Do not include the same information in multiple places. [Link to a SSOT instead.
Some features are also objects. For example, "GitLab's Merge Requests support X" and
"Create a new merge request for Z."
-- Use common contractions when it helps create a friendly and informal tone, especially in tutorials and [UIs](https://design.gitlab.com/content/punctuation/#contractions).
- - Do use contractions like: _it's_, _can't_, _wouldn't_, _you're_, _you've_, _haven't_, don't, _we're_, _that's_, and _won't_. Contractions in instructional documentation such as tutorials can help create a friendly and informal tone.
- - Avoid less common contractions such as: _he'd_, _it'll_, _should've_, and _there'd_.
- - Do not use contractions in reference documentation. Examples:
- - You cannot set a limit higher than 1000.
- - For `parameter1`, the default is 10.
- - Do not use contractions with a proper noun and a verb, such as _GitLab's creating X_.
- - Avoid using contractions when you need to emphasize a negative, such as "Do **not** install X with Y."
-
- Avoid use of the future tense:
- Instead of "after you execute this command, GitLab will display the result", use "after you execute this command, GitLab displays the result".
- Only use the future tense to convey when the action or result will actually occur at a future time.
@@ -286,6 +277,58 @@ as even native users of English might misunderstand them.
- Instead of "e.g.", use "for example," "such as," "for instance," or "like."
- Instead of "etc.", either use "and so on" or consider editing it out, since it can be vague.
+### Contractions
+
+- Use common contractions when it helps create a friendly and informal tone, especially in tutorials, instructional documentation, and [UIs](https://design.gitlab.com/content/punctuation/#contractions).
+
+| Do | Don't |
+|----------|-----------|
+| it's | it is |
+| can't | cannot |
+| wouldn't | would not |
+| you're | you are |
+| you've | you have |
+| haven't | have not |
+| don't | do not |
+| we're | we are |
+| that's' | that is |
+| won't | will not |
+
+- Avoid less common contractions:
+
+| Do | Don't |
+|--------------|-------------|
+| he would | he'd |
+| it will | it'll |
+| should have | should've |
+| there would | there'd |
+
+- Do not use contractions with a proper noun and a verb. For example:
+
+| Do | Don't |
+|----------------------|---------------------|
+| GitLab is creating X | GitLab's creating X |
+
+- Do not use contractions when you need to emphasize a negative. For example:
+
+| Do | Don't |
+|-----------------------------|----------------------------|
+| Do **not** install X with Y | **Don't** install X with Y |
+
+- Do not use contractions in reference documentation. For example:
+
+| Do | Don't |
+|------------------------------------------|----------------------------|
+| Do **not** set a limit greater than 1000 | **Don't** set a limit greater than 1000 |
+| For `parameter1`, the default is 10 | For `parameter1`, the default's 10 |
+
+- Avoid contractions in error messages. Examples:
+
+| Do | Don't |
+|------------------------------------------|----------------------------|
+| Requests to localhost are not allowed | Requests to localhost aren't allowed |
+| Specified URL cannot be used | Specified URL can't be used |
+
## Text
- [Write in Markdown](#markdown).
diff --git a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
deleted file mode 100644
index a17c56c618b..00000000000
--- a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/epic_view_roadmap_v12_9.png b/doc/user/group/epics/img/epic_view_roadmap_v12_9.png
new file mode 100644
index 00000000000..b85f1806123
--- /dev/null
+++ b/doc/user/group/epics/img/epic_view_roadmap_v12_9.png
Binary files differ
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 421a43ba818..eae989b220b 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -182,7 +182,7 @@ If your epic contains one or more [child epics](#multi-level-child-epics-ultimat
have a [start or due date](#start-date-and-due-date), a
[roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic.
-![Child epics roadmap](img/epic_view_roadmap_v12.3.png)
+![Child epics roadmap](img/epic_view_roadmap_v12_9.png)
## Reordering issues and child epics
diff --git a/doc/user/group/roadmap/img/roadmap_view.png b/doc/user/group/roadmap/img/roadmap_view.png
deleted file mode 100644
index 2be3849ba1b..00000000000
--- a/doc/user/group/roadmap/img/roadmap_view.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_view_v12_9.png b/doc/user/group/roadmap/img/roadmap_view_v12_9.png
new file mode 100644
index 00000000000..3aa8cbb8332
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_view_v12_9.png
Binary files differ
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index a72cd990706..6eca9e84ce9 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -10,7 +10,12 @@ An Epic within a group containing **Start date** and/or **Due date**
can be visualized in a form of a timeline (e.g. a Gantt chart). The Epics Roadmap page
shows such a visualization for all the epics which are under a group and/or its subgroups.
-![roadmap view](img/roadmap_view.png)
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5164) in GitLab 12.9.
+
+On the epic bars, you can see their title, progress, and completed weight percentage.
+When you hover over an epic bar, a popover appears with its description, start and due dates, and weight completed.
+
+![roadmap view](img/roadmap_view_v12_9.png)
A dropdown allows you to show only open or closed epics. By default, all epics are shown.
@@ -68,11 +73,7 @@ the timeline header represent the days of the week.
## Timeline bar for an epic
-The timeline bar indicates the approximate position of an epic based on its start
-and due date. If an epic doesn't have a due date, the timeline bar fades
-away towards the future. Similarly, if an epic doesn't have a start date, the
-timeline bar becomes more visible as it approaches the epic's due date on the
-timeline.
+The timeline bar indicates the approximate position of an epic based on its start and due date.
<!-- ## Troubleshooting
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index e1790bfc30c..3c2ca7e6e59 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -820,7 +820,7 @@ Prerequisites for embedding from a Grafana instance:
![Grafana Metric Panel](img/grafana_panel_v12_5.png)
1. In the upper-left corner of the page, select a specific value for each variable required for the queries in the chart.
![Select Query Variables](img/select_query_variables_v12_5.png)
-1. In Grafana, click on a panel's title, then click **Share** to open the panel's sharing dialog to the **Link** tab.
+1. In Grafana, click on a panel's title, then click **Share** to open the panel's sharing dialog to the **Link** tab. If you click the _dashboard's_ share panel instead, GitLab will attempt to embed the first supported panel on the dashboard (if available).
1. If your Prometheus queries use Grafana's custom template variables, ensure that "Template variables" option is toggled to **On**. Of Grafana global template variables, only `$__interval`, `$__from`, and `$__to` are currently supported. Toggle **On** the "Current time range" option to specify the time range of the chart. Otherwise, the default range will be the last 8 hours.
![Grafana Sharing Dialog](img/grafana_sharing_dialog_v12_5.png)
1. Click **Copy** to copy the URL to the clipboard.
diff --git a/lib/banzai/filter/inline_grafana_metrics_filter.rb b/lib/banzai/filter/inline_grafana_metrics_filter.rb
index 60a16b164af..07bde9858e8 100644
--- a/lib/banzai/filter/inline_grafana_metrics_filter.rb
+++ b/lib/banzai/filter/inline_grafana_metrics_filter.rb
@@ -17,8 +17,6 @@ module Banzai
def embed_params(node)
query_params = Gitlab::Metrics::Dashboard::Url.parse_query(node['href'])
- return unless query_params.include?(:panelId)
-
time_window = Grafana::TimeWindow.new(query_params[:from], query_params[:to])
url = url_with_window(node['href'], query_params, time_window.in_milliseconds)
diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
index ef77bbf5626..48458142f1c 100644
--- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -1,6 +1,6 @@
apply:
stage: deploy
- image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.9.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.11.0"
environment:
name: production
variables:
@@ -15,6 +15,7 @@ apply:
JUPYTERHUB_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/jupyterhub/values.yaml
PROMETHEUS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/prometheus/values.yaml
ELASTIC_STACK_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/elastic-stack/values.yaml
+ VAULT_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/vault/values.yaml
script:
- gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml
only:
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index b7b61df4cd6..aa6085de4f9 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -248,6 +248,7 @@ excluded_attributes:
- :token_encrypted
services:
- :template
+ - :instance
error_tracking_setting:
- :encrypted_token
- :encrypted_token_iv
diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
index ce75c54d014..c90c1e3f0bc 100644
--- a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
@@ -13,12 +13,7 @@ module Gitlab
# Reformats the specified panel in the Gitlab
# dashboard-yml format
def transform!
- InputFormatValidator.new(
- grafana_dashboard,
- datasource,
- panel,
- query_params
- ).validate!
+ validate_input!
new_dashboard = formatted_dashboard
@@ -28,6 +23,17 @@ module Gitlab
private
+ def validate_input!
+ ::Grafana::Validator.new(
+ grafana_dashboard,
+ datasource,
+ panel,
+ query_params
+ ).validate!
+ rescue ::Grafana::Validator::Error => e
+ raise ::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError, e.message
+ end
+
def formatted_dashboard
{ panel_groups: [{ panels: [formatted_panel] }] }
end
@@ -56,11 +62,25 @@ module Gitlab
def panel
strong_memoize(:panel) do
grafana_dashboard[:dashboard][:panels].find do |panel|
- panel[:id].to_s == query_params[:panelId]
+ query_params[:panelId] ? matching_panel?(panel) : valid_panel?(panel)
end
end
end
+ # Determines whether a given panel is the one
+ # specified by the linked grafana url
+ def matching_panel?(panel)
+ panel[:id].to_s == query_params[:panelId]
+ end
+
+ # Determines whether any given panel has the potenial
+ # to return valid results from grafana/prometheus
+ def valid_panel?(panel)
+ ::Grafana::Validator
+ .new(grafana_dashboard, datasource, panel, query_params)
+ .valid?
+ end
+
# Grafana url query parameters. Includes information
# on which panel to select and time range.
def query_params
@@ -141,83 +161,6 @@ module Gitlab
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
diff --git a/lib/grafana/validator.rb b/lib/grafana/validator.rb
new file mode 100644
index 00000000000..760263f7ec9
--- /dev/null
+++ b/lib/grafana/validator.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+# Performs checks on whether resources from Grafana can be handled
+# We have certain restrictions on which formats we accept.
+# Some are technical requirements, others are simplifications.
+module Grafana
+ class Validator
+ Error = Class.new(StandardError)
+
+ 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_panel_type!
+ validate_variable_definitions!
+ validate_global_variables!
+ validate_datasource! if datasource
+ end
+
+ def valid?
+ validate!
+
+ true
+ rescue ::Grafana::Validator::Error
+ false
+ end
+
+ private
+
+ # See defaults in Banzai::Filter::InlineGrafanaMetricsFilter.
+ def validate_query_params!
+ return if [:from, :to].all? { |param| query_params.include?(param) }
+
+ raise_error 'Grafana query parameters must include from and to.'
+ end
+
+ # We may choose to support other panel types in future.
+ def validate_panel_type!
+ return if panel && panel[:type] == 'graph' && panel[:lines]
+
+ raise_error 'Panel type must be a line graph.'
+ end
+
+ # We must require variable definitions to create valid prometheus queries.
+ 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
+
+ # We may choose to support further Grafana variables in future.
+ def validate_global_variables!
+ return unless panel_contains_unsupported_vars?
+
+ raise_error "Prometheus must not include #{UNSUPPORTED_GRAFANA_GLOBAL_VARS}"
+ end
+
+ # We may choose to support additional datasources in future.
+ 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 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 Validator::Error, message
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 210e4ca7c12..edff8701c58 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -220,6 +220,9 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
+msgid "%{completedWeight} of %{totalWeight} weight completed"
+msgstr ""
+
msgid "%{cores} cores"
msgstr ""
@@ -600,6 +603,9 @@ msgid_plural "- Users"
msgstr[0] ""
msgstr[1] ""
+msgid "- of - weight completed"
+msgstr ""
+
msgid "- show less"
msgstr ""
@@ -7795,6 +7801,9 @@ msgstr ""
msgid "Epics|An error occurred while saving the %{epicDateType} date"
msgstr ""
+msgid "Epics|An error occurred while updating labels."
+msgstr ""
+
msgid "Epics|Are you sure you want to remove %{bStart}%{targetIssueTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}?"
msgstr ""
@@ -9925,10 +9934,13 @@ msgstr ""
msgid "Group: %{name}"
msgstr ""
-msgid "GroupRoadmap|%{startDateInWords} &ndash; %{endDateInWords}"
+msgid "GroupRoadmap|%{dateWord} – No end date"
+msgstr ""
+
+msgid "GroupRoadmap|%{startDateInWords} – %{endDateInWords}"
msgstr ""
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "GroupRoadmap|No start date – %{dateWord}"
msgstr ""
msgid "GroupRoadmap|Something went wrong while fetching epics"
@@ -9949,9 +9961,6 @@ msgstr ""
msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
-msgstr ""
-
msgid "GroupSAML|Certificate fingerprint"
msgstr ""
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 605fff60c31..41c37cb84e5 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -57,6 +57,18 @@ describe Boards::IssuesController do
expect(development.issues.map(&:relative_position)).not_to include(nil)
end
+ it 'returns issues by closed_at in descending order in closed list' do
+ create(:closed_issue, project: project, title: 'New Issue 1', closed_at: 1.day.ago)
+ create(:closed_issue, project: project, title: 'New Issue 2', closed_at: 1.week.ago)
+
+ list_issues user: user, board: board, list: board.lists.last.id
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['issues'].length).to eq(2)
+ expect(json_response['issues'][0]['title']).to eq('New Issue 1')
+ expect(json_response['issues'][1]['title']).to eq('New Issue 2')
+ end
+
it 'avoids N+1 database queries' do
create(:labeled_issue, project: project, labels: [development])
control_count = ActiveRecord::QueryRecorder.new { list_issues(user: user, board: board, list: list2) }.count
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index 7ae31fc67aa..a13b56deb23 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -86,58 +86,11 @@ describe Dashboard::ProjectsController do
end
describe 'GET /starred.json' do
- subject { get :starred, format: :json }
-
- let(:projects) { create_list(:project, 2, creator: user) }
-
before do
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
-
- projects.each do |project|
- project.add_developer(user)
- create(:users_star_project, project_id: project.id, user_id: user.id)
- end
- end
-
- it 'returns success' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
+ get :starred, format: :json
end
- it 'paginates the records' do
- subject
-
- expect(assigns(:projects).count).to eq(1)
- end
- end
- end
-
- context 'atom requests' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- describe '#index' do
- context 'project pagination' do
- let(:projects) { create_list(:project, 2, creator: user) }
-
- before do
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
-
- projects.each do |project|
- project.add_developer(user)
- end
- end
-
- it 'does not paginate projects, even if page number is passed' do
- get :index, format: :atom
-
- expect(assigns(:events).count).to eq(2)
- end
- end
+ it { is_expected.to respond_with(:success) }
end
end
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index ffa51abf26f..ebab370ccf6 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -4,6 +4,11 @@ FactoryBot.define do
factory :service do
project
type { 'Service' }
+
+ trait :instance do
+ project { nil }
+ instance { true }
+ end
end
factory :custom_issue_tracker_service, class: 'CustomIssueTrackerService' do
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 62abd914fcb..4c723ddf324 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -47,6 +47,31 @@ describe 'Issue Boards', :js do
end
end
+ context 'closed issues' do
+ let!(:issue7) { create(:closed_issue, project: project, title: 'Closed issue 1', closed_at: 1.day.ago) }
+ let!(:issue8) { create(:closed_issue, project: project, title: 'Closed issue 2', closed_at: 1.week.ago) }
+ let!(:issue9) { create(:closed_issue, project: project, title: 'Closed issue 3', closed_at: 2.weeks.ago) }
+
+ before do
+ visit project_board_path(project, board)
+ wait_for_requests
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'orders issues by closed_at' do
+ wait_for_requests
+
+ page.within(find('.board:nth-child(3)')) do
+ first, second, third = all('.board-card').to_a
+
+ expect(first).to have_content(issue7.title)
+ expect(second).to have_content(issue8.title)
+ expect(third).to have_content(issue9.title)
+ end
+ end
+ end
+
context 'ordering in list' do
before do
visit project_board_path(project, board)
diff --git a/spec/fixtures/lib/gitlab/import_export/light/project.json b/spec/fixtures/lib/gitlab/import_export/light/project.json
index 2971ca0f0f8..51e2e9ac623 100644
--- a/spec/fixtures/lib/gitlab/import_export/light/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/light/project.json
@@ -111,6 +111,28 @@
"active": false,
"properties": {},
"template": true,
+ "instance": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "job_events": true,
+ "type": "TeamcityService",
+ "category": "ci",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 101,
+ "title": "JetBrains TeamCity CI",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.315Z",
+ "updated_at": "2016-06-14T15:01:51.315Z",
+ "active": false,
+ "properties": {},
+ "template": false,
+ "instance": true,
"push_events": true,
"issues_events": true,
"merge_requests_events": true,
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 797c52cd31b..02b59d46c71 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -69,7 +69,7 @@ describe('Time series component', () => {
mockedQueryResultFixture,
);
// dashboard is a dynamically generated fixture and stored at environment_metrics_dashboard.json
- [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[0].panels;
+ [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[1].panels;
});
describe('general functions', () => {
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index 777181df10e..5a14ffc03f2 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -89,8 +89,8 @@ describe('Monitoring store Getters', () => {
expect(getMetricStates()).toEqual([metricStates.OK]);
// Filtered by groups
- expect(getMetricStates(state.dashboard.panelGroups[0].key)).toEqual([metricStates.OK]);
- expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([]);
+ expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([metricStates.OK]);
+ expect(getMetricStates(state.dashboard.panelGroups[2].key)).toEqual([]);
});
it('on multiple metrics errors', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload);
@@ -118,18 +118,18 @@ describe('Monitoring store Getters', () => {
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture);
// An error in 2 groups
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
- metricId: groups[0].panels[1].metrics[0].metricId,
+ metricId: groups[1].panels[1].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
- metricId: groups[1].panels[0].metrics[0].metricId,
+ metricId: groups[2].panels[0].metrics[0].metricId,
});
expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]);
- expect(getMetricStates(groups[0].key)).toEqual([
+ expect(getMetricStates(groups[1].key)).toEqual([
metricStates.OK,
metricStates.UNKNOWN_ERROR,
]);
- expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]);
+ expect(getMetricStates(groups[2].key)).toEqual([metricStates.UNKNOWN_ERROR]);
});
});
});
@@ -210,13 +210,13 @@ describe('Monitoring store Getters', () => {
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode);
// First group has metrics
- expect(metricsWithData(state.dashboard.panelGroups[0].key)).toEqual([
+ expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([
mockedQueryResultFixture.metricId,
mockedQueryResultFixtureStatusCode.metricId,
]);
// Second group has no metrics
- expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([]);
+ expect(metricsWithData(state.dashboard.panelGroups[2].key)).toEqual([]);
});
});
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index a94de5494e5..5a79b8ef49c 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -32,12 +32,13 @@ describe('Monitoring mutations', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
const groups = getGroups();
- expect(groups[0].key).toBe('response-metrics-nginx-ingress-vts-0');
- expect(groups[1].key).toBe('response-metrics-nginx-ingress-1');
+ expect(groups[0].key).toBe('system-metrics-kubernetes-0');
+ expect(groups[1].key).toBe('response-metrics-nginx-ingress-vts-1');
+ expect(groups[2].key).toBe('response-metrics-nginx-ingress-2');
});
it('normalizes values', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
- const expectedLabel = '5xx Errors (%)';
+ const expectedLabel = 'Pod average (MB)';
const { label, queryRange } = getGroups()[0].panels[2].metrics[0];
expect(label).toEqual(expectedLabel);
@@ -51,7 +52,7 @@ describe('Monitoring mutations', () => {
expect(groups).toBeDefined();
expect(groups).toHaveLength(6);
- expect(groups[0].panels).toHaveLength(3);
+ expect(groups[0].panels).toHaveLength(7);
expect(groups[0].panels[0].metrics).toHaveLength(1);
expect(groups[0].panels[1].metrics).toHaveLength(1);
expect(groups[0].panels[2].metrics).toHaveLength(1);
@@ -65,9 +66,12 @@ describe('Monitoring mutations', () => {
const groups = getGroups();
expect(groups[0].panels[0].metrics[0].metricId).toEqual(
- 'undefined_response_metrics_nginx_ingress_throughput_status_code',
+ 'undefined_system_metrics_kubernetes_container_memory_total',
);
expect(groups[1].panels[0].metrics[0].metricId).toEqual(
+ 'undefined_response_metrics_nginx_ingress_throughput_status_code',
+ );
+ expect(groups[2].panels[0].metrics[0].metricId).toEqual(
'undefined_response_metrics_nginx_ingress_16_throughput_status_code',
);
});
@@ -135,7 +139,7 @@ describe('Monitoring mutations', () => {
},
];
const dashboard = metricsDashboardPayload;
- const getMetric = () => stateCopy.dashboard.panelGroups[0].panels[0].metrics[0];
+ const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0];
describe('REQUEST_METRIC_RESULT', () => {
beforeEach(() => {
diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
index 2092b9e9db8..28bf5bd3e92 100644
--- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
@@ -46,11 +46,9 @@ describe Banzai::Filter::InlineGrafanaMetricsFilter do
end
context 'when "panelId" parameter is missing' do
- let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis' }
+ let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis?from=1570397739557&to=1570484139557' }
- it 'leaves the markdown unchanged' do
- expect(unescape(doc.to_s)).to eq(input)
- end
+ it_behaves_like 'a metrics embed filter'
end
context 'when time window parameters are missing' do
@@ -86,6 +84,14 @@ describe Banzai::Filter::InlineGrafanaMetricsFilter do
end
end
+ context 'when no parameters are provided' do
+ let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis' }
+
+ it 'inserts a placeholder' do
+ expect(embed_url).to be_present
+ end
+ end
+
private
# Nokogiri escapes the URLs, but we don't care about that
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 7bc17b804df..9c2b202d5bb 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -703,6 +703,12 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
expect(project.services.where(template: true).count).to eq(0)
end
+ it 'does not import any instance services' do
+ expect(restored_project_json).to eq(true)
+
+ expect(project.services.where(instance: true).count).to eq(0)
+ end
+
it 'imports labels' do
create(:group_label, name: 'Another label', group: project.group)
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index bbd83975f11..91b88349ee0 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -459,6 +459,7 @@ Service:
- active
- properties
- template
+- instance
- push_events
- issues_events
- commit_events
diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
index 5c2ec6dae6b..e41004bb57e 100644
--- a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
@@ -11,8 +11,9 @@ describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do
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(:expected_dashboard) { JSON.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) }
- let(:dashboard) { described_class.new(project, {}, params).transform! }
+ subject(:dashboard) { described_class.new(project, {}, params).transform! }
let(:params) do
{
@@ -23,83 +24,34 @@ describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do
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
+ it { is_expected.to eq expected_dashboard }
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'
+ context 'when a panelId is not included in the grafana_url' do
+ before do
+ params[:grafana_url].gsub('&panelId=8', '')
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
+ it { is_expected.to eq expected_dashboard }
- context 'when query param "to" is not specified' do
+ context 'when there is also no valid panel in the dashboard' do
before do
- params[:grafana_url].gsub!('to=1570484139557', '')
+ params[:grafana_dashboard][:dashboard][:panels] = []
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'
+ it 'raises a processing error' do
+ expect { dashboard }.to raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError)
end
-
- it_behaves_like 'processing error'
end
+ 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'
+ context 'when an input is invalid' do
+ before do
+ params[:datasource][:access] = 'not-proxy'
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'
+ it 'raises a processing error' do
+ expect { dashboard }.to raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError)
end
end
end
diff --git a/spec/lib/grafana/validator_spec.rb b/spec/lib/grafana/validator_spec.rb
new file mode 100644
index 00000000000..603e27fd0c0
--- /dev/null
+++ b/spec/lib/grafana/validator_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Grafana::Validator 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(:panel) { grafana_dashboard[:dashboard][:panels].first }
+
+ let(:query_params) do
+ {
+ from: '1570397739557',
+ to: '1570484139557',
+ panelId: '8',
+ 'var-instance': 'localhost:9121'
+ }
+ end
+
+ describe 'validate!' do
+ shared_examples_for 'processing error' do |message|
+ it 'raises a processing error' do
+ expect { subject }
+ .to raise_error(::Grafana::Validator::Error, message)
+ end
+ end
+
+ subject { described_class.new(grafana_dashboard, datasource, panel, query_params).validate! }
+
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+
+ context 'when query param "from" is not specified' do
+ before do
+ query_params.delete(:from)
+ end
+
+ it_behaves_like 'processing error', 'Grafana query parameters must include from and to.'
+ end
+
+ context 'when query param "to" is not specified' do
+ before do
+ query_params.delete(:to)
+ end
+
+ it_behaves_like 'processing error', 'Grafana query parameters must include from and to.'
+ end
+
+ context 'when the panel is not provided' do
+ let(:panel) { nil }
+
+ it_behaves_like 'processing error', 'Panel type must be a line graph.'
+ end
+
+ context 'when the panel is not a graph' do
+ before do
+ panel[:type] = 'singlestat'
+ end
+
+ it_behaves_like 'processing error', 'Panel type must be a line graph.'
+ end
+
+ context 'when the panel is not a line graph' do
+ before do
+ panel[:lines] = false
+ end
+
+ it_behaves_like 'processing error', 'Panel type must be a line graph.'
+ end
+
+ context 'when the query dashboard includes undefined variables' do
+ before do
+ query_params.delete(:'var-instance')
+ end
+
+ it_behaves_like 'processing error', 'All Grafana variables must be defined in the query parameters.'
+ end
+
+ context 'when the expression contains unsupported global variables' do
+ before do
+ grafana_dashboard[:dashboard][:panels][0][:targets][0][:expr] = 'sum(important_metric[$__interval_ms])'
+ end
+
+ it_behaves_like 'processing error', "Prometheus must not include #{described_class::UNSUPPORTED_GRAFANA_GLOBAL_VARS}"
+ end
+
+ context 'when the datasource is not proxyable' do
+ before do
+ datasource[:access] = 'not-proxy'
+ end
+
+ it_behaves_like 'processing error', 'Only Prometheus datasources with proxy access in Grafana are supported.'
+ end
+
+ # Skipping datasource validation allows for checks to be
+ # run without a secondary call to Grafana API
+ context 'when the datasource is not provided' do
+ let(:datasource) { nil }
+
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'valid?' do
+ subject { described_class.new(grafana_dashboard, datasource, panel, query_params).valid? }
+
+ context 'with valid arguments' do
+ it { is_expected.to be true }
+ end
+
+ context 'with invalid arguments' do
+ let(:query_params) { {} }
+
+ it { is_expected.to be false }
+ end
+ end
+end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 2bc8bdaed7c..825e5f5902f 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -18,6 +18,20 @@ describe Service do
expect(build(:service, project_id: nil, template: false)).to be_invalid
end
+ it 'validates presence of project_id if not instance', :aggregate_failures do
+ expect(build(:service, project_id: nil, instance: true)).to be_valid
+ expect(build(:service, project_id: nil, instance: false)).to be_invalid
+ end
+
+ it 'validates absence of project_id if instance', :aggregate_failures do
+ expect(build(:service, project_id: nil, instance: true)).to be_valid
+ expect(build(:service, instance: true)).to be_invalid
+ end
+
+ it 'validates service is template or instance' do
+ expect(build(:service, project_id: nil, template: true, instance: true)).to be_invalid
+ end
+
context 'with an existing service template' do
before do
create(:service, type: 'Service', template: true)
@@ -27,6 +41,16 @@ describe Service do
expect(build(:service, type: 'Service', template: true)).to be_invalid
end
end
+
+ context 'with an existing instance service' do
+ before do
+ create(:service, :instance)
+ end
+
+ it 'validates only one service instance per type' do
+ expect(build(:service, :instance)).to be_invalid
+ end
+ end
end
describe 'Scopes' do
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 931b67b2950..33538703e92 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -33,11 +33,11 @@ describe Boards::Issues::ListService do
let!(:list1_issue3) { create(:labeled_issue, project: project, milestone: m1, labels: [development, p1]) }
let!(:list2_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [testing]) }
- let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
- let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
- let!(:closed_issue3) { create(:issue, :closed, project: project) }
- let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
- let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) }
+ let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug], closed_at: 1.day.ago) }
+ let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3], closed_at: 2.days.ago) }
+ let!(:closed_issue3) { create(:issue, :closed, project: project, closed_at: 1.week.ago) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1], closed_at: 1.year.ago) }
+ let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development], closed_at: 2.years.ago) }
let(:parent) { project }
@@ -94,11 +94,11 @@ describe Boards::Issues::ListService do
let!(:list1_issue3) { create(:labeled_issue, project: project1, milestone: m1, labels: [development, p1, p1_project1]) }
let!(:list2_issue1) { create(:labeled_issue, project: project1, milestone: m1, labels: [testing]) }
- let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
- let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3, p3_project]) }
- let!(:closed_issue3) { create(:issue, :closed, project: project1) }
- let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1, p1_project1]) }
- let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development]) }
+ let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug], closed_at: 1.day.ago) }
+ let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3, p3_project], closed_at: 2.days.ago) }
+ let!(:closed_issue3) { create(:issue, :closed, project: project1, closed_at: 1.week.ago) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1, p1_project1], closed_at: 1.year.ago) }
+ let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development], closed_at: 2.years.ago) }
before do
group.add_developer(user)
diff --git a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb
index ec1c58e5b67..756c4136059 100644
--- a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb
@@ -36,20 +36,22 @@ RSpec.shared_examples 'issues list service' do
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
- it 'returns closed issues when listing issues from Closed' do
- params = { board_id: board.id, id: closed.id }
+ it 'returns opened issues that have label list applied when listing issues from a label list' do
+ params = { board_id: board.id, id: list1.id }
issues = described_class.new(parent, user, params).execute
- expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1]
+ expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
end
+ end
- it 'returns opened issues that have label list applied when listing issues from a label list' do
- params = { board_id: board.id, id: list1.id }
+ context 'issues are ordered by date of closing' do
+ it 'returns closed issues when listing issues from Closed' do
+ params = { board_id: board.id, id: closed.id }
issues = described_class.new(parent, user, params).execute
- expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+ expect(issues).to eq [closed_issue1, closed_issue2, closed_issue3, closed_issue4, closed_issue5]
end
end
diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
index a4f81097ffa..48e4b4a18fd 100644
--- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -67,6 +67,7 @@ RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template,
it 'delegates commit creation to Files::CreateService', :aggregate_failures do
service_instance = instance_double(::Files::CreateService)
+ allow(::Gitlab::Metrics::Dashboard::Processor).to receive(:new).and_return(double(process: file_content_hash))
expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance)
expect(service_instance).to receive(:execute).and_return(status: :success)