Commit 7ac065d0 authored by Paweł Chojnacki's avatar Paweł Chojnacki

Merge branch 'master' into 'bump-prometheus-version-to-fix-text-representation-problem'

# Conflicts:
#   Gemfile.lock
parents b5f3903b d453bb86
...@@ -285,6 +285,7 @@ group :metrics do ...@@ -285,6 +285,7 @@ group :metrics do
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta5' gem 'prometheus-client-mmap', '~>0.7.0.beta5'
gem 'raindrops', '~> 0.18'
end end
group :development do group :development do
......
...@@ -658,7 +658,7 @@ GEM ...@@ -658,7 +658,7 @@ GEM
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.17.0) raindrops (0.18.0)
rake (10.5.0) rake (10.5.0)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
...@@ -1062,6 +1062,7 @@ DEPENDENCIES ...@@ -1062,6 +1062,7 @@ DEPENDENCIES
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9) rails-i18n (~> 4.0.9)
rainbow (~> 2.2) rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rdoc (~> 4.2) rdoc (~> 4.2)
recaptcha (~> 3.0) recaptcha (~> 3.0)
......
...@@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) { ...@@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) {
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
var isAuthorFilter;
isAuthorFilter = $('.js-author-search');
return _this.users(term, options, function(users) { return _this.users(term, options, function(users) {
// GitLabDropdownFilter returns this.instance // GitLabDropdownFilter returns this.instance
// GitLabDropdownRemote returns this.options.instance // GitLabDropdownRemote returns this.options.instance
......
...@@ -60,13 +60,13 @@ class UsersFinder ...@@ -60,13 +60,13 @@ class UsersFinder
end end
def by_external_identity(users) def by_external_identity(users)
return users unless current_user.admin? && params[:extern_uid] && params[:provider] return users unless current_user&.admin? && params[:extern_uid] && params[:provider]
users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid]))
end end
def by_external(users) def by_external(users)
return users = users.where.not(external: true) unless current_user.admin? return users = users.where.not(external: true) unless current_user&.admin?
return users unless params[:external] return users unless params[:external]
users.external users.external
......
...@@ -16,8 +16,8 @@ module FormHelper ...@@ -16,8 +16,8 @@ module FormHelper
end end
end end
def issue_dropdown_options(issuable, has_multiple_assignees = true) def issue_assignees_dropdown_options
options = { {
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee', title: 'Select assignee',
filter: true, filter: true,
...@@ -27,8 +27,8 @@ module FormHelper ...@@ -27,8 +27,8 @@ module FormHelper
first_user: current_user&.username, first_user: current_user&.username,
null_user: true, null_user: true,
current_user: true, current_user: true,
project_id: issuable.project.try(:id), project_id: @project.id,
field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", field_name: 'issue[assignee_ids][]',
default_label: 'Unassigned', default_label: 'Unassigned',
'max-select': 1, 'max-select': 1,
'dropdown-header': 'Assignee', 'dropdown-header': 'Assignee',
...@@ -38,13 +38,5 @@ module FormHelper ...@@ -38,13 +38,5 @@ module FormHelper
current_user_info: current_user.to_json(only: [:id, :name]) current_user_info: current_user.to_json(only: [:id, :name])
} }
} }
if has_multiple_assignees
options[:title] = 'Select assignee(s)'
options[:data][:'dropdown-header'] = 'Assignee(s)'
options[:data].delete(:'max-select')
end
options
end end
end end
...@@ -58,6 +58,11 @@ module GroupsHelper ...@@ -58,6 +58,11 @@ module GroupsHelper
IssuesFinder.new(current_user, group_id: group.id).execute IssuesFinder.new(current_user, group_id: group.id).execute
end end
def remove_group_message(group)
_("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") %
{ group_name: group.name }
end
private private
def group_title_link(group, hidable: false) def group_title_link(group, hidable: false)
......
...@@ -126,6 +126,18 @@ module SearchHelper ...@@ -126,6 +126,18 @@ module SearchHelper
search_path(options) search_path(options)
end end
def search_filter_input_options(type)
{
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
'project-id' => @project.id,
'username-params' => @users.to_json(only: [:id, :username]),
'base-endpoint' => namespace_project_path(@project.namespace, @project)
}
}
end
# Sanitize a HTML field for search display. Most tags are stripped out and the # Sanitize a HTML field for search display. Most tags are stripped out and the
# maximum length is set to 200 characters. # maximum length is set to 200 characters.
def search_md_sanitize(object, field) def search_md_sanitize(object, field)
......
...@@ -102,6 +102,14 @@ module Issuable ...@@ -102,6 +102,14 @@ module Issuable
def locking_enabled? def locking_enabled?
title_changed? || description_changed? title_changed? || description_changed?
end end
def allows_multiple_assignees?
false
end
def has_multiple_assignees?
assignees.count > 1
end
end end
module ClassMethods module ClassMethods
......
...@@ -197,11 +197,19 @@ class MergeRequest < ActiveRecord::Base ...@@ -197,11 +197,19 @@ class MergeRequest < ActiveRecord::Base
} }
end end
# This method is needed for compatibility with issues to not mess view and other code # These method are needed for compatibility with issues to not mess view and other code
def assignees def assignees
Array(assignee) Array(assignee)
end end
def assignee_ids
Array(assignee_id)
end
def assignee_ids=(ids)
write_attribute(:assignee_id, ids.last)
end
def assignee_or_author?(user) def assignee_or_author?(user)
author_id == user.id || assignee_id == user.id author_id == user.id || assignee_id == user.id
end end
......
require_dependency 'declarative_policy' require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base class BasePolicy < DeclarativePolicy::Base
include Gitlab::CurrentSettings
desc "User is an instance admin" desc "User is an instance admin"
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:admin) { @user&.admin? } condition(:admin) { @user&.admin? }
...@@ -10,4 +12,9 @@ class BasePolicy < DeclarativePolicy::Base ...@@ -10,4 +12,9 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:can_create_group) { @user&.can_create_group } condition(:can_create_group) { @user&.can_create_group }
desc "The application is restricted from public visibility"
condition(:restricted_public_level, scope: :global) do
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
end end
...@@ -11,10 +11,16 @@ class GlobalPolicy < BasePolicy ...@@ -11,10 +11,16 @@ class GlobalPolicy < BasePolicy
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:access_locked) { @user.access_locked? } condition(:access_locked) { @user.access_locked? }
rule { anonymous }.prevent_all rule { anonymous }.policy do
prevent :log_in
prevent :access_api
prevent :access_git
prevent :receive_notifications
prevent :use_quick_actions
prevent :create_group
end
rule { default }.policy do rule { default }.policy do
enable :read_users_list
enable :log_in enable :log_in
enable :access_api enable :access_api
enable :access_git enable :access_git
...@@ -37,4 +43,8 @@ class GlobalPolicy < BasePolicy ...@@ -37,4 +43,8 @@ class GlobalPolicy < BasePolicy
rule { access_locked }.policy do rule { access_locked }.policy do
prevent :log_in prevent :log_in
end end
rule { ~restricted_public_level }.policy do
enable :read_users_list
end
end end
class UserPolicy < BasePolicy class UserPolicy < BasePolicy
include Gitlab::CurrentSettings
desc "The application is restricted from public visibility"
condition(:restricted_public_level, scope: :global) do
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
desc "The current user is the user in question" desc "The current user is the user in question"
condition(:user_is_self, score: 0) { @subject == @user } condition(:user_is_self, score: 0) { @subject == @user }
......
...@@ -92,9 +92,12 @@ module QuickActions ...@@ -92,9 +92,12 @@ module QuickActions
desc 'Assign' desc 'Assign'
explanation do |users| explanation do |users|
"Assigns #{users.first.to_reference}." if users.any? users = issuable.allows_multiple_assignees? ? users : users.take(1)
"Assigns #{users.map(&:to_reference).to_sentence}."
end
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
end end
params '@user'
condition do condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
...@@ -104,28 +107,69 @@ module QuickActions ...@@ -104,28 +107,69 @@ module QuickActions
command :assign do |users| command :assign do |users|
next if users.empty? next if users.empty?
if issuable.is_a?(Issue) @updates[:assignee_ids] =
@updates[:assignee_ids] = [users.last.id] if issuable.allows_multiple_assignees?
issuable.assignees.pluck(:id) + users.map(&:id)
else
[users.last.id]
end
end
desc do
if issuable.allows_multiple_assignees?
'Remove all or specific assignee(s)'
else else
@updates[:assignee_id] = users.last.id 'Remove assignee'
end end
end end
desc 'Remove assignee'
explanation do explanation do
"Removes assignee #{issuable.assignees.first.to_reference}." "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
end
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
end end
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.assignees.any? && issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :unassign do parse_params do |unassign_param|
if issuable.is_a?(Issue) # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
@updates[:assignee_ids] = [] extract_users(unassign_param) if issuable.allows_multiple_assignees?
else end
@updates[:assignee_id] = nil command :unassign do |users = nil|
end @updates[:assignee_ids] =
if users&.any?
issuable.assignees.pluck(:id) - users.map(&:id)
else
[]
end
end
desc do
"Change assignee#{'(s)' if issuable.allows_multiple_assignees?}"
end
explanation do |users|
users = issuable.allows_multiple_assignees? ? users : users.take(1)
"Change #{'assignee'.pluralize(users.size)} to #{users.map(&:to_reference).to_sentence}."
end
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
end
condition do
issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
parse_params do |assignee_param|
extract_users(assignee_param)
end
command :reassign do |users|
@updates[:assignee_ids] =
if issuable.allows_multiple_assignees?
users.map(&:id)
else
[users.last.id]
end
end end
desc 'Set milestone' desc 'Set milestone'
......
...@@ -45,10 +45,13 @@ ...@@ -45,10 +45,13 @@
.panel.panel-danger .panel.panel-danger
.panel-heading Remove group .panel-heading Remove group
.panel-body .panel-body
%p = form_tag(@group, method: :delete) do
Removing group will cause all child projects and resources to be removed. %p
%br Removing group will cause all child projects and resources to be removed.
%strong Removed group can not be restored! %br
%strong Removed group can not be restored!
.form-actions .form-actions
= link_to 'Remove group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" = button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
= render 'shared/confirm_modal', phrase: @group.path
...@@ -19,10 +19,11 @@ ...@@ -19,10 +19,11 @@
":data-name" => "assignee.name", ":data-name" => "assignee.name",
":data-username" => "assignee.username" } ":data-username" => "assignee.username" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } }, - dropdown_options = issue_assignees_dropdown_options
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee = dropdown_options[:title]
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
= dropdown_title("Assign to") = dropdown_title("Assign to")
......
- @no_container = true - @no_container = true
- page_title "Charts", "Pipelines" - page_title _("Charts"), _("Pipelines")
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs') = page_specific_javascript_bundle_tag('graphs')
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%div{ class: container_class } %div{ class: container_class }
.sub-header-block .sub-header-block
.oneline .oneline
A collection of graphs for Continuous Integration = _("A collection of graphs regarding Continuous Integration")
#charts.ci-charts #charts.ci-charts
.row .row
......
%h4 Overall stats %h4= s_("PipelineCharts|Overall statistics")
%ul %ul
%li %li
Total: = s_("PipelineCharts|Total:")
%strong= pluralize @counts[:total], 'pipeline' %strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total]
%li %li
Successful: = s_("PipelineCharts|Successful:")
%strong= pluralize @counts[:success], 'pipeline' %strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success]
%li %li
Failed: = s_("PipelineCharts|Failed:")
%strong= pluralize @counts[:failed], 'pipeline' %strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed]
%li %li
Success ratio: = s_("PipelineCharts|Success ratio:")
%strong %strong
#{success_ratio(@counts)}% #{success_ratio(@counts)}%
%div %div
%p.light %p.light
Commit duration in minutes for last 30 commits = _("Commit duration in minutes for last 30 commits")
%canvas#build_timesChart{ height: 200 } %canvas#build_timesChart{ height: 200 }
......
%h4 Pipelines charts %h4= _("Pipelines charts")
%p %p
&nbsp; &nbsp;
%span.cgreen %span.cgreen
= icon("circle") = icon("circle")
success = s_("Pipeline|success")
&nbsp; &nbsp;
%span.cgray %span.cgray
= icon("circle") = icon("circle")
all = s_("Pipeline|all")
.prepend-top-default .prepend-top-default
%p.light %p.light
Jobs for last week = _("Jobs for last week")
(#{date_from_to(Date.today - 7.days, Date.today)}) (#{date_from_to(Date.today - 7.days, Date.today)})
%canvas#weekChart{ height: 200 } %canvas#weekChart{ height: 200 }
.prepend-top-default .prepend-top-default
%p.light %p.light
Jobs for last month = _("Jobs for last month")
(#{date_from_to(Date.today - 30.days, Date.today)}) (#{date_from_to(Date.today - 30.days, Date.today)})
%canvas#monthChart{ height: 200 } %canvas#monthChart{ height: 200 }
.prepend-top-default .prepend-top-default
%p.light %p.light
Jobs for last year = _("Jobs for last year")
%canvas#yearChart.padded{ height: 250 } %canvas#yearChart.padded{ height: 250 }
- [:week, :month, :year].each do |scope| - [:week, :month, :year].each do |scope|
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %li.input-token
%input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } %input.form-control.filtered-search{ search_filter_input_options(type) }
= icon('filter') = icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
......
...@@ -37,19 +37,20 @@ ...@@ -37,19 +37,20 @@
- issuable.assignees.each do |assignee| - issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
- title = 'Select assignee' - title = 'Select assignee'
- if issuable.is_a?(Issue) - if issuable.is_a?(Issue)
- unless issuable.assignees.any? - unless issuable.assignees.any?
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
- dropdown_options = issue_assignees_dropdown_options
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data' - options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" } - data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" }
- data[:multi_select] = true - data[:multi_select] = true
- data['dropdown-title'] = title - data['dropdown-title'] = title
- data['dropdown-header'] = 'Assignee' - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- data['max-select'] = 1 - data['max-select'] = dropdown_options[:data][:'max-select']
- options[:data].merge!(data) - options[:data].merge!(data)
= dropdown_tag(title, options: options) = dropdown_tag(title, options: options)
...@@ -7,5 +7,5 @@ ...@@ -7,5 +7,5 @@
- if issuable.assignees.length === 0 - if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' } = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_dropdown_options(issuable,false)) = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options)
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}" = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
---
title: "Remove group modal like remove project modal (requires typing + confirmation)"
merge_request: 12569
author: Diego Souza
---
title: Allow unauthenticated access to the /api/v4/users API
merge_request: 12445
author:
...@@ -543,6 +543,10 @@ production: &base ...@@ -543,6 +543,10 @@ production: &base
# enabled: true # enabled: true
# host: localhost # host: localhost
# port: 3808 # port: 3808
prometheus:
# Time between sampling of unicorn socket metrics, in seconds
# unicorn_sampler_interval: 10
# #
# 5. Extra customization # 5. Extra customization
......
...@@ -494,6 +494,12 @@ Settings.webpack.dev_server['enabled'] ||= false ...@@ -494,6 +494,12 @@ Settings.webpack.dev_server['enabled'] ||= false
Settings.webpack.dev_server['host'] ||= 'localhost' Settings.webpack.dev_server['host'] ||= 'localhost'
Settings.webpack.dev_server['port'] ||= 3808 Settings.webpack.dev_server['port'] ||= 3808
#
# Prometheus metrics settings
#
Settings['prometheus'] ||= Settingslogic.new({})
Settings.prometheus['unicorn_sampler_interval'] ||= 10
# #
# Testing settings # Testing settings
# #
......
...@@ -119,6 +119,13 @@ def instrument_classes(instrumentation) ...@@ -119,6 +119,13 @@ def instrument_classes(instrumentation)
end end
# rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/AbcSize
Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.prometheus.unicorn_sampler_interval).start
Gitlab::Application.configure do |config|
# 0 should be Sentry to catch errors in this middleware
config.middleware.insert(1, Gitlab::Metrics::ConnectionRackMiddleware)
end
if Gitlab::Metrics.enabled? if Gitlab::Metrics.enabled?
require 'pathname' require 'pathname'
require 'influxdb' require 'influxdb'
...@@ -175,7 +182,7 @@ if Gitlab::Metrics.enabled? ...@@ -175,7 +182,7 @@ if Gitlab::Metrics.enabled?
GC::Profiler.enable GC::Profiler.enable
Gitlab::Metrics::Sampler.new.start Gitlab::Metrics::InfluxSampler.initialize_instance.start
module TrackNewRedisConnections module TrackNewRedisConnections
def connect(*args) def connect(*args)
......
This diff is collapsed.
...@@ -4,10 +4,13 @@ module API ...@@ -4,10 +4,13 @@ module API
before do before do
allow_access_with_scope :read_user if request.get? allow_access_with_scope :read_user if request.get?
authenticate!
end end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
before do
authenticate_non_get!
end
helpers do helpers do
def find_user(params) def find_user(params)
id = params[:user_id] || params[:id] id = params[:user_id] || params[:id]
...@@ -51,15 +54,22 @@ module API ...@@ -51,15 +54,22 @@ module API
use :pagination use :pagination
end end
get do get do
unless can?(current_user, :read_users_list)
render_api_error!("Not authorized.", 403)
end
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
users = UsersFinder.new(current_user, params).execute users = UsersFinder.new(current_user, params).execute
entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic authorized = can?(current_user, :read_users_list)
# When `current_user` is not present, require that the `username`
# parameter is passed, to prevent an unauthenticated user from accessing
# a list of all the users on the GitLab instance. `UsersFinder` performs
# an exact match on the `username` parameter, so we are guaranteed to
# get either 0 or 1 `users` here.
authorized &&= params[:username].present? if current_user.blank?
forbidden!("Not authorized to access /api/v4/users") unless authorized
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
present paginate(users), with: entity present paginate(users), with: entity
end end
...@@ -398,6 +408,10 @@ module API ...@@ -398,6 +408,10 @@ module API
end end
resource :user do resource :user do
before do
authenticate!
end
desc 'Get the currently authenticated user' do desc 'Get the currently authenticated user' do
success Entities::UserPublic success Entities::UserPublic
end end
......
require 'logger'
module Gitlab
module Metrics
class BaseSampler
def self.initialize_instance(*args)
raise "#{name} singleton instance already initialized" if @instance
@instance = new(*args)
at_exit(&@instance.method(:stop))
@instance
end
def self.instance
@instance
end
attr_reader :running
# interval - The sampling interval in seconds.
def initialize(interval)
interval_half = interval.to_f / 2
@interval = interval
@interval_steps = (-interval_half..interval_half).step(0.1).to_a
@mutex = Mutex.new
end
def enabled?
true
end
def start
return unless enabled?
@mutex.synchronize do
return if running
@running = true
@thread = Thread.new do
sleep(sleep_interval)
while running
safe_sample
sleep(sleep_interval)
end
end
end
end
def stop
@mutex.synchronize do
return unless running
@running = false
if @thread
@thread.wakeup if @thread.alive?
@thread.join
@thread = nil
end
end
end
def safe_sample
sample
rescue => e
Rails.logger.warn("#{self.class}: #{e}, stopping")
stop
end
def sample
raise NotImplementedError
end
# Returns the sleep interval with a random adjustment.
#
# The random adjustment is put in place to ensure we:
#
# 1. Don't generate samples at the exact same interval every time (thus
# potentially missing anything that happens in between samples).
# 2. Don't sample data at the same interval two times in a row.
def sleep_interval
while step = @interval_steps.sample
if step != @last_step
@last_step = step
return @interval + @last_step
end
end
end
end
end
end
module Gitlab
module Metrics
class ConnectionRackMiddleware
def initialize(app)
@app = app
end
def self.rack_request_count
@rack_request_count ||= Gitlab::Metrics.counter(:rack_request, 'Rack request count')
end
def self.rack_response_count
@rack_response_count ||= Gitlab::Metrics.counter(:rack_response, 'Rack response count')
end
def self.rack_uncaught_errors_count
@rack_uncaught_errors_count ||= Gitlab::Metrics.counter(:rack_uncaught_errors, 'Rack connections handling uncaught errors count')
end
def self.rack_execution_time
@rack_execution_time ||= Gitlab::Metrics.histogram(:rack_execution_time, 'Rack connection handling execution time',
{}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 1.5, 2, 2.5, 3, 5, 7, 10])
end
def call(env)
method = env['REQUEST_METHOD'].downcase
started = Time.now.to_f
begin
ConnectionRackMiddleware.rack_request_count.increment(method: method)
status, headers, body = @app.call(env)
ConnectionRackMiddleware.rack_response_count.increment(method: method, status: status)
[status, headers, body]
rescue
ConnectionRackMiddleware.rack_uncaught_errors_count.increment
raise
ensure
elapsed = Time.now.to_f - started
ConnectionRackMiddleware.rack_execution_time.observe({}, elapsed)
end
end
end
end
end
...@@ -5,14 +5,11 @@ module Gitlab ...@@ -5,14 +5,11 @@ module Gitlab
# This class is used to gather statistics that can't be directly associated # This class is used to gather statistics that can't be directly associated
# with a transaction such as system memory usage, garbage collection # with a transaction such as system memory usage, garbage collection
# statistics, etc. # statistics, etc.
class Sampler class InfluxSampler < BaseSampler
# interval - The sampling interval in seconds. # interval - The sampling interval in seconds.
def initialize(interval = Metrics.settings[:sample_interval]) def initialize(interval = Metrics.settings[:sample_interval])
interval_half = interval.to_f / 2 super(interval)
@last_step = nil
@interval = interval
@interval_steps = (-interval_half..interval_half).step(0.1).to_a
@last_step = nil
@metrics = [] @metrics = []
...@@ -26,18 +23,6 @@ module Gitlab ...@@ -26,18 +23,6 @@ module Gitlab
end end
end end
def start
Thread.new do
Thread.current.abort_on_exception = true
loop do
sleep(sleep_interval)
sample
end
end
end
def sample def sample
sample_memory_usage sample_memory_usage
sample_file_descriptors sample_file_descriptors
...@@ -86,7 +71,7 @@ module Gitlab ...@@ -86,7 +71,7 @@ module Gitlab
end end
def sample_gc def sample_gc
time = GC::Profiler.total_time * 1000.0 time = GC::Profiler.total_time * 1000.0
stats = GC.stat.merge(total_time: time) stats = GC.stat.merge(total_time: time)
# We want the difference of GC runs compared to the last sample, not the # We want the difference of GC runs compared to the last sample, not the
...@@ -111,23 +96,6 @@ module Gitlab ...@@ -111,23 +96,6 @@ module Gitlab
def sidekiq? def sidekiq?
Sidekiq.server? Sidekiq.server?
end end
# Returns the sleep interval with a random adjustment.
#
# The random adjustment is put in place to ensure we:
#
# 1. Don't generate samples at the exact same interval every time (thus
# potentially missing anything that happens in between samples).
# 2. Don't sample data at the same interval two times in a row.
def sleep_interval
while step = @interval_steps.sample
if step != @last_step
@last_step = step
return @interval + @last_step
end
end
end
end end
end end
end end
...@@ -29,8 +29,8 @@ module Gitlab ...@@ -29,8 +29,8 @@ module Gitlab
provide_metric(name) || registry.summary(name, docstring, base_labels) provide_metric(name) || registry.summary(name, docstring, base_labels)
end end
def gauge(name, docstring, base_labels = {}) def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
provide_metric(name) || registry.gauge(name, docstring, base_labels) provide_metric(name) || registry.gauge(name, docstring, base_labels, multiprocess_mode)
end end
def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS) def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS)
......
module Gitlab
module Metrics
class UnicornSampler < BaseSampler
def initialize(interval)
super(interval)
end
def unicorn_active_connections
@unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
end
def unicorn_queued_connections
@unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
end
def enabled?
# Raindrops::Linux.tcp_listener_stats is only present on Linux
unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats)
end
def sample
Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active)
unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued)
end
Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active)
unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued)
end
end
private
def tcp_listeners
@tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
end
def unix_listeners
@unix_listeners ||= Unicorn.listener_names - tcp_listeners
end
def unicorn_with_listeners?
defined?(Unicorn) && Unicorn.listener_names.any?
end
end
end
end
...@@ -17,9 +17,27 @@ msgstr "" ...@@ -17,9 +17,27 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"\n" "\n"
msgid "%d additional commit has been omitted to prevent performance issues."
msgid_plural "%d additional commits have been omitted to prevent performance issues."
msgstr[0] ""
msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] ""
msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}" msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "" msgstr ""
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
msgid "About auto deploy" msgid "About auto deploy"
msgstr "" msgstr ""
...@@ -61,9 +79,24 @@ msgstr[1] "" ...@@ -61,9 +79,24 @@ msgstr[1] ""
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "" msgstr ""
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
msgid "BranchSwitcherTitle|Switch branch"
msgstr ""
msgid "Branches" msgid "Branches"
msgstr "" msgstr ""
msgid "Browse Directory"
msgstr ""
msgid "Browse File"
msgstr ""
msgid "Browse Files"
msgstr ""
msgid "Browse files" msgid "Browse files"
msgstr "" msgstr ""
...@@ -159,6 +192,9 @@ msgid_plural "Commits" ...@@ -159,6 +192,9 @@ msgid_plural "Commits"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Commit duration in minutes for last 30 commits"
msgstr ""
msgid "Commit message" msgid "Commit message"
msgstr "" msgstr ""
...@@ -171,6 +207,9 @@ msgstr "" ...@@ -171,6 +207,9 @@ msgstr ""
msgid "Commits" msgid "Commits"
msgstr "" msgstr ""
msgid "Commits feed"
msgstr ""
msgid "Commits|History" msgid "Commits|History"
msgstr "" msgstr ""
...@@ -195,6 +234,9 @@ msgstr "" ...@@ -195,6 +234,9 @@ msgstr ""
msgid "Create New Directory" msgid "Create New Directory"
msgstr "" msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
msgid "Create directory" msgid "Create directory"
msgstr "" msgstr ""
...@@ -213,6 +255,9 @@ msgstr "" ...@@ -213,6 +255,9 @@ msgstr ""
msgid "CreateTag|Tag" msgid "CreateTag|Tag"
msgstr "" msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
msgid "Cron Timezone" msgid "Cron Timezone"
msgstr "" msgstr ""
...@@ -323,6 +368,9 @@ msgstr "" ...@@ -323,6 +368,9 @@ msgstr ""
msgid "Files" msgid "Files"
msgstr "" msgstr ""
msgid "Filter by commit message"
msgstr ""
msgid "Find by path" msgid "Find by path"
msgstr "" msgstr ""
...@@ -370,6 +418,15 @@ msgstr "" ...@@ -370,6 +418,15 @@ msgstr ""
msgid "Introducing Cycle Analytics" msgid "Introducing Cycle Analytics"
msgstr "" msgstr ""
msgid "Jobs for last month"
msgstr ""
msgid "Jobs for last week"
msgstr ""
msgid "Jobs for last year"
msgstr ""
msgid "LFSStatus|Disabled" msgid "LFSStatus|Disabled"
msgstr "" msgstr ""
...@@ -535,6 +592,21 @@ msgstr "" ...@@ -535,6 +592,21 @@ msgstr ""
msgid "Pipeline Schedules" msgid "Pipeline Schedules"
msgstr "" msgstr ""
msgid "PipelineCharts|Failed:"
msgstr ""
msgid "PipelineCharts|Overall statistics"
msgstr ""
msgid "PipelineCharts|Success ratio:"
msgstr ""
msgid "PipelineCharts|Successful:"
msgstr ""
msgid "PipelineCharts|Total:"
msgstr ""
msgid "PipelineSchedules|Activated" msgid "PipelineSchedules|Activated"
msgstr "" msgstr ""
...@@ -565,6 +637,18 @@ msgstr "" ...@@ -565,6 +637,18 @@ msgstr ""
msgid "PipelineSheduleIntervalPattern|Custom" msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "" msgstr ""
msgid "Pipelines"
msgstr ""
msgid "Pipelines charts"
msgstr ""
msgid "Pipeline|all"
msgstr ""
msgid "Pipeline|success"
msgstr ""
msgid "Pipeline|with stage" msgid "Pipeline|with stage"
msgstr "" msgstr ""
...@@ -688,7 +772,7 @@ msgstr "" ...@@ -688,7 +772,7 @@ msgstr ""
msgid "Select target branch" msgid "Select target branch"
msgstr "" msgstr ""
msgid "Set a password on your account to pull or push via %{protocol}" msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "" msgstr ""
msgid "Set up CI" msgid "Set up CI"
...@@ -714,10 +798,7 @@ msgstr "" ...@@ -714,10 +798,7 @@ msgstr ""
msgid "StarProject|Star" msgid "StarProject|Star"
msgstr "" msgstr ""
msgid "Start a %{new_merge_request} with these changes" msgid "Start a %{new_merge_request} with these changes"
msgstr ""
msgid "Start a <strong>new merge request</strong> with these changes"
msgstr "" msgstr ""
msgid "Switch branch/tag" msgid "Switch branch/tag"
...@@ -948,9 +1029,15 @@ msgstr "" ...@@ -948,9 +1029,15 @@ msgstr ""
msgid "Upload file" msgid "Upload file"
msgstr "" msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
msgid "Use your global notification setting" msgid "Use your global notification setting"
msgstr "" msgstr ""
msgid "View open merge request"
msgstr ""
msgid "VisibilityLevel|Internal" msgid "VisibilityLevel|Internal"
msgstr "" msgstr ""
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-19 15:50-0500\n" "POT-Creation-Date: 2017-06-28 13:32+0200\n"
"PO-Revision-Date: 2017-06-19 15:50-0500\n" "PO-Revision-Date: 2017-06-28 13:32+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -18,7 +18,7 @@ msgstr "" ...@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid "%d additional commit have been omitted to prevent performance issues." msgid "%d additional commit has been omitted to prevent performance issues."
msgid_plural "%d additional commits have been omitted to prevent performance issues." msgid_plural "%d additional commits have been omitted to prevent performance issues."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
...@@ -31,6 +31,14 @@ msgstr[1] "" ...@@ -31,6 +31,14 @@ msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}" msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "" msgstr ""
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
msgid "About auto deploy" msgid "About auto deploy"
msgstr "" msgstr ""
...@@ -185,6 +193,9 @@ msgid_plural "Commits" ...@@ -185,6 +193,9 @@ msgid_plural "Commits"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Commit duration in minutes for last 30 commits"
msgstr ""
msgid "Commit message" msgid "Commit message"
msgstr "" msgstr ""
...@@ -224,6 +235,9 @@ msgstr "" ...@@ -224,6 +235,9 @@ msgstr ""
msgid "Create New Directory" msgid "Create New Directory"
msgstr "" msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
msgid "Create directory" msgid "Create directory"
msgstr "" msgstr ""
...@@ -242,6 +256,9 @@ msgstr "" ...@@ -242,6 +256,9 @@ msgstr ""
msgid "CreateTag|Tag" msgid "CreateTag|Tag"
msgstr "" msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
msgid "Cron Timezone" msgid "Cron Timezone"
msgstr "" msgstr ""
...@@ -402,6 +419,15 @@ msgstr "" ...@@ -402,6 +419,15 @@ msgstr ""
msgid "Introducing Cycle Analytics" msgid "Introducing Cycle Analytics"
msgstr "" msgstr ""
msgid "Jobs for last month"
msgstr ""
msgid "Jobs for last week"
msgstr ""
msgid "Jobs for last year"
msgstr ""
msgid "LFSStatus|Disabled" msgid "LFSStatus|Disabled"
msgstr "" msgstr ""
...@@ -567,6 +593,21 @@ msgstr "" ...@@ -567,6 +593,21 @@ msgstr ""
msgid "Pipeline Schedules" msgid "Pipeline Schedules"
msgstr "" msgstr ""
msgid "PipelineCharts|Failed:"
msgstr ""
msgid "PipelineCharts|Overall statistics"
msgstr ""
msgid "PipelineCharts|Success ratio:"
msgstr ""
msgid "PipelineCharts|Successful:"
msgstr ""
msgid "PipelineCharts|Total:"
msgstr ""
msgid "PipelineSchedules|Activated" msgid "PipelineSchedules|Activated"
msgstr "" msgstr ""
...@@ -597,6 +638,18 @@ msgstr "" ...@@ -597,6 +638,18 @@ msgstr ""
msgid "PipelineSheduleIntervalPattern|Custom" msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "" msgstr ""
msgid "Pipelines"
msgstr ""
msgid "Pipelines charts"
msgstr ""
msgid "Pipeline|all"
msgstr ""
msgid "Pipeline|success"
msgstr ""
msgid "Pipeline|with stage" msgid "Pipeline|with stage"
msgstr "" msgstr ""
...@@ -720,7 +773,7 @@ msgstr "" ...@@ -720,7 +773,7 @@ msgstr ""
msgid "Select target branch" msgid "Select target branch"
msgstr "" msgstr ""
msgid "Set a password on your account to pull or push via %{protocol}" msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "" msgstr ""
msgid "Set up CI" msgid "Set up CI"
......
...@@ -135,7 +135,7 @@ feature 'Group', feature: true do ...@@ -135,7 +135,7 @@ feature 'Group', feature: true do
expect(page).not_to have_content('secret-group') expect(page).not_to have_content('secret-group')
end end
describe 'group edit' do describe 'group edit', js: true do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:path) { edit_group_path(group) } let(:path) { edit_group_path(group) }
let(:new_name) { 'new-name' } let(:new_name) { 'new-name' }
...@@ -157,8 +157,8 @@ feature 'Group', feature: true do ...@@ -157,8 +157,8 @@ feature 'Group', feature: true do
end end
it 'removes group' do it 'removes group' do
click_link 'Remove group' expect { remove_with_confirm('Remove group', group.path) }.to change {Group.count}.by(-1)
expect(group.members.all.count).to be_zero
expect(page).to have_content "scheduled for deletion" expect(page).to have_content "scheduled for deletion"
end end
end end
...@@ -212,4 +212,10 @@ feature 'Group', feature: true do ...@@ -212,4 +212,10 @@ feature 'Group', feature: true do
expect(page).to have_content(nested_group.name) expect(page).to have_content(nested_group.name)
end end
end end
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
click_button 'Confirm'
end
end end
...@@ -31,8 +31,8 @@ describe 'New/edit issue', :feature, :js do ...@@ -31,8 +31,8 @@ describe 'New/edit issue', :feature, :js do
# the original method, resulting in infinite recurison when called. # the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes. # This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually. # To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options) original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args| allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args) options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2 options[:data][:per_page] = 2
......
require 'spec_helper' require 'spec_helper'
require_relative '../../config/initializers/8_metrics'
describe 'instrument_classes', lib: true do describe 'instrument_classes', lib: true do
let(:config) { double(:config) } let(:config) { double(:config) }
let(:unicorn_sampler) { double(:unicorn_sampler) }
let(:influx_sampler) { double(:influx_sampler) }
before do before do
allow(config).to receive(:instrument_method) allow(config).to receive(:instrument_method)
allow(config).to receive(:instrument_methods) allow(config).to receive(:instrument_methods)
allow(config).to receive(:instrument_instance_method) allow(config).to receive(:instrument_instance_method)
allow(config).to receive(:instrument_instance_methods) allow(config).to receive(:instrument_instance_methods)
allow(Gitlab::Metrics::UnicornSampler).to receive(:initialize_instance).and_return(unicorn_sampler)
allow(Gitlab::Metrics::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler)
allow(unicorn_sampler).to receive(:start)
allow(influx_sampler).to receive(:start)
allow(Gitlab::Application).to receive(:configure)
end end
it 'can autoload and instrument all files' do it 'can autoload and instrument all files' do
require_relative '../../config/initializers/8_metrics'
expect { instrument_classes(config) }.not_to raise_error expect { instrument_classes(config) }.not_to raise_error
end end
end end
...@@ -97,30 +97,40 @@ describe Gitlab::HealthChecks::FsShardsCheck do ...@@ -97,30 +97,40 @@ describe Gitlab::HealthChecks::FsShardsCheck do
}.with_indifferent_access }.with_indifferent_access
end end
it { is_expected.to all(have_attributes(labels: { shard: :default })) } # Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128
around(:each) do |example| # rubocop:disable RSpec/AroundBlock
times_to_try = ENV['CI'] ? 4 : 1
example.run_with_retry retry: times_to_try
end
it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) } it 'provides metrics' do
it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) } expect(subject).to all(have_attributes(labels: { shard: :default }))
it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) } expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0))
it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0))
it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0))
end
end end
context 'storage points to directory that has both read and write rights' do context 'storage points to directory that has both read and write rights' do
before do before do
FileUtils.chmod_R(0755, tmp_dir) FileUtils.chmod_R(0755, tmp_dir)
end end
it { is_expected.to all(have_attributes(labels: { shard: :default })) }
it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) } it 'provides metrics' do
it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) } expect(subject).to all(have_attributes(labels: { shard: :default }))
it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) }
it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1))
it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 1))
it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 1))
expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0))
expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0))
expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0))
end
end end
end end
end end
......
require 'spec_helper'
describe Gitlab::Metrics::ConnectionRackMiddleware do
let(:app) { double('app') }
subject { described_class.new(app) }
around do |example|
Timecop.freeze { example.run }
end
describe '#call' do
let(:status) { 100 }
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:stack_result) { [status, {}, 'body'] }
before do
allow(app).to receive(:call).and_return(stack_result)
end
context '@app.call succeeds with 200' do
before do
allow(app).to receive(:call).and_return([200, nil, nil])
end
it 'increments response count with status label' do
expect(described_class).to receive_message_chain(:rack_response_count, :increment).with(include(status: 200, method: 'get'))
subject.call(env)
end
it 'increments requests count' do
expect(described_class).to receive_message_chain(:rack_request_count, :increment).with(method: 'get')
subject.call(env)
end
it 'measures execution time' do
execution_time = 10
allow(app).to receive(:call) do |*args|
Timecop.freeze(execution_time.seconds)
end
expect(described_class).to receive_message_chain(:rack_execution_time, :observe).with({}, execution_time)
subject.call(env)
end
end
context '@app.call throws exception' do
let(:rack_response_count) { double('rack_response_count') }
before do
allow(app).to receive(:call).and_raise(StandardError)
allow(described_class).to receive(:rack_response_count).and_return(rack_response_count)
end
it 'increments exceptions count' do
expect(described_class).to receive_message_chain(:rack_uncaught_errors_count, :increment)
expect { subject.call(env) }.to raise_error(StandardError)
end
it 'increments requests count' do
expect(described_class).to receive_message_chain(:rack_request_count, :increment).with(method: 'get')
expect { subject.call(env) }.to raise_error(StandardError)
end
it "does't increment response count" do
expect(described_class.rack_response_count).not_to receive(:increment)
expect { subject.call(env) }.to raise_error(StandardError)
end
it 'measures execution time' do
execution_time = 10
allow(app).to receive(:call) do |*args|
Timecop.freeze(execution_time.seconds)
raise StandardError
end
expect(described_class).to receive_message_chain(:rack_execution_time, :observe).with({}, execution_time)
expect { subject.call(env) }.to raise_error(StandardError)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Metrics::Sampler do describe Gitlab::Metrics::InfluxSampler do
let(:sampler) { described_class.new(5) } let(:sampler) { described_class.new(5) }
after do after do
...@@ -8,10 +8,10 @@ describe Gitlab::Metrics::Sampler do ...@@ -8,10 +8,10 @@ describe Gitlab::Metrics::Sampler do
end end
describe '#start' do describe '#start' do
it 'gathers a sample at a given interval' do it 'runs once and gathers a sample at a given interval' do
expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)) expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice
expect(sampler).to receive(:sample) expect(sampler).to receive(:sample).once
expect(sampler).to receive(:loop).and_yield expect(sampler).to receive(:running).and_return(false, true, false)
sampler.start.join sampler.start.join
end end
......
require 'spec_helper'
describe Gitlab::Metrics::UnicornSampler do
subject { described_class.new(1.second) }
describe '#sample' do
let(:unicorn) { double('unicorn') }
let(:raindrops) { double('raindrops') }
let(:stats) { double('stats') }
before do
stub_const('Unicorn', unicorn)
stub_const('Raindrops::Linux', raindrops)
allow(raindrops).to receive(:unix_listener_stats).and_return({})
allow(raindrops).to receive(:tcp_listener_stats).and_return({})
end
context 'unicorn listens on unix sockets' do
let(:socket_address) { '/some/sock' }
let(:sockets) { [socket_address] }
before do
allow(unicorn).to receive(:listener_names).and_return(sockets)
end
it 'samples socket data' do
expect(raindrops).to receive(:unix_listener_stats).with(sockets)
subject.sample
end
context 'stats collected' do
before do
allow(stats).to receive(:active).and_return('active')
allow(stats).to receive(:queued).and_return('queued')
allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats })
end
it 'updates metrics type unix and with addr' do
labels = { type: 'unix', address: socket_address }
expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
subject.sample
end
end
end
context 'unicorn listens on tcp sockets' do
let(:tcp_socket_address) { '0.0.0.0:8080' }
let(:tcp_sockets) { [tcp_socket_address] }
before do
allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
end
it 'samples socket data' do
expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets)
subject.sample
end
context 'stats collected' do
before do
allow(stats).to receive(:active).and_return('active')
allow(stats).to receive(:queued).and_return('queued')
allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats })
end
it 'updates metrics type unix and with addr' do
labels = { type: 'tcp', address: tcp_socket_address }
expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
subject.sample
end
end
end
end
describe '#start' do
context 'when enabled' do
before do
allow(subject).to receive(:enabled?).and_return(true)
end
it 'creates new thread' do
expect(Thread).to receive(:new)
subject.start
end
end
context 'when disabled' do
before do
allow(subject).to receive(:enabled?).and_return(false)
end
it "doesn't create new thread" do
expect(Thread).not_to receive(:new)
subject.start
end
end
end
end
...@@ -105,6 +105,22 @@ describe MergeRequest, models: true do ...@@ -105,6 +105,22 @@ describe MergeRequest, models: true do
end end
end end
describe '#assignee_ids' do
it 'returns an array of the assigned user id' do
subject.assignee_id = 123
expect(subject.assignee_ids).to eq([123])
end
end
describe '#assignee_ids=' do
it 'sets assignee_id to the last id in the array' do
subject.assignee_ids = [123, 456]
expect(subject.assignee_id).to eq(456)
end
end
describe '#assignee_or_author?' do describe '#assignee_or_author?' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
require 'spec_helper'
describe GlobalPolicy, models: true do
let(:current_user) { create(:user) }
let(:user) { create(:user) }
subject { GlobalPolicy.new(current_user, [user]) }
describe "reading the list of users" do
context "for a logged in user" do
it { is_expected.to be_allowed(:read_users_list) }
end
context "for an anonymous user" do
let(:current_user) { nil }
context "when the public level is restricted" do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it { is_expected.not_to be_allowed(:read_users_list) }
end
context "when the public level is not restricted" do
before do
stub_application_setting(restricted_visibility_levels: [])
end
it { is_expected.to be_allowed(:read_users_list) }
end
end
end
end
...@@ -13,9 +13,40 @@ describe API::Users do ...@@ -13,9 +13,40 @@ describe API::Users do
describe 'GET /users' do describe 'GET /users' do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns authorization error when the `username` parameter is not passed" do
get api("/users") get api("/users")
expect(response).to have_http_status(401)
expect(response).to have_http_status(403)
end
it "returns the user when a valid `username` parameter is passed" do
user = create(:user)
get api("/users"), username: user.username
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response[0]['id']).to eq(user.id)
expect(json_response[0]['username']).to eq(user.username)
end
it "returns authorization error when the `username` parameter refers to an inaccessible user" do
user = create(:user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
get api("/users"), username: user.username
expect(response).to have_http_status(403)
end
it "returns an empty response when an invalid `username` parameter is passed" do
get api("/users"), username: 'invalid'
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end end
end end
...@@ -138,6 +169,7 @@ describe API::Users do ...@@ -138,6 +169,7 @@ describe API::Users do
describe "GET /users/:id" do describe "GET /users/:id" do
it "returns a user by id" do it "returns a user by id" do
get api("/users/#{user.id}", user) get api("/users/#{user.id}", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username) expect(json_response['username']).to eq(user.username)
end end
...@@ -148,9 +180,22 @@ describe API::Users do ...@@ -148,9 +180,22 @@ describe API::Users do
expect(json_response['is_admin']).to be_nil expect(json_response['is_admin']).to be_nil
end end
it "returns a 401 if unauthenticated" do context 'for an anonymous user' do
get api("/users/9998") it "returns a user by id" do
expect(response).to have_http_status(401) get api("/users/#{user.id}")
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
end
it "returns a 404 if the target user is present but inaccessible" do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false)
get api("/users/#{user.id}")
expect(response).to have_http_status(404)
end
end end
it "returns a 404 error if user id not found" do it "returns a 404 error if user id not found" do
......
...@@ -359,18 +359,18 @@ describe QuickActions::InterpretService, services: true do ...@@ -359,18 +359,18 @@ describe QuickActions::InterpretService, services: true do
let(:content) { "/assign @#{developer.username}" } let(:content) { "/assign @#{developer.username}" }
context 'Issue' do context 'Issue' do
it 'fetches assignee and populates assignee_id if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue) _, updates = service.execute(content, issue)
expect(updates).to eq(assignee_ids: [developer.id]) expect(updates[:assignee_ids]).to match_array([developer.id])
end end
end end
context 'Merge Request' do context 'Merge Request' do
it 'fetches assignee and populates assignee_id if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, merge_request) _, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_id: developer.id) expect(updates).to eq(assignee_ids: [developer.id])
end end
end end
end end
...@@ -383,7 +383,7 @@ describe QuickActions::InterpretService, services: true do ...@@ -383,7 +383,7 @@ describe QuickActions::InterpretService, services: true do
end end
context 'Issue' do context 'Issue' do
it 'fetches assignee and populates assignee_id if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue) _, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id]) expect(updates[:assignee_ids]).to match_array([developer.id])
...@@ -391,10 +391,10 @@ describe QuickActions::InterpretService, services: true do ...@@ -391,10 +391,10 @@ describe QuickActions::InterpretService, services: true do
end end
context 'Merge Request' do context 'Merge Request' do
it 'fetches assignee and populates assignee_id if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, merge_request) _, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_id: developer.id) expect(updates).to eq(assignee_ids: [developer.id])
end end
end end
end end
...@@ -422,11 +422,27 @@ describe QuickActions::InterpretService, services: true do ...@@ -422,11 +422,27 @@ describe QuickActions::InterpretService, services: true do
end end
context 'Merge Request' do context 'Merge Request' do
it 'populates assignee_id: nil if content contains /unassign' do it 'populates assignee_ids: [] if content contains /unassign' do
merge_request.update(assignee_id: developer.id) merge_request.update(assignee_ids: [developer.id])
_, updates = service.execute(content, merge_request) _, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_id: nil) expect(updates).to eq(assignee_ids: [])
end
end
end
context 'reassign command' do
let(:content) { '/reassign' }
context 'Issue' do
it 'reassigns user if content contains /reassign @user' do
user = create(:user)
issue.update(assignee_ids: [developer.id])
_, updates = service.execute("/reassign @#{user.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment