Merge branch 'master' into 'bump-prometheus-version-to-fix-text-representation-problem'
# Conflicts: # Gemfile.lock
This commit is contained in:
commit
7ac065d0f5
1
Gemfile
1
Gemfile
|
@ -285,6 +285,7 @@ group :metrics do
|
|||
|
||||
# Prometheus
|
||||
gem 'prometheus-client-mmap', '~>0.7.0.beta5'
|
||||
gem 'raindrops', '~> 0.18'
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
|
|
@ -658,7 +658,7 @@ GEM
|
|||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.17.0)
|
||||
raindrops (0.18.0)
|
||||
rake (10.5.0)
|
||||
rblineprof (0.3.6)
|
||||
debugger-ruby_core_source (~> 1.3)
|
||||
|
@ -1062,6 +1062,7 @@ DEPENDENCIES
|
|||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rails-i18n (~> 4.0.9)
|
||||
rainbow (~> 2.2)
|
||||
raindrops (~> 0.18)
|
||||
rblineprof (~> 0.3.6)
|
||||
rdoc (~> 4.2)
|
||||
recaptcha (~> 3.0)
|
||||
|
|
|
@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) {
|
|||
return $dropdown.glDropdown({
|
||||
showMenuAbove: showMenuAbove,
|
||||
data: function(term, callback) {
|
||||
var isAuthorFilter;
|
||||
isAuthorFilter = $('.js-author-search');
|
||||
return _this.users(term, options, function(users) {
|
||||
// GitLabDropdownFilter returns this.instance
|
||||
// GitLabDropdownRemote returns this.options.instance
|
||||
|
|
|
@ -60,13 +60,13 @@ class UsersFinder
|
|||
end
|
||||
|
||||
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]))
|
||||
end
|
||||
|
||||
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]
|
||||
|
||||
users.external
|
||||
|
|
|
@ -16,8 +16,8 @@ module FormHelper
|
|||
end
|
||||
end
|
||||
|
||||
def issue_dropdown_options(issuable, has_multiple_assignees = true)
|
||||
options = {
|
||||
def issue_assignees_dropdown_options
|
||||
{
|
||||
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
|
||||
title: 'Select assignee',
|
||||
filter: true,
|
||||
|
@ -27,8 +27,8 @@ module FormHelper
|
|||
first_user: current_user&.username,
|
||||
null_user: true,
|
||||
current_user: true,
|
||||
project_id: issuable.project.try(:id),
|
||||
field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]",
|
||||
project_id: @project.id,
|
||||
field_name: 'issue[assignee_ids][]',
|
||||
default_label: 'Unassigned',
|
||||
'max-select': 1,
|
||||
'dropdown-header': 'Assignee',
|
||||
|
@ -38,13 +38,5 @@ module FormHelper
|
|||
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
|
||||
|
|
|
@ -58,6 +58,11 @@ module GroupsHelper
|
|||
IssuesFinder.new(current_user, group_id: group.id).execute
|
||||
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
|
||||
|
||||
def group_title_link(group, hidable: false)
|
||||
|
|
|
@ -126,6 +126,18 @@ module SearchHelper
|
|||
search_path(options)
|
||||
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
|
||||
# maximum length is set to 200 characters.
|
||||
def search_md_sanitize(object, field)
|
||||
|
|
|
@ -102,6 +102,14 @@ module Issuable
|
|||
def locking_enabled?
|
||||
title_changed? || description_changed?
|
||||
end
|
||||
|
||||
def allows_multiple_assignees?
|
||||
false
|
||||
end
|
||||
|
||||
def has_multiple_assignees?
|
||||
assignees.count > 1
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
|
|
@ -197,11 +197,19 @@ class MergeRequest < ActiveRecord::Base
|
|||
}
|
||||
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
|
||||
Array(assignee)
|
||||
end
|
||||
|
||||
def assignee_ids
|
||||
Array(assignee_id)
|
||||
end
|
||||
|
||||
def assignee_ids=(ids)
|
||||
write_attribute(:assignee_id, ids.last)
|
||||
end
|
||||
|
||||
def assignee_or_author?(user)
|
||||
author_id == user.id || assignee_id == user.id
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require_dependency 'declarative_policy'
|
||||
|
||||
class BasePolicy < DeclarativePolicy::Base
|
||||
include Gitlab::CurrentSettings
|
||||
|
||||
desc "User is an instance admin"
|
||||
with_options scope: :user, score: 0
|
||||
condition(:admin) { @user&.admin? }
|
||||
|
@ -10,4 +12,9 @@ class BasePolicy < DeclarativePolicy::Base
|
|||
|
||||
with_options scope: :user, score: 0
|
||||
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
|
||||
|
|
|
@ -11,10 +11,16 @@ class GlobalPolicy < BasePolicy
|
|||
with_options scope: :user, score: 0
|
||||
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
|
||||
enable :read_users_list
|
||||
enable :log_in
|
||||
enable :access_api
|
||||
enable :access_git
|
||||
|
@ -37,4 +43,8 @@ class GlobalPolicy < BasePolicy
|
|||
rule { access_locked }.policy do
|
||||
prevent :log_in
|
||||
end
|
||||
|
||||
rule { ~restricted_public_level }.policy do
|
||||
enable :read_users_list
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
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"
|
||||
condition(:user_is_self, score: 0) { @subject == @user }
|
||||
|
||||
|
|
|
@ -92,9 +92,12 @@ module QuickActions
|
|||
|
||||
desc 'Assign'
|
||||
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
|
||||
params '@user'
|
||||
condition do
|
||||
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
|
||||
end
|
||||
|
@ -104,28 +107,69 @@ module QuickActions
|
|||
command :assign do |users|
|
||||
next if users.empty?
|
||||
|
||||
if issuable.is_a?(Issue)
|
||||
@updates[:assignee_ids] = [users.last.id]
|
||||
else
|
||||
@updates[:assignee_id] = users.last.id
|
||||
end
|
||||
@updates[:assignee_ids] =
|
||||
if issuable.allows_multiple_assignees?
|
||||
issuable.assignees.pluck(:id) + users.map(&:id)
|
||||
else
|
||||
[users.last.id]
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Remove assignee'
|
||||
desc do
|
||||
if issuable.allows_multiple_assignees?
|
||||
'Remove all or specific assignee(s)'
|
||||
else
|
||||
'Remove assignee'
|
||||
end
|
||||
end
|
||||
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
|
||||
condition do
|
||||
issuable.persisted? &&
|
||||
issuable.assignees.any? &&
|
||||
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
|
||||
end
|
||||
command :unassign do
|
||||
if issuable.is_a?(Issue)
|
||||
@updates[:assignee_ids] = []
|
||||
else
|
||||
@updates[:assignee_id] = nil
|
||||
end
|
||||
parse_params do |unassign_param|
|
||||
# When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
|
||||
extract_users(unassign_param) if issuable.allows_multiple_assignees?
|
||||
end
|
||||
command :unassign do |users = nil|
|
||||
@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
|
||||
|
||||
desc 'Set milestone'
|
||||
|
|
|
@ -45,10 +45,13 @@
|
|||
.panel.panel-danger
|
||||
.panel-heading Remove group
|
||||
.panel-body
|
||||
%p
|
||||
Removing group will cause all child projects and resources to be removed.
|
||||
%br
|
||||
%strong Removed group can not be restored!
|
||||
= form_tag(@group, method: :delete) do
|
||||
%p
|
||||
Removing group will cause all child projects and resources to be removed.
|
||||
%br
|
||||
%strong Removed group can not be restored!
|
||||
|
||||
.form-actions
|
||||
= link_to 'Remove group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove"
|
||||
.form-actions
|
||||
= 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 @@
|
|||
":data-name" => "assignee.name",
|
||||
":data-username" => "assignee.username" }
|
||||
.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-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
|
||||
Select assignee
|
||||
= dropdown_options[:title]
|
||||
= icon("chevron-down")
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
|
||||
= dropdown_title("Assign to")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- @no_container = true
|
||||
- page_title "Charts", "Pipelines"
|
||||
- page_title _("Charts"), _("Pipelines")
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('common_d3')
|
||||
= page_specific_javascript_bundle_tag('graphs')
|
||||
|
@ -8,7 +8,7 @@
|
|||
%div{ class: container_class }
|
||||
.sub-header-block
|
||||
.oneline
|
||||
A collection of graphs for Continuous Integration
|
||||
= _("A collection of graphs regarding Continuous Integration")
|
||||
|
||||
#charts.ci-charts
|
||||
.row
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
%h4 Overall stats
|
||||
%h4= s_("PipelineCharts|Overall statistics")
|
||||
%ul
|
||||
%li
|
||||
Total:
|
||||
%strong= pluralize @counts[:total], 'pipeline'
|
||||
= s_("PipelineCharts|Total:")
|
||||
%strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total]
|
||||
%li
|
||||
Successful:
|
||||
%strong= pluralize @counts[:success], 'pipeline'
|
||||
= s_("PipelineCharts|Successful:")
|
||||
%strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success]
|
||||
%li
|
||||
Failed:
|
||||
%strong= pluralize @counts[:failed], 'pipeline'
|
||||
= s_("PipelineCharts|Failed:")
|
||||
%strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed]
|
||||
%li
|
||||
Success ratio:
|
||||
= s_("PipelineCharts|Success ratio:")
|
||||
%strong
|
||||
#{success_ratio(@counts)}%
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%div
|
||||
%p.light
|
||||
Commit duration in minutes for last 30 commits
|
||||
= _("Commit duration in minutes for last 30 commits")
|
||||
|
||||
%canvas#build_timesChart{ height: 200 }
|
||||
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
%h4 Pipelines charts
|
||||
%h4= _("Pipelines charts")
|
||||
%p
|
||||
|
||||
%span.cgreen
|
||||
= icon("circle")
|
||||
success
|
||||
= s_("Pipeline|success")
|
||||
|
||||
%span.cgray
|
||||
= icon("circle")
|
||||
all
|
||||
= s_("Pipeline|all")
|
||||
|
||||
.prepend-top-default
|
||||
%p.light
|
||||
Jobs for last week
|
||||
= _("Jobs for last week")
|
||||
(#{date_from_to(Date.today - 7.days, Date.today)})
|
||||
%canvas#weekChart{ height: 200 }
|
||||
|
||||
.prepend-top-default
|
||||
%p.light
|
||||
Jobs for last month
|
||||
= _("Jobs for last month")
|
||||
(#{date_from_to(Date.today - 30.days, Date.today)})
|
||||
%canvas#monthChart{ height: 200 }
|
||||
|
||||
.prepend-top-default
|
||||
%p.light
|
||||
Jobs for last year
|
||||
= _("Jobs for last year")
|
||||
%canvas#yearChart.padded{ height: 250 }
|
||||
|
||||
- [:week, :month, :year].each do |scope|
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
.scroll-container
|
||||
%ul.tokens-container.list-unstyled
|
||||
%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')
|
||||
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
|
||||
%ul{ data: { dropdown: true } }
|
||||
|
|
|
@ -37,19 +37,20 @@
|
|||
- 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 }
|
||||
|
||||
- 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'
|
||||
|
||||
- if issuable.is_a?(Issue)
|
||||
- unless issuable.assignees.any?
|
||||
= 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'
|
||||
- data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" }
|
||||
- data[:multi_select] = true
|
||||
- data['dropdown-title'] = title
|
||||
- data['dropdown-header'] = 'Assignee'
|
||||
- data['max-select'] = 1
|
||||
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
|
||||
- data['max-select'] = dropdown_options[:data][:'max-select']
|
||||
- options[:data].merge!(data)
|
||||
|
||||
= dropdown_tag(title, options: options)
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
- if issuable.assignees.length === 0
|
||||
= 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)}"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: "Remove group modal like remove project modal (requires typing + confirmation)"
|
||||
merge_request: 12569
|
||||
author: Diego Souza
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Allow unauthenticated access to the /api/v4/users API
|
||||
merge_request: 12445
|
||||
author:
|
|
@ -543,6 +543,10 @@ production: &base
|
|||
# enabled: true
|
||||
# host: localhost
|
||||
# port: 3808
|
||||
prometheus:
|
||||
# Time between sampling of unicorn socket metrics, in seconds
|
||||
# unicorn_sampler_interval: 10
|
||||
|
||||
|
||||
#
|
||||
# 5. Extra customization
|
||||
|
|
|
@ -494,6 +494,12 @@ Settings.webpack.dev_server['enabled'] ||= false
|
|||
Settings.webpack.dev_server['host'] ||= 'localhost'
|
||||
Settings.webpack.dev_server['port'] ||= 3808
|
||||
|
||||
#
|
||||
# Prometheus metrics settings
|
||||
#
|
||||
Settings['prometheus'] ||= Settingslogic.new({})
|
||||
Settings.prometheus['unicorn_sampler_interval'] ||= 10
|
||||
|
||||
#
|
||||
# Testing settings
|
||||
#
|
||||
|
|
|
@ -119,6 +119,13 @@ def instrument_classes(instrumentation)
|
|||
end
|
||||
# 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?
|
||||
require 'pathname'
|
||||
require 'influxdb'
|
||||
|
@ -175,7 +182,7 @@ if Gitlab::Metrics.enabled?
|
|||
|
||||
GC::Profiler.enable
|
||||
|
||||
Gitlab::Metrics::Sampler.new.start
|
||||
Gitlab::Metrics::InfluxSampler.initialize_instance.start
|
||||
|
||||
module TrackNewRedisConnections
|
||||
def connect(*args)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Using Docker Images
|
||||
# Using Docker images
|
||||
|
||||
GitLab CI in conjunction with [GitLab Runner](../runners/README.md) can use
|
||||
[Docker Engine](https://www.docker.com/) to test and build any application.
|
||||
|
@ -17,14 +17,16 @@ can also run on your workstation. The added benefit is that you can test all
|
|||
the commands that we will explore later from your shell, rather than having to
|
||||
test them on a dedicated CI server.
|
||||
|
||||
## Register docker runner
|
||||
## Register Docker Runner
|
||||
|
||||
To use GitLab Runner with docker you need to register a new runner to use the
|
||||
`docker` executor:
|
||||
To use GitLab Runner with Docker you need to [register a new Runner][register]
|
||||
to use the `docker` executor.
|
||||
|
||||
A one-line example can be seen below:
|
||||
|
||||
```bash
|
||||
gitlab-ci-multi-runner register \
|
||||
--url "https://gitlab.com/" \
|
||||
sudo gitlab-runner register \
|
||||
--url "https://gitlab.example.com/" \
|
||||
--registration-token "PROJECT_REGISTRATION_TOKEN" \
|
||||
--description "docker-ruby-2.1" \
|
||||
--executor "docker" \
|
||||
|
@ -33,26 +35,26 @@ gitlab-ci-multi-runner register \
|
|||
--docker-mysql latest
|
||||
```
|
||||
|
||||
The registered runner will use the `ruby:2.1` docker image and will run two
|
||||
The registered runner will use the `ruby:2.1` Docker image and will run two
|
||||
services, `postgres:latest` and `mysql:latest`, both of which will be
|
||||
accessible during the build process.
|
||||
|
||||
## What is an image
|
||||
|
||||
The `image` keyword is the name of the docker image the docker executor
|
||||
will run to perform the CI tasks.
|
||||
The `image` keyword is the name of the Docker image the Docker executor
|
||||
will run to perform the CI tasks.
|
||||
|
||||
By default the executor will only pull images from [Docker Hub][hub],
|
||||
By default, the executor will only pull images from [Docker Hub][hub],
|
||||
but this can be configured in the `gitlab-runner/config.toml` by setting
|
||||
the [docker pull policy][] to allow using local images.
|
||||
the [Docker pull policy][] to allow using local images.
|
||||
|
||||
For more information about images and Docker Hub please read
|
||||
the [Docker Fundamentals][] documentation.
|
||||
|
||||
## What is a service
|
||||
|
||||
The `services` keyword defines just another docker image that is run during
|
||||
your job and is linked to the docker image that the `image` keyword defines.
|
||||
The `services` keyword defines just another Docker image that is run during
|
||||
your job and is linked to the Docker image that the `image` keyword defines.
|
||||
This allows you to access the service image during build time.
|
||||
|
||||
The service image can run any application, but the most common use case is to
|
||||
|
@ -60,6 +62,11 @@ run a database container, eg. `mysql`. It's easier and faster to use an
|
|||
existing image and run it as an additional container than install `mysql` every
|
||||
time the project is built.
|
||||
|
||||
You are not limited to have only database services. You can add as many
|
||||
services you need to `.gitlab-ci.yml` or manually modify `config.toml`.
|
||||
Any image found at [Docker Hub][hub] or your private Container Registry can be
|
||||
used as a service.
|
||||
|
||||
You can see some widely used services examples in the relevant documentation of
|
||||
[CI services examples](../services/README.md).
|
||||
|
||||
|
@ -73,22 +80,49 @@ then be used to create a container that is linked to the job container.
|
|||
|
||||
The service container for MySQL will be accessible under the hostname `mysql`.
|
||||
So, in order to access your database service you have to connect to the host
|
||||
named `mysql` instead of a socket or `localhost`.
|
||||
named `mysql` instead of a socket or `localhost`. Read more in [accessing the
|
||||
services](#accessing-the-services).
|
||||
|
||||
## Overwrite image and services
|
||||
### Accessing the services
|
||||
|
||||
See [How to use other images as services](#how-to-use-other-images-as-services).
|
||||
Let's say that you need a Wordpress instance to test some API integration with
|
||||
your application.
|
||||
|
||||
## How to use other images as services
|
||||
You can then use for example the [tutum/wordpress][] image in your
|
||||
`.gitlab-ci.yml`:
|
||||
|
||||
You are not limited to have only database services. You can add as many
|
||||
services you need to `.gitlab-ci.yml` or manually modify `config.toml`.
|
||||
Any image found at [Docker Hub][hub] can be used as a service.
|
||||
```yaml
|
||||
services:
|
||||
- tutum/wordpress:latest
|
||||
```
|
||||
|
||||
## Define image and services from `.gitlab-ci.yml`
|
||||
If you don't [specify a service alias](#available-settings-for-services-entry),
|
||||
when the job is run, `tutum/wordpress` will be started and you will have
|
||||
access to it from your build container under two hostnames to choose from:
|
||||
|
||||
- `tutum-wordpress`
|
||||
- `tutum__wordpress`
|
||||
|
||||
>**Note:**
|
||||
Hostnames with underscores are not RFC valid and may cause problems in 3rd party
|
||||
applications.
|
||||
|
||||
The default aliases for the service's hostname are created from its image name
|
||||
following these rules:
|
||||
|
||||
- Everything after the colon (`:`) is stripped
|
||||
- Slash (`/`) is replaced with double underscores (`__`) and the primary alias
|
||||
is created
|
||||
- Slash (`/`) is replaced with a single dash (`-`) and the secondary alias is
|
||||
created (requires GitLab Runner v1.1.0 or higher)
|
||||
|
||||
To override the default behavior, you can
|
||||
[specify a service alias](#available-settings-for-services-entry).
|
||||
|
||||
## Define `image` and `services` from `.gitlab-ci.yml`
|
||||
|
||||
You can simply define an image that will be used for all jobs and a list of
|
||||
services that you want to use during build time.
|
||||
services that you want to use during build time:
|
||||
|
||||
```yaml
|
||||
image: ruby:2.2
|
||||
|
@ -125,6 +159,203 @@ test:2.2:
|
|||
- bundle exec rake spec
|
||||
```
|
||||
|
||||
Or you can pass some [extended configuration options](#extended-docker-configuration-options)
|
||||
for `image` and `services`:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
name: ruby:2.2
|
||||
entrypoint: ["/bin/bash"]
|
||||
|
||||
services:
|
||||
- name: my-postgres:9.4
|
||||
alias: db-postgres
|
||||
entrypoint: ["/usr/local/bin/db-postgres"]
|
||||
command: ["start"]
|
||||
|
||||
before_script:
|
||||
- bundle install
|
||||
|
||||
test:
|
||||
script:
|
||||
- bundle exec rake spec
|
||||
```
|
||||
|
||||
## Extended Docker configuration options
|
||||
|
||||
> **Note:**
|
||||
This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
|
||||
|
||||
When configuring the `image` or `services` entries, you can use a string or a map as
|
||||
options:
|
||||
|
||||
- when using a string as an option, it must be the full name of the image to use
|
||||
(including the Registry part if you want to download the image from a Registry
|
||||
other than Docker Hub)
|
||||
- when using a map as an option, then it must contain at least the `name`
|
||||
option, which is the same name of the image as used for the string setting
|
||||
|
||||
For example, the following two definitions are equal:
|
||||
|
||||
1. Using a string as an option to `image` and `services`:
|
||||
|
||||
```yaml
|
||||
image: "registry.example.com/my/image:latest"
|
||||
|
||||
services:
|
||||
- postgresql:9.4
|
||||
- redis:latest
|
||||
```
|
||||
|
||||
1. Using a map as an option to `image` and `services`. The use of `image:name` is
|
||||
required:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
name: "registry.example.com/my/image:latest"
|
||||
|
||||
services:
|
||||
- name: postgresql:9.4
|
||||
- name: redis:latest
|
||||
```
|
||||
|
||||
### Available settings for `image`
|
||||
|
||||
> **Note:**
|
||||
This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
|
||||
|
||||
| Setting | Required | Description |
|
||||
|------------|----------|-------------|
|
||||
| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. |
|
||||
| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
|
||||
|
||||
### Available settings for `services`
|
||||
|
||||
> **Note:**
|
||||
This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
|
||||
|
||||
| Setting | Required | Description |
|
||||
|------------|----------|-------------|
|
||||
| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. |
|
||||
| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
|
||||
| `command` | no | Command or script that should be used as the container's command. It will be translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`][cmd] directive, where each shell token is a separate string in the array. |
|
||||
| `alias` | no | Additional alias that can be used to access the service from the job's container. Read [Accessing the services](#accessing-the-services) for more information. |
|
||||
|
||||
### Starting multiple services from the same image
|
||||
|
||||
Before the new extended Docker configuration options, the following configuration
|
||||
would not work properly:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
- mysql:latest
|
||||
- mysql:latest
|
||||
```
|
||||
|
||||
The Runner would start two containers using the `mysql:latest` image, but both
|
||||
of them would be added to the job's container with the `mysql` alias based on
|
||||
the [default hostname naming](#accessing-the-services). This would end with one
|
||||
of the services not being accessible.
|
||||
|
||||
After the new extended Docker configuration options, the above example would
|
||||
look like:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
- name: mysql:latest
|
||||
alias: mysql-1
|
||||
- name: mysql:latest
|
||||
alias: mysql-2
|
||||
```
|
||||
|
||||
The Runner will still start two containers using the `mysql:latest` image,
|
||||
but now each of them will also be accessible with the alias configured
|
||||
in `.gitlab-ci.yml` file.
|
||||
|
||||
### Setting a command for the service
|
||||
|
||||
Let's assume you have a `super/sql:latest` image with some SQL database
|
||||
inside it and you would like to use it as a service for your job. Let's also
|
||||
assume that this image doesn't start the database process while starting
|
||||
the container and the user needs to manually use `/usr/bin/super-sql run` as
|
||||
a command to start the database.
|
||||
|
||||
Before the new extended Docker configuration options, you would need to create
|
||||
your own image based on the `super/sql:latest` image, add the default command,
|
||||
and then use it in job's configuration, like:
|
||||
|
||||
```Dockerfile
|
||||
# my-super-sql:latest image's Dockerfile
|
||||
|
||||
FROM super/sql:latest
|
||||
CMD ["/usr/bin/super-sql", "run"]
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
|
||||
services:
|
||||
- my-super-sql:latest
|
||||
```
|
||||
|
||||
After the new extended Docker configuration options, you can now simply
|
||||
set a `command` in `.gitlab-ci.yml`, like:
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
|
||||
services:
|
||||
- name: super/sql:latest
|
||||
command: ["/usr/bin/super-sql", "run"]
|
||||
```
|
||||
|
||||
As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`][cmd].
|
||||
|
||||
### Overriding the entrypoint of an image
|
||||
|
||||
Let's assume you have a `super/sql:experimental` image with some SQL database
|
||||
inside it and you would like to use it as a base image for your job because you
|
||||
want to execute some tests with this database binary. Let's also assume that
|
||||
this image is configured with `/usr/bin/super-sql run` as an entrypoint. That
|
||||
means, that when starting the container without additional options, it will run
|
||||
the database's process, while Runner expects that the image will have no
|
||||
entrypoint or at least will start with a shell as its entrypoint.
|
||||
|
||||
Previously we would need to create our own image based on the
|
||||
`super/sql:experimental` image, set the entrypoint to a shell, and then use
|
||||
it in job's configuration, e.g.:
|
||||
|
||||
Before the new extended Docker configuration options, you would need to create
|
||||
your own image based on the `super/sql:experimental` image, set the entrypoint
|
||||
to a shell and then use it in job's configuration, like:
|
||||
|
||||
```Dockerfile
|
||||
# my-super-sql:experimental image's Dockerfile
|
||||
|
||||
FROM super/sql:experimental
|
||||
ENTRYPOINT ["/bin/sh"]
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
|
||||
image: my-super-sql:experimental
|
||||
```
|
||||
|
||||
After the new extended Docker configuration options, you can now simply
|
||||
set an `entrypoint` in `.gitlab-ci.yml`, like:
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
|
||||
image:
|
||||
name: super/sql:experimental
|
||||
entrypoint: ["/bin/sh"]
|
||||
```
|
||||
|
||||
As you can see the syntax of `entrypoint` is similar to
|
||||
[Dockerfile's `ENTRYPOINT`][entrypoint].
|
||||
|
||||
## Define image and services in `config.toml`
|
||||
|
||||
Look for the `[runners.docker]` section:
|
||||
|
@ -138,7 +369,7 @@ Look for the `[runners.docker]` section:
|
|||
The image and services defined this way will be added to all job run by
|
||||
that runner.
|
||||
|
||||
## Define an image from a private Docker registry
|
||||
## Define an image from a private Container Registry
|
||||
|
||||
> **Notes:**
|
||||
- This feature requires GitLab Runner **1.8** or higher
|
||||
|
@ -193,44 +424,6 @@ To configure access for `registry.example.com`, follow these steps:
|
|||
You can add configuration for as many registries as you want, adding more
|
||||
registries to the `"auths"` hash as described above.
|
||||
|
||||
## Accessing the services
|
||||
|
||||
Let's say that you need a Wordpress instance to test some API integration with
|
||||
your application.
|
||||
|
||||
You can then use for example the [tutum/wordpress][] image in your
|
||||
`.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
- tutum/wordpress:latest
|
||||
```
|
||||
|
||||
When the job is run, `tutum/wordpress` will be started and you will have
|
||||
access to it from your build container under the hostnames `tutum-wordpress`
|
||||
(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`.
|
||||
|
||||
When using a private registry, the image name also includes a hostname and port
|
||||
of the registry.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
- docker.example.com:5000/wordpress:latest
|
||||
```
|
||||
|
||||
The service hostname will also include the registry hostname. Service will be
|
||||
available under hostnames `docker.example.com-wordpress` (requires GitLab Runner v1.1.0 or newer)
|
||||
and `docker.example.com__wordpress`.
|
||||
|
||||
*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.*
|
||||
|
||||
The alias hostnames for the service are made from the image name following these
|
||||
rules:
|
||||
|
||||
1. Everything after `:` is stripped
|
||||
2. Slash (`/`) is replaced with double underscores (`__`) - primary alias
|
||||
3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer
|
||||
|
||||
## Configuring services
|
||||
|
||||
Many services accept environment variables which allow you to easily change
|
||||
|
@ -257,7 +450,7 @@ See the specific documentation for
|
|||
|
||||
## How Docker integration works
|
||||
|
||||
Below is a high level overview of the steps performed by docker during job
|
||||
Below is a high level overview of the steps performed by Docker during job
|
||||
time.
|
||||
|
||||
1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`.
|
||||
|
@ -274,7 +467,7 @@ time.
|
|||
## How to debug a job locally
|
||||
|
||||
*Note: The following commands are run without root privileges. You should be
|
||||
able to run docker with your regular user account.*
|
||||
able to run Docker with your regular user account.*
|
||||
|
||||
First start with creating a file named `build_script`:
|
||||
|
||||
|
@ -334,3 +527,6 @@ creation.
|
|||
[mysql-hub]: https://hub.docker.com/r/_/mysql/
|
||||
[runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
|
||||
[secret variable]: ../variables/README.md#secret-variables
|
||||
[entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint
|
||||
[cmd]: https://docs.docker.com/engine/reference/builder/#cmd
|
||||
[register]: https://docs.gitlab.com/runner/register/
|
||||
|
|
|
@ -4,10 +4,13 @@ module API
|
|||
|
||||
before do
|
||||
allow_access_with_scope :read_user if request.get?
|
||||
authenticate!
|
||||
end
|
||||
|
||||
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
|
||||
before do
|
||||
authenticate_non_get!
|
||||
end
|
||||
|
||||
helpers do
|
||||
def find_user(params)
|
||||
id = params[:user_id] || params[:id]
|
||||
|
@ -51,15 +54,22 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
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?)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -398,6 +408,10 @@ module API
|
|||
end
|
||||
|
||||
resource :user do
|
||||
before do
|
||||
authenticate!
|
||||
end
|
||||
|
||||
desc 'Get the currently authenticated user' do
|
||||
success Entities::UserPublic
|
||||
end
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
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
|
|
@ -0,0 +1,45 @@
|
|||
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
|
|||
# This class is used to gather statistics that can't be directly associated
|
||||
# with a transaction such as system memory usage, garbage collection
|
||||
# statistics, etc.
|
||||
class Sampler
|
||||
class InfluxSampler < BaseSampler
|
||||
# interval - The sampling interval in seconds.
|
||||
def initialize(interval = Metrics.settings[:sample_interval])
|
||||
interval_half = interval.to_f / 2
|
||||
|
||||
@interval = interval
|
||||
@interval_steps = (-interval_half..interval_half).step(0.1).to_a
|
||||
@last_step = nil
|
||||
super(interval)
|
||||
@last_step = nil
|
||||
|
||||
@metrics = []
|
||||
|
||||
|
@ -26,18 +23,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.new do
|
||||
Thread.current.abort_on_exception = true
|
||||
|
||||
loop do
|
||||
sleep(sleep_interval)
|
||||
|
||||
sample
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sample
|
||||
sample_memory_usage
|
||||
sample_file_descriptors
|
||||
|
@ -86,7 +71,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def sample_gc
|
||||
time = GC::Profiler.total_time * 1000.0
|
||||
time = GC::Profiler.total_time * 1000.0
|
||||
stats = GC.stat.merge(total_time: time)
|
||||
|
||||
# We want the difference of GC runs compared to the last sample, not the
|
||||
|
@ -111,23 +96,6 @@ module Gitlab
|
|||
def sidekiq?
|
||||
Sidekiq.server?
|
||||
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
|
|
@ -29,8 +29,8 @@ module Gitlab
|
|||
provide_metric(name) || registry.summary(name, docstring, base_labels)
|
||||
end
|
||||
|
||||
def gauge(name, docstring, base_labels = {})
|
||||
provide_metric(name) || registry.gauge(name, docstring, base_labels)
|
||||
def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
|
||||
provide_metric(name) || registry.gauge(name, docstring, base_labels, multiprocess_mode)
|
||||
end
|
||||
|
||||
def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
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 ""
|
|||
"Plural-Forms: nplurals=2; plural=n != 1;\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}"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -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}"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchSwitcherPlaceholder|Search branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchSwitcherTitle|Switch branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Browse Directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "Browse File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Browse Files"
|
||||
msgstr ""
|
||||
|
||||
msgid "Browse files"
|
||||
msgstr ""
|
||||
|
||||
|
@ -159,6 +192,9 @@ msgid_plural "Commits"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Commit duration in minutes for last 30 commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commit message"
|
||||
msgstr ""
|
||||
|
||||
|
@ -171,6 +207,9 @@ msgstr ""
|
|||
msgid "Commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commits feed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commits|History"
|
||||
msgstr ""
|
||||
|
||||
|
@ -195,6 +234,9 @@ msgstr ""
|
|||
msgid "Create New Directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create a personal access token on your account to pull or push via %{protocol}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Create directory"
|
||||
msgstr ""
|
||||
|
||||
|
@ -213,6 +255,9 @@ msgstr ""
|
|||
msgid "CreateTag|Tag"
|
||||
msgstr ""
|
||||
|
||||
msgid "CreateTokenToCloneLink|create a personal access token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cron Timezone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -323,6 +368,9 @@ msgstr ""
|
|||
msgid "Files"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by commit message"
|
||||
msgstr ""
|
||||
|
||||
msgid "Find by path"
|
||||
msgstr ""
|
||||
|
||||
|
@ -370,6 +418,15 @@ msgstr ""
|
|||
msgid "Introducing Cycle Analytics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs for last month"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs for last week"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs for last year"
|
||||
msgstr ""
|
||||
|
||||
msgid "LFSStatus|Disabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -535,6 +592,21 @@ msgstr ""
|
|||
msgid "Pipeline Schedules"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -565,6 +637,18 @@ msgstr ""
|
|||
msgid "PipelineSheduleIntervalPattern|Custom"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines charts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|success"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|with stage"
|
||||
msgstr ""
|
||||
|
||||
|
@ -688,7 +772,7 @@ msgstr ""
|
|||
msgid "Select target branch"
|
||||
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 ""
|
||||
|
||||
msgid "Set up CI"
|
||||
|
@ -714,10 +798,7 @@ msgstr ""
|
|||
msgid "StarProject|Star"
|
||||
msgstr ""
|
||||
|
||||
msgid "Start a %{new_merge_request} with these changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Start a <strong>new merge request</strong> with these changes"
|
||||
msgid "Start a %{new_merge_request} with these changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch branch/tag"
|
||||
|
@ -948,9 +1029,15 @@ msgstr ""
|
|||
msgid "Upload file"
|
||||
msgstr ""
|
||||
|
||||
msgid "UploadLink|click to upload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use your global notification setting"
|
||||
msgstr ""
|
||||
|
||||
msgid "View open merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "VisibilityLevel|Internal"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-06-19 15:50-0500\n"
|
||||
"PO-Revision-Date: 2017-06-19 15:50-0500\n"
|
||||
"POT-Creation-Date: 2017-06-28 13:32+0200\n"
|
||||
"PO-Revision-Date: 2017-06-28 13:32+0200\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
@ -18,7 +18,7 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\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."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
@ -31,6 +31,14 @@ msgstr[1] ""
|
|||
msgid "%{commit_author_link} committed %{commit_timeago}"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -185,6 +193,9 @@ msgid_plural "Commits"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Commit duration in minutes for last 30 commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commit message"
|
||||
msgstr ""
|
||||
|
||||
|
@ -224,6 +235,9 @@ msgstr ""
|
|||
msgid "Create New Directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create a personal access token on your account to pull or push via %{protocol}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Create directory"
|
||||
msgstr ""
|
||||
|
||||
|
@ -242,6 +256,9 @@ msgstr ""
|
|||
msgid "CreateTag|Tag"
|
||||
msgstr ""
|
||||
|
||||
msgid "CreateTokenToCloneLink|create a personal access token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cron Timezone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -402,6 +419,15 @@ msgstr ""
|
|||
msgid "Introducing Cycle Analytics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs for last month"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs for last week"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs for last year"
|
||||
msgstr ""
|
||||
|
||||
msgid "LFSStatus|Disabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -567,6 +593,21 @@ msgstr ""
|
|||
msgid "Pipeline Schedules"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -597,6 +638,18 @@ msgstr ""
|
|||
msgid "PipelineSheduleIntervalPattern|Custom"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines charts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|success"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|with stage"
|
||||
msgstr ""
|
||||
|
||||
|
@ -720,7 +773,7 @@ msgstr ""
|
|||
msgid "Select target branch"
|
||||
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 ""
|
||||
|
||||
msgid "Set up CI"
|
||||
|
|
|
@ -135,7 +135,7 @@ feature 'Group', feature: true do
|
|||
expect(page).not_to have_content('secret-group')
|
||||
end
|
||||
|
||||
describe 'group edit' do
|
||||
describe 'group edit', js: true do
|
||||
let(:group) { create(:group) }
|
||||
let(:path) { edit_group_path(group) }
|
||||
let(:new_name) { 'new-name' }
|
||||
|
@ -157,8 +157,8 @@ feature 'Group', feature: true do
|
|||
end
|
||||
|
||||
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"
|
||||
end
|
||||
end
|
||||
|
@ -212,4 +212,10 @@ feature 'Group', feature: true do
|
|||
expect(page).to have_content(nested_group.name)
|
||||
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
|
||||
|
|
|
@ -31,8 +31,8 @@ describe 'New/edit issue', :feature, :js do
|
|||
# the original method, resulting in infinite recurison when called.
|
||||
# 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.
|
||||
original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options)
|
||||
allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args|
|
||||
original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
|
||||
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[:data][:per_page] = 2
|
||||
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
require 'spec_helper'
|
||||
require_relative '../../config/initializers/8_metrics'
|
||||
|
||||
describe 'instrument_classes', lib: true do
|
||||
let(:config) { double(:config) }
|
||||
|
||||
let(:unicorn_sampler) { double(:unicorn_sampler) }
|
||||
let(:influx_sampler) { double(:influx_sampler) }
|
||||
|
||||
before do
|
||||
allow(config).to receive(:instrument_method)
|
||||
allow(config).to receive(:instrument_methods)
|
||||
allow(config).to receive(:instrument_instance_method)
|
||||
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
|
||||
|
||||
it 'can autoload and instrument all files' do
|
||||
require_relative '../../config/initializers/8_metrics'
|
||||
expect { instrument_classes(config) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,30 +97,40 @@ describe Gitlab::HealthChecks::FsShardsCheck do
|
|||
}.with_indifferent_access
|
||||
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 { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) }
|
||||
it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) }
|
||||
it 'provides metrics' do
|
||||
expect(subject).to all(have_attributes(labels: { shard: :default }))
|
||||
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)) }
|
||||
it { is_expected.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_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
|
||||
|
||||
context 'storage points to directory that has both read and write rights' do
|
||||
before do
|
||||
FileUtils.chmod_R(0755, tmp_dir)
|
||||
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 { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) }
|
||||
it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) }
|
||||
it 'provides metrics' do
|
||||
expect(subject).to all(have_attributes(labels: { shard: :default }))
|
||||
|
||||
it { is_expected.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)) }
|
||||
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_accessible, value: 1))
|
||||
expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 1))
|
||||
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
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
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
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Metrics::Sampler do
|
||||
describe Gitlab::Metrics::InfluxSampler do
|
||||
let(:sampler) { described_class.new(5) }
|
||||
|
||||
after do
|
||||
|
@ -8,10 +8,10 @@ describe Gitlab::Metrics::Sampler do
|
|||
end
|
||||
|
||||
describe '#start' do
|
||||
it 'gathers a sample at a given interval' do
|
||||
expect(sampler).to receive(:sleep).with(a_kind_of(Numeric))
|
||||
expect(sampler).to receive(:sample)
|
||||
expect(sampler).to receive(:loop).and_yield
|
||||
it 'runs once and gathers a sample at a given interval' do
|
||||
expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice
|
||||
expect(sampler).to receive(:sample).once
|
||||
expect(sampler).to receive(:running).and_return(false, true, false)
|
||||
|
||||
sampler.start.join
|
||||
end
|
|
@ -0,0 +1,108 @@
|
|||
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
|
|||
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
|
||||
let(:user) { create(:user) }
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
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
|
|||
|
||||
describe 'GET /users' 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")
|
||||
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
|
||||
|
||||
|
@ -138,6 +169,7 @@ describe API::Users do
|
|||
describe "GET /users/:id" do
|
||||
it "returns a user by id" do
|
||||
get api("/users/#{user.id}", user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response['username']).to eq(user.username)
|
||||
end
|
||||
|
@ -148,9 +180,22 @@ describe API::Users do
|
|||
expect(json_response['is_admin']).to be_nil
|
||||
end
|
||||
|
||||
it "returns a 401 if unauthenticated" do
|
||||
get api("/users/9998")
|
||||
expect(response).to have_http_status(401)
|
||||
context 'for an anonymous user' do
|
||||
it "returns a user by id" do
|
||||
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
|
||||
|
||||
it "returns a 404 error if user id not found" do
|
||||
|
|
|
@ -359,18 +359,18 @@ describe QuickActions::InterpretService, services: true do
|
|||
let(:content) { "/assign @#{developer.username}" }
|
||||
|
||||
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)
|
||||
|
||||
expect(updates).to eq(assignee_ids: [developer.id])
|
||||
expect(updates[:assignee_ids]).to match_array([developer.id])
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(updates).to eq(assignee_id: developer.id)
|
||||
expect(updates).to eq(assignee_ids: [developer.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -383,7 +383,7 @@ describe QuickActions::InterpretService, services: true do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(updates[:assignee_ids]).to match_array([developer.id])
|
||||
|
@ -391,10 +391,10 @@ describe QuickActions::InterpretService, services: true do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(updates).to eq(assignee_id: developer.id)
|
||||
expect(updates).to eq(assignee_ids: [developer.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -422,11 +422,27 @@ describe QuickActions::InterpretService, services: true do
|
|||
end
|
||||
|
||||
context 'Merge Request' do
|
||||
it 'populates assignee_id: nil if content contains /unassign' do
|
||||
merge_request.update(assignee_id: developer.id)
|
||||
it 'populates assignee_ids: [] if content contains /unassign' do
|
||||
merge_request.update(assignee_ids: [developer.id])
|
||||
_, 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
|
||||
|
|
Loading…
Reference in New Issue